payload-better-auth 3.1.1 → 3.2.0
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 +4 -0
- package/dist/better-auth/plugin.js +5 -5
- package/dist/better-auth/plugin.js.map +1 -1
- package/dist/better-auth/reconcile-queue.d.ts +7 -0
- package/dist/better-auth/reconcile-queue.js +23 -14
- package/dist/better-auth/reconcile-queue.js.map +1 -1
- package/dist/components/BetterAuthLoginServer.js +1 -1
- package/dist/components/BetterAuthLoginServer.js.map +1 -1
- package/package.json +11 -6
- package/src/better-auth/plugin.ts +5 -5
- package/src/better-auth/reconcile-queue.ts +27 -16
- package/src/components/BetterAuthLoginServer.tsx +1 -1
package/README.md
CHANGED
|
@@ -138,6 +138,10 @@ PAYLOAD_SECRET=your-payload-secret
|
|
|
138
138
|
DATABASE_URI=file:./payload.db
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
## Process Architecture
|
|
142
|
+
|
|
143
|
+
**Run Better Auth and Payload as separate processes.** When both run in the same process (e.g. Better Auth and Payload in one Next.js app), this plugin can trigger interaction loops between the two (reconcile → Payload hooks → sync → Better Auth → reconcile), which can cause noticeable performance issues. Running Better Auth and Payload in separate processes avoids these loops and is the recommended setup for both development and production.
|
|
144
|
+
|
|
141
145
|
## Access Control
|
|
142
146
|
|
|
143
147
|
Your access rules are preserved and combined with Better Auth's internal access. BA sync operations (signed with `BA_TO_PAYLOAD_SECRET`) always pass.
|
|
@@ -14,9 +14,9 @@ const defaultLog = (msg, extra)=>{
|
|
|
14
14
|
return {
|
|
15
15
|
user: {
|
|
16
16
|
create: {
|
|
17
|
-
after: (user)=>{
|
|
17
|
+
after: async (user)=>{
|
|
18
18
|
queue.enqueueEnsure(user, true, 'user-operation');
|
|
19
|
-
|
|
19
|
+
await queue.runEnsureNow(user);
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
delete: {
|
|
@@ -26,9 +26,9 @@ const defaultLog = (msg, extra)=>{
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
update: {
|
|
29
|
-
after: (user)=>{
|
|
29
|
+
after: async (user)=>{
|
|
30
30
|
queue.enqueueEnsure(user, true, 'user-operation');
|
|
31
|
-
|
|
31
|
+
await queue.runEnsureNow(user);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -53,7 +53,7 @@ export const payloadBetterAuthPlugin = (opts)=>{
|
|
|
53
53
|
id: 'reconcile-queue-plugin',
|
|
54
54
|
endpoints: {
|
|
55
55
|
// convenience for tests/admin tools (optional)
|
|
56
|
-
authMethods: createAuthEndpoint('/
|
|
56
|
+
authMethods: createAuthEndpoint('/methods', {
|
|
57
57
|
method: 'GET'
|
|
58
58
|
}, async ({ context, json })=>{
|
|
59
59
|
const authMethods = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/better-auth/plugin.ts"],"sourcesContent":["// src/plugins/reconcile-queue-plugin.ts\nimport type { AuthContext, BetterAuthPlugin } from 'better-auth'\nimport type { SanitizedConfig } from 'payload'\n\nimport { APIError, createAuthEndpoint, createAuthMiddleware } from 'better-auth/api'\n\nimport type { EventBus } from '../eventBus/types'\nimport type { SecondaryStorage } from '../storage/types'\nimport type { AuthMethod } from './helpers'\n\nimport { createDeduplicatedLogger } from '../shared/deduplicatedLogger'\nimport { SESSION_COOKIE_NAME_KEY, TIMESTAMP_PREFIX } from '../storage/keys'\nimport { type InitOptions, Queue } from './reconcile-queue'\nimport {\n type BAUser,\n type BetterAuthUser,\n createDeleteUserFromPayload,\n createListPayloadUsersPage,\n createSyncUserToPayload,\n} from './sources'\n\ntype PayloadSyncPluginContext = { payloadSyncPlugin: { queue: Queue } } & AuthContext\n\nconst defaultLog = (msg: string, extra?: unknown) => {\n console.log(`[reconcile] ${msg}`, extra ? JSON.stringify(extra, null, 2) : '')\n}\n\n/**\n * Type for the user data that will be written to Payload.\n * Excludes auto-generated fields.\n */\nexport type PayloadUserData<TUser extends object> = Omit<\n TUser,\n 'baUserId' | 'betterAuthAccounts' | 'createdAt' | 'id' | 'updatedAt'\n>\n\nexport interface PayloadBetterAuthPluginOptions<\n TUser extends object = Record<string, unknown>,\n TCollectionSlug extends string = string,\n> extends InitOptions {\n /**\n * Prefix for Better Auth collections in Payload (default: '__better_auth').\n * The collections will be named: {prefix}_email_password, {prefix}_magic_link\n */\n collectionPrefix?: string\n enableLogging?: boolean\n /**\n * EventBus for timestamp-based coordination between plugins.\n * Both plugins MUST share the same eventBus instance.\n *\n * Available implementations:\n * - `createSqlitePollingEventBus()` - Uses SQLite for cross-process coordination\n *\n * @example\n * // Create shared eventBus (e.g., in a separate file)\n * import { createSqlitePollingEventBus } from 'payload-better-auth'\n * import { DatabaseSync } from 'node:sqlite'\n * const db = new DatabaseSync('.event-bus.db')\n * export const eventBus = createSqlitePollingEventBus({ db })\n */\n eventBus: EventBus\n /**\n * Map Better Auth user data to Payload user fields.\n * Called on create AND update - allows filling defaults for schema changes.\n *\n * @example\n * mapUserToPayload: (baUser) => ({\n * email: baUser.email ?? '',\n * name: baUser.name ?? 'New User',\n * role: 'user', // default for new required fields\n * })\n */\n mapUserToPayload: (baUser: BetterAuthUser) => PayloadUserData<TUser>\n payloadConfig: Promise<SanitizedConfig>\n /**\n * Secondary storage for state coordination between Better Auth and Payload.\n * Both plugins MUST share the same storage instance.\n *\n * This storage is automatically passed to Better Auth as `secondaryStorage`,\n * enabling session caching - Payload validates sessions directly from storage\n * without HTTP calls to Better Auth.\n *\n * Available storage adapters:\n * - `createSqliteStorage()` - Uses Node.js 22+ native SQLite (no external dependencies, recommended for dev)\n * - `createRedisStorage(redis)` - Redis-backed, for distributed/multi-server production\n *\n * @example\n * // Create shared storage (e.g., in a separate file)\n * import { createSqliteStorage } from 'payload-better-auth'\n * import { DatabaseSync } from 'node:sqlite'\n * const db = new DatabaseSync('.sync-state.db')\n * export const storage = createSqliteStorage({ db })\n */\n storage: SecondaryStorage\n token: string // simple header token for admin endpoints\n /**\n * Slug for the Payload users collection (default: 'users').\n * Must match the collection slug defined in your Payload config.\n */\n usersSlug?: TCollectionSlug\n}\n\n/**\n * Create database hooks that enqueue user changes to the reconciliation queue.\n * All sync operations go through the queue for consistent handling with retries.\n */\nfunction createQueueBasedHooks(queue: Queue) {\n return {\n user: {\n create: {\n after: (user: BAUser): Promise<void> => {\n queue.enqueueEnsure(user, true, 'user-operation')\n return Promise.resolve()\n },\n },\n delete: {\n after: (user: BAUser): Promise<void> => {\n queue.enqueueDelete(user.id, true, 'user-operation')\n return Promise.resolve()\n },\n },\n update: {\n after: (user: BAUser): Promise<void> => {\n queue.enqueueEnsure(user, true, 'user-operation')\n return Promise.resolve()\n },\n },\n },\n }\n}\n\nexport const payloadBetterAuthPlugin = <\n TUser extends object = Record<string, unknown>,\n TCollectionSlug extends string = string,\n>(\n opts: PayloadBetterAuthPluginOptions<TUser, TCollectionSlug>,\n): BetterAuthPlugin => {\n const {\n collectionPrefix = '__better_auth',\n eventBus,\n mapUserToPayload,\n storage,\n usersSlug = 'users' as TCollectionSlug,\n } = opts\n\n // Compute derived collection slugs\n const emailPasswordSlug = `${collectionPrefix}_email_password` as TCollectionSlug\n const magicLinkSlug = `${collectionPrefix}_magic_link` as TCollectionSlug\n\n // Create deduplicated logger\n const logger = createDeduplicatedLogger({\n enabled: opts.enableLogging ?? false,\n prefix: '[better-auth]',\n storage,\n })\n\n // Keep the simple log for queue operations (they handle their own deduplication)\n const queueLog = opts.enableLogging ? defaultLog : undefined\n\n // Track subscription for cleanup\n let unsubscribeFromPayload: (() => void) | null = null\n\n return {\n id: 'reconcile-queue-plugin',\n endpoints: {\n // convenience for tests/admin tools (optional)\n authMethods: createAuthEndpoint(\n '/auth/methods',\n { method: 'GET' },\n async ({\n context,\n json,\n }) => {\n const authMethods: AuthMethod[] = []\n // Check if emailAndPassword is enabled, or if present at all (not present defaults to false)\n if (context.options.emailAndPassword?.enabled) {\n authMethods.push({\n method: 'emailAndPassword',\n options: {\n minPasswordLength: context.options.emailAndPassword.minPasswordLength ?? 0,\n },\n })\n }\n if (context.options.plugins?.some((p) => p.id === 'magic-link')) {\n authMethods.push({ method: 'magicLink' })\n }\n\n return await json(authMethods)\n },\n ),\n deleteNow: createAuthEndpoint(\n '/reconcile/delete',\n { method: 'POST' },\n async ({\n context,\n json,\n request,\n }) => {\n if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {\n throw new APIError('UNAUTHORIZED', { message: 'invalid token' })\n }\n const body = (await request?.json().catch(() => ({}))) as { baId?: string } | undefined\n const baId = body?.baId\n if (!baId) {\n throw new APIError('BAD_REQUEST', { message: 'missing baId' })\n }\n ;(context as PayloadSyncPluginContext).payloadSyncPlugin.queue.enqueueDelete(\n baId,\n true,\n 'user-operation',\n )\n return json({ ok: true })\n },\n ),\n ensureNow: createAuthEndpoint(\n '/reconcile/ensure',\n { method: 'POST' },\n async ({\n context,\n json,\n request,\n }) => {\n if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {\n throw new APIError('UNAUTHORIZED', { message: 'invalid token' })\n }\n const body = (await request?.json().catch(() => ({}))) as { user?: BAUser } | undefined\n const user = body?.user\n if (!user?.id) {\n throw new APIError('BAD_REQUEST', { message: 'missing user' })\n }\n ;(context as PayloadSyncPluginContext).payloadSyncPlugin.queue.enqueueEnsure(\n user,\n true,\n 'user-operation',\n )\n return json({ ok: true })\n },\n ),\n run: createAuthEndpoint(\n '/reconcile/run',\n { method: 'POST' },\n async ({\n context,\n json,\n request,\n }) => {\n if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {\n throw new APIError('UNAUTHORIZED', { message: 'invalid token' })\n }\n await (context as PayloadSyncPluginContext).payloadSyncPlugin.queue.seedFullReconcile()\n return json({ ok: true })\n },\n ),\n status: createAuthEndpoint(\n '/reconcile/status',\n { method: 'GET' },\n async ({\n context,\n json,\n request,\n }) => {\n if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {\n return Promise.reject(\n new APIError('UNAUTHORIZED', { message: 'invalid token' }) as Error,\n )\n }\n return json((context as PayloadSyncPluginContext).payloadSyncPlugin.queue.status())\n },\n ),\n // Warmup endpoint - triggers plugin initialization without auth\n // Returns basic instance info\n warmup: createAuthEndpoint(\n '/warmup',\n { method: 'GET' },\n async ({\n context,\n json,\n }) => {\n const authMethods: string[] = []\n if (context.options.emailAndPassword?.enabled) {\n authMethods.push('emailAndPassword')\n }\n if (context.options.plugins?.some((p) => p.id === 'magic-link')) {\n authMethods.push('magicLink')\n }\n\n return json({\n authMethods,\n initialized: true,\n pluginId: 'reconcile-queue-plugin',\n timestamp: new Date().toISOString(),\n })\n },\n ),\n },\n hooks: {\n before: [\n {\n handler: createAuthMiddleware(async (ctx) => {\n const locale = ctx.getHeader('User-Locale')\n return Promise.resolve({\n context: { ...ctx, body: { ...ctx.body, locale: locale ?? undefined } },\n })\n }),\n matcher: (context) => {\n return context.path === '/sign-up/email'\n },\n },\n ],\n },\n async init({ internalAdapter, options }) {\n // Always log init start for debugging\n logger.always('Plugin init started')\n\n // Compute and store the session cookie name for Payload to read\n // This accounts for cookiePrefix, custom cookie names, and __Secure- prefix\n const cookiePrefix = options.advanced?.cookiePrefix ?? 'better-auth'\n const customCookieName = options.advanced?.cookies?.session_token?.name\n // Better Auth uses secure cookies when:\n // 1. Explicitly set via useSecureCookies option\n // 2. NODE_ENV is 'production'\n // 3. baseURL starts with 'https://'\n const baseUrlStr = typeof options.baseURL === 'string' ? options.baseURL : undefined\n const isHttps = baseUrlStr?.startsWith('https://') ?? false\n const useSecureCookies =\n options.advanced?.useSecureCookies ?? (process.env.NODE_ENV === 'production' || isHttps)\n\n let sessionCookieName: string\n if (customCookieName) {\n // Custom cookie name takes precedence\n sessionCookieName = useSecureCookies ? `__Secure-${customCookieName}` : customCookieName\n } else {\n // Default format: {prefix}.session_token\n const baseName = `${cookiePrefix}.session_token`\n sessionCookieName = useSecureCookies ? `__Secure-${baseName}` : baseName\n }\n\n // Store session cookie name in KV for Payload plugin to read\n await storage.set(SESSION_COOKIE_NAME_KEY, sessionCookieName)\n await logger.log('cookie-config', `Session cookie name: ${sessionCookieName}`)\n\n // Create the reconciliation queue\n const queue = new Queue(\n {\n collectionPrefix,\n deleteUserFromPayload: createDeleteUserFromPayload(\n opts.payloadConfig,\n emailPasswordSlug,\n magicLinkSlug,\n usersSlug,\n ),\n internalAdapter,\n listPayloadUsersPage: createListPayloadUsersPage(opts.payloadConfig, usersSlug),\n log: queueLog,\n mapUserToPayload,\n syncUserToPayload: createSyncUserToPayload(\n opts.payloadConfig,\n emailPasswordSlug,\n magicLinkSlug,\n usersSlug,\n mapUserToPayload,\n ),\n },\n {\n ...opts,\n // Don't run reconcile on boot - we use timestamp-based coordination instead\n runOnBoot: false,\n },\n )\n\n // Log init (deduplicated)\n await logger.log('init', 'Initialized')\n\n // Timestamp-based reconciliation coordination\n async function attemptReconciliation(): Promise<void> {\n logger.always('Syncing users to Payload...')\n await storage.set(TIMESTAMP_PREFIX + 'better-auth', String(Date.now()))\n try {\n await queue.seedFullReconcile()\n logger.always('Sync completed successfully')\n // Success - unsubscribe if we were watching\n if (unsubscribeFromPayload) {\n unsubscribeFromPayload()\n unsubscribeFromPayload = null\n }\n } catch (error) {\n logger.always('Sync failed, will retry when Payload restarts', error)\n // Subscribe to Payload timestamp changes if not already\n if (!unsubscribeFromPayload) {\n unsubscribeFromPayload = eventBus.subscribeToTimestamp('payload', () => {\n attemptReconciliation().catch((err) => {\n logger.always('Sync attempt failed', err)\n })\n })\n }\n }\n }\n\n // Check if Payload is online and started more recently than our last reconcile\n const payloadTsStr = await storage.get(TIMESTAMP_PREFIX + 'payload')\n const baTsStr = await storage.get(TIMESTAMP_PREFIX + 'better-auth')\n const payloadTs = payloadTsStr ? parseInt(payloadTsStr, 10) : null\n const baTs = baTsStr ? parseInt(baTsStr, 10) : null\n\n // Determine reconciliation state\n logger.always('Checking reconciliation state', {\n baTs: baTs ? new Date(baTs).toISOString() : null,\n payloadTs: payloadTs ? new Date(payloadTs).toISOString() : null,\n })\n\n if (payloadTs === null) {\n // Payload hasn't started yet\n logger.always('Waiting for Payload to start...')\n unsubscribeFromPayload = eventBus.subscribeToTimestamp('payload', () => {\n attemptReconciliation().catch((err) => {\n logger.always('Sync attempt failed', err)\n })\n })\n } else if (baTs === null) {\n // First run - always sync\n logger.always('First run - triggering initial sync')\n attemptReconciliation().catch((err) => {\n logger.always('Initial sync failed', err)\n })\n } else if (payloadTs > baTs) {\n // Payload restarted since last reconcile - sync needed\n logger.always('Payload restarted - triggering sync')\n attemptReconciliation().catch((err) => {\n logger.always('Sync failed', err)\n })\n } else {\n // Already reconciled and up-to-date\n logger.always('Already synchronized', {\n lastSync: new Date(baTs).toISOString(),\n })\n unsubscribeFromPayload = eventBus.subscribeToTimestamp('payload', () => {\n attemptReconciliation().catch((err) => {\n logger.always('Sync attempt failed', err)\n })\n })\n }\n\n // Create queue-based database hooks - all user sync goes through the queue\n const queueBasedHooks = createQueueBasedHooks(queue)\n\n return {\n context: { payloadSyncPlugin: { queue } },\n options: {\n databaseHooks: queueBasedHooks,\n // Pass storage to Better Auth as secondaryStorage - this makes BA write sessions\n // to the shared storage, allowing Payload to validate sessions directly from cache\n secondaryStorage: storage,\n user: { deleteUser: { enabled: true } },\n },\n }\n },\n schema: {\n user: {\n fields: {\n locale: {\n type: 'string',\n required: false,\n },\n },\n },\n },\n } satisfies BetterAuthPlugin\n}\n"],"names":["APIError","createAuthEndpoint","createAuthMiddleware","createDeduplicatedLogger","SESSION_COOKIE_NAME_KEY","TIMESTAMP_PREFIX","Queue","createDeleteUserFromPayload","createListPayloadUsersPage","createSyncUserToPayload","defaultLog","msg","extra","console","log","JSON","stringify","createQueueBasedHooks","queue","user","create","after","enqueueEnsure","Promise","resolve","delete","enqueueDelete","id","update","payloadBetterAuthPlugin","opts","collectionPrefix","eventBus","mapUserToPayload","storage","usersSlug","emailPasswordSlug","magicLinkSlug","logger","enabled","enableLogging","prefix","queueLog","undefined","unsubscribeFromPayload","endpoints","authMethods","method","context","json","options","emailAndPassword","push","minPasswordLength","plugins","some","p","deleteNow","request","token","headers","get","message","body","catch","baId","payloadSyncPlugin","ok","ensureNow","run","seedFullReconcile","status","reject","warmup","initialized","pluginId","timestamp","Date","toISOString","hooks","before","handler","ctx","locale","getHeader","matcher","path","init","internalAdapter","always","cookiePrefix","advanced","customCookieName","cookies","session_token","name","baseUrlStr","baseURL","isHttps","startsWith","useSecureCookies","process","env","NODE_ENV","sessionCookieName","baseName","set","deleteUserFromPayload","payloadConfig","listPayloadUsersPage","syncUserToPayload","runOnBoot","attemptReconciliation","String","now","error","subscribeToTimestamp","err","payloadTsStr","baTsStr","payloadTs","parseInt","baTs","lastSync","queueBasedHooks","databaseHooks","secondaryStorage","deleteUser","schema","fields","type","required"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/better-auth/plugin.ts"],"sourcesContent":["// src/plugins/reconcile-queue-plugin.ts\nimport type { AuthContext, BetterAuthPlugin } from 'better-auth'\nimport type { SanitizedConfig } from 'payload'\n\nimport { APIError, createAuthEndpoint, createAuthMiddleware } from 'better-auth/api'\n\nimport type { EventBus } from '../eventBus/types'\nimport type { SecondaryStorage } from '../storage/types'\nimport type { AuthMethod } from './helpers'\n\nimport { createDeduplicatedLogger } from '../shared/deduplicatedLogger'\nimport { SESSION_COOKIE_NAME_KEY, TIMESTAMP_PREFIX } from '../storage/keys'\nimport { type InitOptions, Queue } from './reconcile-queue'\nimport {\n type BAUser,\n type BetterAuthUser,\n createDeleteUserFromPayload,\n createListPayloadUsersPage,\n createSyncUserToPayload,\n} from './sources'\n\ntype PayloadSyncPluginContext = { payloadSyncPlugin: { queue: Queue } } & AuthContext\n\nconst defaultLog = (msg: string, extra?: unknown) => {\n console.log(`[reconcile] ${msg}`, extra ? JSON.stringify(extra, null, 2) : '')\n}\n\n/**\n * Type for the user data that will be written to Payload.\n * Excludes auto-generated fields.\n */\nexport type PayloadUserData<TUser extends object> = Omit<\n TUser,\n 'baUserId' | 'betterAuthAccounts' | 'createdAt' | 'id' | 'updatedAt'\n>\n\nexport interface PayloadBetterAuthPluginOptions<\n TUser extends object = Record<string, unknown>,\n TCollectionSlug extends string = string,\n> extends InitOptions {\n /**\n * Prefix for Better Auth collections in Payload (default: '__better_auth').\n * The collections will be named: {prefix}_email_password, {prefix}_magic_link\n */\n collectionPrefix?: string\n enableLogging?: boolean\n /**\n * EventBus for timestamp-based coordination between plugins.\n * Both plugins MUST share the same eventBus instance.\n *\n * Available implementations:\n * - `createSqlitePollingEventBus()` - Uses SQLite for cross-process coordination\n *\n * @example\n * // Create shared eventBus (e.g., in a separate file)\n * import { createSqlitePollingEventBus } from 'payload-better-auth'\n * import { DatabaseSync } from 'node:sqlite'\n * const db = new DatabaseSync('.event-bus.db')\n * export const eventBus = createSqlitePollingEventBus({ db })\n */\n eventBus: EventBus\n /**\n * Map Better Auth user data to Payload user fields.\n * Called on create AND update - allows filling defaults for schema changes.\n *\n * @example\n * mapUserToPayload: (baUser) => ({\n * email: baUser.email ?? '',\n * name: baUser.name ?? 'New User',\n * role: 'user', // default for new required fields\n * })\n */\n mapUserToPayload: (baUser: BetterAuthUser) => PayloadUserData<TUser>\n payloadConfig: Promise<SanitizedConfig>\n /**\n * Secondary storage for state coordination between Better Auth and Payload.\n * Both plugins MUST share the same storage instance.\n *\n * This storage is automatically passed to Better Auth as `secondaryStorage`,\n * enabling session caching - Payload validates sessions directly from storage\n * without HTTP calls to Better Auth.\n *\n * Available storage adapters:\n * - `createSqliteStorage()` - Uses Node.js 22+ native SQLite (no external dependencies, recommended for dev)\n * - `createRedisStorage(redis)` - Redis-backed, for distributed/multi-server production\n *\n * @example\n * // Create shared storage (e.g., in a separate file)\n * import { createSqliteStorage } from 'payload-better-auth'\n * import { DatabaseSync } from 'node:sqlite'\n * const db = new DatabaseSync('.sync-state.db')\n * export const storage = createSqliteStorage({ db })\n */\n storage: SecondaryStorage\n token: string // simple header token for admin endpoints\n /**\n * Slug for the Payload users collection (default: 'users').\n * Must match the collection slug defined in your Payload config.\n */\n usersSlug?: TCollectionSlug\n}\n\n/**\n * Create database hooks that enqueue user changes to the reconciliation queue.\n * All sync operations go through the queue for consistent handling with retries.\n */\nfunction createQueueBasedHooks(queue: Queue) {\n return {\n user: {\n create: {\n after: async (user: BAUser): Promise<void> => {\n queue.enqueueEnsure(user, true, 'user-operation')\n await queue.runEnsureNow(user)\n },\n },\n delete: {\n after: (user: BAUser): Promise<void> => {\n queue.enqueueDelete(user.id, true, 'user-operation')\n return Promise.resolve()\n },\n },\n update: {\n after: async (user: BAUser): Promise<void> => {\n queue.enqueueEnsure(user, true, 'user-operation')\n await queue.runEnsureNow(user)\n },\n },\n },\n }\n}\n\nexport const payloadBetterAuthPlugin = <\n TUser extends object = Record<string, unknown>,\n TCollectionSlug extends string = string,\n>(\n opts: PayloadBetterAuthPluginOptions<TUser, TCollectionSlug>,\n): BetterAuthPlugin => {\n const {\n collectionPrefix = '__better_auth',\n eventBus,\n mapUserToPayload,\n storage,\n usersSlug = 'users' as TCollectionSlug,\n } = opts\n\n // Compute derived collection slugs\n const emailPasswordSlug = `${collectionPrefix}_email_password` as TCollectionSlug\n const magicLinkSlug = `${collectionPrefix}_magic_link` as TCollectionSlug\n\n // Create deduplicated logger\n const logger = createDeduplicatedLogger({\n enabled: opts.enableLogging ?? false,\n prefix: '[better-auth]',\n storage,\n })\n\n // Keep the simple log for queue operations (they handle their own deduplication)\n const queueLog = opts.enableLogging ? defaultLog : undefined\n\n // Track subscription for cleanup\n let unsubscribeFromPayload: (() => void) | null = null\n\n return {\n id: 'reconcile-queue-plugin',\n endpoints: {\n // convenience for tests/admin tools (optional)\n authMethods: createAuthEndpoint(\n '/methods',\n { method: 'GET' },\n async ({\n context,\n json,\n }) => {\n const authMethods: AuthMethod[] = []\n // Check if emailAndPassword is enabled, or if present at all (not present defaults to false)\n if (context.options.emailAndPassword?.enabled) {\n authMethods.push({\n method: 'emailAndPassword',\n options: {\n minPasswordLength: context.options.emailAndPassword.minPasswordLength ?? 0,\n },\n })\n }\n if (context.options.plugins?.some((p) => p.id === 'magic-link')) {\n authMethods.push({ method: 'magicLink' })\n }\n\n return await json(authMethods)\n },\n ),\n deleteNow: createAuthEndpoint(\n '/reconcile/delete',\n { method: 'POST' },\n async ({\n context,\n json,\n request,\n }) => {\n if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {\n throw new APIError('UNAUTHORIZED', { message: 'invalid token' })\n }\n const body = (await request?.json().catch(() => ({}))) as { baId?: string } | undefined\n const baId = body?.baId\n if (!baId) {\n throw new APIError('BAD_REQUEST', { message: 'missing baId' })\n }\n ;(context as PayloadSyncPluginContext).payloadSyncPlugin.queue.enqueueDelete(\n baId,\n true,\n 'user-operation',\n )\n return json({ ok: true })\n },\n ),\n ensureNow: createAuthEndpoint(\n '/reconcile/ensure',\n { method: 'POST' },\n async ({\n context,\n json,\n request,\n }) => {\n if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {\n throw new APIError('UNAUTHORIZED', { message: 'invalid token' })\n }\n const body = (await request?.json().catch(() => ({}))) as { user?: BAUser } | undefined\n const user = body?.user\n if (!user?.id) {\n throw new APIError('BAD_REQUEST', { message: 'missing user' })\n }\n ;(context as PayloadSyncPluginContext).payloadSyncPlugin.queue.enqueueEnsure(\n user,\n true,\n 'user-operation',\n )\n return json({ ok: true })\n },\n ),\n run: createAuthEndpoint(\n '/reconcile/run',\n { method: 'POST' },\n async ({\n context,\n json,\n request,\n }) => {\n if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {\n throw new APIError('UNAUTHORIZED', { message: 'invalid token' })\n }\n await (context as PayloadSyncPluginContext).payloadSyncPlugin.queue.seedFullReconcile()\n return json({ ok: true })\n },\n ),\n status: createAuthEndpoint(\n '/reconcile/status',\n { method: 'GET' },\n async ({\n context,\n json,\n request,\n }) => {\n if (opts.token && request?.headers.get('x-reconcile-token') !== opts.token) {\n return Promise.reject(\n new APIError('UNAUTHORIZED', { message: 'invalid token' }) as Error,\n )\n }\n return json((context as PayloadSyncPluginContext).payloadSyncPlugin.queue.status())\n },\n ),\n // Warmup endpoint - triggers plugin initialization without auth\n // Returns basic instance info\n warmup: createAuthEndpoint(\n '/warmup',\n { method: 'GET' },\n async ({\n context,\n json,\n }) => {\n const authMethods: string[] = []\n if (context.options.emailAndPassword?.enabled) {\n authMethods.push('emailAndPassword')\n }\n if (context.options.plugins?.some((p) => p.id === 'magic-link')) {\n authMethods.push('magicLink')\n }\n\n return json({\n authMethods,\n initialized: true,\n pluginId: 'reconcile-queue-plugin',\n timestamp: new Date().toISOString(),\n })\n },\n ),\n },\n hooks: {\n before: [\n {\n handler: createAuthMiddleware(async (ctx) => {\n const locale = ctx.getHeader('User-Locale')\n return Promise.resolve({\n context: { ...ctx, body: { ...ctx.body, locale: locale ?? undefined } },\n })\n }),\n matcher: (context) => {\n return context.path === '/sign-up/email'\n },\n },\n ],\n },\n async init({ internalAdapter, options }) {\n // Always log init start for debugging\n logger.always('Plugin init started')\n\n // Compute and store the session cookie name for Payload to read\n // This accounts for cookiePrefix, custom cookie names, and __Secure- prefix\n const cookiePrefix = options.advanced?.cookiePrefix ?? 'better-auth'\n const customCookieName = options.advanced?.cookies?.session_token?.name\n // Better Auth uses secure cookies when:\n // 1. Explicitly set via useSecureCookies option\n // 2. NODE_ENV is 'production'\n // 3. baseURL starts with 'https://'\n const baseUrlStr = typeof options.baseURL === 'string' ? options.baseURL : undefined\n const isHttps = baseUrlStr?.startsWith('https://') ?? false\n const useSecureCookies =\n options.advanced?.useSecureCookies ?? (process.env.NODE_ENV === 'production' || isHttps)\n\n let sessionCookieName: string\n if (customCookieName) {\n // Custom cookie name takes precedence\n sessionCookieName = useSecureCookies ? `__Secure-${customCookieName}` : customCookieName\n } else {\n // Default format: {prefix}.session_token\n const baseName = `${cookiePrefix}.session_token`\n sessionCookieName = useSecureCookies ? `__Secure-${baseName}` : baseName\n }\n\n // Store session cookie name in KV for Payload plugin to read\n await storage.set(SESSION_COOKIE_NAME_KEY, sessionCookieName)\n await logger.log('cookie-config', `Session cookie name: ${sessionCookieName}`)\n\n // Create the reconciliation queue\n const queue = new Queue(\n {\n collectionPrefix,\n deleteUserFromPayload: createDeleteUserFromPayload(\n opts.payloadConfig,\n emailPasswordSlug,\n magicLinkSlug,\n usersSlug,\n ),\n internalAdapter,\n listPayloadUsersPage: createListPayloadUsersPage(opts.payloadConfig, usersSlug),\n log: queueLog,\n mapUserToPayload,\n syncUserToPayload: createSyncUserToPayload(\n opts.payloadConfig,\n emailPasswordSlug,\n magicLinkSlug,\n usersSlug,\n mapUserToPayload,\n ),\n },\n {\n ...opts,\n // Don't run reconcile on boot - we use timestamp-based coordination instead\n runOnBoot: false,\n },\n )\n\n // Log init (deduplicated)\n await logger.log('init', 'Initialized')\n\n // Timestamp-based reconciliation coordination\n async function attemptReconciliation(): Promise<void> {\n logger.always('Syncing users to Payload...')\n await storage.set(TIMESTAMP_PREFIX + 'better-auth', String(Date.now()))\n try {\n await queue.seedFullReconcile()\n logger.always('Sync completed successfully')\n // Success - unsubscribe if we were watching\n if (unsubscribeFromPayload) {\n unsubscribeFromPayload()\n unsubscribeFromPayload = null\n }\n } catch (error) {\n logger.always('Sync failed, will retry when Payload restarts', error)\n // Subscribe to Payload timestamp changes if not already\n if (!unsubscribeFromPayload) {\n unsubscribeFromPayload = eventBus.subscribeToTimestamp('payload', () => {\n attemptReconciliation().catch((err) => {\n logger.always('Sync attempt failed', err)\n })\n })\n }\n }\n }\n\n // Check if Payload is online and started more recently than our last reconcile\n const payloadTsStr = await storage.get(TIMESTAMP_PREFIX + 'payload')\n const baTsStr = await storage.get(TIMESTAMP_PREFIX + 'better-auth')\n const payloadTs = payloadTsStr ? parseInt(payloadTsStr, 10) : null\n const baTs = baTsStr ? parseInt(baTsStr, 10) : null\n\n // Determine reconciliation state\n logger.always('Checking reconciliation state', {\n baTs: baTs ? new Date(baTs).toISOString() : null,\n payloadTs: payloadTs ? new Date(payloadTs).toISOString() : null,\n })\n\n if (payloadTs === null) {\n // Payload hasn't started yet\n logger.always('Waiting for Payload to start...')\n unsubscribeFromPayload = eventBus.subscribeToTimestamp('payload', () => {\n attemptReconciliation().catch((err) => {\n logger.always('Sync attempt failed', err)\n })\n })\n } else if (baTs === null) {\n // First run - always sync\n logger.always('First run - triggering initial sync')\n attemptReconciliation().catch((err) => {\n logger.always('Initial sync failed', err)\n })\n } else if (payloadTs > baTs) {\n // Payload restarted since last reconcile - sync needed\n logger.always('Payload restarted - triggering sync')\n attemptReconciliation().catch((err) => {\n logger.always('Sync failed', err)\n })\n } else {\n // Already reconciled and up-to-date\n logger.always('Already synchronized', {\n lastSync: new Date(baTs).toISOString(),\n })\n unsubscribeFromPayload = eventBus.subscribeToTimestamp('payload', () => {\n attemptReconciliation().catch((err) => {\n logger.always('Sync attempt failed', err)\n })\n })\n }\n\n // Create queue-based database hooks - all user sync goes through the queue\n const queueBasedHooks = createQueueBasedHooks(queue)\n\n return {\n context: { payloadSyncPlugin: { queue } },\n options: {\n databaseHooks: queueBasedHooks,\n // Pass storage to Better Auth as secondaryStorage - this makes BA write sessions\n // to the shared storage, allowing Payload to validate sessions directly from cache\n secondaryStorage: storage,\n user: { deleteUser: { enabled: true } },\n },\n }\n },\n schema: {\n user: {\n fields: {\n locale: {\n type: 'string',\n required: false,\n },\n },\n },\n },\n } satisfies BetterAuthPlugin\n}\n"],"names":["APIError","createAuthEndpoint","createAuthMiddleware","createDeduplicatedLogger","SESSION_COOKIE_NAME_KEY","TIMESTAMP_PREFIX","Queue","createDeleteUserFromPayload","createListPayloadUsersPage","createSyncUserToPayload","defaultLog","msg","extra","console","log","JSON","stringify","createQueueBasedHooks","queue","user","create","after","enqueueEnsure","runEnsureNow","delete","enqueueDelete","id","Promise","resolve","update","payloadBetterAuthPlugin","opts","collectionPrefix","eventBus","mapUserToPayload","storage","usersSlug","emailPasswordSlug","magicLinkSlug","logger","enabled","enableLogging","prefix","queueLog","undefined","unsubscribeFromPayload","endpoints","authMethods","method","context","json","options","emailAndPassword","push","minPasswordLength","plugins","some","p","deleteNow","request","token","headers","get","message","body","catch","baId","payloadSyncPlugin","ok","ensureNow","run","seedFullReconcile","status","reject","warmup","initialized","pluginId","timestamp","Date","toISOString","hooks","before","handler","ctx","locale","getHeader","matcher","path","init","internalAdapter","always","cookiePrefix","advanced","customCookieName","cookies","session_token","name","baseUrlStr","baseURL","isHttps","startsWith","useSecureCookies","process","env","NODE_ENV","sessionCookieName","baseName","set","deleteUserFromPayload","payloadConfig","listPayloadUsersPage","syncUserToPayload","runOnBoot","attemptReconciliation","String","now","error","subscribeToTimestamp","err","payloadTsStr","baTsStr","payloadTs","parseInt","baTs","lastSync","queueBasedHooks","databaseHooks","secondaryStorage","deleteUser","schema","fields","type","required"],"mappings":"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"}
|
|
@@ -45,6 +45,7 @@ export declare class Queue {
|
|
|
45
45
|
private clearFullReconcileTasks;
|
|
46
46
|
private enqueue;
|
|
47
47
|
private listBAUsersPage;
|
|
48
|
+
private runEnsure;
|
|
48
49
|
private runTask;
|
|
49
50
|
private scheduleNextReconcile;
|
|
50
51
|
/** Paginated approach: process users page by page to reduce memory usage */
|
|
@@ -52,6 +53,12 @@ export declare class Queue {
|
|
|
52
53
|
private tick;
|
|
53
54
|
enqueueDelete(baId: string, priority?: boolean, source?: TaskSource, reconcileId?: string): void;
|
|
54
55
|
enqueueEnsure(user: BAUser, priority?: boolean, source?: TaskSource, reconcileId?: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Run ensure (sync user to Payload) immediately without waiting for the queue tick.
|
|
58
|
+
* Used from database hooks so that e.g. magic-link redirect sees the user in Payload
|
|
59
|
+
* before the response is sent. The task remains enqueued for idempotent retry.
|
|
60
|
+
*/
|
|
61
|
+
runEnsureNow(user: BAUser): Promise<void>;
|
|
55
62
|
/** Seed tasks by comparing users page by page (Better-Auth → Payload). */
|
|
56
63
|
seedFullReconcile(): Promise<void>;
|
|
57
64
|
start({ reconcileEveryMs, tickMs }?: {
|
|
@@ -88,6 +88,21 @@ export class Queue {
|
|
|
88
88
|
users
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
|
+
async runEnsure(baUser) {
|
|
92
|
+
const log = this.deps?.log ?? (()=>{});
|
|
93
|
+
const baId = baUser.id;
|
|
94
|
+
// Fetch accounts from Better Auth for this user
|
|
95
|
+
const accounts = await this.deps.internalAdapter.findAccounts(baId);
|
|
96
|
+
log('queue.ensure.accounts', {
|
|
97
|
+
accountCount: accounts?.length ?? 0,
|
|
98
|
+
accounts: accounts?.map((a)=>({
|
|
99
|
+
id: a.id,
|
|
100
|
+
providerId: a.providerId
|
|
101
|
+
})),
|
|
102
|
+
baId
|
|
103
|
+
});
|
|
104
|
+
await this.deps.syncUserToPayload(baUser, accounts);
|
|
105
|
+
}
|
|
91
106
|
async runTask(t) {
|
|
92
107
|
const log = this.deps?.log ?? (()=>{});
|
|
93
108
|
if (t.kind === 'ensure') {
|
|
@@ -95,23 +110,10 @@ export class Queue {
|
|
|
95
110
|
attempts: t.attempts,
|
|
96
111
|
baId: t.baId
|
|
97
112
|
});
|
|
98
|
-
// Get user data (either from task or fetch from BA)
|
|
99
113
|
const baUser = t.baUser ?? {
|
|
100
114
|
id: t.baId
|
|
101
115
|
};
|
|
102
|
-
|
|
103
|
-
const accounts = await this.deps.internalAdapter.findAccounts(t.baId);
|
|
104
|
-
// Debug: log what accounts were found
|
|
105
|
-
log('queue.ensure.accounts', {
|
|
106
|
-
accountCount: accounts?.length ?? 0,
|
|
107
|
-
accounts: accounts?.map((a)=>({
|
|
108
|
-
id: a.id,
|
|
109
|
-
providerId: a.providerId
|
|
110
|
-
})),
|
|
111
|
-
baId: t.baId
|
|
112
|
-
});
|
|
113
|
-
// Sync user with accounts to Payload
|
|
114
|
-
await this.deps.syncUserToPayload(baUser, accounts);
|
|
116
|
+
await this.runEnsure(baUser);
|
|
115
117
|
return;
|
|
116
118
|
}
|
|
117
119
|
// delete
|
|
@@ -264,6 +266,13 @@ export class Queue {
|
|
|
264
266
|
source
|
|
265
267
|
}, priority);
|
|
266
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Run ensure (sync user to Payload) immediately without waiting for the queue tick.
|
|
271
|
+
* Used from database hooks so that e.g. magic-link redirect sees the user in Payload
|
|
272
|
+
* before the response is sent. The task remains enqueued for idempotent retry.
|
|
273
|
+
*/ async runEnsureNow(user) {
|
|
274
|
+
await this.runEnsure(user);
|
|
275
|
+
}
|
|
267
276
|
/** Seed tasks by comparing users page by page (Better-Auth → Payload). */ async seedFullReconcile() {
|
|
268
277
|
const log = this.deps?.log ?? (()=>{});
|
|
269
278
|
this.lastSeedAt = new Date().toISOString();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/better-auth/reconcile-queue.ts"],"sourcesContent":["import type { AuthContext } from 'better-auth'\n\n// src/reconcile-queue.ts\nimport type { BAUser, BetterAuthAccount, BetterAuthUser, PayloadUser } from './sources'\n\nexport interface QueueDeps {\n /** Prefix for Better Auth collections */\n collectionPrefix: string\n\n /** Delete user and associated BA collection entries from Payload */\n deleteUserFromPayload: (baId: string) => Promise<void>\n\n /** Better Auth internal adapter for fetching users and accounts */\n internalAdapter: AuthContext['internalAdapter']\n\n // Paginated loaders (efficient processing)\n listPayloadUsersPage: (\n limit: number,\n page: number,\n ) => Promise<{ hasNextPage: boolean; total: number; users: PayloadUser[] }>\n\n // Logging\n log?: (msg: string, extra?: unknown) => void\n\n /** Map BA user to Payload user data */\n mapUserToPayload: (baUser: BetterAuthUser) => Record<string, unknown>\n\n // Policy\n prunePayloadOrphans?: boolean // default: false\n\n /** Sync user and BA collection entries to Payload */\n syncUserToPayload: (baUser: BAUser, accounts?: BetterAuthAccount[]) => Promise<void>\n}\n\nexport type TaskSource = 'full-reconcile' | 'user-operation'\n\n// Bootstrap options interface\nexport interface InitOptions {\n forceReset?: boolean\n reconcileEveryMs?: number\n runOnBoot?: boolean\n tickMs?: number\n}\n\ntype Task =\n | {\n attempts: number\n baId: string\n baUser?: BAUser\n kind: 'ensure'\n nextAt: number\n reconcileId?: string\n source: TaskSource\n }\n | {\n attempts: number\n baId: string\n kind: 'delete'\n nextAt: number\n reconcileId?: string\n source: TaskSource\n }\n\nconst KEY = (t: Task) => `${t.kind}:${t.baId}`\n\nexport class Queue {\n private deps!: QueueDeps\n private failed = 0\n private keys = new Map<string, Task>()\n private lastError: null | string = null\n private lastSeedAt: null | string = null\n private processed = 0\n\n private processing = false\n private q: Task[] = []\n private reconcileEveryMs = 30 * 60_000 // default 30 minutes\n private reconcileTimeout: NodeJS.Timeout | null = null\n private reconciling = false\n\n private tickTimer: NodeJS.Timeout | null = null\n\n constructor(deps: QueueDeps, opts: InitOptions = {}) {\n this.deps = deps\n\n // Start timers but don't run reconcile immediately\n this.start({\n reconcileEveryMs: opts?.reconcileEveryMs ?? 30 * 60_000,\n tickMs: opts?.tickMs ?? 1000,\n })\n\n // Defer the initial reconcile to avoid circular dependency issues\n if (opts?.runOnBoot ?? true) {\n // Use setTimeout instead of queueMicrotask to give more time for initialization\n setTimeout(() => {\n this.seedFullReconcile().catch(\n (err) => this.deps.log && this.deps.log('[reconcile] seed failed', err),\n )\n }, 2000) // 2 second delay to allow Better Auth and Payload to fully initialize\n }\n }\n\n private bumpFront(task: Task) {\n this.q = [task, ...this.q.filter((t) => t !== task)]\n }\n\n /** Clear all full-reconcile tasks from the queue, preserving user-operation tasks */\n private clearFullReconcileTasks() {\n const log = this.deps?.log ?? (() => {})\n const beforeCount = this.q.length\n const fullReconcileCount = this.q.filter((t) => t.source === 'full-reconcile').length\n\n // Remove full-reconcile tasks from queue and keys map\n this.q = this.q.filter((task) => {\n if (task.source === 'full-reconcile') {\n this.keys.delete(KEY(task))\n return false\n }\n return true\n })\n\n const afterCount = this.q.length\n log('reconcile.clear-previous', {\n afterCount,\n beforeCount,\n clearedFullReconcile: fullReconcileCount,\n preservedUserOps: afterCount,\n })\n }\n\n // ——— Internals ———\n private enqueue(task: Task, priority: boolean) {\n const k = KEY(task)\n const existing = this.keys.get(k)\n if (existing) {\n if (task.kind === 'ensure' && existing.kind === 'ensure' && !existing.baUser && task.baUser) {\n existing.baUser = task.baUser\n }\n if (priority) {\n this.bumpFront(existing)\n }\n return\n }\n if (priority) {\n this.q.unshift(task)\n } else {\n this.q.push(task)\n }\n this.keys.set(k, task)\n }\n\n private async listBAUsersPage({ limit, offset }: { limit: number; offset: number }) {\n // sort by newest (used) first\n // when a delete is happening in the meantime, this will lead to some users not being listed (as the index changes)\n // TODO: fix this by maintaining a delete list.\n const total = await this.deps.internalAdapter.countTotalUsers()\n const users = await this.deps.internalAdapter.listUsers(limit, offset, {\n direction: 'desc',\n field: 'updatedAt',\n })\n return { total, users }\n }\n\n private async runTask(t: Task) {\n const log = this.deps?.log ?? (() => {})\n if (t.kind === 'ensure') {\n log('queue.ensure', { attempts: t.attempts, baId: t.baId })\n\n // Get user data (either from task or fetch from BA)\n const baUser = t.baUser ?? { id: t.baId }\n\n // Fetch accounts from Better Auth for this user\n const accounts = await this.deps.internalAdapter.findAccounts(t.baId)\n\n // Debug: log what accounts were found\n log('queue.ensure.accounts', {\n accountCount: accounts?.length ?? 0,\n accounts: accounts?.map((a) => ({ id: a.id, providerId: a.providerId })),\n baId: t.baId,\n })\n\n // Sync user with accounts to Payload\n await this.deps.syncUserToPayload(baUser, accounts as BetterAuthAccount[])\n return\n }\n // delete\n log('queue.delete', { attempts: t.attempts, baId: t.baId })\n await this.deps.deleteUserFromPayload(t.baId)\n }\n private scheduleNextReconcile() {\n if (this.reconcileTimeout) {\n clearTimeout(this.reconcileTimeout)\n }\n\n this.reconcileTimeout = setTimeout(async () => {\n if (!this.reconciling) {\n this.reconciling = true\n try {\n await this.seedFullReconcile()\n } catch (_error) {\n // Error is already logged in seedFullReconcile\n } finally {\n this.reconciling = false\n // Schedule the next reconcile after this one completes\n this.scheduleNextReconcile()\n }\n }\n }, this.reconcileEveryMs)\n\n // Optional unref for Node.js environments to prevent keeping process alive\n if ('unref' in this.reconcileTimeout && typeof this.reconcileTimeout.unref === 'function') {\n this.reconcileTimeout.unref()\n }\n }\n\n /** Paginated approach: process users page by page to reduce memory usage */\n private async seedFullReconcilePaginated(reconcileId: string) {\n const log = this.deps?.log ?? (() => {})\n const pageSize = 500\n let baIdSet: null | Set<string> = null\n\n // If we need to prune orphans, we need to collect all BA user IDs\n if (this.deps.prunePayloadOrphans) {\n baIdSet = new Set<string>()\n let baOffset = 0\n let baTotal = 0\n\n do {\n const { total, users: baUsers } = await this.listBAUsersPage({\n limit: pageSize,\n offset: baOffset,\n })\n baTotal = total\n\n // Enqueue ensure tasks for this page with full-reconcile source\n for (const u of baUsers) {\n this.enqueueEnsure(u, false, 'full-reconcile', reconcileId)\n baIdSet.add(u.id)\n }\n\n baOffset += baUsers.length\n log('reconcile.seed.ba-page', { processed: baOffset, reconcileId, total: baTotal })\n } while (baOffset < baTotal)\n } else {\n // If not pruning, we can process BA users page by page without storing IDs\n let baOffset = 0\n let baTotal = 0\n\n do {\n // TODO: make sure that we dont go past the window through deletes happening\n // (As a user deletes, the total window size becomes smaller)\n const { total, users: baUsers } = await this.listBAUsersPage({\n limit: pageSize,\n offset: baOffset,\n })\n baTotal = total\n\n // Enqueue ensure tasks for this page with full-reconcile source\n for (const u of baUsers) {\n this.enqueueEnsure(u, false, 'full-reconcile', reconcileId)\n }\n\n baOffset += baUsers.length\n log('reconcile.seed.ba-page', { processed: baOffset, reconcileId, total: baTotal })\n } while (baOffset < baTotal)\n }\n\n // Process Payload users page by page for orphan pruning\n if (this.deps.prunePayloadOrphans && baIdSet) {\n let payloadPage = 1\n let hasNextPage = true\n\n while (hasNextPage) {\n const { hasNextPage: nextPage, users: pUsers } = await this.deps.listPayloadUsersPage(\n pageSize,\n payloadPage,\n )\n hasNextPage = nextPage\n\n for (const pu of pUsers) {\n const baId = pu.baUserId?.toString()\n if (baId && !baIdSet.has(baId)) {\n this.enqueueDelete(baId, false, 'full-reconcile', reconcileId)\n }\n }\n\n payloadPage++\n log('reconcile.seed.payload-page', { page: payloadPage - 1, reconcileId })\n }\n }\n }\n\n private async tick() {\n if (this.processing) {\n return\n }\n const now = Date.now()\n const idx = this.q.findIndex((t) => t.nextAt <= now)\n if (idx === -1) {\n return\n }\n const task = this.q[idx]\n this.processing = true\n try {\n await this.runTask(task)\n this.q.splice(idx, 1)\n this.keys.delete(KEY(task))\n this.processed++\n } catch (e: unknown) {\n this.failed++\n this.lastError = e instanceof Error ? e.message : String(e)\n task.attempts += 1\n const delay =\n Math.min(60_000, Math.pow(2, task.attempts) * 1000) + Math.floor(Math.random() * 500)\n task.nextAt = now + delay\n } finally {\n this.processing = false\n }\n }\n\n enqueueDelete(\n baId: string,\n priority = false,\n source: TaskSource = 'user-operation',\n reconcileId?: string,\n ) {\n this.enqueue(\n { attempts: 0, baId, kind: 'delete', nextAt: Date.now(), reconcileId, source },\n priority,\n )\n }\n\n // ——— Public enqueue API ———\n enqueueEnsure(\n user: BAUser,\n priority = false,\n source: TaskSource = 'user-operation',\n reconcileId?: string,\n ) {\n this.enqueue(\n {\n attempts: 0,\n baId: user.id,\n baUser: user,\n kind: 'ensure',\n nextAt: Date.now(),\n reconcileId,\n source,\n },\n priority,\n )\n }\n\n /** Seed tasks by comparing users page by page (Better-Auth → Payload). */\n async seedFullReconcile() {\n const log = this.deps?.log ?? (() => {})\n this.lastSeedAt = new Date().toISOString()\n const reconcileId = `reconcile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`\n\n log('reconcile.seed.start', { reconcileId })\n\n // Clear all previous full-reconcile tasks, but preserve user-operation tasks\n this.clearFullReconcileTasks()\n\n await this.seedFullReconcilePaginated(reconcileId)\n\n log('reconcile.seed.done', this.status())\n }\n\n start({ reconcileEveryMs = 30 * 60_000, tickMs = 1000 } = {}) {\n this.reconcileEveryMs = reconcileEveryMs\n\n if (!this.tickTimer) {\n this.tickTimer = setInterval(() => this.tick(), tickMs)\n // Optional unref for Node.js environments to prevent keeping process alive\n if ('unref' in this.tickTimer && typeof this.tickTimer.unref === 'function') {\n this.tickTimer.unref()\n }\n }\n\n // Schedule the first reconcile\n this.scheduleNextReconcile()\n }\n\n status() {\n const userOpCount = this.q.filter((t) => t.source === 'user-operation').length\n const fullReconcileCount = this.q.filter((t) => t.source === 'full-reconcile').length\n\n return {\n failed: this.failed,\n fullReconcileTasks: fullReconcileCount,\n lastError: this.lastError,\n lastSeedAt: this.lastSeedAt,\n processed: this.processed,\n processing: this.processing,\n queueSize: this.q.length,\n reconciling: this.reconciling,\n sampleKeys: Array.from(this.keys.keys()).slice(0, 50),\n userOperationTasks: userOpCount,\n }\n }\n}\n"],"names":["KEY","t","kind","baId","Queue","deps","failed","keys","Map","lastError","lastSeedAt","processed","processing","q","reconcileEveryMs","reconcileTimeout","reconciling","tickTimer","opts","start","tickMs","runOnBoot","setTimeout","seedFullReconcile","catch","err","log","bumpFront","task","filter","clearFullReconcileTasks","beforeCount","length","fullReconcileCount","source","delete","afterCount","clearedFullReconcile","preservedUserOps","enqueue","priority","k","existing","get","baUser","unshift","push","set","listBAUsersPage","limit","offset","total","internalAdapter","countTotalUsers","users","listUsers","direction","field","runTask","attempts","id","accounts","findAccounts","accountCount","map","a","providerId","syncUserToPayload","deleteUserFromPayload","scheduleNextReconcile","clearTimeout","_error","unref","seedFullReconcilePaginated","reconcileId","pageSize","baIdSet","prunePayloadOrphans","Set","baOffset","baTotal","baUsers","u","enqueueEnsure","add","payloadPage","hasNextPage","nextPage","pUsers","listPayloadUsersPage","pu","baUserId","toString","has","enqueueDelete","page","tick","now","Date","idx","findIndex","nextAt","splice","e","Error","message","String","delay","Math","min","pow","floor","random","user","toISOString","substr","status","setInterval","userOpCount","fullReconcileTasks","queueSize","sampleKeys","Array","from","slice","userOperationTasks"],"mappings":"AA+DA,MAAMA,MAAM,CAACC,IAAY,GAAGA,EAAEC,IAAI,CAAC,CAAC,EAAED,EAAEE,IAAI,EAAE;AAE9C,OAAO,MAAMC;IACHC,KAAgB;IAChBC,SAAS,EAAC;IACVC,OAAO,IAAIC,MAAmB;IAC9BC,YAA2B,KAAI;IAC/BC,aAA4B,KAAI;IAChCC,YAAY,EAAC;IAEbC,aAAa,MAAK;IAClBC,IAAY,EAAE,CAAA;IACdC,mBAAmB,KAAK,OAAO,qBAAqB;KAAtB;IAC9BC,mBAA0C,KAAI;IAC9CC,cAAc,MAAK;IAEnBC,YAAmC,KAAI;IAE/C,YAAYZ,IAAe,EAAEa,OAAoB,CAAC,CAAC,CAAE;QACnD,IAAI,CAACb,IAAI,GAAGA;QAEZ,mDAAmD;QACnD,IAAI,CAACc,KAAK,CAAC;YACTL,kBAAkBI,MAAMJ,oBAAoB,KAAK;YACjDM,QAAQF,MAAME,UAAU;QAC1B;QAEA,kEAAkE;QAClE,IAAIF,MAAMG,aAAa,MAAM;YAC3B,gFAAgF;YAChFC,WAAW;gBACT,IAAI,CAACC,iBAAiB,GAAGC,KAAK,CAC5B,CAACC,MAAQ,IAAI,CAACpB,IAAI,CAACqB,GAAG,IAAI,IAAI,CAACrB,IAAI,CAACqB,GAAG,CAAC,2BAA2BD;YAEvE,GAAG,OAAM,sEAAsE;QACjF;IACF;IAEQE,UAAUC,IAAU,EAAE;QAC5B,IAAI,CAACf,CAAC,GAAG;YAACe;eAAS,IAAI,CAACf,CAAC,CAACgB,MAAM,CAAC,CAAC5B,IAAMA,MAAM2B;SAAM;IACtD;IAEA,mFAAmF,GACnF,AAAQE,0BAA0B;QAChC,MAAMJ,MAAM,IAAI,CAACrB,IAAI,EAAEqB,OAAQ,CAAA,KAAO,CAAA;QACtC,MAAMK,cAAc,IAAI,CAAClB,CAAC,CAACmB,MAAM;QACjC,MAAMC,qBAAqB,IAAI,CAACpB,CAAC,CAACgB,MAAM,CAAC,CAAC5B,IAAMA,EAAEiC,MAAM,KAAK,kBAAkBF,MAAM;QAErF,sDAAsD;QACtD,IAAI,CAACnB,CAAC,GAAG,IAAI,CAACA,CAAC,CAACgB,MAAM,CAAC,CAACD;YACtB,IAAIA,KAAKM,MAAM,KAAK,kBAAkB;gBACpC,IAAI,CAAC3B,IAAI,CAAC4B,MAAM,CAACnC,IAAI4B;gBACrB,OAAO;YACT;YACA,OAAO;QACT;QAEA,MAAMQ,aAAa,IAAI,CAACvB,CAAC,CAACmB,MAAM;QAChCN,IAAI,4BAA4B;YAC9BU;YACAL;YACAM,sBAAsBJ;YACtBK,kBAAkBF;QACpB;IACF;IAEA,oBAAoB;IACZG,QAAQX,IAAU,EAAEY,QAAiB,EAAE;QAC7C,MAAMC,IAAIzC,IAAI4B;QACd,MAAMc,WAAW,IAAI,CAACnC,IAAI,CAACoC,GAAG,CAACF;QAC/B,IAAIC,UAAU;YACZ,IAAId,KAAK1B,IAAI,KAAK,YAAYwC,SAASxC,IAAI,KAAK,YAAY,CAACwC,SAASE,MAAM,IAAIhB,KAAKgB,MAAM,EAAE;gBAC3FF,SAASE,MAAM,GAAGhB,KAAKgB,MAAM;YAC/B;YACA,IAAIJ,UAAU;gBACZ,IAAI,CAACb,SAAS,CAACe;YACjB;YACA;QACF;QACA,IAAIF,UAAU;YACZ,IAAI,CAAC3B,CAAC,CAACgC,OAAO,CAACjB;QACjB,OAAO;YACL,IAAI,CAACf,CAAC,CAACiC,IAAI,CAAClB;QACd;QACA,IAAI,CAACrB,IAAI,CAACwC,GAAG,CAACN,GAAGb;IACnB;IAEA,MAAcoB,gBAAgB,EAAEC,KAAK,EAAEC,MAAM,EAAqC,EAAE;QAClF,8BAA8B;QAC9B,mHAAmH;QACnH,+CAA+C;QAC/C,MAAMC,QAAQ,MAAM,IAAI,CAAC9C,IAAI,CAAC+C,eAAe,CAACC,eAAe;QAC7D,MAAMC,QAAQ,MAAM,IAAI,CAACjD,IAAI,CAAC+C,eAAe,CAACG,SAAS,CAACN,OAAOC,QAAQ;YACrEM,WAAW;YACXC,OAAO;QACT;QACA,OAAO;YAAEN;YAAOG;QAAM;IACxB;IAEA,MAAcI,QAAQzD,CAAO,EAAE;QAC7B,MAAMyB,MAAM,IAAI,CAACrB,IAAI,EAAEqB,OAAQ,CAAA,KAAO,CAAA;QACtC,IAAIzB,EAAEC,IAAI,KAAK,UAAU;YACvBwB,IAAI,gBAAgB;gBAAEiC,UAAU1D,EAAE0D,QAAQ;gBAAExD,MAAMF,EAAEE,IAAI;YAAC;YAEzD,oDAAoD;YACpD,MAAMyC,SAAS3C,EAAE2C,MAAM,IAAI;gBAAEgB,IAAI3D,EAAEE,IAAI;YAAC;YAExC,gDAAgD;YAChD,MAAM0D,WAAW,MAAM,IAAI,CAACxD,IAAI,CAAC+C,eAAe,CAACU,YAAY,CAAC7D,EAAEE,IAAI;YAEpE,sCAAsC;YACtCuB,IAAI,yBAAyB;gBAC3BqC,cAAcF,UAAU7B,UAAU;gBAClC6B,UAAUA,UAAUG,IAAI,CAACC,IAAO,CAAA;wBAAEL,IAAIK,EAAEL,EAAE;wBAAEM,YAAYD,EAAEC,UAAU;oBAAC,CAAA;gBACrE/D,MAAMF,EAAEE,IAAI;YACd;YAEA,qCAAqC;YACrC,MAAM,IAAI,CAACE,IAAI,CAAC8D,iBAAiB,CAACvB,QAAQiB;YAC1C;QACF;QACA,SAAS;QACTnC,IAAI,gBAAgB;YAAEiC,UAAU1D,EAAE0D,QAAQ;YAAExD,MAAMF,EAAEE,IAAI;QAAC;QACzD,MAAM,IAAI,CAACE,IAAI,CAAC+D,qBAAqB,CAACnE,EAAEE,IAAI;IAC9C;IACQkE,wBAAwB;QAC9B,IAAI,IAAI,CAACtD,gBAAgB,EAAE;YACzBuD,aAAa,IAAI,CAACvD,gBAAgB;QACpC;QAEA,IAAI,CAACA,gBAAgB,GAAGO,WAAW;YACjC,IAAI,CAAC,IAAI,CAACN,WAAW,EAAE;gBACrB,IAAI,CAACA,WAAW,GAAG;gBACnB,IAAI;oBACF,MAAM,IAAI,CAACO,iBAAiB;gBAC9B,EAAE,OAAOgD,QAAQ;gBACf,+CAA+C;gBACjD,SAAU;oBACR,IAAI,CAACvD,WAAW,GAAG;oBACnB,uDAAuD;oBACvD,IAAI,CAACqD,qBAAqB;gBAC5B;YACF;QACF,GAAG,IAAI,CAACvD,gBAAgB;QAExB,2EAA2E;QAC3E,IAAI,WAAW,IAAI,CAACC,gBAAgB,IAAI,OAAO,IAAI,CAACA,gBAAgB,CAACyD,KAAK,KAAK,YAAY;YACzF,IAAI,CAACzD,gBAAgB,CAACyD,KAAK;QAC7B;IACF;IAEA,0EAA0E,GAC1E,MAAcC,2BAA2BC,WAAmB,EAAE;QAC5D,MAAMhD,MAAM,IAAI,CAACrB,IAAI,EAAEqB,OAAQ,CAAA,KAAO,CAAA;QACtC,MAAMiD,WAAW;QACjB,IAAIC,UAA8B;QAElC,kEAAkE;QAClE,IAAI,IAAI,CAACvE,IAAI,CAACwE,mBAAmB,EAAE;YACjCD,UAAU,IAAIE;YACd,IAAIC,WAAW;YACf,IAAIC,UAAU;YAEd,GAAG;gBACD,MAAM,EAAE7B,KAAK,EAAEG,OAAO2B,OAAO,EAAE,GAAG,MAAM,IAAI,CAACjC,eAAe,CAAC;oBAC3DC,OAAO0B;oBACPzB,QAAQ6B;gBACV;gBACAC,UAAU7B;gBAEV,gEAAgE;gBAChE,KAAK,MAAM+B,KAAKD,QAAS;oBACvB,IAAI,CAACE,aAAa,CAACD,GAAG,OAAO,kBAAkBR;oBAC/CE,QAAQQ,GAAG,CAACF,EAAEtB,EAAE;gBAClB;gBAEAmB,YAAYE,QAAQjD,MAAM;gBAC1BN,IAAI,0BAA0B;oBAAEf,WAAWoE;oBAAUL;oBAAavB,OAAO6B;gBAAQ;YACnF,QAASD,WAAWC,QAAQ;QAC9B,OAAO;YACL,2EAA2E;YAC3E,IAAID,WAAW;YACf,IAAIC,UAAU;YAEd,GAAG;gBACD,4EAA4E;gBAC5E,6DAA6D;gBAC7D,MAAM,EAAE7B,KAAK,EAAEG,OAAO2B,OAAO,EAAE,GAAG,MAAM,IAAI,CAACjC,eAAe,CAAC;oBAC3DC,OAAO0B;oBACPzB,QAAQ6B;gBACV;gBACAC,UAAU7B;gBAEV,gEAAgE;gBAChE,KAAK,MAAM+B,KAAKD,QAAS;oBACvB,IAAI,CAACE,aAAa,CAACD,GAAG,OAAO,kBAAkBR;gBACjD;gBAEAK,YAAYE,QAAQjD,MAAM;gBAC1BN,IAAI,0BAA0B;oBAAEf,WAAWoE;oBAAUL;oBAAavB,OAAO6B;gBAAQ;YACnF,QAASD,WAAWC,QAAQ;QAC9B;QAEA,wDAAwD;QACxD,IAAI,IAAI,CAAC3E,IAAI,CAACwE,mBAAmB,IAAID,SAAS;YAC5C,IAAIS,cAAc;YAClB,IAAIC,cAAc;YAElB,MAAOA,YAAa;gBAClB,MAAM,EAAEA,aAAaC,QAAQ,EAAEjC,OAAOkC,MAAM,EAAE,GAAG,MAAM,IAAI,CAACnF,IAAI,CAACoF,oBAAoB,CACnFd,UACAU;gBAEFC,cAAcC;gBAEd,KAAK,MAAMG,MAAMF,OAAQ;oBACvB,MAAMrF,OAAOuF,GAAGC,QAAQ,EAAEC;oBAC1B,IAAIzF,QAAQ,CAACyE,QAAQiB,GAAG,CAAC1F,OAAO;wBAC9B,IAAI,CAAC2F,aAAa,CAAC3F,MAAM,OAAO,kBAAkBuE;oBACpD;gBACF;gBAEAW;gBACA3D,IAAI,+BAA+B;oBAAEqE,MAAMV,cAAc;oBAAGX;gBAAY;YAC1E;QACF;IACF;IAEA,MAAcsB,OAAO;QACnB,IAAI,IAAI,CAACpF,UAAU,EAAE;YACnB;QACF;QACA,MAAMqF,MAAMC,KAAKD,GAAG;QACpB,MAAME,MAAM,IAAI,CAACtF,CAAC,CAACuF,SAAS,CAAC,CAACnG,IAAMA,EAAEoG,MAAM,IAAIJ;QAChD,IAAIE,QAAQ,CAAC,GAAG;YACd;QACF;QACA,MAAMvE,OAAO,IAAI,CAACf,CAAC,CAACsF,IAAI;QACxB,IAAI,CAACvF,UAAU,GAAG;QAClB,IAAI;YACF,MAAM,IAAI,CAAC8C,OAAO,CAAC9B;YACnB,IAAI,CAACf,CAAC,CAACyF,MAAM,CAACH,KAAK;YACnB,IAAI,CAAC5F,IAAI,CAAC4B,MAAM,CAACnC,IAAI4B;YACrB,IAAI,CAACjB,SAAS;QAChB,EAAE,OAAO4F,GAAY;YACnB,IAAI,CAACjG,MAAM;YACX,IAAI,CAACG,SAAS,GAAG8F,aAAaC,QAAQD,EAAEE,OAAO,GAAGC,OAAOH;YACzD3E,KAAK+B,QAAQ,IAAI;YACjB,MAAMgD,QACJC,KAAKC,GAAG,CAAC,QAAQD,KAAKE,GAAG,CAAC,GAAGlF,KAAK+B,QAAQ,IAAI,QAAQiD,KAAKG,KAAK,CAACH,KAAKI,MAAM,KAAK;YACnFpF,KAAKyE,MAAM,GAAGJ,MAAMU;QACtB,SAAU;YACR,IAAI,CAAC/F,UAAU,GAAG;QACpB;IACF;IAEAkF,cACE3F,IAAY,EACZqC,WAAW,KAAK,EAChBN,SAAqB,gBAAgB,EACrCwC,WAAoB,EACpB;QACA,IAAI,CAACnC,OAAO,CACV;YAAEoB,UAAU;YAAGxD;YAAMD,MAAM;YAAUmG,QAAQH,KAAKD,GAAG;YAAIvB;YAAaxC;QAAO,GAC7EM;IAEJ;IAEA,6BAA6B;IAC7B2C,cACE8B,IAAY,EACZzE,WAAW,KAAK,EAChBN,SAAqB,gBAAgB,EACrCwC,WAAoB,EACpB;QACA,IAAI,CAACnC,OAAO,CACV;YACEoB,UAAU;YACVxD,MAAM8G,KAAKrD,EAAE;YACbhB,QAAQqE;YACR/G,MAAM;YACNmG,QAAQH,KAAKD,GAAG;YAChBvB;YACAxC;QACF,GACAM;IAEJ;IAEA,wEAAwE,GACxE,MAAMjB,oBAAoB;QACxB,MAAMG,MAAM,IAAI,CAACrB,IAAI,EAAEqB,OAAQ,CAAA,KAAO,CAAA;QACtC,IAAI,CAAChB,UAAU,GAAG,IAAIwF,OAAOgB,WAAW;QACxC,MAAMxC,cAAc,CAAC,UAAU,EAAEwB,KAAKD,GAAG,GAAG,CAAC,EAAEW,KAAKI,MAAM,GAAGpB,QAAQ,CAAC,IAAIuB,MAAM,CAAC,GAAG,IAAI;QAExFzF,IAAI,wBAAwB;YAAEgD;QAAY;QAE1C,6EAA6E;QAC7E,IAAI,CAAC5C,uBAAuB;QAE5B,MAAM,IAAI,CAAC2C,0BAA0B,CAACC;QAEtChD,IAAI,uBAAuB,IAAI,CAAC0F,MAAM;IACxC;IAEAjG,MAAM,EAAEL,mBAAmB,KAAK,MAAM,EAAEM,SAAS,IAAI,EAAE,GAAG,CAAC,CAAC,EAAE;QAC5D,IAAI,CAACN,gBAAgB,GAAGA;QAExB,IAAI,CAAC,IAAI,CAACG,SAAS,EAAE;YACnB,IAAI,CAACA,SAAS,GAAGoG,YAAY,IAAM,IAAI,CAACrB,IAAI,IAAI5E;YAChD,2EAA2E;YAC3E,IAAI,WAAW,IAAI,CAACH,SAAS,IAAI,OAAO,IAAI,CAACA,SAAS,CAACuD,KAAK,KAAK,YAAY;gBAC3E,IAAI,CAACvD,SAAS,CAACuD,KAAK;YACtB;QACF;QAEA,+BAA+B;QAC/B,IAAI,CAACH,qBAAqB;IAC5B;IAEA+C,SAAS;QACP,MAAME,cAAc,IAAI,CAACzG,CAAC,CAACgB,MAAM,CAAC,CAAC5B,IAAMA,EAAEiC,MAAM,KAAK,kBAAkBF,MAAM;QAC9E,MAAMC,qBAAqB,IAAI,CAACpB,CAAC,CAACgB,MAAM,CAAC,CAAC5B,IAAMA,EAAEiC,MAAM,KAAK,kBAAkBF,MAAM;QAErF,OAAO;YACL1B,QAAQ,IAAI,CAACA,MAAM;YACnBiH,oBAAoBtF;YACpBxB,WAAW,IAAI,CAACA,SAAS;YACzBC,YAAY,IAAI,CAACA,UAAU;YAC3BC,WAAW,IAAI,CAACA,SAAS;YACzBC,YAAY,IAAI,CAACA,UAAU;YAC3B4G,WAAW,IAAI,CAAC3G,CAAC,CAACmB,MAAM;YACxBhB,aAAa,IAAI,CAACA,WAAW;YAC7ByG,YAAYC,MAAMC,IAAI,CAAC,IAAI,CAACpH,IAAI,CAACA,IAAI,IAAIqH,KAAK,CAAC,GAAG;YAClDC,oBAAoBP;QACtB;IACF;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../src/better-auth/reconcile-queue.ts"],"sourcesContent":["import type { AuthContext } from 'better-auth'\n\n// src/reconcile-queue.ts\nimport type { BAUser, BetterAuthAccount, BetterAuthUser, PayloadUser } from './sources'\n\nexport interface QueueDeps {\n /** Prefix for Better Auth collections */\n collectionPrefix: string\n\n /** Delete user and associated BA collection entries from Payload */\n deleteUserFromPayload: (baId: string) => Promise<void>\n\n /** Better Auth internal adapter for fetching users and accounts */\n internalAdapter: AuthContext['internalAdapter']\n\n // Paginated loaders (efficient processing)\n listPayloadUsersPage: (\n limit: number,\n page: number,\n ) => Promise<{ hasNextPage: boolean; total: number; users: PayloadUser[] }>\n\n // Logging\n log?: (msg: string, extra?: unknown) => void\n\n /** Map BA user to Payload user data */\n mapUserToPayload: (baUser: BetterAuthUser) => Record<string, unknown>\n\n // Policy\n prunePayloadOrphans?: boolean // default: false\n\n /** Sync user and BA collection entries to Payload */\n syncUserToPayload: (baUser: BAUser, accounts?: BetterAuthAccount[]) => Promise<void>\n}\n\nexport type TaskSource = 'full-reconcile' | 'user-operation'\n\n// Bootstrap options interface\nexport interface InitOptions {\n forceReset?: boolean\n reconcileEveryMs?: number\n runOnBoot?: boolean\n tickMs?: number\n}\n\ntype Task =\n | {\n attempts: number\n baId: string\n baUser?: BAUser\n kind: 'ensure'\n nextAt: number\n reconcileId?: string\n source: TaskSource\n }\n | {\n attempts: number\n baId: string\n kind: 'delete'\n nextAt: number\n reconcileId?: string\n source: TaskSource\n }\n\nconst KEY = (t: Task) => `${t.kind}:${t.baId}`\n\nexport class Queue {\n private deps!: QueueDeps\n private failed = 0\n private keys = new Map<string, Task>()\n private lastError: null | string = null\n private lastSeedAt: null | string = null\n private processed = 0\n\n private processing = false\n private q: Task[] = []\n private reconcileEveryMs = 30 * 60_000 // default 30 minutes\n private reconcileTimeout: NodeJS.Timeout | null = null\n private reconciling = false\n\n private tickTimer: NodeJS.Timeout | null = null\n\n constructor(deps: QueueDeps, opts: InitOptions = {}) {\n this.deps = deps\n\n // Start timers but don't run reconcile immediately\n this.start({\n reconcileEveryMs: opts?.reconcileEveryMs ?? 30 * 60_000,\n tickMs: opts?.tickMs ?? 1000,\n })\n\n // Defer the initial reconcile to avoid circular dependency issues\n if (opts?.runOnBoot ?? true) {\n // Use setTimeout instead of queueMicrotask to give more time for initialization\n setTimeout(() => {\n this.seedFullReconcile().catch(\n (err) => this.deps.log && this.deps.log('[reconcile] seed failed', err),\n )\n }, 2000) // 2 second delay to allow Better Auth and Payload to fully initialize\n }\n }\n\n private bumpFront(task: Task) {\n this.q = [task, ...this.q.filter((t) => t !== task)]\n }\n\n /** Clear all full-reconcile tasks from the queue, preserving user-operation tasks */\n private clearFullReconcileTasks() {\n const log = this.deps?.log ?? (() => {})\n const beforeCount = this.q.length\n const fullReconcileCount = this.q.filter((t) => t.source === 'full-reconcile').length\n\n // Remove full-reconcile tasks from queue and keys map\n this.q = this.q.filter((task) => {\n if (task.source === 'full-reconcile') {\n this.keys.delete(KEY(task))\n return false\n }\n return true\n })\n\n const afterCount = this.q.length\n log('reconcile.clear-previous', {\n afterCount,\n beforeCount,\n clearedFullReconcile: fullReconcileCount,\n preservedUserOps: afterCount,\n })\n }\n\n // ——— Internals ———\n private enqueue(task: Task, priority: boolean) {\n const k = KEY(task)\n const existing = this.keys.get(k)\n if (existing) {\n if (task.kind === 'ensure' && existing.kind === 'ensure' && !existing.baUser && task.baUser) {\n existing.baUser = task.baUser\n }\n if (priority) {\n this.bumpFront(existing)\n }\n return\n }\n if (priority) {\n this.q.unshift(task)\n } else {\n this.q.push(task)\n }\n this.keys.set(k, task)\n }\n\n private async listBAUsersPage({ limit, offset }: { limit: number; offset: number }) {\n // sort by newest (used) first\n // when a delete is happening in the meantime, this will lead to some users not being listed (as the index changes)\n // TODO: fix this by maintaining a delete list.\n const total = await this.deps.internalAdapter.countTotalUsers()\n const users = await this.deps.internalAdapter.listUsers(limit, offset, {\n direction: 'desc',\n field: 'updatedAt',\n })\n return { total, users }\n }\n\n private async runEnsure(baUser: BAUser): Promise<void> {\n const log = this.deps?.log ?? (() => {})\n const baId = baUser.id\n\n // Fetch accounts from Better Auth for this user\n const accounts = await this.deps.internalAdapter.findAccounts(baId)\n\n log('queue.ensure.accounts', {\n accountCount: accounts?.length ?? 0,\n accounts: accounts?.map((a) => ({ id: a.id, providerId: a.providerId })),\n baId,\n })\n\n await this.deps.syncUserToPayload(baUser, accounts as BetterAuthAccount[])\n }\n\n private async runTask(t: Task) {\n const log = this.deps?.log ?? (() => {})\n if (t.kind === 'ensure') {\n log('queue.ensure', { attempts: t.attempts, baId: t.baId })\n const baUser = t.baUser ?? { id: t.baId }\n await this.runEnsure(baUser)\n return\n }\n // delete\n log('queue.delete', { attempts: t.attempts, baId: t.baId })\n await this.deps.deleteUserFromPayload(t.baId)\n }\n\n private scheduleNextReconcile() {\n if (this.reconcileTimeout) {\n clearTimeout(this.reconcileTimeout)\n }\n\n this.reconcileTimeout = setTimeout(async () => {\n if (!this.reconciling) {\n this.reconciling = true\n try {\n await this.seedFullReconcile()\n } catch (_error) {\n // Error is already logged in seedFullReconcile\n } finally {\n this.reconciling = false\n // Schedule the next reconcile after this one completes\n this.scheduleNextReconcile()\n }\n }\n }, this.reconcileEveryMs)\n\n // Optional unref for Node.js environments to prevent keeping process alive\n if ('unref' in this.reconcileTimeout && typeof this.reconcileTimeout.unref === 'function') {\n this.reconcileTimeout.unref()\n }\n }\n /** Paginated approach: process users page by page to reduce memory usage */\n private async seedFullReconcilePaginated(reconcileId: string) {\n const log = this.deps?.log ?? (() => {})\n const pageSize = 500\n let baIdSet: null | Set<string> = null\n\n // If we need to prune orphans, we need to collect all BA user IDs\n if (this.deps.prunePayloadOrphans) {\n baIdSet = new Set<string>()\n let baOffset = 0\n let baTotal = 0\n\n do {\n const { total, users: baUsers } = await this.listBAUsersPage({\n limit: pageSize,\n offset: baOffset,\n })\n baTotal = total\n\n // Enqueue ensure tasks for this page with full-reconcile source\n for (const u of baUsers) {\n this.enqueueEnsure(u, false, 'full-reconcile', reconcileId)\n baIdSet.add(u.id)\n }\n\n baOffset += baUsers.length\n log('reconcile.seed.ba-page', { processed: baOffset, reconcileId, total: baTotal })\n } while (baOffset < baTotal)\n } else {\n // If not pruning, we can process BA users page by page without storing IDs\n let baOffset = 0\n let baTotal = 0\n\n do {\n // TODO: make sure that we dont go past the window through deletes happening\n // (As a user deletes, the total window size becomes smaller)\n const { total, users: baUsers } = await this.listBAUsersPage({\n limit: pageSize,\n offset: baOffset,\n })\n baTotal = total\n\n // Enqueue ensure tasks for this page with full-reconcile source\n for (const u of baUsers) {\n this.enqueueEnsure(u, false, 'full-reconcile', reconcileId)\n }\n\n baOffset += baUsers.length\n log('reconcile.seed.ba-page', { processed: baOffset, reconcileId, total: baTotal })\n } while (baOffset < baTotal)\n }\n\n // Process Payload users page by page for orphan pruning\n if (this.deps.prunePayloadOrphans && baIdSet) {\n let payloadPage = 1\n let hasNextPage = true\n\n while (hasNextPage) {\n const { hasNextPage: nextPage, users: pUsers } = await this.deps.listPayloadUsersPage(\n pageSize,\n payloadPage,\n )\n hasNextPage = nextPage\n\n for (const pu of pUsers) {\n const baId = pu.baUserId?.toString()\n if (baId && !baIdSet.has(baId)) {\n this.enqueueDelete(baId, false, 'full-reconcile', reconcileId)\n }\n }\n\n payloadPage++\n log('reconcile.seed.payload-page', { page: payloadPage - 1, reconcileId })\n }\n }\n }\n\n private async tick() {\n if (this.processing) {\n return\n }\n const now = Date.now()\n const idx = this.q.findIndex((t) => t.nextAt <= now)\n if (idx === -1) {\n return\n }\n const task = this.q[idx]\n this.processing = true\n try {\n await this.runTask(task)\n this.q.splice(idx, 1)\n this.keys.delete(KEY(task))\n this.processed++\n } catch (e: unknown) {\n this.failed++\n this.lastError = e instanceof Error ? e.message : String(e)\n task.attempts += 1\n const delay =\n Math.min(60_000, Math.pow(2, task.attempts) * 1000) + Math.floor(Math.random() * 500)\n task.nextAt = now + delay\n } finally {\n this.processing = false\n }\n }\n\n enqueueDelete(\n baId: string,\n priority = false,\n source: TaskSource = 'user-operation',\n reconcileId?: string,\n ) {\n this.enqueue(\n { attempts: 0, baId, kind: 'delete', nextAt: Date.now(), reconcileId, source },\n priority,\n )\n }\n\n // ——— Public enqueue API ———\n enqueueEnsure(\n user: BAUser,\n priority = false,\n source: TaskSource = 'user-operation',\n reconcileId?: string,\n ) {\n this.enqueue(\n {\n attempts: 0,\n baId: user.id,\n baUser: user,\n kind: 'ensure',\n nextAt: Date.now(),\n reconcileId,\n source,\n },\n priority,\n )\n }\n\n /**\n * Run ensure (sync user to Payload) immediately without waiting for the queue tick.\n * Used from database hooks so that e.g. magic-link redirect sees the user in Payload\n * before the response is sent. The task remains enqueued for idempotent retry.\n */\n async runEnsureNow(user: BAUser): Promise<void> {\n await this.runEnsure(user)\n }\n\n /** Seed tasks by comparing users page by page (Better-Auth → Payload). */\n async seedFullReconcile() {\n const log = this.deps?.log ?? (() => {})\n this.lastSeedAt = new Date().toISOString()\n const reconcileId = `reconcile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`\n\n log('reconcile.seed.start', { reconcileId })\n\n // Clear all previous full-reconcile tasks, but preserve user-operation tasks\n this.clearFullReconcileTasks()\n\n await this.seedFullReconcilePaginated(reconcileId)\n\n log('reconcile.seed.done', this.status())\n }\n\n start({ reconcileEveryMs = 30 * 60_000, tickMs = 1000 } = {}) {\n this.reconcileEveryMs = reconcileEveryMs\n\n if (!this.tickTimer) {\n this.tickTimer = setInterval(() => this.tick(), tickMs)\n // Optional unref for Node.js environments to prevent keeping process alive\n if ('unref' in this.tickTimer && typeof this.tickTimer.unref === 'function') {\n this.tickTimer.unref()\n }\n }\n\n // Schedule the first reconcile\n this.scheduleNextReconcile()\n }\n\n status() {\n const userOpCount = this.q.filter((t) => t.source === 'user-operation').length\n const fullReconcileCount = this.q.filter((t) => t.source === 'full-reconcile').length\n\n return {\n failed: this.failed,\n fullReconcileTasks: fullReconcileCount,\n lastError: this.lastError,\n lastSeedAt: this.lastSeedAt,\n processed: this.processed,\n processing: this.processing,\n queueSize: this.q.length,\n reconciling: this.reconciling,\n sampleKeys: Array.from(this.keys.keys()).slice(0, 50),\n userOperationTasks: userOpCount,\n }\n }\n}\n"],"names":["KEY","t","kind","baId","Queue","deps","failed","keys","Map","lastError","lastSeedAt","processed","processing","q","reconcileEveryMs","reconcileTimeout","reconciling","tickTimer","opts","start","tickMs","runOnBoot","setTimeout","seedFullReconcile","catch","err","log","bumpFront","task","filter","clearFullReconcileTasks","beforeCount","length","fullReconcileCount","source","delete","afterCount","clearedFullReconcile","preservedUserOps","enqueue","priority","k","existing","get","baUser","unshift","push","set","listBAUsersPage","limit","offset","total","internalAdapter","countTotalUsers","users","listUsers","direction","field","runEnsure","id","accounts","findAccounts","accountCount","map","a","providerId","syncUserToPayload","runTask","attempts","deleteUserFromPayload","scheduleNextReconcile","clearTimeout","_error","unref","seedFullReconcilePaginated","reconcileId","pageSize","baIdSet","prunePayloadOrphans","Set","baOffset","baTotal","baUsers","u","enqueueEnsure","add","payloadPage","hasNextPage","nextPage","pUsers","listPayloadUsersPage","pu","baUserId","toString","has","enqueueDelete","page","tick","now","Date","idx","findIndex","nextAt","splice","e","Error","message","String","delay","Math","min","pow","floor","random","user","runEnsureNow","toISOString","substr","status","setInterval","userOpCount","fullReconcileTasks","queueSize","sampleKeys","Array","from","slice","userOperationTasks"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { EmailPasswordFormClient } from './EmailPasswordFormClient';
|
|
|
3
3
|
export async function fetchAuthMethods({ additionalHeaders, betterAuthBaseUrl, debug = false }) {
|
|
4
4
|
const headers = new Headers(additionalHeaders);
|
|
5
5
|
headers.append('Content-Type', 'application/json');
|
|
6
|
-
const url = `${betterAuthBaseUrl}/api/auth/
|
|
6
|
+
const url = `${betterAuthBaseUrl}/api/auth/methods`;
|
|
7
7
|
if (debug) {
|
|
8
8
|
console.log('[payload-better-auth] fetchAuthMethods: Attempting to fetch auth methods');
|
|
9
9
|
console.log('[payload-better-auth] fetchAuthMethods: - URL:', url);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/BetterAuthLoginServer.tsx"],"sourcesContent":["import type { BetterAuthClientOptions } from 'better-auth'\nimport type React from 'react'\n\nimport type { AuthMethod } from '../better-auth/helpers'\n\nimport { EmailPasswordFormClient } from './EmailPasswordFormClient'\n\nexport type AuthClientOptions = { baseURL: string } & Omit<BetterAuthClientOptions, 'baseURL'>\n\nexport async function fetchAuthMethods({\n additionalHeaders,\n betterAuthBaseUrl,\n debug = false,\n}: {\n additionalHeaders?: HeadersInit\n betterAuthBaseUrl: string\n debug?: boolean\n}): Promise<{ data: AuthMethod[]; error: null } | { data: null; error: Error }> {\n const headers = new Headers(additionalHeaders)\n headers.append('Content-Type', 'application/json')\n const url = `${betterAuthBaseUrl}/api/auth/
|
|
1
|
+
{"version":3,"sources":["../../src/components/BetterAuthLoginServer.tsx"],"sourcesContent":["import type { BetterAuthClientOptions } from 'better-auth'\nimport type React from 'react'\n\nimport type { AuthMethod } from '../better-auth/helpers'\n\nimport { EmailPasswordFormClient } from './EmailPasswordFormClient'\n\nexport type AuthClientOptions = { baseURL: string } & Omit<BetterAuthClientOptions, 'baseURL'>\n\nexport async function fetchAuthMethods({\n additionalHeaders,\n betterAuthBaseUrl,\n debug = false,\n}: {\n additionalHeaders?: HeadersInit\n betterAuthBaseUrl: string\n debug?: boolean\n}): Promise<{ data: AuthMethod[]; error: null } | { data: null; error: Error }> {\n const headers = new Headers(additionalHeaders)\n headers.append('Content-Type', 'application/json')\n const url = `${betterAuthBaseUrl}/api/auth/methods`\n\n if (debug) {\n console.log('[payload-better-auth] fetchAuthMethods: Attempting to fetch auth methods')\n console.log('[payload-better-auth] fetchAuthMethods: - URL:', url)\n console.log('[payload-better-auth] fetchAuthMethods: - betterAuthBaseUrl:', betterAuthBaseUrl)\n }\n\n try {\n const response = await fetch(url, {\n headers,\n method: 'GET',\n })\n\n if (debug) {\n console.log('[payload-better-auth] fetchAuthMethods: Response received')\n console.log('[payload-better-auth] fetchAuthMethods: - status:', response.status)\n console.log('[payload-better-auth] fetchAuthMethods: - statusText:', response.statusText)\n }\n\n if (!response.ok) {\n throw new Error(`Failed to fetch auth methods: ${response.status} ${response.statusText}`)\n }\n\n const data = await response.json()\n\n if (debug) {\n console.log('[payload-better-auth] fetchAuthMethods: Successfully fetched auth methods')\n console.log('[payload-better-auth] fetchAuthMethods: - methods count:', data?.length ?? 0)\n }\n\n return { data, error: null } as { data: AuthMethod[]; error: null }\n } catch (error) {\n console.error('Error fetching auth methods:', error)\n return { data: null, error: error as Error }\n }\n}\n\nexport type BetterAuthLoginServerProps = {\n /**\n * Enable debug logging for troubleshooting connection issues.\n */\n debug?: boolean\n /**\n * Auth client options for client-side requests (uses external/public URL).\n */\n externalAuthClientOptions: AuthClientOptions\n /**\n * Auth client options for server-side requests (uses internal URL).\n */\n internalAuthClientOptions: AuthClientOptions\n}\n\nexport async function BetterAuthLoginServer({\n debug = false,\n externalAuthClientOptions,\n internalAuthClientOptions,\n}: BetterAuthLoginServerProps) {\n const authMethods = await fetchAuthMethods({\n additionalHeaders: internalAuthClientOptions.fetchOptions?.headers,\n betterAuthBaseUrl: internalAuthClientOptions.baseURL,\n debug,\n })\n\n return (\n <div\n style={{\n alignItems: 'center',\n display: 'flex',\n justifyContent: 'center',\n }}\n >\n <div\n style={{\n background: 'white',\n borderRadius: '8px',\n boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',\n maxWidth: '400px',\n padding: '2rem',\n width: '100%',\n }}\n >\n <h2\n style={{\n color: '#333',\n fontSize: '1.5rem',\n fontWeight: '600',\n marginBottom: '2rem',\n textAlign: 'center',\n }}\n >\n Sign In to Admin\n </h2>\n\n {authMethods.data?.some(\n (m) => m.method === 'emailAndPassword' || m.method === 'magicLink',\n ) && (\n <EmailPasswordFormClient\n authClientOptions={externalAuthClientOptions}\n authMethods={authMethods.data}\n />\n )}\n {authMethods.data?.length === 0 && (\n <div\n style={{\n color: '#666',\n padding: '2rem',\n textAlign: 'center',\n }}\n >\n <p>No authentication methods are currently available.</p>\n <p style={{ fontSize: '0.875rem', marginTop: '1rem' }}>\n Please contact your administrator.\n </p>\n </div>\n )}\n {authMethods.error && (\n <div\n style={{\n color: '#666',\n padding: '2rem',\n textAlign: 'center',\n }}\n >\n <p>Couldn't fetch authentication methods from better-auth</p>\n <p style={{ fontSize: '0.875rem', marginTop: '1rem' }}>\n Please contact your administrator.\n </p>\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"names":["EmailPasswordFormClient","fetchAuthMethods","additionalHeaders","betterAuthBaseUrl","debug","headers","Headers","append","url","console","log","response","fetch","method","status","statusText","ok","Error","data","json","length","error","BetterAuthLoginServer","externalAuthClientOptions","internalAuthClientOptions","authMethods","fetchOptions","baseURL","div","style","alignItems","display","justifyContent","background","borderRadius","boxShadow","maxWidth","padding","width","h2","color","fontSize","fontWeight","marginBottom","textAlign","some","m","authClientOptions","p","marginTop"],"mappings":";AAKA,SAASA,uBAAuB,QAAQ,4BAA2B;AAInE,OAAO,eAAeC,iBAAiB,EACrCC,iBAAiB,EACjBC,iBAAiB,EACjBC,QAAQ,KAAK,EAKd;IACC,MAAMC,UAAU,IAAIC,QAAQJ;IAC5BG,QAAQE,MAAM,CAAC,gBAAgB;IAC/B,MAAMC,MAAM,GAAGL,kBAAkB,iBAAiB,CAAC;IAEnD,IAAIC,OAAO;QACTK,QAAQC,GAAG,CAAC;QACZD,QAAQC,GAAG,CAAC,oDAAoDF;QAChEC,QAAQC,GAAG,CAAC,kEAAkEP;IAChF;IAEA,IAAI;QACF,MAAMQ,WAAW,MAAMC,MAAMJ,KAAK;YAChCH;YACAQ,QAAQ;QACV;QAEA,IAAIT,OAAO;YACTK,QAAQC,GAAG,CAAC;YACZD,QAAQC,GAAG,CAAC,uDAAuDC,SAASG,MAAM;YAClFL,QAAQC,GAAG,CAAC,2DAA2DC,SAASI,UAAU;QAC5F;QAEA,IAAI,CAACJ,SAASK,EAAE,EAAE;YAChB,MAAM,IAAIC,MAAM,CAAC,8BAA8B,EAAEN,SAASG,MAAM,CAAC,CAAC,EAAEH,SAASI,UAAU,EAAE;QAC3F;QAEA,MAAMG,OAAO,MAAMP,SAASQ,IAAI;QAEhC,IAAIf,OAAO;YACTK,QAAQC,GAAG,CAAC;YACZD,QAAQC,GAAG,CAAC,8DAA8DQ,MAAME,UAAU;QAC5F;QAEA,OAAO;YAAEF;YAAMG,OAAO;QAAK;IAC7B,EAAE,OAAOA,OAAO;QACdZ,QAAQY,KAAK,CAAC,gCAAgCA;QAC9C,OAAO;YAAEH,MAAM;YAAMG,OAAOA;QAAe;IAC7C;AACF;AAiBA,OAAO,eAAeC,sBAAsB,EAC1ClB,QAAQ,KAAK,EACbmB,yBAAyB,EACzBC,yBAAyB,EACE;IAC3B,MAAMC,cAAc,MAAMxB,iBAAiB;QACzCC,mBAAmBsB,0BAA0BE,YAAY,EAAErB;QAC3DF,mBAAmBqB,0BAA0BG,OAAO;QACpDvB;IACF;IAEA,qBACE,KAACwB;QACCC,OAAO;YACLC,YAAY;YACZC,SAAS;YACTC,gBAAgB;QAClB;kBAEA,cAAA,MAACJ;YACCC,OAAO;gBACLI,YAAY;gBACZC,cAAc;gBACdC,WAAW;gBACXC,UAAU;gBACVC,SAAS;gBACTC,OAAO;YACT;;8BAEA,KAACC;oBACCV,OAAO;wBACLW,OAAO;wBACPC,UAAU;wBACVC,YAAY;wBACZC,cAAc;wBACdC,WAAW;oBACb;8BACD;;gBAIAnB,YAAYP,IAAI,EAAE2B,KACjB,CAACC,IAAMA,EAAEjC,MAAM,KAAK,sBAAsBiC,EAAEjC,MAAM,KAAK,8BAEvD,KAACb;oBACC+C,mBAAmBxB;oBACnBE,aAAaA,YAAYP,IAAI;;gBAGhCO,YAAYP,IAAI,EAAEE,WAAW,mBAC5B,MAACQ;oBACCC,OAAO;wBACLW,OAAO;wBACPH,SAAS;wBACTO,WAAW;oBACb;;sCAEA,KAACI;sCAAE;;sCACH,KAACA;4BAAEnB,OAAO;gCAAEY,UAAU;gCAAYQ,WAAW;4BAAO;sCAAG;;;;gBAK1DxB,YAAYJ,KAAK,kBAChB,MAACO;oBACCC,OAAO;wBACLW,OAAO;wBACPH,SAAS;wBACTO,WAAW;oBACb;;sCAEA,KAACI;sCAAE;;sCACH,KAACA;4BAAEnB,OAAO;gCAAEY,UAAU;gCAAYQ,WAAW;4BAAO;sCAAG;;;;;;;AAQnE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-better-auth",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "A Payload CMS plugin that integrates Better Auth for seamless user authentication and management",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,15 +48,17 @@
|
|
|
48
48
|
"_build": "npm run copyfiles && npm run build:types && npm run build:swc",
|
|
49
49
|
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
50
50
|
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
51
|
-
"dev": "concurrently -k -n \"WEB,MAIL\" -c \"auto\" \"npm run dev:original\" \"npm run maildev\"",
|
|
51
|
+
"dev": "concurrently -k -n \"WEB,MAIL,AUTH\" -c \"auto\" \"npm run dev:original\" \"npm run maildev\" \"npm run dev:auth\"",
|
|
52
|
+
"dev:auth": "dotenv -e ./dev/.env.development -- node --import tsx dev/auth/server.ts",
|
|
53
|
+
"dev:auth:test": "dotenv -e ./dev/.env.test -- node --import tsx dev/auth/server.ts",
|
|
52
54
|
"dev:redis": "npm run docker:redis && concurrently -k -n \"WEB,MAIL\" -c \"auto\" \"npm run dev:redis:original\" \"npm run maildev\"",
|
|
53
55
|
"dev:redis:original": "cross-env USE_REDIS=true REDIS_URL=redis://localhost:6379 npm run dev:original",
|
|
54
56
|
"docker:redis": "docker compose up -d redis",
|
|
55
57
|
"docker:redis:stop": "docker compose down",
|
|
56
58
|
"dev:build": "nx run payload-better-auth:_dev:build",
|
|
57
59
|
"_dev:build": "dotenv -e ./dev/.env.test -- next build dev",
|
|
58
|
-
"dev:original": "dotenv -e ./dev/.env.development -- next dev dev --turbo",
|
|
59
|
-
"dev:start": "dotenv -e ./dev/.env.test -- next start dev",
|
|
60
|
+
"dev:original": "dotenv -e ./dev/.env.development -- sh -c 'next dev dev --turbo -p \"$PAYLOAD_PORT\"'",
|
|
61
|
+
"dev:start": "dotenv -e ./dev/.env.test -- sh -c 'next start dev -p \"$PAYLOAD_PORT\"'",
|
|
60
62
|
"dev:generate-importmap": "npm run dev:payload generate:importmap",
|
|
61
63
|
"dev:generate-types": "npm run dev:payload generate:types",
|
|
62
64
|
"dev:payload": "dotenv -e ./dev/.env.development -- cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
@@ -82,14 +84,14 @@
|
|
|
82
84
|
"test": "nx run payload-better-auth:_test",
|
|
83
85
|
"_test": "npm run test:int && npm run test:e2e",
|
|
84
86
|
"test:e2e": "nx run payload-better-auth:_test:e2e",
|
|
85
|
-
"_test:e2e": "npm run dev:build && dotenv -e ./dev/.env.test -- playwright test",
|
|
87
|
+
"_test:e2e": "npm run build && npm run dev:build && dotenv -e ./dev/.env.test -- playwright test",
|
|
86
88
|
"test:int": "nx run payload-better-auth:_test:int",
|
|
87
89
|
"_test:int": "npm run test:setup && dotenv -e ./dev/.env.test -- vitest run",
|
|
88
90
|
"test:setup": "npm run reset:test",
|
|
89
91
|
"typecheck": "nx run payload-better-auth:_typecheck",
|
|
90
92
|
"_typecheck": "tsc --noEmit -p ./dev/tsconfig.json",
|
|
91
93
|
"semantic-release": "semantic-release",
|
|
92
|
-
"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
|
|
94
|
+
"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:$PAYLOAD_PORT/admin >/dev/null; do sleep 0.25; done; npm run test:int'\""
|
|
93
95
|
},
|
|
94
96
|
"devDependencies": {
|
|
95
97
|
"@better-auth/api-key": "^1.5.5",
|
|
@@ -97,6 +99,7 @@
|
|
|
97
99
|
"@commitlint/cli": "^20.4.4",
|
|
98
100
|
"@commitlint/config-conventional": "^20.4.4",
|
|
99
101
|
"@eslint/eslintrc": "^3.3.5",
|
|
102
|
+
"@hono/node-server": "^1.19.11",
|
|
100
103
|
"@payloadcms/db-mongodb": "^3.79.0",
|
|
101
104
|
"@payloadcms/db-postgres": "^3.79.0",
|
|
102
105
|
"@payloadcms/db-sqlite": "^3.79.0",
|
|
@@ -127,6 +130,7 @@
|
|
|
127
130
|
"eslint-config-next": "16.1.6",
|
|
128
131
|
"execa": "^9.6.1",
|
|
129
132
|
"graphql": "^16.13.1",
|
|
133
|
+
"hono": "^4.12.8",
|
|
130
134
|
"husky": "^9.1.7",
|
|
131
135
|
"ioredis": "^5.10.0",
|
|
132
136
|
"maildev": "^2.2.1",
|
|
@@ -144,6 +148,7 @@
|
|
|
144
148
|
"semantic-release": "^25.0.3",
|
|
145
149
|
"sharp": "0.34.5",
|
|
146
150
|
"sort-package-json": "^3.6.1",
|
|
151
|
+
"tsx": "^4.21.0",
|
|
147
152
|
"typescript": "5.9.3",
|
|
148
153
|
"vite-tsconfig-paths": "^6.1.1",
|
|
149
154
|
"vitest": "^4.1.0"
|
|
@@ -108,9 +108,9 @@ function createQueueBasedHooks(queue: Queue) {
|
|
|
108
108
|
return {
|
|
109
109
|
user: {
|
|
110
110
|
create: {
|
|
111
|
-
after: (user: BAUser): Promise<void> => {
|
|
111
|
+
after: async (user: BAUser): Promise<void> => {
|
|
112
112
|
queue.enqueueEnsure(user, true, 'user-operation')
|
|
113
|
-
|
|
113
|
+
await queue.runEnsureNow(user)
|
|
114
114
|
},
|
|
115
115
|
},
|
|
116
116
|
delete: {
|
|
@@ -120,9 +120,9 @@ function createQueueBasedHooks(queue: Queue) {
|
|
|
120
120
|
},
|
|
121
121
|
},
|
|
122
122
|
update: {
|
|
123
|
-
after: (user: BAUser): Promise<void> => {
|
|
123
|
+
after: async (user: BAUser): Promise<void> => {
|
|
124
124
|
queue.enqueueEnsure(user, true, 'user-operation')
|
|
125
|
-
|
|
125
|
+
await queue.runEnsureNow(user)
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
128
|
},
|
|
@@ -165,7 +165,7 @@ export const payloadBetterAuthPlugin = <
|
|
|
165
165
|
endpoints: {
|
|
166
166
|
// convenience for tests/admin tools (optional)
|
|
167
167
|
authMethods: createAuthEndpoint(
|
|
168
|
-
'/
|
|
168
|
+
'/methods',
|
|
169
169
|
{ method: 'GET' },
|
|
170
170
|
async ({
|
|
171
171
|
context,
|
|
@@ -160,32 +160,35 @@ export class Queue {
|
|
|
160
160
|
return { total, users }
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
private async
|
|
163
|
+
private async runEnsure(baUser: BAUser): Promise<void> {
|
|
164
164
|
const log = this.deps?.log ?? (() => {})
|
|
165
|
-
|
|
166
|
-
log('queue.ensure', { attempts: t.attempts, baId: t.baId })
|
|
165
|
+
const baId = baUser.id
|
|
167
166
|
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
// Fetch accounts from Better Auth for this user
|
|
168
|
+
const accounts = await this.deps.internalAdapter.findAccounts(baId)
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
log('queue.ensure.accounts', {
|
|
171
|
+
accountCount: accounts?.length ?? 0,
|
|
172
|
+
accounts: accounts?.map((a) => ({ id: a.id, providerId: a.providerId })),
|
|
173
|
+
baId,
|
|
174
|
+
})
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
accountCount: accounts?.length ?? 0,
|
|
177
|
-
accounts: accounts?.map((a) => ({ id: a.id, providerId: a.providerId })),
|
|
178
|
-
baId: t.baId,
|
|
179
|
-
})
|
|
176
|
+
await this.deps.syncUserToPayload(baUser, accounts as BetterAuthAccount[])
|
|
177
|
+
}
|
|
180
178
|
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
private async runTask(t: Task) {
|
|
180
|
+
const log = this.deps?.log ?? (() => {})
|
|
181
|
+
if (t.kind === 'ensure') {
|
|
182
|
+
log('queue.ensure', { attempts: t.attempts, baId: t.baId })
|
|
183
|
+
const baUser = t.baUser ?? { id: t.baId }
|
|
184
|
+
await this.runEnsure(baUser)
|
|
183
185
|
return
|
|
184
186
|
}
|
|
185
187
|
// delete
|
|
186
188
|
log('queue.delete', { attempts: t.attempts, baId: t.baId })
|
|
187
189
|
await this.deps.deleteUserFromPayload(t.baId)
|
|
188
190
|
}
|
|
191
|
+
|
|
189
192
|
private scheduleNextReconcile() {
|
|
190
193
|
if (this.reconcileTimeout) {
|
|
191
194
|
clearTimeout(this.reconcileTimeout)
|
|
@@ -211,7 +214,6 @@ export class Queue {
|
|
|
211
214
|
this.reconcileTimeout.unref()
|
|
212
215
|
}
|
|
213
216
|
}
|
|
214
|
-
|
|
215
217
|
/** Paginated approach: process users page by page to reduce memory usage */
|
|
216
218
|
private async seedFullReconcilePaginated(reconcileId: string) {
|
|
217
219
|
const log = this.deps?.log ?? (() => {})
|
|
@@ -350,6 +352,15 @@ export class Queue {
|
|
|
350
352
|
)
|
|
351
353
|
}
|
|
352
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Run ensure (sync user to Payload) immediately without waiting for the queue tick.
|
|
357
|
+
* Used from database hooks so that e.g. magic-link redirect sees the user in Payload
|
|
358
|
+
* before the response is sent. The task remains enqueued for idempotent retry.
|
|
359
|
+
*/
|
|
360
|
+
async runEnsureNow(user: BAUser): Promise<void> {
|
|
361
|
+
await this.runEnsure(user)
|
|
362
|
+
}
|
|
363
|
+
|
|
353
364
|
/** Seed tasks by comparing users page by page (Better-Auth → Payload). */
|
|
354
365
|
async seedFullReconcile() {
|
|
355
366
|
const log = this.deps?.log ?? (() => {})
|
|
@@ -18,7 +18,7 @@ export async function fetchAuthMethods({
|
|
|
18
18
|
}): Promise<{ data: AuthMethod[]; error: null } | { data: null; error: Error }> {
|
|
19
19
|
const headers = new Headers(additionalHeaders)
|
|
20
20
|
headers.append('Content-Type', 'application/json')
|
|
21
|
-
const url = `${betterAuthBaseUrl}/api/auth/
|
|
21
|
+
const url = `${betterAuthBaseUrl}/api/auth/methods`
|
|
22
22
|
|
|
23
23
|
if (debug) {
|
|
24
24
|
console.log('[payload-better-auth] fetchAuthMethods: Attempting to fetch auth methods')
|