@delmaredigital/payload-better-auth 0.6.1 → 0.6.4

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.
@@ -262,8 +262,15 @@
262
262
  }
263
263
  // Get Payload collection slug from model name
264
264
  // Uses factory's getModelName which respects BetterAuthOptions.modelName config
265
+ // Fix: Better Auth's getModelName blindly appends 's' even when name already ends in 's'
266
+ // (e.g., 'jwks' becomes 'jwkss'). We normalize to prevent double-s slugs.
265
267
  const getCollection = (model)=>{
266
- return getModelName(model);
268
+ const name = getModelName(model);
269
+ // Fix double-s from naive pluralization (e.g., jwkss → jwks)
270
+ if (name.endsWith('ss') && !model.endsWith('ss')) {
271
+ return name.slice(0, -1);
272
+ }
273
+ return name;
267
274
  };
268
275
  // The CustomAdapter interface uses generics (T) for return types.
269
276
  // Payload returns concrete types (JsonObject & TypeWithID).
@@ -481,6 +481,12 @@ let apiKeyPermissionsConfig = undefined;
481
481
  console.error('[better-auth] Failed to create auth:', error);
482
482
  throw error;
483
483
  }
484
+ // Warn if nextCookies() plugin is detected — it's incompatible with Payload CMS.
485
+ // Check via betterAuthOptions (if provided) or the auth instance's options.
486
+ const pluginsToCheck = options.admin?.betterAuthOptions?.plugins ?? authInstance.options?.plugins;
487
+ if (pluginsToCheck?.some((p)=>p.id === 'next-cookies')) {
488
+ console.warn('\n⚠️ [payload-better-auth] The nextCookies() plugin was detected in your Better Auth config.\n' + ' This plugin is INCOMPATIBLE with Payload CMS and will cause infinite form-state\n' + ' submissions and input resets in the admin panel.\n\n' + ' The nextCookies() plugin is designed for Server Actions, but payload-better-auth\n' + ' handles cookie passthrough automatically via its endpoint proxy.\n\n' + ' → Remove nextCookies() from your Better Auth plugins to fix this issue.\n' + ' → See: https://github.com/delmaredigital/payload-better-auth/issues/15\n');
489
+ }
484
490
  }
485
491
  // Attach to payload for global access
486
492
  Object.defineProperty(payload, 'betterAuth', {
@@ -546,6 +552,90 @@ let apiKeyPermissionsConfig = undefined;
546
552
  headers
547
553
  });
548
554
  if (!sessionData?.user?.id) {
555
+ // No session found — check for OAuth JWT Bearer token
556
+ const authHeader = headers.get('authorization');
557
+ if (authHeader?.startsWith('Bearer ')) {
558
+ const token = authHeader.slice(7);
559
+ // Try OAuth JWT verification via the oauth-provider's verifyAccessToken
560
+ try {
561
+ const { verifyAccessToken } = await import('better-auth/oauth2');
562
+ const baseURL = auth.options?.baseURL;
563
+ if (!baseURL) throw new Error('baseURL not configured');
564
+ const jwtPayload = await verifyAccessToken(token, {
565
+ jwksUrl: `${baseURL}/api/auth/jwks`,
566
+ verifyOptions: {
567
+ issuer: baseURL,
568
+ audience: baseURL
569
+ }
570
+ });
571
+ if (jwtPayload?.sub) {
572
+ const users = await payload.find({
573
+ collection: usersCollection,
574
+ where: {
575
+ id: {
576
+ equals: jwtPayload.sub
577
+ }
578
+ },
579
+ limit: 1,
580
+ depth: 0
581
+ });
582
+ if (users.docs.length > 0) {
583
+ // Extract org context and scopes from JWT claims
584
+ const oauthOrgId = jwtPayload.organizationId;
585
+ const oauthScopes = typeof jwtPayload.scope === 'string' ? jwtPayload.scope.split(' ') : Array.isArray(jwtPayload.scope) ? jwtPayload.scope : [];
586
+ // Look up org role if orgId is present
587
+ let orgRole;
588
+ if (oauthOrgId) {
589
+ try {
590
+ const memberships = await payload.find({
591
+ collection: membersCollection,
592
+ where: {
593
+ and: [
594
+ {
595
+ user: {
596
+ equals: jwtPayload.sub
597
+ }
598
+ },
599
+ {
600
+ organization: {
601
+ equals: oauthOrgId
602
+ }
603
+ }
604
+ ]
605
+ },
606
+ limit: 1,
607
+ depth: 0
608
+ });
609
+ if (memberships.docs.length > 0) {
610
+ orgRole = memberships.docs[0].role;
611
+ }
612
+ } catch {
613
+ // Members collection might not exist
614
+ }
615
+ }
616
+ const userDoc = users.docs[0];
617
+ const oauthUser = {
618
+ ...userDoc,
619
+ id: userDoc.id,
620
+ oauthScopes,
621
+ collection: usersCollection,
622
+ _strategy: 'better-auth',
623
+ ...oauthOrgId ? {
624
+ activeOrganizationId: oauthOrgId
625
+ } : {},
626
+ ...orgRole ? {
627
+ organizationRole: orgRole
628
+ } : {}
629
+ };
630
+ return {
631
+ user: oauthUser
632
+ };
633
+ }
634
+ }
635
+ } catch {
636
+ // JWT verification failed — not a valid OAuth token, continue to return null
637
+ }
638
+ }
549
639
  return {
550
640
  user: null
551
641
  };
@@ -10,6 +10,7 @@ export type EnabledPluginsResult = {
10
10
  hasMagicLink: boolean;
11
11
  hasMultiSession: boolean;
12
12
  hasOrganization: boolean;
13
+ hasNextCookies: boolean;
13
14
  };
14
15
  /**
15
16
  * Detects which Better Auth plugins are enabled from the options.
@@ -15,7 +15,8 @@
15
15
  hasPasskey: false,
16
16
  hasMagicLink: false,
17
17
  hasMultiSession: false,
18
- hasOrganization: false
18
+ hasOrganization: false,
19
+ hasNextCookies: false
19
20
  };
20
21
  for (const plugin of plugins){
21
22
  // Better Auth plugins have an id property
@@ -42,6 +43,9 @@
42
43
  case 'organization':
43
44
  result.hasOrganization = true;
44
45
  break;
46
+ case 'next-cookies':
47
+ result.hasNextCookies = true;
48
+ break;
45
49
  }
46
50
  }
47
51
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delmaredigital/payload-better-auth",
3
- "version": "0.6.1",
3
+ "version": "0.6.4",
4
4
  "description": "Better Auth adapter and plugins for Payload CMS",
5
5
  "type": "module",
6
6
  "license": "MIT",