#!/usr/bin/env python3 """Emit architecture SVGs for the Verae Time x Zapier getting-started guide.""" from __future__ import annotations from pathlib import Path OUT = Path(__file__).resolve().parents[1] / "docs" / "diagrams" OUT.mkdir(parents=True, exist_ok=True) # Palette matches getting-started.pdf NAVY = "#0F2744" TEAL = "#1A6B6B" TEAL_DK = "#134848" SLATE = "#334155" MUTED = "#64748B" LINE = "#94A3B8" PAPER = "#FFFFFF" BG = "#F8FAFC" PUBLIC = "#E8F1F8" PRIVATE = "#F3EDE0" VERAE = "#E8F6EE" ZAPIER = "#FFF1E6" ACCENT = "#C2410C" OK = "#15803D" WARN = "#B45309" class Svg: def __init__(self, w: int, h: int, title: str): self.w = w self.h = h self.title = title self.parts: list[str] = [] def raw(self, s: str) -> None: self.parts.append(s) def rect( self, x, y, w, h, fill=PAPER, stroke=NAVY, sw=1.4, r=8, opacity=1, ) -> None: self.raw( f'' ) def text( self, x, y, s, size=12, fill=NAVY, anchor="start", weight="500", family="Helvetica, Arial, sans-serif", ) -> None: esc = ( s.replace("&", "&") .replace("<", "<") .replace(">", ">") ) self.raw( f'{esc}' ) def lines(self, x, y, lines, size=11, fill=SLATE, anchor="start", leading=14, weight="400"): for i, line in enumerate(lines): self.text(x, y + i * leading, line, size=size, fill=fill, anchor=anchor, weight=weight) def arrow(self, x1, y1, x2, y2, color=TEAL, sw=1.8, label="", label_off=-10) -> None: self.raw( f'' ) if label: mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_off self.text(mx, my, label, size=10, fill=TEAL_DK, anchor="middle", weight="600") def varrow(self, x, y1, y2, color=TEAL, sw=1.8, label="", lx=8) -> None: self.arrow(x, y1, x, y2, color=color, sw=sw) if label: self.text(x + lx, (y1 + y2) / 2 + 4, label, size=10, fill=TEAL_DK, weight="600") def harrow(self, x1, x2, y, color=TEAL, sw=1.8, label="", label_off=-11) -> None: self.arrow(x1, y, x2, y, color=color, sw=sw, label=label, label_off=label_off) def badge(self, x, y, w, h, text, fill=TEAL, color=PAPER) -> None: self.rect(x, y, w, h, fill=fill, stroke=fill, r=4, sw=0) self.text(x + w / 2, y + h / 2 + 4, text, size=10, fill=color, anchor="middle", weight="700") def save(self, name: str) -> Path: path = OUT / name body = "\n ".join(self.parts) svg = f""" {self.title} {body} """ path.write_text(svg, encoding="utf-8") print(f"wrote {path.relative_to(OUT.parent.parent)} ({path.stat().st_size} bytes)") return path def high_level(): s = Svg(1100, 680, "High-level architecture: Zapier to Verae via middleware") s.text(24, 28, "HIGH-LEVEL ARCHITECTURE", size=11, fill=TEAL, weight="700") s.text(24, 50, "Verae Time x Zapier", size=20, fill=NAVY, weight="700") s.text( 24, 70, "Zapier never calls api.veraetime.net and never speaks NATS. HTTPS only at the public edge.", size=12, fill=MUTED, ) # Zones s.rect(20, 90, 1060, 150, fill=ZAPIER, stroke="#F0D2B8", sw=1.2, r=12) s.text(36, 112, "PUBLIC · Zapier cloud", size=11, fill=ACCENT, weight="700") s.rect(20, 258, 700, 280, fill=PUBLIC, stroke="#C5D6E8", sw=1.2, r=12) s.text(36, 280, "YOUR INFRASTRUCTURE · public HTTPS edge + private workers", size=11, fill=TEAL, weight="700") s.rect(740, 258, 340, 280, fill=PRIVATE, stroke="#E0D2B4", sw=1.2, r=12) s.text(756, 280, "PRIVATE NETWORK", size=11, fill=WARN, weight="700") s.rect(20, 556, 700, 104, fill=VERAE, stroke="#B7DCC7", sw=1.2, r=12) s.text(36, 578, "VERAE TIMESTAMPING SERVICE", size=11, fill=OK, weight="700") s.rect(740, 556, 340, 104, fill=ZAPIER, stroke="#F0D2B8", sw=1.2, r=12) s.text(756, 578, "ZAPIER REST HOOKS", size=11, fill=ACCENT, weight="700") # User s.rect(40, 128, 150, 88, fill=PAPER, stroke=NAVY) s.text(115, 158, "User", size=14, fill=NAVY, anchor="middle", weight="700") s.lines(115, 178, ["Zap editor", "connection form"], size=11, fill=SLATE, anchor="middle") # Zapier app s.rect(250, 122, 360, 100, fill=PAPER, stroke=NAVY, sw=1.8) s.text(430, 148, "Zapier Platform CLI app", size=14, fill=NAVY, anchor="middle", weight="700") s.lines( 430, 168, ["scratch/veraetime (session TS) or verae-zapier (zmw_ JS)", "runs on Zapier cloud when a Zap step executes"], size=11, fill=SLATE, anchor="middle", leading=15, ) s.rect(660, 128, 180, 88, fill=PAPER, stroke=LINE) s.text(750, 158, "Zap trigger", size=13, fill=NAVY, anchor="middle", weight="700") s.lines(750, 178, ["Timestamp Completed", "hooks.zapier.com"], size=11, fill=SLATE, anchor="middle") s.harrow(190, 250, 172, label="configure / run") s.harrow(610, 660, 172, label="hook fires Zap") # Middleware s.rect(48, 300, 644, 120, fill=PAPER, stroke=NAVY, sw=1.8) s.text(370, 326, "verae-zapier-middleware /zapier/v1/*", size=14, fill=NAVY, anchor="middle", weight="700") s.lines( 370, 348, [ "Auth bridge · tenants · entitlements · rate limits · REST Hook store", "GET /health POST /auth/login GET /auth/me POST /timestamp[/wait|/batch]", "POST /verify GET /status/{jobId} POST/DELETE /webhooks/*", ], size=11, fill=SLATE, anchor="middle", leading=16, ) s.varrow(430, 222, 300, label="HTTPS Bearer zmw_ / zmt_") # NATS s.rect(760, 300, 300, 92, fill=PAPER, stroke=NAVY, sw=1.8) s.text(910, 328, "NATS JetStream", size=14, fill=NAVY, anchor="middle", weight="700") s.lines( 910, 348, ["ZAPIER_JOBS · EVENTS · WEBHOOKS", "never on the public internet"], size=11, fill=SLATE, anchor="middle", ) s.harrow(692, 760, 346, label="publish (private)") # Workers s.rect(760, 412, 300, 108, fill=PAPER, stroke=NAVY) s.text(910, 438, "Workers", size=14, fill=NAVY, anchor="middle", weight="700") s.lines( 910, 458, ["job-poller jobs.watch", "event-router jobs.events", "webhook-deliver webhooks.deliver"], size=11, fill=SLATE, anchor="middle", leading=15, ) s.varrow(910, 392, 412) # Verae s.rect(48, 596, 644, 50, fill=PAPER, stroke=NAVY, sw=1.8) s.text(370, 616, "api.veraetime.net", size=12, fill=NAVY, anchor="middle", weight="700") s.text( 370, 634, "POST /auth/login POST /api/timestamp 202 {jobId} GET /api/status/{jobId} POST /api/verify", size=11, fill=SLATE, anchor="middle", ) s.varrow(220, 420, 596, label="sync HTTPS") s.arrow(820, 520, 370, 596, label="poll / create") # Hooks s.rect(760, 596, 300, 50, fill=PAPER, stroke=NAVY) s.text(910, 626, "POST hooks.zapier.com timestamp.completed", size=11, fill=NAVY, anchor="middle", weight="600") s.varrow(910, 520, 596, color=ACCENT, label="HTTPS egress") return s.save("01-high-level-architecture.svg") def security(): s = Svg(1100, 520, "Security boundaries") s.text(24, 28, "SECURITY BOUNDARIES", size=11, fill=TEAL, weight="700") s.text(24, 50, "What is public, what stays private", size=20, fill=NAVY, weight="700") s.rect(20, 80, 1060, 200, fill=ZAPIER, stroke="#F0D2B8", r=12) s.text(40, 106, "PUBLIC INTERNET", size=13, fill=ACCENT, weight="700") s.rect(48, 124, 300, 130, fill=PAPER, stroke=NAVY) s.text(198, 158, "Zapier cloud", size=14, fill=NAVY, anchor="middle", weight="700") s.lines(198, 180, ["CLI app performs", "z.request only", "no NATS, no Verae JWT"], size=12, fill=SLATE, anchor="middle") s.rect(400, 124, 300, 130, fill=PAPER, stroke=NAVY, sw=1.8) s.text(550, 158, "Middleware HTTPS", size=14, fill=NAVY, anchor="middle", weight="700") s.lines(550, 180, ["/zapier/v1 only", "TLS required in prod", "rate limit + entitlements"], size=12, fill=SLATE, anchor="middle") s.rect(752, 124, 300, 130, fill=PAPER, stroke=NAVY) s.text(902, 158, "Zapier hook URLs", size=14, fill=NAVY, anchor="middle", weight="700") s.lines(902, 180, ["untrusted egress", "timeouts required", "SSRF allowlist = Phase 15"], size=12, fill=SLATE, anchor="middle") s.harrow(348, 400, 189, label="HTTPS in") s.harrow(700, 752, 189, label="HTTPS out") s.rect(20, 300, 1060, 196, fill=PRIVATE, stroke="#E0D2B4", r=12) s.text(40, 326, "PRIVATE · do not expose ports 4222 / store / TOKEN_SECRET", size=13, fill=WARN, weight="700") s.rect(48, 348, 320, 126, fill=PAPER, stroke=NAVY) s.text(208, 380, "NATS JetStream", size=14, fill=NAVY, anchor="middle", weight="700") s.lines(208, 402, ["tokenRef, not raw JWT", "private network + auth", "mTLS is Phase 15"], size=12, fill=SLATE, anchor="middle") s.rect(400, 348, 320, 126, fill=PAPER, stroke=NAVY) s.text(560, 380, "Workers + file store", size=14, fill=NAVY, anchor="middle", weight="700") s.lines(560, 402, ["STORE_PATH store.json", "single-node MVP", "Postgres/Redis = Phase 15"], size=12, fill=SLATE, anchor="middle") s.rect(752, 348, 300, 126, fill=PAPER, stroke=NAVY) s.text(902, 380, "api.veraetime.net", size=14, fill=NAVY, anchor="middle", weight="700") s.lines(902, 402, ["Verae JWT stays here", "server-side login only", "MOCK_VERAE for tests"], size=12, fill=SLATE, anchor="middle") s.harrow(368, 400, 411) s.harrow(720, 752, 411) return s.save("02-security-boundaries.svg") def auth(): s = Svg(1100, 560, "Two-hop authentication") s.text(24, 28, "AUTHENTICATION", size=11, fill=TEAL, weight="700") s.text(24, 50, "Two hops · Zapier never sees the Verae JWT", size=20, fill=NAVY, weight="700") # Hop 1 s.rect(20, 80, 1060, 220, fill=PUBLIC, stroke="#C5D6E8", r=12) s.text(40, 106, "HOP 1 · end user → middleware", size=13, fill=TEAL, weight="700") s.rect(48, 126, 220, 148, fill=PAPER, stroke=NAVY) s.text(158, 156, "User in Zapier", size=13, fill=NAVY, anchor="middle", weight="700") s.lines( 158, 178, ["username + password", "or zmw_ API key", "api_base_url", "default :3100"], size=12, fill=SLATE, anchor="middle", ) s.rect(320, 126, 280, 148, fill=PAPER, stroke=NAVY, sw=1.8) s.text(460, 156, "POST /zapier/v1/auth/login", size=13, fill=NAVY, anchor="middle", weight="700") s.lines( 460, 178, ["loginWithCredentials", "or loginWithApiKey", "returns accessToken", "stored as sessionKey (TS)"], size=12, fill=SLATE, anchor="middle", ) s.rect(650, 126, 200, 148, fill=PAPER, stroke=NAVY) s.text(750, 156, "Tokens issued", size=13, fill=NAVY, anchor="middle", weight="700") s.lines(750, 178, ["zmw_ tenant API key", "zmt_ HMAC session", "TOKEN_SECRET", "never commit"], size=12, fill=SLATE, anchor="middle") s.rect(880, 126, 176, 148, fill=PAPER, stroke=NAVY) s.text(968, 156, "Auth test", size=13, fill=NAVY, anchor="middle", weight="700") s.lines(968, 178, ["GET /auth/me", "tenant + plan", "usage", "401 → refresh"], size=12, fill=SLATE, anchor="middle") s.harrow(268, 320, 200) s.harrow(600, 650, 200) s.harrow(850, 880, 200) # Hop 2 s.rect(20, 320, 1060, 216, fill=VERAE, stroke="#B7DCC7", r=12) s.text(40, 346, "HOP 2 · middleware → Verae (server-side only)", size=13, fill=OK, weight="700") s.rect(48, 366, 300, 148, fill=PAPER, stroke=NAVY) s.text(198, 396, "veraeClient", size=13, fill=NAVY, anchor="middle", weight="700") s.lines( 198, 418, ["POST /auth/login on Verae", "or MOCK_VERAE=true", "JWT held in process", "redacted in DEBUG_VERAE"], size=12, fill=SLATE, anchor="middle", ) s.rect(400, 366, 300, 148, fill=PAPER, stroke=NAVY, sw=1.8) s.text(550, 396, "api.veraetime.net", size=13, fill=NAVY, anchor="middle", weight="700") s.lines( 550, 418, ["Authorization: Bearer ", "timestamp / status / verify", "source of truth", "not reachable from Zapier"], size=12, fill=SLATE, anchor="middle", ) s.rect(752, 366, 300, 148, fill=PAPER, stroke=ACCENT, sw=1.8) s.text(902, 396, "Do not leak", size=13, fill=ACCENT, anchor="middle", weight="700") s.lines( 902, 418, ["no Verae JWT in Zapier", "no JWT in NATS (use tokenRef)", "no secrets in git", "no secrets in debug logs"], size=12, fill=SLATE, anchor="middle", ) s.harrow(348, 400, 440) s.harrow(700, 752, 440) return s.save("03-auth-two-hop.svg") def flow_async(): s = Svg(1100, 620, "Create Timestamp async plus REST Hook") s.text(24, 28, "FLOW A", size=11, fill=TEAL, weight="700") s.text(24, 50, "Create Timestamp · async job + hook", size=20, fill=NAVY, weight="700") cols = [ (70, "Zapier app"), (290, "Middleware"), (510, "NATS"), (730, "Workers"), (950, "Verae / Hooks"), ] for x, name in cols: s.rect(x - 80, 78, 160, 36, fill=NAVY, stroke=NAVY, r=6) s.text(x, 101, name, size=12, fill=PAPER, anchor="middle", weight="700") s.raw(f'') s.harrow(70, 290, 148, label="1 POST /timestamp") s.harrow(290, 950, 190, label="2 entitlement + POST /api/timestamp") s.harrow(950, 290, 232, label="3 202 { jobId }", label_off=14) s.harrow(290, 70, 274, label="4 return jobId to Zap", label_off=14) s.harrow(290, 510, 322, label="5 publish jobs.watch") s.harrow(510, 730, 364, label="6 job-poller") s.harrow(730, 950, 406, label="7 GET /api/status/{jobId}") s.harrow(950, 730, 448, label="8 pending: Nak + delay", label_off=14) s.harrow(730, 510, 490, label="9 terminal → jobs.events", label_off=14) s.harrow(510, 730, 532, label="10 match webhooks") s.harrow(730, 950, 574, label="11 POST timestamp.completed") s.text( 24, 612, "Pair this action with the Timestamp Completed hook trigger. Zapier does not poll Verae.", size=11, fill=MUTED, ) return s.save("04-flow-async-timestamp.svg") def flow_wait(): s = Svg(1100, 480, "Create Timestamp and Wait") s.text(24, 28, "FLOW B", size=11, fill=TEAL, weight="700") s.text(24, 50, "Create Timestamp and Wait", size=20, fill=NAVY, weight="700") s.text(24, 72, "In-process path is live today. Wait-via-NATS is Phase 9.", size=12, fill=MUTED) s.rect(40, 100, 200, 80, fill=PAPER, stroke=NAVY) s.text(140, 132, "Zapier", size=13, fill=NAVY, anchor="middle", weight="700") s.lines(140, 152, ["POST /timestamp/wait"], size=11, fill=SLATE, anchor="middle") s.rect(320, 100, 260, 80, fill=PAPER, stroke=NAVY, sw=1.8) s.text(450, 132, "Middleware", size=13, fill=NAVY, anchor="middle", weight="700") s.lines(450, 152, ["create job, then wait"], size=11, fill=SLATE, anchor="middle") s.harrow(240, 320, 140, label="HTTPS") # fork s.rect(320, 230, 260, 100, fill=PUBLIC, stroke=TEAL) s.text(450, 260, "NATS_ENABLED=false", size=12, fill=TEAL, anchor="middle", weight="700") s.lines(450, 280, ["in-process poller (Phase 6)", "returns StatusResponse"], size=11, fill=SLATE, anchor="middle") s.rect(640, 230, 400, 100, fill=PRIVATE, stroke=WARN) s.text(840, 260, "NATS_ENABLED=true · Phase 9 (open)", size=12, fill=WARN, anchor="middle", weight="700") s.lines( 840, 280, ["subscribe to jobs.events", "hard timeout → { jobId, status: pending }"], size=11, fill=SLATE, anchor="middle", ) s.varrow(450, 180, 230) s.arrow(580, 180, 840, 230) s.rect(40, 380, 1020, 72, fill=VERAE, stroke="#B7DCC7") s.text(550, 410, "Return to Zapier", size=13, fill=NAVY, anchor="middle", weight="700") s.lines( 550, 430, ["completed / failed status object or pending + jobId on timeout (Zapier can Find Job Status next)"], size=11, fill=SLATE, anchor="middle", ) s.varrow(450, 330, 380) s.varrow(840, 330, 380) return s.save("05-flow-wait.svg") def nats(): s = Svg(1100, 560, "NATS subject topology") s.text(24, 28, "NATS + JETSTREAM", size=11, fill=TEAL, weight="700") s.text(24, 50, "Private subjects, streams, and workers", size=20, fill=NAVY, weight="700") streams = [ (40, "ZAPIER_JOBS", "verae.zapier.jobs.watch", "Work queue", "job-poller", "GET /api/status/{jobId}"), (310, "ZAPIER_EVENTS", "verae.zapier.jobs.events", "Limits (time)", "event-router", "enqueue webhooks / waiters"), (580, "ZAPIER_WEBHOOKS", "verae.zapier.webhooks.deliver", "Work queue", "webhook-deliver", "POST Zapier targetUrl"), (850, "ZAPIER_USAGE", "verae.zapier.usage", "Optional", "usage-writer", "billing export"), ] for x, stream, subj, ret, cons, call in streams: s.rect(x, 88, 250, 250, fill=PAPER, stroke=NAVY, r=10) s.badge(x + 14, 104, 222, 26, stream) s.text(x + 125, 156, subj, size=11, fill=TEAL_DK, anchor="middle", weight="600") s.lines( x + 125, 182, [ret, "", "consumer: " + cons, call], size=12, fill=SLATE, anchor="middle", leading=18, ) s.harrow(290, 310, 213) s.harrow(560, 580, 213) s.harrow(830, 850, 213, color=LINE) s.rect(40, 364, 1020, 168, fill=PRIVATE, stroke="#E0D2B4", r=10) s.text(60, 392, "Ack semantics", size=13, fill=NAVY, weight="700") s.lines( 60, 418, [ "Job still pending Nak with delay ~ intervalMs or republish attempt+1", "Job terminal publish jobs.events, then Ack the watch message", "Webhook HTTP 2xx Ack · 5xx / network Nak until max_deliver", "Poison message terminate after max_deliver; DEBUG_VERAE=webhooks DLQ log", "Prefer tokenRef over embedding a Verae JWT in any NATS payload", ], size=12, fill=SLATE, leading=22, ) return s.save("06-nats-topology.svg") def operations(): s = Svg(1100, 640, "Zapier operations mapped to middleware and Verae") s.text(24, 28, "OPERATIONS MAP", size=11, fill=TEAL, weight="700") s.text(24, 50, "Connector → /zapier/v1 → Verae wrap", size=20, fill=NAVY, weight="700") headers = [(40, "Zapier"), (280, "Type"), (400, "Middleware"), (700, "Verae")] s.rect(20, 78, 1060, 36, fill=NAVY, stroke=NAVY, r=0) for x, h in headers: s.text(x, 101, h, size=12, fill=PAPER, weight="700") rows = [ ("Create Timestamp", "create", "POST /timestamp", "POST /api/timestamp → 202 jobId"), ("Create Timestamp and Wait", "create", "POST /timestamp/wait", "create + poll / wait"), ("Create Batch Timestamps", "create", "POST /timestamp/batch", "POST /api/batch/timestamp"), ("Verify Certificate", "create", "POST /verify", "POST /api/verify"), ("Find Job Status", "search", "GET /status/{jobId}", "GET /api/status/{jobId}"), ("Find Job Verification", "search (TS)", "GET /status/{jobId}/verification", "GET /api/verify/{jobId}"), ("Timestamp Completed", "hook", "POST /webhooks/subscribe", "(no Verae hook — middleware stores targetUrl)"), ] y = 114 for i, (a, b, c, d) in enumerate(rows): fill = PAPER if i % 2 == 0 else "#EEF2F6" s.rect(20, y, 1060, 52, fill=fill, stroke=LINE, r=0, sw=0.6) s.text(40, y + 32, a, size=13, fill=NAVY, weight="600") s.text(280, y + 32, b, size=12, fill=SLATE) s.text(400, y + 32, c, size=12, fill=TEAL_DK, weight="600") s.text(700, y + 32, d, size=12, fill=SLATE) y += 52 s.text( 24, 500, "Creates return one object. Searches and hook perform return arrays. 404 on status search → [].", size=12, fill=MUTED, ) s.rect(20, 520, 340, 96, fill=ZAPIER, stroke="#F0D2B8") s.text(36, 546, "Zapier cloud", size=12, fill=ACCENT, weight="700") s.lines(36, 566, ["scratch/veraetime", "or verae-zapier"], size=12, fill=SLATE) s.rect(390, 520, 340, 96, fill=PUBLIC, stroke="#C5D6E8") s.text(406, 546, "Middleware :3100", size=12, fill=TEAL, weight="700") s.lines(406, 566, ["api_base_url / MIDDLEWARE_BASE_URL", "never api.veraetime.net from Zapier"], size=12, fill=SLATE) s.rect(760, 520, 320, 96, fill=VERAE, stroke="#B7DCC7") s.text(776, 546, "Verae API", size=12, fill=OK, weight="700") s.lines(776, 566, ["JWT server-side only", "OpenAPI in scratch/our-api/"], size=12, fill=SLATE) s.harrow(360, 390, 568) s.harrow(730, 760, 568) return s.save("07-operations-map.svg") def middleware(): s = Svg(1100, 580, "Middleware internals") s.text(24, 28, "MIDDLEWARE INTERNALS", size=11, fill=TEAL, weight="700") s.text(24, 50, "verae-zapier-api/verae-zapier-middleware", size=20, fill=NAVY, weight="700") s.rect(20, 80, 160, 70, fill=PAPER, stroke=NAVY) s.text(100, 110, "GET /health", size=12, fill=NAVY, anchor="middle", weight="700") s.lines(100, 128, ["liveness"], size=11, fill=SLATE, anchor="middle") s.rect(200, 80, 880, 70, fill=NAVY, stroke=NAVY) s.text(640, 110, "Express /zapier → /v1", size=16, fill=PAPER, anchor="middle", weight="700") s.lines(640, 132, ["authenticate · rateLimit on protected routes"], size=12, fill="#CBD5E1", anchor="middle") # public s.rect(20, 172, 340, 180, fill=PUBLIC, stroke="#C5D6E8") s.text(36, 198, "Public", size=13, fill=TEAL, weight="700") s.lines( 36, 222, [ "POST /v1/auth/login", "GET /v1/auth/me", "POST /v1/signup", "GET /v1/admin/* (X-Admin-Secret)", ], size=13, fill=SLATE, leading=26, ) s.rect(380, 172, 360, 180, fill=PAPER, stroke=NAVY, sw=1.8) s.text(396, 198, "Protected /v1", size=13, fill=NAVY, weight="700") s.lines( 396, 222, [ "/timestamp /timestamp/wait /batch", "/verify", "/status/:jobId[/verification]", "/webhooks/subscribe|unsubscribe", ], size=13, fill=SLATE, leading=26, ) s.rect(760, 172, 320, 180, fill=PRIVATE, stroke="#E0D2B4") s.text(776, 198, "Services + store", size=13, fill=WARN, weight="700") s.lines( 776, 222, [ "auth / entitlement / tenant", "timestamp / verify / webhook", "store.json tenants usage hooks", "veraeClient (+ mock)", ], size=13, fill=SLATE, leading=26, ) s.rect(20, 372, 520, 180, fill=PAPER, stroke=NAVY) s.text(36, 400, "Flags", size=13, fill=NAVY, weight="700") s.lines( 36, 426, [ "NATS_ENABLED=false in-process job poller (Phase 6 product path)", "NATS_ENABLED=true JetStream workers; no in-process poller", "MOCK_VERAE=true deterministic jobs, no live Verae", "DEBUG_VERAE=auth,nats,jobs,webhooks,http,billing", ], size=13, fill=SLATE, leading=26, ) s.rect(560, 372, 520, 180, fill=VERAE, stroke="#B7DCC7") s.text(576, 400, "Outbound", size=13, fill=OK, weight="700") s.lines( 576, 426, [ "HTTPS VERAE_API_BASE_URL (prod: api.veraetime.net)", "NATS NATS_URL nats://127.0.0.1:4222", "HTTPS Zapier targetUrl (webhook worker)", "Never bind NATS to a public interface", ], size=13, fill=SLATE, leading=26, ) return s.save("08-middleware-internals.svg") def phases(): s = Svg(1100, 520, "Implementation phase roadmap") s.text(24, 28, "ROADMAP", size=11, fill=TEAL, weight="700") s.text(24, 50, "TODO.md phases and gates · do not skip", size=20, fill=NAVY, weight="700") done = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, } items = [ (0, "Docs"), (1, "Debug"), (2, "HTTP"), (3, "Store"), (4, "Client"), (5, "Auth"), (6, "Sync API"), (7, "NATS"), (8, "Workers"), (9, "Wait"), (10, "Tenancy"), (11, "Zapier"), (12, "E2E"), (13, "Prod"), (14, "Push"), (15, "Harden"), ] # chain 0-6 x0, y0 = 40, 100 for i, (n, lab) in enumerate(items[:7]): x = x0 + i * 150 fill = TEAL if n in done else PAPER tc = PAPER if n in done else NAVY st = TEAL if n in done else NAVY s.rect(x, y0, 130, 56, fill=fill, stroke=st) s.text(x + 65, y0 + 24, f"{n} {lab}", size=12, fill=tc, anchor="middle", weight="700") s.text(x + 65, y0 + 42, "gate passed" if n in done else "open", size=10, fill="#99F6E4" if n in done else MUTED, anchor="middle") if i < 6: s.harrow(x + 130, x + 150, y0 + 28, color=TEAL) # rail from phase 6 down to the three successor rows spine_x = x0 + 6 * 150 + 65 rail_x = 24 s.raw( f'' ) s.raw( f'' ) s.raw( f'' ) branches = [ (40, 210, [(7, "NATS"), (8, "Workers"), (9, "Wait-via-NATS")]), (40, 310, [(10, "Tenancy")]), (40, 400, [(11, "Zapier app"), (12, "E2E local"), (13, "Production"), (14, "Private push"), (15, "Harden")]), ] for bx, by, seq in branches: s.harrow(rail_x, bx, by + 28, color=TEAL) for i, (n, lab) in enumerate(seq): x = bx + i * 200 fill = TEAL if n in done else ("#FEF3C7" if n == 9 else PAPER) tc = PAPER if n in done else NAVY st = TEAL if n in done else (WARN if n == 9 else NAVY) s.rect(x, by, 180, 56, fill=fill, stroke=st) s.text(x + 90, by + 24, f"{n} {lab}", size=12, fill=tc, anchor="middle", weight="700") note = "gate passed" if n in done else ("open blocker" if n == 9 else "next") s.text(x + 90, by + 42, note, size=10, fill="#99F6E4" if n in done else MUTED, anchor="middle") if i < len(seq) - 1: s.harrow(x + 180, x + 200, by + 28, color=TEAL if n in done else LINE) s.text(24, 500, "Phase 9 is the open blocker before multi-instance wait. Phase 12 needs 8 + 10 + 11.", size=12, fill=MUTED) return s.save("09-phase-roadmap.svg") def workspace(): s = Svg(1100, 560, "Research workspace and integration path") s.text(24, 28, "THIS WORKSPACE", size=11, fill=TEAL, weight="700") s.text(24, 50, "How Grok, CLIs, Mongo, and the stack fit together", size=20, fill=NAVY, weight="700") s.rect(20, 80, 340, 220, fill=PAPER, stroke=NAVY) s.text(36, 108, "Laptop · this repo", size=13, fill=NAVY, weight="700") s.lines( 36, 132, [ "getting-started.md / .pdf", "scratch/veraetime", "verae-zapier-api/", "docs/diagrams/", "/zapier-build skill", "scripts/restart-grok.sh", ], size=13, fill=SLATE, leading=24, ) s.rect(380, 80, 340, 220, fill=PUBLIC, stroke="#C5D6E8") s.text(396, 108, "Local runtime", size=13, fill=TEAL, weight="700") s.lines( 396, 132, [ "zapier-platform 19.1.0", "zapier-sdk 0.77.1", "middleware :3100", "optional NATS :4222", "build + validate (no login)", "register / push need ~/.zapierrc", ], size=13, fill=SLATE, leading=24, ) s.rect(740, 80, 340, 220, fill=PRIVATE, stroke="#E0D2B4") s.text(756, 108, "NS1 Mongo (tunneled)", size=13, fill=WARN, weight="700") s.lines( 756, 132, [ "ssh -L 27017:127.0.0.1:27017 ns1", "db zapier", "apps · templates · help", "platform_reference", "never 70.88.205.138:27017 public", "creds in ~/.mcp-env only", ], size=13, fill=SLATE, leading=24, ) s.harrow(360, 380, 190) s.harrow(720, 740, 190, label="tunnel") s.rect(20, 324, 520, 208, fill=ZAPIER, stroke="#F0D2B8") s.text(36, 352, "Publish path", size=13, fill=ACCENT, weight="700") s.lines( 36, 378, [ "1 validate locally against middleware", "2 zapier-platform login (browser)", "3 public HTTPS middleware (Phase 13)", "4 register + push private version (Phase 14)", "5 one human E2E Zap, then consider listing", ], size=13, fill=SLATE, leading=26, ) s.rect(560, 324, 520, 208, fill=PAPER, stroke=NAVY) s.text(576, 352, "Consume path (optional, not Verae publish)", size=13, fill=NAVY, weight="700") s.lines( 576, 378, [ "zapier-sdk login → call existing apps", "Hosted MCP mcp.zapier.com/api/v1/connect", "discover → enable → inspect → execute", "writes need explicit approval; 2 tasks each", "do not mix with zapier-platform", ], size=13, fill=SLATE, leading=26, ) return s.save("10-workspace-integration.svg") def billing(): s = Svg(1100, 620, "Zapier and Verae billing layers") s.text(24, 28, "BILLING", size=11, fill=TEAL, weight="700") s.text(24, 50, "Two meters · Zapier tasks + Verae timestamps", size=20, fill=NAVY, weight="700") s.text( 24, 72, "Publishing the Verae app is free. The customer’s Zapier plan is not Verae’s invoice.", size=12, fill=MUTED, ) rows = [ (88, ZAPIER, ACCENT, "A Zapier customer plan", "Tasks / month (or Enterprise annual pool)", "Free 100 · Pro from 750 · Team from 2k · Ent custom"), (178, PUBLIC, TEAL, "B Task multipliers", "1 = typical action (Create Timestamp)", "MCP execute = 2 AI Advanced = 3 Premium = 5"), (268, PAPER, NAVY, "C Overflow", "Pay-per-task on paid plans", "Annual 1.25x · monthly 2.5x · cap 3x then pause"), (358, VERAE, OK, "D Verae middleware plan", "Timestamps / verifications / batch / RPM", "free 50 · starter 500 · pro 5k · enterprise contract"), (448, PRIVATE, WARN, "E Partner / embed", "Directory publish = $0 to Verae", "White Label: Zapier bills the product co. (usage)"), (538, "#F1F5F9", SLATE, "F Add-ons (not tasks)", "Agents = activities Chatbots = seat/tier", "Do not mix into the Zapier task estimate"), ] for y, fill, stroke, title, line1, line2 in rows: s.rect(20, y, 1060, 80, fill=fill, stroke=stroke, r=10) s.text(40, y + 28, title, size=14, fill=NAVY, weight="700") s.text(40, y + 50, line1, size=12, fill=SLATE) s.text(40, y + 68, line2, size=12, fill=SLATE) return s.save("11-zapier-billing.svg") def peergos_storage(): s = Svg(1100, 680, "Timestamped files on Peergos with tiered IPFS and cold retrieve") s.text(24, 28, "CLIENT PATTERN", size=11, fill=TEAL, weight="700") s.text(24, 50, "Timestamp · metadata · Peergos · pin cache · cold retrieve", size=18, fill=NAVY, weight="700") s.text( 24, 70, "Keep the index hot. Do not keep every CID live on a gateway.", size=12, fill=MUTED, ) # write path s.rect(20, 88, 1060, 200, fill=PUBLIC, stroke="#C5D6E8", r=12) s.text(36, 112, "WRITE PATH (Zapier orchestrates; Verae timestamps the hash)", size=12, fill=TEAL, weight="700") boxes = [ (40, "1 Source", ["Drive / mail /", "Peergos outbox"]), (250, "2 Hash", ["SHA-256 or", "envelope JSON"]), (460, "3 Verae", ["Create Timestamp", "jobId + hook"]), (670, "4 Store", ["Peergos e2e file", "optional hot pin"]), (880, "5 Index", ["cid · jobId · class", "always on"]), ] for x, title, lines in boxes: s.rect(x, 128, 190, 132, fill=PAPER, stroke=NAVY) s.text(x + 95, 156, title, size=13, fill=NAVY, anchor="middle", weight="700") s.lines(x + 95, 182, lines, size=12, fill=SLATE, anchor="middle", leading=20) for x in (230, 440, 650, 860): s.harrow(x, x + 20, 194) # tiers s.rect(20, 308, 340, 200, fill=ZAPIER, stroke="#F0D2B8") s.text(36, 336, "HOT · cache pin", size=13, fill=ACCENT, weight="700") s.lines( 36, 362, ["Kubo / Pinata / Filebase", "Pinning Services API", "TTL = pinnedUntil", "drop when cold copy exists"], size=13, fill=SLATE, leading=24, ) s.rect(380, 308, 340, 200, fill=VERAE, stroke="#B7DCC7") s.text(396, 336, "WARM · Peergos", size=13, fill=OK, weight="700") s.lines( 396, 362, ["e2e encrypted IPFS blocks", "user capabilities / sharing", "host cannot read plaintext", "not a public CDN"], size=13, fill=SLATE, leading=24, ) s.rect(740, 308, 340, 200, fill=PRIVATE, stroke="#E0D2B4") s.text(756, 336, "COLD · retrieve on demand", size=13, fill=WARN, weight="700") s.lines( 756, 362, ["S3 Glacier IR / Flexible / Deep", "or Filecoin / B2 / Storj", "Iceberg = catalog, not bytes", "restore API → short re-pin"], size=13, fill=SLATE, leading=24, ) s.rect(20, 528, 1060, 128, fill=PAPER, stroke=NAVY, sw=1.8) s.text(36, 556, "READ PATH GET /ipfs/{cid} or gateway miss", size=13, fill=NAVY, weight="700") s.lines( 36, 582, [ "1 look up index by cid (always-on). 2 pin-hot hit → serve. 3 Peergos capability → fetch encrypted blocks.", "4 storageClass glacier-* → StartRestore / vendor retrieve → 202 + Retry-After → rehydrate → optional short pin → serve.", "Never walk every pin provider. Never put raw files or Verae JWTs in the Zap.", ], size=13, fill=SLATE, leading=22, ) return s.save("12-peergos-ipfs-tiered-storage.svg") def main(): high_level() security() auth() flow_async() flow_wait() nats() operations() middleware() phases() workspace() billing() peergos_storage() readme = OUT / "README.md" readme.write_text( """# Architecture diagrams SVG sources for [getting-started.md](../../getting-started.md) and `getting-started.pdf`. | File | What it shows | |------|----------------| | [01-high-level-architecture.svg](01-high-level-architecture.svg) | End-to-end stack: Zapier cloud, middleware, NATS, workers, Verae, REST Hooks | | [02-security-boundaries.svg](02-security-boundaries.svg) | Public HTTPS vs private NATS / store / Verae JWT | | [03-auth-two-hop.svg](03-auth-two-hop.svg) | User → middleware tokens; middleware → Verae JWT | | [04-flow-async-timestamp.svg](04-flow-async-timestamp.svg) | Create Timestamp + hook sequence | | [05-flow-wait.svg](05-flow-wait.svg) | Create and Wait (in-process vs Phase 9 NATS) | | [06-nats-topology.svg](06-nats-topology.svg) | Streams, subjects, consumers, ack rules | | [07-operations-map.svg](07-operations-map.svg) | Zapier nouns → `/zapier/v1` → Verae | | [08-middleware-internals.svg](08-middleware-internals.svg) | Express routes, store, flags | | [09-phase-roadmap.svg](09-phase-roadmap.svg) | Gates 0–15 | | [10-workspace-integration.svg](10-workspace-integration.svg) | Repo, CLIs, Mongo tunnel, publish vs consume | | [11-zapier-billing.svg](11-zapier-billing.svg) | Zapier task plans vs Verae timestamp quotas vs embed | | [12-peergos-ipfs-tiered-storage.svg](12-peergos-ipfs-tiered-storage.svg) | Timestamp + Peergos + pin cache + Glacier retrieve-on-demand | Regenerate: ```bash python3 scripts/generate-diagrams.py python3 scripts/generate-getting-started-pdf.py ``` """, encoding="utf-8", ) print(f"wrote {readme}") if __name__ == "__main__": main()