An empty repository becomes a deployed system: a Vite/React SPA and a Hono Worker on Cloudflare, a hello-world Modal app, and the full two-way auth chain between them — proxy credentials outbound, an HMAC-signed callback inbound. No feature code. The point of the milestone is to prove the security shape before anything of value flows through it.
Three deliverables, all of them thin on purpose. The value is not the code volume — it is that every trust boundary the product will ever need is already crossed, deployed, and asserted in CI.
One Cloudflare Worker serves both the static SPA and the API, so the browser sees exactly one
origin. run_worker_first is declared for /api/* and /internal/*,
so the static-asset handler can never shadow a guarded route.
The single signing scheme in the system, implemented twice — once in TypeScript for the Worker, once in Python for Modal — and pinned byte-for-byte by four shared test vectors that both test suites read from the same JSON file.
A stateless Modal app behind proxy auth that calls back into the Worker with a signed URL, plus the CI that provisions resources, migrates D1, deploys both halves to a per-PR preview, and refuses to go green unless the round trip works.
One request to /api/health crosses every trust boundary the product will use. Hover any
node to isolate its edges. The two red edges are the two authenticated crossings — and they use
two different credentials travelling in opposite directions.
fetch("/api/health"). Renders the returned status. Holds no credential of any kind.not_found_handling: single-page-application.{base_url: origin} with proxy headers. Maps any non-pong to 502.{pong:true}.requires_proxy_auth=True. Rejects anything that is not a workspace proxy token — deploy tokens included.GET /internal/ping with the shared secret, fetches it over httpx, answers {modal:"pong"}. Stateless; holds no Cloudflare credential.--force on each deploy, so rotation is a redeploy.R2? in env.ts. Required from M2.Two crossings, two credential classes, opposite directions. Worker→Modal presents Modal proxy-auth headers; Modal→Worker presents an HMAC signature over the request itself. Neither side ever holds the other platform's credentials: Modal has no Cloudflare token and cannot reach R2 or D1, and the Worker's Modal credential is useless for anything but calling the endpoint. The browser holds neither, and never learns that Modal exists.
Every future Modal→Worker call — fetching an upload, posting a parse result, posting a forecast — uses this one scheme. Defining it at M0 and locking it with cross-language vectors means M2 and M3 inherit a proven primitive instead of re-inventing one under deadline.
/internal/* accepts nothing else: no session cookie,
no bearer token, no allow-list.// WebCrypto, runs on the Cloudflare edge const message = `${method.toUpperCase()}\n${path}\n${exp}\n${await sha256hex(body)}`; return toHex(await crypto.subtle.sign( "HMAC", key, encoder.encode(message)));
# stdlib hmac/hashlib, runs in the Modal container message = f"{method.upper()}\n{path}\n{exp}\n{sha256_hex(body)}" return hmac.new( secret.encode(), message.encode(), hashlib.sha256 ).hexdigest()
worker/security/hmac-vectors.json is the single source of truth. The Bun suite and the
pytest suite both read this same file and assert equality — so the two implementations cannot
drift without a red build. Signatures shown truncated; they are derived from a fixed test secret
committed alongside the vectors, not from any real key.
| method | path | body | sig |
|---|---|---|---|
| GET | /internal/ping | — empty — | 6e9c7eea04d3… |
| POST | /internal/datasets/ds_01/parse-result | {"status":"done","rows":42} | cc7c8f1a9126… |
| POST | /internal/datasets/ds_02/forecast-result | {"note":"ünïcode ✓"} | f2d7ae62b744… |
| GET | /internal/datasets/ds_01/raw | — empty — | 30a85c44a5c9… |
The third vector carries non-ASCII bytes on purpose: it is the one that would catch a
TextEncoder versus str.encode() disagreement, which is exactly the class
of bug that only ever shows up in production with a customer's file.
worker/security/hmac.test.ts, on top of the four
vector-equality tests — ten assertions, all green in CI.
^\d{1,12}$ test before any crypto runs.rows:42 to 43 invalidates the signature.
Comparison is constant-time over the hex strings, and the expiry window is checked
before the HMAC is computed — a malformed exp costs an attacker no
signing work on the Worker's ~10 ms CPU budget.
Six stages on every pull request. Nothing is provisioned, configured or deployed by hand —
including the resource ids, which CI verifies against the committed config and refuses to
paper over. Evidence quoted below is from run #31721014918 on head 905c002.
10 pass, 0 fail (TypeScript §0.3 suite) · 2 passed in 0.01s (Python vector lock)
D1 ton-app-dev: 0588e4b3… · KV ton-app-cache-dev: 909ea857… · provision dev: OK (ids match wrangler.jsonc)
##[warning] BLOCKER: R2 is not enabled on this Cloudflare account. — degraded, not fatal; see finding O1.
✓ No migrations to apply! — 0001_initial.sql (users, sessions, datasets, jobs)
had already been applied by an earlier run in the series, which is the idempotency working.
✓ App deployed in 1.117s — app name resolves from TON_APP_ENV, so the
same file deploys ton-app-dev and ton-app-prod.
Uploaded ton-app-pr-1 (3.34 sec) · 66.42 KiB, gzip 16.46 KiB ·
bindings env.DB, env.KV · three secrets uploaded.
The preview reuses the shared dev resources and drops cron triggers; pr-cleanup.yml
deletes the worker when the PR closes.
M0 QA gate PASS — both assertions below, 11.4 s wall clock. The identical gate
runs again on main in deploy-dev.yml after the dev deploy.
docs/plan.md defines M0's exit condition as two curls against a live preview. Both are
implemented verbatim in scripts/qa/m0.sh and both are quoted below from the actual CI
log. The preview subdomain is redacted here; the deployment itself is real and was reachable during the run.
GET /api/health must return exactly
{"ok":true,"modal":"pong"}. Getting that string requires the Worker to
authenticate into Modal, and Modal to authenticate back into the Worker, and the
signature to verify. A single broken link in the chain produces a 502 with a different body.
Attempt 1 fires 0.6 s after the last wrangler secret put and still sees the old
credential state; the retry loop (10 attempts, 6 s apart) absorbs it and attempt 2 passes
11 s later. The distinction matters — see finding N3.
A second curl hits /internal/ping with a garbage sig and,
critically, an exp set to now + 300 s — deliberately inside the
15-minute window. A 401 therefore proves the signature was checked, not merely that an
expiry was stale.
The route is reachable and returns 401 rather than 404 — the guard ran. With a correct
signature the same URL returns {pong:true}, which is what assertion 1 exercises
from inside Modal.
It is a real deployment, not a mock: a per-PR Worker on Cloudflare's edge, a real Modal container, real secrets pushed through the real path. That is precisely why it caught the proxy-token defect (finding R1) that no unit test could have — both HMAC suites were green while the system was completely unable to talk to Modal.
41 files, +2,396 lines, no deletions — the repository held only README.md and
docs/plan.md before this branch. Roughly half the line count is two lockfiles.
Agent harness state committed with the branch — not product code, and
.omo/ is absent from .gitignore. See finding N1.
What the six CI runs on this branch taught us. One infrastructure defect was found and fixed inside the milestone; one account-level blocker remains open and lands on M2. Click any row to expand.
The milestone's stated purpose was to prove the security topology before any feature code exists, and the gate proves it against a real deployment rather than a mock. The signing contract that every later milestone depends on is defined once, implemented twice, and locked by shared vectors — so M2's parse callback and M3's forecast callback inherit a primitive that is already tested rather than one written in a hurry against a deadline.
statsforecast is not even a dependency yet — M3.r2_buckets bindings and drop the R2? optionality..omo/ to .gitignore and drop the eight committed state files.