standdown 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1288 @@
1
+ 'use strict';
2
+
3
+ // src/detect.ts
4
+ var ORGANIC_REFERRER_SUFFIXES = [
5
+ "google.com",
6
+ "bing.com",
7
+ "duckduckgo.com",
8
+ "yahoo.com",
9
+ "ecosia.org",
10
+ "baidu.com",
11
+ "yandex.com"
12
+ ];
13
+ function detect(signals, policies) {
14
+ const currentUrl = parseUrl(signals.url);
15
+ if (!currentUrl) {
16
+ return {
17
+ matched: [],
18
+ selfMatch: false,
19
+ failClosedReason: "invalid-url"
20
+ };
21
+ }
22
+ const advertiserHost = currentUrl.hostname.toLowerCase();
23
+ const matched = [];
24
+ let selfMatch = false;
25
+ for (const policy of policies) {
26
+ const scopedSelfMatch = hasScopedSelfExemption(
27
+ currentUrl,
28
+ signals.selfPatterns,
29
+ policy
30
+ );
31
+ const unscopedSelfMatch = hasUnscopedSelfExemption(
32
+ currentUrl,
33
+ signals.selfPatterns
34
+ );
35
+ if (scopedSelfMatch || unscopedSelfMatch) {
36
+ selfMatch = true;
37
+ }
38
+ if (scopedSelfMatch) {
39
+ continue;
40
+ }
41
+ matched.push(
42
+ ...collectPolicyMatches(policy, signals, currentUrl, advertiserHost)
43
+ );
44
+ }
45
+ matched.sort((left, right) => kindPriority(left.kind) - kindPriority(right.kind));
46
+ const strongest = matched[0] ? {
47
+ policyId: matched[0].policyId,
48
+ advertiserHost: matched[0].advertiserHost,
49
+ reason: matched[0].reason
50
+ } : void 0;
51
+ return strongest ? { matched, selfMatch, strongest } : { matched, selfMatch };
52
+ }
53
+ function classifyReferrer(signals, advertiserHost) {
54
+ const candidate = signals.initiator ?? signals.referrer;
55
+ if (!candidate) {
56
+ return "direct";
57
+ }
58
+ const referrerHost = hostFromUrl(candidate);
59
+ if (!referrerHost) {
60
+ return "other";
61
+ }
62
+ if ((signals.publisherSites ?? []).some(
63
+ (site) => domainSuffixMatches(referrerHost, site)
64
+ )) {
65
+ return "own-site";
66
+ }
67
+ if (domainSuffixMatches(referrerHost, advertiserHost)) {
68
+ return "advertiser-internal";
69
+ }
70
+ if (ORGANIC_REFERRER_SUFFIXES.some(
71
+ (suffix) => domainSuffixMatches(referrerHost, suffix)
72
+ )) {
73
+ return "organic";
74
+ }
75
+ return "other";
76
+ }
77
+ function domainRuleMatchesUrl(rule, value) {
78
+ const url = parseUrl(value);
79
+ const host = url?.hostname.toLowerCase() ?? value.toLowerCase();
80
+ if (rule.kind === "suffix") {
81
+ return domainSuffixMatches(host, rule.pattern);
82
+ }
83
+ try {
84
+ const regex = new RegExp(rule.pattern, "i");
85
+ return regex.test(host) || (url ? regex.test(url.href) : regex.test(value));
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ function collectPolicyMatches(policy, signals, currentUrl, advertiserHost) {
91
+ const matches = [];
92
+ const advertiserHostMatches = policy.detection.advertiserHosts === void 0 || policy.detection.advertiserHosts.some(
93
+ (rule) => domainRuleMatchesUrl(rule, advertiserHost)
94
+ );
95
+ if (advertiserHostMatches) {
96
+ for (const rule of policy.detection.landingParams ?? []) {
97
+ if (paramRuleMatches(rule, currentUrl.searchParams)) {
98
+ matches.push(
99
+ matchedRule(
100
+ policy,
101
+ "landing-param",
102
+ describeParamRule(rule),
103
+ advertiserHost,
104
+ rule.reason ?? "landing parameter matched"
105
+ )
106
+ );
107
+ }
108
+ }
109
+ }
110
+ for (const rule of policy.detection.redirectDomains ?? []) {
111
+ if ((signals.redirectChain ?? []).some((url) => domainRuleMatchesUrl(rule, url))) {
112
+ matches.push(
113
+ matchedRule(
114
+ policy,
115
+ "redirect-domain",
116
+ `${rule.kind}:${rule.pattern}`,
117
+ advertiserHost,
118
+ rule.comment ?? "redirect domain matched"
119
+ )
120
+ );
121
+ }
122
+ }
123
+ if (advertiserHostMatches) {
124
+ for (const rule of policy.detection.cookiePatterns ?? []) {
125
+ if (cookieRuleMatches(rule, signals.cookieNames ?? [])) {
126
+ matches.push(
127
+ matchedRule(
128
+ policy,
129
+ "cookie",
130
+ `${rule.match}:${rule.name}`,
131
+ advertiserHost,
132
+ rule.reason ?? "first-party cookie name matched"
133
+ )
134
+ );
135
+ }
136
+ }
137
+ }
138
+ if (advertiserHostMatches) {
139
+ for (const rule of policy.detection.initiatorRules ?? []) {
140
+ if (initiatorRuleMatches(rule, signals, advertiserHost)) {
141
+ matches.push(
142
+ matchedRule(
143
+ policy,
144
+ "initiator",
145
+ `referrer-class:${rule.referrerClass}`,
146
+ advertiserHost,
147
+ rule.reason ?? "initiator/referrer class matched"
148
+ )
149
+ );
150
+ }
151
+ }
152
+ }
153
+ return matches;
154
+ }
155
+ function matchedRule(policy, kind, rule, advertiserHost, reason) {
156
+ return {
157
+ policyId: policy.id,
158
+ networkId: policy.network.id,
159
+ networkName: policy.network.name,
160
+ kind,
161
+ rule,
162
+ advertiserHost,
163
+ reason,
164
+ sourceUrl: policy.metadata.sourceUrl
165
+ };
166
+ }
167
+ function kindPriority(kind) {
168
+ if (kind === "redirect-domain") {
169
+ return 0;
170
+ }
171
+ if (kind === "landing-param") {
172
+ return 1;
173
+ }
174
+ if (kind === "cookie") {
175
+ return 2;
176
+ }
177
+ return 3;
178
+ }
179
+ function paramRuleMatches(rule, params) {
180
+ return rule.anyOf.some(
181
+ (group) => group.allOf.every((matcher) => paramMatcherMatches(matcher, params))
182
+ );
183
+ }
184
+ function paramMatcherMatches(matcher, params) {
185
+ const values = params.getAll(matcher.name);
186
+ const mode = matcher.match ?? (matcher.value === void 0 ? "exists" : "equals");
187
+ if (mode === "exists") {
188
+ return values.length > 0;
189
+ }
190
+ const expectedValue = matcher.value;
191
+ if (expectedValue === void 0) {
192
+ return false;
193
+ }
194
+ if (mode === "equals") {
195
+ return values.some((value) => value === expectedValue);
196
+ }
197
+ return values.some((value) => value.includes(expectedValue));
198
+ }
199
+ function cookieRuleMatches(rule, cookieNames) {
200
+ if (rule.match === "exact") {
201
+ return cookieNames.some((name) => name === rule.name);
202
+ }
203
+ return cookieNames.some((name) => name.includes(rule.name));
204
+ }
205
+ function initiatorRuleMatches(rule, signals, advertiserHost) {
206
+ return classifyReferrer(signals, advertiserHost) === rule.referrerClass;
207
+ }
208
+ function hasScopedSelfExemption(currentUrl, selfPatterns, policy) {
209
+ return (selfPatterns ?? []).some((pattern) => {
210
+ const scopedToPolicy = pattern.policyId === policy.id;
211
+ const scopedToNetwork = pattern.networkId === policy.network.id;
212
+ return (scopedToPolicy || scopedToNetwork) && paramMatcherMatches(pattern, currentUrl.searchParams);
213
+ });
214
+ }
215
+ function hasUnscopedSelfExemption(currentUrl, selfPatterns) {
216
+ return (selfPatterns ?? []).some(
217
+ (pattern) => pattern.policyId === void 0 && pattern.networkId === void 0 && paramMatcherMatches(pattern, currentUrl.searchParams)
218
+ );
219
+ }
220
+ function domainSuffixMatches(host, pattern) {
221
+ const normalizedHost = host.toLowerCase().replace(/\.$/, "");
222
+ const normalizedPattern = pattern.toLowerCase().replace(/^\./, "").replace(/\.$/, "");
223
+ return normalizedHost === normalizedPattern || normalizedHost.endsWith(`.${normalizedPattern}`);
224
+ }
225
+ function describeParamRule(rule) {
226
+ return rule.anyOf.map(
227
+ (group) => group.allOf.map((matcher) => {
228
+ const mode = matcher.match ?? (matcher.value === void 0 ? "exists" : "equals");
229
+ return matcher.value === void 0 ? `${matcher.name}:${mode}` : `${matcher.name}:${mode}:${matcher.value}`;
230
+ }).join("&")
231
+ ).join("|");
232
+ }
233
+ function parseUrl(value) {
234
+ try {
235
+ return new URL(value);
236
+ } catch {
237
+ return void 0;
238
+ }
239
+ }
240
+ function hostFromUrl(value) {
241
+ return parseUrl(value)?.hostname.toLowerCase();
242
+ }
243
+
244
+ // src/validation.ts
245
+ var BEHAVIORS = /* @__PURE__ */ new Set([
246
+ "suppress-prompts",
247
+ "no-cookie-write",
248
+ "no-redirect",
249
+ "no-background-tracking"
250
+ ]);
251
+ var PARAM_MATCHES = /* @__PURE__ */ new Set(["exists", "equals", "contains"]);
252
+ var COOKIE_MATCHES = /* @__PURE__ */ new Set(["exact", "substring"]);
253
+ var DOMAIN_KINDS = /* @__PURE__ */ new Set(["suffix", "regex"]);
254
+ var REFERRER_CLASSES = /* @__PURE__ */ new Set([
255
+ "own-site",
256
+ "organic",
257
+ "direct",
258
+ "advertiser-internal",
259
+ "other"
260
+ ]);
261
+ var ACTIVATION_REFERRER_CLASSES = /* @__PURE__ */ new Set(["own-site", "organic", "direct"]);
262
+ function validatePolicy(value) {
263
+ const policy = object(value, "policy");
264
+ string(policy.id, "policy.id");
265
+ literal(policy.schemaVersion, 3, "policy.schemaVersion");
266
+ string(policy.policyVersion, "policy.policyVersion");
267
+ const network = object(policy.network, "policy.network");
268
+ string(network.id, "policy.network.id");
269
+ string(network.name, "policy.network.name");
270
+ if (network.policyUrl !== void 0) {
271
+ urlString(network.policyUrl, "policy.network.policyUrl");
272
+ }
273
+ const detection = object(policy.detection, "policy.detection");
274
+ optionalArray(detection.advertiserHosts, "policy.detection.advertiserHosts").forEach(
275
+ validateDomainRule
276
+ );
277
+ optionalArray(detection.landingParams, "policy.detection.landingParams").forEach(
278
+ validateParamRule
279
+ );
280
+ optionalArray(detection.redirectDomains, "policy.detection.redirectDomains").forEach(
281
+ validateDomainRule
282
+ );
283
+ optionalArray(detection.cookiePatterns, "policy.detection.cookiePatterns").forEach(
284
+ validateCookieRule
285
+ );
286
+ optionalArray(detection.initiatorRules, "policy.detection.initiatorRules").forEach(
287
+ validateInitiatorRule
288
+ );
289
+ const standdown = object(policy.standdown, "policy.standdown");
290
+ literal(standdown.scope, "advertiser", "policy.standdown.scope");
291
+ if (standdown.sessionRule !== "session-or-min" && standdown.sessionRule !== "inactivity-window") {
292
+ throw new TypeError("policy.standdown.sessionRule is invalid");
293
+ }
294
+ nonNegativeFiniteNumber(
295
+ standdown.minDurationMs,
296
+ "policy.standdown.minDurationMs"
297
+ );
298
+ if (standdown.inactivityMs !== void 0) {
299
+ nonNegativeFiniteNumber(
300
+ standdown.inactivityMs,
301
+ "policy.standdown.inactivityMs"
302
+ );
303
+ }
304
+ const behaviors = array(standdown.behaviors, "policy.standdown.behaviors");
305
+ for (const behavior of behaviors) {
306
+ if (!BEHAVIORS.has(behavior)) {
307
+ throw new TypeError(`policy.standdown.behaviors contains invalid behavior`);
308
+ }
309
+ }
310
+ const activation = object(policy.activation, "policy.activation");
311
+ if (activation.mode !== "user-click" && activation.mode !== "never") {
312
+ throw new TypeError("policy.activation.mode is invalid");
313
+ }
314
+ if (activation.maxPromptsPerJourney !== void 0) {
315
+ nonNegativeFiniteNumber(
316
+ activation.maxPromptsPerJourney,
317
+ "policy.activation.maxPromptsPerJourney"
318
+ );
319
+ }
320
+ for (const referrerClass of optionalArray(
321
+ activation.allowedReferrerClasses,
322
+ "policy.activation.allowedReferrerClasses"
323
+ )) {
324
+ if (!ACTIVATION_REFERRER_CLASSES.has(referrerClass)) {
325
+ throw new TypeError(
326
+ "policy.activation.allowedReferrerClasses contains invalid class"
327
+ );
328
+ }
329
+ }
330
+ const metadata = object(policy.metadata, "policy.metadata");
331
+ urlString(metadata.sourceUrl, "policy.metadata.sourceUrl");
332
+ dateString(metadata.lastVerified, "policy.metadata.lastVerified");
333
+ if (metadata.notes !== void 0) {
334
+ string(metadata.notes, "policy.metadata.notes");
335
+ }
336
+ }
337
+ function validatePolicies(values) {
338
+ values.forEach(validatePolicy);
339
+ }
340
+ function validateParamRule(ruleValue) {
341
+ const rule = object(ruleValue, "ParamRule");
342
+ const anyOf = array(rule.anyOf, "ParamRule.anyOf");
343
+ if (anyOf.length === 0) {
344
+ throw new TypeError("ParamRule.anyOf must not be empty");
345
+ }
346
+ for (const groupValue of anyOf) {
347
+ const group = object(groupValue, "ParamRule.anyOf[]");
348
+ const allOf = array(group.allOf, "ParamRule.anyOf[].allOf");
349
+ if (allOf.length === 0) {
350
+ throw new TypeError("ParamRule.anyOf[].allOf must not be empty");
351
+ }
352
+ allOf.forEach(validateParamMatcher);
353
+ }
354
+ if (rule.reason !== void 0) {
355
+ string(rule.reason, "ParamRule.reason");
356
+ }
357
+ }
358
+ function validateParamMatcher(matcherValue) {
359
+ const matcher = object(matcherValue, "ParamMatcher");
360
+ string(matcher.name, "ParamMatcher.name");
361
+ if (matcher.name.trim() === "") {
362
+ throw new TypeError("ParamMatcher.name must not be empty");
363
+ }
364
+ if (matcher.value !== void 0) {
365
+ string(matcher.value, "ParamMatcher.value");
366
+ }
367
+ if (matcher.match !== void 0 && !PARAM_MATCHES.has(matcher.match)) {
368
+ throw new TypeError("ParamMatcher.match is invalid");
369
+ }
370
+ if ((matcher.match === "equals" || matcher.match === "contains") && matcher.value === void 0) {
371
+ throw new TypeError("ParamMatcher.value is required for equals/contains");
372
+ }
373
+ }
374
+ function validateDomainRule(ruleValue) {
375
+ const rule = object(ruleValue, "DomainRule");
376
+ string(rule.pattern, "DomainRule.pattern");
377
+ if (!DOMAIN_KINDS.has(rule.kind)) {
378
+ throw new TypeError("DomainRule.kind is invalid");
379
+ }
380
+ if (rule.kind === "regex") {
381
+ try {
382
+ new RegExp(rule.pattern);
383
+ } catch {
384
+ throw new TypeError("DomainRule.pattern must be a valid regex");
385
+ }
386
+ }
387
+ if (rule.comment !== void 0) {
388
+ string(rule.comment, "DomainRule.comment");
389
+ }
390
+ }
391
+ function validateCookieRule(ruleValue) {
392
+ const rule = object(ruleValue, "CookieRule");
393
+ string(rule.name, "CookieRule.name");
394
+ if (!COOKIE_MATCHES.has(rule.match)) {
395
+ throw new TypeError("CookieRule.match is invalid");
396
+ }
397
+ if (rule.reason !== void 0) {
398
+ string(rule.reason, "CookieRule.reason");
399
+ }
400
+ }
401
+ function validateInitiatorRule(ruleValue) {
402
+ const rule = object(ruleValue, "InitiatorRule");
403
+ if (!REFERRER_CLASSES.has(rule.referrerClass)) {
404
+ throw new TypeError("InitiatorRule.referrerClass is invalid");
405
+ }
406
+ if (rule.reason !== void 0) {
407
+ string(rule.reason, "InitiatorRule.reason");
408
+ }
409
+ }
410
+ function object(value, path) {
411
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
412
+ throw new TypeError(`${path} must be an object`);
413
+ }
414
+ return value;
415
+ }
416
+ function string(value, path) {
417
+ if (typeof value !== "string" || value.trim() === "") {
418
+ throw new TypeError(`${path} must be a non-empty string`);
419
+ }
420
+ }
421
+ function literal(value, expected, path) {
422
+ if (value !== expected) {
423
+ throw new TypeError(`${path} must be ${String(expected)}`);
424
+ }
425
+ }
426
+ function array(value, path) {
427
+ if (!Array.isArray(value)) {
428
+ throw new TypeError(`${path} must be an array`);
429
+ }
430
+ return value;
431
+ }
432
+ function optionalArray(value, path) {
433
+ if (value === void 0) {
434
+ return [];
435
+ }
436
+ return array(value, path);
437
+ }
438
+ function nonNegativeFiniteNumber(value, path) {
439
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
440
+ throw new TypeError(`${path} must be a non-negative finite number`);
441
+ }
442
+ }
443
+ function urlString(value, path) {
444
+ string(value, path);
445
+ try {
446
+ new URL(value);
447
+ } catch {
448
+ throw new TypeError(`${path} must be an absolute URL`);
449
+ }
450
+ }
451
+ function dateString(value, path) {
452
+ string(value, path);
453
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
454
+ throw new TypeError(`${path} must be YYYY-MM-DD`);
455
+ }
456
+ }
457
+
458
+ // src/session.ts
459
+ var StanddownSession = class {
460
+ #store;
461
+ #auditLog;
462
+ #maxAuditEntries;
463
+ #readOnlyAuditLog = [];
464
+ constructor(store, opts) {
465
+ this.#store = store;
466
+ this.#auditLog = opts?.auditLog ?? true;
467
+ this.#maxAuditEntries = Math.max(0, opts?.maxAuditEntries ?? 1e3);
468
+ }
469
+ async ingest(signals, policies) {
470
+ const advertiserHost = hostFromUrl2(signals.url);
471
+ try {
472
+ validatePolicies(policies);
473
+ } catch (error) {
474
+ return this.#failClosedWithAudit(
475
+ signals.now,
476
+ "ingest",
477
+ advertiserHost,
478
+ `malformed-policy: ${messageFromError(error)}`
479
+ );
480
+ }
481
+ const detection = detect(signals, policies);
482
+ if (detection.failClosedReason) {
483
+ return this.#failClosedWithAudit(
484
+ signals.now,
485
+ "ingest",
486
+ advertiserHost,
487
+ detection.failClosedReason,
488
+ detection
489
+ );
490
+ }
491
+ return this.#withState(signals.now, (state) => {
492
+ pruneExpiredSessions(state, signals.now);
493
+ if (detection.strongest) {
494
+ const matchedPolicies = policiesForDetection(policies, detection);
495
+ if (matchedPolicies.length === 0) {
496
+ return {
497
+ decision: failClosedDecision("matched-policy-missing"),
498
+ detection
499
+ };
500
+ }
501
+ const record = upsertSessionRecord(
502
+ state,
503
+ detection.strongest.advertiserHost,
504
+ detection.strongest.policyId,
505
+ matchedPolicies,
506
+ signals.now
507
+ );
508
+ return {
509
+ decision: decisionFromRecord(record, signals.now, {
510
+ reason: detection.strongest.reason,
511
+ referrerClass: classifyReferrer(signals, record.advertiserHost)
512
+ }),
513
+ detection
514
+ };
515
+ }
516
+ const activeDecision = advertiserHost ? activeDecisionForHost(state, advertiserHost, signals.now) : void 0;
517
+ return {
518
+ decision: activeDecision ?? {
519
+ standDown: false,
520
+ reason: detection.selfMatch ? "self-exempted-no-active-standdown" : "no-active-standdown",
521
+ behaviors: [],
522
+ referrerClass: advertiserHost ? classifyReferrer(signals, advertiserHost) : "other"
523
+ },
524
+ detection
525
+ };
526
+ }, "ingest");
527
+ }
528
+ async shouldStandDown(advertiserHost, now) {
529
+ return this.#withState(
530
+ now,
531
+ (state) => {
532
+ pruneExpiredSessions(state, now);
533
+ return {
534
+ decision: activeDecisionForHost(state, advertiserHost, now) ?? {
535
+ standDown: false,
536
+ reason: "no-active-standdown",
537
+ behaviors: []
538
+ }
539
+ };
540
+ },
541
+ "shouldStandDown",
542
+ normalizeHost(advertiserHost),
543
+ { persist: false }
544
+ );
545
+ }
546
+ async recordActivity(now) {
547
+ await this.#withState(
548
+ now,
549
+ (state) => {
550
+ pruneExpiredSessions(state, now);
551
+ for (const record of Object.values(state.sessions)) {
552
+ if (record.sessionRule === "inactivity-window") {
553
+ record.lastActivityAt = now;
554
+ const expiresAt = computeExpiresAt(record);
555
+ if (expiresAt !== void 0) {
556
+ record.expiresAt = expiresAt;
557
+ }
558
+ }
559
+ }
560
+ return {
561
+ decision: {
562
+ standDown: false,
563
+ reason: "activity-recorded",
564
+ behaviors: []
565
+ }
566
+ };
567
+ },
568
+ "recordActivity"
569
+ );
570
+ }
571
+ async exportAuditLog() {
572
+ const state = await this.#loadState();
573
+ return trimAuditLog(
574
+ [...state.auditLog, ...this.#readOnlyAuditLog],
575
+ this.#maxAuditEntries
576
+ ).map(cloneAuditEntry);
577
+ }
578
+ async #withState(now, fn, action, advertiserHost, opts = {}) {
579
+ let state;
580
+ try {
581
+ state = await this.#loadState();
582
+ } catch {
583
+ return failClosedDecision("store-error");
584
+ }
585
+ const result = fn(state);
586
+ if (this.#auditLog) {
587
+ const entry = auditEntry({
588
+ time: now,
589
+ action,
590
+ advertiserHost: advertiserHost ?? result.detection?.strongest?.advertiserHost,
591
+ detection: result.detection,
592
+ decision: result.decision
593
+ });
594
+ if (opts.persist === false) {
595
+ appendAuditEntry(this.#readOnlyAuditLog, entry, this.#maxAuditEntries);
596
+ } else {
597
+ appendAuditEntry(state.auditLog, entry, this.#maxAuditEntries);
598
+ }
599
+ }
600
+ if (opts.persist === false) {
601
+ return result.decision;
602
+ }
603
+ try {
604
+ await this.#store.save(state);
605
+ } catch {
606
+ return failClosedDecision("store-error");
607
+ }
608
+ return result.decision;
609
+ }
610
+ async #failClosedWithAudit(now, action, advertiserHost, reason, detection) {
611
+ const decision = failClosedDecision(reason);
612
+ if (!this.#auditLog) {
613
+ return decision;
614
+ }
615
+ try {
616
+ const state = await this.#loadState();
617
+ appendAuditEntry(
618
+ state.auditLog,
619
+ auditEntry({
620
+ time: now,
621
+ action,
622
+ advertiserHost,
623
+ detection,
624
+ decision
625
+ }),
626
+ this.#maxAuditEntries
627
+ );
628
+ await this.#store.save(state);
629
+ } catch {
630
+ return failClosedDecision("store-error");
631
+ }
632
+ return decision;
633
+ }
634
+ async #loadState() {
635
+ return await this.#store.load() ?? { sessions: {}, auditLog: [] };
636
+ }
637
+ };
638
+ function appendAuditEntry(auditLog, entry, maxEntries) {
639
+ if (maxEntries === 0) {
640
+ auditLog.length = 0;
641
+ return;
642
+ }
643
+ auditLog.push(entry);
644
+ if (auditLog.length > maxEntries) {
645
+ auditLog.splice(0, auditLog.length - maxEntries);
646
+ }
647
+ }
648
+ function trimAuditLog(auditLog, maxEntries) {
649
+ if (maxEntries === 0) {
650
+ return [];
651
+ }
652
+ return auditLog.slice(Math.max(0, auditLog.length - maxEntries));
653
+ }
654
+ function upsertSessionRecord(state, advertiserHost, primaryPolicyId, policies, now) {
655
+ const key = normalizeHost(advertiserHost);
656
+ const existing = state.sessions[key];
657
+ const startedAt = existing?.startedAt ?? now;
658
+ const lastActivityAt = now;
659
+ const sessionRule = existing?.sessionRule === "session-or-min" || policies.some((policy) => policy.standdown.sessionRule === "session-or-min") ? "session-or-min" : "inactivity-window";
660
+ const baseRecord = {
661
+ advertiserHost: key,
662
+ policyId: primaryPolicyId,
663
+ startedAt,
664
+ lastActivityAt,
665
+ sessionRule,
666
+ minDurationMs: Math.max(
667
+ existing?.minDurationMs ?? 0,
668
+ ...policies.map((policy) => policy.standdown.minDurationMs)
669
+ ),
670
+ behaviors: unionBehaviors([
671
+ ...existing?.behaviors ?? [],
672
+ ...policies.flatMap((policy) => policy.standdown.behaviors)
673
+ ])
674
+ };
675
+ if (sessionRule === "inactivity-window") {
676
+ const inactivityMs = Math.max(
677
+ existing?.inactivityMs ?? 0,
678
+ ...policies.flatMap(
679
+ (policy) => policy.standdown.inactivityMs === void 0 ? [] : [policy.standdown.inactivityMs]
680
+ )
681
+ );
682
+ if (inactivityMs > 0) {
683
+ baseRecord.inactivityMs = inactivityMs;
684
+ }
685
+ }
686
+ const computedExpiresAt = computeExpiresAt(baseRecord);
687
+ if (computedExpiresAt !== void 0) {
688
+ baseRecord.expiresAt = Math.max(existing?.expiresAt ?? 0, computedExpiresAt);
689
+ }
690
+ state.sessions[key] = baseRecord;
691
+ return baseRecord;
692
+ }
693
+ function policiesForDetection(policies, detection) {
694
+ if (detection.strongest === void 0) {
695
+ return [];
696
+ }
697
+ const matchingPolicyIds = new Set(
698
+ detection.matched.filter(
699
+ (match) => match.advertiserHost === detection.strongest?.advertiserHost
700
+ ).map((match) => match.policyId)
701
+ );
702
+ return policies.filter((policy) => matchingPolicyIds.has(policy.id));
703
+ }
704
+ function unionBehaviors(behaviors) {
705
+ return [...new Set(behaviors)];
706
+ }
707
+ function activeDecisionForHost(state, advertiserHost, now) {
708
+ const record = state.sessions[normalizeHost(advertiserHost)];
709
+ if (!record || !isActive(record, now)) {
710
+ return void 0;
711
+ }
712
+ return decisionFromRecord(record, now);
713
+ }
714
+ function decisionFromRecord(record, now, overrides) {
715
+ const expiresAt = computeExpiresAt(record);
716
+ const decision = {
717
+ standDown: true,
718
+ policyId: record.policyId,
719
+ reason: overrides?.reason ?? `active-standdown:${record.policyId}`,
720
+ behaviors: [...record.behaviors]
721
+ };
722
+ if (expiresAt !== void 0) {
723
+ decision.expiresAt = expiresAt;
724
+ }
725
+ if (overrides?.referrerClass !== void 0) {
726
+ decision.referrerClass = overrides.referrerClass;
727
+ }
728
+ if (!isActive({ ...record, ...expiresAt === void 0 ? {} : { expiresAt } }, now)) {
729
+ return {
730
+ standDown: false,
731
+ reason: "standdown-expired",
732
+ behaviors: []
733
+ };
734
+ }
735
+ return decision;
736
+ }
737
+ function computeExpiresAt(record) {
738
+ if (record.sessionRule === "inactivity-window" && record.inactivityMs !== void 0) {
739
+ return Math.max(
740
+ record.startedAt + record.minDurationMs,
741
+ record.lastActivityAt + record.inactivityMs
742
+ );
743
+ }
744
+ return record.expiresAt;
745
+ }
746
+ function isActive(record, now) {
747
+ if (record.sessionRule === "session-or-min") {
748
+ return true;
749
+ }
750
+ const expiresAt = computeExpiresAt(record);
751
+ return expiresAt === void 0 ? false : now < expiresAt;
752
+ }
753
+ function pruneExpiredSessions(state, now) {
754
+ for (const [host, record] of Object.entries(state.sessions)) {
755
+ if (!isActive(record, now)) {
756
+ delete state.sessions[host];
757
+ }
758
+ }
759
+ }
760
+ function failClosedDecision(reason) {
761
+ return {
762
+ standDown: true,
763
+ reason,
764
+ behaviors: [
765
+ "suppress-prompts",
766
+ "no-cookie-write",
767
+ "no-redirect",
768
+ "no-background-tracking"
769
+ ]
770
+ };
771
+ }
772
+ function auditEntry(value) {
773
+ const entry = {
774
+ time: value.time,
775
+ action: value.action
776
+ };
777
+ if (value.advertiserHost !== void 0) {
778
+ entry.advertiserHost = value.advertiserHost;
779
+ }
780
+ if (value.detection !== void 0) {
781
+ entry.detection = value.detection;
782
+ }
783
+ if (value.decision !== void 0) {
784
+ entry.decision = value.decision;
785
+ }
786
+ return entry;
787
+ }
788
+ function cloneAuditEntry(entry) {
789
+ const cloned = {
790
+ time: entry.time,
791
+ action: entry.action
792
+ };
793
+ if (entry.advertiserHost !== void 0) {
794
+ cloned.advertiserHost = entry.advertiserHost;
795
+ }
796
+ if (entry.detection !== void 0) {
797
+ const detection = {
798
+ matched: entry.detection.matched.map((match) => ({ ...match })),
799
+ selfMatch: entry.detection.selfMatch
800
+ };
801
+ if (entry.detection.strongest !== void 0) {
802
+ detection.strongest = { ...entry.detection.strongest };
803
+ }
804
+ if (entry.detection.failClosedReason !== void 0) {
805
+ detection.failClosedReason = entry.detection.failClosedReason;
806
+ }
807
+ cloned.detection = detection;
808
+ }
809
+ if (entry.decision !== void 0) {
810
+ cloned.decision = {
811
+ ...entry.decision,
812
+ behaviors: [...entry.decision.behaviors]
813
+ };
814
+ }
815
+ return cloned;
816
+ }
817
+ function normalizeHost(host) {
818
+ return host.toLowerCase().replace(/\.$/, "");
819
+ }
820
+ function hostFromUrl2(value) {
821
+ try {
822
+ return normalizeHost(new URL(value).hostname);
823
+ } catch {
824
+ return void 0;
825
+ }
826
+ }
827
+ function messageFromError(error) {
828
+ return error instanceof Error ? error.message : String(error);
829
+ }
830
+
831
+ // src/stores.ts
832
+ var DEFAULT_STATE_KEY = "standdown:state:v1";
833
+ var DEFAULT_SESSION_ID_KEY = "standdown:session-id:v1";
834
+ var SessionStorageStateStore = class {
835
+ #storage;
836
+ #key;
837
+ constructor(storage, opts = {}) {
838
+ this.#storage = storage;
839
+ this.#key = opts.key ?? DEFAULT_STATE_KEY;
840
+ }
841
+ async load() {
842
+ const raw = this.#storage.getItem(this.#key);
843
+ if (raw === null) {
844
+ return void 0;
845
+ }
846
+ return cloneState(parseState(JSON.parse(raw)));
847
+ }
848
+ async save(state) {
849
+ this.#storage.setItem(this.#key, JSON.stringify(cloneState(state)));
850
+ }
851
+ };
852
+ var LocalStorageTtlStateStore = class {
853
+ #localStorage;
854
+ #identityStorage;
855
+ #key;
856
+ #identityKey;
857
+ #ttlMs;
858
+ #fixedSessionId;
859
+ #now;
860
+ #createSessionId;
861
+ #cachedSessionId;
862
+ constructor(localStorage, opts) {
863
+ this.#localStorage = localStorage;
864
+ this.#identityStorage = opts.identityStorage ?? localStorage;
865
+ this.#key = opts.key ?? DEFAULT_STATE_KEY;
866
+ this.#identityKey = opts.identityKey ?? DEFAULT_SESSION_ID_KEY;
867
+ this.#ttlMs = opts.ttlMs;
868
+ this.#fixedSessionId = opts.sessionId;
869
+ this.#now = opts.now ?? Date.now;
870
+ this.#createSessionId = opts.createSessionId ?? createSessionId;
871
+ }
872
+ async load() {
873
+ const sessionId = this.#currentSessionId();
874
+ const raw = this.#localStorage.getItem(this.#key);
875
+ if (raw === null) {
876
+ return void 0;
877
+ }
878
+ const persisted = parsePersistedState(JSON.parse(raw));
879
+ const state = cloneState(persisted.state);
880
+ if (this.#now() - persisted.savedAt >= this.#ttlMs) {
881
+ return {
882
+ sessions: {},
883
+ auditLog: state.auditLog
884
+ };
885
+ }
886
+ if (sessionIdentityMismatch(persisted.sessionId, sessionId)) {
887
+ return dropSessionBoundRecords(state, this.#now());
888
+ }
889
+ return state;
890
+ }
891
+ async save(state) {
892
+ const sessionId = this.#currentSessionId();
893
+ this.#localStorage.setItem(
894
+ this.#key,
895
+ JSON.stringify({
896
+ schemaVersion: 1,
897
+ sessionId,
898
+ savedAt: this.#now(),
899
+ state: cloneState(state)
900
+ })
901
+ );
902
+ }
903
+ #currentSessionId() {
904
+ if (this.#fixedSessionId !== void 0) {
905
+ return this.#fixedSessionId;
906
+ }
907
+ if (this.#cachedSessionId !== void 0) {
908
+ return this.#cachedSessionId;
909
+ }
910
+ const existing = this.#identityStorage.getItem(this.#identityKey);
911
+ if (existing !== null && existing.length > 0) {
912
+ this.#cachedSessionId = existing;
913
+ return existing;
914
+ }
915
+ const next = this.#createSessionId();
916
+ this.#identityStorage.setItem(this.#identityKey, next);
917
+ this.#cachedSessionId = next;
918
+ return next;
919
+ }
920
+ };
921
+ function dropSessionBoundRecords(state, now) {
922
+ const next = cloneState(state);
923
+ for (const [host, record] of Object.entries(next.sessions)) {
924
+ if (record.sessionRule === "session-or-min" && now >= record.startedAt + record.minDurationMs) {
925
+ delete next.sessions[host];
926
+ }
927
+ }
928
+ return next;
929
+ }
930
+ function createSessionId() {
931
+ const cryptoApi = globalThis.crypto;
932
+ if (typeof cryptoApi?.randomUUID === "function") {
933
+ return cryptoApi.randomUUID();
934
+ }
935
+ return `session-${Date.now()}-${Math.random().toString(36).slice(2)}`;
936
+ }
937
+ function parsePersistedState(value) {
938
+ if (!isRecord(value)) {
939
+ throw new Error("invalid persisted state");
940
+ }
941
+ const sessionId = value.sessionId;
942
+ const savedAt = value.savedAt ?? 0;
943
+ const state = value.state;
944
+ if (sessionId !== void 0 && typeof sessionId !== "string" || typeof savedAt !== "number") {
945
+ throw new Error("invalid persisted state envelope");
946
+ }
947
+ return {
948
+ sessionId,
949
+ savedAt,
950
+ state: parseState(state)
951
+ };
952
+ }
953
+ function parseState(value) {
954
+ if (!isRecord(value) || !isRecord(value.sessions) || !Array.isArray(value.auditLog)) {
955
+ throw new Error("invalid standdown state");
956
+ }
957
+ const sessions = {};
958
+ for (const [host, record] of Object.entries(value.sessions)) {
959
+ sessions[host] = parseSessionRecord(record);
960
+ }
961
+ return {
962
+ sessions,
963
+ auditLog: value.auditLog.map(parseAuditEntry)
964
+ };
965
+ }
966
+ function parseSessionRecord(value) {
967
+ if (!isRecord(value)) {
968
+ throw new Error("invalid session record");
969
+ }
970
+ const {
971
+ advertiserHost,
972
+ policyId,
973
+ startedAt,
974
+ lastActivityAt,
975
+ expiresAt,
976
+ sessionRule,
977
+ minDurationMs,
978
+ inactivityMs,
979
+ behaviors
980
+ } = value;
981
+ if (typeof advertiserHost !== "string" || typeof policyId !== "string" || typeof startedAt !== "number" || typeof lastActivityAt !== "number" || expiresAt !== void 0 && typeof expiresAt !== "number" || sessionRule !== "session-or-min" && sessionRule !== "inactivity-window" || typeof minDurationMs !== "number" || inactivityMs !== void 0 && typeof inactivityMs !== "number" || !Array.isArray(behaviors) || !behaviors.every(isBehavior)) {
982
+ throw new Error("invalid session record");
983
+ }
984
+ const record = {
985
+ advertiserHost,
986
+ policyId,
987
+ startedAt,
988
+ lastActivityAt,
989
+ sessionRule,
990
+ minDurationMs,
991
+ behaviors
992
+ };
993
+ if (expiresAt !== void 0) {
994
+ record.expiresAt = expiresAt;
995
+ }
996
+ if (inactivityMs !== void 0) {
997
+ record.inactivityMs = inactivityMs;
998
+ }
999
+ return record;
1000
+ }
1001
+ function parseAuditEntry(value) {
1002
+ if (!isRecord(value)) {
1003
+ throw new Error("invalid audit entry");
1004
+ }
1005
+ const { time, action, advertiserHost, detection, decision } = value;
1006
+ if (typeof time !== "number" || action !== "ingest" && action !== "shouldStandDown" && action !== "recordActivity" && action !== "refresh" || advertiserHost !== void 0 && typeof advertiserHost !== "string") {
1007
+ throw new Error("invalid audit entry");
1008
+ }
1009
+ const entry = { time, action };
1010
+ if (advertiserHost !== void 0) {
1011
+ entry.advertiserHost = advertiserHost;
1012
+ }
1013
+ if (detection !== void 0) {
1014
+ entry.detection = detection;
1015
+ }
1016
+ if (decision !== void 0) {
1017
+ entry.decision = decision;
1018
+ }
1019
+ return entry;
1020
+ }
1021
+ function cloneState(state) {
1022
+ return {
1023
+ sessions: Object.fromEntries(
1024
+ Object.entries(state.sessions).map(([host, record]) => [
1025
+ host,
1026
+ cloneSessionRecord(record)
1027
+ ])
1028
+ ),
1029
+ auditLog: state.auditLog.map(cloneAuditEntry2)
1030
+ };
1031
+ }
1032
+ function cloneSessionRecord(record) {
1033
+ const next = {
1034
+ advertiserHost: record.advertiserHost,
1035
+ policyId: record.policyId,
1036
+ startedAt: record.startedAt,
1037
+ lastActivityAt: record.lastActivityAt,
1038
+ sessionRule: record.sessionRule,
1039
+ minDurationMs: record.minDurationMs,
1040
+ behaviors: [...record.behaviors]
1041
+ };
1042
+ if (record.expiresAt !== void 0) {
1043
+ next.expiresAt = record.expiresAt;
1044
+ }
1045
+ if (record.inactivityMs !== void 0) {
1046
+ next.inactivityMs = record.inactivityMs;
1047
+ }
1048
+ return next;
1049
+ }
1050
+ function cloneAuditEntry2(entry) {
1051
+ const next = {
1052
+ time: entry.time,
1053
+ action: entry.action
1054
+ };
1055
+ if (entry.advertiserHost !== void 0) {
1056
+ next.advertiserHost = entry.advertiserHost;
1057
+ }
1058
+ if (entry.detection !== void 0) {
1059
+ next.detection = {
1060
+ matched: entry.detection.matched.map((match) => ({ ...match })),
1061
+ selfMatch: entry.detection.selfMatch
1062
+ };
1063
+ if (entry.detection.strongest !== void 0) {
1064
+ next.detection.strongest = { ...entry.detection.strongest };
1065
+ }
1066
+ if (entry.detection.failClosedReason !== void 0) {
1067
+ next.detection.failClosedReason = entry.detection.failClosedReason;
1068
+ }
1069
+ }
1070
+ if (entry.decision !== void 0) {
1071
+ next.decision = {
1072
+ ...entry.decision,
1073
+ behaviors: [...entry.decision.behaviors]
1074
+ };
1075
+ }
1076
+ return next;
1077
+ }
1078
+ function isRecord(value) {
1079
+ return typeof value === "object" && value !== null;
1080
+ }
1081
+ function sessionIdentityMismatch(persistedSessionId, currentSessionId) {
1082
+ return persistedSessionId !== void 0 && currentSessionId !== void 0 && persistedSessionId !== currentSessionId;
1083
+ }
1084
+ function isBehavior(value) {
1085
+ return value === "suppress-prompts" || value === "no-cookie-write" || value === "no-redirect" || value === "no-background-tracking";
1086
+ }
1087
+
1088
+ // src/content.ts
1089
+ var DEFAULT_LOCAL_STORAGE_TTL_MS = 24 * 60 * 60 * 1e3;
1090
+ var FAIL_CLOSED_BEHAVIORS = [
1091
+ "suppress-prompts",
1092
+ "no-cookie-write",
1093
+ "no-redirect",
1094
+ "no-background-tracking"
1095
+ ];
1096
+ function createContentStanddown(opts) {
1097
+ const windowLike = opts.window ?? currentWindow();
1098
+ const now = opts.now ?? Date.now;
1099
+ 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
+ );
1104
+ let disposed = false;
1105
+ let pending = false;
1106
+ const restoreHistory = patchHistory(windowLike, scheduleEvaluation);
1107
+ const onPopState = () => {
1108
+ scheduleEvaluation();
1109
+ };
1110
+ windowLike?.addEventListener?.("popstate", onPopState);
1111
+ async function evaluate() {
1112
+ let signals;
1113
+ try {
1114
+ const signalOptions = { now };
1115
+ if (windowLike !== void 0) {
1116
+ signalOptions.window = windowLike;
1117
+ }
1118
+ if (opts.selfPatterns !== void 0) {
1119
+ signalOptions.selfPatterns = opts.selfPatterns;
1120
+ }
1121
+ if (opts.publisherSites !== void 0) {
1122
+ signalOptions.publisherSites = opts.publisherSites;
1123
+ }
1124
+ signals = collectContentSignals(signalOptions);
1125
+ } catch {
1126
+ const decision2 = failClosedDecision2("signal-collection-error");
1127
+ opts.onDecision?.(decision2, {
1128
+ url: "",
1129
+ now: now()
1130
+ });
1131
+ return decision2;
1132
+ }
1133
+ const decision = await session.ingest(signals, opts.policies);
1134
+ opts.onDecision?.(decision, signals);
1135
+ return decision;
1136
+ }
1137
+ function scheduleEvaluation() {
1138
+ if (disposed || pending) {
1139
+ return;
1140
+ }
1141
+ pending = true;
1142
+ const run = () => {
1143
+ pending = false;
1144
+ if (!disposed) {
1145
+ void evaluate();
1146
+ }
1147
+ };
1148
+ if (windowLike?.setTimeout !== void 0) {
1149
+ windowLike.setTimeout(run, 0);
1150
+ return;
1151
+ }
1152
+ queueMicrotask(run);
1153
+ }
1154
+ async function shouldStandDown(advertiserHost, at = now()) {
1155
+ const host = advertiserHost ?? (windowLike === void 0 ? void 0 : hostFromUrl3(windowLike.location.href));
1156
+ if (host === void 0) {
1157
+ return failClosedDecision2("missing-advertiser-host");
1158
+ }
1159
+ return session.shouldStandDown(host, at);
1160
+ }
1161
+ function dispose() {
1162
+ disposed = true;
1163
+ restoreHistory();
1164
+ windowLike?.removeEventListener?.("popstate", onPopState);
1165
+ }
1166
+ const ready = evaluate();
1167
+ return {
1168
+ session,
1169
+ store,
1170
+ ready,
1171
+ evaluate,
1172
+ shouldStandDown,
1173
+ dispose
1174
+ };
1175
+ }
1176
+ function collectContentSignals(opts = {}) {
1177
+ const windowLike = opts.window ?? currentWindow();
1178
+ const now = opts.now ?? Date.now;
1179
+ if (windowLike === void 0) {
1180
+ throw new Error("content window unavailable");
1181
+ }
1182
+ const signals = {
1183
+ url: windowLike.location.href,
1184
+ now: now()
1185
+ };
1186
+ const referrer = windowLike.document.referrer;
1187
+ const cookieNames = cookieNamesFromString(windowLike.document.cookie);
1188
+ if (referrer !== void 0 && referrer.length > 0) {
1189
+ signals.referrer = referrer;
1190
+ }
1191
+ if (cookieNames.length > 0) {
1192
+ signals.cookieNames = cookieNames;
1193
+ }
1194
+ if (opts.selfPatterns !== void 0) {
1195
+ signals.selfPatterns = opts.selfPatterns;
1196
+ }
1197
+ if (opts.publisherSites !== void 0) {
1198
+ signals.publisherSites = opts.publisherSites;
1199
+ }
1200
+ return signals;
1201
+ }
1202
+ function cookieNamesFromString(cookie) {
1203
+ if (cookie.trim().length === 0) {
1204
+ return [];
1205
+ }
1206
+ return cookie.split(";").map((part) => part.trim()).filter((part) => part.length > 0).map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0);
1207
+ }
1208
+ function createContentStore(windowLike, opts, now) {
1209
+ if (windowLike === void 0) {
1210
+ return new UnavailableStateStore("content window unavailable");
1211
+ }
1212
+ if (opts.storage === "local-ttl") {
1213
+ if (windowLike.localStorage === void 0) {
1214
+ return new UnavailableStateStore("localStorage unavailable");
1215
+ }
1216
+ const storeOptions = {
1217
+ ttlMs: opts.ttlMs ?? DEFAULT_LOCAL_STORAGE_TTL_MS,
1218
+ now
1219
+ };
1220
+ if (windowLike.sessionStorage !== void 0) {
1221
+ storeOptions.sessionStorage = windowLike.sessionStorage;
1222
+ }
1223
+ return new LocalStorageTtlStateStore(windowLike.localStorage, storeOptions);
1224
+ }
1225
+ if (windowLike.sessionStorage === void 0) {
1226
+ return new UnavailableStateStore("sessionStorage unavailable");
1227
+ }
1228
+ return new SessionStorageStateStore(windowLike.sessionStorage);
1229
+ }
1230
+ function patchHistory(windowLike, onNavigation) {
1231
+ const history = windowLike?.history;
1232
+ if (history === void 0) {
1233
+ return () => {
1234
+ };
1235
+ }
1236
+ const originalPushState = history.pushState.bind(history);
1237
+ const originalReplaceState = history.replaceState.bind(history);
1238
+ history.pushState = (...args) => {
1239
+ originalPushState(...args);
1240
+ onNavigation();
1241
+ };
1242
+ history.replaceState = (...args) => {
1243
+ originalReplaceState(...args);
1244
+ onNavigation();
1245
+ };
1246
+ return () => {
1247
+ history.pushState = originalPushState;
1248
+ history.replaceState = originalReplaceState;
1249
+ };
1250
+ }
1251
+ function currentWindow() {
1252
+ const value = globalThis.window;
1253
+ return value;
1254
+ }
1255
+ function hostFromUrl3(value) {
1256
+ try {
1257
+ return new URL(value).hostname.toLowerCase().replace(/\.$/, "");
1258
+ } catch {
1259
+ return void 0;
1260
+ }
1261
+ }
1262
+ function failClosedDecision2(reason) {
1263
+ return {
1264
+ standDown: true,
1265
+ reason,
1266
+ behaviors: [...FAIL_CLOSED_BEHAVIORS]
1267
+ };
1268
+ }
1269
+ var UnavailableStateStore = class {
1270
+ #reason;
1271
+ constructor(reason) {
1272
+ this.#reason = reason;
1273
+ }
1274
+ async load() {
1275
+ throw new Error(this.#reason);
1276
+ }
1277
+ async save() {
1278
+ throw new Error(this.#reason);
1279
+ }
1280
+ };
1281
+
1282
+ exports.LocalStorageTtlStateStore = LocalStorageTtlStateStore;
1283
+ exports.SessionStorageStateStore = SessionStorageStateStore;
1284
+ exports.collectContentSignals = collectContentSignals;
1285
+ exports.cookieNamesFromString = cookieNamesFromString;
1286
+ exports.createContentStanddown = createContentStanddown;
1287
+ //# sourceMappingURL=content.cjs.map
1288
+ //# sourceMappingURL=content.cjs.map