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
package/dist/webext.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(
@@ -729,11 +756,13 @@ var StanddownSession = class {
729
756
  #store;
730
757
  #auditLog;
731
758
  #maxAuditEntries;
759
+ #selfExemptionScope;
732
760
  #readOnlyAuditLog = [];
733
761
  constructor(store, opts) {
734
762
  this.#store = store;
735
763
  this.#auditLog = opts?.auditLog ?? true;
736
764
  this.#maxAuditEntries = Math.max(0, opts?.maxAuditEntries ?? 1e3);
765
+ this.#selfExemptionScope = opts?.selfExemptionScope ?? "policy";
737
766
  }
738
767
  async ingest(signals, policies) {
739
768
  const advertiserHost = hostFromUrl2(signals.url);
@@ -759,38 +788,49 @@ var StanddownSession = class {
759
788
  }
760
789
  return this.#withState(signals.now, (state) => {
761
790
  pruneExpiredSessions(state, signals.now);
762
- if (detection.strongest) {
763
- const matchedPolicies = policiesForDetection(policies, detection);
791
+ let effective = detection;
792
+ if (this.#selfExemptionScope === "session" && advertiserHost) {
793
+ recordSessionExemptions(state, advertiserHost, detection, signals.now);
794
+ effective = applySessionExemptions(
795
+ advertiserHost,
796
+ detection,
797
+ state.exemptions?.[advertiserHost]
798
+ );
799
+ }
800
+ if (effective.strongest) {
801
+ const matchedPolicies = policiesForDetection(policies, effective);
764
802
  if (matchedPolicies.length === 0) {
765
803
  return {
766
804
  decision: failClosedDecision("matched-policy-missing"),
767
- detection
805
+ detection: effective
768
806
  };
769
807
  }
770
808
  const record = upsertSessionRecord(
771
809
  state,
772
- detection.strongest.advertiserHost,
773
- detection.strongest.policyId,
810
+ effective.strongest.advertiserHost,
811
+ effective.strongest.policyId,
774
812
  matchedPolicies,
775
813
  signals.now
776
814
  );
777
815
  return {
778
816
  decision: decisionFromRecord(record, signals.now, {
779
- reason: detection.strongest.reason,
817
+ reason: effective.strongest.reason,
780
818
  referrerClass: classifyReferrer(signals, record.advertiserHost)
781
819
  }),
782
- detection
820
+ detection: effective
783
821
  };
784
822
  }
785
823
  const activeDecision = advertiserHost ? activeDecisionForHost(state, advertiserHost, signals.now) : void 0;
824
+ const sessionExempted = detection.strongest !== void 0 && effective.strongest === void 0;
786
825
  return {
787
826
  decision: activeDecision ?? {
788
827
  standDown: false,
789
- reason: detection.selfMatch ? "self-exempted-no-active-standdown" : "no-active-standdown",
828
+ reason: sessionExempted ? "self-exempted-session" : detection.selfMatch ? "self-exempted-no-active-standdown" : "no-active-standdown",
790
829
  behaviors: [],
791
- referrerClass: advertiserHost ? classifyReferrer(signals, advertiserHost) : "other"
830
+ referrerClass: advertiserHost ? classifyReferrer(signals, advertiserHost) : "other",
831
+ ...signals.signalCoverage === "partial" ? { degraded: true } : {}
792
832
  },
793
- detection
833
+ detection: effective
794
834
  };
795
835
  }, "ingest");
796
836
  }
@@ -959,6 +999,66 @@ function upsertSessionRecord(state, advertiserHost, primaryPolicyId, policies, n
959
999
  state.sessions[key] = baseRecord;
960
1000
  return baseRecord;
961
1001
  }
1002
+ function recordSessionExemptions(state, advertiserHost, detection, now) {
1003
+ const scopes = detection.selfExemptScopes;
1004
+ if (!scopes || scopes.length === 0) {
1005
+ return;
1006
+ }
1007
+ if (activeDecisionForHost(state, advertiserHost, now) !== void 0) {
1008
+ return;
1009
+ }
1010
+ const key = normalizeHost(advertiserHost);
1011
+ if (state.exemptions === void 0) {
1012
+ state.exemptions = {};
1013
+ }
1014
+ const exemptions = state.exemptions;
1015
+ const existing = exemptions[key];
1016
+ const policyIds = new Set(existing?.policyIds ?? []);
1017
+ const networkIds = new Set(existing?.networkIds ?? []);
1018
+ for (const scope of scopes) {
1019
+ policyIds.add(scope.policyId);
1020
+ networkIds.add(scope.networkId);
1021
+ }
1022
+ exemptions[key] = {
1023
+ advertiserHost: key,
1024
+ policyIds: [...policyIds],
1025
+ networkIds: [...networkIds],
1026
+ grantedAt: existing?.grantedAt ?? now
1027
+ };
1028
+ }
1029
+ function applySessionExemptions(advertiserHost, detection, record) {
1030
+ if (!record) {
1031
+ return detection;
1032
+ }
1033
+ const host = normalizeHost(advertiserHost);
1034
+ const policyIds = new Set(record.policyIds);
1035
+ const networkIds = new Set(record.networkIds);
1036
+ const filtered = detection.matched.filter((match) => {
1037
+ if (match.kind === "disabled-host") {
1038
+ return true;
1039
+ }
1040
+ if (normalizeHost(match.advertiserHost) !== host) {
1041
+ return true;
1042
+ }
1043
+ return !(policyIds.has(match.policyId) || networkIds.has(match.networkId));
1044
+ });
1045
+ if (filtered.length === detection.matched.length) {
1046
+ return detection;
1047
+ }
1048
+ const strongest = filtered[0] ? {
1049
+ policyId: filtered[0].policyId,
1050
+ advertiserHost: filtered[0].advertiserHost,
1051
+ reason: filtered[0].reason
1052
+ } : void 0;
1053
+ const next = { matched: filtered, selfMatch: detection.selfMatch };
1054
+ if (strongest) {
1055
+ next.strongest = strongest;
1056
+ }
1057
+ if (detection.selfExemptScopes) {
1058
+ next.selfExemptScopes = detection.selfExemptScopes;
1059
+ }
1060
+ return next;
1061
+ }
962
1062
  function policiesForDetection(policies, detection) {
963
1063
  if (detection.strongest === void 0) {
964
1064
  return [];
@@ -1070,6 +1170,11 @@ function cloneAuditEntry(entry) {
1070
1170
  if (entry.detection.strongest !== void 0) {
1071
1171
  detection.strongest = { ...entry.detection.strongest };
1072
1172
  }
1173
+ if (entry.detection.selfExemptScopes !== void 0) {
1174
+ detection.selfExemptScopes = entry.detection.selfExemptScopes.map(
1175
+ (scope) => ({ ...scope })
1176
+ );
1177
+ }
1073
1178
  if (entry.detection.failClosedReason !== void 0) {
1074
1179
  detection.failClosedReason = entry.detection.failClosedReason;
1075
1180
  }
@@ -1188,6 +1293,7 @@ function dropSessionBoundRecords(state, now) {
1188
1293
  delete next.sessions[host];
1189
1294
  }
1190
1295
  }
1296
+ delete next.exemptions;
1191
1297
  return next;
1192
1298
  }
1193
1299
  function createSessionId() {
@@ -1221,10 +1327,36 @@ function parseState(value) {
1221
1327
  for (const [host, record] of Object.entries(value.sessions)) {
1222
1328
  sessions[host] = parseSessionRecord(record);
1223
1329
  }
1224
- return {
1330
+ const state = {
1225
1331
  sessions,
1226
1332
  auditLog: value.auditLog.map(parseAuditEntry)
1227
1333
  };
1334
+ if (value.exemptions !== void 0) {
1335
+ if (!isRecord(value.exemptions)) {
1336
+ throw new Error("invalid standdown state");
1337
+ }
1338
+ const exemptions = {};
1339
+ for (const [host, record] of Object.entries(value.exemptions)) {
1340
+ exemptions[host] = parseExemptionRecord(record);
1341
+ }
1342
+ state.exemptions = exemptions;
1343
+ }
1344
+ return state;
1345
+ }
1346
+ function parseExemptionRecord(value) {
1347
+ if (!isRecord(value)) {
1348
+ throw new Error("invalid exemption record");
1349
+ }
1350
+ const { advertiserHost, policyIds, networkIds, grantedAt } = value;
1351
+ if (typeof advertiserHost !== "string" || typeof grantedAt !== "number" || !Array.isArray(policyIds) || !policyIds.every((id) => typeof id === "string") || !Array.isArray(networkIds) || !networkIds.every((id) => typeof id === "string")) {
1352
+ throw new Error("invalid exemption record");
1353
+ }
1354
+ return {
1355
+ advertiserHost,
1356
+ policyIds: [...policyIds],
1357
+ networkIds: [...networkIds],
1358
+ grantedAt
1359
+ };
1228
1360
  }
1229
1361
  function parseSessionRecord(value) {
1230
1362
  if (!isRecord(value)) {
@@ -1282,7 +1414,7 @@ function parseAuditEntry(value) {
1282
1414
  return entry;
1283
1415
  }
1284
1416
  function cloneState(state) {
1285
- return {
1417
+ const next = {
1286
1418
  sessions: Object.fromEntries(
1287
1419
  Object.entries(state.sessions).map(([host, record]) => [
1288
1420
  host,
@@ -1291,6 +1423,19 @@ function cloneState(state) {
1291
1423
  ),
1292
1424
  auditLog: state.auditLog.map(cloneAuditEntry2)
1293
1425
  };
1426
+ if (state.exemptions) {
1427
+ next.exemptions = Object.fromEntries(
1428
+ Object.entries(state.exemptions).map(([host, record]) => [
1429
+ host,
1430
+ {
1431
+ ...record,
1432
+ policyIds: [...record.policyIds],
1433
+ networkIds: [...record.networkIds]
1434
+ }
1435
+ ])
1436
+ );
1437
+ }
1438
+ return next;
1294
1439
  }
1295
1440
  function cloneSessionRecord(record) {
1296
1441
  const next = {
@@ -1326,6 +1471,11 @@ function cloneAuditEntry2(entry) {
1326
1471
  if (entry.detection.strongest !== void 0) {
1327
1472
  next.detection.strongest = { ...entry.detection.strongest };
1328
1473
  }
1474
+ if (entry.detection.selfExemptScopes !== void 0) {
1475
+ next.detection.selfExemptScopes = entry.detection.selfExemptScopes.map(
1476
+ (scope) => ({ ...scope })
1477
+ );
1478
+ }
1329
1479
  if (entry.detection.failClosedReason !== void 0) {
1330
1480
  next.detection.failClosedReason = entry.detection.failClosedReason;
1331
1481
  }
@@ -1409,10 +1559,7 @@ function createStanddown(opts) {
1409
1559
  sessionId: opts.sessionId,
1410
1560
  now
1411
1561
  });
1412
- const session = new StanddownSession(
1413
- store,
1414
- opts.auditLog === void 0 ? void 0 : { auditLog: opts.auditLog }
1415
- );
1562
+ const session = new StanddownSession(store, sessionOptions(opts));
1416
1563
  const redirectChains = /* @__PURE__ */ new Map();
1417
1564
  const redirectRequestIds = /* @__PURE__ */ new Map();
1418
1565
  const initiators = /* @__PURE__ */ new Map();
@@ -1503,7 +1650,10 @@ function createStanddown(opts) {
1503
1650
  redirectChain,
1504
1651
  initiator,
1505
1652
  selfPatterns: opts.selfPatterns,
1506
- publisherSites: opts.publisherSites
1653
+ publisherSites: opts.publisherSites,
1654
+ // Without the webRequest plane we never observe redirect chains, so a
1655
+ // non-stand-down could be a false negative — mark the coverage partial.
1656
+ signalCoverage: mode === "webRequest" ? "full" : "partial"
1507
1657
  });
1508
1658
  return session.ingest(signals, activePolicies);
1509
1659
  }
@@ -1601,6 +1751,16 @@ function createStanddown(opts) {
1601
1751
  dispose
1602
1752
  };
1603
1753
  }
1754
+ function sessionOptions(opts) {
1755
+ const sessionOpts = {};
1756
+ if (opts.auditLog !== void 0) {
1757
+ sessionOpts.auditLog = opts.auditLog;
1758
+ }
1759
+ if (opts.selfExemptionScope !== void 0) {
1760
+ sessionOpts.selfExemptionScope = opts.selfExemptionScope;
1761
+ }
1762
+ return Object.keys(sessionOpts).length > 0 ? sessionOpts : void 0;
1763
+ }
1604
1764
  function createChromeStore(chromeApi, opts) {
1605
1765
  if (chromeApi?.storage?.local === void 0) {
1606
1766
  return new UnavailableStateStore();
@@ -1622,6 +1782,9 @@ function navigationSignals(value) {
1622
1782
  url: value.url,
1623
1783
  now: value.now
1624
1784
  };
1785
+ if (value.signalCoverage !== void 0) {
1786
+ signals.signalCoverage = value.signalCoverage;
1787
+ }
1625
1788
  if (value.redirectChain !== void 0 && value.redirectChain.length > 0) {
1626
1789
  signals.redirectChain = [...value.redirectChain];
1627
1790
  }