better-auth-cookie-consent 0.1.0 → 0.2.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 CHANGED
@@ -7,7 +7,7 @@ GDPR-compliant cookie consent management plugin for [Better Auth](https://better
7
7
  - Automatic merge of anonymous consent on sign-in / sign-up
8
8
  - Consent versioning with automatic invalidation
9
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
10
+ - Generic client plugin — `cookieConsentClient<DefaultConsentModel>()` types your consent model end-to-end
11
11
 
12
12
  ## How It Works
13
13
 
@@ -113,7 +113,7 @@ export const auth = betterAuth({
113
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
114
 
115
115
  ```ts
116
- import { defaultConsentSchema } from 'better-auth-cookie-consent';
116
+ import { defaultConsentSchema } from 'better-auth-cookie-consent/client';
117
117
 
118
118
  // Equivalent to:
119
119
  // z.object({
@@ -126,19 +126,14 @@ import { defaultConsentSchema } from 'better-auth-cookie-consent';
126
126
 
127
127
  ## Client Setup
128
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:
129
+ The client plugin accepts a generic type parameter for the consent shape. Use `DefaultConsentModel` to get full end-to-end typing:
130
130
 
131
131
  ```ts
132
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 }
133
+ import { cookieConsentClient, type DefaultConsentModel } from 'better-auth-cookie-consent/client';
139
134
 
140
135
  export const authClient = createAuthClient({
141
- plugins: [cookieConsentClient<ConsentModel>()],
136
+ plugins: [cookieConsentClient<DefaultConsentModel>()],
142
137
  });
143
138
  ```
144
139
 
@@ -250,11 +245,9 @@ The consent object is validated against the server's `validationSchema` on every
250
245
  Use `keyof` on the inferred consent model to type your banner categories:
251
246
 
252
247
  ```ts
253
- import type { z } from 'zod';
254
- import { defaultConsentSchema } from 'better-auth-cookie-consent';
248
+ import { type DefaultConsentModel } from 'better-auth-cookie-consent/client';
255
249
 
256
- type ConsentModel = z.infer<typeof defaultConsentSchema>;
257
- type CategoryId = keyof ConsentModel;
250
+ type CategoryId = keyof DefaultConsentModel;
258
251
 
259
252
  const CATEGORIES: { id: CategoryId; label: string }[] = [
260
253
  { id: 'necessary', label: 'Necessary' },
@@ -296,30 +289,36 @@ The plugin registers a server-side `after` hook that runs on every `sign-in/*` a
296
289
 
297
290
  When the `consentVersion` option changes, the `getConsent` endpoint returns `versionMatch: false` so the client knows to prompt for re-consent.
298
291
 
299
- ## Database Schema
292
+ ## Schema
293
+
294
+ ### CookieConsent
300
295
 
301
- The plugin creates a `cookieConsent` table:
296
+ Table name: `cookieConsent`
302
297
 
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 |
298
+ | Field | Type | Key | Description |
299
+ | -------------- | ------- | ------ | ----------------------------------------- |
300
+ | id | string | pk | Unique identifier for each consent record |
301
+ | userId | string? | unique | ID of an associated better-auth user |
302
+ | anonymousId | string | unique | Anonymous client identifier |
303
+ | consent | string | | JSON-encoded consent preferences |
304
+ | consentVersion | string | | Consent policy version |
305
+ | timestamp | date | | When consent was recorded |
311
306
 
312
- ### Prisma Schema
307
+ #### Prisma
313
308
 
314
309
  ```prisma
315
310
  model CookieConsent {
316
- id String @id @default(cuid())
311
+ id String @id
317
312
  userId String?
313
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
318
314
  anonymousId String
319
315
  consent String
320
316
  consentVersion String
321
- timestamp DateTime @default(now())
322
- user User? @relation(fields: [userId], references: [id])
317
+ timestamp DateTime
318
+
319
+ @@unique([userId])
320
+ @@unique([anonymousId])
321
+ @@map("cookieConsent")
323
322
  }
324
323
  ```
325
324
 
package/dist/client.d.mts CHANGED
@@ -1,7 +1,5 @@
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";
1
+ import { n as cookieConsentPlugin, o as Consent, r as defaultConsentSchema } from "./index-B9HQT45G.mjs";
2
+ import z from "zod";
5
3
 
6
4
  //#region src/client.d.ts
7
5
  /**
@@ -12,29 +10,31 @@ interface ConsentState<TConsent extends Consent = Consent> {
12
10
  consentVersion: string | null;
13
11
  versionMatch: boolean;
14
12
  }
13
+ /**
14
+ * Inferred type from the default consent schema.
15
+ */
16
+ type DefaultConsentModel = z.infer<typeof defaultConsentSchema>;
15
17
  /**
16
18
  * Client plugin for cookie consent management.
17
19
  *
18
- * @typeParam TConsent - The consent shape, typically `z.infer<typeof yourConsentSchema>`.
20
+ * @typeParam TConsent - The consent shape, defaults to `DefaultConsentModel`.
19
21
  *
20
22
  * @example
21
23
  * ```ts
22
- * import { defaultConsentSchema } from 'better-auth-cookie-consent';
23
- * import { cookieConsentClient } from 'better-auth-cookie-consent/client';
24
- * import type { z } from 'zod';
24
+ * import { cookieConsentClient, type DefaultConsentModel } from 'better-auth-cookie-consent/client';
25
25
  *
26
26
  * const authClient = createAuthClient({
27
- * plugins: [cookieConsentClient<z.infer<typeof defaultConsentSchema>>()],
27
+ * plugins: [cookieConsentClient<DefaultConsentModel>()],
28
28
  * });
29
29
  * ```
30
30
  */
31
31
  declare const cookieConsentClient: <TConsent extends Consent = Consent>() => {
32
32
  id: "cookie-consent";
33
33
  $InferServerPlugin: ReturnType<typeof cookieConsentPlugin>;
34
- getAtoms($fetch: better_auth_client0.BetterFetch): {
35
- $consent: nanostores.PreinitializedWritableAtom<ConsentState<TConsent>> & object;
34
+ getAtoms($fetch: import("better-auth/client").BetterFetch): {
35
+ $consent: import("nanostores").PreinitializedWritableAtom<ConsentState<TConsent>> & object;
36
36
  };
37
- getActions($fetch: better_auth_client0.BetterFetch, $store: better_auth0.ClientStore): {
37
+ getActions($fetch: import("better-auth/client").BetterFetch, $store: import("better-auth").ClientStore): {
38
38
  cookieConsent: {
39
39
  /**
40
40
  * Set consent preferences on the server.
@@ -101,14 +101,7 @@ declare const cookieConsentClient: <TConsent extends Consent = Consent>() => {
101
101
  }>;
102
102
  };
103
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
104
  };
112
105
  //#endregion
113
- export { ConsentState, cookieConsentClient };
106
+ export { ConsentState, DefaultConsentModel, cookieConsentClient };
114
107
  //# sourceMappingURL=client.d.mts.map
package/dist/client.mjs CHANGED
@@ -1,19 +1,16 @@
1
- import { t as COOKIE_CONSENT_ERROR_CODES } from "./error-codes-D3VdAX9J.mjs";
2
1
  import { atom } from "nanostores";
3
2
  //#region src/client.ts
4
3
  /**
5
4
  * Client plugin for cookie consent management.
6
5
  *
7
- * @typeParam TConsent - The consent shape, typically `z.infer<typeof yourConsentSchema>`.
6
+ * @typeParam TConsent - The consent shape, defaults to `DefaultConsentModel`.
8
7
  *
9
8
  * @example
10
9
  * ```ts
11
- * import { defaultConsentSchema } from 'better-auth-cookie-consent';
12
- * import { cookieConsentClient } from 'better-auth-cookie-consent/client';
13
- * import type { z } from 'zod';
10
+ * import { cookieConsentClient, type DefaultConsentModel } from 'better-auth-cookie-consent/client';
14
11
  *
15
12
  * const authClient = createAuthClient({
16
- * plugins: [cookieConsentClient<z.infer<typeof defaultConsentSchema>>()],
13
+ * plugins: [cookieConsentClient<DefaultConsentModel>()],
17
14
  * });
18
15
  * ```
19
16
  */
@@ -45,6 +42,11 @@ const cookieConsentClient = () => {
45
42
  return res;
46
43
  }
47
44
  return { cookieConsent: {
45
+ /**
46
+ * Set consent preferences on the server.
47
+ * Also used for accept-all / reject-all by passing the
48
+ * appropriate consent object (all `true` or all `false`).
49
+ */
48
50
  setConsent: async (data) => {
49
51
  const res = await $fetch("/cookie-consent/set", {
50
52
  method: "POST",
@@ -57,9 +59,15 @@ const cookieConsentClient = () => {
57
59
  });
58
60
  return res;
59
61
  },
62
+ /**
63
+ * Retrieve consent from the server.
64
+ */
60
65
  getConsent: async (anonymousId) => {
61
66
  return syncFromServer(anonymousId);
62
67
  },
68
+ /**
69
+ * Merge anonymous consent into the authenticated user's record.
70
+ */
63
71
  mergeConsent: async (anonymousId) => {
64
72
  const res = await $fetch("/cookie-consent/merge", {
65
73
  method: "POST",
@@ -69,8 +77,7 @@ const cookieConsentClient = () => {
69
77
  return res;
70
78
  }
71
79
  } };
72
- },
73
- $ERROR_CODES: COOKIE_CONSENT_ERROR_CODES
80
+ }
74
81
  };
75
82
  };
76
83
  //#endregion
@@ -1 +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"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { BetterAuthClientPlugin } from 'better-auth/client';\nimport { atom } from 'nanostores';\nimport type z from 'zod';\n\nimport type { Consent, cookieConsentPlugin, defaultConsentSchema } 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 * Inferred type from the default consent schema.\n */\nexport type DefaultConsentModel = z.infer<typeof defaultConsentSchema>;\n\n/**\n * Client plugin for cookie consent management.\n *\n * @typeParam TConsent - The consent shape, defaults to `DefaultConsentModel`.\n *\n * @example\n * ```ts\n * import { cookieConsentClient, type DefaultConsentModel } from 'better-auth-cookie-consent/client';\n *\n * const authClient = createAuthClient({\n * plugins: [cookieConsentClient<DefaultConsentModel>()],\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":";;;;;;;;;;;;;;;;AAkCA,MAAa,4BAAgE;CAC3E,OAAO;EACL,IAAI;EACJ,oBAAoB,CAAC;EAErB,SAAS,QAAQ;GAMf,OAAO,EAAE,UALQ,KAA6B;IAC5C,SAAS;IACT,gBAAgB;IAChB,cAAc;GAChB,CACgB,EAAE;EACpB;EAEA,WAAW,QAAQ,QAAQ;GACzB,MAAM,cAAc,OAAO,MAAM;GAEjC,eAAe,eAAe,aAAsB;IAClD,IAAI,OAAO;IACX,IAAI,aAAa;KACf,MAAM,SAAS,IAAI,gBAAgB,EAAE,YAAY,CAAC;KAClD,OAAO,GAAG,KAAK,GAAG,OAAO,SAAS;IACpC;IACA,MAAM,MAAM,MAAM,OAUf,GAAG,QAAQ,EAAE,QAAQ,MAAM,CAAC;IAC/B,IAAI,IAAI,MACN,YAAY,IAAI;KACd,SAAS,IAAI,KAAK,SAAS,WAAW;KACtC,gBAAgB,IAAI,KAAK,SAAS,kBAAkB;KACpD,cAAc,IAAI,KAAK;IACzB,CAAC;IAEH,OAAO;GACT;GAEA,OAAO,EACL,eAAe;;;;;;IAMb,YAAY,OAAO,SAIb;KACJ,MAAM,MAAM,MAAM,OAA4B,uBAAuB;MACnE,QAAQ;MACR,MAAM;KACR,CAAC;KACD,IAAI,IAAI,MAAM,QACZ,YAAY,IAAI;MACd,SAAS,KAAK;MACd,gBAAgB,KAAK;MACrB,cAAc;KAChB,CAAC;KAEH,OAAO;IACT;;;;IAKA,YAAY,OAAO,gBAAyB;KAC1C,OAAO,eAAe,WAAW;IACnC;;;;IAKA,cAAc,OAAO,gBAAwB;KAC3C,MAAM,MAAM,MAAM,OAChB,yBACA;MACE,QAAQ;MACR,MAAM,EAAE,YAAY;KACtB,CACF;KACA,IAAI,IAAI,MAAM,QACZ,MAAM,eAAe,WAAW;KAElC,OAAO;IACT;GACF,EACF;EACF;CAGF;AACF"}
@@ -1,5 +1,4 @@
1
- import * as z from "zod";
2
- import * as better_auth0 from "better-auth";
1
+ import * as z$1 from "zod";
3
2
  import { BetterAuthOptions, DBAdapter, InferOptionSchema, StandardSchemaV1 } from "better-auth";
4
3
 
5
4
  //#region src/schema.d.ts
@@ -9,6 +8,7 @@ declare const cookieConsent: {
9
8
  userId: {
10
9
  type: "string";
11
10
  required: false;
11
+ unique: true;
12
12
  references: {
13
13
  model: string;
14
14
  field: string;
@@ -17,6 +17,7 @@ declare const cookieConsent: {
17
17
  anonymousId: {
18
18
  type: "string";
19
19
  required: true;
20
+ unique: true;
20
21
  };
21
22
  consent: {
22
23
  type: "string";
@@ -30,6 +31,7 @@ declare const cookieConsent: {
30
31
  type: "date";
31
32
  defaultValue: () => Date;
32
33
  required: true;
34
+ input: false;
33
35
  };
34
36
  };
35
37
  };
@@ -40,8 +42,8 @@ declare const cookieConsent: {
40
42
  * Consent categories mapped to boolean values.
41
43
  * Each key represents a cookie category (e.g., "analytics", "marketing").
42
44
  */
43
- type ConsentSchemaModel = z.ZodObject<Record<string, z.ZodBoolean>>;
44
- type Consent = z.infer<ConsentSchemaModel>;
45
+ type ConsentSchemaModel = z$1.ZodObject<Record<string, z$1.ZodBoolean>>;
46
+ type Consent = z$1.infer<ConsentSchemaModel>;
45
47
  /**
46
48
  * Options for the cookie consent plugin.
47
49
  */
@@ -145,12 +147,12 @@ declare const ANONYMOUS_ID_COOKIE = "cookie-consent-anon-id";
145
147
  * })
146
148
  * ```
147
149
  */
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>;
150
+ declare const defaultConsentSchema: z$1.ZodObject<{
151
+ necessary: z$1.ZodBoolean;
152
+ analytics: z$1.ZodBoolean;
153
+ marketing: z$1.ZodBoolean;
154
+ functional: z$1.ZodBoolean;
155
+ }, z$1.core.$strip>;
154
156
  declare const cookieConsentPlugin: <O extends CookieConsentOptions>(options?: O) => {
155
157
  id: "cookie-consent";
156
158
  schema: {
@@ -159,6 +161,7 @@ declare const cookieConsentPlugin: <O extends CookieConsentOptions>(options?: O)
159
161
  userId: {
160
162
  type: "string";
161
163
  required: false;
164
+ unique: true;
162
165
  references: {
163
166
  model: string;
164
167
  field: string;
@@ -167,6 +170,7 @@ declare const cookieConsentPlugin: <O extends CookieConsentOptions>(options?: O)
167
170
  anonymousId: {
168
171
  type: "string";
169
172
  required: true;
173
+ unique: true;
170
174
  };
171
175
  consent: {
172
176
  type: "string";
@@ -180,26 +184,27 @@ declare const cookieConsentPlugin: <O extends CookieConsentOptions>(options?: O)
180
184
  type: "date";
181
185
  defaultValue: () => Date;
182
186
  required: true;
187
+ input: false;
183
188
  };
184
189
  };
185
190
  };
186
191
  };
187
192
  endpoints: {
188
- setConsent: better_auth0.StrictEndpoint<"/cookie-consent/set", {
193
+ setConsent: import("better-auth").StrictEndpoint<"/cookie-consent/set", {
189
194
  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
+ body: z$1.ZodObject<{
196
+ anonymousId: z$1.ZodString;
197
+ consent: z$1.ZodRecord<z$1.ZodString, z$1.ZodBoolean>;
198
+ consentVersion: z$1.ZodString;
199
+ }, z$1.core.$strip>;
195
200
  }, {
196
201
  status: boolean;
197
202
  }>;
198
- getConsent: better_auth0.StrictEndpoint<"/cookie-consent/get", {
203
+ getConsent: import("better-auth").StrictEndpoint<"/cookie-consent/get", {
199
204
  method: "GET";
200
- query: z.ZodObject<{
201
- anonymousId: z.ZodOptional<z.ZodString>;
202
- }, z.core.$strip>;
205
+ query: z$1.ZodObject<{
206
+ anonymousId: z$1.ZodOptional<z$1.ZodString>;
207
+ }, z$1.core.$strip>;
203
208
  }, {
204
209
  consent: null;
205
210
  versionMatch: boolean;
@@ -214,11 +219,11 @@ declare const cookieConsentPlugin: <O extends CookieConsentOptions>(options?: O)
214
219
  };
215
220
  versionMatch: boolean;
216
221
  }>;
217
- mergeConsent: better_auth0.StrictEndpoint<"/cookie-consent/merge", {
222
+ mergeConsent: import("better-auth").StrictEndpoint<"/cookie-consent/merge", {
218
223
  method: "POST";
219
- body: z.ZodObject<{
220
- anonymousId: z.ZodString;
221
- }, z.core.$strip>;
224
+ body: z$1.ZodObject<{
225
+ anonymousId: z$1.ZodString;
226
+ }, z$1.core.$strip>;
222
227
  }, {
223
228
  status: boolean;
224
229
  merged: boolean;
@@ -226,8 +231,8 @@ declare const cookieConsentPlugin: <O extends CookieConsentOptions>(options?: O)
226
231
  };
227
232
  hooks: {
228
233
  after: {
229
- matcher: (context: better_auth0.HookEndpointContext) => boolean;
230
- handler: (inputContext: better_auth0.MiddlewareInputContext<better_auth0.MiddlewareOptions>) => Promise<void>;
234
+ matcher: (context: import("better-auth").HookEndpointContext) => boolean;
235
+ handler: (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>;
231
236
  }[];
232
237
  };
233
238
  options: NoInfer<O>;
@@ -237,11 +242,11 @@ declare const cookieConsentPlugin: <O extends CookieConsentOptions>(options?: O)
237
242
  max: number;
238
243
  }[];
239
244
  $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
+ MISSING_ANONYMOUS_ID: import("better-auth").RawError<"MISSING_ANONYMOUS_ID">;
246
+ CONSENT_NOT_FOUND: import("better-auth").RawError<"CONSENT_NOT_FOUND">;
247
+ INVALID_CONSENT: import("better-auth").RawError<"INVALID_CONSENT">;
248
+ VERSION_MISMATCH: import("better-auth").RawError<"VERSION_MISMATCH">;
249
+ AUTHENTICATION_REQUIRED: import("better-auth").RawError<"AUTHENTICATION_REQUIRED">;
245
250
  };
246
251
  };
247
252
  /**
@@ -267,4 +272,4 @@ declare function getConsentFromCtx(ctx: {
267
272
  declare function hasConsent(ctx: Parameters<typeof getConsentFromCtx>[0], category: string): Promise<boolean>;
268
273
  //#endregion
269
274
  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
275
+ //# sourceMappingURL=index-B9HQT45G.d.mts.map
package/dist/index.d.mts CHANGED
@@ -1,2 +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 };
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-B9HQT45G.mjs";
2
+ export { ANONYMOUS_ID_COOKIE, type Consent, type ConsentSchemaModel, type CookieConsentOptions, type CookieConsentPayload, type CookieConsentRecord, cookieConsentPlugin, defaultConsentSchema, getConsentFromCtx, hasConsent };
package/dist/index.mjs CHANGED
@@ -1,16 +1,24 @@
1
- import { t as COOKIE_CONSENT_ERROR_CODES } from "./error-codes-D3VdAX9J.mjs";
2
1
  import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx } from "better-auth/api";
3
- import * as z from "zod";
4
- import { BASE_ERROR_CODES } from "better-auth";
2
+ import * as z$1 from "zod";
3
+ import { BASE_ERROR_CODES, defineErrorCodes } from "better-auth";
5
4
  import { mergeSchema } from "better-auth/db";
5
+ //#region src/error-codes.ts
6
+ const COOKIE_CONSENT_ERROR_CODES = defineErrorCodes({
7
+ MISSING_ANONYMOUS_ID: "Anonymous ID is required",
8
+ CONSENT_NOT_FOUND: "Cookie consent record not found",
9
+ INVALID_CONSENT: "Consent must be a non-empty object with boolean values",
10
+ VERSION_MISMATCH: "Consent version is outdated and must be renewed",
11
+ AUTHENTICATION_REQUIRED: "Authentication is required for this operation"
12
+ });
13
+ //#endregion
6
14
  //#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" })
15
+ const setConsentSchema = z$1.object({
16
+ anonymousId: z$1.string().meta({ description: "Anonymous identifier for unauthenticated users" }),
17
+ consent: z$1.record(z$1.string(), z$1.boolean()).meta({ description: "Consent preferences as category-boolean pairs" }),
18
+ consentVersion: z$1.string().meta({ description: "Version of the consent policy" })
11
19
  });
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" }) });
20
+ const getConsentSchema = z$1.object({ anonymousId: z$1.string().optional().meta({ description: "Anonymous identifier fallback when no session exists" }) });
21
+ const mergeConsentSchema = z$1.object({ anonymousId: z$1.string().meta({ description: "Anonymous identifier to merge consent from" }) });
14
22
  const setConsent = (options) => createAuthEndpoint("/cookie-consent/set", {
15
23
  method: "POST",
16
24
  body: setConsentSchema
@@ -104,51 +112,53 @@ const mergeConsent = (_options) => createAuthEndpoint("/cookie-consent/merge", {
104
112
  * @returns `true` if consent was merged, `false` if no anonymous record was found.
105
113
  */
106
114
  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({
115
+ return await adapter.transaction(async (tx) => {
116
+ const anonymousRecord = await tx.findOne({
124
117
  model: "cookieConsent",
125
118
  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
- }
119
+ field: "anonymousId",
120
+ value: anonymousId
121
+ }]
122
+ });
123
+ if (!anonymousRecord || anonymousRecord.userId) return false;
124
+ const userRecord = await tx.findOne({
125
+ model: "cookieConsent",
126
+ where: [{
127
+ field: "userId",
128
+ value: userId
129
+ }]
135
130
  });
136
- await adapter.delete({
131
+ if (userRecord) {
132
+ await tx.delete({
133
+ model: "cookieConsent",
134
+ where: [{
135
+ field: "id",
136
+ value: anonymousRecord.id
137
+ }]
138
+ });
139
+ await tx.update({
140
+ model: "cookieConsent",
141
+ where: [{
142
+ field: "id",
143
+ value: userRecord.id
144
+ }],
145
+ update: {
146
+ consent: anonymousRecord.consent,
147
+ consentVersion: anonymousRecord.consentVersion,
148
+ anonymousId: anonymousRecord.anonymousId,
149
+ timestamp: /* @__PURE__ */ new Date()
150
+ }
151
+ });
152
+ } else await tx.update({
137
153
  model: "cookieConsent",
138
154
  where: [{
139
155
  field: "id",
140
156
  value: anonymousRecord.id
141
- }]
157
+ }],
158
+ update: { userId }
142
159
  });
143
- } else await adapter.update({
144
- model: "cookieConsent",
145
- where: [{
146
- field: "id",
147
- value: anonymousRecord.id
148
- }],
149
- update: { userId }
160
+ return true;
150
161
  });
151
- return true;
152
162
  }
153
163
  /**
154
164
  * Find a consent record by userId (preferred) or anonymousId fallback.
@@ -193,6 +203,7 @@ const cookieConsent = { cookieConsent: { fields: {
193
203
  userId: {
194
204
  type: "string",
195
205
  required: false,
206
+ unique: true,
196
207
  references: {
197
208
  model: "user",
198
209
  field: "id"
@@ -200,7 +211,8 @@ const cookieConsent = { cookieConsent: { fields: {
200
211
  },
201
212
  anonymousId: {
202
213
  type: "string",
203
- required: true
214
+ required: true,
215
+ unique: true
204
216
  },
205
217
  consent: {
206
218
  type: "string",
@@ -213,7 +225,8 @@ const cookieConsent = { cookieConsent: { fields: {
213
225
  timestamp: {
214
226
  type: "date",
215
227
  defaultValue: () => /* @__PURE__ */ new Date(),
216
- required: true
228
+ required: true,
229
+ input: false
217
230
  }
218
231
  } } };
219
232
  const getSchema = (options) => {
@@ -257,11 +270,11 @@ function parseCookieValue(cookieHeader, name) {
257
270
  * })
258
271
  * ```
259
272
  */
260
- const defaultConsentSchema = z.object({
261
- necessary: z.boolean(),
262
- analytics: z.boolean(),
263
- marketing: z.boolean(),
264
- functional: z.boolean()
273
+ const defaultConsentSchema = z$1.object({
274
+ necessary: z$1.boolean(),
275
+ analytics: z$1.boolean(),
276
+ marketing: z$1.boolean(),
277
+ functional: z$1.boolean()
265
278
  });
266
279
  const cookieConsentPlugin = (options = {}) => {
267
280
  return {
@@ -1 +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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"}
1
+ {"version":3,"file":"index.mjs","names":["z","z"],"sources":["../src/error-codes.ts","../src/routes.ts","../src/schema.ts","../src/index.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","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 return await adapter.transaction(async (tx) => {\n const anonymousRecord = await tx.findOne<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'anonymousId', value: anonymousId }],\n });\n\n // Skip if record doesn't exist or is already linked to a user (already merged)\n if (!anonymousRecord || anonymousRecord.userId) return false;\n\n const userRecord = await tx.findOne<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'userId', value: userId }],\n });\n\n if (userRecord) {\n // Delete the anonymous record first so the new anonymousId can\n // be carried over without a unique-constraint conflict.\n await tx.delete({\n model: 'cookieConsent',\n where: [{ field: 'id', value: anonymousRecord.id }],\n });\n await tx.update<CookieConsentRecord>({\n model: 'cookieConsent',\n where: [{ field: 'id', value: userRecord.id }],\n update: {\n consent: anonymousRecord.consent,\n consentVersion: anonymousRecord.consentVersion,\n anonymousId: anonymousRecord.anonymousId,\n timestamp: new Date(),\n },\n });\n } else {\n await tx.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/**\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 unique: true,\n references: {\n model: 'user',\n field: 'id',\n },\n },\n anonymousId: {\n type: 'string',\n required: true,\n unique: 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 input: false,\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 CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "better-auth-cookie-consent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Better Auth Cookie Consent plugin for GDPR-compliant consent management",
5
5
  "homepage": "https://github.com/marcjulian/better-auth-plugins#readme",
6
6
  "bugs": {
7
7
  "url": "https://github.com/marcjulian/better-auth-plugins/issues"
8
8
  },
9
9
  "license": "MIT",
10
- "author": "Gary Großgarten <info@gary.dev>",
10
+ "author": "Marc Stammerjohann <me@marcjulian.de>",
11
11
  "repository": {
12
12
  "type": "git",
13
13
  "url": "git+https://github.com/marcjulian/better-auth-plugins.git"
@@ -26,17 +26,17 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "nanostores": "^1.1.1",
29
- "zod": "^4.3.6"
29
+ "zod": "^4.4.3"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^25.3.5",
33
33
  "bumpp": "^10.4.1",
34
- "tsdown": "^0.21.1",
34
+ "tsdown": "^0.22.0",
35
35
  "typescript": "^5.9.3",
36
36
  "vitest": "^4.0.18"
37
37
  },
38
38
  "peerDependencies": {
39
- "better-auth": "^1.5.0"
39
+ "better-auth": "1.6.11"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsdown",
@@ -1,13 +0,0 @@
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
@@ -1 +0,0 @@
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"}