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/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
|
-
|
|
551
|
+
const base = { matched, selfMatch };
|
|
552
|
+
if (strongest) {
|
|
553
|
+
base.strongest = strongest;
|
|
554
|
+
}
|
|
555
|
+
if (selfExemptScopes.length > 0) {
|
|
556
|
+
base.selfExemptScopes = selfExemptScopes;
|
|
557
|
+
}
|
|
558
|
+
return base;
|
|
535
559
|
}
|
|
536
560
|
function classifyReferrer(signals, advertiserHost) {
|
|
537
561
|
const candidate = signals.initiator ?? signals.referrer;
|
|
@@ -648,16 +672,19 @@ function matchedRule(policy, kind, rule, advertiserHost, reason) {
|
|
|
648
672
|
};
|
|
649
673
|
}
|
|
650
674
|
function kindPriority(kind) {
|
|
651
|
-
if (kind === "
|
|
675
|
+
if (kind === "disabled-host") {
|
|
652
676
|
return 0;
|
|
653
677
|
}
|
|
654
|
-
if (kind === "
|
|
678
|
+
if (kind === "redirect-domain") {
|
|
655
679
|
return 1;
|
|
656
680
|
}
|
|
657
|
-
if (kind === "
|
|
681
|
+
if (kind === "landing-param") {
|
|
658
682
|
return 2;
|
|
659
683
|
}
|
|
660
|
-
|
|
684
|
+
if (kind === "cookie") {
|
|
685
|
+
return 3;
|
|
686
|
+
}
|
|
687
|
+
return 4;
|
|
661
688
|
}
|
|
662
689
|
function paramRuleMatches(rule, params) {
|
|
663
690
|
return rule.anyOf.some(
|
|
@@ -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
|
-
|
|
763
|
-
|
|
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
|
-
|
|
773
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|