@headless-lms/adapter-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mdwt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,100 @@
1
+ //#region src/access.d.ts
2
+ declare const statement: {
3
+ readonly course: readonly ["create", "read", "update", "delete"];
4
+ readonly progress: readonly ["view"];
5
+ readonly organization: readonly ["update", "delete"];
6
+ readonly member: readonly ["create", "update", "delete"];
7
+ readonly invitation: readonly ["create", "cancel"];
8
+ readonly team: readonly ["create", "update", "delete"];
9
+ readonly ac: readonly ["create", "read", "update", "delete"];
10
+ };
11
+ declare const ac: {
12
+ newRole<const TRoleStatements extends import("better-auth/plugins/access").Statements>(statements: import("better-auth/plugins/access").RoleInput<{
13
+ readonly course: readonly ["create", "read", "update", "delete"];
14
+ readonly progress: readonly ["view"];
15
+ readonly organization: readonly ["update", "delete"];
16
+ readonly member: readonly ["create", "update", "delete"];
17
+ readonly invitation: readonly ["create", "cancel"];
18
+ readonly team: readonly ["create", "update", "delete"];
19
+ readonly ac: readonly ["create", "read", "update", "delete"];
20
+ }, TRoleStatements>): import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<TRoleStatements>, {
21
+ readonly course: readonly ["create", "read", "update", "delete"];
22
+ readonly progress: readonly ["view"];
23
+ readonly organization: readonly ["update", "delete"];
24
+ readonly member: readonly ["create", "update", "delete"];
25
+ readonly invitation: readonly ["create", "cancel"];
26
+ readonly team: readonly ["create", "update", "delete"];
27
+ readonly ac: readonly ["create", "read", "update", "delete"];
28
+ }>;
29
+ statements: {
30
+ readonly course: readonly ["create", "read", "update", "delete"];
31
+ readonly progress: readonly ["view"];
32
+ readonly organization: readonly ["update", "delete"];
33
+ readonly member: readonly ["create", "update", "delete"];
34
+ readonly invitation: readonly ["create", "cancel"];
35
+ readonly team: readonly ["create", "update", "delete"];
36
+ readonly ac: readonly ["create", "read", "update", "delete"];
37
+ };
38
+ };
39
+ declare const roles: {
40
+ owner: import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<{
41
+ readonly course: ["create", "read", "update", "delete"];
42
+ readonly progress: ["view"];
43
+ readonly organization: readonly ["update", "delete"];
44
+ readonly member: readonly ["create", "update", "delete"];
45
+ readonly invitation: readonly ["create", "cancel"];
46
+ readonly team: readonly ["create", "update", "delete"];
47
+ readonly ac: readonly ["create", "read", "update", "delete"];
48
+ }>, {
49
+ readonly course: readonly ["create", "read", "update", "delete"];
50
+ readonly progress: readonly ["view"];
51
+ readonly organization: readonly ["update", "delete"];
52
+ readonly member: readonly ["create", "update", "delete"];
53
+ readonly invitation: readonly ["create", "cancel"];
54
+ readonly team: readonly ["create", "update", "delete"];
55
+ readonly ac: readonly ["create", "read", "update", "delete"];
56
+ }>;
57
+ admin: import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<{
58
+ readonly course: ["create", "read", "update", "delete"];
59
+ readonly progress: ["view"];
60
+ readonly organization: readonly ["update"];
61
+ readonly invitation: readonly ["create", "cancel"];
62
+ readonly member: readonly ["create", "update", "delete"];
63
+ readonly team: readonly ["create", "update", "delete"];
64
+ readonly ac: readonly ["create", "read", "update", "delete"];
65
+ }>, {
66
+ readonly course: readonly ["create", "read", "update", "delete"];
67
+ readonly progress: readonly ["view"];
68
+ readonly organization: readonly ["update", "delete"];
69
+ readonly member: readonly ["create", "update", "delete"];
70
+ readonly invitation: readonly ["create", "cancel"];
71
+ readonly team: readonly ["create", "update", "delete"];
72
+ readonly ac: readonly ["create", "read", "update", "delete"];
73
+ }>;
74
+ instructor: import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<{
75
+ readonly course: ["read", "update"];
76
+ readonly progress: ["view"];
77
+ }>, {
78
+ readonly course: readonly ["create", "read", "update", "delete"];
79
+ readonly progress: readonly ["view"];
80
+ readonly organization: readonly ["update", "delete"];
81
+ readonly member: readonly ["create", "update", "delete"];
82
+ readonly invitation: readonly ["create", "cancel"];
83
+ readonly team: readonly ["create", "update", "delete"];
84
+ readonly ac: readonly ["create", "read", "update", "delete"];
85
+ }>;
86
+ student: import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<{
87
+ readonly course: ["read"];
88
+ }>, {
89
+ readonly course: readonly ["create", "read", "update", "delete"];
90
+ readonly progress: readonly ["view"];
91
+ readonly organization: readonly ["update", "delete"];
92
+ readonly member: readonly ["create", "update", "delete"];
93
+ readonly invitation: readonly ["create", "cancel"];
94
+ readonly team: readonly ["create", "update", "delete"];
95
+ readonly ac: readonly ["create", "read", "update", "delete"];
96
+ }>;
97
+ };
98
+ //#endregion
99
+ export { ac, roles, statement };
100
+ //# sourceMappingURL=access.d.ts.map
package/dist/access.js ADDED
@@ -0,0 +1,45 @@
1
+ import { createAccessControl } from "better-auth/plugins/access";
2
+ import { adminAc, defaultStatements, ownerAc } from "better-auth/plugins/organization/access";
3
+ //#region src/access.ts
4
+ const statement = {
5
+ ...defaultStatements,
6
+ course: [
7
+ "create",
8
+ "read",
9
+ "update",
10
+ "delete"
11
+ ],
12
+ progress: ["view"]
13
+ };
14
+ const ac = createAccessControl(statement);
15
+ const roles = {
16
+ owner: ac.newRole({
17
+ ...ownerAc.statements,
18
+ course: [
19
+ "create",
20
+ "read",
21
+ "update",
22
+ "delete"
23
+ ],
24
+ progress: ["view"]
25
+ }),
26
+ admin: ac.newRole({
27
+ ...adminAc.statements,
28
+ course: [
29
+ "create",
30
+ "read",
31
+ "update",
32
+ "delete"
33
+ ],
34
+ progress: ["view"]
35
+ }),
36
+ instructor: ac.newRole({
37
+ course: ["read", "update"],
38
+ progress: ["view"]
39
+ }),
40
+ student: ac.newRole({ course: ["read"] })
41
+ };
42
+ //#endregion
43
+ export { ac, roles, statement };
44
+
45
+ //# sourceMappingURL=access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.js","names":[],"sources":["../src/access.ts"],"sourcesContent":["// Better Auth org-plugin access control. Better Auth is the system of record for\n// roles; these four roles mirror the domain Role union (core/organizations/roles).\nimport { createAccessControl } from 'better-auth/plugins/access';\nimport { defaultStatements, ownerAc, adminAc } from 'better-auth/plugins/organization/access';\n\nexport const statement = {\n ...defaultStatements,\n course: ['create', 'read', 'update', 'delete'],\n progress: ['view'],\n} as const;\n\nconst ac = createAccessControl(statement);\n\nexport const roles = {\n owner: ac.newRole({\n ...ownerAc.statements,\n course: ['create', 'read', 'update', 'delete'],\n progress: ['view'],\n }),\n admin: ac.newRole({\n ...adminAc.statements,\n course: ['create', 'read', 'update', 'delete'],\n progress: ['view'],\n }),\n instructor: ac.newRole({\n course: ['read', 'update'],\n progress: ['view'],\n }),\n student: ac.newRole({\n course: ['read'],\n }),\n};\n\nexport { ac };\n"],"mappings":";;;AAKA,MAAa,YAAY;CACvB,GAAG;CACH,QAAQ;EAAC;EAAU;EAAQ;EAAU;CAAQ;CAC7C,UAAU,CAAC,MAAM;AACnB;AAEA,MAAM,KAAK,oBAAoB,SAAS;AAExC,MAAa,QAAQ;CACnB,OAAO,GAAG,QAAQ;EAChB,GAAG,QAAQ;EACX,QAAQ;GAAC;GAAU;GAAQ;GAAU;EAAQ;EAC7C,UAAU,CAAC,MAAM;CACnB,CAAC;CACD,OAAO,GAAG,QAAQ;EAChB,GAAG,QAAQ;EACX,QAAQ;GAAC;GAAU;GAAQ;GAAU;EAAQ;EAC7C,UAAU,CAAC,MAAM;CACnB,CAAC;CACD,YAAY,GAAG,QAAQ;EACrB,QAAQ,CAAC,QAAQ,QAAQ;EACzB,UAAU,CAAC,MAAM;CACnB,CAAC;CACD,SAAS,GAAG,QAAQ,EAClB,QAAQ,CAAC,MAAM,EACjB,CAAC;AACH"}
@@ -0,0 +1,417 @@
1
+ import { $strip } from "./node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.js";
2
+ import { CreateAuthOptions, Hooks } from "./types.js";
3
+ import { AuthHeaders, AuthOrganization, MemberWriteContext, OrgAdmin, Role, UpdateOrganizationInput } from "@headless-lms/core/organizations";
4
+ import { ActiveSession, Logger, NewOrganizationInput, SessionVerifier } from "@headless-lms/core/types";
5
+ import { IncomingHttpHeaders } from "node:http";
6
+ import { SessionAdmin } from "@headless-lms/core/identity";
7
+ //#region src/better-auth.d.ts
8
+ declare function createAuth(opts: CreateAuthOptions): import("better-auth").Auth<{
9
+ baseURL: string;
10
+ secret: string;
11
+ trustedOrigins: string[];
12
+ session: {
13
+ cookieCache: {
14
+ enabled: true;
15
+ maxAge: number;
16
+ };
17
+ };
18
+ advanced: {
19
+ database: {
20
+ generateId: () => string;
21
+ };
22
+ crossSubDomainCookies: {
23
+ enabled: true;
24
+ domain: string | undefined;
25
+ };
26
+ defaultCookieAttributes: {
27
+ sameSite: "lax";
28
+ secure: boolean;
29
+ httpOnly: true;
30
+ };
31
+ };
32
+ database: (options: import("better-auth").BetterAuthOptions) => import("better-auth").DBAdapter<import("better-auth").BetterAuthOptions>;
33
+ emailAndPassword: {
34
+ enabled: true;
35
+ sendResetPassword: (data: {
36
+ user: import("better-auth").User;
37
+ url: string;
38
+ token: string;
39
+ }, request?: Request) => Promise<void>;
40
+ };
41
+ plugins: [{
42
+ id: "magic-link";
43
+ version: string;
44
+ endpoints: {
45
+ signInMagicLink: import("better-auth").StrictEndpoint<"/sign-in/magic-link", {
46
+ method: "POST";
47
+ requireHeaders: true;
48
+ use: ((inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>)[];
49
+ body: import("better-auth").ZodObject<{
50
+ email: import("better-auth").ZodEmail;
51
+ name: import("better-auth").ZodOptional<import("better-auth").ZodString>;
52
+ callbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
53
+ newUserCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
54
+ errorCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
55
+ metadata: import("better-auth").ZodOptional<import("better-auth").ZodRecord<import("better-auth").ZodString, import("better-auth").ZodAny>>;
56
+ }, $strip>;
57
+ metadata: {
58
+ openapi: {
59
+ operationId: string;
60
+ description: string;
61
+ responses: {
62
+ 200: {
63
+ description: string;
64
+ content: {
65
+ "application/json": {
66
+ schema: {
67
+ type: "object";
68
+ properties: {
69
+ status: {
70
+ type: string;
71
+ };
72
+ };
73
+ };
74
+ };
75
+ };
76
+ };
77
+ };
78
+ };
79
+ };
80
+ }, {
81
+ status: boolean;
82
+ }>;
83
+ magicLinkVerify: import("better-auth").StrictEndpoint<"/magic-link/verify", {
84
+ method: "GET";
85
+ query: import("better-auth").ZodObject<{
86
+ token: import("better-auth").ZodString;
87
+ callbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
88
+ errorCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
89
+ newUserCallbackURL: import("better-auth").ZodOptional<import("better-auth").ZodString>;
90
+ }, $strip>;
91
+ use: ((inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>)[];
92
+ requireHeaders: true;
93
+ metadata: {
94
+ openapi: {
95
+ operationId: string;
96
+ description: string;
97
+ responses: {
98
+ 200: {
99
+ description: string;
100
+ content: {
101
+ "application/json": {
102
+ schema: {
103
+ type: "object";
104
+ properties: {
105
+ session: {
106
+ $ref: string;
107
+ };
108
+ user: {
109
+ $ref: string;
110
+ };
111
+ };
112
+ };
113
+ };
114
+ };
115
+ };
116
+ };
117
+ };
118
+ };
119
+ }, {
120
+ token: string;
121
+ user: {
122
+ id: string;
123
+ createdAt: Date;
124
+ updatedAt: Date;
125
+ email: string;
126
+ emailVerified: boolean;
127
+ name: string;
128
+ image?: string | null | undefined;
129
+ };
130
+ session: {
131
+ id: string;
132
+ createdAt: Date;
133
+ updatedAt: Date;
134
+ userId: string;
135
+ expiresAt: Date;
136
+ token: string;
137
+ ipAddress?: string | null | undefined;
138
+ userAgent?: string | null | undefined;
139
+ };
140
+ }>;
141
+ };
142
+ rateLimit: {
143
+ pathMatcher(path: string): boolean;
144
+ window: number;
145
+ max: number;
146
+ }[];
147
+ options: import("better-auth/plugins").MagicLinkOptions;
148
+ }, import("better-auth/plugins").DefaultOrganizationPlugin<{
149
+ ac: {
150
+ newRole<const TRoleStatements extends import("better-auth/plugins").Statements>(statements: import("better-auth/plugins").RoleInput<{
151
+ readonly course: readonly ["create", "read", "update", "delete"];
152
+ readonly progress: readonly ["view"];
153
+ readonly organization: readonly ["update", "delete"];
154
+ readonly member: readonly ["create", "update", "delete"];
155
+ readonly invitation: readonly ["create", "cancel"];
156
+ readonly team: readonly ["create", "update", "delete"];
157
+ readonly ac: readonly ["create", "read", "update", "delete"];
158
+ }, TRoleStatements>): import("better-auth/plugins").Role<import("better-auth/plugins").ExactRoleStatements<TRoleStatements>, {
159
+ readonly course: readonly ["create", "read", "update", "delete"];
160
+ readonly progress: readonly ["view"];
161
+ readonly organization: readonly ["update", "delete"];
162
+ readonly member: readonly ["create", "update", "delete"];
163
+ readonly invitation: readonly ["create", "cancel"];
164
+ readonly team: readonly ["create", "update", "delete"];
165
+ readonly ac: readonly ["create", "read", "update", "delete"];
166
+ }>;
167
+ statements: {
168
+ readonly course: readonly ["create", "read", "update", "delete"];
169
+ readonly progress: readonly ["view"];
170
+ readonly organization: readonly ["update", "delete"];
171
+ readonly member: readonly ["create", "update", "delete"];
172
+ readonly invitation: readonly ["create", "cancel"];
173
+ readonly team: readonly ["create", "update", "delete"];
174
+ readonly ac: readonly ["create", "read", "update", "delete"];
175
+ };
176
+ };
177
+ roles: {
178
+ owner: import("better-auth/plugins").Role<import("better-auth/plugins").ExactRoleStatements<{
179
+ readonly course: ["create", "read", "update", "delete"];
180
+ readonly progress: ["view"];
181
+ readonly organization: readonly ["update", "delete"];
182
+ readonly member: readonly ["create", "update", "delete"];
183
+ readonly invitation: readonly ["create", "cancel"];
184
+ readonly team: readonly ["create", "update", "delete"];
185
+ readonly ac: readonly ["create", "read", "update", "delete"];
186
+ }>, {
187
+ readonly course: readonly ["create", "read", "update", "delete"];
188
+ readonly progress: readonly ["view"];
189
+ readonly organization: readonly ["update", "delete"];
190
+ readonly member: readonly ["create", "update", "delete"];
191
+ readonly invitation: readonly ["create", "cancel"];
192
+ readonly team: readonly ["create", "update", "delete"];
193
+ readonly ac: readonly ["create", "read", "update", "delete"];
194
+ }>;
195
+ admin: import("better-auth/plugins").Role<import("better-auth/plugins").ExactRoleStatements<{
196
+ readonly course: ["create", "read", "update", "delete"];
197
+ readonly progress: ["view"];
198
+ readonly organization: readonly ["update"];
199
+ readonly invitation: readonly ["create", "cancel"];
200
+ readonly member: readonly ["create", "update", "delete"];
201
+ readonly team: readonly ["create", "update", "delete"];
202
+ readonly ac: readonly ["create", "read", "update", "delete"];
203
+ }>, {
204
+ readonly course: readonly ["create", "read", "update", "delete"];
205
+ readonly progress: readonly ["view"];
206
+ readonly organization: readonly ["update", "delete"];
207
+ readonly member: readonly ["create", "update", "delete"];
208
+ readonly invitation: readonly ["create", "cancel"];
209
+ readonly team: readonly ["create", "update", "delete"];
210
+ readonly ac: readonly ["create", "read", "update", "delete"];
211
+ }>;
212
+ instructor: import("better-auth/plugins").Role<import("better-auth/plugins").ExactRoleStatements<{
213
+ readonly course: ["read", "update"];
214
+ readonly progress: ["view"];
215
+ }>, {
216
+ readonly course: readonly ["create", "read", "update", "delete"];
217
+ readonly progress: readonly ["view"];
218
+ readonly organization: readonly ["update", "delete"];
219
+ readonly member: readonly ["create", "update", "delete"];
220
+ readonly invitation: readonly ["create", "cancel"];
221
+ readonly team: readonly ["create", "update", "delete"];
222
+ readonly ac: readonly ["create", "read", "update", "delete"];
223
+ }>;
224
+ student: import("better-auth/plugins").Role<import("better-auth/plugins").ExactRoleStatements<{
225
+ readonly course: ["read"];
226
+ }>, {
227
+ readonly course: readonly ["create", "read", "update", "delete"];
228
+ readonly progress: readonly ["view"];
229
+ readonly organization: readonly ["update", "delete"];
230
+ readonly member: readonly ["create", "update", "delete"];
231
+ readonly invitation: readonly ["create", "cancel"];
232
+ readonly team: readonly ["create", "update", "delete"];
233
+ readonly ac: readonly ["create", "read", "update", "delete"];
234
+ }>;
235
+ };
236
+ creatorRole: string;
237
+ organizationHooks: Hooks;
238
+ }>, {
239
+ id: "custom-session";
240
+ version: string;
241
+ hooks: {
242
+ after: {
243
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
244
+ handler: (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
245
+ user: {
246
+ id: string;
247
+ createdAt: Date;
248
+ updatedAt: Date;
249
+ email: string;
250
+ emailVerified: boolean;
251
+ name: string;
252
+ image?: string | null | undefined;
253
+ };
254
+ session: {
255
+ id: string;
256
+ createdAt: Date;
257
+ updatedAt: Date;
258
+ userId: string;
259
+ expiresAt: Date;
260
+ token: string;
261
+ ipAddress?: string | null | undefined;
262
+ userAgent?: string | null | undefined;
263
+ };
264
+ activeMemberRole: string | null;
265
+ activeOrganization: {
266
+ id: string;
267
+ name: string;
268
+ slug: string;
269
+ } | null;
270
+ organizations: {
271
+ id: string;
272
+ name: string;
273
+ slug: string;
274
+ }[];
275
+ }[] | undefined>;
276
+ }[];
277
+ };
278
+ endpoints: {
279
+ getSession: import("better-auth").StrictEndpoint<"/get-session", {
280
+ method: "GET";
281
+ query: import("better-auth").ZodOptional<import("better-auth").ZodObject<{
282
+ disableCookieCache: import("better-auth").ZodOptional<import("better-auth").ZodCoercedBoolean<unknown>>;
283
+ disableRefresh: import("better-auth").ZodOptional<import("better-auth").ZodCoercedBoolean<unknown>>;
284
+ }, $strip>>;
285
+ metadata: {
286
+ CUSTOM_SESSION: boolean;
287
+ openapi: {
288
+ description: string;
289
+ responses: {
290
+ "200": {
291
+ description: string;
292
+ content: {
293
+ "application/json": {
294
+ schema: {
295
+ type: "array";
296
+ nullable: boolean;
297
+ items: {
298
+ $ref: string;
299
+ };
300
+ };
301
+ };
302
+ };
303
+ };
304
+ };
305
+ };
306
+ };
307
+ requireHeaders: true;
308
+ }, {
309
+ user: {
310
+ id: string;
311
+ createdAt: Date;
312
+ updatedAt: Date;
313
+ email: string;
314
+ emailVerified: boolean;
315
+ name: string;
316
+ image?: string | null | undefined;
317
+ };
318
+ session: {
319
+ id: string;
320
+ createdAt: Date;
321
+ updatedAt: Date;
322
+ userId: string;
323
+ expiresAt: Date;
324
+ token: string;
325
+ ipAddress?: string | null | undefined;
326
+ userAgent?: string | null | undefined;
327
+ };
328
+ activeMemberRole: string | null;
329
+ activeOrganization: {
330
+ id: string;
331
+ name: string;
332
+ slug: string;
333
+ } | null;
334
+ organizations: {
335
+ id: string;
336
+ name: string;
337
+ slug: string;
338
+ }[];
339
+ } | null>;
340
+ };
341
+ $Infer: {
342
+ Session: {
343
+ user: {
344
+ id: string;
345
+ createdAt: Date;
346
+ updatedAt: Date;
347
+ email: string;
348
+ emailVerified: boolean;
349
+ name: string;
350
+ image?: string | null | undefined;
351
+ };
352
+ session: {
353
+ id: string;
354
+ createdAt: Date;
355
+ updatedAt: Date;
356
+ userId: string;
357
+ expiresAt: Date;
358
+ token: string;
359
+ ipAddress?: string | null | undefined;
360
+ userAgent?: string | null | undefined;
361
+ };
362
+ activeMemberRole: string | null;
363
+ activeOrganization: {
364
+ id: string;
365
+ name: string;
366
+ slug: string;
367
+ } | null;
368
+ organizations: {
369
+ id: string;
370
+ name: string;
371
+ slug: string;
372
+ }[];
373
+ };
374
+ };
375
+ options: import("better-auth/plugins").CustomSessionPluginOptions | undefined;
376
+ }];
377
+ databaseHooks: {
378
+ user: {
379
+ create: {
380
+ before: (user: import("better-auth").User & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
381
+ data: import("better-auth").User;
382
+ }>;
383
+ };
384
+ };
385
+ session: {
386
+ create: {
387
+ before: ((session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
388
+ data: import("better-auth").Session & Record<string, unknown>;
389
+ }>) | undefined;
390
+ };
391
+ };
392
+ };
393
+ }>;
394
+ type Auth = ReturnType<typeof createAuth>;
395
+ declare class BetterAuth implements SessionVerifier, OrgAdmin, SessionAdmin {
396
+ protected auth: Auth;
397
+ protected logger: Logger;
398
+ constructor(opts: CreateAuthOptions);
399
+ handler(request: Request): Promise<Response>;
400
+ verify(headers: IncomingHttpHeaders): Promise<ActiveSession | null>;
401
+ /** Stamps the org onto the caller's session row. The 5-minute cookie cache
402
+ * still holds the pre-stamp session, so callers refresh it client-side with
403
+ * `getSession({ disableCookieCache: true })`. */
404
+ stampActiveOrganization(headers: AuthHeaders, orgId: string): Promise<boolean>;
405
+ createOrganization(headers: AuthHeaders, input: NewOrganizationInput): Promise<AuthOrganization>;
406
+ setActiveOrganization(headers: AuthHeaders, externalId: string): Promise<void>;
407
+ updateOrganization(headers: AuthHeaders, externalId: string, input: UpdateOrganizationInput): Promise<AuthOrganization>;
408
+ deleteOrganization(headers: AuthHeaders, externalId: string): Promise<void>;
409
+ grantMembership(orgExternalId: string, userExternalId: string, role: string): Promise<void>;
410
+ updateRole(ctx: MemberWriteContext, userExternalId: string, role: Role): Promise<void>;
411
+ removeMember(ctx: MemberWriteContext, userExternalId: string): Promise<void>;
412
+ protected headersOf(ctx: MemberWriteContext): Headers;
413
+ protected resolveMemberId(ctx: MemberWriteContext, userExternalId: string): Promise<string>;
414
+ }
415
+ //#endregion
416
+ export { Auth, BetterAuth, createAuth };
417
+ //# sourceMappingURL=better-auth.d.ts.map
@@ -0,0 +1,222 @@
1
+ import { ac, roles } from "./access.js";
2
+ import { fromNodeHeaders } from "better-auth/node";
3
+ import { APIError, betterAuth } from "better-auth";
4
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
5
+ import * as authSchema from "@headless-lms/adapter-db/schema/better-auth";
6
+ import { prefixId } from "@headless-lms/core/shared/id";
7
+ import { customSession, magicLink, organization } from "better-auth/plugins";
8
+ import { eq } from "drizzle-orm";
9
+ import { OrganizationRuleError, parseRole } from "@headless-lms/core/organizations";
10
+ //#region src/better-auth.ts
11
+ function createAuth(opts) {
12
+ const { sendResetPassword, sendMagicLink, beforeUserCreate, beforeCreateSession } = opts.hooks;
13
+ return betterAuth({
14
+ baseURL: opts.baseUrl,
15
+ secret: opts.secret,
16
+ trustedOrigins: opts.trustedOrigins,
17
+ session: { cookieCache: {
18
+ enabled: true,
19
+ maxAge: 300
20
+ } },
21
+ advanced: {
22
+ database: { generateId: () => prefixId("id") },
23
+ crossSubDomainCookies: {
24
+ enabled: true,
25
+ domain: opts.cookieDomain || void 0
26
+ },
27
+ defaultCookieAttributes: {
28
+ sameSite: "lax",
29
+ secure: opts.secureCookies ?? false,
30
+ httpOnly: true
31
+ }
32
+ },
33
+ database: drizzleAdapter(opts.db, {
34
+ provider: "pg",
35
+ schema: authSchema
36
+ }),
37
+ emailAndPassword: {
38
+ enabled: true,
39
+ sendResetPassword
40
+ },
41
+ plugins: [
42
+ magicLink({
43
+ disableSignUp: true,
44
+ sendMagicLink
45
+ }),
46
+ organization({
47
+ ac,
48
+ roles,
49
+ creatorRole: "owner",
50
+ organizationHooks: opts.hooks
51
+ }),
52
+ customSession(async ({ user, session }) => {
53
+ const memberships = await opts.db.select({
54
+ role: authSchema.member.role,
55
+ id: authSchema.organization.id,
56
+ name: authSchema.organization.name,
57
+ slug: authSchema.organization.slug
58
+ }).from(authSchema.member).innerJoin(authSchema.organization, eq(authSchema.member.organizationId, authSchema.organization.id)).where(eq(authSchema.member.userId, user.id));
59
+ const activeOrgId = session.activeOrganizationId ?? null;
60
+ const active = memberships.find((m) => m.id === activeOrgId) ?? null;
61
+ return {
62
+ user,
63
+ session,
64
+ activeMemberRole: active?.role ?? null,
65
+ activeOrganization: active ? {
66
+ id: active.id,
67
+ name: active.name,
68
+ slug: active.slug
69
+ } : null,
70
+ organizations: memberships.map(({ id, name, slug }) => ({
71
+ id,
72
+ name,
73
+ slug
74
+ }))
75
+ };
76
+ })
77
+ ],
78
+ databaseHooks: {
79
+ user: { create: { before: beforeUserCreate } },
80
+ session: { create: { before: beforeCreateSession } }
81
+ }
82
+ });
83
+ }
84
+ var BetterAuth = class {
85
+ auth;
86
+ logger;
87
+ constructor(opts) {
88
+ this.auth = createAuth(opts);
89
+ this.logger = opts.logger;
90
+ }
91
+ async handler(request) {
92
+ return this.auth.handler(request);
93
+ }
94
+ async verify(headers) {
95
+ const res = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });
96
+ if (!res) return null;
97
+ return {
98
+ user: {
99
+ id: res.user.id,
100
+ createdAt: res.user.createdAt,
101
+ email: res.user.email,
102
+ firstName: res.user.name,
103
+ externalId: res.user.id,
104
+ lastName: res.user.name,
105
+ updatedAt: res.user.updatedAt
106
+ },
107
+ session: { activeOrganizationId: res.session.activeOrganizationId ?? null }
108
+ };
109
+ }
110
+ /** Stamps the org onto the caller's session row. The 5-minute cookie cache
111
+ * still holds the pre-stamp session, so callers refresh it client-side with
112
+ * `getSession({ disableCookieCache: true })`. */
113
+ async stampActiveOrganization(headers, orgId) {
114
+ const token = ((await this.auth.api.getSession({ headers: fromNodeHeaders(headers) }))?.session)?.token;
115
+ if (!token) return false;
116
+ await (await this.auth.$context).internalAdapter.updateSession(token, { activeOrganizationId: orgId });
117
+ return true;
118
+ }
119
+ async createOrganization(headers, input) {
120
+ const org = await this.auth.api.createOrganization({
121
+ body: {
122
+ name: input.name,
123
+ slug: input.slug
124
+ },
125
+ headers: fromNodeHeaders(headers)
126
+ });
127
+ if (!org) throw new Error(`Couldn't create organization`);
128
+ return {
129
+ id: org.id,
130
+ name: org.name,
131
+ slug: org.slug,
132
+ createdAt: org.createdAt
133
+ };
134
+ }
135
+ async setActiveOrganization(headers, externalId) {
136
+ await this.auth.api.setActiveOrganization({
137
+ body: { organizationId: externalId },
138
+ headers: fromNodeHeaders(headers)
139
+ });
140
+ }
141
+ async updateOrganization(headers, externalId, input) {
142
+ try {
143
+ const org = await this.auth.api.updateOrganization({
144
+ body: {
145
+ organizationId: externalId,
146
+ data: {
147
+ name: input.name,
148
+ slug: input.slug
149
+ }
150
+ },
151
+ headers: fromNodeHeaders(headers)
152
+ });
153
+ if (!org) throw new Error(`Couldn't update organization`);
154
+ return {
155
+ id: org.id,
156
+ name: org.name,
157
+ slug: org.slug,
158
+ createdAt: org.createdAt
159
+ };
160
+ } catch (err) {
161
+ if (err instanceof APIError) throw new OrganizationRuleError(err.message || "Could not update organization");
162
+ throw err;
163
+ }
164
+ }
165
+ async deleteOrganization(headers, externalId) {
166
+ try {
167
+ await this.auth.api.deleteOrganization({
168
+ body: { organizationId: externalId },
169
+ headers: fromNodeHeaders(headers)
170
+ });
171
+ } catch (err) {
172
+ if (err instanceof APIError) throw new OrganizationRuleError(err.message || "Could not delete organization");
173
+ throw err;
174
+ }
175
+ }
176
+ async grantMembership(orgExternalId, userExternalId, role) {
177
+ await this.auth.api.addMember({ body: {
178
+ userId: userExternalId,
179
+ organizationId: orgExternalId,
180
+ role: parseRole(role)
181
+ } });
182
+ }
183
+ async updateRole(ctx, userExternalId, role) {
184
+ await this.auth.api.updateMemberRole({
185
+ body: {
186
+ memberId: await this.resolveMemberId(ctx, userExternalId),
187
+ role,
188
+ organizationId: ctx.authOrgId
189
+ },
190
+ headers: this.headersOf(ctx)
191
+ });
192
+ }
193
+ async removeMember(ctx, userExternalId) {
194
+ await this.auth.api.removeMember({
195
+ body: {
196
+ memberIdOrEmail: await this.resolveMemberId(ctx, userExternalId),
197
+ organizationId: ctx.authOrgId
198
+ },
199
+ headers: this.headersOf(ctx)
200
+ });
201
+ }
202
+ headersOf(ctx) {
203
+ return fromNodeHeaders(ctx.headers);
204
+ }
205
+ async resolveMemberId(ctx, userExternalId) {
206
+ const memberId = (await this.auth.api.listMembers({
207
+ query: {
208
+ organizationId: ctx.authOrgId,
209
+ filterField: "userId",
210
+ filterValue: userExternalId,
211
+ limit: 1
212
+ },
213
+ headers: this.headersOf(ctx)
214
+ }))?.members?.[0]?.id;
215
+ if (!memberId) throw new OrganizationRuleError("That person is no longer a member of this organization");
216
+ return memberId;
217
+ }
218
+ };
219
+ //#endregion
220
+ export { BetterAuth, createAuth };
221
+
222
+ //# sourceMappingURL=better-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"better-auth.js","names":[],"sources":["../src/better-auth.ts"],"sourcesContent":["import type {\n ActiveSession,\n Logger, NewOrganizationInput,\n SessionVerifier,\n} from '@headless-lms/core/types';\nimport type { IncomingHttpHeaders } from 'node:http';\nimport { fromNodeHeaders } from 'better-auth/node';\nimport { APIError, betterAuth } from 'better-auth';\nimport type { CreateAuthOptions } from './types.js';\nimport { drizzleAdapter } from 'better-auth/adapters/drizzle';\nimport * as authSchema from '@headless-lms/adapter-db/schema/better-auth';\nimport { prefixId } from '@headless-lms/core/shared/id';\nimport { customSession, magicLink, organization } from 'better-auth/plugins';\nimport { eq } from 'drizzle-orm';\nimport { ac, roles } from './access.js';\nimport { type AuthHeaders, type AuthOrganization, type MemberWriteContext, type OrgAdmin, OrganizationRuleError, parseRole, type Role, type UpdateOrganizationInput } from '@headless-lms/core/organizations';\nimport type { SessionAdmin } from '@headless-lms/core/identity';\n\nexport function createAuth(opts: CreateAuthOptions) {\n const { sendResetPassword, sendMagicLink, beforeUserCreate, beforeCreateSession } = opts.hooks;\n\n const auth = betterAuth({\n baseURL: opts.baseUrl,\n secret: opts.secret,\n trustedOrigins: opts.trustedOrigins,\n session: {\n // Signed short-lived cookie cache: avoids a Postgres session lookup on\n // every request. The BFF verifies the session per request (each API call\n // + every SSR getSession), so without this each one is a DB round-trip.\n // The cache holds for maxAge; sign-out / expiry still invalidate it.\n cookieCache: {\n enabled: true,\n maxAge: 5 * 60,\n },\n },\n advanced: {\n database: {\n // Prefixed, KSUID-bodied ids for every better-auth table (usr_, org_, …).\n generateId: () => prefixId('id'),\n },\n // Cross-subdomain shared session cookie for admin/api/web on one parent\n // domain (e.g. `.example.com` in prod). Left unset in local dev so the\n // host-only `localhost` cookie is used, which is already shared across\n // ports (cookies are not port-scoped).\n crossSubDomainCookies: {\n enabled: true,\n domain: opts.cookieDomain || undefined,\n },\n // Same-site cookie for the shared-parent-domain plan. Only switch to\n // `sameSite: \"none\"` + `secure` if admin and api are genuinely cross-site\n // (different registrable domains).\n defaultCookieAttributes: {\n sameSite: 'lax',\n secure: opts.secureCookies ?? false,\n httpOnly: true,\n },\n },\n database: drizzleAdapter(opts.db, {\n provider: 'pg',\n schema: authSchema,\n }),\n emailAndPassword: {\n enabled: true,\n sendResetPassword,\n },\n plugins: [\n magicLink({\n disableSignUp: true,\n sendMagicLink,\n }),\n organization({\n ac,\n roles,\n creatorRole: 'owner',\n organizationHooks: opts.hooks,\n }),\n // Enrich get-session with the caller's memberships (role + org) so BFFs\n // resolve auth in one call instead of fanning out to get-active-member /\n // get-full-organization / organization/list. Custom fields bypass the\n // cookie cache, so this costs one indexed query per get-session.\n customSession(async ({ user, session }) => {\n const memberships = await opts.db\n .select({\n role: authSchema.member.role,\n id: authSchema.organization.id,\n name: authSchema.organization.name,\n slug: authSchema.organization.slug,\n })\n .from(authSchema.member)\n .innerJoin(authSchema.organization, eq(authSchema.member.organizationId, authSchema.organization.id))\n .where(eq(authSchema.member.userId, user.id));\n const activeOrgId =\n (session as { activeOrganizationId?: string | null }).activeOrganizationId ?? null;\n const active = memberships.find((m) => m.id === activeOrgId) ?? null;\n return {\n user,\n session,\n activeMemberRole: active?.role ?? null,\n activeOrganization: active ? { id: active.id, name: active.name, slug: active.slug } : null,\n organizations: memberships.map(({ id, name, slug }) => ({ id, name, slug })),\n };\n }),\n ],\n databaseHooks: {\n user: {\n create: {\n before: beforeUserCreate,\n },\n },\n session: {\n create: {\n before: beforeCreateSession,\n },\n },\n },\n });\n return auth;\n}\n\nexport type Auth = ReturnType<typeof createAuth>;\n\nexport class BetterAuth implements SessionVerifier, OrgAdmin, SessionAdmin {\n protected auth: Auth;\n protected logger: Logger;\n\n constructor(opts: CreateAuthOptions) {\n this.auth = createAuth(opts);\n this.logger = opts.logger;\n }\n\n async handler(request: Request) :Promise<Response>{\n return this.auth.handler(request);\n }\n\n async verify(headers: IncomingHttpHeaders): Promise<ActiveSession | null> {\n const res = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });\n if (!res) {\n return null;\n }\n return {\n user: {\n id: res.user.id,\n createdAt: res.user.createdAt,\n email: res.user.email,\n firstName: res.user.name,\n externalId: res.user.id,\n lastName: res.user.name,\n updatedAt: res.user.updatedAt,\n },\n session: {\n // customSession erases the org plugin's session extension from the\n // inferred type; the field is still present at runtime.\n activeOrganizationId:\n (res.session as { activeOrganizationId?: string | null }).activeOrganizationId ?? null,\n },\n };\n }\n\n\n /** Stamps the org onto the caller's session row. The 5-minute cookie cache\n * still holds the pre-stamp session, so callers refresh it client-side with\n * `getSession({ disableCookieCache: true })`. */\n async stampActiveOrganization(headers: AuthHeaders, orgId: string): Promise<boolean> {\n const session = await this.auth.api.getSession({ headers: fromNodeHeaders(headers) });\n const token = (session?.session as { token?: string } | undefined)?.token;\n if (!token) {\n return false;\n }\n const ctx = await this.auth.$context;\n await ctx.internalAdapter.updateSession(token, { activeOrganizationId: orgId });\n return true;\n }\n\n async createOrganization(\n headers: AuthHeaders,\n input: NewOrganizationInput,\n ): Promise<AuthOrganization> {\n const org = await this.auth.api.createOrganization({\n body: { name: input.name, slug: input.slug },\n headers: fromNodeHeaders(headers),\n });\n if (!org) {\n throw new Error(`Couldn't create organization`);\n }\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt };\n }\n async setActiveOrganization(headers: AuthHeaders, externalId: string): Promise<void> {\n await this.auth.api.setActiveOrganization({\n body: { organizationId: externalId },\n headers: fromNodeHeaders(headers),\n });\n }\n async updateOrganization(\n headers: AuthHeaders,\n externalId: string,\n input: UpdateOrganizationInput,\n ): Promise<AuthOrganization> {\n try {\n const org = await this.auth.api.updateOrganization({\n body: { organizationId: externalId, data: { name: input.name, slug: input.slug } },\n headers: fromNodeHeaders(headers),\n });\n if (!org) {\n throw new Error(`Couldn't update organization`);\n }\n return { id: org.id, name: org.name, slug: org.slug, createdAt: org.createdAt };\n } catch (err) {\n // Surface Better Auth validation/permission failures (e.g. slug taken) as\n // a domain rule error the route maps to 409 rather than a raw 500.\n if (err instanceof APIError) {\n throw new OrganizationRuleError(err.message || 'Could not update organization');\n }\n throw err;\n }\n }\n async deleteOrganization(headers: AuthHeaders, externalId: string): Promise<void> {\n try {\n await this.auth.api.deleteOrganization({\n body: { organizationId: externalId },\n headers: fromNodeHeaders(headers),\n });\n } catch (err) {\n if (err instanceof APIError) {\n throw new OrganizationRuleError(err.message || 'Could not delete organization');\n }\n throw err;\n }\n }\n async grantMembership(\n orgExternalId: string,\n userExternalId: string,\n role: string,\n ): Promise<void> {\n // Server-side (no session): the accepted invitation is the authorisation.\n await this.auth.api.addMember({\n body: { userId: userExternalId, organizationId: orgExternalId, role: parseRole(role) },\n });\n }\n async updateRole(ctx: MemberWriteContext, userExternalId: string, role: Role): Promise<void> {\n await this.auth.api.updateMemberRole({\n body: {\n memberId: await this.resolveMemberId(ctx, userExternalId),\n role,\n organizationId: ctx.authOrgId,\n },\n headers: this.headersOf(ctx),\n });\n }\n async removeMember(ctx: MemberWriteContext, userExternalId: string): Promise<void> {\n await this.auth.api.removeMember({\n body: {\n memberIdOrEmail: await this.resolveMemberId(ctx, userExternalId),\n organizationId: ctx.authOrgId,\n },\n headers: this.headersOf(ctx),\n });\n }\n\n protected headersOf(ctx: MemberWriteContext): Headers {\n return fromNodeHeaders(ctx.headers);\n }\n\n // Better Auth's member endpoints key on its own member id, which the domain\n // deliberately does not store — `(org, user)` already identifies the record,\n // so it is looked up here instead of mirrored into org_users.\n protected async resolveMemberId(\n ctx: MemberWriteContext,\n userExternalId: string,\n ): Promise<string> {\n const res = await this.auth.api.listMembers({\n query: {\n organizationId: ctx.authOrgId,\n filterField: 'userId',\n filterValue: userExternalId,\n limit: 1,\n },\n headers: this.headersOf(ctx),\n });\n const memberId = res?.members?.[0]?.id;\n if (!memberId) {\n throw new OrganizationRuleError('That person is no longer a member of this organization');\n }\n return memberId;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { CreateAuthOptions, Hooks } from "./types.js";
2
+ import { Auth, BetterAuth, createAuth } from "./better-auth.js";
3
+ export { Auth, BetterAuth, CreateAuthOptions, Hooks, createAuth };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { BetterAuth, createAuth } from "./better-auth.js";
2
+ import "./types.js";
3
+ export { BetterAuth, createAuth };
@@ -0,0 +1,8 @@
1
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.d.cts
2
+ type $strip = {
3
+ out: {};
4
+ in: {};
5
+ };
6
+ //#endregion
7
+ export { $strip };
8
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { GenericEndpointContext, Session, User } from "better-auth";
2
+ import { OrganizationOptions } from "better-auth/plugins";
3
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+ import { Logger } from "@headless-lms/core/types";
5
+ //#region src/types.d.ts
6
+ type Hooks = NonNullable<OrganizationOptions['organizationHooks']> & {
7
+ beforeUserCreate: (user: User & Record<string, unknown>, context: GenericEndpointContext | null) => Promise<boolean | void | {
8
+ data: User;
9
+ }>;
10
+ beforeCreateSession?: (session: Session & Record<string, unknown>, context: GenericEndpointContext | null) => Promise<boolean | void | {
11
+ data: Session & Record<string, unknown>;
12
+ }>;
13
+ sendResetPassword: (data: {
14
+ user: User;
15
+ url: string;
16
+ token: string;
17
+ }, request?: Request) => Promise<void>;
18
+ sendMagicLink: (data: {
19
+ email: string;
20
+ url: string;
21
+ token: string;
22
+ metadata?: Record<string, unknown>;
23
+ }, ctx?: GenericEndpointContext | undefined) => Promise<void>;
24
+ };
25
+ interface CreateAuthOptions {
26
+ db: NodePgDatabase;
27
+ baseUrl: string;
28
+ secret: string;
29
+ trustedOrigins: string[];
30
+ /** Logs failures that must not abort an auth flow (e.g. a failed invite email). */
31
+ logger: Logger;
32
+ /** Handlers for what the engine reports. The adapter owns no rules. */
33
+ hooks: Hooks;
34
+ /** Parent domain for cross-subdomain session cookies (e.g. ".example.com"); undefined → host-only cookie. */
35
+ cookieDomain?: string;
36
+ /** Mark session cookies Secure (set behind HTTPS / in production). */
37
+ secureCookies?: boolean;
38
+ }
39
+ //#endregion
40
+ export { CreateAuthOptions, Hooks };
41
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ import "better-auth";
2
+ import "better-auth/plugins";
3
+ export {};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@headless-lms/adapter-auth",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "dependencies": {
17
+ "better-auth": "^1.6.25",
18
+ "drizzle-orm": "^0.45.2",
19
+ "@headless-lms/adapter-db": "0.1.0",
20
+ "@headless-lms/core": "0.1.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.10.2",
24
+ "tsdown": "^0.22.9",
25
+ "typescript": "^5.7.2",
26
+ "vitest": "^2.1.8"
27
+ },
28
+ "description": "better-auth adapter for the headless LMS",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/mdwt/headless-lms.git",
33
+ "directory": "adapters/auth"
34
+ },
35
+ "homepage": "https://github.com/mdwt/headless-lms#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/mdwt/headless-lms/issues"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "test": "vitest run",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit"
46
+ }
47
+ }