@pedr0ni/nestjs-better-auth 1.0.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/dist/index.cjs ADDED
@@ -0,0 +1,535 @@
1
+ 'use strict';
2
+
3
+ const common = require('@nestjs/common');
4
+ const core = require('@nestjs/core');
5
+ const node = require('better-auth/node');
6
+ const plugins = require('better-auth/plugins');
7
+ const express = require('express');
8
+ const shared_utils_js = require('@nestjs/common/utils/shared.utils.js');
9
+ const utils_js = require('@nestjs/core/middleware/utils.js');
10
+
11
+ function _interopNamespaceCompat(e) {
12
+ if (e && typeof e === 'object' && 'default' in e) return e;
13
+ const n = Object.create(null);
14
+ if (e) {
15
+ for (const k in e) {
16
+ n[k] = e[k];
17
+ }
18
+ }
19
+ n.default = e;
20
+ return n;
21
+ }
22
+
23
+ const express__namespace = /*#__PURE__*/_interopNamespaceCompat(express);
24
+
25
+ const BEFORE_HOOK_KEY = Symbol("BEFORE_HOOK");
26
+ const AFTER_HOOK_KEY = Symbol("AFTER_HOOK");
27
+ const HOOK_KEY = Symbol("HOOK");
28
+ const AUTH_MODULE_OPTIONS_KEY = Symbol("AUTH_MODULE_OPTIONS");
29
+
30
+ let GqlExecutionContext;
31
+ function getGqlExecutionContext() {
32
+ if (!GqlExecutionContext) {
33
+ GqlExecutionContext = require("@nestjs/graphql").GqlExecutionContext;
34
+ }
35
+ return GqlExecutionContext;
36
+ }
37
+ function getRequestFromContext(context) {
38
+ const contextType = context.getType();
39
+ if (contextType === "graphql") {
40
+ return getGqlExecutionContext().create(context).getContext().req;
41
+ }
42
+ if (contextType === "ws") {
43
+ return context.switchToWs().getClient();
44
+ }
45
+ return context.switchToHttp().getRequest();
46
+ }
47
+
48
+ const AllowAnonymous = () => common.SetMetadata("PUBLIC", true);
49
+ const OptionalAuth = () => common.SetMetadata("OPTIONAL", true);
50
+ const Roles = (roles) => common.SetMetadata("ROLES", roles);
51
+ const OrgRoles = (roles) => common.SetMetadata("ORG_ROLES", roles);
52
+ const Public = AllowAnonymous;
53
+ const Optional = OptionalAuth;
54
+ const Session = common.createParamDecorator((_data, context) => {
55
+ const request = getRequestFromContext(context);
56
+ return request.session;
57
+ });
58
+ const BeforeHook = (path) => common.SetMetadata(BEFORE_HOOK_KEY, path);
59
+ const AfterHook = (path) => common.SetMetadata(AFTER_HOOK_KEY, path);
60
+ const Hook = () => common.SetMetadata(HOOK_KEY, true);
61
+
62
+ const MODULE_OPTIONS_TOKEN = Symbol("AUTH_MODULE_OPTIONS");
63
+ const { ConfigurableModuleClass, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = new common.ConfigurableModuleBuilder({
64
+ optionsInjectionToken: MODULE_OPTIONS_TOKEN
65
+ }).setClassMethodName("forRoot").setExtras(
66
+ {
67
+ isGlobal: true,
68
+ disableGlobalAuthGuard: false,
69
+ disableControllers: false
70
+ },
71
+ (def, extras) => {
72
+ return {
73
+ ...def,
74
+ exports: [MODULE_OPTIONS_TOKEN],
75
+ global: extras.isGlobal
76
+ };
77
+ }
78
+ ).build();
79
+
80
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
81
+ var __decorateClass$2 = (decorators, target, key, kind) => {
82
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
83
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
84
+ if (decorator = decorators[i])
85
+ result = (decorator(result)) || result;
86
+ return result;
87
+ };
88
+ var __decorateParam$2 = (index, decorator) => (target, key) => decorator(target, key, index);
89
+ exports.AuthService = class AuthService {
90
+ constructor(options) {
91
+ this.options = options;
92
+ }
93
+ /**
94
+ * Returns the API endpoints provided by the auth instance
95
+ */
96
+ get api() {
97
+ return this.options.auth.api;
98
+ }
99
+ /**
100
+ * Returns the complete auth instance
101
+ * Access this for plugin-specific functionality
102
+ */
103
+ get instance() {
104
+ return this.options.auth;
105
+ }
106
+ };
107
+ exports.AuthService = __decorateClass$2([
108
+ __decorateParam$2(0, common.Inject(MODULE_OPTIONS_TOKEN))
109
+ ], exports.AuthService);
110
+
111
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
112
+ var __decorateClass$1 = (decorators, target, key, kind) => {
113
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
114
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
115
+ if (decorator = decorators[i])
116
+ result = (decorator(result)) || result;
117
+ return result;
118
+ };
119
+ var __decorateParam$1 = (index, decorator) => (target, key) => decorator(target, key, index);
120
+ let GraphQLErrorClass;
121
+ function getGraphQLError() {
122
+ if (!GraphQLErrorClass) {
123
+ try {
124
+ GraphQLErrorClass = require("graphql").GraphQLError;
125
+ } catch (_error) {
126
+ throw new Error(
127
+ "graphql is required for GraphQL support. Please install it: npm install graphql"
128
+ );
129
+ }
130
+ }
131
+ return GraphQLErrorClass;
132
+ }
133
+ let WsException;
134
+ function getWsException() {
135
+ if (!WsException) {
136
+ try {
137
+ WsException = require("@nestjs/websockets").WsException;
138
+ } catch (_error) {
139
+ throw new Error(
140
+ "@nestjs/websockets is required for WebSocket support. Please install it: npm install @nestjs/websockets @nestjs/platform-socket.io"
141
+ );
142
+ }
143
+ }
144
+ return WsException;
145
+ }
146
+ const AuthContextErrorMap = {
147
+ http: {
148
+ UNAUTHORIZED: (args) => new common.UnauthorizedException(
149
+ args ?? {
150
+ code: "UNAUTHORIZED",
151
+ message: "Unauthorized"
152
+ }
153
+ ),
154
+ FORBIDDEN: (args) => new common.ForbiddenException(
155
+ args ?? {
156
+ code: "FORBIDDEN",
157
+ message: "Insufficient permissions"
158
+ }
159
+ )
160
+ },
161
+ graphql: {
162
+ UNAUTHORIZED: (args) => {
163
+ const GraphQLError = getGraphQLError();
164
+ if (typeof args === "string") {
165
+ return new GraphQLError(args);
166
+ } else if (typeof args === "object") {
167
+ return new GraphQLError(
168
+ // biome-ignore lint: if `message` is not set, a default is already in place.
169
+ args?.message ?? "Unauthorized",
170
+ args
171
+ );
172
+ }
173
+ return new GraphQLError("Unauthorized");
174
+ },
175
+ FORBIDDEN: (args) => {
176
+ const GraphQLError = getGraphQLError();
177
+ if (typeof args === "string") {
178
+ return new GraphQLError(args);
179
+ } else if (typeof args === "object") {
180
+ return new GraphQLError(
181
+ // biome-ignore lint: if `message` is not set, a default is already in place.
182
+ args?.message ?? "Forbidden",
183
+ args
184
+ );
185
+ }
186
+ return new GraphQLError("Forbidden");
187
+ }
188
+ },
189
+ ws: {
190
+ UNAUTHORIZED: (args) => {
191
+ const WsExceptionClass = getWsException();
192
+ return new WsExceptionClass(args ?? "UNAUTHORIZED");
193
+ },
194
+ FORBIDDEN: (args) => {
195
+ const WsExceptionClass = getWsException();
196
+ return new WsExceptionClass(args ?? "FORBIDDEN");
197
+ }
198
+ },
199
+ rpc: {
200
+ UNAUTHORIZED: () => new Error("UNAUTHORIZED"),
201
+ FORBIDDEN: () => new Error("FORBIDDEN")
202
+ }
203
+ };
204
+ exports.AuthGuard = class AuthGuard {
205
+ constructor(reflector, options) {
206
+ this.reflector = reflector;
207
+ this.options = options;
208
+ }
209
+ /**
210
+ * Validates if the current request is authenticated
211
+ * Attaches session and user information to the request object
212
+ * Supports HTTP, GraphQL and WebSocket execution contexts
213
+ * @param context - The execution context of the current request
214
+ * @returns True if the request is authorized to proceed, throws an error otherwise
215
+ */
216
+ async canActivate(context) {
217
+ const request = getRequestFromContext(context);
218
+ const session = await this.options.auth.api.getSession({
219
+ headers: node.fromNodeHeaders(
220
+ request.headers || request?.handshake?.headers || []
221
+ )
222
+ });
223
+ request.session = session;
224
+ request.user = session?.user ?? null;
225
+ const isPublic = this.reflector.getAllAndOverride("PUBLIC", [
226
+ context.getHandler(),
227
+ context.getClass()
228
+ ]);
229
+ if (isPublic) return true;
230
+ const isOptional = this.reflector.getAllAndOverride("OPTIONAL", [
231
+ context.getHandler(),
232
+ context.getClass()
233
+ ]);
234
+ if (!session && isOptional) return true;
235
+ const ctxType = context.getType();
236
+ if (!session) throw AuthContextErrorMap[ctxType].UNAUTHORIZED();
237
+ const headers = node.fromNodeHeaders(
238
+ request.headers || request?.handshake?.headers || []
239
+ );
240
+ const requiredRoles = this.reflector.getAllAndOverride("ROLES", [
241
+ context.getHandler(),
242
+ context.getClass()
243
+ ]);
244
+ if (requiredRoles && requiredRoles.length > 0) {
245
+ const hasRole = this.checkUserRole(session, requiredRoles);
246
+ if (!hasRole) throw AuthContextErrorMap[ctxType].FORBIDDEN();
247
+ }
248
+ const requiredOrgRoles = this.reflector.getAllAndOverride(
249
+ "ORG_ROLES",
250
+ [context.getHandler(), context.getClass()]
251
+ );
252
+ if (requiredOrgRoles && requiredOrgRoles.length > 0) {
253
+ const hasOrgRole = await this.checkOrgRole(
254
+ session,
255
+ headers,
256
+ requiredOrgRoles
257
+ );
258
+ if (!hasOrgRole) throw AuthContextErrorMap[ctxType].FORBIDDEN();
259
+ }
260
+ return true;
261
+ }
262
+ /**
263
+ * Checks if a role value matches any of the required roles
264
+ * Handles both array and comma-separated string role formats
265
+ * @param role - The role value to check (string, array, or undefined)
266
+ * @param requiredRoles - Array of roles that grant access
267
+ * @returns True if the role matches any required role
268
+ */
269
+ matchesRequiredRole(role, requiredRoles) {
270
+ if (!role) return false;
271
+ if (Array.isArray(role)) {
272
+ return role.some((r) => requiredRoles.includes(r));
273
+ }
274
+ if (typeof role === "string") {
275
+ return role.split(",").some((r) => requiredRoles.includes(r.trim()));
276
+ }
277
+ return false;
278
+ }
279
+ /**
280
+ * Fetches the user's role within an organization from the member table
281
+ * Uses Better Auth's organization plugin API if available
282
+ * @param headers - The request headers containing session cookies
283
+ * @returns The member's role in the organization, or undefined if not found
284
+ */
285
+ async getMemberRoleInOrganization(headers) {
286
+ try {
287
+ const authApi = this.options.auth.api;
288
+ if (typeof authApi.getActiveMemberRole === "function") {
289
+ const result = await authApi.getActiveMemberRole({ headers });
290
+ return result?.role;
291
+ }
292
+ if (typeof authApi.getActiveMember === "function") {
293
+ const member = await authApi.getActiveMember({ headers });
294
+ return member?.role;
295
+ }
296
+ return void 0;
297
+ } catch (error) {
298
+ throw error;
299
+ }
300
+ }
301
+ /**
302
+ * Checks if the user has any of the required roles in user.role only.
303
+ * Used by @Roles() decorator for system-level role checks (admin plugin).
304
+ * @param session - The user's session
305
+ * @param requiredRoles - Array of roles that grant access
306
+ * @returns True if user.role matches any required role
307
+ */
308
+ checkUserRole(session, requiredRoles) {
309
+ return this.matchesRequiredRole(session.user.role, requiredRoles);
310
+ }
311
+ /**
312
+ * Checks if the user has any of the required roles in their organization.
313
+ * Used by @OrgRoles() decorator for organization-level role checks.
314
+ * Requires an active organization in the session.
315
+ * @param session - The user's session
316
+ * @param headers - The request headers for API calls
317
+ * @param requiredRoles - Array of roles that grant access
318
+ * @returns True if org member role matches any required role
319
+ */
320
+ async checkOrgRole(session, headers, requiredRoles) {
321
+ const activeOrgId = session.session?.activeOrganizationId;
322
+ if (!activeOrgId) {
323
+ return false;
324
+ }
325
+ try {
326
+ const memberRole = await this.getMemberRoleInOrganization(headers);
327
+ return this.matchesRequiredRole(memberRole, requiredRoles);
328
+ } catch (error) {
329
+ console.error("Organization plugin error:", error);
330
+ return false;
331
+ }
332
+ }
333
+ };
334
+ exports.AuthGuard = __decorateClass$1([
335
+ common.Injectable(),
336
+ __decorateParam$1(0, common.Inject(core.Reflector)),
337
+ __decorateParam$1(1, common.Inject(MODULE_OPTIONS_TOKEN))
338
+ ], exports.AuthGuard);
339
+
340
+ const rawBodyParser = (req, _res, buffer) => {
341
+ if (Buffer.isBuffer(buffer)) {
342
+ req.rawBody = buffer;
343
+ }
344
+ return true;
345
+ };
346
+ function SkipBodyParsingMiddleware(options = {}) {
347
+ const { basePath = "/api/auth", enableRawBodyParser = false } = options;
348
+ const jsonParserOptions = enableRawBodyParser ? { verify: rawBodyParser } : {};
349
+ return (req, res, next) => {
350
+ if (req.baseUrl.startsWith(basePath)) {
351
+ next();
352
+ return;
353
+ }
354
+ express__namespace.json(jsonParserOptions)(req, res, (err) => {
355
+ if (err) {
356
+ next(err);
357
+ return;
358
+ }
359
+ express__namespace.urlencoded({ extended: true })(req, res, next);
360
+ });
361
+ };
362
+ }
363
+
364
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
365
+ var __decorateClass = (decorators, target, key, kind) => {
366
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
367
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
368
+ if (decorator = decorators[i])
369
+ result = (decorator(result)) || result;
370
+ return result;
371
+ };
372
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
373
+ const HOOKS = [
374
+ { metadataKey: BEFORE_HOOK_KEY, hookType: "before" },
375
+ { metadataKey: AFTER_HOOK_KEY, hookType: "after" }
376
+ ];
377
+ exports.AuthModule = class AuthModule extends ConfigurableModuleClass {
378
+ constructor(applicationConfig, discoveryService, metadataScanner, adapter, options) {
379
+ super();
380
+ this.applicationConfig = applicationConfig;
381
+ this.discoveryService = discoveryService;
382
+ this.metadataScanner = metadataScanner;
383
+ this.adapter = adapter;
384
+ this.options = options;
385
+ this.basePath = shared_utils_js.normalizePath(
386
+ this.options.auth.options.basePath ?? "/api/auth"
387
+ );
388
+ const globalPrefixOptions = this.applicationConfig.getGlobalPrefixOptions();
389
+ this.applicationConfig.setGlobalPrefixOptions({
390
+ exclude: [
391
+ ...globalPrefixOptions.exclude ?? [],
392
+ ...utils_js.mapToExcludeRoute([this.basePath])
393
+ ]
394
+ });
395
+ }
396
+ logger = new common.Logger(exports.AuthModule.name);
397
+ basePath;
398
+ onModuleInit() {
399
+ const providers = this.discoveryService.getProviders().filter(
400
+ ({ metatype }) => metatype && Reflect.getMetadata(HOOK_KEY, metatype)
401
+ );
402
+ const hasHookProviders = providers.length > 0;
403
+ const hooksConfigured = typeof this.options.auth?.options?.hooks === "object";
404
+ if (hasHookProviders && !hooksConfigured)
405
+ throw new Error(
406
+ "Detected @Hook providers but Better Auth 'hooks' are not configured. Add 'hooks: {}' to your betterAuth(...) options."
407
+ );
408
+ if (!hooksConfigured) return;
409
+ for (const provider of providers) {
410
+ const providerPrototype = Object.getPrototypeOf(provider.instance);
411
+ const methods = this.metadataScanner.getAllMethodNames(providerPrototype);
412
+ for (const method of methods) {
413
+ const providerMethod = providerPrototype[method];
414
+ this.setupHooks(providerMethod, provider.instance);
415
+ }
416
+ }
417
+ }
418
+ configure(consumer) {
419
+ const trustedOrigins = this.options.auth.options.trustedOrigins;
420
+ const isNotFunctionBased = trustedOrigins && Array.isArray(trustedOrigins);
421
+ if (!this.options.disableTrustedOriginsCors && isNotFunctionBased) {
422
+ this.adapter.httpAdapter.enableCors({
423
+ origin: trustedOrigins,
424
+ methods: ["GET", "POST", "PUT", "DELETE"],
425
+ credentials: true
426
+ });
427
+ } else if (trustedOrigins && !this.options.disableTrustedOriginsCors && !isNotFunctionBased)
428
+ throw new Error(
429
+ "Function-based trustedOrigins not supported in NestJS. Use string array or disable CORS with disableTrustedOriginsCors: true."
430
+ );
431
+ if (!this.options.disableBodyParser) {
432
+ consumer.apply(
433
+ SkipBodyParsingMiddleware({
434
+ basePath: this.basePath,
435
+ enableRawBodyParser: this.options.enableRawBodyParser
436
+ })
437
+ ).forRoutes("*path");
438
+ }
439
+ const handler = node.toNodeHandler(this.options.auth);
440
+ consumer.apply((req, res) => {
441
+ if (this.options.middleware) {
442
+ return this.options.middleware(req, res, () => handler(req, res));
443
+ }
444
+ return handler(req, res);
445
+ }).forRoutes(this.basePath);
446
+ this.logger.log(`AuthModule initialized BetterAuth on '${this.basePath}'`);
447
+ }
448
+ setupHooks(providerMethod, providerClass) {
449
+ if (!this.options.auth.options.hooks) return;
450
+ for (const { metadataKey, hookType } of HOOKS) {
451
+ const hasHook = Reflect.hasMetadata(metadataKey, providerMethod);
452
+ if (!hasHook) continue;
453
+ const hookPath = Reflect.getMetadata(metadataKey, providerMethod);
454
+ const originalHook = this.options.auth.options.hooks[hookType];
455
+ this.options.auth.options.hooks[hookType] = plugins.createAuthMiddleware(
456
+ async (ctx) => {
457
+ if (originalHook) {
458
+ await originalHook(ctx);
459
+ }
460
+ if (hookPath && hookPath !== ctx.path) return;
461
+ await providerMethod.apply(providerClass, [ctx]);
462
+ }
463
+ );
464
+ }
465
+ }
466
+ static forRootAsync(options) {
467
+ const forRootAsyncResult = super.forRootAsync(options);
468
+ const { module } = forRootAsyncResult;
469
+ return {
470
+ ...forRootAsyncResult,
471
+ module: options.disableControllers ? AuthModuleWithoutControllers : module,
472
+ controllers: options.disableControllers ? [] : forRootAsyncResult.controllers,
473
+ providers: [
474
+ ...forRootAsyncResult.providers ?? [],
475
+ ...!options.disableGlobalAuthGuard ? [
476
+ {
477
+ provide: core.APP_GUARD,
478
+ useClass: exports.AuthGuard
479
+ }
480
+ ] : []
481
+ ]
482
+ };
483
+ }
484
+ static forRoot(arg1, arg2) {
485
+ const normalizedOptions = typeof arg1 === "object" && arg1 !== null && "auth" in arg1 ? arg1 : { ...arg2 ?? {}, auth: arg1 };
486
+ const forRootResult = super.forRoot(normalizedOptions);
487
+ const { module } = forRootResult;
488
+ return {
489
+ ...forRootResult,
490
+ module: normalizedOptions.disableControllers ? AuthModuleWithoutControllers : module,
491
+ controllers: normalizedOptions.disableControllers ? [] : forRootResult.controllers,
492
+ providers: [
493
+ ...forRootResult.providers ?? [],
494
+ ...!normalizedOptions.disableGlobalAuthGuard ? [
495
+ {
496
+ provide: core.APP_GUARD,
497
+ useClass: exports.AuthGuard
498
+ }
499
+ ] : []
500
+ ]
501
+ };
502
+ }
503
+ };
504
+ exports.AuthModule = __decorateClass([
505
+ common.Module({
506
+ imports: [core.DiscoveryModule],
507
+ providers: [exports.AuthService],
508
+ exports: [exports.AuthService]
509
+ }),
510
+ __decorateParam(0, common.Inject(core.ApplicationConfig)),
511
+ __decorateParam(1, common.Inject(core.DiscoveryService)),
512
+ __decorateParam(2, common.Inject(core.MetadataScanner)),
513
+ __decorateParam(3, common.Inject(core.HttpAdapterHost)),
514
+ __decorateParam(4, common.Inject(MODULE_OPTIONS_TOKEN))
515
+ ], exports.AuthModule);
516
+ class AuthModuleWithoutControllers extends exports.AuthModule {
517
+ configure() {
518
+ return;
519
+ }
520
+ }
521
+
522
+ exports.AFTER_HOOK_KEY = AFTER_HOOK_KEY;
523
+ exports.AUTH_MODULE_OPTIONS_KEY = AUTH_MODULE_OPTIONS_KEY;
524
+ exports.AfterHook = AfterHook;
525
+ exports.AllowAnonymous = AllowAnonymous;
526
+ exports.BEFORE_HOOK_KEY = BEFORE_HOOK_KEY;
527
+ exports.BeforeHook = BeforeHook;
528
+ exports.HOOK_KEY = HOOK_KEY;
529
+ exports.Hook = Hook;
530
+ exports.Optional = Optional;
531
+ exports.OptionalAuth = OptionalAuth;
532
+ exports.OrgRoles = OrgRoles;
533
+ exports.Public = Public;
534
+ exports.Roles = Roles;
535
+ exports.Session = Session;