master-zapier-plan-draft/research/zapier/scripts/generate-diagrams.py
George Lambert b4150c8250 Milestone 0: import zappier billing, Verae middleware, and Zapier research
Compose-ready workspace: packages/zappier (rate card, portal, Stripe),
packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier
(CLI app), vendor/zapier-platform, and research/zapier vendor corpus.

Gate 0 structure checks pass. Product code and research are not yet wired.
2026-09-09 02:37:36 -04:00

1011 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{r}" '
f'fill="{fill}" fill-opacity="{opacity}" stroke="{stroke}" '
f'stroke-width="{sw}"/>'
)
def text(
self,
x,
y,
s,
size=12,
fill=NAVY,
anchor="start",
weight="500",
family="Helvetica, Arial, sans-serif",
) -> None:
esc = (
s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
self.raw(
f'<text x="{x}" y="{y}" font-family="{family}" font-size="{size}" '
f'font-weight="{weight}" fill="{fill}" text-anchor="{anchor}">{esc}</text>'
)
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'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{color}" '
f'stroke-width="{sw}" marker-end="url(#arrow)"/>'
)
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"""<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="{self.w}" height="{self.h}"
viewBox="0 0 {self.w} {self.h}" role="img" aria-label="{self.title}">
<title>{self.title}</title>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="{TEAL}"/>
</marker>
<marker id="arrow-navy" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="{NAVY}"/>
</marker>
<marker id="arrow-accent" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="{ACCENT}"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="{BG}"/>
{body}
</svg>
"""
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 <JWT>", "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'<line x1="{x}" y1="114" x2="{x}" y2="590" stroke="{LINE}" stroke-dasharray="3 5"/>')
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'<line x1="{spine_x}" y1="{y0 + 56}" x2="{spine_x}" y2="190" '
f'stroke="{TEAL}" stroke-width="1.8"/>'
)
s.raw(
f'<line x1="{rail_x}" y1="190" x2="{spine_x}" y2="190" '
f'stroke="{TEAL}" stroke-width="1.8"/>'
)
s.raw(
f'<line x1="{rail_x}" y1="190" x2="{rail_x}" y2="428" '
f'stroke="{TEAL}" stroke-width="1.8"/>'
)
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 customers Zapier plan is not Veraes 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 015 |
| [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()