tool · bash + Cloudflare Worker · ours, source private

ship

One command from a file or folder to a link. Three access tiers, enforced at the edge, nothing public unless you say so. It replaced a $30/month Surge subscription with a free R2 bucket.

what it does

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>.

how to run it

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

storageR2 bucket; keys <prefix>/<slug>/… where prefix ∈ p | thd | to
servingone Worker on files.<domain>/*: path → key passthrough, content-types, 404 and 410
accesstwo 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.
expiryKV 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.

what breaks

Known edges

  • Access gates authentication, not delivery. An --external link 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 .md was stored as application/octet-stream and 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.