master-zapier-plan-draft/research/zapier/scripts/generate-getting-started-pdf.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

1523 lines
59 KiB
Python
Raw Permalink 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
"""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 tenants 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 # 012 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 1314)."
)
)
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 08, 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 08, 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 &amp;&amp; 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 customers 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 customers Zapier plan (tasks). Publish is free.",
"Customers 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 customers 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",
"Users 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()