better-auth-cookie-consent 0.1.1 → 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 +27 -28
- package/dist/client.d.mts +13 -13
- package/dist/client.mjs +14 -5
- package/dist/client.mjs.map +1 -1
- package/dist/{index-B8ovmV25.d.mts → index-B9HQT45G.d.mts} +37 -32
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +55 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
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<
|
|
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 `
|
|
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 {
|
|
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<
|
|
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
|
|
254
|
-
import { defaultConsentSchema } from 'better-auth-cookie-consent';
|
|
248
|
+
import { type DefaultConsentModel } from 'better-auth-cookie-consent/client';
|
|
255
249
|
|
|
256
|
-
type
|
|
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
|
-
##
|
|
292
|
+
## Schema
|
|
293
|
+
|
|
294
|
+
### CookieConsent
|
|
300
295
|
|
|
301
|
-
|
|
296
|
+
Table name: `cookieConsent`
|
|
302
297
|
|
|
303
|
-
|
|
|
304
|
-
| -------------- | ------- |
|
|
305
|
-
| id | string |
|
|
306
|
-
| userId | string? |
|
|
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
|
-
|
|
307
|
+
#### Prisma
|
|
313
308
|
|
|
314
309
|
```prisma
|
|
315
310
|
model CookieConsent {
|
|
316
|
-
id String @id
|
|
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
|
|
322
|
-
|
|
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-
|
|
2
|
-
import
|
|
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,
|
|
20
|
+
* @typeParam TConsent - The consent shape, defaults to `DefaultConsentModel`.
|
|
19
21
|
*
|
|
20
22
|
* @example
|
|
21
23
|
* ```ts
|
|
22
|
-
* import {
|
|
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<
|
|
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:
|
|
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:
|
|
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.
|
|
@@ -103,5 +103,5 @@ declare const cookieConsentClient: <TConsent extends Consent = Consent>() => {
|
|
|
103
103
|
};
|
|
104
104
|
};
|
|
105
105
|
//#endregion
|
|
106
|
-
export { ConsentState, cookieConsentClient };
|
|
106
|
+
export { ConsentState, DefaultConsentModel, cookieConsentClient };
|
|
107
107
|
//# sourceMappingURL=client.d.mts.map
|
package/dist/client.mjs
CHANGED
|
@@ -3,16 +3,14 @@ import { atom } from "nanostores";
|
|
|
3
3
|
/**
|
|
4
4
|
* Client plugin for cookie consent management.
|
|
5
5
|
*
|
|
6
|
-
* @typeParam TConsent - The consent shape,
|
|
6
|
+
* @typeParam TConsent - The consent shape, defaults to `DefaultConsentModel`.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```ts
|
|
10
|
-
* import {
|
|
11
|
-
* import { cookieConsentClient } from 'better-auth-cookie-consent/client';
|
|
12
|
-
* import type { z } from 'zod';
|
|
10
|
+
* import { cookieConsentClient, type DefaultConsentModel } from 'better-auth-cookie-consent/client';
|
|
13
11
|
*
|
|
14
12
|
* const authClient = createAuthClient({
|
|
15
|
-
* plugins: [cookieConsentClient<
|
|
13
|
+
* plugins: [cookieConsentClient<DefaultConsentModel>()],
|
|
16
14
|
* });
|
|
17
15
|
* ```
|
|
18
16
|
*/
|
|
@@ -44,6 +42,11 @@ const cookieConsentClient = () => {
|
|
|
44
42
|
return res;
|
|
45
43
|
}
|
|
46
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
|
+
*/
|
|
47
50
|
setConsent: async (data) => {
|
|
48
51
|
const res = await $fetch("/cookie-consent/set", {
|
|
49
52
|
method: "POST",
|
|
@@ -56,9 +59,15 @@ const cookieConsentClient = () => {
|
|
|
56
59
|
});
|
|
57
60
|
return res;
|
|
58
61
|
},
|
|
62
|
+
/**
|
|
63
|
+
* Retrieve consent from the server.
|
|
64
|
+
*/
|
|
59
65
|
getConsent: async (anonymousId) => {
|
|
60
66
|
return syncFromServer(anonymousId);
|
|
61
67
|
},
|
|
68
|
+
/**
|
|
69
|
+
* Merge anonymous consent into the authenticated user's record.
|
|
70
|
+
*/
|
|
62
71
|
mergeConsent: async (anonymousId) => {
|
|
63
72
|
const res = await $fetch("/cookie-consent/merge", {
|
|
64
73
|
method: "POST",
|
package/dist/client.mjs.map
CHANGED
|
@@ -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';\
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
230
|
-
handler: (inputContext:
|
|
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:
|
|
241
|
-
CONSENT_NOT_FOUND:
|
|
242
|
-
INVALID_CONSENT:
|
|
243
|
-
VERSION_MISMATCH:
|
|
244
|
-
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-
|
|
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-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx } from "better-auth/api";
|
|
2
|
-
import * as z from "zod";
|
|
2
|
+
import * as z$1 from "zod";
|
|
3
3
|
import { BASE_ERROR_CODES, defineErrorCodes } from "better-auth";
|
|
4
4
|
import { mergeSchema } from "better-auth/db";
|
|
5
5
|
//#region src/error-codes.ts
|
|
@@ -12,13 +12,13 @@ const COOKIE_CONSENT_ERROR_CODES = defineErrorCodes({
|
|
|
12
12
|
});
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/routes.ts
|
|
15
|
-
const setConsentSchema = z.object({
|
|
16
|
-
anonymousId: z.string().meta({ description: "Anonymous identifier for unauthenticated users" }),
|
|
17
|
-
consent: z.record(z.string(), z.boolean()).meta({ description: "Consent preferences as category-boolean pairs" }),
|
|
18
|
-
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" })
|
|
19
19
|
});
|
|
20
|
-
const getConsentSchema = z.object({ anonymousId: z.string().optional().meta({ description: "Anonymous identifier fallback when no session exists" }) });
|
|
21
|
-
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" }) });
|
|
22
22
|
const setConsent = (options) => createAuthEndpoint("/cookie-consent/set", {
|
|
23
23
|
method: "POST",
|
|
24
24
|
body: setConsentSchema
|
|
@@ -112,51 +112,53 @@ const mergeConsent = (_options) => createAuthEndpoint("/cookie-consent/merge", {
|
|
|
112
112
|
* @returns `true` if consent was merged, `false` if no anonymous record was found.
|
|
113
113
|
*/
|
|
114
114
|
async function mergeAnonymousConsentToUser(adapter, userId, anonymousId) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
where: [{
|
|
118
|
-
field: "anonymousId",
|
|
119
|
-
value: anonymousId
|
|
120
|
-
}]
|
|
121
|
-
});
|
|
122
|
-
if (!anonymousRecord || anonymousRecord.userId) return false;
|
|
123
|
-
const userRecord = await adapter.findOne({
|
|
124
|
-
model: "cookieConsent",
|
|
125
|
-
where: [{
|
|
126
|
-
field: "userId",
|
|
127
|
-
value: userId
|
|
128
|
-
}]
|
|
129
|
-
});
|
|
130
|
-
if (userRecord) {
|
|
131
|
-
await adapter.update({
|
|
115
|
+
return await adapter.transaction(async (tx) => {
|
|
116
|
+
const anonymousRecord = await tx.findOne({
|
|
132
117
|
model: "cookieConsent",
|
|
133
118
|
where: [{
|
|
134
|
-
field: "
|
|
135
|
-
value:
|
|
136
|
-
}]
|
|
137
|
-
update: {
|
|
138
|
-
consent: anonymousRecord.consent,
|
|
139
|
-
consentVersion: anonymousRecord.consentVersion,
|
|
140
|
-
anonymousId,
|
|
141
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
142
|
-
}
|
|
119
|
+
field: "anonymousId",
|
|
120
|
+
value: anonymousId
|
|
121
|
+
}]
|
|
143
122
|
});
|
|
144
|
-
|
|
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
|
+
}]
|
|
130
|
+
});
|
|
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({
|
|
145
153
|
model: "cookieConsent",
|
|
146
154
|
where: [{
|
|
147
155
|
field: "id",
|
|
148
156
|
value: anonymousRecord.id
|
|
149
|
-
}]
|
|
157
|
+
}],
|
|
158
|
+
update: { userId }
|
|
150
159
|
});
|
|
151
|
-
|
|
152
|
-
model: "cookieConsent",
|
|
153
|
-
where: [{
|
|
154
|
-
field: "id",
|
|
155
|
-
value: anonymousRecord.id
|
|
156
|
-
}],
|
|
157
|
-
update: { userId }
|
|
160
|
+
return true;
|
|
158
161
|
});
|
|
159
|
-
return true;
|
|
160
162
|
}
|
|
161
163
|
/**
|
|
162
164
|
* Find a consent record by userId (preferred) or anonymousId fallback.
|
|
@@ -201,6 +203,7 @@ const cookieConsent = { cookieConsent: { fields: {
|
|
|
201
203
|
userId: {
|
|
202
204
|
type: "string",
|
|
203
205
|
required: false,
|
|
206
|
+
unique: true,
|
|
204
207
|
references: {
|
|
205
208
|
model: "user",
|
|
206
209
|
field: "id"
|
|
@@ -208,7 +211,8 @@ const cookieConsent = { cookieConsent: { fields: {
|
|
|
208
211
|
},
|
|
209
212
|
anonymousId: {
|
|
210
213
|
type: "string",
|
|
211
|
-
required: true
|
|
214
|
+
required: true,
|
|
215
|
+
unique: true
|
|
212
216
|
},
|
|
213
217
|
consent: {
|
|
214
218
|
type: "string",
|
|
@@ -221,7 +225,8 @@ const cookieConsent = { cookieConsent: { fields: {
|
|
|
221
225
|
timestamp: {
|
|
222
226
|
type: "date",
|
|
223
227
|
defaultValue: () => /* @__PURE__ */ new Date(),
|
|
224
|
-
required: true
|
|
228
|
+
required: true,
|
|
229
|
+
input: false
|
|
225
230
|
}
|
|
226
231
|
} } };
|
|
227
232
|
const getSchema = (options) => {
|
|
@@ -265,11 +270,11 @@ function parseCookieValue(cookieHeader, name) {
|
|
|
265
270
|
* })
|
|
266
271
|
* ```
|
|
267
272
|
*/
|
|
268
|
-
const defaultConsentSchema = z.object({
|
|
269
|
-
necessary: z.boolean(),
|
|
270
|
-
analytics: z.boolean(),
|
|
271
|
-
marketing: z.boolean(),
|
|
272
|
-
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()
|
|
273
278
|
});
|
|
274
279
|
const cookieConsentPlugin = (options = {}) => {
|
|
275
280
|
return {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"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 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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth-cookie-consent",
|
|
3
|
-
"version": "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": {
|
|
@@ -26,17 +26,17 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"nanostores": "^1.1.1",
|
|
29
|
-
"zod": "^4.3
|
|
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.
|
|
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": "
|
|
39
|
+
"better-auth": "1.6.11"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsdown",
|