standdown 0.1.0 → 0.2.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.
- package/README.md +221 -18
- package/dist/{chunk-25A7QG5Z.mjs → chunk-26BG7FFX.mjs} +3 -3
- package/dist/{chunk-25A7QG5Z.mjs.map → chunk-26BG7FFX.mjs.map} +1 -1
- package/dist/{chunk-SEZFAOUU.mjs → chunk-JKQTHHIP.mjs} +49 -4
- package/dist/chunk-JKQTHHIP.mjs.map +1 -0
- package/dist/{chunk-SVN6UWA4.mjs → chunk-VE7YBR5A.mjs} +142 -24
- package/dist/chunk-VE7YBR5A.mjs.map +1 -0
- package/dist/content.cjs +188 -28
- package/dist/content.cjs.map +1 -1
- package/dist/content.d.cts +11 -4
- package/dist/content.d.ts +11 -4
- package/dist/content.mjs +18 -8
- package/dist/content.mjs.map +1 -1
- package/dist/index.cjs +140 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +3 -7
- package/dist/index.mjs.map +1 -1
- package/dist/policies.cjs +18 -11
- package/dist/policies.cjs.map +1 -1
- package/dist/policies.d.cts +98 -86
- package/dist/policies.d.ts +98 -86
- package/dist/policies.mjs +18 -12
- package/dist/policies.mjs.map +1 -1
- package/dist/{session-C5gH-LaQ.d.ts → session-DA9hkYlD.d.ts} +10 -1
- package/dist/{session-n0px3Agb.d.cts → session-iosT1mEL.d.cts} +10 -1
- package/dist/{stores-CELX9aHN.d.ts → stores-Behkya7C.d.ts} +1 -1
- package/dist/{stores-CKCdKDdX.d.cts → stores-DfovpSqe.d.cts} +1 -1
- package/dist/{types-A9OJ7SWj.d.cts → types-DgKvVCmQ.d.cts} +53 -7
- package/dist/{types-A9OJ7SWj.d.ts → types-DgKvVCmQ.d.ts} +53 -7
- package/dist/webext.cjs +191 -28
- package/dist/webext.cjs.map +1 -1
- package/dist/webext.d.cts +11 -4
- package/dist/webext.d.ts +11 -4
- package/dist/webext.mjs +22 -9
- package/dist/webext.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-SEZFAOUU.mjs.map +0 -1
- package/dist/chunk-SVN6UWA4.mjs.map +0 -1
package/dist/content.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/content.ts"],"names":["decision"],"mappings":"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","file":"content.mjs","sourcesContent":["import { StanddownSession } from './session';\nimport {\n LocalStorageTtlStateStore,\n SessionStorageStateStore,\n type WebStorageLike,\n} from './stores';\nimport type {\n Behavior,\n Decision,\n SelfExemption,\n Signals,\n StanddownPolicy,\n StateStore,\n} from './types';\n\nexport {\n LocalStorageTtlStateStore,\n SessionStorageStateStore,\n type WebStorageLike,\n} from './stores';\n\nconst DEFAULT_LOCAL_STORAGE_TTL_MS = 24 * 60 * 60 * 1_000;\nconst FAIL_CLOSED_BEHAVIORS = [\n 'suppress-prompts',\n 'no-cookie-write',\n 'no-redirect',\n 'no-background-tracking',\n] as const satisfies readonly Behavior[];\n\nexport type ContentStorageMode = 'session' | 'local-ttl';\n\nexport interface CreateContentStanddownOptions {\n readonly policies: readonly StanddownPolicy[];\n readonly selfPatterns?: readonly SelfExemption[];\n readonly publisherSites?: readonly string[];\n readonly store?: StateStore;\n readonly storage?: ContentStorageMode;\n /**\n * Sliding envelope TTL for `storage: 'local-ttl'`. Defaults to 24 hours.\n * Every save refreshes the envelope; expiry clears session records but keeps\n * audit entries. Per-policy stand-down durations are still enforced by core.\n */\n readonly ttlMs?: number;\n readonly window?: ContentWindowLike;\n readonly now?: () => number;\n readonly auditLog?: boolean;\n readonly onDecision?: (decision: Decision, signals: Signals) => void;\n}\n\nexport interface ContentStanddownController {\n readonly session: StanddownSession;\n readonly store: StateStore;\n readonly ready: Promise<Decision>;\n evaluate(): Promise<Decision>;\n shouldStandDown(advertiserHost?: string, now?: number): Promise<Decision>;\n dispose(): void;\n}\n\nexport interface CollectContentSignalsOptions {\n readonly window?: ContentWindowLike;\n readonly selfPatterns?: readonly SelfExemption[];\n readonly publisherSites?: readonly string[];\n readonly now?: () => number;\n}\n\nexport interface ContentWindowLike {\n readonly location: { readonly href: string };\n readonly document: {\n readonly referrer?: string;\n readonly cookie: string;\n };\n readonly sessionStorage?: WebStorageLike | undefined;\n readonly localStorage?: WebStorageLike | undefined;\n readonly history?: ContentHistoryLike;\n readonly setTimeout?: (handler: () => void, timeout?: number) => unknown;\n addEventListener?(type: 'popstate', listener: () => void): void;\n removeEventListener?(type: 'popstate', listener: () => void): void;\n}\n\nexport interface ContentHistoryLike {\n pushState(data: unknown, unused: string, url?: string | URL | null): void;\n replaceState(data: unknown, unused: string, url?: string | URL | null): void;\n}\n\nexport function createContentStanddown(\n opts: CreateContentStanddownOptions,\n): ContentStanddownController {\n const windowLike = opts.window ?? currentWindow();\n const now = opts.now ?? Date.now;\n const store = opts.store ?? createContentStore(windowLike, opts, now);\n const session = new StanddownSession(\n store,\n opts.auditLog === undefined ? undefined : { auditLog: opts.auditLog },\n );\n\n let disposed = false;\n let pending = false;\n const restoreHistory = patchHistory(windowLike, scheduleEvaluation);\n const onPopState = () => {\n scheduleEvaluation();\n };\n\n windowLike?.addEventListener?.('popstate', onPopState);\n\n async function evaluate(): Promise<Decision> {\n let signals: Signals;\n\n try {\n const signalOptions: {\n window?: ContentWindowLike;\n selfPatterns?: readonly SelfExemption[];\n publisherSites?: readonly string[];\n now: () => number;\n } = { now };\n\n if (windowLike !== undefined) {\n signalOptions.window = windowLike;\n }\n\n if (opts.selfPatterns !== undefined) {\n signalOptions.selfPatterns = opts.selfPatterns;\n }\n\n if (opts.publisherSites !== undefined) {\n signalOptions.publisherSites = opts.publisherSites;\n }\n\n signals = collectContentSignals(signalOptions);\n } catch {\n const decision = failClosedDecision('signal-collection-error');\n opts.onDecision?.(decision, {\n url: '',\n now: now(),\n });\n return decision;\n }\n\n const decision = await session.ingest(signals, opts.policies);\n opts.onDecision?.(decision, signals);\n return decision;\n }\n\n function scheduleEvaluation(): void {\n if (disposed || pending) {\n return;\n }\n\n pending = true;\n const run = () => {\n pending = false;\n\n if (!disposed) {\n void evaluate();\n }\n };\n\n if (windowLike?.setTimeout !== undefined) {\n windowLike.setTimeout(run, 0);\n return;\n }\n\n queueMicrotask(run);\n }\n\n async function shouldStandDown(\n advertiserHost?: string,\n at = now(),\n ): Promise<Decision> {\n const host =\n advertiserHost ??\n (windowLike === undefined ? undefined : hostFromUrl(windowLike.location.href));\n\n if (host === undefined) {\n return failClosedDecision('missing-advertiser-host');\n }\n\n return session.shouldStandDown(host, at);\n }\n\n function dispose(): void {\n disposed = true;\n restoreHistory();\n windowLike?.removeEventListener?.('popstate', onPopState);\n }\n\n const ready = evaluate();\n\n return {\n session,\n store,\n ready,\n evaluate,\n shouldStandDown,\n dispose,\n };\n}\n\nexport function collectContentSignals(\n opts: CollectContentSignalsOptions = {},\n): Signals {\n const windowLike = opts.window ?? currentWindow();\n const now = opts.now ?? Date.now;\n\n if (windowLike === undefined) {\n throw new Error('content window unavailable');\n }\n\n const signals: Signals = {\n url: windowLike.location.href,\n now: now(),\n };\n const referrer = windowLike.document.referrer;\n const cookieNames = cookieNamesFromString(windowLike.document.cookie);\n\n if (referrer !== undefined && referrer.length > 0) {\n signals.referrer = referrer;\n }\n\n if (cookieNames.length > 0) {\n signals.cookieNames = cookieNames;\n }\n\n if (opts.selfPatterns !== undefined) {\n signals.selfPatterns = opts.selfPatterns;\n }\n\n if (opts.publisherSites !== undefined) {\n signals.publisherSites = opts.publisherSites;\n }\n\n return signals;\n}\n\nexport function cookieNamesFromString(cookie: string): string[] {\n if (cookie.trim().length === 0) {\n return [];\n }\n\n return cookie\n .split(';')\n .map((part) => part.trim())\n .filter((part) => part.length > 0)\n .map((part) => part.split('=')[0]?.trim() ?? '')\n .filter((name) => name.length > 0);\n}\n\nfunction createContentStore(\n windowLike: ContentWindowLike | undefined,\n opts: CreateContentStanddownOptions,\n now: () => number,\n): StateStore {\n if (windowLike === undefined) {\n return new UnavailableStateStore('content window unavailable');\n }\n\n if (opts.storage === 'local-ttl') {\n if (windowLike.localStorage === undefined) {\n return new UnavailableStateStore('localStorage unavailable');\n }\n\n const storeOptions: {\n ttlMs: number;\n sessionStorage?: WebStorageLike;\n now: () => number;\n } = {\n ttlMs: opts.ttlMs ?? DEFAULT_LOCAL_STORAGE_TTL_MS,\n now,\n };\n\n if (windowLike.sessionStorage !== undefined) {\n storeOptions.sessionStorage = windowLike.sessionStorage;\n }\n\n return new LocalStorageTtlStateStore(windowLike.localStorage, storeOptions);\n }\n\n if (windowLike.sessionStorage === undefined) {\n return new UnavailableStateStore('sessionStorage unavailable');\n }\n\n return new SessionStorageStateStore(windowLike.sessionStorage);\n}\n\nfunction patchHistory(\n windowLike: ContentWindowLike | undefined,\n onNavigation: () => void,\n): () => void {\n const history = windowLike?.history;\n\n if (history === undefined) {\n return () => {};\n }\n\n const originalPushState = history.pushState.bind(history);\n const originalReplaceState = history.replaceState.bind(history);\n\n history.pushState = (...args) => {\n originalPushState(...args);\n onNavigation();\n };\n\n history.replaceState = (...args) => {\n originalReplaceState(...args);\n onNavigation();\n };\n\n return () => {\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n };\n}\n\nfunction currentWindow(): ContentWindowLike | undefined {\n const value = (globalThis as typeof globalThis & {\n window?: ContentWindowLike;\n }).window;\n\n return value;\n}\n\nfunction hostFromUrl(value: string): string | undefined {\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\nclass UnavailableStateStore implements StateStore {\n readonly #reason: string;\n\n constructor(reason: string) {\n this.#reason = reason;\n }\n\n async load(): Promise<never> {\n throw new Error(this.#reason);\n }\n\n async save(): Promise<never> {\n throw new Error(this.#reason);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/content.ts"],"names":["decision"],"mappings":"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","file":"content.mjs","sourcesContent":["import { StanddownSession } from './session';\nimport {\n LocalStorageTtlStateStore,\n SessionStorageStateStore,\n type WebStorageLike,\n} from './stores';\nimport type {\n Behavior,\n Decision,\n SelfExemption,\n Signals,\n StanddownPolicy,\n StateStore,\n} from './types';\n\nexport {\n LocalStorageTtlStateStore,\n SessionStorageStateStore,\n type WebStorageLike,\n} from './stores';\n\nconst DEFAULT_LOCAL_STORAGE_TTL_MS = 24 * 60 * 60 * 1_000;\nconst FAIL_CLOSED_BEHAVIORS = [\n 'suppress-prompts',\n 'no-cookie-write',\n 'no-redirect',\n 'no-background-tracking',\n] as const satisfies readonly Behavior[];\n\nexport type ContentStorageMode = 'session' | 'local-ttl';\n\nexport interface CreateContentStanddownOptions {\n readonly policies: readonly StanddownPolicy[];\n readonly selfPatterns?: readonly SelfExemption[];\n readonly publisherSites?: readonly string[];\n readonly store?: StateStore;\n readonly storage?: ContentStorageMode;\n /**\n * Sliding envelope TTL for `storage: 'local-ttl'`. Defaults to 24 hours.\n * Every save refreshes the envelope; expiry clears session records but keeps\n * audit entries. Per-policy stand-down durations are still enforced by core.\n */\n readonly ttlMs?: number;\n readonly window?: ContentWindowLike;\n readonly now?: () => number;\n readonly auditLog?: boolean;\n /**\n * How long a `selfPatterns` match suppresses stand-down for its advertiser\n * host. `'policy'` (default) exempts only the navigation carrying the param;\n * `'session'` persists the exemption for the host across later param-less\n * navigations (Dupe's `ignore_param` semantics).\n */\n readonly selfExemptionScope?: 'policy' | 'session';\n readonly onDecision?: (decision: Decision, signals: Signals) => void;\n}\n\nexport interface ContentStanddownController {\n readonly session: StanddownSession;\n readonly store: StateStore;\n readonly ready: Promise<Decision>;\n evaluate(): Promise<Decision>;\n shouldStandDown(advertiserHost?: string, now?: number): Promise<Decision>;\n dispose(): void;\n}\n\nexport interface CollectContentSignalsOptions {\n readonly window?: ContentWindowLike;\n readonly selfPatterns?: readonly SelfExemption[];\n readonly publisherSites?: readonly string[];\n readonly now?: () => number;\n}\n\nexport interface ContentWindowLike {\n readonly location: { readonly href: string };\n readonly document: {\n readonly referrer?: string;\n readonly cookie: string;\n };\n readonly sessionStorage?: WebStorageLike | undefined;\n readonly localStorage?: WebStorageLike | undefined;\n readonly history?: ContentHistoryLike;\n readonly setTimeout?: (handler: () => void, timeout?: number) => unknown;\n addEventListener?(type: 'popstate', listener: () => void): void;\n removeEventListener?(type: 'popstate', listener: () => void): void;\n}\n\nexport interface ContentHistoryLike {\n pushState(data: unknown, unused: string, url?: string | URL | null): void;\n replaceState(data: unknown, unused: string, url?: string | URL | null): void;\n}\n\nexport function createContentStanddown(\n opts: CreateContentStanddownOptions,\n): ContentStanddownController {\n const windowLike = opts.window ?? currentWindow();\n const now = opts.now ?? Date.now;\n const store = opts.store ?? createContentStore(windowLike, opts, now);\n const session = new StanddownSession(store, contentSessionOptions(opts));\n\n let disposed = false;\n let pending = false;\n const restoreHistory = patchHistory(windowLike, scheduleEvaluation);\n const onPopState = () => {\n scheduleEvaluation();\n };\n\n windowLike?.addEventListener?.('popstate', onPopState);\n\n async function evaluate(): Promise<Decision> {\n let signals: Signals;\n\n try {\n const signalOptions: {\n window?: ContentWindowLike;\n selfPatterns?: readonly SelfExemption[];\n publisherSites?: readonly string[];\n now: () => number;\n } = { now };\n\n if (windowLike !== undefined) {\n signalOptions.window = windowLike;\n }\n\n if (opts.selfPatterns !== undefined) {\n signalOptions.selfPatterns = opts.selfPatterns;\n }\n\n if (opts.publisherSites !== undefined) {\n signalOptions.publisherSites = opts.publisherSites;\n }\n\n signals = collectContentSignals(signalOptions);\n } catch {\n const decision = failClosedDecision('signal-collection-error');\n opts.onDecision?.(decision, {\n url: '',\n now: now(),\n });\n return decision;\n }\n\n const decision = await session.ingest(signals, opts.policies);\n opts.onDecision?.(decision, signals);\n return decision;\n }\n\n function scheduleEvaluation(): void {\n if (disposed || pending) {\n return;\n }\n\n pending = true;\n const run = () => {\n pending = false;\n\n if (!disposed) {\n void evaluate();\n }\n };\n\n if (windowLike?.setTimeout !== undefined) {\n windowLike.setTimeout(run, 0);\n return;\n }\n\n queueMicrotask(run);\n }\n\n async function shouldStandDown(\n advertiserHost?: string,\n at = now(),\n ): Promise<Decision> {\n const host =\n advertiserHost ??\n (windowLike === undefined ? undefined : hostFromUrl(windowLike.location.href));\n\n if (host === undefined) {\n return failClosedDecision('missing-advertiser-host');\n }\n\n return session.shouldStandDown(host, at);\n }\n\n function dispose(): void {\n disposed = true;\n restoreHistory();\n windowLike?.removeEventListener?.('popstate', onPopState);\n }\n\n const ready = evaluate();\n\n return {\n session,\n store,\n ready,\n evaluate,\n shouldStandDown,\n dispose,\n };\n}\n\nexport function collectContentSignals(\n opts: CollectContentSignalsOptions = {},\n): Signals {\n const windowLike = opts.window ?? currentWindow();\n const now = opts.now ?? Date.now;\n\n if (windowLike === undefined) {\n throw new Error('content window unavailable');\n }\n\n const signals: Signals = {\n url: windowLike.location.href,\n now: now(),\n // The content plane never observes redirect chains, so a non-stand-down\n // here can miss redirect-only attribution.\n signalCoverage: 'partial',\n };\n const referrer = windowLike.document.referrer;\n const cookieNames = cookieNamesFromString(windowLike.document.cookie);\n\n if (referrer !== undefined && referrer.length > 0) {\n signals.referrer = referrer;\n }\n\n if (cookieNames.length > 0) {\n signals.cookieNames = cookieNames;\n }\n\n if (opts.selfPatterns !== undefined) {\n signals.selfPatterns = opts.selfPatterns;\n }\n\n if (opts.publisherSites !== undefined) {\n signals.publisherSites = opts.publisherSites;\n }\n\n return signals;\n}\n\nexport function cookieNamesFromString(cookie: string): string[] {\n if (cookie.trim().length === 0) {\n return [];\n }\n\n return cookie\n .split(';')\n .map((part) => part.trim())\n .filter((part) => part.length > 0)\n .map((part) => part.split('=')[0]?.trim() ?? '')\n .filter((name) => name.length > 0);\n}\n\nfunction contentSessionOptions(\n opts: CreateContentStanddownOptions,\n):\n | { auditLog?: boolean; selfExemptionScope?: 'policy' | 'session' }\n | undefined {\n const sessionOpts: {\n auditLog?: boolean;\n selfExemptionScope?: 'policy' | 'session';\n } = {};\n\n if (opts.auditLog !== undefined) {\n sessionOpts.auditLog = opts.auditLog;\n }\n\n if (opts.selfExemptionScope !== undefined) {\n sessionOpts.selfExemptionScope = opts.selfExemptionScope;\n }\n\n return Object.keys(sessionOpts).length > 0 ? sessionOpts : undefined;\n}\n\nfunction createContentStore(\n windowLike: ContentWindowLike | undefined,\n opts: CreateContentStanddownOptions,\n now: () => number,\n): StateStore {\n if (windowLike === undefined) {\n return new UnavailableStateStore('content window unavailable');\n }\n\n if (opts.storage === 'local-ttl') {\n if (windowLike.localStorage === undefined) {\n return new UnavailableStateStore('localStorage unavailable');\n }\n\n const storeOptions: {\n ttlMs: number;\n sessionStorage?: WebStorageLike;\n now: () => number;\n } = {\n ttlMs: opts.ttlMs ?? DEFAULT_LOCAL_STORAGE_TTL_MS,\n now,\n };\n\n if (windowLike.sessionStorage !== undefined) {\n storeOptions.sessionStorage = windowLike.sessionStorage;\n }\n\n return new LocalStorageTtlStateStore(windowLike.localStorage, storeOptions);\n }\n\n if (windowLike.sessionStorage === undefined) {\n return new UnavailableStateStore('sessionStorage unavailable');\n }\n\n return new SessionStorageStateStore(windowLike.sessionStorage);\n}\n\nfunction patchHistory(\n windowLike: ContentWindowLike | undefined,\n onNavigation: () => void,\n): () => void {\n const history = windowLike?.history;\n\n if (history === undefined) {\n return () => {};\n }\n\n const originalPushState = history.pushState.bind(history);\n const originalReplaceState = history.replaceState.bind(history);\n\n history.pushState = (...args) => {\n originalPushState(...args);\n onNavigation();\n };\n\n history.replaceState = (...args) => {\n originalReplaceState(...args);\n onNavigation();\n };\n\n return () => {\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n };\n}\n\nfunction currentWindow(): ContentWindowLike | undefined {\n const value = (globalThis as typeof globalThis & {\n window?: ContentWindowLike;\n }).window;\n\n return value;\n}\n\nfunction hostFromUrl(value: string): string | undefined {\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\nclass UnavailableStateStore implements StateStore {\n readonly #reason: string;\n\n constructor(reason: string) {\n this.#reason = reason;\n }\n\n async load(): Promise<never> {\n throw new Error(this.#reason);\n }\n\n async save(): Promise<never> {\n throw new Error(this.#reason);\n }\n}\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,9 @@ function validatePolicy(value) {
|
|
|
45
45
|
optionalArray(detection.initiatorRules, "policy.detection.initiatorRules").forEach(
|
|
46
46
|
validateInitiatorRule
|
|
47
47
|
);
|
|
48
|
+
optionalArray(detection.disableHosts, "policy.detection.disableHosts").forEach(
|
|
49
|
+
validateDomainRule
|
|
50
|
+
);
|
|
48
51
|
const standdown = object(policy.standdown, "policy.standdown");
|
|
49
52
|
literal(standdown.scope, "advertiser", "policy.standdown.scope");
|
|
50
53
|
if (standdown.sessionRule !== "session-or-min" && standdown.sessionRule !== "inactivity-window") {
|
|
@@ -70,12 +73,6 @@ function validatePolicy(value) {
|
|
|
70
73
|
if (activation.mode !== "user-click" && activation.mode !== "never") {
|
|
71
74
|
throw new TypeError("policy.activation.mode is invalid");
|
|
72
75
|
}
|
|
73
|
-
if (activation.maxPromptsPerJourney !== void 0) {
|
|
74
|
-
nonNegativeFiniteNumber(
|
|
75
|
-
activation.maxPromptsPerJourney,
|
|
76
|
-
"policy.activation.maxPromptsPerJourney"
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
76
|
for (const referrerClass of optionalArray(
|
|
80
77
|
activation.allowedReferrerClasses,
|
|
81
78
|
"policy.activation.allowedReferrerClasses"
|
|
@@ -504,8 +501,24 @@ function detect(signals, policies) {
|
|
|
504
501
|
}
|
|
505
502
|
const advertiserHost = currentUrl.hostname.toLowerCase();
|
|
506
503
|
const matched = [];
|
|
504
|
+
const selfExemptScopes = [];
|
|
507
505
|
let selfMatch = false;
|
|
508
506
|
for (const policy of policies) {
|
|
507
|
+
const disabledHostRule = (policy.detection.disableHosts ?? []).find(
|
|
508
|
+
(rule) => domainRuleMatchesUrl(rule, advertiserHost)
|
|
509
|
+
);
|
|
510
|
+
if (disabledHostRule) {
|
|
511
|
+
matched.push(
|
|
512
|
+
matchedRule(
|
|
513
|
+
policy,
|
|
514
|
+
"disabled-host",
|
|
515
|
+
`${disabledHostRule.kind}:${disabledHostRule.pattern}`,
|
|
516
|
+
advertiserHost,
|
|
517
|
+
disabledHostRule.comment ?? "host disabled for this network"
|
|
518
|
+
)
|
|
519
|
+
);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
509
522
|
const scopedSelfMatch = hasScopedSelfExemption(
|
|
510
523
|
currentUrl,
|
|
511
524
|
signals.selfPatterns,
|
|
@@ -519,6 +532,10 @@ function detect(signals, policies) {
|
|
|
519
532
|
selfMatch = true;
|
|
520
533
|
}
|
|
521
534
|
if (scopedSelfMatch) {
|
|
535
|
+
selfExemptScopes.push({
|
|
536
|
+
policyId: policy.id,
|
|
537
|
+
networkId: policy.network.id
|
|
538
|
+
});
|
|
522
539
|
continue;
|
|
523
540
|
}
|
|
524
541
|
matched.push(
|
|
@@ -531,7 +548,14 @@ function detect(signals, policies) {
|
|
|
531
548
|
advertiserHost: matched[0].advertiserHost,
|
|
532
549
|
reason: matched[0].reason
|
|
533
550
|
} : void 0;
|
|
534
|
-
|
|
551
|
+
const base = { matched, selfMatch };
|
|
552
|
+
if (strongest) {
|
|
553
|
+
base.strongest = strongest;
|
|
554
|
+
}
|
|
555
|
+
if (selfExemptScopes.length > 0) {
|
|
556
|
+
base.selfExemptScopes = selfExemptScopes;
|
|
557
|
+
}
|
|
558
|
+
return base;
|
|
535
559
|
}
|
|
536
560
|
function classifyReferrer(signals, advertiserHost) {
|
|
537
561
|
const candidate = signals.initiator ?? signals.referrer;
|
|
@@ -648,16 +672,19 @@ function matchedRule(policy, kind, rule, advertiserHost, reason) {
|
|
|
648
672
|
};
|
|
649
673
|
}
|
|
650
674
|
function kindPriority(kind) {
|
|
651
|
-
if (kind === "
|
|
675
|
+
if (kind === "disabled-host") {
|
|
652
676
|
return 0;
|
|
653
677
|
}
|
|
654
|
-
if (kind === "
|
|
678
|
+
if (kind === "redirect-domain") {
|
|
655
679
|
return 1;
|
|
656
680
|
}
|
|
657
|
-
if (kind === "
|
|
681
|
+
if (kind === "landing-param") {
|
|
658
682
|
return 2;
|
|
659
683
|
}
|
|
660
|
-
|
|
684
|
+
if (kind === "cookie") {
|
|
685
|
+
return 3;
|
|
686
|
+
}
|
|
687
|
+
return 4;
|
|
661
688
|
}
|
|
662
689
|
function paramRuleMatches(rule, params) {
|
|
663
690
|
return rule.anyOf.some(
|
|
@@ -743,10 +770,6 @@ function guardActivation(req) {
|
|
|
743
770
|
if (req.benefit.description.trim() === "") {
|
|
744
771
|
return { allowed: false, reason: "missing-user-benefit" };
|
|
745
772
|
}
|
|
746
|
-
const maxPrompts = req.policy.activation.maxPromptsPerJourney;
|
|
747
|
-
if (maxPrompts !== void 0 && (req.decision.promptCount ?? 0) >= maxPrompts) {
|
|
748
|
-
return { allowed: false, reason: "max-prompts-exhausted" };
|
|
749
|
-
}
|
|
750
773
|
const allowedReferrerClasses = req.policy.activation.allowedReferrerClasses;
|
|
751
774
|
if (allowedReferrerClasses !== void 0) {
|
|
752
775
|
if (req.decision.referrerClass === void 0) {
|
|
@@ -976,11 +999,13 @@ var StanddownSession = class {
|
|
|
976
999
|
#store;
|
|
977
1000
|
#auditLog;
|
|
978
1001
|
#maxAuditEntries;
|
|
1002
|
+
#selfExemptionScope;
|
|
979
1003
|
#readOnlyAuditLog = [];
|
|
980
1004
|
constructor(store, opts) {
|
|
981
1005
|
this.#store = store;
|
|
982
1006
|
this.#auditLog = opts?.auditLog ?? true;
|
|
983
1007
|
this.#maxAuditEntries = Math.max(0, opts?.maxAuditEntries ?? 1e3);
|
|
1008
|
+
this.#selfExemptionScope = opts?.selfExemptionScope ?? "policy";
|
|
984
1009
|
}
|
|
985
1010
|
async ingest(signals, policies) {
|
|
986
1011
|
const advertiserHost = hostFromUrl2(signals.url);
|
|
@@ -1006,38 +1031,49 @@ var StanddownSession = class {
|
|
|
1006
1031
|
}
|
|
1007
1032
|
return this.#withState(signals.now, (state) => {
|
|
1008
1033
|
pruneExpiredSessions(state, signals.now);
|
|
1009
|
-
|
|
1010
|
-
|
|
1034
|
+
let effective = detection;
|
|
1035
|
+
if (this.#selfExemptionScope === "session" && advertiserHost) {
|
|
1036
|
+
recordSessionExemptions(state, advertiserHost, detection, signals.now);
|
|
1037
|
+
effective = applySessionExemptions(
|
|
1038
|
+
advertiserHost,
|
|
1039
|
+
detection,
|
|
1040
|
+
state.exemptions?.[advertiserHost]
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
if (effective.strongest) {
|
|
1044
|
+
const matchedPolicies = policiesForDetection(policies, effective);
|
|
1011
1045
|
if (matchedPolicies.length === 0) {
|
|
1012
1046
|
return {
|
|
1013
1047
|
decision: failClosedDecision("matched-policy-missing"),
|
|
1014
|
-
detection
|
|
1048
|
+
detection: effective
|
|
1015
1049
|
};
|
|
1016
1050
|
}
|
|
1017
1051
|
const record = upsertSessionRecord(
|
|
1018
1052
|
state,
|
|
1019
|
-
|
|
1020
|
-
|
|
1053
|
+
effective.strongest.advertiserHost,
|
|
1054
|
+
effective.strongest.policyId,
|
|
1021
1055
|
matchedPolicies,
|
|
1022
1056
|
signals.now
|
|
1023
1057
|
);
|
|
1024
1058
|
return {
|
|
1025
1059
|
decision: decisionFromRecord(record, signals.now, {
|
|
1026
|
-
reason:
|
|
1060
|
+
reason: effective.strongest.reason,
|
|
1027
1061
|
referrerClass: classifyReferrer(signals, record.advertiserHost)
|
|
1028
1062
|
}),
|
|
1029
|
-
detection
|
|
1063
|
+
detection: effective
|
|
1030
1064
|
};
|
|
1031
1065
|
}
|
|
1032
1066
|
const activeDecision = advertiserHost ? activeDecisionForHost(state, advertiserHost, signals.now) : void 0;
|
|
1067
|
+
const sessionExempted = detection.strongest !== void 0 && effective.strongest === void 0;
|
|
1033
1068
|
return {
|
|
1034
1069
|
decision: activeDecision ?? {
|
|
1035
1070
|
standDown: false,
|
|
1036
|
-
reason: detection.selfMatch ? "self-exempted-no-active-standdown" : "no-active-standdown",
|
|
1071
|
+
reason: sessionExempted ? "self-exempted-session" : detection.selfMatch ? "self-exempted-no-active-standdown" : "no-active-standdown",
|
|
1037
1072
|
behaviors: [],
|
|
1038
|
-
referrerClass: advertiserHost ? classifyReferrer(signals, advertiserHost) : "other"
|
|
1073
|
+
referrerClass: advertiserHost ? classifyReferrer(signals, advertiserHost) : "other",
|
|
1074
|
+
...signals.signalCoverage === "partial" ? { degraded: true } : {}
|
|
1039
1075
|
},
|
|
1040
|
-
detection
|
|
1076
|
+
detection: effective
|
|
1041
1077
|
};
|
|
1042
1078
|
}, "ingest");
|
|
1043
1079
|
}
|
|
@@ -1206,6 +1242,66 @@ function upsertSessionRecord(state, advertiserHost, primaryPolicyId, policies, n
|
|
|
1206
1242
|
state.sessions[key] = baseRecord;
|
|
1207
1243
|
return baseRecord;
|
|
1208
1244
|
}
|
|
1245
|
+
function recordSessionExemptions(state, advertiserHost, detection, now) {
|
|
1246
|
+
const scopes = detection.selfExemptScopes;
|
|
1247
|
+
if (!scopes || scopes.length === 0) {
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (activeDecisionForHost(state, advertiserHost, now) !== void 0) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const key = normalizeHost(advertiserHost);
|
|
1254
|
+
if (state.exemptions === void 0) {
|
|
1255
|
+
state.exemptions = {};
|
|
1256
|
+
}
|
|
1257
|
+
const exemptions = state.exemptions;
|
|
1258
|
+
const existing = exemptions[key];
|
|
1259
|
+
const policyIds = new Set(existing?.policyIds ?? []);
|
|
1260
|
+
const networkIds = new Set(existing?.networkIds ?? []);
|
|
1261
|
+
for (const scope of scopes) {
|
|
1262
|
+
policyIds.add(scope.policyId);
|
|
1263
|
+
networkIds.add(scope.networkId);
|
|
1264
|
+
}
|
|
1265
|
+
exemptions[key] = {
|
|
1266
|
+
advertiserHost: key,
|
|
1267
|
+
policyIds: [...policyIds],
|
|
1268
|
+
networkIds: [...networkIds],
|
|
1269
|
+
grantedAt: existing?.grantedAt ?? now
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
function applySessionExemptions(advertiserHost, detection, record) {
|
|
1273
|
+
if (!record) {
|
|
1274
|
+
return detection;
|
|
1275
|
+
}
|
|
1276
|
+
const host = normalizeHost(advertiserHost);
|
|
1277
|
+
const policyIds = new Set(record.policyIds);
|
|
1278
|
+
const networkIds = new Set(record.networkIds);
|
|
1279
|
+
const filtered = detection.matched.filter((match) => {
|
|
1280
|
+
if (match.kind === "disabled-host") {
|
|
1281
|
+
return true;
|
|
1282
|
+
}
|
|
1283
|
+
if (normalizeHost(match.advertiserHost) !== host) {
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
return !(policyIds.has(match.policyId) || networkIds.has(match.networkId));
|
|
1287
|
+
});
|
|
1288
|
+
if (filtered.length === detection.matched.length) {
|
|
1289
|
+
return detection;
|
|
1290
|
+
}
|
|
1291
|
+
const strongest = filtered[0] ? {
|
|
1292
|
+
policyId: filtered[0].policyId,
|
|
1293
|
+
advertiserHost: filtered[0].advertiserHost,
|
|
1294
|
+
reason: filtered[0].reason
|
|
1295
|
+
} : void 0;
|
|
1296
|
+
const next = { matched: filtered, selfMatch: detection.selfMatch };
|
|
1297
|
+
if (strongest) {
|
|
1298
|
+
next.strongest = strongest;
|
|
1299
|
+
}
|
|
1300
|
+
if (detection.selfExemptScopes) {
|
|
1301
|
+
next.selfExemptScopes = detection.selfExemptScopes;
|
|
1302
|
+
}
|
|
1303
|
+
return next;
|
|
1304
|
+
}
|
|
1209
1305
|
function policiesForDetection(policies, detection) {
|
|
1210
1306
|
if (detection.strongest === void 0) {
|
|
1211
1307
|
return [];
|
|
@@ -1302,7 +1398,7 @@ function auditEntry(value) {
|
|
|
1302
1398
|
return entry;
|
|
1303
1399
|
}
|
|
1304
1400
|
function cloneState(state) {
|
|
1305
|
-
|
|
1401
|
+
const cloned = {
|
|
1306
1402
|
sessions: Object.fromEntries(
|
|
1307
1403
|
Object.entries(state.sessions).map(([host, record]) => [
|
|
1308
1404
|
host,
|
|
@@ -1314,6 +1410,19 @@ function cloneState(state) {
|
|
|
1314
1410
|
),
|
|
1315
1411
|
auditLog: state.auditLog.map(cloneAuditEntry)
|
|
1316
1412
|
};
|
|
1413
|
+
if (state.exemptions) {
|
|
1414
|
+
cloned.exemptions = Object.fromEntries(
|
|
1415
|
+
Object.entries(state.exemptions).map(([host, record]) => [
|
|
1416
|
+
host,
|
|
1417
|
+
{
|
|
1418
|
+
...record,
|
|
1419
|
+
policyIds: [...record.policyIds],
|
|
1420
|
+
networkIds: [...record.networkIds]
|
|
1421
|
+
}
|
|
1422
|
+
])
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
return cloned;
|
|
1317
1426
|
}
|
|
1318
1427
|
function cloneAuditEntry(entry) {
|
|
1319
1428
|
const cloned = {
|
|
@@ -1331,6 +1440,11 @@ function cloneAuditEntry(entry) {
|
|
|
1331
1440
|
if (entry.detection.strongest !== void 0) {
|
|
1332
1441
|
detection.strongest = { ...entry.detection.strongest };
|
|
1333
1442
|
}
|
|
1443
|
+
if (entry.detection.selfExemptScopes !== void 0) {
|
|
1444
|
+
detection.selfExemptScopes = entry.detection.selfExemptScopes.map(
|
|
1445
|
+
(scope) => ({ ...scope })
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1334
1448
|
if (entry.detection.failClosedReason !== void 0) {
|
|
1335
1449
|
detection.failClosedReason = entry.detection.failClosedReason;
|
|
1336
1450
|
}
|