@delmaredigital/payload-better-auth 0.3.11 → 0.3.13

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
@@ -552,22 +552,48 @@ betterAuthStrategy({
552
552
  |--------|------|-------------|
553
553
  | `usersCollection` | `string` | The collection slug for users (default: `'users'`) |
554
554
 
555
- ### `getServerSession(payload, headers)`
555
+ ### `getServerSession<TUser>(payload, headers)`
556
556
 
557
- Get the current session on the server.
557
+ Get the current session on the server. Pass your Payload `User` type for full type safety:
558
558
 
559
559
  ```ts
560
- const session = await getServerSession(payload, headersList)
561
- // Returns: { user: { id, email, name, ... }, session: { id, expiresAt, ... } } | null
560
+ import { getServerSession } from '@delmaredigital/payload-better-auth'
561
+ import type { User } from '@/payload-types'
562
+
563
+ const session = await getServerSession<User>(payload, headersList)
564
+ // session.user.role, session.user.firstName, etc. are fully typed
562
565
  ```
563
566
 
564
- ### `getServerUser(payload, headers)`
567
+ ### `getServerUser<TUser>(payload, headers)`
565
568
 
566
- Get the current user on the server (shorthand for `session.user`).
569
+ Get the current user on the server (shorthand for `session.user`):
567
570
 
568
571
  ```ts
569
- const user = await getServerUser(payload, headersList)
570
- // Returns: { id, email, name, ... } | null
572
+ import { getServerUser } from '@delmaredigital/payload-better-auth'
573
+ import type { User } from '@/payload-types'
574
+
575
+ const user = await getServerUser<User>(payload, headersList)
576
+ // user.role, user.firstName, etc. are fully typed
577
+ ```
578
+
579
+ ### `createSessionHelpers<TUser>()`
580
+
581
+ Create typed session helpers bound to your User type. Define once, import everywhere — no generics needed at call sites:
582
+
583
+ ```ts
584
+ // lib/auth.ts
585
+ import { createSessionHelpers } from '@delmaredigital/payload-better-auth'
586
+ import type { User } from '@/payload-types'
587
+
588
+ export const { getServerSession, getServerUser } = createSessionHelpers<User>()
589
+ ```
590
+
591
+ ```ts
592
+ // app/page.tsx
593
+ import { getServerSession } from '@/lib/auth'
594
+
595
+ const session = await getServerSession(payload, headersList)
596
+ // session.user is typed as User — no generic needed
571
597
  ```
572
598
 
573
599
  ### `withBetterAuthDefaults(options)`
package/dist/index.d.ts CHANGED
@@ -22,7 +22,7 @@ export { extractApiKeyFromRequest, getApiKeyInfo, hasScope, hasAnyScope as hasAn
22
22
  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
- export { getServerSession, getServerUser } from './utils/session.js';
25
+ export { getServerSession, getServerUser, createSessionHelpers } from './utils/session.js';
26
26
  export type { Session } from './utils/session.js';
27
27
  export { firstUserAdminHooks } from './utils/firstUserAdmin.js';
28
28
  export type { FirstUserAdminOptions } from './utils/firstUserAdmin.js';
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ export { extractApiKeyFromRequest, getApiKeyInfo, hasScope, hasAnyScope as hasAn
20
20
  // Auth config detection utility
21
21
  export { detectAuthConfig } from './utils/detectAuthConfig.js';
22
22
  // Session utilities
23
- export { getServerSession, getServerUser } from './utils/session.js';
23
+ export { getServerSession, getServerUser, createSessionHelpers } from './utils/session.js';
24
24
  // First user admin hook utility
25
25
  export { firstUserAdminHooks } from './utils/firstUserAdmin.js';
26
26
  // Better Auth defaults utility
@@ -19,9 +19,16 @@
19
19
  * },
20
20
  * }
21
21
  * ```
22
- */ // ─────────────────────────────────────────────────────────────────────────────
22
+ */ import { createHash } from 'node:crypto';
23
+ // ─────────────────────────────────────────────────────────────────────────────
23
24
  // Helpers
24
25
  // ─────────────────────────────────────────────────────────────────────────────
26
+ /**
27
+ * Hash an API key using the same algorithm as Better Auth's defaultKeyHasher.
28
+ * Produces a SHA-256 hash encoded as Base64URL (no padding).
29
+ */ function hashApiKey(key) {
30
+ return createHash('sha256').update(key).digest('base64url');
31
+ }
25
32
  /**
26
33
  * Extract API key from request headers.
27
34
  * Supports Bearer token format: Authorization: Bearer <api-key>
@@ -40,16 +47,37 @@
40
47
  * Returns null if key not found or disabled.
41
48
  */ export async function getApiKeyInfo(req, apiKey, apiKeysCollection = 'apiKeys') {
42
49
  try {
50
+ // Hash the raw API key to match Better Auth's storage format (SHA-256 + Base64URL).
51
+ // We query for both the hashed and raw key to support both modes:
52
+ // - Hashing enabled (default): only the hashed query matches
53
+ // - Hashing disabled (disableKeyHashing: true): only the plaintext query matches
54
+ const hashedKey = hashApiKey(apiKey);
43
55
  // Try the provided collection name first
44
56
  let results = await req.payload.find({
45
57
  collection: apiKeysCollection,
58
+ overrideAccess: true,
46
59
  where: {
47
- key: {
48
- equals: apiKey
49
- },
50
- enabled: {
51
- not_equals: false
52
- }
60
+ and: [
61
+ {
62
+ enabled: {
63
+ not_equals: false
64
+ }
65
+ },
66
+ {
67
+ or: [
68
+ {
69
+ key: {
70
+ equals: hashedKey
71
+ }
72
+ },
73
+ {
74
+ key: {
75
+ equals: apiKey
76
+ }
77
+ }
78
+ ]
79
+ }
80
+ ]
53
81
  },
54
82
  limit: 1,
55
83
  depth: 0
@@ -59,13 +87,29 @@
59
87
  const altSlug = apiKeysCollection === 'apiKeys' ? 'api-keys' : 'apiKeys';
60
88
  results = await req.payload.find({
61
89
  collection: altSlug,
90
+ overrideAccess: true,
62
91
  where: {
63
- key: {
64
- equals: apiKey
65
- },
66
- enabled: {
67
- not_equals: false
68
- }
92
+ and: [
93
+ {
94
+ enabled: {
95
+ not_equals: false
96
+ }
97
+ },
98
+ {
99
+ or: [
100
+ {
101
+ key: {
102
+ equals: hashedKey
103
+ }
104
+ },
105
+ {
106
+ key: {
107
+ equals: apiKey
108
+ }
109
+ }
110
+ ]
111
+ }
112
+ ]
69
113
  },
70
114
  limit: 1,
71
115
  depth: 0
@@ -83,8 +127,11 @@
83
127
  if (Array.isArray(parsed)) {
84
128
  scopes = parsed;
85
129
  } else if (typeof parsed === 'object') {
86
- // If it's an object, extract keys or flatten
87
- scopes = Object.keys(parsed);
130
+ // Flatten Better Auth permissions format {"resource": ["action1", "action2"]}
131
+ // into scope strings like ["resource:action1", "resource:action2"]
132
+ scopes = Object.entries(parsed).flatMap(([resource, actions])=>Array.isArray(actions) ? actions.map((action)=>`${resource}:${action}`) : [
133
+ resource
134
+ ]);
88
135
  }
89
136
  } catch {
90
137
  // If not JSON, treat as comma-separated
@@ -115,6 +162,13 @@
115
162
  metadata = doc.metadata;
116
163
  }
117
164
  }
165
+ // Prefer scope names from metadata (stored by admin UI as original scope strings
166
+ // like ["pages:read", "*"]) over permissions-derived scopes, because the permissions
167
+ // field uses Better Auth's internal format (e.g. {"pages": {"$": ["read"]}}) and
168
+ // Object.keys() on that only yields collection names, not proper scope strings.
169
+ if (metadata?.scopes && Array.isArray(metadata.scopes)) {
170
+ scopes = metadata.scopes;
171
+ }
118
172
  return {
119
173
  id: String(doc.id),
120
174
  userId,
@@ -4,14 +4,15 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  import type { BasePayload } from 'payload';
7
- export type Session = {
8
- user: {
9
- id: string;
10
- email: string;
11
- name?: string;
12
- image?: string;
13
- [key: string]: unknown;
14
- };
7
+ type DefaultUser = {
8
+ id: string;
9
+ email: string;
10
+ name?: string;
11
+ image?: string;
12
+ [key: string]: unknown;
13
+ };
14
+ export type Session<TUser = DefaultUser> = {
15
+ user: TUser;
15
16
  session: {
16
17
  id: string;
17
18
  expiresAt: Date;
@@ -21,35 +22,43 @@ export type Session = {
21
22
  /**
22
23
  * Get the current session from headers.
23
24
  *
25
+ * Accepts an optional generic type parameter to narrow the user type.
26
+ * Pass your Payload-generated `User` type for full type safety.
27
+ *
24
28
  * @example
25
29
  * ```ts
26
30
  * import { headers } from 'next/headers'
27
- * import { getServerSession } from '@delmare/payload-better-auth'
31
+ * import { getServerSession } from '@delmaredigital/payload-better-auth'
32
+ * import type { User } from '@/payload-types'
28
33
  *
29
34
  * export default async function Page() {
30
35
  * const headersList = await headers()
31
- * const session = await getServerSession(payload, headersList)
36
+ * const session = await getServerSession<User>(payload, headersList)
32
37
  *
33
38
  * if (!session) {
34
39
  * redirect('/login')
35
40
  * }
36
41
  *
42
+ * // session.user.role is fully typed
37
43
  * return <div>Hello {session.user.name}</div>
38
44
  * }
39
45
  * ```
40
46
  */
41
- export declare function getServerSession(payload: BasePayload, headers: Headers): Promise<Session | null>;
47
+ export declare function getServerSession<TUser = DefaultUser>(payload: BasePayload, headers: Headers): Promise<Session<TUser> | null>;
42
48
  /**
43
49
  * Get the current user from the session.
44
50
  *
51
+ * Accepts an optional generic type parameter to narrow the user type.
52
+ *
45
53
  * @example
46
54
  * ```ts
47
55
  * import { headers } from 'next/headers'
48
- * import { getServerUser } from '@delmare/payload-better-auth'
56
+ * import { getServerUser } from '@delmaredigital/payload-better-auth'
57
+ * import type { User } from '@/payload-types'
49
58
  *
50
59
  * export default async function Page() {
51
60
  * const headersList = await headers()
52
- * const user = await getServerUser(payload, headersList)
61
+ * const user = await getServerUser<User>(payload, headersList)
53
62
  *
54
63
  * if (!user) {
55
64
  * redirect('/login')
@@ -59,4 +68,32 @@ export declare function getServerSession(payload: BasePayload, headers: Headers)
59
68
  * }
60
69
  * ```
61
70
  */
62
- export declare function getServerUser(payload: BasePayload, headers: Headers): Promise<Session['user'] | null>;
71
+ export declare function getServerUser<TUser = DefaultUser>(payload: BasePayload, headers: Headers): Promise<TUser | null>;
72
+ /**
73
+ * Create typed session helpers bound to your User type.
74
+ *
75
+ * Define once in a shared file, then import the typed helpers
76
+ * everywhere — no generics needed at call sites.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // lib/auth.ts
81
+ * import { createSessionHelpers } from '@delmaredigital/payload-better-auth'
82
+ * import type { User } from '@/payload-types'
83
+ *
84
+ * export const { getServerSession, getServerUser } = createSessionHelpers<User>()
85
+ * ```
86
+ *
87
+ * ```ts
88
+ * // app/page.tsx
89
+ * import { getServerSession } from '@/lib/auth'
90
+ *
91
+ * const session = await getServerSession(payload, headersList)
92
+ * // session.user is typed as User — no generic needed
93
+ * ```
94
+ */
95
+ export declare function createSessionHelpers<TUser = DefaultUser>(): {
96
+ getServerSession: (payload: BasePayload, headers: Headers) => Promise<Session<TUser> | null>;
97
+ getServerUser: (payload: BasePayload, headers: Headers) => Promise<TUser | null>;
98
+ };
99
+ export {};
@@ -5,19 +5,24 @@
5
5
  */ /**
6
6
  * Get the current session from headers.
7
7
  *
8
+ * Accepts an optional generic type parameter to narrow the user type.
9
+ * Pass your Payload-generated `User` type for full type safety.
10
+ *
8
11
  * @example
9
12
  * ```ts
10
13
  * import { headers } from 'next/headers'
11
- * import { getServerSession } from '@delmare/payload-better-auth'
14
+ * import { getServerSession } from '@delmaredigital/payload-better-auth'
15
+ * import type { User } from '@/payload-types'
12
16
  *
13
17
  * export default async function Page() {
14
18
  * const headersList = await headers()
15
- * const session = await getServerSession(payload, headersList)
19
+ * const session = await getServerSession<User>(payload, headersList)
16
20
  *
17
21
  * if (!session) {
18
22
  * redirect('/login')
19
23
  * }
20
24
  *
25
+ * // session.user.role is fully typed
21
26
  * return <div>Hello {session.user.name}</div>
22
27
  * }
23
28
  * ```
@@ -40,14 +45,17 @@
40
45
  /**
41
46
  * Get the current user from the session.
42
47
  *
48
+ * Accepts an optional generic type parameter to narrow the user type.
49
+ *
43
50
  * @example
44
51
  * ```ts
45
52
  * import { headers } from 'next/headers'
46
- * import { getServerUser } from '@delmare/payload-better-auth'
53
+ * import { getServerUser } from '@delmaredigital/payload-better-auth'
54
+ * import type { User } from '@/payload-types'
47
55
  *
48
56
  * export default async function Page() {
49
57
  * const headersList = await headers()
50
- * const user = await getServerUser(payload, headersList)
58
+ * const user = await getServerUser<User>(payload, headersList)
51
59
  *
52
60
  * if (!user) {
53
61
  * redirect('/login')
@@ -60,3 +68,31 @@
60
68
  const session = await getServerSession(payload, headers);
61
69
  return session?.user ?? null;
62
70
  }
71
+ /**
72
+ * Create typed session helpers bound to your User type.
73
+ *
74
+ * Define once in a shared file, then import the typed helpers
75
+ * everywhere — no generics needed at call sites.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * // lib/auth.ts
80
+ * import { createSessionHelpers } from '@delmaredigital/payload-better-auth'
81
+ * import type { User } from '@/payload-types'
82
+ *
83
+ * export const { getServerSession, getServerUser } = createSessionHelpers<User>()
84
+ * ```
85
+ *
86
+ * ```ts
87
+ * // app/page.tsx
88
+ * import { getServerSession } from '@/lib/auth'
89
+ *
90
+ * const session = await getServerSession(payload, headersList)
91
+ * // session.user is typed as User — no generic needed
92
+ * ```
93
+ */ export function createSessionHelpers() {
94
+ return {
95
+ getServerSession: (payload, headers)=>getServerSession(payload, headers),
96
+ getServerUser: (payload, headers)=>getServerUser(payload, headers)
97
+ };
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delmaredigital/payload-better-auth",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "Better Auth adapter and plugins for Payload CMS",
5
5
  "type": "module",
6
6
  "license": "MIT",