@delmaredigital/payload-better-auth 0.3.6 → 0.3.8

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.
Files changed (164) hide show
  1. package/README.md +60 -12
  2. package/dist/adapter/collections.d.ts.map +1 -1
  3. package/dist/adapter/collections.js +126 -88
  4. package/dist/adapter/collections.js.map +1 -1
  5. package/dist/adapter/index.js +197 -150
  6. package/dist/adapter/index.js.map +1 -1
  7. package/dist/components/BeforeLogin.d.ts +1 -1
  8. package/dist/components/BeforeLogin.d.ts.map +1 -1
  9. package/dist/components/BeforeLogin.js +15 -7
  10. package/dist/components/BeforeLogin.js.map +1 -1
  11. package/dist/components/LoginView.d.ts +2 -2
  12. package/dist/components/LoginView.d.ts.map +1 -1
  13. package/dist/components/LoginView.js +660 -218
  14. package/dist/components/LoginView.js.map +1 -1
  15. package/dist/components/LoginViewWrapper.d.ts +1 -1
  16. package/dist/components/LoginViewWrapper.d.ts.map +1 -1
  17. package/dist/components/LoginViewWrapper.js +14 -4
  18. package/dist/components/LoginViewWrapper.js.map +1 -1
  19. package/dist/components/LogoutButton.d.ts +1 -1
  20. package/dist/components/LogoutButton.d.ts.map +1 -1
  21. package/dist/components/LogoutButton.js +19 -11
  22. package/dist/components/LogoutButton.js.map +1 -1
  23. package/dist/components/PasskeyRegisterButton.d.ts +2 -2
  24. package/dist/components/PasskeyRegisterButton.d.ts.map +1 -1
  25. package/dist/components/PasskeyRegisterButton.js +20 -16
  26. package/dist/components/PasskeyRegisterButton.js.map +1 -1
  27. package/dist/components/PasskeySignInButton.d.ts +2 -2
  28. package/dist/components/PasskeySignInButton.d.ts.map +1 -1
  29. package/dist/components/PasskeySignInButton.js +14 -12
  30. package/dist/components/PasskeySignInButton.js.map +1 -1
  31. package/dist/components/auth/ForgotPasswordView.d.ts +1 -1
  32. package/dist/components/auth/ForgotPasswordView.d.ts.map +1 -1
  33. package/dist/components/auth/ForgotPasswordView.js +133 -43
  34. package/dist/components/auth/ForgotPasswordView.js.map +1 -1
  35. package/dist/components/auth/ResetPasswordView.d.ts +1 -1
  36. package/dist/components/auth/ResetPasswordView.d.ts.map +1 -1
  37. package/dist/components/auth/ResetPasswordView.js +154 -50
  38. package/dist/components/auth/ResetPasswordView.js.map +1 -1
  39. package/dist/components/auth/index.js +2 -2
  40. package/dist/components/auth/index.js.map +1 -1
  41. package/dist/components/management/ApiKeysManagementClient.d.ts +2 -2
  42. package/dist/components/management/ApiKeysManagementClient.d.ts.map +1 -1
  43. package/dist/components/management/ApiKeysManagementClient.js +539 -222
  44. package/dist/components/management/ApiKeysManagementClient.js.map +1 -1
  45. package/dist/components/management/PasskeysManagementClient.d.ts +2 -2
  46. package/dist/components/management/PasskeysManagementClient.d.ts.map +1 -1
  47. package/dist/components/management/PasskeysManagementClient.js +215 -92
  48. package/dist/components/management/PasskeysManagementClient.js.map +1 -1
  49. package/dist/components/management/SecurityNavLinks.d.ts +1 -1
  50. package/dist/components/management/SecurityNavLinks.d.ts.map +1 -1
  51. package/dist/components/management/SecurityNavLinks.js +51 -24
  52. package/dist/components/management/SecurityNavLinks.js.map +1 -1
  53. package/dist/components/management/TwoFactorManagementClient.d.ts +2 -2
  54. package/dist/components/management/TwoFactorManagementClient.d.ts.map +1 -1
  55. package/dist/components/management/TwoFactorManagementClient.js +270 -111
  56. package/dist/components/management/TwoFactorManagementClient.js.map +1 -1
  57. package/dist/components/management/index.js +2 -2
  58. package/dist/components/management/index.js.map +1 -1
  59. package/dist/components/management/views/ApiKeysView.d.ts +1 -1
  60. package/dist/components/management/views/ApiKeysView.d.ts.map +1 -1
  61. package/dist/components/management/views/ApiKeysView.js +19 -4
  62. package/dist/components/management/views/ApiKeysView.js.map +1 -1
  63. package/dist/components/management/views/PasskeysView.d.ts +1 -1
  64. package/dist/components/management/views/PasskeysView.d.ts.map +1 -1
  65. package/dist/components/management/views/PasskeysView.js +16 -4
  66. package/dist/components/management/views/PasskeysView.js.map +1 -1
  67. package/dist/components/management/views/TwoFactorView.d.ts +1 -1
  68. package/dist/components/management/views/TwoFactorView.d.ts.map +1 -1
  69. package/dist/components/management/views/TwoFactorView.js +16 -4
  70. package/dist/components/management/views/TwoFactorView.js.map +1 -1
  71. package/dist/components/management/views/index.js +2 -2
  72. package/dist/components/management/views/index.js.map +1 -1
  73. package/dist/components/twoFactor/TwoFactorSetupView.d.ts +1 -1
  74. package/dist/components/twoFactor/TwoFactorSetupView.d.ts.map +1 -1
  75. package/dist/components/twoFactor/TwoFactorSetupView.js +240 -87
  76. package/dist/components/twoFactor/TwoFactorSetupView.js.map +1 -1
  77. package/dist/components/twoFactor/TwoFactorVerifyView.d.ts +1 -1
  78. package/dist/components/twoFactor/TwoFactorVerifyView.d.ts.map +1 -1
  79. package/dist/components/twoFactor/TwoFactorVerifyView.js +108 -45
  80. package/dist/components/twoFactor/TwoFactorVerifyView.js.map +1 -1
  81. package/dist/components/twoFactor/index.js +2 -2
  82. package/dist/components/twoFactor/index.js.map +1 -1
  83. package/dist/exports/client.d.ts +2356 -2
  84. package/dist/exports/client.d.ts.map +1 -1
  85. package/dist/exports/client.js +48 -8
  86. package/dist/exports/client.js.map +1 -1
  87. package/dist/exports/components.js +2 -2
  88. package/dist/exports/components.js.map +1 -1
  89. package/dist/exports/management.js +3 -3
  90. package/dist/exports/management.js.map +1 -1
  91. package/dist/exports/rsc.js +2 -2
  92. package/dist/exports/rsc.js.map +1 -1
  93. package/dist/generated-types.js +4 -2
  94. package/dist/generated-types.js.map +1 -1
  95. package/dist/index.js +6 -6
  96. package/dist/index.js.map +1 -1
  97. package/dist/plugin/index.d.ts +35 -2
  98. package/dist/plugin/index.d.ts.map +1 -1
  99. package/dist/plugin/index.js +198 -162
  100. package/dist/plugin/index.js.map +1 -1
  101. package/dist/scripts/generate-types.js +66 -50
  102. package/dist/scripts/generate-types.js.map +1 -1
  103. package/dist/types/apiKey.js +7 -2
  104. package/dist/types/apiKey.js.map +1 -1
  105. package/dist/types/betterAuth.js +23 -2
  106. package/dist/types/betterAuth.js.map +1 -1
  107. package/dist/utils/access.js +78 -81
  108. package/dist/utils/access.js.map +1 -1
  109. package/dist/utils/apiKeyAccess.js +65 -72
  110. package/dist/utils/apiKeyAccess.js.map +1 -1
  111. package/dist/utils/betterAuthDefaults.js +8 -8
  112. package/dist/utils/betterAuthDefaults.js.map +1 -1
  113. package/dist/utils/detectAuthConfig.js +8 -11
  114. package/dist/utils/detectAuthConfig.js.map +1 -1
  115. package/dist/utils/detectEnabledPlugins.js +6 -7
  116. package/dist/utils/detectEnabledPlugins.js.map +1 -1
  117. package/dist/utils/firstUserAdmin.js +18 -20
  118. package/dist/utils/firstUserAdmin.js.map +1 -1
  119. package/dist/utils/generateScopes.js +40 -41
  120. package/dist/utils/generateScopes.js.map +1 -1
  121. package/dist/utils/session.js +8 -9
  122. package/dist/utils/session.js.map +1 -1
  123. package/package.json +97 -26
  124. package/src/adapter/collections.ts +621 -0
  125. package/src/adapter/index.ts +712 -0
  126. package/src/components/BeforeLogin.tsx +39 -0
  127. package/src/components/LoginView.tsx +1516 -0
  128. package/src/components/LoginViewWrapper.tsx +35 -0
  129. package/src/components/LogoutButton.tsx +58 -0
  130. package/src/components/PasskeyRegisterButton.tsx +105 -0
  131. package/src/components/PasskeySignInButton.tsx +96 -0
  132. package/src/components/auth/ForgotPasswordView.tsx +274 -0
  133. package/src/components/auth/ResetPasswordView.tsx +331 -0
  134. package/src/components/auth/index.ts +8 -0
  135. package/src/components/management/ApiKeysManagementClient.tsx +988 -0
  136. package/src/components/management/PasskeysManagementClient.tsx +409 -0
  137. package/src/components/management/SecurityNavLinks.tsx +117 -0
  138. package/src/components/management/TwoFactorManagementClient.tsx +560 -0
  139. package/src/components/management/index.ts +20 -0
  140. package/src/components/management/views/ApiKeysView.tsx +57 -0
  141. package/src/components/management/views/PasskeysView.tsx +42 -0
  142. package/src/components/management/views/TwoFactorView.tsx +42 -0
  143. package/src/components/management/views/index.ts +10 -0
  144. package/src/components/twoFactor/TwoFactorSetupView.tsx +515 -0
  145. package/src/components/twoFactor/TwoFactorVerifyView.tsx +238 -0
  146. package/src/components/twoFactor/index.ts +8 -0
  147. package/src/exports/client.ts +77 -0
  148. package/src/exports/components.ts +30 -0
  149. package/src/exports/management.ts +25 -0
  150. package/src/exports/rsc.ts +11 -0
  151. package/src/generated-types.ts +269 -0
  152. package/src/index.ts +135 -0
  153. package/src/plugin/index.ts +834 -0
  154. package/src/scripts/generate-types.ts +269 -0
  155. package/src/types/apiKey.ts +63 -0
  156. package/src/types/betterAuth.ts +253 -0
  157. package/src/utils/access.ts +410 -0
  158. package/src/utils/apiKeyAccess.ts +443 -0
  159. package/src/utils/betterAuthDefaults.ts +102 -0
  160. package/src/utils/detectAuthConfig.ts +47 -0
  161. package/src/utils/detectEnabledPlugins.ts +69 -0
  162. package/src/utils/firstUserAdmin.ts +164 -0
  163. package/src/utils/generateScopes.ts +150 -0
  164. package/src/utils/session.ts +91 -0
@@ -0,0 +1,443 @@
1
+ /**
2
+ * API Key Scope Enforcement Utilities
3
+ *
4
+ * These utilities help enforce API key scopes in Payload access control.
5
+ * They extract the API key from requests, validate scopes, and provide
6
+ * type-safe access control functions.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { requireScope, requireAnyScope } from '@delmaredigital/payload-better-auth'
11
+ *
12
+ * export const Posts: CollectionConfig = {
13
+ * slug: 'posts',
14
+ * access: {
15
+ * read: requireAnyScope(['posts:read', 'content:read']),
16
+ * create: requireScope('posts:write'),
17
+ * update: requireScope('posts:write'),
18
+ * delete: requireScope('posts:delete'),
19
+ * },
20
+ * }
21
+ * ```
22
+ */
23
+
24
+ import type { Access, PayloadRequest } from 'payload'
25
+
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ // Types
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+
30
+ export type ApiKeyInfo = {
31
+ /** The API key ID */
32
+ id: string
33
+ /** User ID who owns this key */
34
+ userId: string
35
+ /** Array of granted scope strings */
36
+ scopes: string[]
37
+ /** The raw key (only first/last chars visible) */
38
+ keyPrefix?: string
39
+ /** Optional metadata */
40
+ metadata?: Record<string, unknown>
41
+ }
42
+
43
+ export type ApiKeyAccessConfig = {
44
+ /**
45
+ * API keys collection slug.
46
+ * @default 'apiKeys' or 'api-keys' (auto-detected)
47
+ */
48
+ apiKeysCollection?: string
49
+ /**
50
+ * Allow access if user is authenticated (non-API key session).
51
+ * Useful for allowing both API keys and regular sessions.
52
+ * @default false
53
+ */
54
+ allowAuthenticatedUsers?: boolean
55
+ /**
56
+ * Custom function to extract API key from request.
57
+ * By default, extracts from Authorization: Bearer <key> header.
58
+ */
59
+ extractApiKey?: (req: PayloadRequest) => string | null
60
+ }
61
+
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+ // Helpers
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Extract API key from request headers.
68
+ * Supports Bearer token format: Authorization: Bearer <api-key>
69
+ */
70
+ export function extractApiKeyFromRequest(req: PayloadRequest): string | null {
71
+ const authHeader = req.headers?.get('authorization')
72
+ if (!authHeader) return null
73
+
74
+ // Support "Bearer <key>" format
75
+ if (authHeader.startsWith('Bearer ')) {
76
+ return authHeader.slice(7).trim()
77
+ }
78
+
79
+ // Support raw key in Authorization header
80
+ return authHeader.trim()
81
+ }
82
+
83
+ /**
84
+ * Look up API key info from the database.
85
+ * Returns null if key not found or disabled.
86
+ */
87
+ export async function getApiKeyInfo(
88
+ req: PayloadRequest,
89
+ apiKey: string,
90
+ apiKeysCollection = 'apiKeys'
91
+ ): Promise<ApiKeyInfo | null> {
92
+ try {
93
+ // Try the provided collection name first
94
+ let results = await req.payload.find({
95
+ collection: apiKeysCollection,
96
+ where: {
97
+ key: { equals: apiKey },
98
+ enabled: { not_equals: false },
99
+ },
100
+ limit: 1,
101
+ depth: 0,
102
+ }).catch(() => null)
103
+
104
+ // If not found, try alternative slug
105
+ if (!results || results.docs.length === 0) {
106
+ const altSlug = apiKeysCollection === 'apiKeys' ? 'api-keys' : 'apiKeys'
107
+ results = await req.payload.find({
108
+ collection: altSlug,
109
+ where: {
110
+ key: { equals: apiKey },
111
+ enabled: { not_equals: false },
112
+ },
113
+ limit: 1,
114
+ depth: 0,
115
+ }).catch(() => null)
116
+ }
117
+
118
+ if (!results || results.docs.length === 0) {
119
+ return null
120
+ }
121
+
122
+ const doc = results.docs[0] as {
123
+ id: string | number
124
+ user?: string | number | { id: string | number }
125
+ userId?: string | number
126
+ permissions?: string
127
+ scopes?: string[]
128
+ start?: string
129
+ metadata?: string | Record<string, unknown>
130
+ }
131
+
132
+ // Parse scopes from permissions field (Better Auth format) or scopes array
133
+ let scopes: string[] = []
134
+ if (doc.permissions) {
135
+ try {
136
+ const parsed = JSON.parse(doc.permissions)
137
+ if (Array.isArray(parsed)) {
138
+ scopes = parsed
139
+ } else if (typeof parsed === 'object') {
140
+ // If it's an object, extract keys or flatten
141
+ scopes = Object.keys(parsed)
142
+ }
143
+ } catch {
144
+ // If not JSON, treat as comma-separated
145
+ scopes = doc.permissions.split(',').map((s) => s.trim()).filter(Boolean)
146
+ }
147
+ } else if (Array.isArray(doc.scopes)) {
148
+ scopes = doc.scopes
149
+ }
150
+
151
+ // Get user ID (handle both direct field and relationship)
152
+ let userId: string
153
+ if (doc.userId) {
154
+ userId = String(doc.userId)
155
+ } else if (doc.user) {
156
+ userId = typeof doc.user === 'object' ? String(doc.user.id) : String(doc.user)
157
+ } else {
158
+ return null
159
+ }
160
+
161
+ // Parse metadata
162
+ let metadata: Record<string, unknown> | undefined
163
+ if (doc.metadata) {
164
+ if (typeof doc.metadata === 'string') {
165
+ try {
166
+ metadata = JSON.parse(doc.metadata)
167
+ } catch {
168
+ // Ignore parse errors
169
+ }
170
+ } else {
171
+ metadata = doc.metadata
172
+ }
173
+ }
174
+
175
+ return {
176
+ id: String(doc.id),
177
+ userId,
178
+ scopes,
179
+ keyPrefix: doc.start,
180
+ metadata,
181
+ }
182
+ } catch {
183
+ return null
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Check if an API key has a specific scope.
189
+ * Supports wildcard patterns like 'posts:*' matching 'posts:read', 'posts:write', etc.
190
+ */
191
+ export function hasScope(keyScopes: string[], requiredScope: string): boolean {
192
+ return keyScopes.some((scope) => {
193
+ // Exact match
194
+ if (scope === requiredScope) return true
195
+
196
+ // Wildcard match: 'posts:*' matches 'posts:read'
197
+ if (scope.endsWith(':*')) {
198
+ const prefix = scope.slice(0, -1) // Remove '*', keep ':'
199
+ return requiredScope.startsWith(prefix)
200
+ }
201
+
202
+ // Global wildcard
203
+ if (scope === '*') return true
204
+
205
+ return false
206
+ })
207
+ }
208
+
209
+ /**
210
+ * Check if an API key has any of the specified scopes.
211
+ */
212
+ export function hasAnyScope(keyScopes: string[], requiredScopes: string[]): boolean {
213
+ return requiredScopes.some((scope) => hasScope(keyScopes, scope))
214
+ }
215
+
216
+ /**
217
+ * Check if an API key has all of the specified scopes.
218
+ */
219
+ export function hasAllScopes(keyScopes: string[], requiredScopes: string[]): boolean {
220
+ return requiredScopes.every((scope) => hasScope(keyScopes, scope))
221
+ }
222
+
223
+ // ─────────────────────────────────────────────────────────────────────────────
224
+ // Access Control Functions
225
+ // ─────────────────────────────────────────────────────────────────────────────
226
+
227
+ /**
228
+ * Create an access control function that requires a specific scope.
229
+ *
230
+ * @param scope - The required scope string (e.g., 'posts:read')
231
+ * @param config - Configuration options
232
+ * @returns Payload access function
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * access: {
237
+ * read: requireScope('posts:read'),
238
+ * create: requireScope('posts:write'),
239
+ * }
240
+ * ```
241
+ */
242
+ export function requireScope(
243
+ scope: string,
244
+ config: ApiKeyAccessConfig = {}
245
+ ): Access {
246
+ const {
247
+ apiKeysCollection = 'apiKeys',
248
+ allowAuthenticatedUsers = false,
249
+ extractApiKey = extractApiKeyFromRequest,
250
+ } = config
251
+
252
+ return async ({ req }) => {
253
+ // If authenticated users are allowed and user is logged in without API key
254
+ if (allowAuthenticatedUsers && req.user) {
255
+ const apiKey = extractApiKey(req)
256
+ if (!apiKey) {
257
+ return true // User authenticated via session, no API key = allow
258
+ }
259
+ }
260
+
261
+ // Extract API key from request
262
+ const apiKey = extractApiKey(req)
263
+ if (!apiKey) {
264
+ return false
265
+ }
266
+
267
+ // Look up API key
268
+ const keyInfo = await getApiKeyInfo(req, apiKey, apiKeysCollection)
269
+ if (!keyInfo) {
270
+ return false
271
+ }
272
+
273
+ // Check scope
274
+ return hasScope(keyInfo.scopes, scope)
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Create an access control function that requires any of the specified scopes.
280
+ *
281
+ * @param scopes - Array of acceptable scopes (at least one must match)
282
+ * @param config - Configuration options
283
+ * @returns Payload access function
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * access: {
288
+ * read: requireAnyScope(['posts:read', 'content:read', 'admin:*']),
289
+ * }
290
+ * ```
291
+ */
292
+ export function requireAnyScope(
293
+ scopes: string[],
294
+ config: ApiKeyAccessConfig = {}
295
+ ): Access {
296
+ const {
297
+ apiKeysCollection = 'apiKeys',
298
+ allowAuthenticatedUsers = false,
299
+ extractApiKey = extractApiKeyFromRequest,
300
+ } = config
301
+
302
+ return async ({ req }) => {
303
+ // If authenticated users are allowed and user is logged in without API key
304
+ if (allowAuthenticatedUsers && req.user) {
305
+ const apiKey = extractApiKey(req)
306
+ if (!apiKey) {
307
+ return true
308
+ }
309
+ }
310
+
311
+ const apiKey = extractApiKey(req)
312
+ if (!apiKey) {
313
+ return false
314
+ }
315
+
316
+ const keyInfo = await getApiKeyInfo(req, apiKey, apiKeysCollection)
317
+ if (!keyInfo) {
318
+ return false
319
+ }
320
+
321
+ return hasAnyScope(keyInfo.scopes, scopes)
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Create an access control function that requires all specified scopes.
327
+ *
328
+ * @param scopes - Array of required scopes (all must be present)
329
+ * @param config - Configuration options
330
+ * @returns Payload access function
331
+ *
332
+ * @example
333
+ * ```ts
334
+ * access: {
335
+ * delete: requireAllScopes(['posts:delete', 'admin:write']),
336
+ * }
337
+ * ```
338
+ */
339
+ export function requireAllScopes(
340
+ scopes: string[],
341
+ config: ApiKeyAccessConfig = {}
342
+ ): Access {
343
+ const {
344
+ apiKeysCollection = 'apiKeys',
345
+ allowAuthenticatedUsers = false,
346
+ extractApiKey = extractApiKeyFromRequest,
347
+ } = config
348
+
349
+ return async ({ req }) => {
350
+ // If authenticated users are allowed and user is logged in without API key
351
+ if (allowAuthenticatedUsers && req.user) {
352
+ const apiKey = extractApiKey(req)
353
+ if (!apiKey) {
354
+ return true
355
+ }
356
+ }
357
+
358
+ const apiKey = extractApiKey(req)
359
+ if (!apiKey) {
360
+ return false
361
+ }
362
+
363
+ const keyInfo = await getApiKeyInfo(req, apiKey, apiKeysCollection)
364
+ if (!keyInfo) {
365
+ return false
366
+ }
367
+
368
+ return hasAllScopes(keyInfo.scopes, scopes)
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Create an access control function that allows either:
374
+ * 1. Authenticated users (via session)
375
+ * 2. API key with required scope
376
+ *
377
+ * This is useful for endpoints that should work with both auth methods.
378
+ *
379
+ * @param scope - The required scope for API key access
380
+ * @param config - Configuration options
381
+ * @returns Payload access function
382
+ *
383
+ * @example
384
+ * ```ts
385
+ * access: {
386
+ * read: allowSessionOrScope('posts:read'),
387
+ * }
388
+ * ```
389
+ */
390
+ export function allowSessionOrScope(
391
+ scope: string,
392
+ config: Omit<ApiKeyAccessConfig, 'allowAuthenticatedUsers'> = {}
393
+ ): Access {
394
+ return requireScope(scope, { ...config, allowAuthenticatedUsers: true })
395
+ }
396
+
397
+ /**
398
+ * Create an access control function that allows either:
399
+ * 1. Authenticated users (via session)
400
+ * 2. API key with any of the required scopes
401
+ *
402
+ * @param scopes - Array of acceptable scopes for API key access
403
+ * @param config - Configuration options
404
+ * @returns Payload access function
405
+ */
406
+ export function allowSessionOrAnyScope(
407
+ scopes: string[],
408
+ config: Omit<ApiKeyAccessConfig, 'allowAuthenticatedUsers'> = {}
409
+ ): Access {
410
+ return requireAnyScope(scopes, { ...config, allowAuthenticatedUsers: true })
411
+ }
412
+
413
+ // ─────────────────────────────────────────────────────────────────────────────
414
+ // Better Auth Integration
415
+ // ─────────────────────────────────────────────────────────────────────────────
416
+
417
+ /**
418
+ * Validate an API key and get its info.
419
+ *
420
+ * This performs a database lookup to validate the key and retrieve
421
+ * its associated scopes and user.
422
+ *
423
+ * @param req - Payload request
424
+ * @param apiKeysCollection - The API keys collection slug
425
+ * @returns API key info if valid, null otherwise
426
+ *
427
+ * @example
428
+ * ```ts
429
+ * const keyInfo = await validateApiKey(req)
430
+ * if (keyInfo) {
431
+ * console.log('Valid API key for user:', keyInfo.userId)
432
+ * console.log('Scopes:', keyInfo.scopes)
433
+ * }
434
+ * ```
435
+ */
436
+ export async function validateApiKey(
437
+ req: PayloadRequest,
438
+ apiKeysCollection = 'apiKeys'
439
+ ): Promise<ApiKeyInfo | null> {
440
+ const apiKey = extractApiKeyFromRequest(req)
441
+ if (!apiKey) return null
442
+ return getApiKeyInfo(req, apiKey, apiKeysCollection)
443
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Utility to apply sensible defaults to Better Auth options.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ import type { BetterAuthOptions } from 'better-auth'
8
+ import { apiKey as betterAuthApiKey } from 'better-auth/plugins'
9
+
10
+ type ApiKeyPluginOptions = Parameters<typeof betterAuthApiKey>[0]
11
+
12
+ /**
13
+ * API Key plugin with sensible defaults for use with this package.
14
+ *
15
+ * Enables metadata storage by default so that scopes can be displayed
16
+ * in the admin UI after key creation.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { apiKeyWithDefaults } from '@delmaredigital/payload-better-auth'
21
+ *
22
+ * export const betterAuthOptions = {
23
+ * plugins: [
24
+ * apiKeyWithDefaults(), // metadata enabled by default
25
+ * ],
26
+ * }
27
+ * ```
28
+ *
29
+ * @example With custom options
30
+ * ```ts
31
+ * apiKeyWithDefaults({
32
+ * rateLimit: { max: 100, window: 60 },
33
+ * // enableMetadata is already true
34
+ * })
35
+ * ```
36
+ */
37
+ export function apiKeyWithDefaults(options?: ApiKeyPluginOptions) {
38
+ return betterAuthApiKey({
39
+ enableMetadata: true,
40
+ ...options,
41
+ })
42
+ }
43
+
44
+ /**
45
+ * Applies sensible defaults to Better Auth options.
46
+ *
47
+ * Currently applies the following defaults:
48
+ * - `trustedOrigins`: If not explicitly provided but `baseURL` is set,
49
+ * defaults to `[baseURL]`. This handles the common single-domain case
50
+ * where the app's origin should be trusted for auth requests.
51
+ *
52
+ * Multi-domain setups can still explicitly set `trustedOrigins` to include
53
+ * multiple origins.
54
+ *
55
+ * @example Simple case - trustedOrigins defaults to [baseURL]
56
+ * ```ts
57
+ * import { withBetterAuthDefaults } from '@delmaredigital/payload-better-auth'
58
+ *
59
+ * const auth = betterAuth(withBetterAuthDefaults({
60
+ * baseURL: 'https://myapp.com',
61
+ * // trustedOrigins automatically becomes ['https://myapp.com']
62
+ * }))
63
+ * ```
64
+ *
65
+ * @example Multi-domain case - explicit trustedOrigins respected
66
+ * ```ts
67
+ * const auth = betterAuth(withBetterAuthDefaults({
68
+ * baseURL: 'https://myapp.com',
69
+ * trustedOrigins: ['https://myapp.com', 'https://other-domain.com'],
70
+ * // trustedOrigins stays as explicitly provided
71
+ * }))
72
+ * ```
73
+ *
74
+ * @example With createBetterAuthPlugin
75
+ * ```ts
76
+ * createBetterAuthPlugin({
77
+ * createAuth: (payload) => betterAuth(withBetterAuthDefaults({
78
+ * database: payloadAdapter({ payloadClient: payload }),
79
+ * baseURL: process.env.BETTER_AUTH_URL,
80
+ * })),
81
+ * })
82
+ * ```
83
+ */
84
+ export function withBetterAuthDefaults<T extends BetterAuthOptions>(
85
+ options: T
86
+ ): T {
87
+ // If trustedOrigins is explicitly provided, use it as-is
88
+ if (options.trustedOrigins !== undefined) {
89
+ return options
90
+ }
91
+
92
+ // If baseURL is set, default trustedOrigins to [baseURL]
93
+ if (options.baseURL) {
94
+ return {
95
+ ...options,
96
+ trustedOrigins: [options.baseURL],
97
+ }
98
+ }
99
+
100
+ // No defaults to apply
101
+ return options
102
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Utility to detect auth configuration in Payload config
3
+ */
4
+
5
+ import type { Config, CollectionConfig } from 'payload'
6
+
7
+ export type AuthDetectionResult = {
8
+ /** Whether any collection has disableLocalStrategy: true */
9
+ hasDisableLocalStrategy: boolean
10
+ /** The slug of the auth collection (if found) */
11
+ authCollectionSlug: string | null
12
+ /** The auth collection config (if found) */
13
+ authCollectionConfig: CollectionConfig | null
14
+ }
15
+
16
+ /**
17
+ * Scans Payload config to detect if any collection uses disableLocalStrategy.
18
+ * Used to determine whether to auto-inject admin components.
19
+ */
20
+ export function detectAuthConfig(config: Config): AuthDetectionResult {
21
+ const collections = config.collections ?? []
22
+
23
+ for (const collection of collections) {
24
+ if (collection.auth) {
25
+ const auth = collection.auth
26
+ // disableLocalStrategy can be `true` or an object with options
27
+ if (
28
+ auth === true ||
29
+ (typeof auth === 'object' && auth.disableLocalStrategy)
30
+ ) {
31
+ return {
32
+ hasDisableLocalStrategy:
33
+ auth === true ||
34
+ (typeof auth === 'object' && !!auth.disableLocalStrategy),
35
+ authCollectionSlug: collection.slug,
36
+ authCollectionConfig: collection,
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ return {
43
+ hasDisableLocalStrategy: false,
44
+ authCollectionSlug: null,
45
+ authCollectionConfig: null,
46
+ }
47
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Utility to detect which Better Auth plugins are enabled
3
+ */
4
+
5
+ import type { BetterAuthOptions } from 'better-auth'
6
+
7
+ export type EnabledPluginsResult = {
8
+ hasAdmin: boolean
9
+ hasApiKey: boolean
10
+ hasTwoFactor: boolean
11
+ hasPasskey: boolean
12
+ hasMagicLink: boolean
13
+ hasMultiSession: boolean
14
+ hasOrganization: boolean
15
+ }
16
+
17
+ /**
18
+ * Detects which Better Auth plugins are enabled from the options.
19
+ * Inspects the plugins array by checking plugin identifiers.
20
+ *
21
+ * @param options - Better Auth options containing plugins array
22
+ * @returns Object with boolean flags for each supported plugin
23
+ */
24
+ export function detectEnabledPlugins(
25
+ options?: Partial<BetterAuthOptions>
26
+ ): EnabledPluginsResult {
27
+ const plugins = options?.plugins ?? []
28
+
29
+ const result: EnabledPluginsResult = {
30
+ hasAdmin: false,
31
+ hasApiKey: false,
32
+ hasTwoFactor: false,
33
+ hasPasskey: false,
34
+ hasMagicLink: false,
35
+ hasMultiSession: false,
36
+ hasOrganization: false,
37
+ }
38
+
39
+ for (const plugin of plugins) {
40
+ // Better Auth plugins have an id property
41
+ const id = (plugin as { id?: string }).id
42
+
43
+ switch (id) {
44
+ case 'admin':
45
+ result.hasAdmin = true
46
+ break
47
+ case 'api-key':
48
+ result.hasApiKey = true
49
+ break
50
+ case 'two-factor':
51
+ result.hasTwoFactor = true
52
+ break
53
+ case 'passkey':
54
+ result.hasPasskey = true
55
+ break
56
+ case 'magic-link':
57
+ result.hasMagicLink = true
58
+ break
59
+ case 'multi-session':
60
+ result.hasMultiSession = true
61
+ break
62
+ case 'organization':
63
+ result.hasOrganization = true
64
+ break
65
+ }
66
+ }
67
+
68
+ return result
69
+ }