@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.
@@ -0,0 +1,77 @@
1
+ import * as z from 'zod/mini';
2
+
3
+ declare type ConsentState = z.infer<typeof consentStateSchema>;
4
+
5
+ declare const consentStateSchema: z.ZodMiniReadonly<z.ZodMiniObject<{
6
+ choices: z.ZodMiniReadonly<z.ZodMiniArray<z.ZodMiniReadonly<z.ZodMiniObject<{
7
+ useId: z.ZodMiniString<string>;
8
+ consentVersion: z.ZodMiniInt;
9
+ decision: z.ZodMiniEnum<{
10
+ granted: "granted";
11
+ denied: "denied";
12
+ }>;
13
+ }, z.core.$strict>>>>;
14
+ }, z.core.$strict>>;
15
+
16
+ export declare type ConsentStorage = Readonly<{
17
+ /** Null means no record. Invalid or unreadable records return failure, never empty choices. */
18
+ load: () => Promise<ConsentStorageResult<ConsentState | null>>;
19
+ /** Captures validated state at invocation; writes run in invocation order. */
20
+ save: (state: unknown) => Promise<ConsentStorageResult<void>>;
21
+ /** Available only when the adapter supports external record invalidation. Does not load or save. */
22
+ subscribe?: (listener: () => void) => () => void;
23
+ }>;
24
+
25
+ /** One record, scoped by the adapter's key or endpoint. Only null means absent. */
26
+ export declare type ConsentStorageAdapter = Readonly<{
27
+ read: () => string | null | Promise<string | null>;
28
+ write: (record: string) => void | Promise<void>;
29
+ /** Optional lazy, key-scoped external invalidations; notifications contain no stored values. */
30
+ subscribe?: (listener: () => void) => () => void;
31
+ }>;
32
+
33
+ /** Storage failures deliberately exclude raw errors, URLs, headers, and stored values. */
34
+ export declare type ConsentStorageError = Readonly<{
35
+ type: 'read' | 'write';
36
+ }> | Readonly<{
37
+ type: 'validation';
38
+ issues: readonly ValidationIssue[];
39
+ }>;
40
+
41
+ export declare type ConsentStorageResult<T> = Readonly<{
42
+ success: true;
43
+ data: T;
44
+ }> | Readonly<{
45
+ success: false;
46
+ error: ConsentStorageError;
47
+ }>;
48
+
49
+ /** Explicit persistence, independent of any runtime. No reads/writes occur on creation. */
50
+ export declare function createConsentStorage(adapter: ConsentStorageAdapter): ConsentStorage;
51
+
52
+ /** GET returns the wire record (204/404 means absent); PUT replaces it. No retries. */
53
+ export declare function createFetchStorageAdapter({ url, fetch: fetchRequest, requestInit, }: FetchStorageAdapterOptions): ConsentStorageAdapter;
54
+
55
+ export declare function createLocalStorageAdapter({ key, storage }: LocalStorageAdapterOptions): ConsentStorageAdapter;
56
+
57
+ export declare type FetchStorageAdapterOptions = Readonly<{
58
+ url: string;
59
+ fetch?: typeof globalThis.fetch;
60
+ /** GET/PUT, body, no-store caching and rejected redirects are controlled by the adapter. */
61
+ requestInit?: Omit<RequestInit, 'method' | 'body' | 'cache' | 'redirect'>;
62
+ }>;
63
+
64
+ export declare type LocalStorageAdapterOptions = Readonly<{
65
+ key: string;
66
+ /** Defaults to globalThis.localStorage, resolved only during read/write/explicit subscription. */
67
+ storage?: Pick<Storage, 'getItem' | 'setItem'>;
68
+ }>;
69
+
70
+ /** A validation issue without a copy of the rejected input. */
71
+ declare type ValidationIssue = Readonly<{
72
+ code: string;
73
+ path: readonly (string | number)[];
74
+ message: string;
75
+ }>;
76
+
77
+ export { }
@@ -0,0 +1,207 @@
1
+ import { t as assertStorageIdentifier } from "./assertStorageIdentifier-BQuj-9bp.js";
2
+ import * as z from "zod/mini";
3
+ //#region src/utils/validation.ts
4
+ var nonemptyStringSchema = /* @__PURE__ */ (() => z.string({ error: "Expected a nonempty string." }).check(z.refine((value) => value.trim().length > 0, { error: "Expected a nonempty string." })))();
5
+ var consentVersionSchema = /* @__PURE__ */ (() => z.int({ error: "Expected a positive safe integer consent version." }).check(z.positive({ error: "Expected a positive safe integer consent version." })))();
6
+ /** Internal adapter: keep Zod errors out of the public result contract. */
7
+ function validateWithSchema(schema, input) {
8
+ const result = schema.safeParse(input, { error: (issue) => issue.code === "unrecognized_keys" ? `Unexpected fields: ${issue.keys.join(", ")}.` : issue.code === "invalid_type" ? `Expected ${issue.expected}.` : "Invalid consent data." });
9
+ if (result.success) return {
10
+ success: true,
11
+ data: result.data
12
+ };
13
+ return {
14
+ success: false,
15
+ issues: result.error.issues.map((issue) => ({
16
+ code: issue.code,
17
+ path: issue.path.map((segment) => typeof segment === "number" ? segment : String(segment)),
18
+ message: issue.message
19
+ }))
20
+ };
21
+ }
22
+ //#endregion
23
+ //#region src/consent/schemas.ts
24
+ var consentDecisionSchema = /* @__PURE__ */ z.enum(["granted", "denied"], { error: "Expected granted or denied." });
25
+ var consentChoiceSchema = /* @__PURE__ */ (() => z.readonly(z.strictObject({
26
+ useId: nonemptyStringSchema,
27
+ consentVersion: consentVersionSchema,
28
+ decision: consentDecisionSchema
29
+ })))();
30
+ var consentStateSchema = /* @__PURE__ */ (() => z.readonly(z.strictObject({ choices: z.readonly(z.array(consentChoiceSchema).check((context) => {
31
+ const ids = /* @__PURE__ */ new Set();
32
+ context.value.forEach((choice, index) => {
33
+ if (ids.has(choice.useId)) context.issues.push({
34
+ code: "custom",
35
+ path: [index, "useId"],
36
+ message: "Only one consent choice per use is allowed.",
37
+ input: choice.useId
38
+ });
39
+ ids.add(choice.useId);
40
+ });
41
+ })) })))();
42
+ //#endregion
43
+ //#region src/storage/schemas.ts
44
+ var consentRecordV1Schema = /* @__PURE__ */ (() => z.readonly(z.strictObject({
45
+ formatVersion: z.literal(1),
46
+ state: consentStateSchema
47
+ })))();
48
+ var consentRecordHeaderSchema = /* @__PURE__ */ (() => z.object({ formatVersion: z.int().check(z.positive()) }))();
49
+ //#endregion
50
+ //#region src/storage/decodeConsentState.ts
51
+ function failure(code, message, path = []) {
52
+ return {
53
+ success: false,
54
+ issues: [{
55
+ code,
56
+ path,
57
+ message
58
+ }]
59
+ };
60
+ }
61
+ /** Restores validated choices, not permissions. Missing storage is handled by the caller. */
62
+ function decodeConsentState(input) {
63
+ if (typeof input !== "string") return failure("invalid_type", "Expected a JSON string.");
64
+ let record;
65
+ try {
66
+ record = JSON.parse(input);
67
+ } catch {
68
+ return failure("invalid_json", "Invalid consent JSON.");
69
+ }
70
+ const header = validateWithSchema(consentRecordHeaderSchema, record);
71
+ if (!header.success) return header;
72
+ if (header.data.formatVersion !== 1) return failure("unsupported_format_version", "Unsupported consent storage format version.", ["formatVersion"]);
73
+ const result = validateWithSchema(consentRecordV1Schema, record);
74
+ if (!result.success) return result;
75
+ return {
76
+ success: true,
77
+ data: result.data.state
78
+ };
79
+ }
80
+ //#endregion
81
+ //#region src/storage/encodeConsentState.ts
82
+ /** Encodes decoded JSON-like choices without policy reconciliation or storage I/O. */
83
+ function encodeConsentState(input) {
84
+ const result = validateWithSchema(consentRecordV1Schema, {
85
+ formatVersion: 1,
86
+ state: input
87
+ });
88
+ if (!result.success) return result;
89
+ return {
90
+ success: true,
91
+ data: JSON.stringify(result.data)
92
+ };
93
+ }
94
+ //#endregion
95
+ //#region src/storage/createConsentStorage.ts
96
+ /** Explicit persistence, independent of any runtime. No reads/writes occur on creation. */
97
+ function createConsentStorage(adapter) {
98
+ let pending = Promise.resolve();
99
+ const subscribe = adapter.subscribe;
100
+ function enqueue(operation) {
101
+ const result = pending.then(operation);
102
+ pending = result.then(() => void 0, () => void 0);
103
+ return result;
104
+ }
105
+ return Object.freeze({
106
+ ...subscribe ? { subscribe: (listener) => subscribe.call(adapter, listener) } : {},
107
+ load: () => enqueue(async () => {
108
+ let record;
109
+ try {
110
+ record = await adapter.read();
111
+ } catch {
112
+ return {
113
+ success: false,
114
+ error: { type: "read" }
115
+ };
116
+ }
117
+ if (record === null) return {
118
+ success: true,
119
+ data: null
120
+ };
121
+ const decoded = decodeConsentState(record);
122
+ return decoded.success ? decoded : {
123
+ success: false,
124
+ error: {
125
+ type: "validation",
126
+ issues: decoded.issues
127
+ }
128
+ };
129
+ }),
130
+ save: (state) => {
131
+ const encoded = encodeConsentState(state);
132
+ if (!encoded.success) return Promise.resolve({
133
+ success: false,
134
+ error: {
135
+ type: "validation",
136
+ issues: encoded.issues
137
+ }
138
+ });
139
+ return enqueue(async () => {
140
+ try {
141
+ await adapter.write(encoded.data);
142
+ return {
143
+ success: true,
144
+ data: void 0
145
+ };
146
+ } catch {
147
+ return {
148
+ success: false,
149
+ error: { type: "write" }
150
+ };
151
+ }
152
+ });
153
+ }
154
+ });
155
+ }
156
+ //#endregion
157
+ //#region src/storage/fetch/createFetchStorageAdapter.ts
158
+ /** GET returns the wire record (204/404 means absent); PUT replaces it. No retries. */
159
+ function createFetchStorageAdapter({ url, fetch: fetchRequest, requestInit }) {
160
+ assertStorageIdentifier(url);
161
+ async function request(method, body) {
162
+ const headers = new Headers(requestInit?.headers);
163
+ headers.set("Accept", "application/json");
164
+ if (method === "PUT") headers.set("Content-Type", "application/json");
165
+ const init = {
166
+ ...requestInit,
167
+ method,
168
+ headers,
169
+ body: body ?? null,
170
+ cache: "no-store",
171
+ redirect: "error"
172
+ };
173
+ return fetchRequest ? fetchRequest(url, init) : globalThis.fetch(url, init);
174
+ }
175
+ return Object.freeze({
176
+ read: async () => {
177
+ const response = await request("GET");
178
+ if (response.status === 204 || response.status === 404) return null;
179
+ if (response.status !== 200) throw new Error(`Consent read failed (HTTP ${response.status}).`);
180
+ return response.text();
181
+ },
182
+ write: async (record) => {
183
+ const response = await request("PUT", record);
184
+ if (!response.ok) throw new Error(`Consent write failed (HTTP ${response.status}).`);
185
+ }
186
+ });
187
+ }
188
+ //#endregion
189
+ //#region src/storage/localstorage/createLocalStorageAdapter.ts
190
+ function createLocalStorageAdapter({ key, storage }) {
191
+ assertStorageIdentifier(key);
192
+ return Object.freeze({
193
+ read: () => (storage ?? globalThis.localStorage).getItem(key),
194
+ write: (record) => (storage ?? globalThis.localStorage).setItem(key, record),
195
+ subscribe(listener) {
196
+ if (typeof window === "undefined") return () => {};
197
+ const backend = storage ?? globalThis.localStorage;
198
+ const changed = (event) => {
199
+ if (event.storageArea === backend && (event.key === key || event.key === null)) listener();
200
+ };
201
+ window.addEventListener("storage", changed);
202
+ return () => window.removeEventListener("storage", changed);
203
+ }
204
+ });
205
+ }
206
+ //#endregion
207
+ export { createConsentStorage, createFetchStorageAdapter, createLocalStorageAdapter };
@@ -0,0 +1,15 @@
1
+ //#region src/enforcement/_utils/validate.ts
2
+ /**
3
+ * Constructor guard shared by every enforcement entry point. Runs before any caller-supplied
4
+ * getter, so a malformed guard is rejected at creation rather than during a granted call.
5
+ */
6
+ function requireConsent(consent) {
7
+ const candidate = consent;
8
+ if (!candidate || typeof candidate.evaluate !== "function" || typeof candidate.subscribe !== "function") throw new TypeError("Expected a consent runtime.");
9
+ }
10
+ /** Use IDs are exact: surrounding whitespace is significant and never trimmed. */
11
+ function requireUseId(useId) {
12
+ if (typeof useId !== "string" || useId.trim().length === 0) throw new TypeError("Expected a non blank use ID.");
13
+ }
14
+ //#endregion
15
+ export { requireUseId as n, requireConsent as t };
package/package.json ADDED
@@ -0,0 +1,128 @@
1
+ {
2
+ "name": "@peax/consent-core",
3
+ "version": "0.6.0",
4
+ "description": "Framework-independent consent state and runtime primitives.",
5
+ "keywords": [
6
+ "consent-management",
7
+ "cookie-consent",
8
+ "privacy",
9
+ "gdpr",
10
+ "typescript",
11
+ "framework-agnostic"
12
+ ],
13
+ "license": "Apache-2.0",
14
+ "homepage": "https://github.com/peaxco/consent",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/peaxco/consent.git",
18
+ "directory": "packages/consent-core"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/peaxco/consent/issues"
22
+ },
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "main": "./dist/index.js",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "./enforcement/fetch": {
38
+ "types": "./dist/enforcement/fetch.d.ts",
39
+ "import": "./dist/enforcement/fetch.js",
40
+ "default": "./dist/enforcement/fetch.js"
41
+ },
42
+ "./enforcement/axios": {
43
+ "types": "./dist/enforcement/axios.d.ts",
44
+ "import": "./dist/enforcement/axios.js",
45
+ "default": "./dist/enforcement/axios.js"
46
+ },
47
+ "./enforcement/request-rules": {
48
+ "types": "./dist/enforcement/request-rules.d.ts",
49
+ "import": "./dist/enforcement/request-rules.js",
50
+ "default": "./dist/enforcement/request-rules.js"
51
+ },
52
+ "./enforcement/script": {
53
+ "types": "./dist/enforcement/script.d.ts",
54
+ "import": "./dist/enforcement/script.js",
55
+ "default": "./dist/enforcement/script.js"
56
+ },
57
+ "./enforcement/iframe": {
58
+ "types": "./dist/enforcement/iframe.d.ts",
59
+ "import": "./dist/enforcement/iframe.js",
60
+ "default": "./dist/enforcement/iframe.js"
61
+ },
62
+ "./enforcement/integration": {
63
+ "types": "./dist/enforcement/integration.d.ts",
64
+ "import": "./dist/enforcement/integration.js",
65
+ "default": "./dist/enforcement/integration.js"
66
+ },
67
+ "./enforcement/posthog": {
68
+ "types": "./dist/enforcement/posthog.d.ts",
69
+ "import": "./dist/enforcement/posthog.js",
70
+ "default": "./dist/enforcement/posthog.js"
71
+ },
72
+ "./enforcement/markup": {
73
+ "types": "./dist/enforcement/markup.d.ts",
74
+ "import": "./dist/enforcement/markup.js",
75
+ "default": "./dist/enforcement/markup.js"
76
+ },
77
+ "./storage": {
78
+ "types": "./dist/storage.d.ts",
79
+ "import": "./dist/storage.js",
80
+ "default": "./dist/storage.js"
81
+ },
82
+ "./storage/localforage": {
83
+ "types": "./dist/storage/localforage.d.ts",
84
+ "import": "./dist/storage/localforage.js",
85
+ "default": "./dist/storage/localforage.js"
86
+ },
87
+ "./storage/sync": {
88
+ "types": "./dist/storage/sync.d.ts",
89
+ "import": "./dist/storage/sync.js",
90
+ "default": "./dist/storage/sync.js"
91
+ }
92
+ },
93
+ "publishConfig": {
94
+ "access": "public",
95
+ "provenance": true,
96
+ "registry": "https://registry.npmjs.org"
97
+ },
98
+ "scripts": {
99
+ "build": "vite build && vite build --mode entries",
100
+ "test": "vitest run",
101
+ "typecheck": "tsc --noEmit"
102
+ },
103
+ "dependencies": {
104
+ "zod": "^4.6.5"
105
+ },
106
+ "peerDependencies": {
107
+ "axios": "^1.20.0",
108
+ "localforage": "^1.10.0"
109
+ },
110
+ "peerDependenciesMeta": {
111
+ "axios": {
112
+ "optional": true
113
+ },
114
+ "localforage": {
115
+ "optional": true
116
+ }
117
+ },
118
+ "devDependencies": {
119
+ "@types/node": "^26.6.4",
120
+ "axios": "^1.20.0",
121
+ "jsdom": "30.1.1",
122
+ "localforage": "^1.10.0",
123
+ "typescript": "5.9.3",
124
+ "vite": "^8.3.2",
125
+ "vite-plugin-dts": "^5.1.1",
126
+ "vitest": "^5.0.3"
127
+ }
128
+ }