better-auth-cookie-consent 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.
package/README.md ADDED
@@ -0,0 +1,328 @@
1
+ # better-auth-cookie-consent
2
+
3
+ GDPR-compliant cookie consent management plugin for [Better Auth](https://better-auth.com/).
4
+
5
+ - Store and manage user cookie consent preferences
6
+ - Works for both anonymous and authenticated users
7
+ - Automatic merge of anonymous consent on sign-in / sign-up
8
+ - Consent versioning with automatic invalidation
9
+ - Validation schema support (e.g. Zod) to enforce consent shape
10
+ - Generic client plugin — `cookieConsentClient<z.infer<typeof schema>>()` types your consent model end-to-end
11
+
12
+ ## How It Works
13
+
14
+ ### Anonymous User Flow
15
+
16
+ ```
17
+ ┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
18
+ │ User visits site │────▶│ Cookie banner shown │────▶│ User accepts/ │
19
+ │ (no session, │ │ (no server call if │ │ rejects/ │
20
+ │ no anonId) │ │ no anonId cookie) │ │ customizes │
21
+ └──────────────────┘ └─────────────────────┘ └────────┬─────────┘
22
+
23
+
24
+ ┌──────────────────┐
25
+ │ Client calls │
26
+ │ setConsent with │
27
+ │ typed consent │
28
+ │ object → server │
29
+ │ stores it │
30
+ └──────────────────┘
31
+ ```
32
+
33
+ ### Sign-In / Sign-Up Merge Flow
34
+
35
+ ```
36
+ ┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
37
+ │ Anonymous user │────▶│ User signs in or │────▶│ Server hook reads│
38
+ │ has given consent │ │ signs up │ │ anonId from │
39
+ │ (anonId cookie │ │ │ │ cookie & merges │
40
+ │ is set) │ │ │ │ consent to user │
41
+ └──────────────────┘ └─────────────────────┘ └────────┬─────────┘
42
+
43
+
44
+ ┌──────────────────┐
45
+ │ Consent now tied │
46
+ │ to userId — │
47
+ │ persists across │
48
+ │ devices/sessions │
49
+ └──────────────────┘
50
+ ```
51
+
52
+ ### Session-Aware Banner Flow
53
+
54
+ ```
55
+ ┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
56
+ │ Banner visible │────▶│ User logs in │────▶│ Client detects │
57
+ │ (no consent) │ │ │ │ session change │
58
+ │ │ │ │ │ (null → session) │
59
+ └──────────────────┘ └─────────────────────┘ └────────┬─────────┘
60
+
61
+
62
+ ┌──────────────────┐
63
+ │ Fetches consent │
64
+ │ from server → │
65
+ │ hides banner, │
66
+ │ persists anonId │
67
+ │ cookie for logout │
68
+ └──────────────────┘
69
+ ```
70
+
71
+ ## Installation
72
+
73
+ ```bash
74
+ # npm
75
+ npm install better-auth-cookie-consent
76
+
77
+ # pnpm
78
+ pnpm add better-auth-cookie-consent
79
+ ```
80
+
81
+ ## Server Setup
82
+
83
+ ```ts
84
+ import { betterAuth } from 'better-auth';
85
+ import { cookieConsentPlugin, defaultConsentSchema } from 'better-auth-cookie-consent';
86
+
87
+ export const auth = betterAuth({
88
+ // ... your config
89
+ plugins: [
90
+ cookieConsentPlugin({
91
+ consentVersion: 'v1',
92
+ consent: { validationSchema: defaultConsentSchema },
93
+ onConsentChange: async ({ consent }) => {
94
+ console.log('Consent changed:', consent);
95
+ },
96
+ }),
97
+ ],
98
+ });
99
+ ```
100
+
101
+ ### Plugin Options
102
+
103
+ | Option | Type | Default | Description |
104
+ | ------------------------ | ------------------ | ------- | -------------------------------------------- |
105
+ | consentVersion | `string` | `"v1"` | Current consent policy version |
106
+ | consent.validationSchema | `StandardSchemaV1` | — | Schema to validate consent (e.g. Zod object) |
107
+ | onConsentChange | `function` | — | Callback when consent is created/updated |
108
+ | rateLimit | `object` | — | Rate limit for set/merge endpoints |
109
+ | schema | `object` | — | Schema overrides for the cookieConsent table |
110
+
111
+ ### Validation Schema
112
+
113
+ The plugin validates consent JSON against a `StandardSchemaV1` (e.g. Zod schema) on every write. A preset `defaultConsentSchema` is exported for the standard categories:
114
+
115
+ ```ts
116
+ import { defaultConsentSchema } from 'better-auth-cookie-consent';
117
+
118
+ // Equivalent to:
119
+ // z.object({
120
+ // necessary: z.boolean(),
121
+ // analytics: z.boolean(),
122
+ // marketing: z.boolean(),
123
+ // functional: z.boolean(),
124
+ // })
125
+ ```
126
+
127
+ ## Client Setup
128
+
129
+ The client plugin accepts a generic type parameter for the consent shape. Use `z.infer<typeof schema>` to get full end-to-end typing:
130
+
131
+ ```ts
132
+ import { createAuthClient } from 'better-auth/client';
133
+ import { defaultConsentSchema } from 'better-auth-cookie-consent';
134
+ import { cookieConsentClient } from 'better-auth-cookie-consent/client';
135
+ import type { z } from 'zod';
136
+
137
+ type ConsentModel = z.infer<typeof defaultConsentSchema>;
138
+ // { necessary: boolean; analytics: boolean; marketing: boolean; functional: boolean }
139
+
140
+ export const authClient = createAuthClient({
141
+ plugins: [cookieConsentClient<ConsentModel>()],
142
+ });
143
+ ```
144
+
145
+ This types `setConsent`, `getConsent`, and the nanostore atom so that your consent object is fully checked at compile time.
146
+
147
+ ## Usage
148
+
149
+ ### Set Consent
150
+
151
+ ```ts
152
+ await authClient.cookieConsent.setConsent({
153
+ anonymousId: 'anon-123',
154
+ consent: {
155
+ necessary: true,
156
+ analytics: true,
157
+ marketing: false,
158
+ functional: true,
159
+ },
160
+ consentVersion: 'v1',
161
+ });
162
+ ```
163
+
164
+ ### Accept All / Reject All
165
+
166
+ Build the consent object on the client and call `setConsent`:
167
+
168
+ ```ts
169
+ // Accept all
170
+ await authClient.cookieConsent.setConsent({
171
+ anonymousId: 'anon-123',
172
+ consent: { necessary: true, analytics: true, marketing: true, functional: true },
173
+ consentVersion: 'v1',
174
+ });
175
+
176
+ // Reject all (keep necessary)
177
+ await authClient.cookieConsent.setConsent({
178
+ anonymousId: 'anon-123',
179
+ consent: { necessary: true, analytics: false, marketing: false, functional: false },
180
+ consentVersion: 'v1',
181
+ });
182
+ ```
183
+
184
+ ### Get Consent
185
+
186
+ ```ts
187
+ const { data } = await authClient.cookieConsent.getConsent('anon-123');
188
+ // data.consent — the consent record (or null)
189
+ // data.versionMatch — whether stored version matches current
190
+ ```
191
+
192
+ ### Merge Anonymous Consent After Login
193
+
194
+ ```ts
195
+ await authClient.cookieConsent.mergeConsent('anon-123');
196
+ ```
197
+
198
+ > **Note:** Manual merging is typically unnecessary — the plugin automatically merges anonymous consent when a user signs in or signs up via server-side hooks.
199
+
200
+ ## Cookie Banner — What You Need to Handle
201
+
202
+ The plugin provides the server and client APIs but **does not include a cookie banner UI**. You need to implement the banner in your application. Here's what the banner should handle:
203
+
204
+ ### 1. Anonymous ID Management
205
+
206
+ Store a unique anonymous ID in a cookie named `cookie-consent-anon-id`. This cookie must be:
207
+
208
+ - Created when the user first interacts with the banner (e.g. `crypto.randomUUID()`)
209
+ - Sent as a standard browser cookie so the server can read it during sign-in/sign-up hooks
210
+ - Readable via SSR (e.g. `injectRequest()` in Analog.js)
211
+
212
+ ```ts
213
+ // Set the anonymous ID cookie
214
+ document.cookie = `cookie-consent-anon-id=${id}; path=/; max-age=31536000; SameSite=Lax`;
215
+ ```
216
+
217
+ ### 2. Banner Visibility Logic
218
+
219
+ ```
220
+ On page load:
221
+ IF anonId cookie exists OR user is logged in:
222
+ → Fetch consent from server (GET /cookie-consent/get)
223
+ → If consent exists and version matches → hide banner
224
+ → Otherwise → show banner
225
+ ELSE:
226
+ → Show banner immediately (no server call needed)
227
+ ```
228
+
229
+ ### 3. Session Change Detection
230
+
231
+ Subscribe to the auth client's session state. When the session transitions from `null` → logged-in:
232
+
233
+ 1. Fetch consent from the server
234
+ 2. If consent exists, hide the banner and persist the `anonymousId` cookie
235
+ 3. This ensures the banner disappears immediately on login without a page reload
236
+ 4. The persisted `anonymousId` ensures consent is still found after logout + page reload
237
+
238
+ ### 4. Accept / Reject / Customize
239
+
240
+ All actions use the `setConsent` endpoint — the client builds the consent object:
241
+
242
+ - **Accept All**: Build `{ necessary: true, analytics: true, ... }` and call `setConsent`
243
+ - **Reject All**: Build `{ necessary: true, analytics: false, ... }` and call `setConsent`
244
+ - **Custom**: Use form values and call `setConsent`
245
+
246
+ The consent object is validated against the server's `validationSchema` on every write.
247
+
248
+ ### 5. Typing Categories
249
+
250
+ Use `keyof` on the inferred consent model to type your banner categories:
251
+
252
+ ```ts
253
+ import type { z } from 'zod';
254
+ import { defaultConsentSchema } from 'better-auth-cookie-consent';
255
+
256
+ type ConsentModel = z.infer<typeof defaultConsentSchema>;
257
+ type CategoryId = keyof ConsentModel;
258
+
259
+ const CATEGORIES: { id: CategoryId; label: string }[] = [
260
+ { id: 'necessary', label: 'Necessary' },
261
+ { id: 'analytics', label: 'Analytics' },
262
+ // TypeScript enforces that id must be a valid consent category
263
+ ];
264
+ ```
265
+
266
+ ### 6. Re-open Banner
267
+
268
+ After consent is recorded, provide a way for users to manage their preferences (e.g. a "Manage Cookies" link in the footer).
269
+
270
+ ## Server-Side Helpers
271
+
272
+ ```ts
273
+ import { getConsentFromCtx, hasConsent } from 'better-auth-cookie-consent';
274
+
275
+ // Inside an endpoint handler:
276
+ const record = await getConsentFromCtx(ctx);
277
+
278
+ if (await hasConsent(ctx, 'analytics')) {
279
+ // tracking is allowed
280
+ }
281
+ ```
282
+
283
+ ## API Endpoints
284
+
285
+ | Method | Path | Description |
286
+ | ------ | ----------------------- | ------------------------------- |
287
+ | POST | `/cookie-consent/set` | Create or update consent |
288
+ | GET | `/cookie-consent/get` | Retrieve consent |
289
+ | POST | `/cookie-consent/merge` | Merge anonymous consent to user |
290
+
291
+ ## Auto-Merge on Sign-In / Sign-Up
292
+
293
+ The plugin registers a server-side `after` hook that runs on every `sign-in/*` and `sign-up/*` path. When the `cookie-consent-anon-id` cookie is present in the request, the hook automatically merges the anonymous consent record to the newly authenticated user. This covers all auth methods (email, social, biometrics, passkey, etc.).
294
+
295
+ ## Consent Versioning
296
+
297
+ When the `consentVersion` option changes, the `getConsent` endpoint returns `versionMatch: false` so the client knows to prompt for re-consent.
298
+
299
+ ## Database Schema
300
+
301
+ The plugin creates a `cookieConsent` table:
302
+
303
+ | Column | Type | Description |
304
+ | -------------- | ------- | -------------------------------- |
305
+ | id | string | Primary key |
306
+ | userId | string? | References user table |
307
+ | anonymousId | string | Anonymous client identifier |
308
+ | consent | string | JSON-encoded consent preferences |
309
+ | consentVersion | string | Consent policy version |
310
+ | timestamp | date | When consent was recorded |
311
+
312
+ ### Prisma Schema
313
+
314
+ ```prisma
315
+ model CookieConsent {
316
+ id String @id @default(cuid())
317
+ userId String?
318
+ anonymousId String
319
+ consent String
320
+ consentVersion String
321
+ timestamp DateTime @default(now())
322
+ user User? @relation(fields: [userId], references: [id])
323
+ }
324
+ ```
325
+
326
+ ## License
327
+
328
+ MIT
@@ -0,0 +1,114 @@
1
+ import { n as cookieConsentPlugin, o as Consent } from "./index-SuR6zFkq.mjs";
2
+ import * as better_auth0 from "better-auth";
3
+ import * as nanostores from "nanostores";
4
+ import * as better_auth_client0 from "better-auth/client";
5
+
6
+ //#region src/client.d.ts
7
+ /**
8
+ * Client-side consent state, kept in sync with the server.
9
+ */
10
+ interface ConsentState<TConsent extends Consent = Consent> {
11
+ consent: TConsent | null;
12
+ consentVersion: string | null;
13
+ versionMatch: boolean;
14
+ }
15
+ /**
16
+ * Client plugin for cookie consent management.
17
+ *
18
+ * @typeParam TConsent - The consent shape, typically `z.infer<typeof yourConsentSchema>`.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { defaultConsentSchema } from 'better-auth-cookie-consent';
23
+ * import { cookieConsentClient } from 'better-auth-cookie-consent/client';
24
+ * import type { z } from 'zod';
25
+ *
26
+ * const authClient = createAuthClient({
27
+ * plugins: [cookieConsentClient<z.infer<typeof defaultConsentSchema>>()],
28
+ * });
29
+ * ```
30
+ */
31
+ declare const cookieConsentClient: <TConsent extends Consent = Consent>() => {
32
+ id: "cookie-consent";
33
+ $InferServerPlugin: ReturnType<typeof cookieConsentPlugin>;
34
+ getAtoms($fetch: better_auth_client0.BetterFetch): {
35
+ $consent: nanostores.PreinitializedWritableAtom<ConsentState<TConsent>> & object;
36
+ };
37
+ getActions($fetch: better_auth_client0.BetterFetch, $store: better_auth0.ClientStore): {
38
+ cookieConsent: {
39
+ /**
40
+ * Set consent preferences on the server.
41
+ * Also used for accept-all / reject-all by passing the
42
+ * appropriate consent object (all `true` or all `false`).
43
+ */
44
+ setConsent: (data: {
45
+ anonymousId: string;
46
+ consent: TConsent;
47
+ consentVersion: string;
48
+ }) => Promise<{
49
+ data: {
50
+ status: boolean;
51
+ };
52
+ error: null;
53
+ } | {
54
+ data: null;
55
+ error: {
56
+ message?: string | undefined;
57
+ status: number;
58
+ statusText: string;
59
+ };
60
+ }>;
61
+ /**
62
+ * Retrieve consent from the server.
63
+ */
64
+ getConsent: (anonymousId?: string) => Promise<{
65
+ data: null;
66
+ error: {
67
+ message?: string | undefined;
68
+ status: number;
69
+ statusText: string;
70
+ };
71
+ } | {
72
+ data: {
73
+ consent: {
74
+ id: string;
75
+ userId?: string | null;
76
+ anonymousId: string;
77
+ consent: TConsent;
78
+ consentVersion: string;
79
+ timestamp: string;
80
+ } | null;
81
+ versionMatch: boolean;
82
+ };
83
+ error: null;
84
+ }>;
85
+ /**
86
+ * Merge anonymous consent into the authenticated user's record.
87
+ */
88
+ mergeConsent: (anonymousId: string) => Promise<{
89
+ data: null;
90
+ error: {
91
+ message?: string | undefined;
92
+ status: number;
93
+ statusText: string;
94
+ };
95
+ } | {
96
+ data: {
97
+ status: boolean;
98
+ merged: boolean;
99
+ };
100
+ error: null;
101
+ }>;
102
+ };
103
+ };
104
+ $ERROR_CODES: {
105
+ MISSING_ANONYMOUS_ID: better_auth0.RawError<"MISSING_ANONYMOUS_ID">;
106
+ CONSENT_NOT_FOUND: better_auth0.RawError<"CONSENT_NOT_FOUND">;
107
+ INVALID_CONSENT: better_auth0.RawError<"INVALID_CONSENT">;
108
+ VERSION_MISMATCH: better_auth0.RawError<"VERSION_MISMATCH">;
109
+ AUTHENTICATION_REQUIRED: better_auth0.RawError<"AUTHENTICATION_REQUIRED">;
110
+ };
111
+ };
112
+ //#endregion
113
+ export { ConsentState, cookieConsentClient };
114
+ //# sourceMappingURL=client.d.mts.map
@@ -0,0 +1,79 @@
1
+ import { t as COOKIE_CONSENT_ERROR_CODES } from "./error-codes-D3VdAX9J.mjs";
2
+ import { atom } from "nanostores";
3
+ //#region src/client.ts
4
+ /**
5
+ * Client plugin for cookie consent management.
6
+ *
7
+ * @typeParam TConsent - The consent shape, typically `z.infer<typeof yourConsentSchema>`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { defaultConsentSchema } from 'better-auth-cookie-consent';
12
+ * import { cookieConsentClient } from 'better-auth-cookie-consent/client';
13
+ * import type { z } from 'zod';
14
+ *
15
+ * const authClient = createAuthClient({
16
+ * plugins: [cookieConsentClient<z.infer<typeof defaultConsentSchema>>()],
17
+ * });
18
+ * ```
19
+ */
20
+ const cookieConsentClient = () => {
21
+ return {
22
+ id: "cookie-consent",
23
+ $InferServerPlugin: {},
24
+ getAtoms($fetch) {
25
+ return { $consent: atom({
26
+ consent: null,
27
+ consentVersion: null,
28
+ versionMatch: false
29
+ }) };
30
+ },
31
+ getActions($fetch, $store) {
32
+ const consentAtom = $store.atoms.$consent;
33
+ async function syncFromServer(anonymousId) {
34
+ let path = "/cookie-consent/get";
35
+ if (anonymousId) {
36
+ const params = new URLSearchParams({ anonymousId });
37
+ path = `${path}?${params.toString()}`;
38
+ }
39
+ const res = await $fetch(`${path}`, { method: "GET" });
40
+ if (res.data) consentAtom.set({
41
+ consent: res.data.consent?.consent ?? null,
42
+ consentVersion: res.data.consent?.consentVersion ?? null,
43
+ versionMatch: res.data.versionMatch
44
+ });
45
+ return res;
46
+ }
47
+ return { cookieConsent: {
48
+ setConsent: async (data) => {
49
+ const res = await $fetch("/cookie-consent/set", {
50
+ method: "POST",
51
+ body: data
52
+ });
53
+ if (res.data?.status) consentAtom.set({
54
+ consent: data.consent,
55
+ consentVersion: data.consentVersion,
56
+ versionMatch: true
57
+ });
58
+ return res;
59
+ },
60
+ getConsent: async (anonymousId) => {
61
+ return syncFromServer(anonymousId);
62
+ },
63
+ mergeConsent: async (anonymousId) => {
64
+ const res = await $fetch("/cookie-consent/merge", {
65
+ method: "POST",
66
+ body: { anonymousId }
67
+ });
68
+ if (res.data?.merged) await syncFromServer(anonymousId);
69
+ return res;
70
+ }
71
+ } };
72
+ },
73
+ $ERROR_CODES: COOKIE_CONSENT_ERROR_CODES
74
+ };
75
+ };
76
+ //#endregion
77
+ export { cookieConsentClient };
78
+
79
+ //# sourceMappingURL=client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from 'better-auth/client';\nimport { atom } from 'nanostores';\n\nimport { COOKIE_CONSENT_ERROR_CODES } from './error-codes';\nimport type { Consent, cookieConsentPlugin } from './index';\n\n/**\n * Client-side consent state, kept in sync with the server.\n */\nexport interface ConsentState<TConsent extends Consent = Consent> {\n consent: TConsent | null;\n consentVersion: string | null;\n versionMatch: boolean;\n}\n\n/**\n * Client plugin for cookie consent management.\n *\n * @typeParam TConsent - The consent shape, typically `z.infer<typeof yourConsentSchema>`.\n *\n * @example\n * ```ts\n * import { defaultConsentSchema } from 'better-auth-cookie-consent';\n * import { cookieConsentClient } from 'better-auth-cookie-consent/client';\n * import type { z } from 'zod';\n *\n * const authClient = createAuthClient({\n * plugins: [cookieConsentClient<z.infer<typeof defaultConsentSchema>>()],\n * });\n * ```\n */\nexport const cookieConsentClient = <TConsent extends Consent = Consent>() => {\n return {\n id: 'cookie-consent',\n $InferServerPlugin: {} as ReturnType<typeof cookieConsentPlugin>,\n\n getAtoms($fetch) {\n const $consent = atom<ConsentState<TConsent>>({\n consent: null,\n consentVersion: null,\n versionMatch: false,\n });\n return { $consent };\n },\n\n getActions($fetch, $store) {\n const consentAtom = $store.atoms.$consent as ReturnType<typeof atom<ConsentState<TConsent>>>;\n\n async function syncFromServer(anonymousId?: string) {\n let path = '/cookie-consent/get';\n if (anonymousId) {\n const params = new URLSearchParams({ anonymousId });\n path = `${path}?${params.toString()}`;\n }\n const res = await $fetch<{\n consent: {\n id: string;\n userId?: string | null;\n anonymousId: string;\n consent: TConsent;\n consentVersion: string;\n timestamp: string;\n } | null;\n versionMatch: boolean;\n }>(`${path}`, { method: 'GET' });\n if (res.data) {\n consentAtom.set({\n consent: res.data.consent?.consent ?? null,\n consentVersion: res.data.consent?.consentVersion ?? null,\n versionMatch: res.data.versionMatch,\n });\n }\n return res;\n }\n\n return {\n cookieConsent: {\n /**\n * Set consent preferences on the server.\n * Also used for accept-all / reject-all by passing the\n * appropriate consent object (all `true` or all `false`).\n */\n setConsent: async (data: {\n anonymousId: string;\n consent: TConsent;\n consentVersion: string;\n }) => {\n const res = await $fetch<{ status: boolean }>('/cookie-consent/set', {\n method: 'POST',\n body: data,\n });\n if (res.data?.status) {\n consentAtom.set({\n consent: data.consent,\n consentVersion: data.consentVersion,\n versionMatch: true,\n });\n }\n return res;\n },\n\n /**\n * Retrieve consent from the server.\n */\n getConsent: async (anonymousId?: string) => {\n return syncFromServer(anonymousId);\n },\n\n /**\n * Merge anonymous consent into the authenticated user's record.\n */\n mergeConsent: async (anonymousId: string) => {\n const res = await $fetch<{ status: boolean; merged: boolean }>(\n '/cookie-consent/merge',\n {\n method: 'POST',\n body: { anonymousId },\n },\n );\n if (res.data?.merged) {\n await syncFromServer(anonymousId);\n }\n return res;\n },\n },\n };\n },\n\n $ERROR_CODES: COOKIE_CONSENT_ERROR_CODES,\n } satisfies BetterAuthClientPlugin;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA+BA,MAAa,4BAAgE;AAC3E,QAAO;EACL,IAAI;EACJ,oBAAoB,EAAE;EAEtB,SAAS,QAAQ;AAMf,UAAO,EAAE,UALQ,KAA6B;IAC5C,SAAS;IACT,gBAAgB;IAChB,cAAc;IACf,CAAC,EACiB;;EAGrB,WAAW,QAAQ,QAAQ;GACzB,MAAM,cAAc,OAAO,MAAM;GAEjC,eAAe,eAAe,aAAsB;IAClD,IAAI,OAAO;AACX,QAAI,aAAa;KACf,MAAM,SAAS,IAAI,gBAAgB,EAAE,aAAa,CAAC;AACnD,YAAO,GAAG,KAAK,GAAG,OAAO,UAAU;;IAErC,MAAM,MAAM,MAAM,OAUf,GAAG,QAAQ,EAAE,QAAQ,OAAO,CAAC;AAChC,QAAI,IAAI,KACN,aAAY,IAAI;KACd,SAAS,IAAI,KAAK,SAAS,WAAW;KACtC,gBAAgB,IAAI,KAAK,SAAS,kBAAkB;KACpD,cAAc,IAAI,KAAK;KACxB,CAAC;AAEJ,WAAO;;AAGT,UAAO,EACL,eAAe;IAMb,YAAY,OAAO,SAIb;KACJ,MAAM,MAAM,MAAM,OAA4B,uBAAuB;MACnE,QAAQ;MACR,MAAM;MACP,CAAC;AACF,SAAI,IAAI,MAAM,OACZ,aAAY,IAAI;MACd,SAAS,KAAK;MACd,gBAAgB,KAAK;MACrB,cAAc;MACf,CAAC;AAEJ,YAAO;;IAMT,YAAY,OAAO,gBAAyB;AAC1C,YAAO,eAAe,YAAY;;IAMpC,cAAc,OAAO,gBAAwB;KAC3C,MAAM,MAAM,MAAM,OAChB,yBACA;MACE,QAAQ;MACR,MAAM,EAAE,aAAa;MACtB,CACF;AACD,SAAI,IAAI,MAAM,OACZ,OAAM,eAAe,YAAY;AAEnC,YAAO;;IAEV,EACF;;EAGH,cAAc;EACf"}
@@ -0,0 +1,13 @@
1
+ import { defineErrorCodes } from "better-auth";
2
+ //#region src/error-codes.ts
3
+ const COOKIE_CONSENT_ERROR_CODES = defineErrorCodes({
4
+ MISSING_ANONYMOUS_ID: "Anonymous ID is required",
5
+ CONSENT_NOT_FOUND: "Cookie consent record not found",
6
+ INVALID_CONSENT: "Consent must be a non-empty object with boolean values",
7
+ VERSION_MISMATCH: "Consent version is outdated and must be renewed",
8
+ AUTHENTICATION_REQUIRED: "Authentication is required for this operation"
9
+ });
10
+ //#endregion
11
+ export { COOKIE_CONSENT_ERROR_CODES as t };
12
+
13
+ //# sourceMappingURL=error-codes-D3VdAX9J.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-codes-D3VdAX9J.mjs","names":[],"sources":["../src/error-codes.ts"],"sourcesContent":["import { defineErrorCodes } from 'better-auth';\n\nexport const COOKIE_CONSENT_ERROR_CODES = defineErrorCodes({\n MISSING_ANONYMOUS_ID: 'Anonymous ID is required',\n CONSENT_NOT_FOUND: 'Cookie consent record not found',\n INVALID_CONSENT: 'Consent must be a non-empty object with boolean values',\n VERSION_MISMATCH: 'Consent version is outdated and must be renewed',\n AUTHENTICATION_REQUIRED: 'Authentication is required for this operation',\n});\n"],"mappings":";;AAEA,MAAa,6BAA6B,iBAAiB;CACzD,sBAAsB;CACtB,mBAAmB;CACnB,iBAAiB;CACjB,kBAAkB;CAClB,yBAAyB;CAC1B,CAAC"}
@@ -0,0 +1,270 @@
1
+ import * as z from "zod";
2
+ import * as better_auth0 from "better-auth";
3
+ import { BetterAuthOptions, DBAdapter, InferOptionSchema, StandardSchemaV1 } from "better-auth";
4
+
5
+ //#region src/schema.d.ts
6
+ declare const cookieConsent: {
7
+ cookieConsent: {
8
+ fields: {
9
+ userId: {
10
+ type: "string";
11
+ required: false;
12
+ references: {
13
+ model: string;
14
+ field: string;
15
+ };
16
+ };
17
+ anonymousId: {
18
+ type: "string";
19
+ required: true;
20
+ };
21
+ consent: {
22
+ type: "string";
23
+ required: true;
24
+ };
25
+ consentVersion: {
26
+ type: "string";
27
+ required: true;
28
+ };
29
+ timestamp: {
30
+ type: "date";
31
+ defaultValue: () => Date;
32
+ required: true;
33
+ };
34
+ };
35
+ };
36
+ };
37
+ //#endregion
38
+ //#region src/type.d.ts
39
+ /**
40
+ * Consent categories mapped to boolean values.
41
+ * Each key represents a cookie category (e.g., "analytics", "marketing").
42
+ */
43
+ type ConsentSchemaModel = z.ZodObject<Record<string, z.ZodBoolean>>;
44
+ type Consent = z.infer<ConsentSchemaModel>;
45
+ /**
46
+ * Options for the cookie consent plugin.
47
+ */
48
+ interface CookieConsentOptions {
49
+ /**
50
+ * Current consent version identifier.
51
+ * When this changes, stored consents with older versions
52
+ * are considered outdated and users must re-consent.
53
+ * @default "v1"
54
+ */
55
+ consentVersion?: string;
56
+ /**
57
+ * Callback invoked whenever consent changes.
58
+ * @param data the consent record and request
59
+ */
60
+ onConsentChange?: (data: {
61
+ consent: CookieConsentRecord;
62
+ }, request?: Request) => Promise<void>;
63
+ /**
64
+ * Rate limit configuration for consent endpoints.
65
+ */
66
+ rateLimit?: {
67
+ /**
68
+ * Time window in seconds for which the rate limit applies.
69
+ * @default 10
70
+ */
71
+ window: number;
72
+ /**
73
+ * Maximum number of requests allowed within the time window.
74
+ * @default 10 requests
75
+ */
76
+ max: number;
77
+ };
78
+ /**
79
+ * Schema overrides for the cookie consent table.
80
+ */
81
+ schema?: InferOptionSchema<typeof cookieConsent> | undefined;
82
+ /**
83
+ * Consent validation configuration.
84
+ */
85
+ consent?: {
86
+ /**
87
+ * A Standard Schema (e.g. Zod) used to validate the consent
88
+ * object on every `setConsent` call. When omitted, the
89
+ * consent object is accepted as-is (any `Consent`).
90
+ */
91
+ validationSchema?: StandardSchemaV1 & ConsentSchemaModel;
92
+ };
93
+ }
94
+ /**
95
+ * A stored cookie consent record.
96
+ */
97
+ interface CookieConsentRecord {
98
+ /**
99
+ * Database identifier.
100
+ */
101
+ id: string;
102
+ /**
103
+ * The authenticated user ID, if available.
104
+ */
105
+ userId?: string | null;
106
+ /**
107
+ * An anonymous identifier for unauthenticated users.
108
+ */
109
+ anonymousId: string;
110
+ /**
111
+ * JSON-encoded consent preferences.
112
+ */
113
+ consent: string;
114
+ /**
115
+ * The version of the consent policy.
116
+ */
117
+ consentVersion: string;
118
+ /**
119
+ * Timestamp when the consent was recorded.
120
+ */
121
+ timestamp: Date;
122
+ }
123
+ /**
124
+ * Payload for creating/updating a consent record (without auto-generated fields).
125
+ */
126
+ type CookieConsentPayload = Omit<CookieConsentRecord, 'id'>;
127
+ //#endregion
128
+ //#region src/index.d.ts
129
+ /**
130
+ * Cookie name used to store the anonymous consent ID.
131
+ * The client sets this cookie so the server-side hook can
132
+ * read it during sign-in to auto-merge anonymous consent.
133
+ */
134
+ declare const ANONYMOUS_ID_COOKIE = "cookie-consent-anon-id";
135
+ /**
136
+ * Preset validation schema for cookie consent.
137
+ * Validates the standard consent categories: necessary, analytics, marketing, functional.
138
+ * Use this with `consent.validationSchema` in the plugin options.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * cookieConsentPlugin({
143
+ * consentVersion: 'v1',
144
+ * consent: { validationSchema: defaultConsentSchema },
145
+ * })
146
+ * ```
147
+ */
148
+ declare const defaultConsentSchema: z.ZodObject<{
149
+ necessary: z.ZodBoolean;
150
+ analytics: z.ZodBoolean;
151
+ marketing: z.ZodBoolean;
152
+ functional: z.ZodBoolean;
153
+ }, z.core.$strip>;
154
+ declare const cookieConsentPlugin: <O extends CookieConsentOptions>(options?: O) => {
155
+ id: "cookie-consent";
156
+ schema: {
157
+ cookieConsent: {
158
+ fields: {
159
+ userId: {
160
+ type: "string";
161
+ required: false;
162
+ references: {
163
+ model: string;
164
+ field: string;
165
+ };
166
+ };
167
+ anonymousId: {
168
+ type: "string";
169
+ required: true;
170
+ };
171
+ consent: {
172
+ type: "string";
173
+ required: true;
174
+ };
175
+ consentVersion: {
176
+ type: "string";
177
+ required: true;
178
+ };
179
+ timestamp: {
180
+ type: "date";
181
+ defaultValue: () => Date;
182
+ required: true;
183
+ };
184
+ };
185
+ };
186
+ };
187
+ endpoints: {
188
+ setConsent: better_auth0.StrictEndpoint<"/cookie-consent/set", {
189
+ method: "POST";
190
+ body: z.ZodObject<{
191
+ anonymousId: z.ZodString;
192
+ consent: z.ZodRecord<z.ZodString, z.ZodBoolean>;
193
+ consentVersion: z.ZodString;
194
+ }, z.core.$strip>;
195
+ }, {
196
+ status: boolean;
197
+ }>;
198
+ getConsent: better_auth0.StrictEndpoint<"/cookie-consent/get", {
199
+ method: "GET";
200
+ query: z.ZodObject<{
201
+ anonymousId: z.ZodOptional<z.ZodString>;
202
+ }, z.core.$strip>;
203
+ }, {
204
+ consent: null;
205
+ versionMatch: boolean;
206
+ } | {
207
+ consent: {
208
+ id: string;
209
+ userId: string | null | undefined;
210
+ anonymousId: string;
211
+ consent: Consent;
212
+ consentVersion: string;
213
+ timestamp: Date;
214
+ };
215
+ versionMatch: boolean;
216
+ }>;
217
+ mergeConsent: better_auth0.StrictEndpoint<"/cookie-consent/merge", {
218
+ method: "POST";
219
+ body: z.ZodObject<{
220
+ anonymousId: z.ZodString;
221
+ }, z.core.$strip>;
222
+ }, {
223
+ status: boolean;
224
+ merged: boolean;
225
+ }>;
226
+ };
227
+ hooks: {
228
+ after: {
229
+ matcher: (context: better_auth0.HookEndpointContext) => boolean;
230
+ handler: (inputContext: better_auth0.MiddlewareInputContext<better_auth0.MiddlewareOptions>) => Promise<void>;
231
+ }[];
232
+ };
233
+ options: NoInfer<O>;
234
+ rateLimit: {
235
+ pathMatcher: (path: string) => boolean;
236
+ window: number;
237
+ max: number;
238
+ }[];
239
+ $ERROR_CODES: {
240
+ MISSING_ANONYMOUS_ID: better_auth0.RawError<"MISSING_ANONYMOUS_ID">;
241
+ CONSENT_NOT_FOUND: better_auth0.RawError<"CONSENT_NOT_FOUND">;
242
+ INVALID_CONSENT: better_auth0.RawError<"INVALID_CONSENT">;
243
+ VERSION_MISMATCH: better_auth0.RawError<"VERSION_MISMATCH">;
244
+ AUTHENTICATION_REQUIRED: better_auth0.RawError<"AUTHENTICATION_REQUIRED">;
245
+ };
246
+ };
247
+ /**
248
+ * Extract the parsed consent object from the endpoint context.
249
+ * Returns `null` if no consent is available.
250
+ */
251
+ declare function getConsentFromCtx(ctx: {
252
+ context: {
253
+ adapter: DBAdapter<BetterAuthOptions>;
254
+ session?: {
255
+ user?: {
256
+ id?: string;
257
+ };
258
+ } | null;
259
+ };
260
+ query?: {
261
+ anonymousId?: string;
262
+ };
263
+ }): Promise<CookieConsentRecord | null>;
264
+ /**
265
+ * Check if a specific consent category is granted.
266
+ */
267
+ declare function hasConsent(ctx: Parameters<typeof getConsentFromCtx>[0], category: string): Promise<boolean>;
268
+ //#endregion
269
+ export { hasConsent as a, CookieConsentOptions as c, getConsentFromCtx as i, CookieConsentPayload as l, cookieConsentPlugin as n, Consent as o, defaultConsentSchema as r, ConsentSchemaModel as s, ANONYMOUS_ID_COOKIE as t, CookieConsentRecord as u };
270
+ //# sourceMappingURL=index-SuR6zFkq.d.mts.map
@@ -0,0 +1,2 @@
1
+ import { a as hasConsent, c as CookieConsentOptions, i as getConsentFromCtx, l as CookieConsentPayload, n as cookieConsentPlugin, o as Consent, r as defaultConsentSchema, s as ConsentSchemaModel, t as ANONYMOUS_ID_COOKIE, u as CookieConsentRecord } from "./index-SuR6zFkq.mjs";
2
+ export { ANONYMOUS_ID_COOKIE, Consent, ConsentSchemaModel, CookieConsentOptions, CookieConsentPayload, CookieConsentRecord, cookieConsentPlugin, defaultConsentSchema, getConsentFromCtx, hasConsent };
package/dist/index.mjs ADDED
@@ -0,0 +1,333 @@
1
+ import { t as COOKIE_CONSENT_ERROR_CODES } from "./error-codes-D3VdAX9J.mjs";
2
+ import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx } from "better-auth/api";
3
+ import * as z from "zod";
4
+ import { BASE_ERROR_CODES } from "better-auth";
5
+ import { mergeSchema } from "better-auth/db";
6
+ //#region src/routes.ts
7
+ const setConsentSchema = z.object({
8
+ anonymousId: z.string().meta({ description: "Anonymous identifier for unauthenticated users" }),
9
+ consent: z.record(z.string(), z.boolean()).meta({ description: "Consent preferences as category-boolean pairs" }),
10
+ consentVersion: z.string().meta({ description: "Version of the consent policy" })
11
+ });
12
+ const getConsentSchema = z.object({ anonymousId: z.string().optional().meta({ description: "Anonymous identifier fallback when no session exists" }) });
13
+ const mergeConsentSchema = z.object({ anonymousId: z.string().meta({ description: "Anonymous identifier to merge consent from" }) });
14
+ const setConsent = (options) => createAuthEndpoint("/cookie-consent/set", {
15
+ method: "POST",
16
+ body: setConsentSchema
17
+ }, async (ctx) => {
18
+ const { anonymousId, consent, consentVersion } = ctx.body;
19
+ if (!anonymousId) throw APIError.from("BAD_REQUEST", COOKIE_CONSENT_ERROR_CODES.MISSING_ANONYMOUS_ID);
20
+ if (!consent || Object.keys(consent).length === 0) throw APIError.from("BAD_REQUEST", COOKIE_CONSENT_ERROR_CODES.INVALID_CONSENT);
21
+ const validatedConsent = validateConsent(options, consent, ctx.context.logger);
22
+ const userId = (await getSessionFromCtx(ctx))?.user?.id ?? null;
23
+ const existing = await findConsentRecord(ctx, userId, anonymousId);
24
+ const consentJson = JSON.stringify(validatedConsent);
25
+ const now = /* @__PURE__ */ new Date();
26
+ if (existing) await ctx.context.adapter.update({
27
+ model: "cookieConsent",
28
+ where: [{
29
+ field: "id",
30
+ value: existing.id
31
+ }],
32
+ update: {
33
+ userId,
34
+ consent: consentJson,
35
+ consentVersion,
36
+ timestamp: now
37
+ }
38
+ });
39
+ else await ctx.context.adapter.create({
40
+ model: "cookieConsent",
41
+ data: {
42
+ userId,
43
+ anonymousId,
44
+ consent: consentJson,
45
+ consentVersion,
46
+ timestamp: now
47
+ }
48
+ });
49
+ if (options.onConsentChange) {
50
+ const record = await findConsentRecord(ctx, userId, anonymousId) ?? {
51
+ id: "",
52
+ userId,
53
+ anonymousId,
54
+ consent: consentJson,
55
+ consentVersion,
56
+ timestamp: now
57
+ };
58
+ await ctx.context.runInBackgroundOrAwait(options.onConsentChange({ consent: record }, ctx.request));
59
+ }
60
+ return ctx.json({ status: true });
61
+ });
62
+ const getConsent = (options) => createAuthEndpoint("/cookie-consent/get", {
63
+ method: "GET",
64
+ query: getConsentSchema
65
+ }, async (ctx) => {
66
+ const userId = (await getSessionFromCtx(ctx))?.user?.id ?? null;
67
+ const anonymousId = ctx.query?.anonymousId;
68
+ if (!userId && !anonymousId) throw APIError.from("BAD_REQUEST", COOKIE_CONSENT_ERROR_CODES.MISSING_ANONYMOUS_ID);
69
+ const record = await findConsentRecord(ctx, userId, anonymousId);
70
+ if (!record) return ctx.json({
71
+ consent: null,
72
+ versionMatch: false
73
+ });
74
+ const currentVersion = options.consentVersion ?? "v1";
75
+ const versionMatch = record.consentVersion === currentVersion;
76
+ return ctx.json({
77
+ consent: {
78
+ id: record.id,
79
+ userId: record.userId,
80
+ anonymousId: record.anonymousId,
81
+ consent: JSON.parse(record.consent),
82
+ consentVersion: record.consentVersion,
83
+ timestamp: record.timestamp
84
+ },
85
+ versionMatch
86
+ });
87
+ });
88
+ const mergeConsent = (_options) => createAuthEndpoint("/cookie-consent/merge", {
89
+ method: "POST",
90
+ body: mergeConsentSchema
91
+ }, async (ctx) => {
92
+ const userId = (await getSessionFromCtx(ctx))?.user?.id;
93
+ if (!userId) throw APIError.from("UNAUTHORIZED", COOKIE_CONSENT_ERROR_CODES.AUTHENTICATION_REQUIRED);
94
+ const merged = await mergeAnonymousConsentToUser(ctx.context.adapter, userId, ctx.body.anonymousId);
95
+ return ctx.json({
96
+ status: true,
97
+ merged
98
+ });
99
+ });
100
+ /**
101
+ * Merge anonymous consent into a user's record.
102
+ * Shared between the merge endpoint and the sign-in/sign-up hook.
103
+ *
104
+ * @returns `true` if consent was merged, `false` if no anonymous record was found.
105
+ */
106
+ async function mergeAnonymousConsentToUser(adapter, userId, anonymousId) {
107
+ const anonymousRecord = await adapter.findOne({
108
+ model: "cookieConsent",
109
+ where: [{
110
+ field: "anonymousId",
111
+ value: anonymousId
112
+ }]
113
+ });
114
+ if (!anonymousRecord || anonymousRecord.userId) return false;
115
+ const userRecord = await adapter.findOne({
116
+ model: "cookieConsent",
117
+ where: [{
118
+ field: "userId",
119
+ value: userId
120
+ }]
121
+ });
122
+ if (userRecord) {
123
+ await adapter.update({
124
+ model: "cookieConsent",
125
+ where: [{
126
+ field: "id",
127
+ value: userRecord.id
128
+ }],
129
+ update: {
130
+ consent: anonymousRecord.consent,
131
+ consentVersion: anonymousRecord.consentVersion,
132
+ anonymousId,
133
+ timestamp: /* @__PURE__ */ new Date()
134
+ }
135
+ });
136
+ await adapter.delete({
137
+ model: "cookieConsent",
138
+ where: [{
139
+ field: "id",
140
+ value: anonymousRecord.id
141
+ }]
142
+ });
143
+ } else await adapter.update({
144
+ model: "cookieConsent",
145
+ where: [{
146
+ field: "id",
147
+ value: anonymousRecord.id
148
+ }],
149
+ update: { userId }
150
+ });
151
+ return true;
152
+ }
153
+ /**
154
+ * Find a consent record by userId (preferred) or anonymousId fallback.
155
+ */
156
+ async function findConsentRecord(ctx, userId, anonymousId) {
157
+ if (userId) {
158
+ const byUser = await ctx.context.adapter.findOne({
159
+ model: "cookieConsent",
160
+ where: [{
161
+ field: "userId",
162
+ value: userId
163
+ }]
164
+ });
165
+ if (byUser) return byUser;
166
+ }
167
+ if (anonymousId) return ctx.context.adapter.findOne({
168
+ model: "cookieConsent",
169
+ where: [{
170
+ field: "anonymousId",
171
+ value: anonymousId
172
+ }]
173
+ });
174
+ return null;
175
+ }
176
+ /**
177
+ * Validate the consent object against the configured validation schema.
178
+ * Returns the validated consent or the original consent when no schema is set.
179
+ */
180
+ function validateConsent(options, consent, logger) {
181
+ if (!options.consent?.validationSchema) return consent;
182
+ const validationResult = options.consent.validationSchema["~standard"].validate(consent);
183
+ if (validationResult instanceof Promise) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.ASYNC_VALIDATION_NOT_SUPPORTED);
184
+ if (validationResult.issues) {
185
+ logger.error("Invalid consent", validationResult.issues);
186
+ throw APIError.from("BAD_REQUEST", COOKIE_CONSENT_ERROR_CODES.INVALID_CONSENT);
187
+ }
188
+ return validationResult.value;
189
+ }
190
+ //#endregion
191
+ //#region src/schema.ts
192
+ const cookieConsent = { cookieConsent: { fields: {
193
+ userId: {
194
+ type: "string",
195
+ required: false,
196
+ references: {
197
+ model: "user",
198
+ field: "id"
199
+ }
200
+ },
201
+ anonymousId: {
202
+ type: "string",
203
+ required: true
204
+ },
205
+ consent: {
206
+ type: "string",
207
+ required: true
208
+ },
209
+ consentVersion: {
210
+ type: "string",
211
+ required: true
212
+ },
213
+ timestamp: {
214
+ type: "date",
215
+ defaultValue: () => /* @__PURE__ */ new Date(),
216
+ required: true
217
+ }
218
+ } } };
219
+ const getSchema = (options) => {
220
+ return mergeSchema(cookieConsent, options.schema);
221
+ };
222
+ //#endregion
223
+ //#region src/index.ts
224
+ /**
225
+ * Cookie name used to store the anonymous consent ID.
226
+ * The client sets this cookie so the server-side hook can
227
+ * read it during sign-in to auto-merge anonymous consent.
228
+ */
229
+ const ANONYMOUS_ID_COOKIE = "cookie-consent-anon-id";
230
+ /**
231
+ * Parse a single cookie value from a raw `Cookie` header string.
232
+ */
233
+ function parseCookieValue(cookieHeader, name) {
234
+ if (!cookieHeader) return void 0;
235
+ for (const pair of cookieHeader.split("; ")) {
236
+ const [key, ...valueParts] = pair.split("=");
237
+ if (key === name && valueParts.length > 0) {
238
+ const raw = valueParts.join("=");
239
+ try {
240
+ return decodeURIComponent(raw);
241
+ } catch {
242
+ return raw;
243
+ }
244
+ }
245
+ }
246
+ }
247
+ /**
248
+ * Preset validation schema for cookie consent.
249
+ * Validates the standard consent categories: necessary, analytics, marketing, functional.
250
+ * Use this with `consent.validationSchema` in the plugin options.
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * cookieConsentPlugin({
255
+ * consentVersion: 'v1',
256
+ * consent: { validationSchema: defaultConsentSchema },
257
+ * })
258
+ * ```
259
+ */
260
+ const defaultConsentSchema = z.object({
261
+ necessary: z.boolean(),
262
+ analytics: z.boolean(),
263
+ marketing: z.boolean(),
264
+ functional: z.boolean()
265
+ });
266
+ const cookieConsentPlugin = (options = {}) => {
267
+ return {
268
+ id: "cookie-consent",
269
+ schema: getSchema(options),
270
+ endpoints: {
271
+ setConsent: setConsent(options),
272
+ getConsent: getConsent(options),
273
+ mergeConsent: mergeConsent(options)
274
+ },
275
+ hooks: { after: [{
276
+ matcher: (context) => {
277
+ return !!context.path && (context.path.startsWith("/sign-in/") || context.path.startsWith("/sign-up/"));
278
+ },
279
+ handler: createAuthMiddleware(async (ctx) => {
280
+ const userId = ctx.context.newSession?.user?.id;
281
+ if (!userId) return;
282
+ const anonymousId = parseCookieValue(ctx.headers?.get("cookie"), ANONYMOUS_ID_COOKIE);
283
+ if (!anonymousId) return;
284
+ await mergeAnonymousConsentToUser(ctx.context.adapter, userId, anonymousId);
285
+ })
286
+ }] },
287
+ options,
288
+ rateLimit: [{
289
+ pathMatcher: (path) => ["/cookie-consent/set", "/cookie-consent/merge"].includes(path),
290
+ window: options.rateLimit?.window ?? 10,
291
+ max: options.rateLimit?.max ?? 10
292
+ }],
293
+ $ERROR_CODES: COOKIE_CONSENT_ERROR_CODES
294
+ };
295
+ };
296
+ /**
297
+ * Extract the parsed consent object from the endpoint context.
298
+ * Returns `null` if no consent is available.
299
+ */
300
+ async function getConsentFromCtx(ctx) {
301
+ const userId = ctx.context.session?.user?.id ?? null;
302
+ const anonymousId = ctx.query?.anonymousId;
303
+ if (userId) {
304
+ const byUser = await ctx.context.adapter.findOne({
305
+ model: "cookieConsent",
306
+ where: [{
307
+ field: "userId",
308
+ value: userId
309
+ }]
310
+ });
311
+ if (byUser) return byUser;
312
+ }
313
+ if (anonymousId) return ctx.context.adapter.findOne({
314
+ model: "cookieConsent",
315
+ where: [{
316
+ field: "anonymousId",
317
+ value: anonymousId
318
+ }]
319
+ });
320
+ return null;
321
+ }
322
+ /**
323
+ * Check if a specific consent category is granted.
324
+ */
325
+ async function hasConsent(ctx, category) {
326
+ const record = await getConsentFromCtx(ctx);
327
+ if (!record) return false;
328
+ return JSON.parse(record.consent)[category] === true;
329
+ }
330
+ //#endregion
331
+ export { ANONYMOUS_ID_COOKIE, cookieConsentPlugin, defaultConsentSchema, getConsentFromCtx, hasConsent };
332
+
333
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/routes.ts","../src/schema.ts","../src/index.ts"],"sourcesContent":["import {\n BASE_ERROR_CODES,\n type BetterAuthOptions,\n type DBAdapter,\n type InternalLogger,\n} from 'better-auth';\nimport { APIError, createAuthEndpoint, getSessionFromCtx } from 'better-auth/api';\nimport * as z from 'zod';\n\nimport { COOKIE_CONSENT_ERROR_CODES } from './error-codes';\nimport type {\n Consent,\n CookieConsentOptions,\n CookieConsentPayload,\n CookieConsentRecord,\n} from './type';\n\nconst setConsentSchema = z.object({\n anonymousId: z.string().meta({\n description: 'Anonymous identifier for unauthenticated users',\n }),\n consent: z.record(z.string(), z.boolean()).meta({\n description: 'Consent preferences as category-boolean pairs',\n }),\n consentVersion: z.string().meta({\n description: 'Version of the consent policy',\n }),\n});\n\nconst getConsentSchema = z.object({\n anonymousId: z.string().optional().meta({\n description: 'Anonymous identifier fallback when no session exists',\n }),\n});\n\nconst mergeConsentSchema = z.object({\n anonymousId: z.string().meta({\n description: 'Anonymous identifier to merge consent from',\n }),\n});\n\nexport const setConsent = <O extends CookieConsentOptions>(options: O) =>\n createAuthEndpoint(\n '/cookie-consent/set',\n {\n method: 'POST',\n body: setConsentSchema,\n },\n async (ctx) => {\n const { anonymousId, consent, consentVersion } = ctx.body;\n\n if (!anonymousId) {\n throw APIError.from('BAD_REQUEST', COOKIE_CONSENT_ERROR_CODES.MISSING_ANONYMOUS_ID);\n }\n\n if (!consent || Object.keys(consent).length === 0) {\n throw APIError.from('BAD_REQUEST', COOKIE_CONSENT_ERROR_CODES.INVALID_CONSENT);\n }\n\n const validatedConsent = validateConsent(options, consent, ctx.context.logger);\n\n const session = await getSessionFromCtx(ctx);\n const userId = session?.user?.id ?? null;\n\n // Look for existing record by userId or anonymousId\n const existing = await findConsentRecord(ctx, userId, anonymousId);\n\n const consentJson = JSON.stringify(validatedConsent);\n const now = new Date();\n\n if (existing) {\n await ctx.context.adapter.update<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'id', value: existing.id }],\n update: {\n userId,\n consent: consentJson,\n consentVersion,\n timestamp: now,\n },\n });\n } else {\n await ctx.context.adapter.create<CookieConsentPayload, CookieConsentRecord>({\n model: 'cookieConsent',\n data: {\n userId,\n anonymousId,\n consent: consentJson,\n consentVersion,\n timestamp: now,\n },\n });\n }\n\n if (options.onConsentChange) {\n const record =\n (await findConsentRecord(ctx, userId, anonymousId)) ??\n ({\n id: '',\n userId,\n anonymousId,\n consent: consentJson,\n consentVersion,\n timestamp: now,\n } as CookieConsentRecord);\n\n await ctx.context.runInBackgroundOrAwait(\n options.onConsentChange({ consent: record }, ctx.request),\n );\n }\n\n return ctx.json({ status: true });\n },\n );\n\nexport const getConsent = <O extends CookieConsentOptions>(options: O) =>\n createAuthEndpoint(\n '/cookie-consent/get',\n {\n method: 'GET',\n query: getConsentSchema,\n },\n async (ctx) => {\n const session = await getSessionFromCtx(ctx);\n const userId = session?.user?.id ?? null;\n const anonymousId = ctx.query?.anonymousId;\n\n if (!userId && !anonymousId) {\n throw APIError.from('BAD_REQUEST', COOKIE_CONSENT_ERROR_CODES.MISSING_ANONYMOUS_ID);\n }\n\n const record = await findConsentRecord(ctx, userId, anonymousId);\n\n if (!record) {\n return ctx.json({ consent: null, versionMatch: false });\n }\n\n const currentVersion = options.consentVersion ?? 'v1';\n const versionMatch = record.consentVersion === currentVersion;\n\n return ctx.json({\n consent: {\n id: record.id,\n userId: record.userId,\n anonymousId: record.anonymousId,\n consent: JSON.parse(record.consent) as Consent,\n consentVersion: record.consentVersion,\n timestamp: record.timestamp,\n },\n versionMatch,\n });\n },\n );\n\nexport const mergeConsent = <O extends CookieConsentOptions>(_options: O) =>\n createAuthEndpoint(\n '/cookie-consent/merge',\n {\n method: 'POST',\n body: mergeConsentSchema,\n },\n async (ctx) => {\n const session = await getSessionFromCtx(ctx);\n const userId = session?.user?.id;\n\n if (!userId) {\n throw APIError.from('UNAUTHORIZED', COOKIE_CONSENT_ERROR_CODES.AUTHENTICATION_REQUIRED);\n }\n\n const merged = await mergeAnonymousConsentToUser(\n ctx.context.adapter,\n userId,\n ctx.body.anonymousId,\n );\n return ctx.json({ status: true, merged });\n },\n );\n\n/**\n * Merge anonymous consent into a user's record.\n * Shared between the merge endpoint and the sign-in/sign-up hook.\n *\n * @returns `true` if consent was merged, `false` if no anonymous record was found.\n */\nexport async function mergeAnonymousConsentToUser(\n adapter: DBAdapter<BetterAuthOptions>,\n userId: string,\n anonymousId: string,\n): Promise<boolean> {\n const anonymousRecord = await adapter.findOne<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'anonymousId', value: anonymousId }],\n });\n\n if (!anonymousRecord || anonymousRecord.userId) return false;\n // ^ Skip if record doesn't exist or is already linked to a user (already merged)\n\n const userRecord = await adapter.findOne<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'userId', value: userId }],\n });\n\n if (userRecord) {\n // User already has consent; update with anonymous data\n await adapter.update<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'id', value: userRecord.id }],\n update: {\n consent: anonymousRecord.consent,\n consentVersion: anonymousRecord.consentVersion,\n anonymousId,\n timestamp: new Date(),\n },\n });\n await adapter.delete({\n model: 'cookieConsent',\n where: [{ field: 'id', value: anonymousRecord.id }],\n });\n } else {\n // Attach anonymous consent to user\n await adapter.update<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'id', value: anonymousRecord.id }],\n update: { userId },\n });\n }\n\n return true;\n}\n\n/**\n * Find a consent record by userId (preferred) or anonymousId fallback.\n */\nasync function findConsentRecord(\n ctx: {\n context: {\n adapter: DBAdapter<BetterAuthOptions>;\n };\n },\n userId: string | null | undefined,\n anonymousId: string | undefined,\n): Promise<CookieConsentRecord | null> {\n if (userId) {\n const byUser = await ctx.context.adapter.findOne<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'userId', value: userId }],\n });\n if (byUser) return byUser;\n }\n\n if (anonymousId) {\n return ctx.context.adapter.findOne<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'anonymousId', value: anonymousId }],\n });\n }\n\n return null;\n}\n\n/**\n * Validate the consent object against the configured validation schema.\n * Returns the validated consent or the original consent when no schema is set.\n */\nfunction validateConsent(\n options: CookieConsentOptions,\n consent: Consent,\n logger: InternalLogger,\n): Consent {\n if (!options.consent?.validationSchema) {\n return consent;\n }\n const validationResult = options.consent.validationSchema['~standard'].validate(consent);\n\n if (validationResult instanceof Promise) {\n throw APIError.from('INTERNAL_SERVER_ERROR', BASE_ERROR_CODES.ASYNC_VALIDATION_NOT_SUPPORTED);\n }\n\n if (validationResult.issues) {\n logger.error('Invalid consent', validationResult.issues);\n throw APIError.from('BAD_REQUEST', COOKIE_CONSENT_ERROR_CODES.INVALID_CONSENT);\n }\n\n return validationResult.value as Consent;\n}\n","import { type BetterAuthPluginDBSchema } from 'better-auth';\nimport { mergeSchema } from 'better-auth/db';\n\nimport type { CookieConsentOptions } from './type';\n\nexport const cookieConsent = {\n cookieConsent: {\n fields: {\n userId: {\n type: 'string',\n required: false,\n references: {\n model: 'user',\n field: 'id',\n },\n },\n anonymousId: {\n type: 'string',\n required: true,\n },\n consent: {\n type: 'string',\n required: true,\n },\n consentVersion: {\n type: 'string',\n required: true,\n },\n timestamp: {\n type: 'date',\n defaultValue: () => new Date(),\n required: true,\n },\n },\n },\n} satisfies BetterAuthPluginDBSchema;\n\nexport const getSchema = <O extends CookieConsentOptions>(options: O) => {\n return mergeSchema(cookieConsent, options.schema);\n};\n","import type { BetterAuthOptions, BetterAuthPlugin, DBAdapter } from 'better-auth';\nimport { createAuthMiddleware } from 'better-auth/api';\nimport * as z from 'zod';\n\nimport { COOKIE_CONSENT_ERROR_CODES } from './error-codes';\nimport { getConsent, mergeAnonymousConsentToUser, mergeConsent, setConsent } from './routes';\nimport { getSchema } from './schema';\nimport type {\n Consent,\n ConsentSchemaModel,\n CookieConsentOptions,\n CookieConsentRecord,\n} from './type';\n\n/**\n * Cookie name used to store the anonymous consent ID.\n * The client sets this cookie so the server-side hook can\n * read it during sign-in to auto-merge anonymous consent.\n */\nexport const ANONYMOUS_ID_COOKIE = 'cookie-consent-anon-id';\n\n/**\n * Parse a single cookie value from a raw `Cookie` header string.\n */\nfunction parseCookieValue(\n cookieHeader: string | null | undefined,\n name: string,\n): string | undefined {\n if (!cookieHeader) return undefined;\n for (const pair of cookieHeader.split('; ')) {\n const [key, ...valueParts] = pair.split('=');\n if (key === name && valueParts.length > 0) {\n const raw = valueParts.join('=');\n try {\n return decodeURIComponent(raw);\n } catch {\n return raw;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Preset validation schema for cookie consent.\n * Validates the standard consent categories: necessary, analytics, marketing, functional.\n * Use this with `consent.validationSchema` in the plugin options.\n *\n * @example\n * ```ts\n * cookieConsentPlugin({\n * consentVersion: 'v1',\n * consent: { validationSchema: defaultConsentSchema },\n * })\n * ```\n */\nexport const defaultConsentSchema = z.object({\n necessary: z.boolean(),\n analytics: z.boolean(),\n marketing: z.boolean(),\n functional: z.boolean(),\n}) satisfies ConsentSchemaModel;\n\nexport const cookieConsentPlugin = <O extends CookieConsentOptions>(options: O = {} as O) => {\n return {\n id: 'cookie-consent',\n schema: getSchema(options),\n endpoints: {\n setConsent: setConsent(options),\n getConsent: getConsent(options),\n mergeConsent: mergeConsent(options),\n },\n hooks: {\n after: [\n {\n // After sign-in or sign-up (any method), merge anonymous consent to user.\n // Uses prefix matching to cover all auth methods: email, social,\n // biometrics, passkey, phone, etc.\n matcher: (context) => {\n return (\n !!context.path &&\n (context.path.startsWith('/sign-in/') || context.path.startsWith('/sign-up/'))\n );\n },\n handler: createAuthMiddleware(async (ctx) => {\n const userId = ctx.context.newSession?.user?.id;\n if (!userId) return;\n\n // Read anonymousId from the cookie set by the client\n const anonymousId = parseCookieValue(ctx.headers?.get('cookie'), ANONYMOUS_ID_COOKIE);\n if (!anonymousId) return;\n\n await mergeAnonymousConsentToUser(ctx.context.adapter, userId, anonymousId);\n }),\n },\n ],\n },\n options: options as NoInfer<O>,\n rateLimit: [\n {\n pathMatcher: (path) => ['/cookie-consent/set', '/cookie-consent/merge'].includes(path),\n window: options.rateLimit?.window ?? 10,\n max: options.rateLimit?.max ?? 10,\n },\n ],\n $ERROR_CODES: COOKIE_CONSENT_ERROR_CODES,\n } satisfies BetterAuthPlugin;\n};\n\n// ─── Helper utilities ──────────────────────────────────────────────────\n\n/**\n * Extract the parsed consent object from the endpoint context.\n * Returns `null` if no consent is available.\n */\nexport async function getConsentFromCtx(ctx: {\n context: {\n adapter: DBAdapter<BetterAuthOptions>;\n session?: { user?: { id?: string } } | null;\n };\n query?: { anonymousId?: string };\n}): Promise<CookieConsentRecord | null> {\n const userId = ctx.context.session?.user?.id ?? null;\n const anonymousId = ctx.query?.anonymousId;\n\n if (userId) {\n const byUser = await ctx.context.adapter.findOne<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'userId', value: userId }],\n });\n if (byUser) return byUser;\n }\n\n if (anonymousId) {\n return ctx.context.adapter.findOne<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'anonymousId', value: anonymousId }],\n });\n }\n\n return null;\n}\n\n/**\n * Check if a specific consent category is granted.\n */\nexport async function hasConsent(\n ctx: Parameters<typeof getConsentFromCtx>[0],\n category: string,\n): Promise<boolean> {\n const record = await getConsentFromCtx(ctx);\n if (!record) return false;\n const parsed = JSON.parse(record.consent) as Consent;\n return parsed[category] === true;\n}\n\nexport type * from './type';\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "better-auth-cookie-consent",
3
+ "version": "0.1.0",
4
+ "description": "Better Auth Cookie Consent plugin for GDPR-compliant consent management",
5
+ "homepage": "https://github.com/marcjulian/better-auth-plugins#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/marcjulian/better-auth-plugins/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "Gary Großgarten <info@gary.dev>",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/marcjulian/better-auth-plugins.git"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/index.mjs",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.mts",
22
+ "exports": {
23
+ ".": "./dist/index.mjs",
24
+ "./client": "./dist/client.mjs",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "dependencies": {
28
+ "nanostores": "^1.1.1",
29
+ "zod": "^4.3.6"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^25.3.5",
33
+ "bumpp": "^10.4.1",
34
+ "tsdown": "^0.21.1",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.0.18"
37
+ },
38
+ "peerDependencies": {
39
+ "better-auth": "^1.5.0"
40
+ },
41
+ "scripts": {
42
+ "build": "tsdown",
43
+ "dev": "tsdown --watch",
44
+ "test": "vitest",
45
+ "typecheck": "tsc --noEmit"
46
+ }
47
+ }