payload-better-auth 1.0.10 → 1.1.6

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 (47) hide show
  1. package/README.md +111 -174
  2. package/dist/better-auth/databaseHooks.js +1 -1
  3. package/dist/better-auth/databaseHooks.js.map +1 -1
  4. package/dist/better-auth/plugin.d.ts +1 -1
  5. package/dist/better-auth/plugin.js +3 -3
  6. package/dist/better-auth/plugin.js.map +1 -1
  7. package/dist/better-auth/reconcile-queue.d.ts +1 -1
  8. package/dist/better-auth/reconcile-queue.js.map +1 -1
  9. package/dist/better-auth/sources.js +1 -1
  10. package/dist/better-auth/sources.js.map +1 -1
  11. package/dist/collections/Users/index.js +1 -1
  12. package/dist/collections/Users/index.js.map +1 -1
  13. package/dist/components/BetterAuthLoginServer.d.ts +19 -6
  14. package/dist/components/BetterAuthLoginServer.js +24 -8
  15. package/dist/components/BetterAuthLoginServer.js.map +1 -1
  16. package/dist/components/EmailPasswordFormClient.d.ts +1 -1
  17. package/dist/components/EmailPasswordFormClient.js.map +1 -1
  18. package/dist/exports/client.d.ts +1 -1
  19. package/dist/exports/client.js +1 -1
  20. package/dist/exports/client.js.map +1 -1
  21. package/dist/exports/rsc.d.ts +1 -1
  22. package/dist/exports/rsc.js +1 -1
  23. package/dist/exports/rsc.js.map +1 -1
  24. package/dist/index.d.ts +5 -4
  25. package/dist/index.js +5 -4
  26. package/dist/index.js.map +1 -1
  27. package/dist/payload/plugin.d.ts +21 -2
  28. package/dist/payload/plugin.js +29 -7
  29. package/dist/payload/plugin.js.map +1 -1
  30. package/dist/utils/payload-reconcile.js +2 -6
  31. package/dist/utils/payload-reconcile.js.map +1 -1
  32. package/package.json +137 -63
  33. package/src/better-auth/crypto-shared.ts +169 -0
  34. package/src/better-auth/databaseHooks.ts +30 -0
  35. package/src/better-auth/helpers.ts +3 -0
  36. package/src/better-auth/plugin.ts +214 -0
  37. package/src/better-auth/reconcile-queue.ts +401 -0
  38. package/src/better-auth/sources.ts +123 -0
  39. package/src/collections/Users/index.ts +148 -0
  40. package/src/components/BetterAuthLoginServer.tsx +154 -0
  41. package/src/components/EmailPasswordFormClient.tsx +204 -0
  42. package/src/components/VerifyEmailInfoViewClient.tsx +62 -0
  43. package/src/exports/client.ts +1 -0
  44. package/src/exports/rsc.ts +1 -0
  45. package/src/index.ts +9 -0
  46. package/src/payload/plugin.ts +163 -0
  47. package/src/utils/payload-reconcile.ts +50 -0
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "payload-better-auth",
3
- "version": "1.0.10",
4
- "description": "A blank template to get started with Payload 3.0",
3
+ "version": "1.1.6",
4
+ "description": "A Payload CMS plugin that integrates Better Auth for seamless user authentication and management",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/benjaminpreiss/payload-better-auth"
9
+ },
6
10
  "type": "module",
7
11
  "exports": {
8
12
  ".": {
@@ -24,78 +28,148 @@
24
28
  "main": "./dist/index.js",
25
29
  "types": "./dist/index.d.ts",
26
30
  "files": [
27
- "dist"
31
+ "dist",
32
+ "src"
28
33
  ],
34
+ "scripts": {
35
+ "build": "nx run payload-better-auth:_build",
36
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
37
+ "build:types": "tsc --outDir dist --rootDir ./src",
38
+ "_build": "npm run copyfiles && npm run build:types && npm run build:swc",
39
+ "clean": "rimraf {dist,*.tsbuildinfo}",
40
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
41
+ "dev": "concurrently -k -n \"WEB,MAIL\" -c \"auto\" \"npm run dev:original\" \"npm run maildev\"",
42
+ "dev:build": "nx run payload-better-auth:_dev:build",
43
+ "_dev:build": "dotenv -e ./dev/.env.test -- next build dev",
44
+ "dev:original": "dotenv -e ./dev/.env.development -- next dev dev --turbo",
45
+ "dev:start": "dotenv -e ./dev/.env.test -- next start dev",
46
+ "dev:generate-importmap": "npm run dev:payload generate:importmap",
47
+ "dev:generate-types": "npm run dev:payload generate:types",
48
+ "dev:payload": "dotenv -e ./dev/.env.development -- cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
49
+ "generate:importmap": "npm run dev:generate-importmap",
50
+ "generate:types": "npm run dev:generate-types",
51
+ "db:reset": "rimraf ./payload.db ./better-auth.db",
52
+ "db:reset:test": "rimraf ./dev/tests/test-payload.db ./dev/tests/test-better-auth.db",
53
+ "payload:migrate": "dotenv -e ./dev/.env.development -- cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload migrate:create --skip-empty && dotenv -e ./dev/.env.development -- cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload migrate",
54
+ "payload:migrate:test": "dotenv -e ./dev/.env.test -- cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload migrate:create --skip-empty && dotenv -e ./dev/.env.test -- cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload migrate",
55
+ "better-auth:migrate": "dotenv -e ./dev/.env.development -- better-auth migrate --yes --config ./dev/lib/auth.ts",
56
+ "better-auth:migrate:test": "dotenv -e ./dev/.env.test -- better-auth migrate --yes --config ./dev/lib/auth.ts",
57
+ "reset": "npm run db:reset && npm run payload:migrate && npm run better-auth:migrate",
58
+ "reset:test": "npm run db:reset:test && npm run payload:migrate:test && npm run better-auth:migrate:test",
59
+ "lint": "nx run payload-better-auth:_lint",
60
+ "_lint": "eslint --stats ./src ./dev",
61
+ "lint:fix:all": "eslint --fix --stats ./src ./dev ./dev/tests",
62
+ "lint:fix": "eslint ./src --fix",
63
+ "maildev": "maildev --smtp=1025 --web=1080",
64
+ "prepare": "husky",
65
+ "prepublishOnly": "npm run clean && npm run build",
66
+ "test": "nx run payload-better-auth:_test",
67
+ "_test": "npm run test:int && npm run test:e2e",
68
+ "test:e2e": "nx run payload-better-auth:_test:e2e",
69
+ "_test:e2e": "npm run dev:build && dotenv -e ./dev/.env.test -- playwright test",
70
+ "test:int": "nx run payload-better-auth:_test:int",
71
+ "_test:int": "npm run test:setup && dotenv -e ./dev/.env.test -- vitest run",
72
+ "test:setup": "npm run reset:test",
73
+ "typecheck": "nx run payload-better-auth:_typecheck",
74
+ "_typecheck": "tsc --noEmit -p ./dev/tsconfig.json",
75
+ "semantic-release": "semantic-release",
76
+ "test:int-with-wait": "dotenv -e ./dev/.env.test -- concurrently --success first -k -n \"dev,test\" \"npm run dev\" \"sh -lc 'until curl -fsS http://127.0.0.1:3000/admin >/dev/null; do sleep 0.25; done; npm run test:int'\""
77
+ },
29
78
  "devDependencies": {
30
- "@eslint/eslintrc": "^3.3.1",
31
- "@payloadcms/db-mongodb": "^3.57.0",
32
- "@payloadcms/db-postgres": "^3.57.0",
33
- "@payloadcms/db-sqlite": "^3.57.0",
79
+ "@better-auth/cli": "^1.4.10",
80
+ "@commitlint/cli": "^20.3.1",
81
+ "@commitlint/config-conventional": "^20.3.1",
82
+ "@eslint/eslintrc": "^3.3.3",
83
+ "@payloadcms/db-mongodb": "^3.70.0",
84
+ "@payloadcms/db-postgres": "^3.70.0",
85
+ "@payloadcms/db-sqlite": "^3.70.0",
34
86
  "@payloadcms/eslint-config": "^3.28.0",
35
- "@payloadcms/next": "^3.57.0",
36
- "@payloadcms/richtext-lexical": "^3.57.0",
37
- "@payloadcms/ui": "^3.57.0",
38
- "@playwright/test": "^1.55.1",
39
- "@swc-node/register": "1.10.9",
40
- "@swc/cli": "0.6.0",
41
- "@types/node": "^22.18.6",
42
- "@types/nodemailer": "^7.0.2",
43
- "@types/react": "19.1.8",
44
- "@types/react-dom": "19.1.6",
45
- "better-sqlite3": "^12.4.1",
87
+ "@payloadcms/next": "^3.70.0",
88
+ "@payloadcms/richtext-lexical": "^3.70.0",
89
+ "@payloadcms/ui": "^3.70.0",
90
+ "@playwright/test": "^1.57.0",
91
+ "@semantic-release/changelog": "^6.0.3",
92
+ "@semantic-release/git": "^10.0.1",
93
+ "@semantic-release/github": "^12.0.2",
94
+ "@semantic-release/npm": "^13.1.3",
95
+ "@swc-node/register": "1.11.1",
96
+ "@swc/cli": "0.7.9",
97
+ "@types/better-sqlite3": "^7.6.13",
98
+ "@types/node": "^25.0.6",
99
+ "@types/nodemailer": "^7.0.5",
100
+ "@types/react": "19.2.8",
101
+ "@types/react-dom": "19.2.3",
102
+ "better-auth": "^1.4.10",
103
+ "better-sqlite3": "^12.6.0",
46
104
  "concurrently": "^9.2.1",
47
105
  "copyfiles": "2.4.1",
48
- "cross-env": "^7.0.3",
49
- "dotenv-cli": "^10.0.0",
50
- "eslint": "^9.36.0",
51
- "eslint-config-next": "15.4.4",
52
- "execa": "^9.6.0",
53
- "graphql": "^16.11.0",
106
+ "cross-env": "^10.1.0",
107
+ "dotenv-cli": "^11.0.0",
108
+ "drizzle-orm": "^0.44.0",
109
+ "eslint": "^9.39.2",
110
+ "eslint-config-next": "16.1.1",
111
+ "execa": "^9.6.1",
112
+ "graphql": "^16.12.0",
113
+ "husky": "^9.1.7",
54
114
  "maildev": "^2.2.1",
55
- "mongodb-memory-server": "10.1.4",
56
- "next": "15.4.4",
57
- "nodemailer": "^7.0.6",
58
- "open": "^10.2.0",
59
- "payload": "^3.57.0",
60
- "prettier": "^3.6.2",
61
- "qs-esm": "7.0.2",
62
- "react": "19.1.0",
63
- "react-dom": "19.1.0",
64
- "rimraf": "3.0.2",
65
- "sharp": "0.34.2",
66
- "sort-package-json": "^2.15.1",
67
- "typescript": "5.7.3",
68
- "vite-tsconfig-paths": "^5.1.4",
69
- "vitest": "^3.2.4"
115
+ "mongodb-memory-server": "11.0.1",
116
+ "next": "16.1.1",
117
+ "nodemailer": "^7.0.12",
118
+ "nx": "^22.3.3",
119
+ "open": "^11.0.0",
120
+ "payload": "^3.70.0",
121
+ "prettier": "^3.7.4",
122
+ "qs-esm": "7.0.3",
123
+ "react": "19.2.3",
124
+ "react-dom": "19.2.3",
125
+ "rimraf": "6.1.2",
126
+ "semantic-release": "^25.0.2",
127
+ "sharp": "0.34.5",
128
+ "sort-package-json": "^3.6.0",
129
+ "typescript": "5.9.3",
130
+ "vite-tsconfig-paths": "^6.0.4",
131
+ "vitest": "^4.0.16"
70
132
  },
71
133
  "peerDependencies": {
72
- "better-auth": "^1.4.0-beta.5",
134
+ "better-auth": "^1.4.10",
73
135
  "payload": "^3.37.0"
74
136
  },
137
+ "packageManager": "pnpm@10.16.1",
75
138
  "engines": {
76
139
  "node": "^18.20.2 || >=20.9.0",
77
140
  "pnpm": "^9 || ^10"
78
141
  },
79
- "registry": "https://registry.npmjs.org/",
80
- "scripts": {
81
- "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
82
- "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
83
- "build:types": "tsc --outDir dist --rootDir ./src",
84
- "clean": "rimraf {dist,*.tsbuildinfo}",
85
- "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
86
- "dev": "concurrently -k -n \"WEB,MAIL\" -c \"auto\" \"pnpm:dev:original\" \"pnpm:maildev\"",
87
- "dev:original": "next dev dev --turbo",
88
- "dev:generate-importmap": "pnpm dev:payload generate:importmap",
89
- "dev:generate-types": "pnpm dev:payload generate:types",
90
- "dev:payload": "dotenv -e ./dev/.env -- cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
91
- "generate:importmap": "pnpm dev:generate-importmap",
92
- "generate:types": "pnpm dev:generate-types",
93
- "lint": "eslint",
94
- "lint:fix": "eslint ./src --fix",
95
- "maildev": "maildev --smtp=1025 --web=1080",
96
- "test": "pnpm test:int && pnpm test:e2e",
97
- "test:e2e": "playwright test",
98
- "test:int": "vitest",
99
- "test:int-with-wait": "pnpm exec concurrently --success first -k -n \"dev,test\" \"pnpm run dev\" \"sh -lc 'until curl -fsS http://127.0.0.1:3000/admin >/dev/null; do sleep 0.25; done; pnpm run test:int'\""
100
- }
101
- }
142
+ "publishConfig": {
143
+ "exports": {
144
+ ".": {
145
+ "import": "./dist/index.js",
146
+ "types": "./dist/index.d.ts",
147
+ "default": "./dist/index.js"
148
+ },
149
+ "./client": {
150
+ "import": "./dist/exports/client.js",
151
+ "types": "./dist/exports/client.d.ts",
152
+ "default": "./dist/exports/client.js"
153
+ },
154
+ "./rsc": {
155
+ "import": "./dist/exports/rsc.js",
156
+ "types": "./dist/exports/rsc.d.ts",
157
+ "default": "./dist/exports/rsc.js"
158
+ }
159
+ },
160
+ "main": "./dist/index.js",
161
+ "types": "./dist/index.d.ts"
162
+ },
163
+ "pnpm": {
164
+ "overrides": {
165
+ "drizzle-orm": "^0.44.0"
166
+ },
167
+ "onlyBuiltDependencies": [
168
+ "sharp",
169
+ "esbuild",
170
+ "unrs-resolver",
171
+ "better-sqlite3"
172
+ ]
173
+ },
174
+ "nx": {}
175
+ }
@@ -0,0 +1,169 @@
1
+ // crypto-shared.ts
2
+ import crypto from 'crypto'
3
+
4
+ /**
5
+ * Type for serializable values that can be canonically stringified
6
+ */
7
+ type SerializableValue =
8
+ | boolean
9
+ | null
10
+ | number
11
+ | SerializableArray
12
+ | SerializableObject
13
+ | string
14
+ | undefined
15
+
16
+ interface SerializableObject {
17
+ [key: string]: SerializableValue
18
+ }
19
+
20
+ interface SerializableArray extends Array<SerializableValue> {}
21
+
22
+ /**
23
+ * Signature object containing timestamp, nonce, and MAC
24
+ */
25
+ export interface CryptoSignature {
26
+ /** HMAC-SHA256 signature */
27
+ mac: string
28
+ /** Unique nonce for this signature */
29
+ nonce: string
30
+ /** Unix timestamp as string */
31
+ ts: string
32
+ }
33
+
34
+ /**
35
+ * Input parameters for signature verification
36
+ */
37
+ export interface VerifySignatureInput {
38
+ /** The data that was signed */
39
+ body: unknown
40
+ /** Maximum allowed time skew in seconds (default: 300) */
41
+ maxSkewSec?: number
42
+ /** Secret key for verification */
43
+ secret: string
44
+ /** The signature to verify */
45
+ signature: CryptoSignature
46
+ }
47
+
48
+ /**
49
+ * Input parameters for signature creation
50
+ */
51
+ export interface SignCanonicalInput {
52
+ /** The data to sign */
53
+ body: unknown
54
+ /** Secret key for signing */
55
+ secret: string
56
+ }
57
+
58
+ /**
59
+ * Converts an object to a canonical string representation
60
+ * Handles circular references and ensures consistent ordering
61
+ */
62
+ function canonicalStringify(obj: unknown): string {
63
+ const seen = new WeakSet<object>()
64
+
65
+ const walk = (v: unknown): string => {
66
+ if (v && typeof v === 'object') {
67
+ if (seen.has(v)) {
68
+ throw new Error('Circular reference detected in object')
69
+ }
70
+ seen.add(v)
71
+
72
+ if (Array.isArray(v)) {
73
+ const result = `[${v.map(walk).join(',')}]`
74
+ seen.delete(v)
75
+ return result
76
+ }
77
+
78
+ const keys = Object.keys(v).sort()
79
+ const result = `{${keys.map((k) => `"${k}":${walk((v as Record<string, unknown>)[k])}`).join(',')}}`
80
+ seen.delete(v)
81
+ return result
82
+ }
83
+ return JSON.stringify(v)
84
+ }
85
+
86
+ return walk(obj)
87
+ }
88
+
89
+ /**
90
+ * Creates a cryptographic signature for the given data
91
+ * @param body - The data to sign
92
+ * @param secret - Secret key for signing
93
+ * @returns Signature object with timestamp, nonce, and MAC
94
+ */
95
+ export function signCanonical(body: unknown, secret: string): CryptoSignature {
96
+ if (!secret || typeof secret !== 'string') {
97
+ throw new Error('Secret must be a non-empty string')
98
+ }
99
+
100
+ const ts = Math.floor(Date.now() / 1000).toString()
101
+ const nonce = crypto.randomUUID()
102
+ const payload = canonicalStringify(body)
103
+ const mac = crypto.createHmac('sha256', secret).update(`${ts}.${nonce}.${payload}`).digest('hex')
104
+
105
+ return { mac, nonce, ts }
106
+ }
107
+
108
+ /**
109
+ * Verifies a cryptographic signature
110
+ * @param body - The original data that was signed
111
+ * @param sig - The signature to verify
112
+ * @param secret - Secret key for verification
113
+ * @param maxSkewSec - Maximum allowed time skew in seconds (default: 300)
114
+ * @returns true if signature is valid, false otherwise
115
+ */
116
+ export function verifyCanonical(
117
+ body: unknown,
118
+ sig: CryptoSignature,
119
+ secret: string,
120
+ maxSkewSec: number = 300,
121
+ ) {
122
+ if (!secret || typeof secret !== 'string') {
123
+ return false
124
+ }
125
+
126
+ if (!sig || typeof sig !== 'object' || !sig.ts || !sig.nonce || !sig.mac) {
127
+ return false
128
+ }
129
+
130
+ // Validate timestamp
131
+ const now = Math.floor(Date.now() / 1000)
132
+ const t = Number(sig.ts)
133
+ if (!Number.isFinite(t) || Math.abs(now - t) > maxSkewSec) {
134
+ return false
135
+ }
136
+
137
+ try {
138
+ const payload = canonicalStringify(body)
139
+ const expected = crypto
140
+ .createHmac('sha256', secret)
141
+ .update(`${sig.ts}.${sig.nonce}.${payload}`)
142
+ .digest('hex')
143
+
144
+ return crypto.timingSafeEqual(
145
+ new Uint8Array(Buffer.from(sig.mac, 'hex')),
146
+ new Uint8Array(Buffer.from(expected, 'hex')),
147
+ )
148
+ } catch {
149
+ return false
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Convenience function for verifying signatures with input object
155
+ * @param input - Verification parameters
156
+ * @returns true if signature is valid, false otherwise
157
+ */
158
+ export function verifySignature(input: VerifySignatureInput) {
159
+ return verifyCanonical(input.body, input.signature, input.secret, input.maxSkewSec)
160
+ }
161
+
162
+ /**
163
+ * Convenience function for creating signatures with input object
164
+ * @param input - Signing parameters
165
+ * @returns Signature object
166
+ */
167
+ export function createSignature(input: SignCanonicalInput) {
168
+ return signCanonical(input.body, input.secret)
169
+ }
@@ -0,0 +1,30 @@
1
+ import type { betterAuth } from 'better-auth'
2
+ import type { SanitizedConfig } from 'payload'
3
+
4
+ import { createDeleteUserFromPayload, createSyncUserToPayload } from './sources'
5
+
6
+ export function createDatabaseHooks({
7
+ config,
8
+ }: {
9
+ config: Promise<SanitizedConfig>
10
+ }): Parameters<typeof betterAuth>['0']['databaseHooks'] {
11
+ const syncUserToPayload = createSyncUserToPayload(config)
12
+ const deleteUserFromPayload = createDeleteUserFromPayload(config)
13
+ return {
14
+ user: {
15
+ create: {
16
+ // After the BA user exists, sync to Payload. On failure, enqueue in memory.
17
+ after: async (user) => {
18
+ // push BA-induced ensure to the **front** of the queue
19
+ await syncUserToPayload(user)
20
+ },
21
+ },
22
+ // TODO: possibly offer "update"
23
+ delete: {
24
+ after: async (user) => {
25
+ await deleteUserFromPayload(user.id)
26
+ },
27
+ },
28
+ },
29
+ }
30
+ }
@@ -0,0 +1,3 @@
1
+ export type AuthMethod =
2
+ | { method: 'emailAndPassword'; options: { minPasswordLength: number } }
3
+ | { method: 'magicLink' }
@@ -0,0 +1,214 @@
1
+ // src/plugins/reconcile-queue-plugin.ts
2
+ import type { AuthContext, BetterAuthPlugin, DeepPartial } from 'better-auth'
3
+ import type { SanitizedConfig } from 'payload'
4
+
5
+ import { APIError } from 'better-auth/api'
6
+ import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins'
7
+
8
+ import type { AuthMethod } from './helpers'
9
+
10
+ import { createDatabaseHooks } from './databaseHooks'
11
+ import { type InitOptions, Queue } from './reconcile-queue'
12
+ import {
13
+ type BAUser,
14
+ createDeleteUserFromPayload,
15
+ createListPayloadUsersPage,
16
+ createSyncUserToPayload,
17
+ } from './sources'
18
+
19
+ type PayloadSyncPluginContext = { payloadSyncPlugin: { queue: Queue } } & AuthContext
20
+
21
+ type CreateAdminsUser = Parameters<AuthContext['internalAdapter']['createUser']>['0']
22
+
23
+ const defaultLog = (msg: string, extra?: unknown) => {
24
+ console.log(`[reconcile] ${msg}`, extra ? JSON.stringify(extra, null, 2) : '')
25
+ }
26
+
27
+ export const payloadBetterAuthPlugin = (
28
+ opts: {
29
+ createAdmins?: { overwrite?: boolean; user: CreateAdminsUser }[]
30
+ enableLogging?: boolean
31
+ payloadConfig: Promise<SanitizedConfig>
32
+ token: string // simple header token for admin endpoints,
33
+ } & InitOptions,
34
+ ): BetterAuthPlugin => {
35
+ return {
36
+ id: 'reconcile-queue-plugin',
37
+ endpoints: {
38
+ run: createAuthEndpoint(
39
+ '/reconcile/run',
40
+ { method: 'POST' },
41
+ async ({ context, json, request }) => {
42
+ if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {
43
+ throw new APIError('UNAUTHORIZED', { message: 'invalid token' })
44
+ }
45
+ await (context as PayloadSyncPluginContext).payloadSyncPlugin.queue.seedFullReconcile()
46
+ return json({ ok: true })
47
+ },
48
+ ),
49
+ status: createAuthEndpoint(
50
+ '/reconcile/status',
51
+ { method: 'GET' },
52
+ async ({ context, json, request }) => {
53
+ if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {
54
+ return Promise.reject(
55
+ new APIError('UNAUTHORIZED', { message: 'invalid token' }) as Error,
56
+ )
57
+ }
58
+ return json((context as PayloadSyncPluginContext).payloadSyncPlugin.queue.status())
59
+ },
60
+ ),
61
+ // convenience for tests/admin tools (optional)
62
+ authMethods: createAuthEndpoint(
63
+ '/auth/methods',
64
+ { method: 'GET' },
65
+ async ({ context, json }) => {
66
+ const authMethods: AuthMethod[] = []
67
+ // Check if emailAndPassword is enabled, or if present at all (not present defaults to false)
68
+ if (context.options.emailAndPassword?.enabled) {
69
+ authMethods.push({
70
+ method: 'emailAndPassword',
71
+ options: {
72
+ minPasswordLength: context.options.emailAndPassword.minPasswordLength ?? 0,
73
+ },
74
+ })
75
+ }
76
+ if (context.options.plugins?.some((p) => p.id === 'magic-link')) {
77
+ authMethods.push({ method: 'magicLink' })
78
+ }
79
+
80
+ return await json(authMethods)
81
+ },
82
+ ),
83
+ deleteNow: createAuthEndpoint(
84
+ '/reconcile/delete',
85
+ { method: 'POST' },
86
+ async ({ context, json, request }) => {
87
+ if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {
88
+ throw new APIError('UNAUTHORIZED', { message: 'invalid token' })
89
+ }
90
+ const body = (await request?.json().catch(() => ({}))) as { baId?: string } | undefined
91
+ const baId = body?.baId
92
+ if (!baId) {
93
+ throw new APIError('BAD_REQUEST', { message: 'missing baId' })
94
+ }
95
+ ;(context as PayloadSyncPluginContext).payloadSyncPlugin.queue.enqueueDelete(
96
+ baId,
97
+ true,
98
+ 'user-operation',
99
+ )
100
+ return json({ ok: true })
101
+ },
102
+ ),
103
+ ensureNow: createAuthEndpoint(
104
+ '/reconcile/ensure',
105
+ { method: 'POST' },
106
+ async ({ context, json, request }) => {
107
+ if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {
108
+ throw new APIError('UNAUTHORIZED', { message: 'invalid token' })
109
+ }
110
+ const body = (await request?.json().catch(() => ({}))) as { user?: BAUser } | undefined
111
+ const user = body?.user
112
+ if (!user?.id) {
113
+ throw new APIError('BAD_REQUEST', { message: 'missing user' })
114
+ }
115
+ ;(context as PayloadSyncPluginContext).payloadSyncPlugin.queue.enqueueEnsure(
116
+ user,
117
+ true,
118
+ 'user-operation',
119
+ )
120
+ return json({ ok: true })
121
+ },
122
+ ),
123
+ },
124
+ hooks: {
125
+ before: [
126
+ {
127
+ handler: createAuthMiddleware(async (ctx) => {
128
+ const locale = ctx.getHeader('User-Locale')
129
+ return Promise.resolve({
130
+ context: { ...ctx, body: { ...ctx.body, locale: locale ?? undefined } },
131
+ })
132
+ }),
133
+ matcher: (context) => {
134
+ return context.path === '/sign-up/email'
135
+ },
136
+ },
137
+ ],
138
+ },
139
+ schema: {
140
+ user: {
141
+ fields: {
142
+ locale: {
143
+ type: 'string',
144
+ required: false,
145
+ },
146
+ },
147
+ },
148
+ },
149
+ // TODO: the queue must be destroyed on better auth instance destruction, as it utilizes timers.
150
+ async init({ internalAdapter, password }) {
151
+ if (opts.createAdmins) {
152
+ try {
153
+ await Promise.all(
154
+ opts.createAdmins.map(async ({ overwrite, user }) => {
155
+ const alreadyExistingUser = await internalAdapter.findUserByEmail(user.email)
156
+ if (alreadyExistingUser) {
157
+ if (overwrite) {
158
+ // clear accounts
159
+ await internalAdapter.deleteAccounts(alreadyExistingUser.user.id)
160
+ const createdUser = await internalAdapter.updateUser(
161
+ alreadyExistingUser.user.id,
162
+ {
163
+ ...user,
164
+ role: 'admin',
165
+ },
166
+ )
167
+ // assuming this creates an account?
168
+ await internalAdapter.linkAccount({
169
+ accountId: createdUser.id,
170
+ password: await password.hash(user.password),
171
+ providerId: 'credential',
172
+ userId: createdUser.id,
173
+ })
174
+ }
175
+ }
176
+ // if the user doesnt exist there can't be an account
177
+ else {
178
+ const createdUser = await internalAdapter.createUser({ ...user, role: 'admin' })
179
+ await internalAdapter.linkAccount({
180
+ accountId: createdUser.id,
181
+ password: await password.hash(user.password),
182
+ providerId: 'credential',
183
+ userId: createdUser.id,
184
+ })
185
+ }
186
+ }),
187
+ )
188
+ } catch (error) {
189
+ if (opts.enableLogging) {
190
+ defaultLog('Failed to create Admin user', error)
191
+ }
192
+ }
193
+ }
194
+
195
+ const queue = new Queue(
196
+ {
197
+ deleteUserFromPayload: createDeleteUserFromPayload(opts.payloadConfig),
198
+ internalAdapter,
199
+ listPayloadUsersPage: createListPayloadUsersPage(opts.payloadConfig),
200
+ log: opts.enableLogging ? defaultLog : undefined,
201
+ syncUserToPayload: createSyncUserToPayload(opts.payloadConfig),
202
+ },
203
+ opts,
204
+ )
205
+ return {
206
+ context: { payloadSyncPlugin: { queue } } as DeepPartial<Omit<AuthContext, 'options'>>,
207
+ options: {
208
+ databaseHooks: createDatabaseHooks({ config: opts.payloadConfig }),
209
+ user: { deleteUser: { enabled: true } },
210
+ },
211
+ }
212
+ },
213
+ }
214
+ }