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.
Files changed (40) hide show
  1. package/README.md +221 -18
  2. package/dist/{chunk-25A7QG5Z.mjs → chunk-26BG7FFX.mjs} +3 -3
  3. package/dist/{chunk-25A7QG5Z.mjs.map → chunk-26BG7FFX.mjs.map} +1 -1
  4. package/dist/{chunk-SEZFAOUU.mjs → chunk-JKQTHHIP.mjs} +49 -4
  5. package/dist/chunk-JKQTHHIP.mjs.map +1 -0
  6. package/dist/{chunk-SVN6UWA4.mjs → chunk-VE7YBR5A.mjs} +142 -24
  7. package/dist/chunk-VE7YBR5A.mjs.map +1 -0
  8. package/dist/content.cjs +188 -28
  9. package/dist/content.cjs.map +1 -1
  10. package/dist/content.d.cts +11 -4
  11. package/dist/content.d.ts +11 -4
  12. package/dist/content.mjs +18 -8
  13. package/dist/content.mjs.map +1 -1
  14. package/dist/index.cjs +140 -26
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +3 -3
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.mjs +3 -7
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/policies.cjs +18 -11
  21. package/dist/policies.cjs.map +1 -1
  22. package/dist/policies.d.cts +98 -86
  23. package/dist/policies.d.ts +98 -86
  24. package/dist/policies.mjs +18 -12
  25. package/dist/policies.mjs.map +1 -1
  26. package/dist/{session-C5gH-LaQ.d.ts → session-DA9hkYlD.d.ts} +10 -1
  27. package/dist/{session-n0px3Agb.d.cts → session-iosT1mEL.d.cts} +10 -1
  28. package/dist/{stores-CELX9aHN.d.ts → stores-Behkya7C.d.ts} +1 -1
  29. package/dist/{stores-CKCdKDdX.d.cts → stores-DfovpSqe.d.cts} +1 -1
  30. package/dist/{types-A9OJ7SWj.d.cts → types-DgKvVCmQ.d.cts} +53 -7
  31. package/dist/{types-A9OJ7SWj.d.ts → types-DgKvVCmQ.d.ts} +53 -7
  32. package/dist/webext.cjs +191 -28
  33. package/dist/webext.cjs.map +1 -1
  34. package/dist/webext.d.cts +11 -4
  35. package/dist/webext.d.ts +11 -4
  36. package/dist/webext.mjs +22 -9
  37. package/dist/webext.mjs.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-SEZFAOUU.mjs.map +0 -1
  40. package/dist/chunk-SVN6UWA4.mjs.map +0 -1
@@ -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
- return strongest ? { matched, selfMatch, strongest } : { matched, selfMatch };
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 === "redirect-domain") {
675
+ if (kind === "disabled-host") {
652
676
  return 0;
653
677
  }
654
- if (kind === "landing-param") {
678
+ if (kind === "redirect-domain") {
655
679
  return 1;
656
680
  }
657
- if (kind === "cookie") {
681
+ if (kind === "landing-param") {
658
682
  return 2;
659
683
  }
660
- return 3;
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
- if (detection.strongest) {
1010
- const matchedPolicies = policiesForDetection(policies, detection);
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
- detection.strongest.advertiserHost,
1020
- detection.strongest.policyId,
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: detection.strongest.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
- return {
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
  }