@delmaredigital/payload-better-auth 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -545,12 +545,14 @@ Payload auth strategy for Better Auth session validation.
545
545
  ```ts
546
546
  betterAuthStrategy({
547
547
  usersCollection: 'users',
548
+ idType: 'number', // default — coerces session field IDs for serial IDs
548
549
  })
549
550
  ```
550
551
 
551
552
  | Option | Type | Description |
552
553
  |--------|------|-------------|
553
554
  | `usersCollection` | `string` | The collection slug for users (default: `'users'`) |
555
+ | `idType` | `'number' \| 'text'` | Coerces string IDs in session fields (`activeOrganizationId`, etc.) to numbers. Defaults to `'number'` matching the adapter default. Set to `'text'` for UUID IDs. |
554
556
 
555
557
  ### `getServerSession<TUser>(payload, headers)`
556
558
 
@@ -576,7 +578,7 @@ const user = await getServerUser<User>(payload, headersList)
576
578
  // user.role, user.firstName, etc. are fully typed
577
579
  ```
578
580
 
579
- ### `createSessionHelpers<TUser>()`
581
+ ### `createSessionHelpers<TUser>(options?)`
580
582
 
581
583
  Create typed session helpers bound to your User type. Define once, import everywhere — no generics needed at call sites:
582
584
 
@@ -596,6 +598,18 @@ const session = await getServerSession(payload, headersList)
596
598
  // session.user is typed as User — no generic needed
597
599
  ```
598
600
 
601
+ **Serial IDs (Payload default):** Better Auth always returns string IDs from `api.getSession()`, which causes Payload relationship fields to reject them. Pass `idType: 'number'` to coerce ID fields to numbers automatically:
602
+
603
+ ```ts
604
+ export const { getServerSession, getServerUser } = createSessionHelpers<User>({
605
+ idType: 'number', // coerces user.id, session.userId, etc. to numbers
606
+ })
607
+ ```
608
+
609
+ | Option | Type | Description |
610
+ |--------|------|-------------|
611
+ | `idType` | `'number' \| 'text'` | Set to `'number'` when using serial IDs to coerce string IDs to numbers. Matches the adapter's `adapterConfig.idType` option. |
612
+
599
613
  ### `withBetterAuthDefaults(options)`
600
614
 
601
615
  Applies sensible defaults to Better Auth options. Useful for simplifying common configurations.
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export type { ApiKeyInfo, ApiKeyAccessConfig, } from './utils/apiKeyAccess.js';
23
23
  export { detectAuthConfig } from './utils/detectAuthConfig.js';
24
24
  export type { AuthDetectionResult } from './utils/detectAuthConfig.js';
25
25
  export { getServerSession, getServerUser, createSessionHelpers } from './utils/session.js';
26
- export type { Session } from './utils/session.js';
26
+ export type { Session, SessionHelperOptions } from './utils/session.js';
27
27
  export { firstUserAdminHooks } from './utils/firstUserAdmin.js';
28
28
  export type { FirstUserAdminOptions } from './utils/firstUserAdmin.js';
29
29
  export { withBetterAuthDefaults, apiKeyWithDefaults } from './utils/betterAuthDefaults.js';
@@ -178,6 +178,17 @@ export type BetterAuthStrategyOptions = {
178
178
  * @default 'members'
179
179
  */
180
180
  membersCollection?: string;
181
+ /**
182
+ * ID type strategy matching your adapter's `adapterConfig.idType`.
183
+ *
184
+ * When `'number'` (default), coerces string IDs in session fields
185
+ * (e.g., `activeOrganizationId`) to numbers before merging onto `req.user`.
186
+ * Better Auth always returns string IDs from `api.getSession()`, but Payload
187
+ * relationship fields expect numbers when using serial IDs.
188
+ *
189
+ * @default 'number'
190
+ */
191
+ idType?: 'number' | 'text';
181
192
  };
182
193
  /**
183
194
  * Payload auth strategy that uses Better Auth for authentication.
@@ -459,7 +459,7 @@ let apiKeyScopesConfig = undefined;
459
459
  * }
460
460
  * ```
461
461
  */ export function betterAuthStrategy(options = {}) {
462
- const { usersCollection = 'users', membersCollection = 'members' } = options;
462
+ const { usersCollection = 'users', membersCollection = 'members', idType = 'number' } = options;
463
463
  return {
464
464
  name: 'better-auth',
465
465
  authenticate: async ({ payload, headers })=>{
@@ -498,6 +498,19 @@ let apiKeyScopesConfig = undefined;
498
498
  // Extract session fields to merge onto user (e.g., activeOrganizationId from org plugin)
499
499
  // Exclude fields that might conflict with user fields
500
500
  const { id: _sessionId, userId: _userId, expiresAt: _expiresAt, token: _token, ...sessionFields } = sessionData.session || {};
501
+ // Coerce string IDs in session fields to numbers when using serial IDs.
502
+ // BA's api.getSession() always returns strings, but Payload relationship
503
+ // fields expect numbers for serial IDs.
504
+ if (idType === 'number') {
505
+ for (const [key, value] of Object.entries(sessionFields)){
506
+ if (typeof value !== 'string') continue;
507
+ if (key === 'id' || /(?:Id|_id)$/.test(key)) {
508
+ if (/^\d+$/.test(value)) {
509
+ sessionFields[key] = parseInt(value, 10);
510
+ }
511
+ }
512
+ }
513
+ }
501
514
  // If there's an active organization, fetch the user's role in that org
502
515
  let organizationRole;
503
516
  if (sessionFields.activeOrganizationId) {
@@ -69,6 +69,19 @@ export declare function getServerSession<TUser = DefaultUser>(payload: BasePaylo
69
69
  * ```
70
70
  */
71
71
  export declare function getServerUser<TUser = DefaultUser>(payload: BasePayload, headers: Headers): Promise<TUser | null>;
72
+ export type SessionHelperOptions = {
73
+ /**
74
+ * ID type strategy matching your adapter's `adapterConfig.idType`.
75
+ *
76
+ * Set to `'number'` when using Payload's default serial IDs.
77
+ * Better Auth always returns string IDs from `api.getSession()` —
78
+ * this option coerces `id` and `*Id` / `*_id` fields to numbers
79
+ * so they work directly in Payload relationship fields.
80
+ *
81
+ * @default undefined (no coercion)
82
+ */
83
+ idType?: 'number' | 'text';
84
+ };
72
85
  /**
73
86
  * Create typed session helpers bound to your User type.
74
87
  *
@@ -84,15 +97,24 @@ export declare function getServerUser<TUser = DefaultUser>(payload: BasePayload,
84
97
  * export const { getServerSession, getServerUser } = createSessionHelpers<User>()
85
98
  * ```
86
99
  *
100
+ * @example
101
+ * ```ts
102
+ * // With serial IDs (Payload default) — coerces string IDs to numbers
103
+ * export const { getServerSession, getServerUser } = createSessionHelpers<User>({
104
+ * idType: 'number',
105
+ * })
106
+ * ```
107
+ *
87
108
  * ```ts
88
109
  * // app/page.tsx
89
110
  * import { getServerSession } from '@/lib/auth'
90
111
  *
91
112
  * const session = await getServerSession(payload, headersList)
92
113
  * // session.user is typed as User — no generic needed
114
+ * // session.user.id is a number when idType: 'number'
93
115
  * ```
94
116
  */
95
- export declare function createSessionHelpers<TUser = DefaultUser>(): {
117
+ export declare function createSessionHelpers<TUser = DefaultUser>(options?: SessionHelperOptions): {
96
118
  getServerSession: (payload: BasePayload, headers: Headers) => Promise<Session<TUser> | null>;
97
119
  getServerUser: (payload: BasePayload, headers: Headers) => Promise<TUser | null>;
98
120
  };
@@ -68,6 +68,24 @@
68
68
  const session = await getServerSession(payload, headers);
69
69
  return session?.user ?? null;
70
70
  }
71
+ /**
72
+ * Coerce numeric-string ID fields to numbers on a shallow object.
73
+ * Matches the adapter's heuristic: `id`, fields ending in `Id` or `_id`.
74
+ */ function coerceIds(obj) {
75
+ if (!obj || typeof obj !== 'object') return obj;
76
+ const result = {
77
+ ...obj
78
+ };
79
+ for (const [key, value] of Object.entries(result)){
80
+ if (typeof value !== 'string') continue;
81
+ if (key === 'id' || /(?:Id|_id)$/.test(key)) {
82
+ if (/^\d+$/.test(value)) {
83
+ result[key] = parseInt(value, 10);
84
+ }
85
+ }
86
+ }
87
+ return result;
88
+ }
71
89
  /**
72
90
  * Create typed session helpers bound to your User type.
73
91
  *
@@ -83,16 +101,37 @@
83
101
  * export const { getServerSession, getServerUser } = createSessionHelpers<User>()
84
102
  * ```
85
103
  *
104
+ * @example
105
+ * ```ts
106
+ * // With serial IDs (Payload default) — coerces string IDs to numbers
107
+ * export const { getServerSession, getServerUser } = createSessionHelpers<User>({
108
+ * idType: 'number',
109
+ * })
110
+ * ```
111
+ *
86
112
  * ```ts
87
113
  * // app/page.tsx
88
114
  * import { getServerSession } from '@/lib/auth'
89
115
  *
90
116
  * const session = await getServerSession(payload, headersList)
91
117
  * // session.user is typed as User — no generic needed
118
+ * // session.user.id is a number when idType: 'number'
92
119
  * ```
93
- */ export function createSessionHelpers() {
120
+ */ export function createSessionHelpers(options) {
121
+ const shouldCoerceIds = options?.idType === 'number';
122
+ const typedGetServerSession = async (payload, headers)=>{
123
+ const session = await getServerSession(payload, headers);
124
+ if (!session || !shouldCoerceIds) return session;
125
+ return {
126
+ user: coerceIds(session.user),
127
+ session: coerceIds(session.session)
128
+ };
129
+ };
94
130
  return {
95
- getServerSession: (payload, headers)=>getServerSession(payload, headers),
96
- getServerUser: (payload, headers)=>getServerUser(payload, headers)
131
+ getServerSession: typedGetServerSession,
132
+ getServerUser: async (payload, headers)=>{
133
+ const session = await typedGetServerSession(payload, headers);
134
+ return session?.user ?? null;
135
+ }
97
136
  };
98
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delmaredigital/payload-better-auth",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "Better Auth adapter and plugins for Payload CMS",
5
5
  "type": "module",
6
6
  "license": "MIT",