# 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". [secure context]: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts