Publishing with a namespace that is the permission
ship report.html uploads the file to an R2 bucket and prints a URL on files.thehiddendoor.ai. The path prefix is the access policy:
| --internal | /thd/<slug>/ — the default. Google Workspace sign-in for our domains, via Cloudflare Access. |
| --external | /to/<slug>/ — any recipient, one-time email PIN. |
| --public | /p/<slug>/ — anyone, no gate. |
Safe by default: a link is internal unless you said --public. Slugs are random unless you pass --as; a fixed slug that already exists is refused, never silently overwritten, unless you add --force — which exists for the one legitimate case, a recurring report that lives at one stable URL and refreshes each run. --expires 24h|7d|30d writes an epoch to KV and the Worker serves 410 after it; an hourly cron deletes the object.
HTML and folders publish at <slug>/ as a clean URL. Markdown is rendered through a themed page and published the same way, with the raw source alongside. Anything else publishes at <slug>/<filename>.
Usage
ship report.html # internal → /thd/<random>/
ship report.html --public --as q3-report # public → /p/q3-report/
ship offer.html --external --as client-x # PIN → /to/client-x/
ship ./site --public --as launch # a folder → a multi-file site
ship report.html --expires 7d # auto-expire, any tier
ship report.html --as weekly --force # refresh a canonical URL in place
The parts, if you are building your own
| storage | R2 bucket; keys <prefix>/<slug>/… where prefix ∈ p | thd | to |
| serving | one Worker on files.<domain>/*: path → key passthrough, content-types, 404 and 410 |
| access | two Cloudflare Access apps, one per gated prefix. /p has no app. Access matches the most specific path first, so a per-slug app can lock one link to one named recipient. |
| expiry | KV slug → epoch; Worker checks it; hourly cron prunes R2 |
| cli | ~300 lines of bash around wrangler r2 object put |
The source is in a private repo and is wired to our account, bucket and Access identity providers. The design above is the whole of it; there is no clever part left out.
Known edges
- Access gates authentication, not delivery. An
--externallink still has to be emailed by you; Cloudflare only checks the PIN when the recipient arrives. - The base external tier accepts any email. Locking a link to one exact address is a per-slug Access app, made by hand through the API with a write-scoped token — the default read token 403s. Not yet a flag.
- Markdown used to ship as a download. Until 2026-08-22 a
.mdwas stored asapplication/octet-streamand the link opened an unreadable file. Fixed by rendering; the raw source still rides alongside. If you build your own, set the content-type on put. - Bot Fight Mode ate the host once. Super Bot Fight Mode challenged the Worker's own responses; the fix is a WAF skip for the files host. If links work in a browser and fail in
curl, look there first. - Legacy links live at the root. Anything shipped before the tiers existed is at
/<slug>/with no prefix, ungated, and still works. That is a migration debt, not a feature.