clone

push a directory of html from the terminal and get a four-character url back. a deno cli on cloudflare

feat/source-stable-push
nzip / ARCHITECTURE.md
ARCHITECTURE.md 10.6K rendered
1# nzip architecture23nzip is a Deno CLI backed by a Cloudflare Worker, R2 object storage, and D14relational state. The CLI and Worker share one runtime-neutral package so both5sides canonicalize and hash manifests identically.67## Components89```mermaid10flowchart TB11 CLI["Deno CLI"] -- "bearer API at control host" --> W["Worker"]12 SHORT(["GET control/2a3f"]) -- "308" --> SITE["https://2a3f.site-domain/"]13 V(["visitor"]) -- "GET site hostname" --> C{"browser cache"}14 C -- "fresh" --> RESP["public response"]15 C -- "miss or revalidate" --> W16 W --> RESP17 W --> R2[("R2 blobs")]18 W --> D1[("D1 state")]19```2021- `shared/` defines manifests, hashing, target parsing, limits, media types, and22 API contracts.23- `cli/` bundles local files, uploads missing blobs, commits sites, and manages24 owner operations.25- `worker/` serves public sites and implements authenticated management,26 security telemetry, notifications, cleanup, and delivery retries.2728## Push protocol2930A push is a stateless three-step exchange; the manifest is the state carried31between steps:32331. `POST /api/push/prepare` sends the manifest and returns missing blob hashes.342. `PUT /api/blob/{sha256}` uploads only missing blobs, with bounded35 concurrency. The Worker re-hashes each body and rejects mismatches or36 oversized blobs.373. `POST /api/push/commit` verifies every blob, resolves or allocates the38 address, applies TTL and password policy, repoints the site, and appends39 retained history atomically. TTL resolution is explicit request, existing40 site, vault default, then the 14-day global fallback; the response identifies41 the winning layer.4243Content-addressed blobs deduplicate across every site. The last ten pushes per44site remain addressable for revert operations.4546## App-origin reservation4748`POST /api/apps` reserves a named address before content exists. The reservation49is separate from the live `sites` row and participates in allocation forever.50This gives lofi a stable origin before `credentialOrigins`, WebAuthn RP IDs,51service-worker scope, OPFS, or IndexedDB state are configured. A reservation52without content serves a small non-indexed placeholder. Deploying creates or53updates the ordinary site row at the reserved address; deletion and expiry may54remove that row but never the reservation tombstone.5556The CLI's tracked `nzip.app.json` stores only framework, target, address, origin,57and build configuration. The owner bearer token remains solely in the mode-060058CLI config. `app deploy` runs lofi's Deno build at the origin root, validates the59build identity and PWA artifacts, and sends the generated CSP with the commit so60the Worker can mirror it as an HTTP response header. Sync-node configuration is61application data and remains orthogonal to hosting.6263## Serving and caching6465The configured control origin owns authenticated APIs, notification enrollment,66and legacy short paths. A legacy `/<address>` request receives a permanent `308`67to `https://<address>.<site-domain>/`; the control origin never serves artifact68bytes. The wildcard route accepts only exact four-character lowercase69hexadecimal hostnames. Management and notification routes are not dispatched on70artifact hosts.7172Each artifact hostname is a distinct browser origin. Origin-scoped storage and73service-worker control therefore stay within one site, and nzip's `__Host-`74unlock cookie is host-only. Artifact responses also disable `document.domain`75relaxation. Until `SITE_DOMAIN` is registered in the Public Suffix List, an76artifact can still set a parent-domain cookie or deliberately choose that parent77as its WebAuthn RP ID. Applications that require isolation must use host-only78cookies and their exact hostname as the RP ID.7980Serving normally requires one D1 read for site state and two R2 reads for the81manifest and file. Public responses use `ETag` revalidation and a 60-second82browser cache entry. Every Worker response also sends83`Cloudflare-CDN-Cache-Control: no-store`: the exact custom domain and wildcard84route currently share an unsafe hostname-agnostic edge cache key, so an edge85entry must never be replayed on a sibling site origin.8687Public responses retain site cache tags and purge hooks for a future host-safe88edge-cache design. Protected responses and errors are never cached.8990Site roots always use `/`. Directory paths redirect to the trailing-slash form91so relative asset paths resolve correctly, and explicit `index.html` paths92redirect to their containing directory. For migration compatibility, an93unresolved `/<address>/...` asset path on that address's hostname falls back to94the corresponding root-relative asset. A real file at the prefixed path always95wins.9697## Storage model9899Core publishing state:100101| table | purpose |102| ------------------ | --------------------------------------------------------------------- |103| `vaults` | named slots, descriptions, and optional default TTL |104| `vault_defaults` | temporary/permanent lifecycle selection |105| `app_reservations` | permanent address and alias tombstones for application origins |106| `sites` | live address, manifest, expiry, password, and optional app CSP policy |107| `pushes` | retained per-site manifest history |108109Security and notification state:110111| table | purpose |112| ----------------------------- | ---------------------------------------------------- |113| `security_probes` | deduplicated scanner/address observations |114| `security_signals` | rate-limit and suspicious-hit confirmations |115| `security_incidents` | alert lifecycle, severity, and suppression state |116| `security_notifications` | durable alert email outbox |117| `notification_pairing_window` | owner-controlled enrollment deadline |118| `notification_devices` | pending, approved, active, and revoked device claims |119| `notification_events` | bounded notification payloads and click targets |120| `notification_deliveries` | leased delivery attempts, retries, and outcomes |121122An R2 object is deleted only when no live site or retained history entry123references it and it is older than 24 hours. This prevents concurrent garbage124collection from deleting an in-flight push.125126## Notification flow127128Notification setup separates enrollment, owner approval, and subscription129attachment. Delivery is also asynchronous: the owner request persists an event130and per-device attempts before background work contacts a Web Push provider.131132```mermaid133flowchart TB134 OWNER["owner: notify pair"] --> WINDOW[("10-minute window")]135 PHONE["phone browser"] -- "enroll" --> PENDING[("pending device")]136 WINDOW --> PENDING137 PENDING --> CODE["display pairing code"]138 CODE --> APPROVE["owner approves code"]139 APPROVE --> APPROVED[("approved claim")]140 APPROVED -- "activate" --> PWA["installed PWA"]141 PWA -- "attach subscription" --> ACTIVE[("active device")]142```143144```mermaid145flowchart TB146 OWNER["owner CLI"] -- "notify send" --> API["notification API"]147 API --> EVENT[("event + deliveries")]148 ACTIVE[("active devices")] --> DRAIN["scheduled drain"]149 EVENT --> DRAIN150 DRAIN --> PUSH["Web Push provider"]151 PUSH --> SW["service worker"]152 SW --> NOTICE["system notification"]153 NOTICE -- "tap" --> TARGET["validated click target"]154 TARGET --> SITE["root or pinned site"]155```156157Pairing codes cannot approve themselves; the approval path always requires the158owner bearer token. Subscription endpoints must match the configured159provider-origin allowlist. Notification click targets are resolved at tap time160and open only the deployment root or the unchanged site manifest recorded with161the event.162163## Background work164165The five-minute schedule evaluates enumeration windows and drains notification166deliveries. The daily schedule expires sites, garbage-collects content, prunes167telemetry and notification state, and sends the security activity digest.168169Durable outboxes separate request acceptance from email and Web Push delivery.170Delivery workers use leases, bounded retry schedules, and terminal outcomes so171overlapping cron executions remain safe.172173## Observability174175```mermaid176flowchart TB177 RESP["Worker response"] --> CLASSIFY["classify request"]178 CLASSIFY --> SAMPLE{"scanner sample"}179 SAMPLE -- "selected" --> LOGS["Workers Logs"]180 SAMPLE -- "not selected" --> DROP["no event"]181 PROBES[("D1 probe windows")] --> CRON["5-minute evaluator"]182 CRON --> WINDOW["incident event"]183 CRON --> OUTBOX[("alert outbox")]184 LOGS --> DASH["Observability"]185 WINDOW --> DASH186 PROBES --> METRICS["D1 row metrics"]187```188189Workers Logs provide sampled request-level context. D1 probe windows remain190bounded but unsampled for automatic incident decisions. D1 row metrics are the191direct storage-budget signal.192193## Free-tier design target194195nzip is designed so a small personal deployment can stay inside Cloudflare's196included usage. This is a design target, not a guarantee: quotas are197account-wide and Cloudflare may change them.198199| resource | bounding strategy | usage reference |200| ------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |201| Workers | short path; every browser miss executes the Worker | [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) |202| Workers Logs | invocation logs off; deterministic identity sample | [Workers Logs pricing](https://developers.cloudflare.com/workers/observability/logs/workers-logs/#pricing) |203| D1 | deduplicated windows, write caps, and seven-day pruning | [D1 pricing](https://developers.cloudflare.com/d1/platform/pricing/) |204| R2 | content deduplication, history cap, and daily GC | [R2 pricing](https://developers.cloudflare.com/r2/pricing/) |205| Alert email | one verified Email Routing destination | [Email Service pricing](https://developers.cloudflare.com/email-service/platform/pricing/) |206207Probe persistence happens after the public response and catches its own errors.208Exhausting the telemetry budget reduces detection coverage until quotas reset209but does not stop ordinary site serving.