experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(permissions): extract resolveWithStored — pure + unit-testable

The interaction between policy and stored decisions had a non-obvious
invariant ("stored decisions only override 'prompt'") that lived
inline in `permission-handler.ts`'s setPermissionRequestHandler
callback. That made it integration-testable but not unit-testable —
the only coverage for "stored allow can never override hard-deny"
was a Playwright run.

Extracted into `permission-policy.ts:resolveWithStored(url, permission,
lookupStored)`:

* Takes the same URL + permission as `resolveDecision`, plus a
pure callback that returns `boolean | null` for the stored
decision (no module-scoped state, no electron deps).
* Returns the FINAL `Decision` the request handler should act on.
* Skips the lookup entirely for hard-allow / hard-deny / empty
origin paths.

`permission-handler.ts` is now a one-line call into this helper +
the existing logging + callback + UI surface.

Unit coverage (`tests/unit/permission-handler.test.js` 22/22, +9):

* chrome-extension origin: stored deny is ignored (policy allow
wins).
* peek:// origin: stored deny is ignored (policy allow wins).
* hard-deny permission (e.g. usb): stored allow is ignored
(policy deny wins).
* prompt + stored allow: returns allow.
* prompt + stored deny: returns deny.
* prompt + no stored: returns prompt.
* prompt + empty origin: skips lookup (asserts the lookup
callback was NOT called), returns prompt.
* lookup is only called for prompt decisions (hard-allow + hard-
deny paths skip it — proven by tracking call count).
* lookup receives the parsed origin (not the full URL) and the
raw permission name.

Integration regression: permission-prompt 6/6 still green.

+135 -17
+9 -17
backend/electron/permission-handler.ts
··· 32 32 import { findHostWindowId } from './webview-guest-registry.js'; 33 33 import { 34 34 resolveDecision, 35 + resolveWithStored, 35 36 originFromUrl, 36 37 labelFor, 37 38 type PermissionName, ··· 84 85 (details && (details as { requestingUrl?: string }).requestingUrl) || 85 86 webContents?.getURL?.() || 86 87 ''; 87 - const decision = resolveDecision(requestingUrl, permission as PermissionName); 88 - 89 - // Stored per-origin decision (Phase 3) overrides 'prompt' — but never 90 - // overrides 'deny' or 'allow' from the policy. A site that's hard-allowed 91 - // (chrome-extension://, peek://) never gets a stale stored deny applied; 92 - // a site that's hard-denied (e.g. unknown permission) never gets a stale 93 - // stored allow. 94 - if (decision === 'prompt') { 95 - const stored = lookupStoredDecision(originFromUrl(requestingUrl), permission as PermissionName); 96 - if (stored !== null) { 97 - console.log( 98 - `[permission] ${permission} requested by ${requestingUrl || '(unknown)'} → ${stored ? 'allow' : 'deny'} (stored)`, 99 - ); 100 - callback(stored); 101 - return; 102 - } 103 - } 88 + // Combined policy + stored-decision resolution. See 89 + // permission-policy.ts:resolveWithStored for the invariant — stored 90 + // decisions only override 'prompt'. 91 + const decision = resolveWithStored( 92 + requestingUrl, 93 + permission as PermissionName, 94 + lookupStoredDecision, 95 + ); 104 96 105 97 console.log( 106 98 `[permission] ${permission} requested by ${requestingUrl || '(unknown)'} → ${decision}`,
+34
backend/electron/permission-policy.ts
··· 87 87 return DEFAULT_POLICY[permission] ?? 'deny'; 88 88 } 89 89 90 + /** 91 + * Combined policy + stored-decision resolver. 92 + * 93 + * Returns the FINAL decision the request handler should act on, after 94 + * consulting: 95 + * 1. The policy (`resolveDecision`). 96 + * 2. The user's stored per-origin decision, if any. 97 + * 98 + * Critical invariant: stored decisions only override `'prompt'`. They never 99 + * override `'allow'` (chrome-extension://, peek://) or `'deny'` (a permission 100 + * the policy has explicitly fail-closed). A site that's hard-allowed by 101 + * policy can never have a stale stored deny take effect; a site that's 102 + * hard-denied can never have a stale stored allow take effect. 103 + * 104 + * The `lookupStored` callback returns `boolean | null`: true for a stored 105 + * allow, false for a stored deny, null if no decision is stored. Pure 106 + * function — no side effects, no side-channel state — so it's trivially 107 + * unit-testable. 108 + */ 109 + export function resolveWithStored( 110 + url: string, 111 + permission: PermissionName, 112 + lookupStored: (origin: string, permission: PermissionName) => boolean | null, 113 + ): Decision { 114 + const policy = resolveDecision(url, permission); 115 + if (policy !== 'prompt') return policy; 116 + const origin = originFromUrl(url); 117 + if (!origin || origin === '(unknown)') return 'prompt'; 118 + const stored = lookupStored(origin, permission); 119 + if (stored === true) return 'allow'; 120 + if (stored === false) return 'deny'; 121 + return 'prompt'; 122 + } 123 + 90 124 export function originFromUrl(url: string): string { 91 125 try { 92 126 return new URL(url).origin;
+92
tests/unit/permission-handler.test.js
··· 12 12 13 13 import { 14 14 resolveDecision as resolve, 15 + resolveWithStored, 15 16 } from '../../dist/backend/electron/permission-policy.js'; 16 17 17 18 describe('permission-handler: chrome-extension origins', () => { ··· 97 98 assert.equal(resolve('', 'geolocation'), 'prompt'); 98 99 }); 99 100 }); 101 + 102 + describe('permission-handler: resolveWithStored — policy + stored-decision interaction', () => { 103 + // Hard policy decisions ('allow', 'deny') must NEVER be overridden by a 104 + // stored decision. Otherwise a stale stored allow could keep granting 105 + // permissions to a permission type we just fail-closed in code. 106 + const lookupAlwaysAllow = () => true; 107 + const lookupAlwaysDeny = () => false; 108 + const lookupNothing = () => null; 109 + 110 + it('chrome-extension: stored deny is ignored — policy allow wins', () => { 111 + assert.equal( 112 + resolveWithStored('chrome-extension://abc/popup.html', 'geolocation', lookupAlwaysDeny), 113 + 'allow', 114 + ); 115 + }); 116 + 117 + it('peek://: stored deny is ignored — policy allow wins', () => { 118 + assert.equal( 119 + resolveWithStored('peek://app/page/index.html', 'clipboard-read', lookupAlwaysDeny), 120 + 'allow', 121 + ); 122 + }); 123 + 124 + it('hard-deny permission (e.g. usb): stored allow is ignored — policy deny wins', () => { 125 + assert.equal( 126 + resolveWithStored('https://example.com', 'usb', lookupAlwaysAllow), 127 + 'deny', 128 + ); 129 + }); 130 + 131 + it('prompt permission with stored allow: returns allow', () => { 132 + assert.equal( 133 + resolveWithStored('https://example.com', 'geolocation', lookupAlwaysAllow), 134 + 'allow', 135 + ); 136 + }); 137 + 138 + it('prompt permission with stored deny: returns deny', () => { 139 + assert.equal( 140 + resolveWithStored('https://example.com', 'geolocation', lookupAlwaysDeny), 141 + 'deny', 142 + ); 143 + }); 144 + 145 + it('prompt permission with no stored decision: returns prompt', () => { 146 + assert.equal( 147 + resolveWithStored('https://example.com', 'geolocation', lookupNothing), 148 + 'prompt', 149 + ); 150 + }); 151 + 152 + it('prompt permission with empty origin: skips lookup, returns prompt', () => { 153 + // No origin to attribute a stored decision to — there'd be no useful 154 + // identity to key off, so we don't even consult the lookup. 155 + let lookupCalled = false; 156 + const tracker = () => { lookupCalled = true; return true; }; 157 + const decision = resolveWithStored('', 'geolocation', tracker); 158 + assert.equal(decision, 'prompt'); 159 + assert.equal(lookupCalled, false, 'should not look up stored decision for empty origin'); 160 + }); 161 + 162 + it('lookup is only called for prompt decisions (not allow/deny)', () => { 163 + let lookupCalls = 0; 164 + const tracker = () => { lookupCalls++; return null; }; 165 + 166 + // Hard allow path — lookup should NOT fire. 167 + resolveWithStored('chrome-extension://abc/popup.html', 'geolocation', tracker); 168 + assert.equal(lookupCalls, 0); 169 + 170 + // Hard deny path — lookup should NOT fire. 171 + resolveWithStored('https://example.com', 'usb', tracker); 172 + assert.equal(lookupCalls, 0); 173 + 174 + // Prompt path — lookup SHOULD fire. 175 + resolveWithStored('https://example.com', 'geolocation', tracker); 176 + assert.equal(lookupCalls, 1); 177 + }); 178 + 179 + it('lookup receives the parsed origin (not the full URL)', () => { 180 + let receivedOrigin = null; 181 + let receivedPermission = null; 182 + const capturer = (origin, permission) => { 183 + receivedOrigin = origin; 184 + receivedPermission = permission; 185 + return null; 186 + }; 187 + resolveWithStored('https://example.com/some/deep/path?query=1', 'geolocation', capturer); 188 + assert.equal(receivedOrigin, 'https://example.com'); 189 + assert.equal(receivedPermission, 'geolocation'); 190 + }); 191 + });