standdown 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ import { verifyPolicyBundle } from './chunk-25A7QG5Z.mjs';
2
+ import { ChromeLocalStateStore } from './chunk-SEZFAOUU.mjs';
3
+ export { ChromeLocalStateStore } from './chunk-SEZFAOUU.mjs';
4
+ import { StanddownSession } from './chunk-SVN6UWA4.mjs';
5
+
6
+ // src/webext.ts
7
+ var QUERY_MESSAGE_TYPE = "standdown:shouldStandDown";
8
+ var FAIL_CLOSED_BEHAVIORS = [
9
+ "suppress-prompts",
10
+ "no-cookie-write",
11
+ "no-redirect",
12
+ "no-background-tracking"
13
+ ];
14
+ var DEFAULT_REFRESH_INTERVAL_MS = 60 * 60 * 1e3;
15
+ var MAX_ADAPTER_AUDIT_ENTRIES = 1e3;
16
+ function createStanddown(opts) {
17
+ const chromeApi = opts.chrome ?? currentChrome();
18
+ const now = opts.now ?? Date.now;
19
+ const fetchImpl = opts.fetch ?? currentFetch();
20
+ let activePolicies = opts.policies.map((policy) => clonePolicy(policy));
21
+ let refreshTimer;
22
+ const store = opts.store ?? createChromeStore(chromeApi, {
23
+ sessionId: opts.sessionId,
24
+ now
25
+ });
26
+ const session = new StanddownSession(
27
+ store,
28
+ opts.auditLog === void 0 ? void 0 : { auditLog: opts.auditLog }
29
+ );
30
+ const redirectChains = /* @__PURE__ */ new Map();
31
+ const redirectRequestIds = /* @__PURE__ */ new Map();
32
+ const initiators = /* @__PURE__ */ new Map();
33
+ const tabHosts = /* @__PURE__ */ new Map();
34
+ const hasWebRequest = typeof chromeApi?.webRequest?.onBeforeRequest?.addListener === "function";
35
+ const hasWebNavigation = typeof chromeApi?.webNavigation?.onCommitted?.addListener === "function";
36
+ if (!hasWebNavigation) {
37
+ throw new TypeError(
38
+ "standdown/webext requires chrome.webNavigation.onCommitted"
39
+ );
40
+ }
41
+ const mode = hasWebRequest ? "webRequest" : "webNavigation";
42
+ const onBeforeRequest = (details) => {
43
+ if (!isTopLevelTabRequest(details.tabId, details.type, details.frameId)) {
44
+ return;
45
+ }
46
+ const existingRequestId = redirectRequestIds.get(details.tabId);
47
+ const startsNewRequest = details.requestId !== void 0 && existingRequestId !== void 0 && details.requestId !== existingRequestId;
48
+ const chain = startsNewRequest ? [] : redirectChains.get(details.tabId) ?? [];
49
+ if (details.requestId !== void 0) {
50
+ redirectRequestIds.set(details.tabId, details.requestId);
51
+ }
52
+ chain.push(details.url);
53
+ redirectChains.set(details.tabId, chain.slice(-20));
54
+ const initiator = details.initiator ?? details.originUrl;
55
+ if (initiator !== void 0 && !initiators.has(details.tabId)) {
56
+ initiators.set(details.tabId, initiator);
57
+ }
58
+ };
59
+ const onCommitted = (details) => {
60
+ if (details.tabId < 0 || (details.frameId ?? 0) !== 0) {
61
+ return;
62
+ }
63
+ void ingestNavigation(details.tabId, details.url);
64
+ };
65
+ const onRemoved = (tabId) => {
66
+ redirectChains.delete(tabId);
67
+ redirectRequestIds.delete(tabId);
68
+ initiators.delete(tabId);
69
+ tabHosts.delete(tabId);
70
+ };
71
+ const onMessage = (message, sender, sendResponse) => {
72
+ if (!isShouldStandDownMessage(message)) {
73
+ return void 0;
74
+ }
75
+ void handleMessage(message, sender).then(sendResponse, () => {
76
+ sendResponse({
77
+ ok: false,
78
+ decision: failClosedDecision("message-handler-error")
79
+ });
80
+ });
81
+ return true;
82
+ };
83
+ if (mode === "webRequest") {
84
+ chromeApi?.webRequest?.onBeforeRequest?.addListener(
85
+ onBeforeRequest,
86
+ { urls: ["<all_urls>"], types: ["main_frame"] }
87
+ );
88
+ }
89
+ if (hasWebNavigation) {
90
+ chromeApi?.webNavigation?.onCommitted?.addListener(onCommitted);
91
+ }
92
+ chromeApi?.runtime?.onMessage?.addListener(onMessage);
93
+ chromeApi?.tabs?.onRemoved?.addListener(onRemoved);
94
+ if (opts.refresh !== void 0) {
95
+ if (opts.refresh.intervalMs !== 0) {
96
+ refreshTimer = setInterval(
97
+ () => {
98
+ void refreshNow();
99
+ },
100
+ opts.refresh.intervalMs ?? DEFAULT_REFRESH_INTERVAL_MS
101
+ );
102
+ }
103
+ }
104
+ async function ingestNavigation(tabId, url) {
105
+ const host = hostFromUrl(url);
106
+ if (host !== void 0) {
107
+ tabHosts.set(tabId, host);
108
+ }
109
+ const redirectChain = mode === "webRequest" ? redirectChains.get(tabId) : void 0;
110
+ const initiator = initiators.get(tabId);
111
+ redirectChains.delete(tabId);
112
+ redirectRequestIds.delete(tabId);
113
+ initiators.delete(tabId);
114
+ const signals = navigationSignals({
115
+ url,
116
+ now: now(),
117
+ redirectChain,
118
+ initiator,
119
+ selfPatterns: opts.selfPatterns,
120
+ publisherSites: opts.publisherSites
121
+ });
122
+ return session.ingest(signals, activePolicies);
123
+ }
124
+ async function shouldStandDown(tabId, at = now()) {
125
+ const host = tabHosts.get(tabId) ?? await hostForTab(chromeApi, tabId);
126
+ if (host === void 0) {
127
+ return failClosedDecision("missing-tab-url");
128
+ }
129
+ return session.shouldStandDown(host, at);
130
+ }
131
+ async function shouldStandDownForUrl(url, at = now()) {
132
+ const host = hostFromUrl(url);
133
+ if (host === void 0) {
134
+ return failClosedDecision("invalid-tab-url");
135
+ }
136
+ return session.shouldStandDown(host, at);
137
+ }
138
+ async function handleMessage(message, sender) {
139
+ if (!isShouldStandDownMessage(message)) {
140
+ return void 0;
141
+ }
142
+ const senderUrl = sender?.tab?.url ?? sender?.tab?.pendingUrl;
143
+ const decision = message.url !== void 0 ? await shouldStandDownForUrl(message.url) : senderUrl !== void 0 ? await shouldStandDownForUrl(senderUrl) : await shouldStandDown(message.tabId ?? sender?.tab?.id ?? -1);
144
+ return { ok: true, decision };
145
+ }
146
+ async function refreshNow() {
147
+ if (opts.refresh === void 0) {
148
+ return { ok: false, violation: "refresh-not-configured" };
149
+ }
150
+ if (fetchImpl === void 0) {
151
+ const result = { ok: false, violation: "refresh-fetch-unavailable" };
152
+ await auditRefresh(store, now(), result);
153
+ return result;
154
+ }
155
+ try {
156
+ const response = await fetchImpl(opts.refresh.url);
157
+ if (!response.ok) {
158
+ const result2 = {
159
+ ok: false,
160
+ violation: `refresh-fetch-failed:${response.status}`
161
+ };
162
+ await auditRefresh(store, now(), result2);
163
+ return result2;
164
+ }
165
+ const bundle = await response.json();
166
+ const verified = await verifyPolicyBundle(
167
+ activePolicies,
168
+ bundle,
169
+ opts.refresh.publicKeyJwk
170
+ );
171
+ if (!verified.ok) {
172
+ const result2 = { ok: false, violation: verified.violation };
173
+ await auditRefresh(store, now(), result2);
174
+ return result2;
175
+ }
176
+ activePolicies = verified.policies.map((policy) => clonePolicy(policy));
177
+ const result = { ok: true, applied: activePolicies.length };
178
+ await auditRefresh(store, now(), result);
179
+ return result;
180
+ } catch (error) {
181
+ const result = {
182
+ ok: false,
183
+ violation: `refresh-error:${messageFromError(error)}`
184
+ };
185
+ await auditRefresh(store, now(), result);
186
+ return result;
187
+ }
188
+ }
189
+ function getPolicies() {
190
+ return activePolicies.map((policy) => clonePolicy(policy));
191
+ }
192
+ function dispose() {
193
+ if (refreshTimer !== void 0) {
194
+ clearInterval(refreshTimer);
195
+ }
196
+ chromeApi?.webRequest?.onBeforeRequest?.removeListener?.(onBeforeRequest);
197
+ chromeApi?.webNavigation?.onCommitted?.removeListener?.(onCommitted);
198
+ chromeApi?.runtime?.onMessage?.removeListener?.(onMessage);
199
+ chromeApi?.tabs?.onRemoved?.removeListener?.(onRemoved);
200
+ redirectChains.clear();
201
+ redirectRequestIds.clear();
202
+ initiators.clear();
203
+ tabHosts.clear();
204
+ }
205
+ return {
206
+ mode,
207
+ session,
208
+ store,
209
+ ingestNavigation,
210
+ shouldStandDown,
211
+ shouldStandDownForUrl,
212
+ refreshNow,
213
+ getPolicies,
214
+ handleMessage,
215
+ dispose
216
+ };
217
+ }
218
+ function createChromeStore(chromeApi, opts) {
219
+ if (chromeApi?.storage?.local === void 0) {
220
+ return new UnavailableStateStore();
221
+ }
222
+ const storeOptions = { now: opts.now };
223
+ if (chromeApi.runtime !== void 0) {
224
+ storeOptions.runtime = chromeApi.runtime;
225
+ }
226
+ if (chromeApi.storage.session !== void 0) {
227
+ storeOptions.sessionStorage = chromeApi.storage.session;
228
+ }
229
+ if (opts.sessionId !== void 0) {
230
+ storeOptions.sessionId = opts.sessionId;
231
+ }
232
+ return new ChromeLocalStateStore(chromeApi.storage.local, storeOptions);
233
+ }
234
+ function navigationSignals(value) {
235
+ const signals = {
236
+ url: value.url,
237
+ now: value.now
238
+ };
239
+ if (value.redirectChain !== void 0 && value.redirectChain.length > 0) {
240
+ signals.redirectChain = [...value.redirectChain];
241
+ }
242
+ if (value.initiator !== void 0) {
243
+ signals.initiator = value.initiator;
244
+ }
245
+ if (value.selfPatterns !== void 0) {
246
+ signals.selfPatterns = value.selfPatterns;
247
+ }
248
+ if (value.publisherSites !== void 0) {
249
+ signals.publisherSites = value.publisherSites;
250
+ }
251
+ return signals;
252
+ }
253
+ async function hostForTab(chromeApi, tabId) {
254
+ if (tabId < 0 || chromeApi?.tabs?.get === void 0) {
255
+ return void 0;
256
+ }
257
+ return new Promise((resolve) => {
258
+ try {
259
+ const result = chromeApi.tabs?.get?.(tabId, (tab) => {
260
+ resolve(hostFromUrl(tab?.url ?? tab?.pendingUrl));
261
+ });
262
+ if (isPromiseLike(result)) {
263
+ result.then(
264
+ (tab) => resolve(hostFromUrl(tab?.url ?? tab?.pendingUrl)),
265
+ () => resolve(void 0)
266
+ );
267
+ }
268
+ } catch {
269
+ resolve(void 0);
270
+ }
271
+ });
272
+ }
273
+ function isTopLevelTabRequest(tabId, type, frameId) {
274
+ return tabId >= 0 && (type === void 0 || type === "main_frame") && (frameId ?? 0) === 0;
275
+ }
276
+ function isShouldStandDownMessage(message) {
277
+ return typeof message === "object" && message !== null && message.type === QUERY_MESSAGE_TYPE;
278
+ }
279
+ function hostFromUrl(value) {
280
+ if (value === void 0) {
281
+ return void 0;
282
+ }
283
+ try {
284
+ return new URL(value).hostname.toLowerCase().replace(/\.$/, "");
285
+ } catch {
286
+ return void 0;
287
+ }
288
+ }
289
+ function failClosedDecision(reason) {
290
+ return {
291
+ standDown: true,
292
+ reason,
293
+ behaviors: [...FAIL_CLOSED_BEHAVIORS]
294
+ };
295
+ }
296
+ function currentChrome() {
297
+ return globalThis.chrome;
298
+ }
299
+ function currentFetch() {
300
+ const fetchValue = globalThis.fetch;
301
+ return typeof fetchValue === "function" ? fetchValue : void 0;
302
+ }
303
+ async function auditRefresh(store, time, result) {
304
+ const state = await store.load() ?? emptyState();
305
+ const entry = {
306
+ time,
307
+ action: "refresh",
308
+ decision: {
309
+ standDown: false,
310
+ reason: result.ok ? `refresh-applied:${result.applied}` : `refresh-rejected:${result.violation}`,
311
+ behaviors: []
312
+ }
313
+ };
314
+ state.auditLog.push(entry);
315
+ if (state.auditLog.length > MAX_ADAPTER_AUDIT_ENTRIES) {
316
+ state.auditLog.splice(0, state.auditLog.length - MAX_ADAPTER_AUDIT_ENTRIES);
317
+ }
318
+ await store.save(state);
319
+ }
320
+ function emptyState() {
321
+ return {
322
+ sessions: {},
323
+ auditLog: []
324
+ };
325
+ }
326
+ function clonePolicy(policy) {
327
+ return JSON.parse(JSON.stringify(policy));
328
+ }
329
+ function isPromiseLike(value) {
330
+ return typeof value === "object" && value !== null && typeof value.then === "function";
331
+ }
332
+ function messageFromError(error) {
333
+ return error instanceof Error ? error.message : String(error);
334
+ }
335
+ var UnavailableStateStore = class {
336
+ async load() {
337
+ throw new Error("chrome.storage.local unavailable");
338
+ }
339
+ async save() {
340
+ throw new Error("chrome.storage.local unavailable");
341
+ }
342
+ };
343
+
344
+ export { createStanddown };
345
+ //# sourceMappingURL=webext.mjs.map
346
+ //# sourceMappingURL=webext.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/webext.ts"],"names":["result"],"mappings":"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","file":"webext.mjs","sourcesContent":["import { verifyPolicyBundle } from './bundle';\nimport { StanddownSession } from './session';\nimport {\n ChromeLocalStateStore,\n type ChromeRuntimeLike,\n type ChromeStorageAreaLike,\n} from './stores';\nimport type {\n AuditEntry,\n Behavior,\n Decision,\n SelfExemption,\n Signals,\n SignedPolicyBundle,\n StanddownPolicy,\n StanddownState,\n StateStore,\n} from './types';\n\nexport {\n ChromeLocalStateStore,\n type ChromeRuntimeLike,\n type ChromeStorageAreaLike,\n} from './stores';\n\nconst QUERY_MESSAGE_TYPE = 'standdown:shouldStandDown';\nconst FAIL_CLOSED_BEHAVIORS = [\n 'suppress-prompts',\n 'no-cookie-write',\n 'no-redirect',\n 'no-background-tracking',\n] as const satisfies readonly Behavior[];\nconst DEFAULT_REFRESH_INTERVAL_MS = 60 * 60 * 1_000;\nconst MAX_ADAPTER_AUDIT_ENTRIES = 1_000;\n\nexport type StanddownWebextMode = 'webRequest' | 'webNavigation';\n\nexport interface StanddownRefreshConfig {\n readonly url: string;\n readonly publicKeyJwk: JsonWebKey;\n /** Set to 0 to disable the periodic timer while keeping refreshNow(). */\n readonly intervalMs?: number;\n}\n\nexport interface CreateStanddownOptions {\n readonly policies: readonly StanddownPolicy[];\n readonly selfPatterns?: readonly SelfExemption[];\n readonly publisherSites?: readonly string[];\n readonly refresh?: StanddownRefreshConfig;\n readonly fetch?: FetchLike;\n readonly chrome?: ChromeLike;\n readonly store?: StateStore;\n readonly sessionId?: string;\n readonly now?: () => number;\n readonly auditLog?: boolean;\n}\n\nexport interface StanddownWebextController {\n readonly mode: StanddownWebextMode;\n readonly session: StanddownSession;\n readonly store: StateStore;\n ingestNavigation(tabId: number, url: string): Promise<Decision>;\n shouldStandDown(tabId: number, now?: number): Promise<Decision>;\n shouldStandDownForUrl(url: string, now?: number): Promise<Decision>;\n refreshNow(): Promise<StanddownRefreshResult>;\n getPolicies(): readonly StanddownPolicy[];\n handleMessage(\n message: unknown,\n sender?: RuntimeMessageSenderLike,\n ): Promise<StanddownMessageResponse | undefined>;\n dispose(): void;\n}\n\nexport interface StanddownShouldStandDownMessage {\n readonly type: typeof QUERY_MESSAGE_TYPE;\n readonly tabId?: number;\n readonly url?: string;\n}\n\nexport interface StanddownMessageResponse {\n readonly ok: boolean;\n readonly decision: Decision;\n}\n\nexport type StanddownRefreshResult =\n | { ok: true; applied: number }\n | { ok: false; violation: string };\n\nexport type FetchLike = (url: string) => Promise<{\n ok: boolean;\n status: number;\n json(): Promise<unknown>;\n}>;\n\nexport interface ChromeLike {\n readonly webRequest?: {\n readonly onBeforeRequest?: ChromeEventLike<WebRequestBeforeListener>;\n };\n readonly webNavigation?: {\n readonly onCommitted?: ChromeEventLike<WebNavigationCommittedListener>;\n };\n readonly runtime?: ChromeRuntimeLike & {\n readonly onMessage?: ChromeEventLike<RuntimeMessageListener>;\n };\n readonly storage?: {\n readonly local?: ChromeStorageAreaLike;\n readonly session?: ChromeStorageAreaLike;\n };\n readonly tabs?: {\n get?(\n tabId: number,\n callback?: (tab: TabLike | undefined) => void,\n ): undefined | Promise<TabLike | undefined>;\n readonly onRemoved?: ChromeEventLike<TabRemovedListener>;\n };\n}\n\nexport interface ChromeEventLike<TListener> {\n addListener(listener: TListener, ...args: unknown[]): void;\n removeListener?(listener: TListener): void;\n}\n\nexport interface WebRequestBeforeDetails {\n readonly tabId: number;\n readonly requestId?: string;\n readonly url: string;\n readonly type?: string;\n readonly frameId?: number;\n readonly initiator?: string;\n readonly originUrl?: string;\n}\n\nexport interface WebNavigationCommittedDetails {\n readonly tabId: number;\n readonly url: string;\n readonly frameId?: number;\n}\n\nexport interface RuntimeMessageSenderLike {\n readonly tab?: TabLike;\n}\n\nexport interface TabLike {\n readonly id?: number;\n readonly url?: string;\n readonly pendingUrl?: string;\n}\n\ntype WebRequestBeforeListener = (details: WebRequestBeforeDetails) => void;\ntype WebNavigationCommittedListener = (\n details: WebNavigationCommittedDetails,\n) => void;\ntype TabRemovedListener = (tabId: number) => void;\ntype RuntimeMessageListener = (\n message: unknown,\n sender: RuntimeMessageSenderLike,\n sendResponse: (response: StanddownMessageResponse | undefined) => void,\n) => boolean | undefined;\n\nexport function createStanddown(\n opts: CreateStanddownOptions,\n): StanddownWebextController {\n const chromeApi = opts.chrome ?? currentChrome();\n const now = opts.now ?? Date.now;\n const fetchImpl = opts.fetch ?? currentFetch();\n let activePolicies = opts.policies.map((policy) => clonePolicy(policy));\n let refreshTimer: ReturnType<typeof setInterval> | undefined;\n\n const store =\n opts.store ??\n createChromeStore(chromeApi, {\n sessionId: opts.sessionId,\n now,\n });\n const session = new StanddownSession(\n store,\n opts.auditLog === undefined ? undefined : { auditLog: opts.auditLog },\n );\n const redirectChains = new Map<number, string[]>();\n const redirectRequestIds = new Map<number, string>();\n const initiators = new Map<number, string>();\n const tabHosts = new Map<number, string>();\n const hasWebRequest =\n typeof chromeApi?.webRequest?.onBeforeRequest?.addListener === 'function';\n const hasWebNavigation =\n typeof chromeApi?.webNavigation?.onCommitted?.addListener === 'function';\n\n if (!hasWebNavigation) {\n throw new TypeError(\n 'standdown/webext requires chrome.webNavigation.onCommitted',\n );\n }\n\n const mode: StanddownWebextMode = hasWebRequest ? 'webRequest' : 'webNavigation';\n\n const onBeforeRequest: WebRequestBeforeListener = (details) => {\n if (!isTopLevelTabRequest(details.tabId, details.type, details.frameId)) {\n return;\n }\n\n const existingRequestId = redirectRequestIds.get(details.tabId);\n const startsNewRequest =\n details.requestId !== undefined &&\n existingRequestId !== undefined &&\n details.requestId !== existingRequestId;\n const chain = startsNewRequest\n ? []\n : redirectChains.get(details.tabId) ?? [];\n\n if (details.requestId !== undefined) {\n redirectRequestIds.set(details.tabId, details.requestId);\n }\n\n chain.push(details.url);\n redirectChains.set(details.tabId, chain.slice(-20));\n\n const initiator = details.initiator ?? details.originUrl;\n\n if (initiator !== undefined && !initiators.has(details.tabId)) {\n initiators.set(details.tabId, initiator);\n }\n };\n\n const onCommitted: WebNavigationCommittedListener = (details) => {\n if (details.tabId < 0 || (details.frameId ?? 0) !== 0) {\n return;\n }\n\n void ingestNavigation(details.tabId, details.url);\n };\n\n const onRemoved: TabRemovedListener = (tabId) => {\n redirectChains.delete(tabId);\n redirectRequestIds.delete(tabId);\n initiators.delete(tabId);\n tabHosts.delete(tabId);\n };\n\n const onMessage: RuntimeMessageListener = (message, sender, sendResponse) => {\n if (!isShouldStandDownMessage(message)) {\n return undefined;\n }\n\n void handleMessage(message, sender).then(sendResponse, () => {\n sendResponse({\n ok: false,\n decision: failClosedDecision('message-handler-error'),\n });\n });\n\n return true;\n };\n\n if (mode === 'webRequest') {\n chromeApi?.webRequest?.onBeforeRequest?.addListener(\n onBeforeRequest,\n { urls: ['<all_urls>'], types: ['main_frame'] },\n );\n }\n\n if (hasWebNavigation) {\n chromeApi?.webNavigation?.onCommitted?.addListener(onCommitted);\n }\n\n chromeApi?.runtime?.onMessage?.addListener(onMessage);\n chromeApi?.tabs?.onRemoved?.addListener(onRemoved);\n\n if (opts.refresh !== undefined) {\n if (opts.refresh.intervalMs !== 0) {\n refreshTimer = setInterval(\n () => {\n void refreshNow();\n },\n opts.refresh.intervalMs ?? DEFAULT_REFRESH_INTERVAL_MS,\n );\n }\n }\n\n async function ingestNavigation(\n tabId: number,\n url: string,\n ): Promise<Decision> {\n const host = hostFromUrl(url);\n\n if (host !== undefined) {\n tabHosts.set(tabId, host);\n }\n\n const redirectChain = mode === 'webRequest' ? redirectChains.get(tabId) : undefined;\n const initiator = initiators.get(tabId);\n\n redirectChains.delete(tabId);\n redirectRequestIds.delete(tabId);\n initiators.delete(tabId);\n\n const signals = navigationSignals({\n url,\n now: now(),\n redirectChain,\n initiator,\n selfPatterns: opts.selfPatterns,\n publisherSites: opts.publisherSites,\n });\n\n return session.ingest(signals, activePolicies);\n }\n\n async function shouldStandDown(\n tabId: number,\n at = now(),\n ): Promise<Decision> {\n const host = tabHosts.get(tabId) ?? (await hostForTab(chromeApi, tabId));\n\n if (host === undefined) {\n return failClosedDecision('missing-tab-url');\n }\n\n return session.shouldStandDown(host, at);\n }\n\n async function shouldStandDownForUrl(\n url: string,\n at = now(),\n ): Promise<Decision> {\n const host = hostFromUrl(url);\n\n if (host === undefined) {\n return failClosedDecision('invalid-tab-url');\n }\n\n return session.shouldStandDown(host, at);\n }\n\n async function handleMessage(\n message: unknown,\n sender?: RuntimeMessageSenderLike,\n ): Promise<StanddownMessageResponse | undefined> {\n if (!isShouldStandDownMessage(message)) {\n return undefined;\n }\n\n const senderUrl = sender?.tab?.url ?? sender?.tab?.pendingUrl;\n const decision =\n message.url !== undefined\n ? await shouldStandDownForUrl(message.url)\n : senderUrl !== undefined\n ? await shouldStandDownForUrl(senderUrl)\n : await shouldStandDown(message.tabId ?? sender?.tab?.id ?? -1);\n\n return { ok: true, decision };\n }\n\n async function refreshNow(): Promise<StanddownRefreshResult> {\n if (opts.refresh === undefined) {\n return { ok: false, violation: 'refresh-not-configured' };\n }\n\n if (fetchImpl === undefined) {\n const result = { ok: false, violation: 'refresh-fetch-unavailable' } as const;\n await auditRefresh(store, now(), result);\n return result;\n }\n\n try {\n const response = await fetchImpl(opts.refresh.url);\n\n if (!response.ok) {\n const result = {\n ok: false,\n violation: `refresh-fetch-failed:${response.status}`,\n } as const;\n await auditRefresh(store, now(), result);\n return result;\n }\n\n const bundle = (await response.json()) as SignedPolicyBundle;\n const verified = await verifyPolicyBundle(\n activePolicies,\n bundle,\n opts.refresh.publicKeyJwk,\n );\n\n if (!verified.ok) {\n const result = { ok: false, violation: verified.violation } as const;\n await auditRefresh(store, now(), result);\n return result;\n }\n\n activePolicies = verified.policies.map((policy) => clonePolicy(policy));\n const result = { ok: true, applied: activePolicies.length } as const;\n await auditRefresh(store, now(), result);\n return result;\n } catch (error) {\n const result = {\n ok: false,\n violation: `refresh-error:${messageFromError(error)}`,\n } as const;\n await auditRefresh(store, now(), result);\n return result;\n }\n }\n\n function getPolicies(): readonly StanddownPolicy[] {\n return activePolicies.map((policy) => clonePolicy(policy));\n }\n\n function dispose(): void {\n if (refreshTimer !== undefined) {\n clearInterval(refreshTimer);\n }\n\n chromeApi?.webRequest?.onBeforeRequest?.removeListener?.(onBeforeRequest);\n chromeApi?.webNavigation?.onCommitted?.removeListener?.(onCommitted);\n chromeApi?.runtime?.onMessage?.removeListener?.(onMessage);\n chromeApi?.tabs?.onRemoved?.removeListener?.(onRemoved);\n redirectChains.clear();\n redirectRequestIds.clear();\n initiators.clear();\n tabHosts.clear();\n }\n\n return {\n mode,\n session,\n store,\n ingestNavigation,\n shouldStandDown,\n shouldStandDownForUrl,\n refreshNow,\n getPolicies,\n handleMessage,\n dispose,\n };\n}\n\nfunction createChromeStore(\n chromeApi: ChromeLike | undefined,\n opts: { sessionId: string | undefined; now: () => number },\n): StateStore {\n if (chromeApi?.storage?.local === undefined) {\n return new UnavailableStateStore();\n }\n\n const storeOptions: {\n runtime?: ChromeRuntimeLike;\n sessionStorage?: ChromeStorageAreaLike;\n sessionId?: string;\n now: () => number;\n } = { now: opts.now };\n\n if (chromeApi.runtime !== undefined) {\n storeOptions.runtime = chromeApi.runtime;\n }\n\n if (chromeApi.storage.session !== undefined) {\n storeOptions.sessionStorage = chromeApi.storage.session;\n }\n\n if (opts.sessionId !== undefined) {\n storeOptions.sessionId = opts.sessionId;\n }\n\n return new ChromeLocalStateStore(chromeApi.storage.local, storeOptions);\n}\n\nfunction navigationSignals(value: {\n url: string;\n now: number;\n redirectChain: readonly string[] | undefined;\n initiator: string | undefined;\n selfPatterns: readonly SelfExemption[] | undefined;\n publisherSites: readonly string[] | undefined;\n}): Signals {\n const signals: Signals = {\n url: value.url,\n now: value.now,\n };\n\n if (value.redirectChain !== undefined && value.redirectChain.length > 0) {\n signals.redirectChain = [...value.redirectChain];\n }\n\n if (value.initiator !== undefined) {\n signals.initiator = value.initiator;\n }\n\n if (value.selfPatterns !== undefined) {\n signals.selfPatterns = value.selfPatterns;\n }\n\n if (value.publisherSites !== undefined) {\n signals.publisherSites = value.publisherSites;\n }\n\n return signals;\n}\n\nasync function hostForTab(\n chromeApi: ChromeLike | undefined,\n tabId: number,\n): Promise<string | undefined> {\n if (tabId < 0 || chromeApi?.tabs?.get === undefined) {\n return undefined;\n }\n\n return new Promise((resolve) => {\n try {\n const result = chromeApi.tabs?.get?.(tabId, (tab) => {\n resolve(hostFromUrl(tab?.url ?? tab?.pendingUrl));\n });\n\n if (isPromiseLike(result)) {\n result.then(\n (tab) => resolve(hostFromUrl(tab?.url ?? tab?.pendingUrl)),\n () => resolve(undefined),\n );\n }\n } catch {\n resolve(undefined);\n }\n });\n}\n\nfunction isTopLevelTabRequest(\n tabId: number,\n type: string | undefined,\n frameId: number | undefined,\n): boolean {\n return tabId >= 0 && (type === undefined || type === 'main_frame') && (frameId ?? 0) === 0;\n}\n\nfunction isShouldStandDownMessage(\n message: unknown,\n): message is StanddownShouldStandDownMessage {\n return (\n typeof message === 'object' &&\n message !== null &&\n (message as { type?: unknown }).type === QUERY_MESSAGE_TYPE\n );\n}\n\nfunction hostFromUrl(value: string | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n try {\n return new URL(value).hostname.toLowerCase().replace(/\\.$/, '');\n } catch {\n return undefined;\n }\n}\n\nfunction failClosedDecision(reason: string): Decision {\n return {\n standDown: true,\n reason,\n behaviors: [...FAIL_CLOSED_BEHAVIORS],\n };\n}\n\nfunction currentChrome(): ChromeLike | undefined {\n return (globalThis as typeof globalThis & { chrome?: ChromeLike }).chrome;\n}\n\nfunction currentFetch(): FetchLike | undefined {\n const fetchValue = (globalThis as typeof globalThis & { fetch?: FetchLike })\n .fetch;\n\n return typeof fetchValue === 'function' ? fetchValue : undefined;\n}\n\nasync function auditRefresh(\n store: StateStore,\n time: number,\n result: StanddownRefreshResult,\n): Promise<void> {\n const state = (await store.load()) ?? emptyState();\n const entry: AuditEntry = {\n time,\n action: 'refresh',\n decision: {\n standDown: false,\n reason: result.ok\n ? `refresh-applied:${result.applied}`\n : `refresh-rejected:${result.violation}`,\n behaviors: [],\n },\n };\n\n state.auditLog.push(entry);\n\n if (state.auditLog.length > MAX_ADAPTER_AUDIT_ENTRIES) {\n state.auditLog.splice(0, state.auditLog.length - MAX_ADAPTER_AUDIT_ENTRIES);\n }\n\n await store.save(state);\n}\n\nfunction emptyState(): StanddownState {\n return {\n sessions: {},\n auditLog: [],\n };\n}\n\nfunction clonePolicy(policy: StanddownPolicy): StanddownPolicy {\n return JSON.parse(JSON.stringify(policy)) as StanddownPolicy;\n}\n\nfunction isPromiseLike<T>(value: unknown): value is Promise<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { then?: unknown }).then === 'function'\n );\n}\n\nfunction messageFromError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nclass UnavailableStateStore implements StateStore {\n async load(): Promise<never> {\n throw new Error('chrome.storage.local unavailable');\n }\n\n async save(): Promise<never> {\n throw new Error('chrome.storage.local unavailable');\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "standdown",
3
+ "version": "0.1.0",
4
+ "description": "Affiliate stand-down, done right, for browser extensions.",
5
+ "author": "Dupe (https://dupe.com)",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/dupe-com/standdown.git"
9
+ },
10
+ "homepage": "https://github.com/dupe-com/standdown#readme",
11
+ "bugs": "https://github.com/dupe-com/standdown/issues",
12
+ "keywords": [
13
+ "affiliate",
14
+ "stand-down",
15
+ "standdown",
16
+ "browser-extension",
17
+ "compliance",
18
+ "attribution",
19
+ "mv3",
20
+ "affiliate-marketing",
21
+ "cookie-stuffing"
22
+ ],
23
+ "license": "MIT",
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE",
30
+ "POLICIES.md"
31
+ ],
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.mjs",
36
+ "require": "./dist/index.cjs"
37
+ },
38
+ "./policies": {
39
+ "types": "./dist/policies.d.ts",
40
+ "import": "./dist/policies.mjs",
41
+ "require": "./dist/policies.cjs"
42
+ },
43
+ "./webext": {
44
+ "types": "./dist/webext.d.ts",
45
+ "import": "./dist/webext.mjs",
46
+ "require": "./dist/webext.cjs"
47
+ },
48
+ "./content": {
49
+ "types": "./dist/content.d.ts",
50
+ "import": "./dist/content.mjs",
51
+ "require": "./dist/content.cjs"
52
+ },
53
+ "./package.json": "./package.json"
54
+ },
55
+ "main": "./dist/index.cjs",
56
+ "module": "./dist/index.mjs",
57
+ "types": "./dist/index.d.ts",
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
61
+ "scripts": {
62
+ "typecheck": "tsc --noEmit",
63
+ "lint": "biome check .",
64
+ "build": "tsup",
65
+ "test": "vitest run",
66
+ "policies-cite-check": "node scripts/policies-cite-check.mjs"
67
+ },
68
+ "dependencies": {},
69
+ "devDependencies": {
70
+ "@biomejs/biome": "2.5.1",
71
+ "@types/chrome": "^0.2.2",
72
+ "@types/node": "^24.0.14",
73
+ "tsup": "^8.5.0",
74
+ "typescript": "^5.8.3",
75
+ "vitest": "^3.2.4"
76
+ }
77
+ }