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.
1523 lines
59 KiB
Python
1523 lines
59 KiB
Python
#!/usr/bin/env python3
|
||
"""Render getting-started.pdf from the architecture / integration guide.
|
||
|
||
Uses Platypus Paragraphs for bullets (never ListFlowable — it can emit the
|
||
literal word "bullet"). Tables wrap every cell in a Paragraph.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import subprocess
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
from reportlab.lib import colors
|
||
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
||
from reportlab.lib.pagesizes import letter
|
||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||
from reportlab.lib.units import inch
|
||
from reportlab.platypus import (
|
||
Image,
|
||
KeepTogether,
|
||
PageBreak,
|
||
Paragraph,
|
||
Preformatted,
|
||
SimpleDocTemplate,
|
||
Spacer,
|
||
Table,
|
||
TableStyle,
|
||
)
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
OUT = ROOT / "getting-started.pdf"
|
||
DIAG = ROOT / "docs" / "diagrams"
|
||
|
||
# viewBox sizes from scripts/generate-diagrams.py
|
||
FIGS = {
|
||
"01-high-level-architecture.svg": (1100, 680),
|
||
"02-security-boundaries.svg": (1100, 520),
|
||
"03-auth-two-hop.svg": (1100, 560),
|
||
"04-flow-async-timestamp.svg": (1100, 620),
|
||
"05-flow-wait.svg": (1100, 480),
|
||
"06-nats-topology.svg": (1100, 560),
|
||
"07-operations-map.svg": (1100, 640),
|
||
"08-middleware-internals.svg": (1100, 580),
|
||
"09-phase-roadmap.svg": (1100, 520),
|
||
"10-workspace-integration.svg": (1100, 560),
|
||
"11-zapier-billing.svg": (1100, 620),
|
||
"12-peergos-ipfs-tiered-storage.svg": (1100, 680),
|
||
}
|
||
|
||
NAVY = colors.HexColor("#0F2744")
|
||
TEAL = colors.HexColor("#1A6B6B")
|
||
SLATE = colors.HexColor("#334155")
|
||
RULE = colors.HexColor("#CBD5E1")
|
||
ROW = colors.HexColor("#F1F5F9")
|
||
HEAD_BG = colors.HexColor("#0F2744")
|
||
HEAD_FG = colors.white
|
||
CODE_BG = colors.HexColor("#F8FAFC")
|
||
ACCENT = colors.HexColor("#C2410C")
|
||
|
||
|
||
def styles():
|
||
base = getSampleStyleSheet()
|
||
s = {
|
||
"cover_kicker": ParagraphStyle(
|
||
"cover_kicker",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica",
|
||
fontSize=10,
|
||
textColor=TEAL,
|
||
tracking=1.2,
|
||
spaceAfter=10,
|
||
alignment=TA_CENTER,
|
||
),
|
||
"cover_title": ParagraphStyle(
|
||
"cover_title",
|
||
parent=base["Title"],
|
||
fontName="Helvetica-Bold",
|
||
fontSize=26,
|
||
leading=32,
|
||
textColor=NAVY,
|
||
alignment=TA_CENTER,
|
||
spaceAfter=12,
|
||
),
|
||
"cover_sub": ParagraphStyle(
|
||
"cover_sub",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica",
|
||
fontSize=12,
|
||
leading=17,
|
||
textColor=SLATE,
|
||
alignment=TA_CENTER,
|
||
spaceAfter=8,
|
||
),
|
||
"h1": ParagraphStyle(
|
||
"h1",
|
||
parent=base["Heading1"],
|
||
fontName="Helvetica-Bold",
|
||
fontSize=15,
|
||
leading=19,
|
||
textColor=NAVY,
|
||
spaceBefore=16,
|
||
spaceAfter=8,
|
||
borderPadding=0,
|
||
),
|
||
"h2": ParagraphStyle(
|
||
"h2",
|
||
parent=base["Heading2"],
|
||
fontName="Helvetica-Bold",
|
||
fontSize=12,
|
||
leading=16,
|
||
textColor=TEAL,
|
||
spaceBefore=12,
|
||
spaceAfter=6,
|
||
),
|
||
"h3": ParagraphStyle(
|
||
"h3",
|
||
parent=base["Heading3"],
|
||
fontName="Helvetica-Bold",
|
||
fontSize=10.5,
|
||
leading=14,
|
||
textColor=NAVY,
|
||
spaceBefore=9,
|
||
spaceAfter=4,
|
||
),
|
||
"body": ParagraphStyle(
|
||
"body",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica",
|
||
fontSize=9.5,
|
||
leading=13.2,
|
||
textColor=SLATE,
|
||
alignment=TA_LEFT,
|
||
spaceAfter=7,
|
||
),
|
||
"bullet": ParagraphStyle(
|
||
"bullet",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica",
|
||
fontSize=9.5,
|
||
leading=13,
|
||
textColor=SLATE,
|
||
leftIndent=14,
|
||
firstLineIndent=-10,
|
||
spaceAfter=3,
|
||
),
|
||
"toc": ParagraphStyle(
|
||
"toc",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica",
|
||
fontSize=10,
|
||
leading=16,
|
||
textColor=NAVY,
|
||
leftIndent=8,
|
||
spaceAfter=2,
|
||
),
|
||
"cell": ParagraphStyle(
|
||
"cell",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica",
|
||
fontSize=8,
|
||
leading=10.5,
|
||
textColor=SLATE,
|
||
),
|
||
"cell_h": ParagraphStyle(
|
||
"cell_h",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica-Bold",
|
||
fontSize=8,
|
||
leading=10.5,
|
||
textColor=HEAD_FG,
|
||
),
|
||
"code": ParagraphStyle(
|
||
"code",
|
||
parent=base["Code"],
|
||
fontName="Courier",
|
||
fontSize=7.6,
|
||
leading=10.2,
|
||
textColor=NAVY,
|
||
backColor=CODE_BG,
|
||
leftIndent=6,
|
||
rightIndent=6,
|
||
spaceBefore=4,
|
||
spaceAfter=8,
|
||
),
|
||
"caption": ParagraphStyle(
|
||
"caption",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica-Oblique",
|
||
fontSize=8,
|
||
leading=11,
|
||
textColor=colors.HexColor("#64748B"),
|
||
spaceAfter=8,
|
||
),
|
||
"footer": ParagraphStyle(
|
||
"footer",
|
||
parent=base["Normal"],
|
||
fontName="Helvetica",
|
||
fontSize=8,
|
||
textColor=colors.HexColor("#64748B"),
|
||
),
|
||
}
|
||
return s
|
||
|
||
|
||
S = styles()
|
||
USABLE = letter[0] - 1.4 * inch
|
||
|
||
|
||
def P(text: str, style="body"):
|
||
return Paragraph(text, S[style])
|
||
|
||
|
||
def B(text: str):
|
||
return Paragraph(f"• {text}", S["bullet"])
|
||
|
||
|
||
def H1(text: str):
|
||
return Paragraph(text, S["h1"])
|
||
|
||
|
||
def H2(text: str):
|
||
return Paragraph(text, S["h2"])
|
||
|
||
|
||
def H3(text: str):
|
||
return Paragraph(text, S["h3"])
|
||
|
||
|
||
def CODE(text: str):
|
||
return Preformatted(text.rstrip() + "\n", S["code"])
|
||
|
||
|
||
_PNG_CACHE: Path | None = None
|
||
|
||
|
||
def _png_dir() -> Path:
|
||
global _PNG_CACHE
|
||
if _PNG_CACHE is None:
|
||
_PNG_CACHE = Path(tempfile.mkdtemp(prefix="gs-diagrams-"))
|
||
return _PNG_CACHE
|
||
|
||
|
||
def fig(name: str, caption: str):
|
||
"""Rasterize an SVG and return a KeepTogether of image + caption."""
|
||
svg = DIAG / name
|
||
if not svg.exists():
|
||
raise FileNotFoundError(svg)
|
||
vw, vh = FIGS[name]
|
||
png = _png_dir() / name.replace(".svg", ".png")
|
||
if not png.exists():
|
||
subprocess.run(
|
||
["rsvg-convert", "-w", "1800", "-f", "png", "-o", str(png), str(svg)],
|
||
check=True,
|
||
)
|
||
iw = USABLE
|
||
ih = USABLE * vh / vw
|
||
return KeepTogether(
|
||
[
|
||
Spacer(1, 6),
|
||
Image(str(png), width=iw, height=ih),
|
||
P(caption, "caption"),
|
||
]
|
||
)
|
||
|
||
|
||
def tbl(headers: list[str], rows: list[list[str]], widths: list[float] | None = None):
|
||
if widths is None:
|
||
n = len(headers)
|
||
widths = [USABLE / n] * n
|
||
data = [[Paragraph(h, S["cell_h"]) for h in headers]]
|
||
for row in rows:
|
||
data.append([Paragraph(c, S["cell"]) for c in row])
|
||
t = Table(data, colWidths=widths, repeatRows=1)
|
||
style_cmds = [
|
||
("BACKGROUND", (0, 0), (-1, 0), HEAD_BG),
|
||
("TEXTCOLOR", (0, 0), (-1, 0), HEAD_FG),
|
||
("BACKGROUND", (0, 1), (-1, -1), colors.white),
|
||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||
("LEFTPADDING", (0, 0), (-1, -1), 5),
|
||
("RIGHTPADDING", (0, 0), (-1, -1), 5),
|
||
("TOPPADDING", (0, 0), (-1, -1), 4),
|
||
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||
("GRID", (0, 0), (-1, -1), 0.3, RULE),
|
||
("LINEBELOW", (0, 0), (-1, 0), 1, TEAL),
|
||
]
|
||
for i in range(1, len(data)):
|
||
if i % 2 == 0:
|
||
style_cmds.append(("BACKGROUND", (0, i), (-1, i), ROW))
|
||
t.setStyle(TableStyle(style_cmds))
|
||
return t
|
||
|
||
|
||
def header_footer(canvas, doc):
|
||
canvas.saveState()
|
||
w, h = letter
|
||
canvas.setFillColor(NAVY)
|
||
canvas.rect(0, h - 28, w, 28, fill=1, stroke=0)
|
||
canvas.setFillColor(colors.white)
|
||
canvas.setFont("Helvetica", 8)
|
||
canvas.drawString(0.7 * inch, h - 18, "Verae Time × Zapier")
|
||
canvas.drawRightString(w - 0.7 * inch, h - 18, "Architecture · Functionality · Integration")
|
||
canvas.setFillColor(TEAL)
|
||
canvas.rect(0, 0, w, 22, fill=1, stroke=0)
|
||
canvas.setFillColor(colors.white)
|
||
canvas.setFont("Helvetica", 8)
|
||
canvas.drawString(0.7 * inch, 8, "Internal · 17 August 2026 · /Users/marchon/research/zapier")
|
||
canvas.drawRightString(w - 0.7 * inch, 8, f"{doc.page}")
|
||
canvas.restoreState()
|
||
|
||
|
||
def cover_footer(canvas, doc):
|
||
canvas.saveState()
|
||
w, h = letter
|
||
canvas.setFillColor(NAVY)
|
||
canvas.rect(0, 0, w, 56, fill=1, stroke=0)
|
||
canvas.setFillColor(TEAL)
|
||
canvas.rect(0, 56, w, 4, fill=1, stroke=0)
|
||
canvas.setFillColor(colors.white)
|
||
canvas.setFont("Helvetica", 9)
|
||
canvas.drawCentredString(w / 2, 28, "Research workspace · not a public listing guide")
|
||
canvas.setFont("Helvetica", 8)
|
||
canvas.drawCentredString(w / 2, 14, "17 August 2026")
|
||
canvas.restoreState()
|
||
|
||
|
||
def story():
|
||
out = []
|
||
|
||
# --- cover ---
|
||
out.append(Spacer(1, 1.6 * inch))
|
||
out.append(P("VERAE TIME · ZAPIER PLATFORM", "cover_kicker"))
|
||
out.append(P("System architecture,<br/>functionality, and integration", "cover_title"))
|
||
out.append(Spacer(1, 8))
|
||
out.append(
|
||
P(
|
||
"How Zapier automations reach the Verae Timestamping Service through "
|
||
"a hosted HTTP edge and private NATS workers — and how to build, "
|
||
"validate, and publish the connector from this workspace.",
|
||
"cover_sub",
|
||
)
|
||
)
|
||
out.append(Spacer(1, 18))
|
||
out.append(
|
||
tbl(
|
||
["Document", "Audience", "Status"],
|
||
[
|
||
[
|
||
"getting-started.md / .pdf",
|
||
"Humans and Grok in this repo",
|
||
"Working architecture + next steps",
|
||
],
|
||
[
|
||
"Start playbook",
|
||
"Any Zapier coding task",
|
||
"PLATFORM-REFERENCE.md · Mongo guide/build-new-connector",
|
||
],
|
||
],
|
||
[2.3 * inch, 2.1 * inch, 2.1 * inch],
|
||
)
|
||
)
|
||
out.append(PageBreak())
|
||
|
||
# --- toc ---
|
||
out.append(H1("Contents"))
|
||
for line in [
|
||
"1. How to start this workspace",
|
||
"2. System architecture",
|
||
"3. Functionality",
|
||
"4. Integration guide",
|
||
"5. Workspace map",
|
||
"6. What is already here vs what is not",
|
||
"7. Next steps",
|
||
"8. Hard rules",
|
||
"9. Zapier billing models",
|
||
"10. Client pattern — timestamp, Peergos, tiered IPFS",
|
||
"Appendix — SVG diagrams in docs/diagrams/",
|
||
]:
|
||
out.append(P(line, "toc"))
|
||
out.append(Spacer(1, 10))
|
||
out.append(
|
||
P(
|
||
"This PDF is generated from the same material as "
|
||
"<font face='Courier'>getting-started.md</font>. Prefer the Markdown "
|
||
"file when editing; regenerate with "
|
||
"<font face='Courier'>scripts/generate-getting-started-pdf.py</font>.",
|
||
"caption",
|
||
)
|
||
)
|
||
|
||
# --- 1 ---
|
||
out.append(H1("1. How to start this workspace"))
|
||
out.append(CODE("cd /Users/marchon/research/zapier\n./scripts/restart-grok.sh # Mongo tunnel + grok --resume"))
|
||
out.append(P("Already in Grok, run <font face='Courier'>/zapier-build</font>. Mandatory first query:"))
|
||
out.append(CODE('db.platform_reference.findOne({ kind: "guide", key: "build-new-connector" })'))
|
||
out.append(
|
||
P(
|
||
"Keep the Mongo tunnel up (<font face='Courier'>./scripts/ensure-mongo-tunnel.sh</font>). "
|
||
"Check readiness with <font face='Courier'>./scripts/zapier-status.sh</font>. "
|
||
"Load CLI PATH with <font face='Courier'>source scripts/dev-env.sh</font>. "
|
||
"Leave the TUI with <font face='Courier'>/quit</font> (not <font face='Courier'>/new</font>). "
|
||
"See RESTART.md."
|
||
)
|
||
)
|
||
|
||
# --- 2 ---
|
||
out.append(H1("2. System architecture"))
|
||
out.append(H2("2.1 Purpose"))
|
||
out.append(
|
||
P(
|
||
"Connect Zapier automations to Verae blockchain timestamping without "
|
||
"exposing raw Verae JWTs to end users, without requiring Zapier to poll "
|
||
"async jobs on api.veraetime.net, without coupling billing to the core "
|
||
"timestamping API, and without running a multi-instance edge on in-memory queues only."
|
||
)
|
||
)
|
||
out.append(H2("2.2 Bottom line"))
|
||
out.append(
|
||
fig(
|
||
"01-high-level-architecture.svg",
|
||
"Figure 1. High-level architecture. Zapier talks only to middleware HTTPS. "
|
||
"NATS and the Verae JWT stay on your side of the edge.",
|
||
)
|
||
)
|
||
out.append(
|
||
CODE(
|
||
"Users → Zapier UI\n"
|
||
"Zapier cloud runs the Platform CLI app (verae-zapier or scratch/veraetime)\n"
|
||
" → HTTPS only → verae-zapier-middleware /zapier/v1/*\n"
|
||
" → (sync) HTTPS → https://api.veraetime.net\n"
|
||
" → (async) NATS JetStream → workers\n"
|
||
" → HTTPS → api.veraetime.net (status poll)\n"
|
||
" → HTTPS → hooks.zapier.com (REST Hook delivery)"
|
||
)
|
||
)
|
||
out.append(B("Zapier never connects to NATS."))
|
||
out.append(B("Zapier never calls api.veraetime.net directly."))
|
||
out.append(B("Middleware HTTP owns auth, tenancy, entitlements, metering, and the public API surface."))
|
||
out.append(
|
||
B(
|
||
"NATS owns durable job watching, completion events, and reliable webhook delivery "
|
||
"when NATS_ENABLED=true."
|
||
)
|
||
)
|
||
out.append(
|
||
B(
|
||
"With NATS_ENABLED=false, an in-process poller still implements the same HTTP "
|
||
"product path (Phase 6)."
|
||
)
|
||
)
|
||
|
||
out.append(H2("2.3 Components"))
|
||
out.append(
|
||
tbl(
|
||
["Component", "Runs where", "Role"],
|
||
[
|
||
[
|
||
"Zapier Platform CLI app",
|
||
"Zapier cloud",
|
||
"Auth fields; map operations to /zapier/v1/*; attach Bearer; translate 402/403",
|
||
],
|
||
[
|
||
"verae-zapier-middleware",
|
||
"Your infrastructure, public HTTPS",
|
||
"Tenants, API keys, Verae login bridge, entitlements, REST Hooks, NATS publish",
|
||
],
|
||
[
|
||
"NATS + JetStream",
|
||
"Private network",
|
||
"Work queues for job watch and webhook delivery; event stream for terminal states",
|
||
],
|
||
[
|
||
"Workers",
|
||
"Same deploy or separate",
|
||
"Job poller, event router, webhook deliverer",
|
||
],
|
||
[
|
||
"api.veraetime.net",
|
||
"Verae production",
|
||
"Source of truth: login, timestamp jobs, status, verification",
|
||
],
|
||
],
|
||
[1.7 * inch, 1.7 * inch, 3.1 * inch],
|
||
)
|
||
)
|
||
out.append(Spacer(1, 8))
|
||
|
||
out.append(H3("Two connector implementations"))
|
||
out.append(
|
||
tbl(
|
||
["Path", "Language", "Auth", "Status"],
|
||
[
|
||
[
|
||
"verae-zapier-api/<br/>verae-zapier/",
|
||
"JavaScript",
|
||
"Custom API key (zmw_…)",
|
||
"Phase 11 package.<br/>MIDDLEWARE_<br/>BASE_URL env",
|
||
],
|
||
[
|
||
"scratch/veraetime/",
|
||
"TypeScript (CLI 19.1.0)",
|
||
"Session: user/pass or API key → accessToken",
|
||
"Local golden connector; build + validate",
|
||
],
|
||
],
|
||
[1.85 * inch, 1.4 * inch, 1.7 * inch, 1.55 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Both talk only to middleware. The TypeScript app is what this workspace "
|
||
"validates day-to-day. The JavaScript app is the vendored product package. "
|
||
"Neither calls api.veraetime.net, speaks NATS, nor enforces plan quotas."
|
||
)
|
||
)
|
||
|
||
out.append(H3("Middleware HTTP edge"))
|
||
out.append(
|
||
fig(
|
||
"08-middleware-internals.svg",
|
||
"Figure 2. Middleware internals: public auth/signup, protected /zapier/v1 "
|
||
"routes, file store, and outbound Verae / NATS / hook calls.",
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Path: verae-zapier-api/verae-zapier-middleware/. Express: GET /health, "
|
||
"mount /zapier. Public: POST /zapier/v1/auth/login, GET /zapier/v1/auth/me, "
|
||
"tenant signup. Protected (Bearer or x-api-key + rate limit): timestamp, "
|
||
"verify, status, webhooks. Admin: /zapier/v1/admin/* behind X-Admin-Secret. "
|
||
"Store: file JSON MVP (STORE_PATH, default ./data/store.json) — single-node only."
|
||
)
|
||
)
|
||
|
||
out.append(H3("NATS + workers"))
|
||
out.append(
|
||
tbl(
|
||
["Worker", "Consumes", "Calls"],
|
||
[
|
||
["Job poller", "verae.zapier.jobs.watch", "GET /api/status/{jobId} on Verae"],
|
||
["Event router", "verae.zapier.jobs.events", "Enqueues webhook deliveries"],
|
||
["Webhook deliver", "verae.zapier.webhooks.deliver", "POST Zapier targetUrl"],
|
||
],
|
||
[1.7 * inch, 2.4 * inch, 2.4 * inch],
|
||
)
|
||
)
|
||
out.append(P("Streams: ZAPIER_JOBS, ZAPIER_EVENTS, ZAPIER_WEBHOOKS (optional ZAPIER_USAGE).", "caption"))
|
||
|
||
out.append(H3("Verae Timestamping Service"))
|
||
out.append(B("Swagger UI: https://api.veraetime.net/docs/swagger/index.html"))
|
||
out.append(B("OpenAPI: https://api.veraetime.net/docs/swagger/openapi.yaml"))
|
||
out.append(B("Local copy: scratch/our-api/openapi.yaml"))
|
||
out.append(B("Auth: POST /auth/login → JWT in token; other API routes Authorization: Bearer"))
|
||
out.append(B("Async create: POST /api/timestamp → 202 { jobId }"))
|
||
|
||
out.append(H2("2.4 Security boundaries"))
|
||
out.append(
|
||
fig(
|
||
"02-security-boundaries.svg",
|
||
"Figure 3. Public internet is HTTPS only. NATS, the tenant store, and the "
|
||
"Verae JWT never leave the private network.",
|
||
)
|
||
)
|
||
out.append(
|
||
CODE(
|
||
"Public Internet\n"
|
||
" - Zapier cloud -> Middleware HTTPS only\n"
|
||
" - Middleware -> Zapier REST Hook HTTPS only\n"
|
||
"\n"
|
||
"Private\n"
|
||
" - Middleware <-> NATS (never expose NATS ports)\n"
|
||
" - Middleware / workers -> api.veraetime.net HTTPS"
|
||
)
|
||
)
|
||
out.append(B("Never put raw Verae JWTs in NATS when a tokenRef will do."))
|
||
out.append(B("Never expose NATS (4222) to the public internet."))
|
||
out.append(B("Debug logs must redact Bearer, zmw_, zmt_, passwords, and hook query secrets."))
|
||
out.append(B("Treat targetUrl as untrusted egress (timeouts; SSRF allowlist is Phase 15)."))
|
||
out.append(B("Do not commit ~/.zapierrc, .env, store.json, or ~/.mcp-env."))
|
||
|
||
out.append(H2("2.5 Scaling model"))
|
||
out.append(
|
||
P(
|
||
"HTTP edge replicas are stateless except for a shared store. File JSON is "
|
||
"single-node; multi-node needs Postgres/Redis (Phase 15). NATS consumers "
|
||
"use queue groups — more workers increase poll/deliver throughput and must "
|
||
"not double-complete the same job. Rollback: NATS_ENABLED=false still serves "
|
||
"the full HTTP API with the in-process poller."
|
||
)
|
||
)
|
||
|
||
# --- 3 ---
|
||
out.append(H1("3. Functionality"))
|
||
out.append(H2("3.1 Authentication (two hops)"))
|
||
out.append(
|
||
fig(
|
||
"03-auth-two-hop.svg",
|
||
"Figure 4. Two-hop auth. Zapier holds zmw_ or zmt_ only. The Verae JWT "
|
||
"is created and stored server-side by veraeClient.",
|
||
)
|
||
)
|
||
out.append(H3("End user → middleware"))
|
||
out.append(
|
||
tbl(
|
||
["Mode", "User enters", "What happens"],
|
||
[
|
||
[
|
||
"Session (TypeScript app)",
|
||
"Username + password, or middleware API key",
|
||
"POST /zapier/v1/auth/login → { accessToken } stored as sessionKey",
|
||
],
|
||
[
|
||
"Custom key (JS app)",
|
||
"Tenant API key zmw_…",
|
||
"Authorization: Bearer on every request; test is GET /zapier/v1/auth/me",
|
||
],
|
||
],
|
||
[1.9 * inch, 2.2 * inch, 2.4 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Connection test: GET /zapier/v1/auth/me → tenant, plan, usage. "
|
||
"401 on later calls: the TypeScript app throws z.errors.RefreshAuthError "
|
||
"so Zapier re-runs session perform."
|
||
)
|
||
)
|
||
out.append(H3("Middleware → Verae"))
|
||
out.append(
|
||
P(
|
||
"Middleware logs into Verae with the tenant’s Verae credentials (or the mock "
|
||
"client when MOCK_VERAE=true) and holds the JWT server-side. Zapier never "
|
||
"sees that JWT. Middleware issues zmw_ (long-lived tenant API key) and zmt_ "
|
||
"(HMAC session token signed with TOKEN_SECRET)."
|
||
)
|
||
)
|
||
|
||
out.append(H2("3.2 Operations map"))
|
||
out.append(
|
||
fig(
|
||
"07-operations-map.svg",
|
||
"Figure 5. Every Zapier noun maps to a /zapier/v1 route. Production "
|
||
"api_base_url is the deployed middleware, not api.veraetime.net.",
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"All Zapier routes are under /zapier/v1. Connector default base is "
|
||
"http://127.0.0.1:3100. Production base is the deployed middleware, "
|
||
"not api.veraetime.net."
|
||
)
|
||
)
|
||
out.append(
|
||
tbl(
|
||
["Zapier", "Type", "Middleware", "Verae wrap", "Notes"],
|
||
[
|
||
[
|
||
"Create Timestamp",
|
||
"create",
|
||
"POST /timestamp",
|
||
"POST /api/timestamp",
|
||
"202 { jobId }. Pair with hook trigger.",
|
||
],
|
||
[
|
||
"Create Timestamp and Wait",
|
||
"create",
|
||
"POST /timestamp/wait",
|
||
"create + poll/wait",
|
||
"Terminal status, or pending + jobId on timeout.",
|
||
],
|
||
[
|
||
"Create Batch Timestamps",
|
||
"create",
|
||
"POST /timestamp/batch",
|
||
"POST /api/batch/timestamp",
|
||
"{ items: [{ data, hashAlg? }] }",
|
||
],
|
||
[
|
||
"Verify Certificate",
|
||
"create",
|
||
"POST /verify",
|
||
"POST /api/verify",
|
||
"{ certificate } → valid / timestamp / blockIndex",
|
||
],
|
||
[
|
||
"Find Job Status",
|
||
"search",
|
||
"GET /status/{jobId}",
|
||
"GET /api/status/{jobId}",
|
||
"Empty array if 404.",
|
||
],
|
||
[
|
||
"Find Job Verification",
|
||
"search (TS)",
|
||
"GET /status/{jobId}/verification",
|
||
"GET /api/verify/{jobId}",
|
||
"Search.",
|
||
],
|
||
[
|
||
"Timestamp Completed",
|
||
"hook trigger",
|
||
"POST /webhooks/subscribe · DELETE unsubscribe",
|
||
"(no Verae hook)",
|
||
"Event timestamp.completed; Zapier supplies targetUrl.",
|
||
],
|
||
],
|
||
[1.35 * inch, 0.85 * inch, 1.55 * inch, 1.4 * inch, 1.35 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Create input: required data (text), optional hashAlg (default SHA256). "
|
||
"Not in v1: Verae admin HTML, metrics, queue UI, user CRUD, batch verify/status, "
|
||
"get-block-by-hash. Add later from the OpenAPI if a Zap needs them — do not guess.",
|
||
"caption",
|
||
)
|
||
)
|
||
|
||
out.append(H2("3.3 Request flows"))
|
||
out.append(H3("A. Create Timestamp (async + hook)"))
|
||
out.append(
|
||
fig(
|
||
"04-flow-async-timestamp.svg",
|
||
"Figure 6. Async create: 202 jobId immediately, then workers poll Verae "
|
||
"and POST timestamp.completed to the Zapier REST Hook.",
|
||
)
|
||
)
|
||
out.append(
|
||
CODE(
|
||
"Zapier → POST /zapier/v1/timestamp\n"
|
||
"Middleware: authenticate, checkEntitlement, POST /api/timestamp\n"
|
||
"Middleware: publish jobs.watch → return 202 { jobId }\n"
|
||
"Worker: poll GET /api/status/{jobId} until terminal → publish jobs.events\n"
|
||
"Event router: match webhooks → publish webhooks.deliver\n"
|
||
"Webhook worker: POST hooks.zapier.com/… (timestamp.completed)\n"
|
||
"Zapier trigger: Timestamp Completed fires the rest of the Zap"
|
||
)
|
||
)
|
||
out.append(H3("B. Create Timestamp and Wait"))
|
||
out.append(
|
||
fig(
|
||
"05-flow-wait.svg",
|
||
"Figure 7. Wait path. NATS_ENABLED=false is live (in-process poller). "
|
||
"Wait-via-NATS is Phase 9.",
|
||
)
|
||
)
|
||
out.append(
|
||
CODE(
|
||
"Zapier → POST /zapier/v1/timestamp/wait\n"
|
||
"Middleware: create + wait (in-process if NATS off; NATS events when Phase 9 lands)\n"
|
||
"→ StatusResponse (completed/failed) or { jobId, status: \"pending\" } on timeout"
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Today, wait works on the in-process path (NATS_ENABLED=false). "
|
||
"Wait-via-NATS is Phase 9 (open)."
|
||
)
|
||
)
|
||
out.append(H3("C. Auth connection test"))
|
||
out.append(
|
||
CODE(
|
||
"Zapier → GET /zapier/v1/auth/me Authorization: Bearer zmw_… or zmt_…\n"
|
||
"Middleware: resolve key/session → tenant → optional validate Verae token\n"
|
||
"→ { tenantId, plan, usage, ... }"
|
||
)
|
||
)
|
||
|
||
out.append(H2("3.4 NATS subjects (private)"))
|
||
out.append(
|
||
fig(
|
||
"06-nats-topology.svg",
|
||
"Figure 8. JetStream streams ZAPIER_JOBS, ZAPIER_EVENTS, ZAPIER_WEBHOOKS "
|
||
"(optional ZAPIER_USAGE) and ack rules.",
|
||
)
|
||
)
|
||
out.append(
|
||
tbl(
|
||
["Subject", "Publisher", "Consumer", "Payload gist"],
|
||
[
|
||
[
|
||
"verae.zapier.jobs.watch",
|
||
"HTTP edge after create",
|
||
"job-poller (queue)",
|
||
"tenantId, jobId, tokenRef, attempts, traceId",
|
||
],
|
||
[
|
||
"verae.zapier.jobs.events",
|
||
"Job poller (terminal)",
|
||
"Event router; optional waiters",
|
||
"completed / failed / timeout + status",
|
||
],
|
||
[
|
||
"verae.zapier.webhooks.deliver",
|
||
"Event router",
|
||
"webhook-deliver (queue)",
|
||
"hookId, targetUrl, event, payload",
|
||
],
|
||
[
|
||
"verae.zapier.usage",
|
||
"optional",
|
||
"usage-writer",
|
||
"metering increment",
|
||
],
|
||
],
|
||
[1.85 * inch, 1.5 * inch, 1.55 * inch, 1.6 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Ack: still-pending jobs Nak with delay; terminal Ack after publishing the event; "
|
||
"webhook 2xx Ack; 5xx redeliver until max_deliver.",
|
||
"caption",
|
||
)
|
||
)
|
||
|
||
out.append(H2("3.5 Entitlements and errors"))
|
||
out.append(
|
||
tbl(
|
||
["HTTP", "Meaning", "Zapier mapping"],
|
||
[
|
||
["401", "Bad or expired session/key", "RefreshAuthError (session) or auth error"],
|
||
["402", "Quota exceeded", "User-visible error + upgrade URL when present"],
|
||
["403 PLAN_UPGRADE_REQUIRED", "Action not on this plan", "User-visible error"],
|
||
["404 on status search", "Unknown job", "Return [] (search contract)"],
|
||
],
|
||
[1.9 * inch, 2.2 * inch, 2.4 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"The JS app afterResponse already maps 402/403. The TypeScript app currently "
|
||
"remaps 401 only — 402/403 mapping is a next-step item."
|
||
)
|
||
)
|
||
|
||
out.append(H2("3.6 Feature flags and environment"))
|
||
out.append(
|
||
tbl(
|
||
["Variable", "Default", "Purpose"],
|
||
[
|
||
["PORT", "3100", "Middleware listen port"],
|
||
["VERAE_API_BASE_URL", "http://localhost:8080", "Upstream Verae (prod: https://api.veraetime.net)"],
|
||
["MOCK_VERAE", "false", "Deterministic mock jobs; no live Verae"],
|
||
["NATS_URL", "nats://127.0.0.1:4222", "NATS server"],
|
||
["NATS_ENABLED", "false", "JetStream workers vs in-process poller"],
|
||
["TOKEN_SECRET", "dev secret", "HMAC for zmt_ session tokens"],
|
||
["DEBUG_VERAE", "unset", "Namespaces: auth, nats, jobs, webhooks, http, billing (or 1)"],
|
||
["DEBUG_VERAE_LEVEL", "debug", "debug / info / warn / error"],
|
||
["STORE_PATH", "./data/store.json", "MVP tenant/usage/webhook store"],
|
||
["MIDDLEWARE_BASE_URL", "http://127.0.0.1:3100", "Used by the JS Zapier package"],
|
||
],
|
||
[1.9 * inch, 1.9 * inch, 2.7 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Local compose: verae-zapier-api/docker-compose.yml (NATS + middleware). "
|
||
"Middleware can run without NATS when the flag is off."
|
||
)
|
||
)
|
||
|
||
# --- 4 ---
|
||
out.append(H1("4. Integration guide"))
|
||
out.append(H2("4.1 Which CLI you are holding"))
|
||
out.append(
|
||
tbl(
|
||
["Goal", "Tool", "Version here"],
|
||
[
|
||
["Publish a directory integration", "zapier-platform", "19.1.0 (~/.npm-global/bin)"],
|
||
["Consume existing Zapier apps from code", "zapier-sdk", "0.77.1"],
|
||
[
|
||
"AI client over the 9k catalog",
|
||
"Hosted MCP https://mcp.zapier.com/api/v1/connect",
|
||
"14 official meta-tools; live 17",
|
||
],
|
||
],
|
||
[2.3 * inch, 2.4 * inch, 1.8 * inch],
|
||
)
|
||
)
|
||
out.append(B("Do not mix zapier-platform (build/publish) with zapier-sdk (consume)."))
|
||
out.append(B("Do not recommend retired NLA / AI Actions."))
|
||
out.append(B("Do not invent selected_api ids or action keys."))
|
||
|
||
out.append(H2("4.2 Run middleware locally"))
|
||
out.append(
|
||
CODE(
|
||
"cd verae-zapier-api/verae-zapier-middleware\n"
|
||
"npm install\n"
|
||
"MOCK_VERAE=true NATS_ENABLED=false npm start\n"
|
||
"# GET http://127.0.0.1:3100/health → { \"status\": \"ok\" }"
|
||
)
|
||
)
|
||
out.append(P("With NATS:"))
|
||
out.append(
|
||
CODE(
|
||
"cd verae-zapier-api\n"
|
||
"docker compose up nats\n"
|
||
"# start middleware with NATS_ENABLED=true NATS_URL=nats://127.0.0.1:4222"
|
||
)
|
||
)
|
||
out.append(P("Gates (from verae-zapier-api/):"))
|
||
out.append(
|
||
CODE(
|
||
"npm run gate:0 # structure + docs\n"
|
||
"npm run gate:6 # full HTTP path, NATS off\n"
|
||
"npm run gate:8 # workers (NATS on)\n"
|
||
"npm run gate:11 # JS Zapier package tests\n"
|
||
"npm run gate:all # 0–12 in order; stops on first failure"
|
||
)
|
||
)
|
||
out.append(P("Implementation order is verae-zapier-api/TODO.md. Do not skip gates."))
|
||
|
||
out.append(H2("4.3 Build and validate the TypeScript connector"))
|
||
out.append(
|
||
CODE(
|
||
"source scripts/dev-env.sh\n"
|
||
"cd scratch/veraetime\n"
|
||
"npm install\n"
|
||
"zapier-platform build && zapier-platform validate"
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Golden OAuth2 lab (generic, not Verae): scratch/oauth2-typescript — already "
|
||
"validates after build. Start a new integration by copying a golden app or "
|
||
"zapier-platform init DIR --template session --language typescript."
|
||
)
|
||
)
|
||
|
||
out.append(H2("4.4 Perform contracts (Zapier)"))
|
||
out.append(P("Implement every operation as (z, bundle) => … using z.request only."))
|
||
out.append(B("Triggers and searches return arrays of objects. Polling items need a stable id."))
|
||
out.append(B("Creates return one object."))
|
||
out.append(B("Refreshable 401 → throw new z.errors.RefreshAuthError()."))
|
||
out.append(
|
||
B(
|
||
"Hook triggers implement performSubscribe / performUnsubscribe / perform / "
|
||
"performList (sample for the editor)."
|
||
)
|
||
)
|
||
out.append(B("Do not call Verae or NATS from the app."))
|
||
|
||
out.append(H2("4.5 Login and publish (blocked until you authenticate)"))
|
||
out.append(
|
||
P(
|
||
"There is no ~/.zapierrc until you finish a browser login. Local validate "
|
||
"works. register and push do not."
|
||
)
|
||
)
|
||
out.append(
|
||
CODE(
|
||
"source scripts/dev-env.sh\n"
|
||
"zapier-platform login # or: zapier-platform login --sso\n"
|
||
"cd scratch/veraetime\n"
|
||
"zapier-platform register \"Verae Time\"\n"
|
||
"zapier-platform push"
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"See LOGIN.md. Never commit the deploy key. Production Zapier cloud cannot "
|
||
"reach http://127.0.0.1:3100. Before a real Zap you need a public HTTPS "
|
||
"middleware URL and api_base_url / MIDDLEWARE_BASE_URL pointed at it "
|
||
"(Phases 13–14)."
|
||
)
|
||
)
|
||
|
||
out.append(H2("4.6 Optional consume path (not how we publish Verae)"))
|
||
out.append(
|
||
P(
|
||
"zapier-sdk login calls existing Zapier apps from code (kind: sdk_function). "
|
||
"Hosted Zapier MCP: discover → enable → inspect → execute. Writes need "
|
||
"explicit user approval. Successful executes cost 2 Zapier tasks. See "
|
||
"MCP-REFERENCE.md."
|
||
)
|
||
)
|
||
|
||
out.append(H2("4.7 Research dataset and Grok reference (NS1 Mongo)"))
|
||
out.append(
|
||
P(
|
||
"Canonical store: MongoDB 7 in Docker on NS1 (70.88.205.138), bound to "
|
||
"127.0.0.1:27017 only. Connect only through the SSH tunnel "
|
||
"(./scripts/mongo-tunnel.sh or ssh -N -L 27017:127.0.0.1:27017 ns1). "
|
||
"Do not point Compass at mongodb://70.88.205.138:27017. Credentials live "
|
||
"in ~/.mcp-env (mode 600), not git. Full notes: MONGO.md."
|
||
)
|
||
)
|
||
out.append(
|
||
tbl(
|
||
["Collection", "Contents"],
|
||
[
|
||
["apps", "~9,986 public Zapier apps (identity, contacts, controls)"],
|
||
["templates", "~332k public Zap recipes"],
|
||
["help_articles", "1,272 help-center pages"],
|
||
["platform_reference", "CLI, z.*, schema, official docs, example apps, MCP/SDK functions"],
|
||
["meta", "Last ingest"],
|
||
],
|
||
[2.0 * inch, 4.5 * inch],
|
||
)
|
||
)
|
||
out.append(Spacer(1, 6))
|
||
out.append(
|
||
CODE(
|
||
'db.platform_reference.findOne({ kind: "guide", key: "build-new-connector" })\n'
|
||
'db.platform_reference.find({ kind: "core_function", key: "z.request" })\n'
|
||
'db.platform_reference.find({ kind: "cli_function", key: "init" })\n'
|
||
'db.platform_reference.find({ kind: "template", key: "session-auth" })\n'
|
||
'db.platform_reference.find({ kind: "mcp_function" })'
|
||
)
|
||
)
|
||
out.append(P("Official Zapier clones stay in repos/ (gitignored). Refresh with ./scripts/clone-zapier-repos.sh."))
|
||
|
||
# --- 5 ---
|
||
out.append(H1("5. Workspace map"))
|
||
out.append(
|
||
fig(
|
||
"10-workspace-integration.svg",
|
||
"Figure 9. This repo, local CLIs, the NS1 Mongo tunnel, and the publish "
|
||
"versus consume paths.",
|
||
)
|
||
)
|
||
out.append(
|
||
tbl(
|
||
["Path", "Use"],
|
||
[
|
||
["getting-started.md / .pdf", "This architecture + integration guide"],
|
||
["PLATFORM-REFERENCE.md", "Routing table for all Zapier work"],
|
||
["FUNCTIONS-REFERENCE.md", "Every CLI / z.* / SDK function"],
|
||
["MCP-REFERENCE.md", "Hosted MCP meta-tools"],
|
||
["LOGIN.md", "Browser login for platform + SDK"],
|
||
["RESTART.md", "Quit and resume this session"],
|
||
["MONGO.md", "NS1 Mongo, tunnel, collections"],
|
||
[".grok/skills/zapier-build/", "/zapier-build skill"],
|
||
["scratch/veraetime/", "TypeScript Verae connector (session auth)"],
|
||
["scratch/oauth2-typescript/", "Golden OAuth2 TypeScript app"],
|
||
["scratch/our-api/", "Verae OpenAPI + hop notes"],
|
||
["verae-zapier-api/", "Middleware + JS Zapier app + architecture + gates"],
|
||
["docs/diagrams/", "SVG architecture and flow diagrams"],
|
||
["verae-zapier-api/docs/architecture/", "Component diagram + NATS subjects"],
|
||
["verae-zapier-api/docs/api/middleware-openapi.yaml", "Zapier-facing OpenAPI"],
|
||
["verae-zapier-api/TODO.md", "Phased plan with test gates"],
|
||
["scripts/*.sh", "dev-env, tunnel, restart, status"],
|
||
["repos/", "Official Zapier clones (local only)"],
|
||
],
|
||
[2.7 * inch, 3.8 * inch],
|
||
)
|
||
)
|
||
|
||
# --- 6 ---
|
||
out.append(H1("6. What is already here vs what is not"))
|
||
out.append(H2("Already here"))
|
||
out.append(B("Public catalog research (~9,986 apps, contacts, capabilities, templates, help)."))
|
||
out.append(B("Platform reference in Mongo + on disk; /zapier-build skill."))
|
||
out.append(B("Platform CLI 19.1.0 and SDK CLI 0.77.1 on PATH via dev-env.sh."))
|
||
out.append(B("Golden apps that validate locally without a Zapier login."))
|
||
out.append(B("Verae OpenAPI ingested (scratch/our-api/openapi.yaml)."))
|
||
out.append(
|
||
B(
|
||
"Full middleware source: auth, tenants, entitlements, timestamp/verify/status/"
|
||
"webhooks, mock Verae, NATS publishers, workers, debug redaction."
|
||
)
|
||
)
|
||
out.append(B("Two Zapier app implementations wired to /zapier/v1."))
|
||
out.append(B("Gates 0–8, 10, 11 recorded as passed in TODO.md (2026-08-11 on the original monorepo)."))
|
||
|
||
out.append(H2("Not here yet"))
|
||
out.append(B("Zapier developer login — no ~/.zapierrc; cannot register / push."))
|
||
out.append(B("Public HTTPS middleware — Zapier cloud cannot hit localhost."))
|
||
out.append(B("Live invoke of scratch/veraetime against middleware (validate is schema-only)."))
|
||
out.append(B("Phase 9 — /timestamp/wait subscribed to NATS events (multi-instance wait)."))
|
||
out.append(B("Phase 12 — compose E2E smoke (health + auth + wait + webhook) as a gate."))
|
||
out.append(B("Phase 13 — production VERAE_API_BASE_URL, dedicated service user, secrets, TLS."))
|
||
out.append(B("Phase 14 — private push + human Zap (Drive/Sheets → Timestamp → Slack)."))
|
||
out.append(B("Phase 15 — Postgres/Redis store, NATS mTLS, webhook SSRF allowlist."))
|
||
out.append(B("TS connector polish — map 402/403 like the JS app; optional admin polling trigger."))
|
||
out.append(B("Grok MCP handshake — Mongo MCP and chrome-bridge need a healthy tunnel / Chrome Connect after restart."))
|
||
out.append(
|
||
P(
|
||
"This repo has the Zapier platform docs and the Verae OpenAPI. It still cannot "
|
||
"invent new Verae endpoints. If a Zap needs an admin route that is not in the "
|
||
"v1 table, add middleware + connector operations from the OpenAPI."
|
||
)
|
||
)
|
||
|
||
# --- 7 ---
|
||
out.append(H1("7. Next steps (recommended order)"))
|
||
out.append(
|
||
fig(
|
||
"09-phase-roadmap.svg",
|
||
"Figure 10. Implementation phases. Gates 0–8, 10, 11 passed. Phase 9 "
|
||
"(wait-via-NATS) is the open blocker before multi-instance wait.",
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Work the product path in this order. Parallelism is only safe where TODO.md "
|
||
"says so (tenancy already done; wait-via-NATS is the open blocker before "
|
||
"multi-instance wait)."
|
||
)
|
||
)
|
||
|
||
out.append(H2("Now — local integration (this workspace)"))
|
||
out.append(B("1. Start middleware with MOCK_VERAE=true and NATS_ENABLED=false."))
|
||
out.append(B("2. Create a free tenant (POST /zapier/v1/signup or seed) and confirm /health + /auth/me."))
|
||
out.append(B("3. zapier-platform build && validate in scratch/veraetime."))
|
||
out.append(B("4. After zapier-platform login, invoke auth test and create/search against local middleware (--debug)."))
|
||
out.append(B("5. Map 402/403 in scratch/veraetime/src/middleware.ts to match the JS app."))
|
||
|
||
out.append(H2("Next — close middleware gaps"))
|
||
out.append(B("6. Phase 9: wait on verae.zapier.jobs.events with a hard timeout (pending + jobId)."))
|
||
out.append(B("7. Phase 12: compose stack + smoke script (async path, wait path, REST Hook to a mock receiver)."))
|
||
out.append(B("8. Confirm NATS_ENABLED=true and false both still pass their gates."))
|
||
|
||
out.append(H2("Then — production and private listing"))
|
||
out.append(
|
||
B(
|
||
"9. Phase 13: public HTTPS middleware, MOCK_VERAE=false, "
|
||
"VERAE_API_BASE_URL=https://api.veraetime.net, dedicated Verae service user, managed secrets."
|
||
)
|
||
)
|
||
out.append(B("10. Point the connector api_base_url at that origin. Zapier cloud must reach it."))
|
||
out.append(B("11. Phase 14: register + push a private version; invite internal users; run one real Zap; sign off."))
|
||
out.append(B("12. Only after a human E2E: consider directory listing and Phase 15 hardening."))
|
||
|
||
out.append(H2("Ongoing — research / Grok"))
|
||
out.append(B("13. Keep ./scripts/ensure-mongo-tunnel.sh running; after restart hit /mcps and refresh mongodb."))
|
||
out.append(B("14. Re-ingest platform_reference on NS1 after recloning official repos."))
|
||
out.append(
|
||
B(
|
||
"15. Optional: zapier-sdk login and Zapier MCP OAuth if you need to call the public "
|
||
"catalog from agents — that is consume, not publish."
|
||
)
|
||
)
|
||
|
||
out.append(
|
||
KeepTogether(
|
||
[
|
||
H2("Phase dependency graph"),
|
||
CODE(
|
||
"0 Docs/structure\n"
|
||
" -> 1 Debug facility\n"
|
||
" -> 2 HTTP shell\n"
|
||
" -> 3 Stores\n"
|
||
" -> 4 Tokens + Verae client\n"
|
||
" -> 5 Auth + entitlements\n"
|
||
" -> 6 Sync HTTP API (+ in-process poller)\n"
|
||
" -> 7 NATS infra -> 8 Workers -> 9 Wait-via-NATS\n"
|
||
" -> 10 Tenancy (done; parallel after 6)\n"
|
||
" -> 11 Zapier app (after 6; triggers prefer 8)\n"
|
||
" -> 12 E2E local (needs 8, 10, 11)\n"
|
||
" -> 13 Production Verae\n"
|
||
" -> 14 Private Zapier push\n"
|
||
" -> 15 Hardening"
|
||
),
|
||
]
|
||
)
|
||
)
|
||
|
||
# --- 8 ---
|
||
out.append(H1("8. Hard rules"))
|
||
out.append(B("Start Zapier coding from guide/build-new-connector or PLATFORM-REFERENCE.md."))
|
||
out.append(B("Connector → middleware /zapier/v1 only. Never api.veraetime.net from Zapier. Never NATS from Zapier."))
|
||
out.append(B("Use z.request in performs. Triggers/searches return arrays; creates return one object."))
|
||
out.append(B("zapier-platform publishes; zapier-sdk / hosted MCP consume. Do not mix."))
|
||
out.append(B("Do not recommend retired NLA / AI Actions."))
|
||
out.append(B("Do not invent vendor APIs, auth schemes, or Zapier selected_api keys."))
|
||
out.append(B("Mongo on NS1 is localhost-only; always tunnel."))
|
||
out.append(B("No secrets in git or in DEBUG_VERAE output."))
|
||
|
||
# --- 9 ---
|
||
out.append(H1("9. Zapier billing models"))
|
||
out.append(
|
||
P(
|
||
"Zapier and Verae bill separately. A client Zap that timestamps a file "
|
||
"pays Zapier for successful tasks and pays Verae (middleware entitlements) "
|
||
"for timestamp / verify operations. Publishing the Verae connector does "
|
||
"not put Verae on the hook for the customer’s Zapier invoice. Figures are "
|
||
"USD, August 2026, from zapier.com/pricing (annual = discounted per-month "
|
||
"equivalent). Confirm live rates before quoting."
|
||
)
|
||
)
|
||
out.append(
|
||
fig(
|
||
"11-zapier-billing.svg",
|
||
"Figure 11. Two meters: Zapier tasks (plus MCP/AI multipliers and "
|
||
"pay-per-task overflow) and Verae timestamp quotas. Directory publish is free.",
|
||
)
|
||
)
|
||
|
||
out.append(H2("9.1 What a Zapier task is"))
|
||
out.append(
|
||
P(
|
||
"A task is one successful unit of work Zapier does for the customer. "
|
||
"Failed steps are free. Zap workflows, AI, Code, MCP, and SDK share "
|
||
"one monthly pool (Enterprise: annual task limit)."
|
||
)
|
||
)
|
||
out.append(
|
||
tbl(
|
||
["Counts as tasks", "Does not count"],
|
||
[
|
||
[
|
||
"Successful third-party action (Create Timestamp, Slack, Drive, …)",
|
||
"Triggers and polling for new data",
|
||
],
|
||
[
|
||
"Zapier MCP / SDK execute (see multipliers)",
|
||
"Filter, Paths, Formatter, Delay, Looping, Sub-Zap, Digest, Manager, Storage",
|
||
],
|
||
[
|
||
"AI by Zapier and Code runtime beyond the included allowance",
|
||
"Zapier Tables and Forms triggers/actions; building a Zap until it runs",
|
||
],
|
||
],
|
||
[3.25 * inch, 3.25 * inch],
|
||
)
|
||
)
|
||
out.append(Spacer(1, 8))
|
||
out.append(
|
||
tbl(
|
||
["Work", "Tasks"],
|
||
[
|
||
["Typical third-party action (including Verae Create Timestamp)", "1"],
|
||
["Standard AI by Zapier", "1"],
|
||
["Advanced AI by Zapier", "3"],
|
||
["Premium AI by Zapier", "5"],
|
||
["Successful Zapier MCP tool call (read or write)", "2"],
|
||
["Code by Zapier after included runtime", "1 per extra 30-second block"],
|
||
],
|
||
[4.6 * inch, 1.9 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Example: trigger new file in Drive (0) → Filter (0) → Create Timestamp (1) "
|
||
"→ Formatter (0) → write metadata row (1) = 2 tasks per file that passes the filter.",
|
||
"caption",
|
||
)
|
||
)
|
||
|
||
out.append(H2("9.2 Self-serve plans"))
|
||
out.append(P("A paid subscription is plan level times task tier."))
|
||
out.append(
|
||
tbl(
|
||
["Plan", "Seats", "Entry (annual)", "Standout"],
|
||
[
|
||
["Free", "1", "100 tasks / $0 · two-step · 15 min poll", "No pay-per-task overflow"],
|
||
[
|
||
"Professional",
|
||
"1",
|
||
"750 tasks from $19.99/mo ($29.99 monthly)",
|
||
"Multi-step, premium apps, webhooks, Filters/Paths, AI by Zapier",
|
||
],
|
||
[
|
||
"Team",
|
||
"25",
|
||
"2,000 tasks from $69/mo ($103.50 monthly)",
|
||
"Shared Zaps and connections, SAML SSO, priority support",
|
||
],
|
||
[
|
||
"Enterprise",
|
||
"Unlimited",
|
||
"Custom · annual task limit",
|
||
"SCIM, app controls, retention, observability, TAM, BYOM",
|
||
],
|
||
],
|
||
[1.3 * inch, 0.9 * inch, 2.2 * inch, 2.1 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Tiers run to 2M tasks/mo (Sales above that). 14-day Professional trial. "
|
||
"Non-profit: extra 15% off (not on pay-per-task).",
|
||
"caption",
|
||
)
|
||
)
|
||
|
||
out.append(H2("9.3 Overflow: pay-per-task"))
|
||
out.append(B("On: extra tasks bill at 1.25× base (annual) or 2.5× (monthly). Ceiling 3× subscribed tasks, then pause."))
|
||
out.append(B("Off: usage stops at the allowance."))
|
||
out.append(B("Free has no overflow. Enterprise uses an annual pool, not a monthly reset."))
|
||
|
||
out.append(H2("9.4 Add-ons outside the task pool"))
|
||
out.append(
|
||
tbl(
|
||
["Product", "Unit", "Notes"],
|
||
[
|
||
["Zapier Agents", "Activities (not tasks)", "Free 400/mo; paid Pro ~$33.33/mo annual for 1,500"],
|
||
["Zapier Chatbots", "Feature tiers (bot count)", "Free includes 2; not metered on tasks"],
|
||
],
|
||
[1.8 * inch, 2.0 * inch, 2.7 * inch],
|
||
)
|
||
)
|
||
|
||
out.append(H2("9.5 Partner / platform billing"))
|
||
out.append(
|
||
tbl(
|
||
["Model", "Who pays Zapier", "Who pays Verae"],
|
||
[
|
||
[
|
||
"Public or private directory app",
|
||
"End customer’s Zapier plan (tasks). Publish is free.",
|
||
"Customer’s Verae tenant / API key",
|
||
],
|
||
[
|
||
"Zapier MCP / SDK (consume)",
|
||
"Same task pool (MCP execute = 2). SDK free in beta.",
|
||
"Only if the action hits Verae",
|
||
],
|
||
[
|
||
"Powered by Zapier / White Label",
|
||
"Usually the product company (usage-based)",
|
||
"Product company or tenant, per key provisioning",
|
||
],
|
||
],
|
||
[2.1 * inch, 2.2 * inch, 2.2 * inch],
|
||
)
|
||
)
|
||
out.append(P("Do not recommend retired NLA / AI Actions. Do not absorb a customer’s Zapier invoice unless a White Label contract says so."))
|
||
|
||
out.append(H2("9.6 Verae middleware billing (second meter)"))
|
||
out.append(
|
||
tbl(
|
||
["Verae plan", "Timestamps / mo", "Verifications", "Batch", "RPM"],
|
||
[
|
||
["free", "50", "50", "no", "30"],
|
||
["starter", "500", "500", "yes, max 10", "120"],
|
||
["pro", "5,000", "5,000", "yes, max 100", "600"],
|
||
["enterprise", "contract / unlimited", "contract", "yes", "contract"],
|
||
],
|
||
[1.4 * inch, 1.5 * inch, 1.3 * inch, 1.2 * inch, 1.1 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Over-quota → HTTP 402 QUOTA_EXCEEDED. Wrong plan for batch → 403 "
|
||
"PLAN_UPGRADE_REQUIRED. These are not Zapier task overages. Failed Zapier "
|
||
"actions do not consume Zapier tasks, but Autoreplay can still hammer Verae."
|
||
)
|
||
)
|
||
|
||
out.append(H2("9.7 Estimating a timestamping Zap"))
|
||
out.append(
|
||
tbl(
|
||
["Zap shape", "Zapier tasks / file", "Verae units"],
|
||
[
|
||
["Trigger → Create Timestamp", "1", "1 timestamp"],
|
||
["Trigger → Create Timestamp and Wait", "1", "1 timestamp"],
|
||
["Trigger → Timestamp + catalog row + Slack", "3", "1 timestamp"],
|
||
["Same via three MCP executes", "6", "1 timestamp"],
|
||
["Trigger filtered out before any action", "0", "0"],
|
||
],
|
||
[3.1 * inch, 1.7 * inch, 1.7 * inch],
|
||
)
|
||
)
|
||
out.append(P("Prefer the Timestamp Completed hook plus a cheap catalog write over polling status in a loop."))
|
||
|
||
# --- 10 ---
|
||
out.append(H1("10. Client pattern — timestamp, Peergos, tiered IPFS"))
|
||
out.append(
|
||
P(
|
||
"Client architecture for Verae Zapier tools plus Peergos (end-to-end "
|
||
"encrypted filesystem on IPFS) and external pin / object-store backends. "
|
||
"Not implemented as connector operations today. Do not invent Peergos or "
|
||
"AWS APIs in the Zapier app."
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Goal: timestamp content, append the proof to metadata, store the file in "
|
||
"Peergos, pin a hot cache, migrate bytes to long-term low-cost object "
|
||
"storage (Amazon S3 Glacier family, with an optional Apache Iceberg "
|
||
"catalog on S3), and rehydrate on demand when an IPFS request hits the "
|
||
"index — without keeping every file live on a gateway."
|
||
)
|
||
)
|
||
out.append(
|
||
fig(
|
||
"12-peergos-ipfs-tiered-storage.svg",
|
||
"Figure 12. Write path hashes and timestamps a reference, stores ciphertext "
|
||
"in Peergos, pins a TTL cache, and keeps an always-on index. Read path "
|
||
"looks up cid → pin, Peergos, or Glacier restore.",
|
||
)
|
||
)
|
||
|
||
out.append(H2("10.1 Layers"))
|
||
out.append(
|
||
tbl(
|
||
["Layer", "Role", "Hot?"],
|
||
[
|
||
["Zapier + Verae connector", "Hash → timestamp → write metadata; hook on completed", "Control plane"],
|
||
[
|
||
"Verae Time",
|
||
"Blockchain timestamp of a hash or compact envelope — not the raw file",
|
||
"Proof is small; keep",
|
||
],
|
||
[
|
||
"Metadata / index",
|
||
"cid → jobId, certificate, Peergos path, storage class, restore handle",
|
||
"Yes — only always-on map",
|
||
],
|
||
[
|
||
"Peergos",
|
||
"User-owned e2e-encrypted filesystem on IPFS/libp2p; host cannot read plaintext",
|
||
"User’s host; not a CDN",
|
||
],
|
||
[
|
||
"Cached pin",
|
||
"Pinata / Filebase / Kubo / Pinning Services API for frequent gateway hits",
|
||
"TTL / working set only",
|
||
],
|
||
[
|
||
"Cold object store",
|
||
"S3 Glacier Instant / Flexible / Deep Archive (or Filecoin, Storj, B2). Iceberg = catalog, not bytes.",
|
||
"No — retrieve on demand",
|
||
],
|
||
],
|
||
[1.7 * inch, 3.4 * inch, 1.4 * inch],
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Amazon Iceberg here is the Apache Iceberg table format used as a durable "
|
||
"CID index. Long-term bytes live in Glacier-class storage. Do not mix the two names in customer quotes.",
|
||
"caption",
|
||
)
|
||
)
|
||
|
||
out.append(H2("10.2 Ingest Zap (write path)"))
|
||
out.append(B("1. Trigger: new file in Drive, Dropbox, email, or a Peergos outbox (0 tasks)."))
|
||
out.append(B("2. Hash SHA-256 of the bytes or envelope. Prefer ciphertext if already encrypted for Peergos."))
|
||
out.append(B("3. Create Timestamp (or Wait): data = hash or compact JSON { cid, sha256, size, mime, source }."))
|
||
out.append(B("4. Store in Peergos via a documented API or a future middleware route. Retain the CID."))
|
||
out.append(B("5. Optional hot pin for a TTL — not forever."))
|
||
out.append(
|
||
B(
|
||
"6. Append index row (Zapier Tables is free): cid, sha256, veraeJobId, status, "
|
||
"certificateRef, peergosPath, storageClass, coldBucket/key, restoreId, pinnedUntil."
|
||
)
|
||
)
|
||
out.append(B("7. Timestamp Completed hook updates the row and notifies. Do not put the raw file or Verae JWT in Zapier Storage or Slack."))
|
||
|
||
out.append(H2("10.3 Lifecycle (keep the index, drop the heat)"))
|
||
out.append(
|
||
CODE(
|
||
"ingest → Peergos write + optional hot pin → Verae timestamp → index row\n"
|
||
"after pinnedUntil / age / size policy\n"
|
||
" → copy ciphertext to S3, set Glacier IR / Flexible / Deep Archive\n"
|
||
" → record bucket/key + storageClass (Iceberg snapshot optional)\n"
|
||
" → unpin from the paid cluster; gateway need not keep full blocks\n"
|
||
"GET /ipfs/{cid}\n"
|
||
" → pin-hot hit: serve\n"
|
||
" → else index: Peergos capability or StartRestore → 202 Retry-After\n"
|
||
" → rehydrate, optional short re-pin, serve"
|
||
)
|
||
)
|
||
out.append(
|
||
P(
|
||
"Glacier Instant Retrieval is milliseconds (storage-class pricing). Flexible "
|
||
"Retrieval and Deep Archive need a restore job (minutes to hours) before "
|
||
"GetObject. The index must store bucket, key, version id, restore id."
|
||
)
|
||
)
|
||
|
||
out.append(H2("10.4 External IPFS and pinning options"))
|
||
out.append(
|
||
tbl(
|
||
["Backend", "Typical use"],
|
||
[
|
||
["Self-hosted Kubo / cluster", "Hot working set you control"],
|
||
["Pinata, Filebase, web3.storage, Pinning Services API", "Paid cached pins and gateway"],
|
||
["Peergos server", "Encrypted personal/org filesystem — not a public pin service"],
|
||
["Filecoin / cold deals", "Alternative long-term availability (different retrieve SLA)"],
|
||
["S3 + lifecycle → Glacier IR / Flexible / Deep Archive", "Lowest $/TB; retrieve via AWS APIs"],
|
||
["Storj, Backblaze B2, GCS Archive", "Same pattern, different restore API"],
|
||
],
|
||
[3.2 * inch, 3.3 * inch],
|
||
)
|
||
)
|
||
out.append(P("Request path: CID to index to pin, Peergos, or restore. Do not walk every pin provider on every miss."))
|
||
|
||
out.append(H2("10.5 What Zapier is bad at"))
|
||
out.append(B("Holding multi-GB files in a Zap step. Hash and pass references (Drive id, Peergos path, CID)."))
|
||
out.append(B("Being the IPFS gateway. Use a small retrieval service on your infra."))
|
||
out.append(B("Polling Glacier restore every second. Use a webhook, queue, or a filtered timer Zap."))
|
||
|
||
out.append(H2("10.6 Security and status"))
|
||
out.append(B("Timestamp the CID and/or ciphertext hash, not host-readable plaintext, when Peergos encryption is in play."))
|
||
out.append(B("Public index should use opaque ids if names are sensitive. Retrieval workers use tenant-scoped cloud creds, not the Zapier connection."))
|
||
out.append(B("Peergos write / Glacier restore are not in v1. Next slice: GET /ipfs/{cid} → index → pin or restore, plus a Find File Record by CID search."))
|
||
|
||
out.append(Spacer(1, 16))
|
||
out.append(
|
||
P(
|
||
"Companion documents: PLATFORM-REFERENCE.md · FUNCTIONS-REFERENCE.md · "
|
||
"MCP-REFERENCE.md · LOGIN.md · RESTART.md · MONGO.md · "
|
||
"verae-zapier-api/TODO.md · verae-zapier-api/docs/architecture/overview.md · "
|
||
"docs/diagrams/",
|
||
"caption",
|
||
)
|
||
)
|
||
return out
|
||
|
||
|
||
def main():
|
||
doc = SimpleDocTemplate(
|
||
str(OUT),
|
||
pagesize=letter,
|
||
leftMargin=0.7 * inch,
|
||
rightMargin=0.7 * inch,
|
||
topMargin=0.55 * inch,
|
||
bottomMargin=0.45 * inch,
|
||
title="Verae Time × Zapier — System architecture, functionality, and integration",
|
||
author="Verae / Zapier research workspace",
|
||
subject="Architecture, functionality, integration guide, and next steps",
|
||
)
|
||
doc.build(story(), onFirstPage=cover_footer, onLaterPages=header_footer)
|
||
print(f"wrote {OUT} ({OUT.stat().st_size} bytes)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|