Strong shares phase 2: recipient viewer with vendored age (#19) #115

Merged
Cordy merged 9 commits from feat/strong-shares-ui into main 2026-08-04 21:22:55 +00:00
Owner

Phase 2 of #19: the recipient now gets a normal, readable file instead of a .age blob.

Viewer. /s/<token> on a strong file share serves a small page that fetches ?payload=1 (the ciphertext minted by phase 1), decrypts it in the browser, and hands over the plain file. Key material never reaches the server: fragment mode reads location.hash locally (browsers don't transmit fragments); password mode keeps the typed passphrase in a JS variable — it goes to the gate, which needs it anyway to authorize the download and derive the payload key.

Vendored age implementation — decision recorded. In-browser decryption needs an age implementation, and we have no JS build pipeline. Approved approach: a pinned, self-contained esbuild bundle of typage (Filippo's official TypeScript age — same author as filippo.io/age, crypto primitives are @noble/*, all inlined) committed to internal/share/assets/age.js and embedded in the binary via go:embed, served from /s/_assets/age.js.

  • The first attempt used esm.sh's ?bundle, which returned a shim still importing from the CDN — a self-containment check in the vendoring recipe caught it and failed the build. The committed artifact is now verified to contain no external imports.
  • Provenance, version, byte count, sha256, build date and the update procedure are in a header comment on the file; the recipe lives in the workflow history.
  • Rationale, in the file: the share viewer must work air-gapped and must never put a third-party origin in the request path of a page that handles a recipient's decryption key.
  • README caveat needed (phase 3): the "four third-party dependencies" line should note this is one vendored UI asset alongside the Go deps.

Gate changes. The password gate answers JSON (204/401) when the viewer's fetch asks for it, and strong shares render the viewer before the gate so the password can be collected in-page. Directory shares keep the server-rendered form; only file shares get the viewer.

Dialog. Share creation now carries the selected mode when a sharedlg-mode control exists (the control itself, plus showing the fragment link, is the remaining UI slice).

Full suite green. Next: phase 3 — dialog mode selector + handbook page with the claim boundary, then a dogfood pass on .249.

Phase 2 of #19: the recipient now gets a normal, readable file instead of a `.age` blob. **Viewer.** `/s/<token>` on a strong file share serves a small page that fetches `?payload=1` (the ciphertext minted by phase 1), decrypts it **in the browser**, and hands over the plain file. Key material never reaches the server: fragment mode reads `location.hash` locally (browsers don't transmit fragments); password mode keeps the typed passphrase in a JS variable — it goes to the gate, which needs it anyway to authorize the download and derive the payload key. **Vendored age implementation — decision recorded.** In-browser decryption needs an age implementation, and we have no JS build pipeline. Approved approach: a **pinned, self-contained esbuild bundle of typage** (Filippo's official TypeScript age — same author as `filippo.io/age`, crypto primitives are `@noble/*`, all inlined) committed to `internal/share/assets/age.js` and embedded in the binary via `go:embed`, served from `/s/_assets/age.js`. - The first attempt used esm.sh's `?bundle`, which returned a **shim still importing from the CDN** — a self-containment check in the vendoring recipe caught it and failed the build. The committed artifact is now verified to contain no external imports. - Provenance, version, byte count, sha256, build date and the update procedure are in a header comment on the file; the recipe lives in the workflow history. - Rationale, in the file: the share viewer must work air-gapped and must never put a third-party origin in the request path of a page that handles a recipient's decryption key. - **README caveat needed** (phase 3): the "four third-party dependencies" line should note this is one vendored UI asset alongside the Go deps. **Gate changes.** The password gate answers JSON (`204`/`401`) when the viewer's `fetch` asks for it, and strong shares render the viewer *before* the gate so the password can be collected in-page. Directory shares keep the server-rendered form; only file shares get the viewer. **Dialog.** Share creation now carries the selected mode when a `sharedlg-mode` control exists (the control itself, plus showing the fragment link, is the remaining UI slice). Full suite green. Next: phase 3 — dialog mode selector + handbook page with the claim boundary, then a dogfood pass on `.249`.
Cordy merged commit fae6eaa8b5 into main 2026-08-04 21:22:55 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#115
No description provided.