standdown 0.1.0 → 0.2.1
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 +262 -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 +3 -2
- package/dist/chunk-SEZFAOUU.mjs.map +0 -1
- package/dist/chunk-SVN6UWA4.mjs.map +0 -1
package/dist/content.cjs
CHANGED
|
@@ -21,8 +21,24 @@ function detect(signals, policies) {
|
|
|
21
21
|
}
|
|
22
22
|
const advertiserHost = currentUrl.hostname.toLowerCase();
|
|
23
23
|
const matched = [];
|
|
24
|
+
const selfExemptScopes = [];
|
|
24
25
|
let selfMatch = false;
|
|
25
26
|
for (const policy of policies) {
|
|
27
|
+
const disabledHostRule = (policy.detection.disableHosts ?? []).find(
|
|
28
|
+
(rule) => domainRuleMatchesUrl(rule, advertiserHost)
|
|
29
|
+
);
|
|
30
|
+
if (disabledHostRule) {
|
|
31
|
+
matched.push(
|
|
32
|
+
matchedRule(
|
|
33
|
+
policy,
|
|
34
|
+
"disabled-host",
|
|
35
|
+
`${disabledHostRule.kind}:${disabledHostRule.pattern}`,
|
|
36
|
+
advertiserHost,
|
|
37
|
+
disabledHostRule.comment ?? "host disabled for this network"
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
26
42
|
const scopedSelfMatch = hasScopedSelfExemption(
|
|
27
43
|
currentUrl,
|
|
28
44
|
signals.selfPatterns,
|
|
@@ -36,6 +52,10 @@ function detect(signals, policies) {
|
|
|
36
52
|
selfMatch = true;
|
|
37
53
|
}
|
|
38
54
|
if (scopedSelfMatch) {
|
|
55
|
+
selfExemptScopes.push({
|
|
56
|
+
policyId: policy.id,
|
|
57
|
+
networkId: policy.network.id
|
|
58
|
+
});
|
|
39
59
|
continue;
|
|
40
60
|
}
|
|
41
61
|
matched.push(
|
|
@@ -48,7 +68,14 @@ function detect(signals, policies) {
|
|
|
48
68
|
advertiserHost: matched[0].advertiserHost,
|
|
49
69
|
reason: matched[0].reason
|
|
50
70
|
} : void 0;
|
|
51
|
-
|
|
71
|
+
const base = { matched, selfMatch };
|
|
72
|
+
if (strongest) {
|
|
73
|
+
base.strongest = strongest;
|
|
74
|
+
}
|
|
75
|
+
if (selfExemptScopes.length > 0) {
|
|
76
|
+
base.selfExemptScopes = selfExemptScopes;
|
|
77
|
+
}
|
|
78
|
+
return base;
|
|
52
79
|
}
|
|
53
80
|
function classifyReferrer(signals, advertiserHost) {
|
|
54
81
|
const candidate = signals.initiator ?? signals.referrer;
|
|
@@ -165,16 +192,19 @@ function matchedRule(policy, kind, rule, advertiserHost, reason) {
|
|
|
165
192
|
};
|
|
166
193
|
}
|
|
167
194
|
function kindPriority(kind) {
|
|
168
|
-
if (kind === "
|
|
195
|
+
if (kind === "disabled-host") {
|
|
169
196
|
return 0;
|
|
170
197
|
}
|
|
171
|
-
if (kind === "
|
|
198
|
+
if (kind === "redirect-domain") {
|
|
172
199
|
return 1;
|
|
173
200
|
}
|
|
174
|
-
if (kind === "
|
|
201
|
+
if (kind === "landing-param") {
|
|
175
202
|
return 2;
|
|
176
203
|
}
|
|
177
|
-
|
|
204
|
+
if (kind === "cookie") {
|
|
205
|
+
return 3;
|
|
206
|
+
}
|
|
207
|
+
return 4;
|
|
178
208
|
}
|
|
179
209
|
function paramRuleMatches(rule, params) {
|
|
180
210
|
return rule.anyOf.some(
|
|
@@ -286,6 +316,9 @@ function validatePolicy(value) {
|
|
|
286
316
|
optionalArray(detection.initiatorRules, "policy.detection.initiatorRules").forEach(
|
|
287
317
|
validateInitiatorRule
|
|
288
318
|
);
|
|
319
|
+
optionalArray(detection.disableHosts, "policy.detection.disableHosts").forEach(
|
|
320
|
+
validateDomainRule
|
|
321
|
+
);
|
|
289
322
|
const standdown = object(policy.standdown, "policy.standdown");
|
|
290
323
|
literal(standdown.scope, "advertiser", "policy.standdown.scope");
|
|
291
324
|
if (standdown.sessionRule !== "session-or-min" && standdown.sessionRule !== "inactivity-window") {
|
|
@@ -311,12 +344,6 @@ function validatePolicy(value) {
|
|
|
311
344
|
if (activation.mode !== "user-click" && activation.mode !== "never") {
|
|
312
345
|
throw new TypeError("policy.activation.mode is invalid");
|
|
313
346
|
}
|
|
314
|
-
if (activation.maxPromptsPerJourney !== void 0) {
|
|
315
|
-
nonNegativeFiniteNumber(
|
|
316
|
-
activation.maxPromptsPerJourney,
|
|
317
|
-
"policy.activation.maxPromptsPerJourney"
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
347
|
for (const referrerClass of optionalArray(
|
|
321
348
|
activation.allowedReferrerClasses,
|
|
322
349
|
"policy.activation.allowedReferrerClasses"
|
|
@@ -460,11 +487,13 @@ var StanddownSession = class {
|
|
|
460
487
|
#store;
|
|
461
488
|
#auditLog;
|
|
462
489
|
#maxAuditEntries;
|
|
490
|
+
#selfExemptionScope;
|
|
463
491
|
#readOnlyAuditLog = [];
|
|
464
492
|
constructor(store, opts) {
|
|
465
493
|
this.#store = store;
|
|
466
494
|
this.#auditLog = opts?.auditLog ?? true;
|
|
467
495
|
this.#maxAuditEntries = Math.max(0, opts?.maxAuditEntries ?? 1e3);
|
|
496
|
+
this.#selfExemptionScope = opts?.selfExemptionScope ?? "policy";
|
|
468
497
|
}
|
|
469
498
|
async ingest(signals, policies) {
|
|
470
499
|
const advertiserHost = hostFromUrl2(signals.url);
|
|
@@ -490,38 +519,49 @@ var StanddownSession = class {
|
|
|
490
519
|
}
|
|
491
520
|
return this.#withState(signals.now, (state) => {
|
|
492
521
|
pruneExpiredSessions(state, signals.now);
|
|
493
|
-
|
|
494
|
-
|
|
522
|
+
let effective = detection;
|
|
523
|
+
if (this.#selfExemptionScope === "session" && advertiserHost) {
|
|
524
|
+
recordSessionExemptions(state, advertiserHost, detection, signals.now);
|
|
525
|
+
effective = applySessionExemptions(
|
|
526
|
+
advertiserHost,
|
|
527
|
+
detection,
|
|
528
|
+
state.exemptions?.[advertiserHost]
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (effective.strongest) {
|
|
532
|
+
const matchedPolicies = policiesForDetection(policies, effective);
|
|
495
533
|
if (matchedPolicies.length === 0) {
|
|
496
534
|
return {
|
|
497
535
|
decision: failClosedDecision("matched-policy-missing"),
|
|
498
|
-
detection
|
|
536
|
+
detection: effective
|
|
499
537
|
};
|
|
500
538
|
}
|
|
501
539
|
const record = upsertSessionRecord(
|
|
502
540
|
state,
|
|
503
|
-
|
|
504
|
-
|
|
541
|
+
effective.strongest.advertiserHost,
|
|
542
|
+
effective.strongest.policyId,
|
|
505
543
|
matchedPolicies,
|
|
506
544
|
signals.now
|
|
507
545
|
);
|
|
508
546
|
return {
|
|
509
547
|
decision: decisionFromRecord(record, signals.now, {
|
|
510
|
-
reason:
|
|
548
|
+
reason: effective.strongest.reason,
|
|
511
549
|
referrerClass: classifyReferrer(signals, record.advertiserHost)
|
|
512
550
|
}),
|
|
513
|
-
detection
|
|
551
|
+
detection: effective
|
|
514
552
|
};
|
|
515
553
|
}
|
|
516
554
|
const activeDecision = advertiserHost ? activeDecisionForHost(state, advertiserHost, signals.now) : void 0;
|
|
555
|
+
const sessionExempted = detection.strongest !== void 0 && effective.strongest === void 0;
|
|
517
556
|
return {
|
|
518
557
|
decision: activeDecision ?? {
|
|
519
558
|
standDown: false,
|
|
520
|
-
reason: detection.selfMatch ? "self-exempted-no-active-standdown" : "no-active-standdown",
|
|
559
|
+
reason: sessionExempted ? "self-exempted-session" : detection.selfMatch ? "self-exempted-no-active-standdown" : "no-active-standdown",
|
|
521
560
|
behaviors: [],
|
|
522
|
-
referrerClass: advertiserHost ? classifyReferrer(signals, advertiserHost) : "other"
|
|
561
|
+
referrerClass: advertiserHost ? classifyReferrer(signals, advertiserHost) : "other",
|
|
562
|
+
...signals.signalCoverage === "partial" ? { degraded: true } : {}
|
|
523
563
|
},
|
|
524
|
-
detection
|
|
564
|
+
detection: effective
|
|
525
565
|
};
|
|
526
566
|
}, "ingest");
|
|
527
567
|
}
|
|
@@ -690,6 +730,66 @@ function upsertSessionRecord(state, advertiserHost, primaryPolicyId, policies, n
|
|
|
690
730
|
state.sessions[key] = baseRecord;
|
|
691
731
|
return baseRecord;
|
|
692
732
|
}
|
|
733
|
+
function recordSessionExemptions(state, advertiserHost, detection, now) {
|
|
734
|
+
const scopes = detection.selfExemptScopes;
|
|
735
|
+
if (!scopes || scopes.length === 0) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (activeDecisionForHost(state, advertiserHost, now) !== void 0) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const key = normalizeHost(advertiserHost);
|
|
742
|
+
if (state.exemptions === void 0) {
|
|
743
|
+
state.exemptions = {};
|
|
744
|
+
}
|
|
745
|
+
const exemptions = state.exemptions;
|
|
746
|
+
const existing = exemptions[key];
|
|
747
|
+
const policyIds = new Set(existing?.policyIds ?? []);
|
|
748
|
+
const networkIds = new Set(existing?.networkIds ?? []);
|
|
749
|
+
for (const scope of scopes) {
|
|
750
|
+
policyIds.add(scope.policyId);
|
|
751
|
+
networkIds.add(scope.networkId);
|
|
752
|
+
}
|
|
753
|
+
exemptions[key] = {
|
|
754
|
+
advertiserHost: key,
|
|
755
|
+
policyIds: [...policyIds],
|
|
756
|
+
networkIds: [...networkIds],
|
|
757
|
+
grantedAt: existing?.grantedAt ?? now
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function applySessionExemptions(advertiserHost, detection, record) {
|
|
761
|
+
if (!record) {
|
|
762
|
+
return detection;
|
|
763
|
+
}
|
|
764
|
+
const host = normalizeHost(advertiserHost);
|
|
765
|
+
const policyIds = new Set(record.policyIds);
|
|
766
|
+
const networkIds = new Set(record.networkIds);
|
|
767
|
+
const filtered = detection.matched.filter((match) => {
|
|
768
|
+
if (match.kind === "disabled-host") {
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
if (normalizeHost(match.advertiserHost) !== host) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
return !(policyIds.has(match.policyId) || networkIds.has(match.networkId));
|
|
775
|
+
});
|
|
776
|
+
if (filtered.length === detection.matched.length) {
|
|
777
|
+
return detection;
|
|
778
|
+
}
|
|
779
|
+
const strongest = filtered[0] ? {
|
|
780
|
+
policyId: filtered[0].policyId,
|
|
781
|
+
advertiserHost: filtered[0].advertiserHost,
|
|
782
|
+
reason: filtered[0].reason
|
|
783
|
+
} : void 0;
|
|
784
|
+
const next = { matched: filtered, selfMatch: detection.selfMatch };
|
|
785
|
+
if (strongest) {
|
|
786
|
+
next.strongest = strongest;
|
|
787
|
+
}
|
|
788
|
+
if (detection.selfExemptScopes) {
|
|
789
|
+
next.selfExemptScopes = detection.selfExemptScopes;
|
|
790
|
+
}
|
|
791
|
+
return next;
|
|
792
|
+
}
|
|
693
793
|
function policiesForDetection(policies, detection) {
|
|
694
794
|
if (detection.strongest === void 0) {
|
|
695
795
|
return [];
|
|
@@ -801,6 +901,11 @@ function cloneAuditEntry(entry) {
|
|
|
801
901
|
if (entry.detection.strongest !== void 0) {
|
|
802
902
|
detection.strongest = { ...entry.detection.strongest };
|
|
803
903
|
}
|
|
904
|
+
if (entry.detection.selfExemptScopes !== void 0) {
|
|
905
|
+
detection.selfExemptScopes = entry.detection.selfExemptScopes.map(
|
|
906
|
+
(scope) => ({ ...scope })
|
|
907
|
+
);
|
|
908
|
+
}
|
|
804
909
|
if (entry.detection.failClosedReason !== void 0) {
|
|
805
910
|
detection.failClosedReason = entry.detection.failClosedReason;
|
|
806
911
|
}
|
|
@@ -925,6 +1030,7 @@ function dropSessionBoundRecords(state, now) {
|
|
|
925
1030
|
delete next.sessions[host];
|
|
926
1031
|
}
|
|
927
1032
|
}
|
|
1033
|
+
delete next.exemptions;
|
|
928
1034
|
return next;
|
|
929
1035
|
}
|
|
930
1036
|
function createSessionId() {
|
|
@@ -958,10 +1064,36 @@ function parseState(value) {
|
|
|
958
1064
|
for (const [host, record] of Object.entries(value.sessions)) {
|
|
959
1065
|
sessions[host] = parseSessionRecord(record);
|
|
960
1066
|
}
|
|
961
|
-
|
|
1067
|
+
const state = {
|
|
962
1068
|
sessions,
|
|
963
1069
|
auditLog: value.auditLog.map(parseAuditEntry)
|
|
964
1070
|
};
|
|
1071
|
+
if (value.exemptions !== void 0) {
|
|
1072
|
+
if (!isRecord(value.exemptions)) {
|
|
1073
|
+
throw new Error("invalid standdown state");
|
|
1074
|
+
}
|
|
1075
|
+
const exemptions = {};
|
|
1076
|
+
for (const [host, record] of Object.entries(value.exemptions)) {
|
|
1077
|
+
exemptions[host] = parseExemptionRecord(record);
|
|
1078
|
+
}
|
|
1079
|
+
state.exemptions = exemptions;
|
|
1080
|
+
}
|
|
1081
|
+
return state;
|
|
1082
|
+
}
|
|
1083
|
+
function parseExemptionRecord(value) {
|
|
1084
|
+
if (!isRecord(value)) {
|
|
1085
|
+
throw new Error("invalid exemption record");
|
|
1086
|
+
}
|
|
1087
|
+
const { advertiserHost, policyIds, networkIds, grantedAt } = value;
|
|
1088
|
+
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")) {
|
|
1089
|
+
throw new Error("invalid exemption record");
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
advertiserHost,
|
|
1093
|
+
policyIds: [...policyIds],
|
|
1094
|
+
networkIds: [...networkIds],
|
|
1095
|
+
grantedAt
|
|
1096
|
+
};
|
|
965
1097
|
}
|
|
966
1098
|
function parseSessionRecord(value) {
|
|
967
1099
|
if (!isRecord(value)) {
|
|
@@ -1019,7 +1151,7 @@ function parseAuditEntry(value) {
|
|
|
1019
1151
|
return entry;
|
|
1020
1152
|
}
|
|
1021
1153
|
function cloneState(state) {
|
|
1022
|
-
|
|
1154
|
+
const next = {
|
|
1023
1155
|
sessions: Object.fromEntries(
|
|
1024
1156
|
Object.entries(state.sessions).map(([host, record]) => [
|
|
1025
1157
|
host,
|
|
@@ -1028,6 +1160,19 @@ function cloneState(state) {
|
|
|
1028
1160
|
),
|
|
1029
1161
|
auditLog: state.auditLog.map(cloneAuditEntry2)
|
|
1030
1162
|
};
|
|
1163
|
+
if (state.exemptions) {
|
|
1164
|
+
next.exemptions = Object.fromEntries(
|
|
1165
|
+
Object.entries(state.exemptions).map(([host, record]) => [
|
|
1166
|
+
host,
|
|
1167
|
+
{
|
|
1168
|
+
...record,
|
|
1169
|
+
policyIds: [...record.policyIds],
|
|
1170
|
+
networkIds: [...record.networkIds]
|
|
1171
|
+
}
|
|
1172
|
+
])
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
return next;
|
|
1031
1176
|
}
|
|
1032
1177
|
function cloneSessionRecord(record) {
|
|
1033
1178
|
const next = {
|
|
@@ -1063,6 +1208,11 @@ function cloneAuditEntry2(entry) {
|
|
|
1063
1208
|
if (entry.detection.strongest !== void 0) {
|
|
1064
1209
|
next.detection.strongest = { ...entry.detection.strongest };
|
|
1065
1210
|
}
|
|
1211
|
+
if (entry.detection.selfExemptScopes !== void 0) {
|
|
1212
|
+
next.detection.selfExemptScopes = entry.detection.selfExemptScopes.map(
|
|
1213
|
+
(scope) => ({ ...scope })
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1066
1216
|
if (entry.detection.failClosedReason !== void 0) {
|
|
1067
1217
|
next.detection.failClosedReason = entry.detection.failClosedReason;
|
|
1068
1218
|
}
|
|
@@ -1097,10 +1247,7 @@ function createContentStanddown(opts) {
|
|
|
1097
1247
|
const windowLike = opts.window ?? currentWindow();
|
|
1098
1248
|
const now = opts.now ?? Date.now;
|
|
1099
1249
|
const store = opts.store ?? createContentStore(windowLike, opts, now);
|
|
1100
|
-
const session = new StanddownSession(
|
|
1101
|
-
store,
|
|
1102
|
-
opts.auditLog === void 0 ? void 0 : { auditLog: opts.auditLog }
|
|
1103
|
-
);
|
|
1250
|
+
const session = new StanddownSession(store, contentSessionOptions(opts));
|
|
1104
1251
|
let disposed = false;
|
|
1105
1252
|
let pending = false;
|
|
1106
1253
|
const restoreHistory = patchHistory(windowLike, scheduleEvaluation);
|
|
@@ -1181,7 +1328,10 @@ function collectContentSignals(opts = {}) {
|
|
|
1181
1328
|
}
|
|
1182
1329
|
const signals = {
|
|
1183
1330
|
url: windowLike.location.href,
|
|
1184
|
-
now: now()
|
|
1331
|
+
now: now(),
|
|
1332
|
+
// The content plane never observes redirect chains, so a non-stand-down
|
|
1333
|
+
// here can miss redirect-only attribution.
|
|
1334
|
+
signalCoverage: "partial"
|
|
1185
1335
|
};
|
|
1186
1336
|
const referrer = windowLike.document.referrer;
|
|
1187
1337
|
const cookieNames = cookieNamesFromString(windowLike.document.cookie);
|
|
@@ -1205,6 +1355,16 @@ function cookieNamesFromString(cookie) {
|
|
|
1205
1355
|
}
|
|
1206
1356
|
return cookie.split(";").map((part) => part.trim()).filter((part) => part.length > 0).map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0);
|
|
1207
1357
|
}
|
|
1358
|
+
function contentSessionOptions(opts) {
|
|
1359
|
+
const sessionOpts = {};
|
|
1360
|
+
if (opts.auditLog !== void 0) {
|
|
1361
|
+
sessionOpts.auditLog = opts.auditLog;
|
|
1362
|
+
}
|
|
1363
|
+
if (opts.selfExemptionScope !== void 0) {
|
|
1364
|
+
sessionOpts.selfExemptionScope = opts.selfExemptionScope;
|
|
1365
|
+
}
|
|
1366
|
+
return Object.keys(sessionOpts).length > 0 ? sessionOpts : void 0;
|
|
1367
|
+
}
|
|
1208
1368
|
function createContentStore(windowLike, opts, now) {
|
|
1209
1369
|
if (windowLike === void 0) {
|
|
1210
1370
|
return new UnavailableStateStore("content window unavailable");
|