Files
platform/docs/http-secure-context-audit.md
pastilhasandClaude Opus 5 cd209483e3 fix the clipboard over http, and audit the rest
navigator.clipboard is secure-context only, like crypto.randomUUID before it —
over plain http on a tailnet address the object does not exist. Twenty call
sites across eighteen files, in three states that all looked fine in review:
bare calls that threw and killed the handler, optional-chained calls that
silently did nothing, and one carrying the comment "Officer is always behind
HTTPS", which it is not.

The optional-chained ones are the worst of the three: a copy button that reports
success and copies nothing is indistinguishable from a working one until someone
pastes.

helpers/clipboard.ts falls back to document.execCommand('copy') over an
off-screen textarea — deprecated, and it works on any origin because it predates
the secure-context rule. Off-screen rather than hidden, because display:none and
visibility:hidden elements cannot be selected and the copy fails silently.

Reading the clipboard has no equivalent: execCommand('paste') was never permitted
from script. The file browser's paste-a-file path now checks canReadClipboard()
and explains itself instead of throwing.

docs/http-secure-context-audit.md is the full sweep the owner asked for: what was
fixed, what cannot be, and what was checked and found clear. crypto.subtle is
used nowhere in the frontend, which was the one worth confirming since it has no
cheap fallback. Notification's six matches are type names, not the API.
geolocation and navigator.share are already guarded. getUserMedia is in four
files and is being removed — but QrTransfer uses it for the CAMERA, not a
microphone, so "remove audio" does not cover it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 03:56:57 +00:00

4.1 KiB

What breaks over plain http

Audited 2026-08-13, after crypto.randomUUID took the chat page down at the end of every turn.

Officer is reached at http://officer-dev:9000 — a tailnet address, so neither https nor localhost, and therefore not a secure context. A set of browser APIs are unavailable there by specification, not by policy, and there is no flag that changes it.

The failure mode is what makes this worth a document. Two of the three shapes below are silent:

shape what a user sees
crypto.randomUUID() TypeError — and if it is inside a useState initialiser, the whole tree unmounts
navigator.clipboard.writeText() TypeError, killing the click handler
navigator.clipboard?.writeText() nothing at all — the button reports success and copies nothing

The optional-chained one is the worst: indistinguishable from working until somebody pastes.


Fixed

crypto.randomUUID — 18 call sites

Secure-context only. crypto.getRandomValues is not — it lives on Crypto rather than SubtleCrypto — so helpers/random-id.ts builds the same v4 UUID from the same CSPRNG when randomUUID is absent. Same entropy, same version and variant bits.

navigator.clipboard.writeText — 20 call sites across 18 files

Secure-context only. helpers/clipboard.ts falls back to document.execCommand('copy') over an off-screen textarea, which predates the secure-context rule and works on any origin. Deprecated and working beats modern and absent.

One call site carried the comment "Officer is always behind HTTPS". It was not.


Cannot be fixed this way

navigator.clipboard.read() — pasting a file in the file browser

No fallback exists. document.execCommand('paste') was never permitted from script, so on an insecure origin there is no way to pull clipboard contents on demand — only a real paste event the user initiates, which is a different interaction. Now guarded by canReadClipboard() and refuses with an explanation instead of throwing.

getUserMedia — audio recording, 4 files

apps/Chat/useAudioRecording.ts, apps/FileBrowser/.../DictateDialog.tsx, apps/QrTransfer/Receiver.tsx, and a test. Requires a secure context and cannot be polyfilled — the browser will not hand out a microphone or camera over http.

Being removed rather than guarded: the owner uses an external dictation app. Note QrTransfer uses it for the CAMERA rather than a microphone, so removing "audio" does not cover it — that one needs its own decision.

navigator.credentials — passkeys

WebAuthn is secure-context only. helpers/passkeys.ts exists and cannot work over http, whatever is done to it. Not currently reachable, so nothing is broken today.


Checked and clear

  • crypto.subtle — not used anywhere in the frontend. This was the one worth confirming, since it would have had no cheap fallback.
  • Notification — the six matches are type names, not the browser API. Nothing calls new Notification or requestPermission.
  • Service workers, WebUSB, WebSerial, WebBluetooth, Payment Request, Wake Lock, Storage Manager, SharedArrayBuffer — not used.
  • navigator.geolocation (widgets/Weather) — secure-context only, but already guarded with if (!navigator.geolocation) return;, so it degrades rather than throws. The widget simply cannot locate you over http.
  • navigator.share (Headscale/InvitesView) — already guarded with a typeof check, and its comment notes it is absent on desktop browsers anyway.
  • WebSockets, IndexedDB, localStorage, EventSource — no secure-context restriction. Chat, terminal and the sidecar transports are unaffected.

The alternative

All of this disappears with a certificate, and tailscale cert issues a real one for the MagicDNS name in about one command — no public DNS, no port 80 challenge, no renewal to remember. Worth knowing that the choice here was "make it work over http", not "http is the only option".