feat(permissions): per-permission policy — Phase 1 (visibility + default-deny for unknown)
Centralizes Chromium permission request handling on the profile session
in a new `backend/electron/permission-handler.ts` module. Replaces the
blanket-allow that `chrome-extensions.ts` had installed on the *entire*
profile session — that handler was intended for chrome extensions but
also auto-allowed every webview guest's geolocation, media, notifications,
midi, etc., requests with no user awareness or audit trail.
Phase 1 (this commit) is visibility-only — no UI prompt yet — but it:
* Logs every permission request to stderr with `origin → permission →
decision` so we can see what real-world sites are asking for before
designing the prompt UI.
* Splits policy by origin class:
chrome-extension:// → allow (preserves prior behavior; extensions
declare permissions in manifest.json)
peek:// → allow (we control these pages)
everything else → consult DEFAULT_POLICY map per permission
* Defaults DEFAULT_POLICY entries to 'allow' for the permissions the
blanket handler used to allow (`geolocation`, `media`, `notifications`,
`midi`, `clipboard-*`, `display-capture`, `pointerLock`, `fullscreen`,
`openExternal`) so no live site regresses on rollout.
* Fail-closed for permissions with no policy entry (`hid`, `serial`,
`usb`, `idle-detection`, the various background/sync APIs, etc.) —
these previously got the blanket allow; now they're denied unless we
deliberately add an entry. Defensible default for permissions we
haven't audited.
The module is shaped for Phase 2: a `pendingRequests` map is already
wired in for the deferred-callback flow, and `'prompt'` is a valid
`Decision` that the request handler will route through the (not-yet-
written) IPC bridge once the UI lands. If a future policy entry is set
to `'prompt'` before the UI ships, the handler logs a warning and
denies — never leaves a page silently waiting forever.
`installPermissionHandler(profileSession)` is invoked exactly once from
`session-partition.ts` when the profile session is first created. The
chrome-extensions.ts code path no longer installs its own handler;
removed alongside a comment explaining where the handler now lives.
Tests:
* `tests/unit/permission-handler.test.js` (10/10) exercises the pure
`_resolveDecisionForTests(url, permission)` resolver: chrome-extension
URLs always allow; peek:// always allow; web origins allow the
Phase-1 default set + deny unknown/risky; empty origin documents the
current allow-by-default behavior so when Phase 2 flips geolocation
to 'prompt' the assertion changes too.
* unit total: 638/638 (was 628 + 10 new).
`Session` is now a `import type` — the test imports `dist/.../permission-
handler.js` under ELECTRON_RUN_AS_NODE without dragging in `electron`.
Tasks doc updated with Phase 2/3/4 follow-on work spelled out.