@peax/consent-core 0.6.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.
- package/LICENSE +201 -0
- package/README.md +207 -0
- package/dist/ConsentEnforcementError-Bn_ayWJI.js +16 -0
- package/dist/assertStorageIdentifier-BQuj-9bp.js +7 -0
- package/dist/createConsentIframe-Bw-cl--m.js +93 -0
- package/dist/createConsentRequestScope-D7U8uK6m.js +58 -0
- package/dist/createConsentScript-C_4ecSTX.js +102 -0
- package/dist/createDomGuard-CuIEC0HP.js +101 -0
- package/dist/enforcement/axios.d.ts +222 -0
- package/dist/enforcement/axios.js +92 -0
- package/dist/enforcement/fetch.d.ts +222 -0
- package/dist/enforcement/fetch.js +40 -0
- package/dist/enforcement/iframe.d.ts +245 -0
- package/dist/enforcement/iframe.js +2 -0
- package/dist/enforcement/integration.d.ts +230 -0
- package/dist/enforcement/integration.js +181 -0
- package/dist/enforcement/markup.d.ts +214 -0
- package/dist/enforcement/markup.js +300 -0
- package/dist/enforcement/posthog.d.ts +68 -0
- package/dist/enforcement/posthog.js +96 -0
- package/dist/enforcement/request-rules.d.ts +248 -0
- package/dist/enforcement/request-rules.js +434 -0
- package/dist/enforcement/script.d.ts +240 -0
- package/dist/enforcement/script.js +2 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +445 -0
- package/dist/storage/localforage.d.ts +23 -0
- package/dist/storage/localforage.js +26 -0
- package/dist/storage/sync.d.ts +278 -0
- package/dist/storage/sync.js +370 -0
- package/dist/storage.d.ts +77 -0
- package/dist/storage.js +207 -0
- package/dist/validate-D16G7mdG.js +15 -0
- package/package.json +128 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as z from 'zod/mini';
|
|
2
|
+
|
|
3
|
+
/** Disjoint category/use decisions for one scope actually presented to the user. */
|
|
4
|
+
declare type BatchCommand = z.infer<typeof consentDecisionsCommandSchema>;
|
|
5
|
+
|
|
6
|
+
/** Records choices only for category members in userConsentedPolicy, not future members. */
|
|
7
|
+
declare type CategoryConsentCommand = z.infer<typeof categoryConsentCommandSchema>;
|
|
8
|
+
|
|
9
|
+
declare const categoryConsentCommandSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
10
|
+
userConsentedPolicy: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
11
|
+
purposes: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
12
|
+
id: z.ZodMiniString<string>;
|
|
13
|
+
title: z.ZodMiniString<string>;
|
|
14
|
+
description: z.ZodMiniString<string>;
|
|
15
|
+
}, z.core.$strict>>>>;
|
|
16
|
+
uses: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
17
|
+
id: z.ZodMiniString<string>;
|
|
18
|
+
serviceId: z.ZodMiniString<string>;
|
|
19
|
+
categoryId: z.ZodMiniString<string>;
|
|
20
|
+
purposeId: z.ZodMiniString<string>;
|
|
21
|
+
consentVersion: z.ZodMiniInt;
|
|
22
|
+
}, z.core.$strict>>>>;
|
|
23
|
+
}, z.core.$strict>>;
|
|
24
|
+
categoryId: z.ZodMiniString<string>;
|
|
25
|
+
decision: z.ZodMiniEnum<{
|
|
26
|
+
granted: "granted";
|
|
27
|
+
denied: "denied";
|
|
28
|
+
}>;
|
|
29
|
+
}, z.core.$strict>>;
|
|
30
|
+
|
|
31
|
+
/** userConsentedPolicy is the scope shown for this decision, including a denial. */
|
|
32
|
+
declare type ConsentDecisionCommand = z.infer<typeof consentDecisionCommandSchema>;
|
|
33
|
+
|
|
34
|
+
declare const consentDecisionCommandSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
35
|
+
userConsentedPolicy: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
36
|
+
purposes: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
37
|
+
id: z.ZodMiniString<string>;
|
|
38
|
+
title: z.ZodMiniString<string>;
|
|
39
|
+
description: z.ZodMiniString<string>;
|
|
40
|
+
}, z.core.$strict>>>>;
|
|
41
|
+
uses: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
42
|
+
id: z.ZodMiniString<string>;
|
|
43
|
+
serviceId: z.ZodMiniString<string>;
|
|
44
|
+
categoryId: z.ZodMiniString<string>;
|
|
45
|
+
purposeId: z.ZodMiniString<string>;
|
|
46
|
+
consentVersion: z.ZodMiniInt;
|
|
47
|
+
}, z.core.$strict>>>>;
|
|
48
|
+
}, z.core.$strict>>;
|
|
49
|
+
useId: z.ZodMiniString<string>;
|
|
50
|
+
decision: z.ZodMiniEnum<{
|
|
51
|
+
granted: "granted";
|
|
52
|
+
denied: "denied";
|
|
53
|
+
}>;
|
|
54
|
+
}, z.core.$strict>>;
|
|
55
|
+
|
|
56
|
+
declare type ConsentDecisionsCommand = Readonly<Omit<BatchCommand, 'decisions'> & {
|
|
57
|
+
decisions: readonly ExclusiveTarget<BatchCommand['decisions'][number]>[];
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
declare const consentDecisionsCommandSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
61
|
+
userConsentedPolicy: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
62
|
+
purposes: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
63
|
+
id: z.ZodMiniString<string>;
|
|
64
|
+
title: z.ZodMiniString<string>;
|
|
65
|
+
description: z.ZodMiniString<string>;
|
|
66
|
+
}, z.core.$strict>>>>;
|
|
67
|
+
uses: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
68
|
+
id: z.ZodMiniString<string>;
|
|
69
|
+
serviceId: z.ZodMiniString<string>;
|
|
70
|
+
categoryId: z.ZodMiniString<string>;
|
|
71
|
+
purposeId: z.ZodMiniString<string>;
|
|
72
|
+
consentVersion: z.ZodMiniInt;
|
|
73
|
+
}, z.core.$strict>>>>;
|
|
74
|
+
}, z.core.$strict>>;
|
|
75
|
+
decisions: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniUnion<readonly [z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
76
|
+
useId: z.ZodMiniString<string>;
|
|
77
|
+
decision: z.ZodMiniEnum<{
|
|
78
|
+
granted: "granted";
|
|
79
|
+
denied: "denied";
|
|
80
|
+
}>;
|
|
81
|
+
}, z.core.$strict>>, z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
82
|
+
categoryId: z.ZodMiniString<string>;
|
|
83
|
+
decision: z.ZodMiniEnum<{
|
|
84
|
+
granted: "granted";
|
|
85
|
+
denied: "denied";
|
|
86
|
+
}>;
|
|
87
|
+
}, z.core.$strict>>]>>>;
|
|
88
|
+
}, z.core.$strict>>;
|
|
89
|
+
|
|
90
|
+
declare type ConsentDeniedReason = 'unknown-use' | 'undecided' | 'denied' | 'consent-version-mismatch';
|
|
91
|
+
|
|
92
|
+
declare type ConsentEvaluation = Readonly<{
|
|
93
|
+
allowed: true;
|
|
94
|
+
}> | Readonly<{
|
|
95
|
+
allowed: false;
|
|
96
|
+
reason: ConsentDeniedReason;
|
|
97
|
+
}>;
|
|
98
|
+
|
|
99
|
+
declare type ConsentPolicy = z.infer<typeof consentPolicySchema>;
|
|
100
|
+
|
|
101
|
+
declare const consentPolicySchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
102
|
+
purposes: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
103
|
+
id: z.ZodMiniString<string>;
|
|
104
|
+
title: z.ZodMiniString<string>;
|
|
105
|
+
description: z.ZodMiniString<string>;
|
|
106
|
+
}, z.core.$strict>>>>;
|
|
107
|
+
uses: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
108
|
+
id: z.ZodMiniString<string>;
|
|
109
|
+
serviceId: z.ZodMiniString<string>;
|
|
110
|
+
categoryId: z.ZodMiniString<string>;
|
|
111
|
+
purposeId: z.ZodMiniString<string>;
|
|
112
|
+
consentVersion: z.ZodMiniInt;
|
|
113
|
+
}, z.core.$strict>>>>;
|
|
114
|
+
}, z.core.$strict>>;
|
|
115
|
+
|
|
116
|
+
export declare type ConsentRequestRule = Readonly<{
|
|
117
|
+
id: string;
|
|
118
|
+
useId: string;
|
|
119
|
+
match: Readonly<{
|
|
120
|
+
hostname: string;
|
|
121
|
+
includeSubdomains?: boolean;
|
|
122
|
+
pathnamePrefix?: string;
|
|
123
|
+
methods?: readonly string[];
|
|
124
|
+
}>;
|
|
125
|
+
}>;
|
|
126
|
+
|
|
127
|
+
export declare type ConsentRequestRuleBlockedEvent = Readonly<{
|
|
128
|
+
transport: ConsentRequestRuleTransport;
|
|
129
|
+
ruleIds: readonly string[];
|
|
130
|
+
blockedUses: readonly ConsentRequestRuleBlockedUse[];
|
|
131
|
+
reason: ConsentRequestRuleErrorReason;
|
|
132
|
+
}>;
|
|
133
|
+
|
|
134
|
+
export declare type ConsentRequestRuleBlockedUse = Readonly<{
|
|
135
|
+
useId: string;
|
|
136
|
+
reason: ConsentRequestRuleErrorReason;
|
|
137
|
+
}>;
|
|
138
|
+
|
|
139
|
+
/** No request URL, credentials, body or native transport error is retained. */
|
|
140
|
+
export declare class ConsentRequestRuleError extends Error implements ConsentRequestRuleBlockedEvent {
|
|
141
|
+
readonly name = "ConsentRequestRuleError";
|
|
142
|
+
readonly transport: ConsentRequestRuleBlockedEvent['transport'];
|
|
143
|
+
readonly ruleIds: ConsentRequestRuleBlockedEvent['ruleIds'];
|
|
144
|
+
readonly blockedUses: ConsentRequestRuleBlockedEvent['blockedUses'];
|
|
145
|
+
readonly reason: ConsentRequestRuleBlockedEvent['reason'];
|
|
146
|
+
constructor(event: ConsentRequestRuleBlockedEvent);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export declare type ConsentRequestRuleErrorReason = ConsentDeniedReason | 'disposed' | 'grant-ended' | 'evaluation-error';
|
|
150
|
+
|
|
151
|
+
export declare type ConsentRequestRuleGuard = Readonly<{
|
|
152
|
+
/** Install before SDK initialization. Idempotent; configuration/ownership failures throw. */
|
|
153
|
+
start: () => void;
|
|
154
|
+
/** Cancel matched work and restore only patches still owned by this guard. Terminal. */
|
|
155
|
+
dispose: () => void;
|
|
156
|
+
}>;
|
|
157
|
+
|
|
158
|
+
export declare type ConsentRequestRuleGuardOptions = Readonly<{
|
|
159
|
+
/** Forward the runtime's stable immutable getSnapshot() to enable grant-capture caching. */
|
|
160
|
+
consent: Pick<ConsentRuntime, 'evaluate' | 'subscribe'> & Partial<Pick<ConsentRuntime, 'getSnapshot'>>;
|
|
161
|
+
rules: readonly ConsentRequestRule[];
|
|
162
|
+
/** Defaults to the current window at start(), never at import/construction. */
|
|
163
|
+
target?: Window & typeof globalThis;
|
|
164
|
+
onBlocked?: (event: ConsentRequestRuleBlockedEvent) => void;
|
|
165
|
+
onIssue?: (issue: ConsentRequestRuleIssue) => void;
|
|
166
|
+
}>;
|
|
167
|
+
|
|
168
|
+
export declare type ConsentRequestRuleIssue = Readonly<{
|
|
169
|
+
code: 'evaluation-error' | 'callback-error' | 'patch-replaced' | 'untracked-xhr' | 'cleanup-error';
|
|
170
|
+
}>;
|
|
171
|
+
|
|
172
|
+
export declare type ConsentRequestRuleTransport = 'fetch' | 'xhr';
|
|
173
|
+
|
|
174
|
+
/** A current use with no decision or a grant for a different consent version. */
|
|
175
|
+
declare type ConsentRequirement = Readonly<{
|
|
176
|
+
useId: string;
|
|
177
|
+
categoryId: string;
|
|
178
|
+
/** The version required by the current policy for this use. */
|
|
179
|
+
consentVersion: number;
|
|
180
|
+
reason: 'undecided' | 'consent-version-mismatch';
|
|
181
|
+
}>;
|
|
182
|
+
|
|
183
|
+
/** In-memory runtime; methods can be passed as callbacks without binding. */
|
|
184
|
+
declare type ConsentRuntime = Readonly<{
|
|
185
|
+
getSnapshot: () => ConsentSnapshot;
|
|
186
|
+
evaluate: (useId: string) => ConsentEvaluation;
|
|
187
|
+
/** Includes undecided uses and version-mismatched grants, not explicit denials. */
|
|
188
|
+
getMismatchedConsent: () => readonly ConsentRequirement[];
|
|
189
|
+
/**
|
|
190
|
+
* Replace all choices from trusted persistence, preserving unknown uses and recorded versions.
|
|
191
|
+
* Validation checks structure, not evidence of user consent; use decision methods for new choices.
|
|
192
|
+
* Does not replace policy, renew versions or perform I/O. Effective changes notify existing guards.
|
|
193
|
+
*/
|
|
194
|
+
restoreState: (state: unknown) => ValidationResult<ConsentSnapshot>;
|
|
195
|
+
setConsentDecision: (command: ConsentDecisionCommand) => ValidationResult<ConsentSnapshot>;
|
|
196
|
+
setCategoryConsent: (command: CategoryConsentCommand) => ValidationResult<ConsentSnapshot>;
|
|
197
|
+
/** Validates all targets before one commit; observer errors can throw after that commit. */
|
|
198
|
+
setConsentDecisions: (command: ConsentDecisionsCommand) => ValidationResult<ConsentSnapshot>;
|
|
199
|
+
updatePolicy: (currentAppPolicy: unknown) => ValidationResult<ConsentSnapshot>;
|
|
200
|
+
subscribe: (listener: () => void) => () => void;
|
|
201
|
+
}>;
|
|
202
|
+
|
|
203
|
+
/** One active app policy and recorded choices; not a history of policies shown to users. */
|
|
204
|
+
declare type ConsentSnapshot = Readonly<{
|
|
205
|
+
currentAppPolicy: ConsentPolicy;
|
|
206
|
+
state: ConsentState;
|
|
207
|
+
}>;
|
|
208
|
+
|
|
209
|
+
declare type ConsentState = z.infer<typeof consentStateSchema>;
|
|
210
|
+
|
|
211
|
+
declare const consentStateSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
212
|
+
choices: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
|
|
213
|
+
useId: z.ZodMiniString<string>;
|
|
214
|
+
consentVersion: z.ZodMiniInt;
|
|
215
|
+
decision: z.ZodMiniEnum<{
|
|
216
|
+
granted: "granted";
|
|
217
|
+
denied: "denied";
|
|
218
|
+
}>;
|
|
219
|
+
}, z.core.$strict>>>>;
|
|
220
|
+
}, z.core.$strict>>;
|
|
221
|
+
|
|
222
|
+
/** Optional window-local safety net. No browser access, subscriptions or patches until start(). */
|
|
223
|
+
export declare function createConsentRequestRuleGuard(options: ConsentRequestRuleGuardOptions): ConsentRequestRuleGuard;
|
|
224
|
+
|
|
225
|
+
declare type ExclusiveTarget<T> = T extends {
|
|
226
|
+
useId: string;
|
|
227
|
+
} ? T & {
|
|
228
|
+
readonly categoryId?: never;
|
|
229
|
+
} : T & {
|
|
230
|
+
readonly useId?: never;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** A validation issue without a copy of the rejected input. */
|
|
234
|
+
declare type ValidationIssue = Readonly<{
|
|
235
|
+
code: string;
|
|
236
|
+
path: readonly (string | number)[];
|
|
237
|
+
message: string;
|
|
238
|
+
}>;
|
|
239
|
+
|
|
240
|
+
declare type ValidationResult<T> = Readonly<{
|
|
241
|
+
success: true;
|
|
242
|
+
data: T;
|
|
243
|
+
}> | Readonly<{
|
|
244
|
+
success: false;
|
|
245
|
+
issues: readonly ValidationIssue[];
|
|
246
|
+
}>;
|
|
247
|
+
|
|
248
|
+
export { }
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { n as retainSignal, t as createConsentRequestScope } from "../createConsentRequestScope-D7U8uK6m.js";
|
|
2
|
+
import { n as requireUseId, t as requireConsent } from "../validate-D16G7mdG.js";
|
|
3
|
+
//#region src/enforcement/request-rules/ConsentRequestRuleError.ts
|
|
4
|
+
/** No request URL, credentials, body or native transport error is retained. */
|
|
5
|
+
var ConsentRequestRuleError = class extends Error {
|
|
6
|
+
name = "ConsentRequestRuleError";
|
|
7
|
+
transport;
|
|
8
|
+
ruleIds;
|
|
9
|
+
blockedUses;
|
|
10
|
+
reason;
|
|
11
|
+
constructor(event) {
|
|
12
|
+
super("Request blocked by consent.");
|
|
13
|
+
this.transport = event.transport;
|
|
14
|
+
this.ruleIds = Object.freeze([...event.ruleIds]);
|
|
15
|
+
this.blockedUses = Object.freeze(event.blockedUses.map((use) => Object.freeze({ ...use })));
|
|
16
|
+
this.reason = event.reason;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/enforcement/request-rules/compileRules.ts
|
|
21
|
+
function object(value, keys) {
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !keys.includes(key))) throw new TypeError("Invalid request rule configuration.");
|
|
23
|
+
}
|
|
24
|
+
function compileRules(input) {
|
|
25
|
+
if (!Array.isArray(input)) throw new TypeError("Expected request rules.");
|
|
26
|
+
const ids = /* @__PURE__ */ new Set();
|
|
27
|
+
return Object.freeze(input.map((rule) => {
|
|
28
|
+
object(rule, [
|
|
29
|
+
"id",
|
|
30
|
+
"useId",
|
|
31
|
+
"match"
|
|
32
|
+
]);
|
|
33
|
+
if (typeof rule.id !== "string" || !rule.id.trim()) throw new TypeError("Expected a non blank request rule ID.");
|
|
34
|
+
requireUseId(rule.useId);
|
|
35
|
+
if (ids.has(rule.id)) throw new TypeError("Duplicate request rule ID.");
|
|
36
|
+
ids.add(rule.id);
|
|
37
|
+
object(rule.match, [
|
|
38
|
+
"hostname",
|
|
39
|
+
"includeSubdomains",
|
|
40
|
+
"pathnamePrefix",
|
|
41
|
+
"methods"
|
|
42
|
+
]);
|
|
43
|
+
const match = rule.match;
|
|
44
|
+
if (typeof match.hostname !== "string" || !match.hostname || match.hostname.trim() !== match.hostname) throw new TypeError("Expected a hostname.");
|
|
45
|
+
const url = new URL(`https://${match.hostname}`);
|
|
46
|
+
if (url.username || url.password || url.port || url.pathname !== "/" || url.search || url.hash || match.hostname.includes(":") && !(match.hostname.startsWith("[") && match.hostname.endsWith("]")) || /[/\\?#@]/.test(match.hostname)) throw new TypeError("Expected a hostname, not a URL or port.");
|
|
47
|
+
if (match.includeSubdomains !== void 0 && typeof match.includeSubdomains !== "boolean") throw new TypeError("Expected includeSubdomains.");
|
|
48
|
+
if (match.pathnamePrefix !== void 0 && (typeof match.pathnamePrefix !== "string" || !match.pathnamePrefix.startsWith("/"))) throw new TypeError("Expected a pathname prefix.");
|
|
49
|
+
let methods;
|
|
50
|
+
if (match.methods !== void 0) {
|
|
51
|
+
if (!Array.isArray(match.methods) || !match.methods.length) throw new TypeError("Expected request methods.");
|
|
52
|
+
methods = new Set(match.methods.map((method) => {
|
|
53
|
+
if (typeof method !== "string" || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(method)) throw new TypeError("Expected an HTTP method token.");
|
|
54
|
+
return method.toUpperCase();
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
id: rule.id,
|
|
59
|
+
useId: rule.useId,
|
|
60
|
+
hostname: url.hostname.replace(/\.$/, ""),
|
|
61
|
+
includeSubdomains: match.includeSubdomains ?? false,
|
|
62
|
+
pathnamePrefix: match.pathnamePrefix ?? "/",
|
|
63
|
+
methods
|
|
64
|
+
});
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
function matchRules(rules, url, method) {
|
|
68
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return [];
|
|
69
|
+
const hostname = url.hostname.replace(/\.$/, "");
|
|
70
|
+
return rules.filter((rule) => (hostname === rule.hostname || rule.includeSubdomains && hostname.endsWith(`.${rule.hostname}`)) && url.pathname.startsWith(rule.pathnamePrefix) && (!rule.methods || rule.methods.has(method.toUpperCase())));
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/enforcement/request-rules/createFetchInterceptor.ts
|
|
74
|
+
/** Dispatch only; the factory captures nativeFetch before app-owned subscriptions and owns patches. */
|
|
75
|
+
function createFetchInterceptor(target, nativeFetch, rules, permissions) {
|
|
76
|
+
return async (input, init) => {
|
|
77
|
+
permissions.assertAllowed("fetch", [], /* @__PURE__ */ new Map());
|
|
78
|
+
const captured = permissions.captureGrants();
|
|
79
|
+
const request = new target.Request(input, init);
|
|
80
|
+
const matched = matchRules(rules, new target.URL(request.url), request.method);
|
|
81
|
+
const signals = permissions.assertAllowed("fetch", matched, captured);
|
|
82
|
+
if (!matched.length) return nativeFetch.call(target, request);
|
|
83
|
+
const signal = target.AbortSignal.any([request.signal, ...signals]);
|
|
84
|
+
if (signal.aborted) permissions.assertAllowed("fetch", matched, captured);
|
|
85
|
+
signal.throwIfAborted();
|
|
86
|
+
const response = await nativeFetch.call(target, request, {
|
|
87
|
+
signal,
|
|
88
|
+
referrer: request.referrer,
|
|
89
|
+
referrerPolicy: request.referrerPolicy
|
|
90
|
+
});
|
|
91
|
+
retainSignal(signal, response, response.body);
|
|
92
|
+
return response;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/enforcement/request-rules/createRulePermissions.ts
|
|
97
|
+
/** One existing request scope per configured use, not a new policy evaluator. */
|
|
98
|
+
function createRulePermissions(options, rules, report, isDisposed) {
|
|
99
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
100
|
+
let closed = false;
|
|
101
|
+
let cache;
|
|
102
|
+
let cacheRevision = 0;
|
|
103
|
+
function invalidateCapture() {
|
|
104
|
+
cache = void 0;
|
|
105
|
+
cacheRevision++;
|
|
106
|
+
}
|
|
107
|
+
function readSnapshot() {
|
|
108
|
+
try {
|
|
109
|
+
return options.consent.getSnapshot?.();
|
|
110
|
+
} catch {
|
|
111
|
+
invalidateCapture();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const scopeConsent = {
|
|
116
|
+
subscribe: (listener) => options.consent.subscribe(listener),
|
|
117
|
+
evaluate: (useId) => {
|
|
118
|
+
try {
|
|
119
|
+
return options.consent.evaluate(useId);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
invalidateCapture();
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
for (const rule of rules) if (!scopes.has(rule.useId)) scopes.set(rule.useId, createConsentRequestScope(scopeConsent, rule.useId, () => /* @__PURE__ */ new Error("Consent grant ended.")));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
for (const scope of scopes.values()) try {
|
|
130
|
+
scope.dispose();
|
|
131
|
+
} catch {
|
|
132
|
+
report("cleanup-error");
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
function block(transport, matched, blockedUses, reason) {
|
|
137
|
+
const error = new ConsentRequestRuleError({
|
|
138
|
+
transport,
|
|
139
|
+
ruleIds: matched.map((rule) => rule.id),
|
|
140
|
+
blockedUses,
|
|
141
|
+
reason
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
Promise.resolve(options.onBlocked?.(Object.freeze({
|
|
145
|
+
transport,
|
|
146
|
+
ruleIds: error.ruleIds,
|
|
147
|
+
blockedUses: error.blockedUses,
|
|
148
|
+
reason
|
|
149
|
+
}))).catch(() => report("callback-error"));
|
|
150
|
+
} catch {
|
|
151
|
+
report("callback-error");
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
/** Capture before normalization without letting unrelated unavailable uses block essential traffic. */
|
|
157
|
+
captureGrants() {
|
|
158
|
+
if (closed || isDisposed()) return /* @__PURE__ */ new Map();
|
|
159
|
+
const snapshot = readSnapshot();
|
|
160
|
+
if (snapshot && cache?.snapshot === snapshot) return cache.grants;
|
|
161
|
+
invalidateCapture();
|
|
162
|
+
const revision = cacheRevision;
|
|
163
|
+
const captured = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const [useId, scope] of scopes) try {
|
|
165
|
+
captured.set(useId, scope.begin());
|
|
166
|
+
} catch {
|
|
167
|
+
captured.set(useId, void 0);
|
|
168
|
+
}
|
|
169
|
+
const latestSnapshot = snapshot ? readSnapshot() : void 0;
|
|
170
|
+
if (snapshot && latestSnapshot === snapshot && revision === cacheRevision && !closed && !isDisposed()) cache = {
|
|
171
|
+
snapshot,
|
|
172
|
+
grants: captured
|
|
173
|
+
};
|
|
174
|
+
return captured;
|
|
175
|
+
},
|
|
176
|
+
/** Return original signals only if ALL matched uses are allowed; otherwise report and throw once. */
|
|
177
|
+
assertAllowed(transport, matched, captured) {
|
|
178
|
+
if (closed || isDisposed()) block(transport, matched, [], "disposed");
|
|
179
|
+
const blocked = [];
|
|
180
|
+
const signals = [];
|
|
181
|
+
for (const useId of new Set(matched.map((rule) => rule.useId))) try {
|
|
182
|
+
const result = options.consent.evaluate(useId);
|
|
183
|
+
const signal = captured.get(useId);
|
|
184
|
+
if (!result.allowed) blocked.push({
|
|
185
|
+
useId,
|
|
186
|
+
reason: result.reason
|
|
187
|
+
});
|
|
188
|
+
else if (!signal || signal.aborted) {
|
|
189
|
+
invalidateCapture();
|
|
190
|
+
blocked.push({
|
|
191
|
+
useId,
|
|
192
|
+
reason: "grant-ended"
|
|
193
|
+
});
|
|
194
|
+
} else signals.push(signal);
|
|
195
|
+
} catch {
|
|
196
|
+
invalidateCapture();
|
|
197
|
+
report("evaluation-error");
|
|
198
|
+
blocked.push({
|
|
199
|
+
useId,
|
|
200
|
+
reason: "evaluation-error"
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (blocked.length) block(transport, matched, blocked, blocked[0]?.reason ?? "grant-ended");
|
|
204
|
+
return signals;
|
|
205
|
+
},
|
|
206
|
+
dispose() {
|
|
207
|
+
closed = true;
|
|
208
|
+
invalidateCapture();
|
|
209
|
+
for (const scope of scopes.values()) try {
|
|
210
|
+
scope.dispose();
|
|
211
|
+
} catch {
|
|
212
|
+
report("cleanup-error");
|
|
213
|
+
}
|
|
214
|
+
scopes.clear();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/enforcement/request-rules/normalizeXhrBody.ts
|
|
220
|
+
/** Web IDL string conversion must run before our final URL/permission check. */
|
|
221
|
+
function domString(value) {
|
|
222
|
+
if (typeof value === "symbol") throw new TypeError("Cannot convert a Symbol to a string.");
|
|
223
|
+
return String(value);
|
|
224
|
+
}
|
|
225
|
+
var probes = [
|
|
226
|
+
(value) => Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "byteLength")?.get?.call(value),
|
|
227
|
+
(value, target) => Object.getOwnPropertyDescriptor(target.Blob.prototype, "size")?.get?.call(value),
|
|
228
|
+
(value, target) => target.FormData.prototype.has.call(value, ""),
|
|
229
|
+
(value, target) => target.URLSearchParams.prototype.has.call(value, ""),
|
|
230
|
+
(value, target) => Object.getOwnPropertyDescriptor(target.Node.prototype, "nodeType")?.get?.call(value) === 9 ? true : void 0
|
|
231
|
+
];
|
|
232
|
+
/** Native brand probes retain cross-realm BodyInit values without duck typing/getters. */
|
|
233
|
+
function normalizeXhrBody(value, target) {
|
|
234
|
+
if (value == null) return null;
|
|
235
|
+
if (ArrayBuffer.isView(value)) return value;
|
|
236
|
+
for (const probe of probes) try {
|
|
237
|
+
if (probe(value, target) !== void 0) return value;
|
|
238
|
+
} catch {}
|
|
239
|
+
return domString(value);
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/enforcement/request-rules/createXhrInterceptor.ts
|
|
243
|
+
function createXhrInterceptor(target, rules, permissions, report) {
|
|
244
|
+
const prototype = target.XMLHttpRequest.prototype;
|
|
245
|
+
const nativeOpen = prototype.open;
|
|
246
|
+
const nativeSend = prototype.send;
|
|
247
|
+
const nativeAbort = prototype.abort;
|
|
248
|
+
const entries = /* @__PURE__ */ new WeakMap();
|
|
249
|
+
const active = /* @__PURE__ */ new Set();
|
|
250
|
+
const untracked = /* @__PURE__ */ new WeakSet();
|
|
251
|
+
const open = function(...args) {
|
|
252
|
+
permissions.assertAllowed("xhr", [], /* @__PURE__ */ new Map());
|
|
253
|
+
if (args.length < 2) return Reflect.apply(nativeOpen, this, args);
|
|
254
|
+
const method = domString(args[0]);
|
|
255
|
+
const urlInput = domString(args[1]);
|
|
256
|
+
const rest = args.slice(2, 5).map((value, index) => index === 0 ? Boolean(value) : value == null ? null : domString(value));
|
|
257
|
+
let url;
|
|
258
|
+
try {
|
|
259
|
+
url = new target.URL(urlInput, target.document.baseURI);
|
|
260
|
+
} catch {
|
|
261
|
+
return Reflect.apply(nativeOpen, this, [
|
|
262
|
+
method,
|
|
263
|
+
urlInput,
|
|
264
|
+
...rest
|
|
265
|
+
]);
|
|
266
|
+
}
|
|
267
|
+
const previous = entries.get(this);
|
|
268
|
+
const entry = {
|
|
269
|
+
url,
|
|
270
|
+
method
|
|
271
|
+
};
|
|
272
|
+
entries.set(this, entry);
|
|
273
|
+
try {
|
|
274
|
+
Reflect.apply(nativeOpen, this, [
|
|
275
|
+
method,
|
|
276
|
+
url.href,
|
|
277
|
+
...rest
|
|
278
|
+
]);
|
|
279
|
+
previous?.release?.();
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (entries.get(this) === entry) {
|
|
282
|
+
if (previous) entries.set(this, previous);
|
|
283
|
+
else entries.delete(this);
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const send = function(body) {
|
|
289
|
+
const captured = permissions.captureGrants();
|
|
290
|
+
const normalized = normalizeXhrBody(body, target);
|
|
291
|
+
const entry = entries.get(this);
|
|
292
|
+
const matched = entry ? matchRules(rules, entry.url, entry.method) : [];
|
|
293
|
+
const signals = permissions.assertAllowed("xhr", matched, captured);
|
|
294
|
+
if (!entry) {
|
|
295
|
+
if (!untracked.has(this)) {
|
|
296
|
+
untracked.add(this);
|
|
297
|
+
report("untracked-xhr");
|
|
298
|
+
}
|
|
299
|
+
return nativeSend.call(this, normalized);
|
|
300
|
+
}
|
|
301
|
+
if (!matched.length) return nativeSend.call(this, normalized);
|
|
302
|
+
if (entry.release) return nativeSend.call(this, normalized);
|
|
303
|
+
const signal = target.AbortSignal.any(signals);
|
|
304
|
+
let live = true;
|
|
305
|
+
const release = () => {
|
|
306
|
+
if (!live) return;
|
|
307
|
+
live = false;
|
|
308
|
+
signal.removeEventListener("abort", cancel);
|
|
309
|
+
this.removeEventListener("loadend", release);
|
|
310
|
+
active.delete(cancel);
|
|
311
|
+
if (entry.release === release) delete entry.release;
|
|
312
|
+
};
|
|
313
|
+
const cancel = () => {
|
|
314
|
+
release();
|
|
315
|
+
if (entries.get(this) !== entry) return;
|
|
316
|
+
try {
|
|
317
|
+
nativeAbort.call(this);
|
|
318
|
+
} catch {
|
|
319
|
+
report("cleanup-error");
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
entry.release = release;
|
|
323
|
+
active.add(cancel);
|
|
324
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
325
|
+
this.addEventListener("loadend", release);
|
|
326
|
+
try {
|
|
327
|
+
if (signal.aborted) permissions.assertAllowed("xhr", matched, captured);
|
|
328
|
+
signal.throwIfAborted();
|
|
329
|
+
nativeSend.call(this, normalized);
|
|
330
|
+
if (this.readyState === 4) release();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
release();
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
open,
|
|
338
|
+
send,
|
|
339
|
+
dispose() {
|
|
340
|
+
for (const cancel of [...active]) cancel();
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/enforcement/request-rules/installPatch.ts
|
|
346
|
+
/** Restore the exact original descriptor, without overwriting a subsequent library's patch. */
|
|
347
|
+
function installPatch(target, key, wrapper, replaced) {
|
|
348
|
+
const original = Object.getOwnPropertyDescriptor(target, key);
|
|
349
|
+
Object.defineProperty(target, key, {
|
|
350
|
+
configurable: true,
|
|
351
|
+
enumerable: original?.enumerable ?? true,
|
|
352
|
+
writable: true,
|
|
353
|
+
value: wrapper
|
|
354
|
+
});
|
|
355
|
+
return () => {
|
|
356
|
+
if (target[key] !== wrapper) {
|
|
357
|
+
replaced();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (original) Object.defineProperty(target, key, original);
|
|
361
|
+
else Reflect.deleteProperty(target, key);
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/enforcement/request-rules/createConsentRequestRuleGuard.ts
|
|
366
|
+
var ownerKey = Symbol.for("@peax/consent-core/request-rules-owner");
|
|
367
|
+
/** Optional window-local safety net. No browser access, subscriptions or patches until start(). */
|
|
368
|
+
function createConsentRequestRuleGuard(options) {
|
|
369
|
+
requireConsent(options.consent);
|
|
370
|
+
const rules = compileRules(options.rules);
|
|
371
|
+
for (const callback of [options.onBlocked, options.onIssue]) if (callback !== void 0 && typeof callback !== "function") throw new TypeError("Expected a request-rule callback.");
|
|
372
|
+
let disposed = false;
|
|
373
|
+
let started = false;
|
|
374
|
+
let permissions;
|
|
375
|
+
let xhr;
|
|
376
|
+
const restore = [];
|
|
377
|
+
let releaseOwner;
|
|
378
|
+
function report(code) {
|
|
379
|
+
try {
|
|
380
|
+
Promise.resolve(options.onIssue?.(Object.freeze({ code }))).catch(() => {});
|
|
381
|
+
} catch {}
|
|
382
|
+
}
|
|
383
|
+
function teardown() {
|
|
384
|
+
permissions?.dispose();
|
|
385
|
+
xhr?.dispose();
|
|
386
|
+
for (const undo of restore.splice(0).reverse()) try {
|
|
387
|
+
undo();
|
|
388
|
+
} catch {
|
|
389
|
+
report("cleanup-error");
|
|
390
|
+
}
|
|
391
|
+
releaseOwner?.();
|
|
392
|
+
releaseOwner = void 0;
|
|
393
|
+
}
|
|
394
|
+
return Object.freeze({
|
|
395
|
+
start() {
|
|
396
|
+
if (disposed) throw new TypeError("Request-rule guard is disposed.");
|
|
397
|
+
if (started) return;
|
|
398
|
+
const target = options.target ?? (typeof window === "undefined" ? void 0 : window);
|
|
399
|
+
if (typeof target?.fetch !== "function" || typeof target.Request !== "function" || typeof target.AbortSignal?.any !== "function" || typeof target.XMLHttpRequest?.prototype.open !== "function" || typeof target.XMLHttpRequest.prototype.send !== "function") throw new TypeError("Expected a window with Fetch and XMLHttpRequest.");
|
|
400
|
+
const registry = target;
|
|
401
|
+
if (registry[ownerKey]) throw new TypeError("A request-rule guard already owns this window.");
|
|
402
|
+
const token = {};
|
|
403
|
+
Object.defineProperty(target, ownerKey, {
|
|
404
|
+
configurable: true,
|
|
405
|
+
value: token
|
|
406
|
+
});
|
|
407
|
+
releaseOwner = () => {
|
|
408
|
+
if (registry[ownerKey] === token) Reflect.deleteProperty(target, ownerKey);
|
|
409
|
+
};
|
|
410
|
+
try {
|
|
411
|
+
const nativeFetch = target.fetch;
|
|
412
|
+
const current = createRulePermissions(options, rules, report, () => disposed);
|
|
413
|
+
permissions = current;
|
|
414
|
+
if (disposed) throw new TypeError("Request-rule guard was disposed during startup.");
|
|
415
|
+
const fetch = createFetchInterceptor(target, nativeFetch, rules, current);
|
|
416
|
+
xhr = createXhrInterceptor(target, rules, current, report);
|
|
417
|
+
restore.push(installPatch(target, "fetch", fetch, () => report("patch-replaced")));
|
|
418
|
+
restore.push(installPatch(target.XMLHttpRequest.prototype, "open", xhr.open, () => report("patch-replaced")));
|
|
419
|
+
restore.push(installPatch(target.XMLHttpRequest.prototype, "send", xhr.send, () => report("patch-replaced")));
|
|
420
|
+
started = true;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
teardown();
|
|
423
|
+
throw new TypeError("Could not install request-rule guard.", { cause: error });
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
dispose() {
|
|
427
|
+
if (disposed) return;
|
|
428
|
+
disposed = true;
|
|
429
|
+
teardown();
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
export { ConsentRequestRuleError, createConsentRequestRuleGuard };
|