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 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
- return Promise.resolve();
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
- return Promise.resolve();
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('/auth/methods', {
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":"AAAA,wCAAwC;AAIxC,SAASA,QAAQ,EAAEC,kBAAkB,EAAEC,oBAAoB,QAAS,kBAAiB;AAMrF,SAASC,wBAAwB,QAAQ,+BAA8B;AACvE,SAASC,uBAAuB,EAAEC,gBAAgB,QAAQ,kBAAiB;AAC3E,SAA2BC,KAAK,QAAQ,oBAAmB;AAC3D,SAGEC,2BAA2B,EAC3BC,0BAA0B,EAC1BC,uBAAuB,QAClB,YAAW;AAIlB,MAAMC,aAAa,CAACC,KAAaC;IAC/BC,QAAQC,GAAG,CAAC,CAAC,YAAY,EAAEH,KAAK,EAAEC,QAAQG,KAAKC,SAAS,CAACJ,OAAO,MAAM,KAAK;AAC7E;AA6EA;;;CAGC,GACD,SAASK,sBAAsBC,KAAY;IACzC,OAAO;QACLC,MAAM;YACJC,QAAQ;gBACNC,OAAO,OAAOF;oBACZD,MAAMI,aAAa,CAACH,MAAM,MAAM;oBAChC,MAAMD,MAAMK,YAAY,CAACJ;gBAC3B;YACF;YACAK,QAAQ;gBACNH,OAAO,CAACF;oBACND,MAAMO,aAAa,CAACN,KAAKO,EAAE,EAAE,MAAM;oBACnC,OAAOC,QAAQC,OAAO;gBACxB;YACF;YACAC,QAAQ;gBACNR,OAAO,OAAOF;oBACZD,MAAMI,aAAa,CAACH,MAAM,MAAM;oBAChC,MAAMD,MAAMK,YAAY,CAACJ;gBAC3B;YACF;QACF;IACF;AACF;AAEA,OAAO,MAAMW,0BAA0B,CAIrCC;IAEA,MAAM,EACJC,mBAAmB,eAAe,EAClCC,QAAQ,EACRC,gBAAgB,EAChBC,OAAO,EACPC,YAAY,OAA0B,EACvC,GAAGL;IAEJ,mCAAmC;IACnC,MAAMM,oBAAoB,GAAGL,iBAAiB,eAAe,CAAC;IAC9D,MAAMM,gBAAgB,GAAGN,iBAAiB,WAAW,CAAC;IAEtD,6BAA6B;IAC7B,MAAMO,SAASpC,yBAAyB;QACtCqC,SAAST,KAAKU,aAAa,IAAI;QAC/BC,QAAQ;QACRP;IACF;IAEA,iFAAiF;IACjF,MAAMQ,WAAWZ,KAAKU,aAAa,GAAG/B,aAAakC;IAEnD,iCAAiC;IACjC,IAAIC,yBAA8C;IAElD,OAAO;QACLnB,IAAI;QACJoB,WAAW;YACT,+CAA+C;YAC/CC,aAAa9C,mBACX,YACA;gBAAE+C,QAAQ;YAAM,GAChB,OAAO,EACLC,OAAO,EACPC,IAAI,EACL;gBACC,MAAMH,cAA4B,EAAE;gBACpC,6FAA6F;gBAC7F,IAAIE,QAAQE,OAAO,CAACC,gBAAgB,EAAEZ,SAAS;oBAC7CO,YAAYM,IAAI,CAAC;wBACfL,QAAQ;wBACRG,SAAS;4BACPG,mBAAmBL,QAAQE,OAAO,CAACC,gBAAgB,CAACE,iBAAiB,IAAI;wBAC3E;oBACF;gBACF;gBACA,IAAIL,QAAQE,OAAO,CAACI,OAAO,EAAEC,KAAK,CAACC,IAAMA,EAAE/B,EAAE,KAAK,eAAe;oBAC/DqB,YAAYM,IAAI,CAAC;wBAAEL,QAAQ;oBAAY;gBACzC;gBAEA,OAAO,MAAME,KAAKH;YACpB;YAEFW,WAAWzD,mBACT,qBACA;gBAAE+C,QAAQ;YAAO,GACjB,OAAO,EACLC,OAAO,EACPC,IAAI,EACJS,OAAO,EACR;gBACC,IAAI5B,KAAK6B,KAAK,IAAID,SAASE,QAAQC,IAAI,yBAAyB/B,KAAK6B,KAAK,EAAE;oBAC1E,MAAM,IAAI5D,SAAS,gBAAgB;wBAAE+D,SAAS;oBAAgB;gBAChE;gBACA,MAAMC,OAAQ,MAAML,SAAST,OAAOe,MAAM,IAAO,CAAA,CAAC,CAAA;gBAClD,MAAMC,OAAOF,MAAME;gBACnB,IAAI,CAACA,MAAM;oBACT,MAAM,IAAIlE,SAAS,eAAe;wBAAE+D,SAAS;oBAAe;gBAC9D;;gBACEd,QAAqCkB,iBAAiB,CAACjD,KAAK,CAACO,aAAa,CAC1EyC,MACA,MACA;gBAEF,OAAOhB,KAAK;oBAAEkB,IAAI;gBAAK;YACzB;YAEFC,WAAWpE,mBACT,qBACA;gBAAE+C,QAAQ;YAAO,GACjB,OAAO,EACLC,OAAO,EACPC,IAAI,EACJS,OAAO,EACR;gBACC,IAAI5B,KAAK6B,KAAK,IAAID,SAASE,QAAQC,IAAI,yBAAyB/B,KAAK6B,KAAK,EAAE;oBAC1E,MAAM,IAAI5D,SAAS,gBAAgB;wBAAE+D,SAAS;oBAAgB;gBAChE;gBACA,MAAMC,OAAQ,MAAML,SAAST,OAAOe,MAAM,IAAO,CAAA,CAAC,CAAA;gBAClD,MAAM9C,OAAO6C,MAAM7C;gBACnB,IAAI,CAACA,MAAMO,IAAI;oBACb,MAAM,IAAI1B,SAAS,eAAe;wBAAE+D,SAAS;oBAAe;gBAC9D;;gBACEd,QAAqCkB,iBAAiB,CAACjD,KAAK,CAACI,aAAa,CAC1EH,MACA,MACA;gBAEF,OAAO+B,KAAK;oBAAEkB,IAAI;gBAAK;YACzB;YAEFE,KAAKrE,mBACH,kBACA;gBAAE+C,QAAQ;YAAO,GACjB,OAAO,EACLC,OAAO,EACPC,IAAI,EACJS,OAAO,EACR;gBACC,IAAI5B,KAAK6B,KAAK,IAAID,SAASE,QAAQC,IAAI,yBAAyB/B,KAAK6B,KAAK,EAAE;oBAC1E,MAAM,IAAI5D,SAAS,gBAAgB;wBAAE+D,SAAS;oBAAgB;gBAChE;gBACA,MAAM,AAACd,QAAqCkB,iBAAiB,CAACjD,KAAK,CAACqD,iBAAiB;gBACrF,OAAOrB,KAAK;oBAAEkB,IAAI;gBAAK;YACzB;YAEFI,QAAQvE,mBACN,qBACA;gBAAE+C,QAAQ;YAAM,GAChB,OAAO,EACLC,OAAO,EACPC,IAAI,EACJS,OAAO,EACR;gBACC,IAAI5B,KAAK6B,KAAK,IAAID,SAASE,QAAQC,IAAI,yBAAyB/B,KAAK6B,KAAK,EAAE;oBAC1E,OAAOjC,QAAQ8C,MAAM,CACnB,IAAIzE,SAAS,gBAAgB;wBAAE+D,SAAS;oBAAgB;gBAE5D;gBACA,OAAOb,KAAK,AAACD,QAAqCkB,iBAAiB,CAACjD,KAAK,CAACsD,MAAM;YAClF;YAEF,gEAAgE;YAChE,8BAA8B;YAC9BE,QAAQzE,mBACN,WACA;gBAAE+C,QAAQ;YAAM,GAChB,OAAO,EACLC,OAAO,EACPC,IAAI,EACL;gBACC,MAAMH,cAAwB,EAAE;gBAChC,IAAIE,QAAQE,OAAO,CAACC,gBAAgB,EAAEZ,SAAS;oBAC7CO,YAAYM,IAAI,CAAC;gBACnB;gBACA,IAAIJ,QAAQE,OAAO,CAACI,OAAO,EAAEC,KAAK,CAACC,IAAMA,EAAE/B,EAAE,KAAK,eAAe;oBAC/DqB,YAAYM,IAAI,CAAC;gBACnB;gBAEA,OAAOH,KAAK;oBACVH;oBACA4B,aAAa;oBACbC,UAAU;oBACVC,WAAW,IAAIC,OAAOC,WAAW;gBACnC;YACF;QAEJ;QACAC,OAAO;YACLC,QAAQ;gBACN;oBACEC,SAAShF,qBAAqB,OAAOiF;wBACnC,MAAMC,SAASD,IAAIE,SAAS,CAAC;wBAC7B,OAAO1D,QAAQC,OAAO,CAAC;4BACrBqB,SAAS;gCAAE,GAAGkC,GAAG;gCAAEnB,MAAM;oCAAE,GAAGmB,IAAInB,IAAI;oCAAEoB,QAAQA,UAAUxC;gCAAU;4BAAE;wBACxE;oBACF;oBACA0C,SAAS,CAACrC;wBACR,OAAOA,QAAQsC,IAAI,KAAK;oBAC1B;gBACF;aACD;QACH;QACA,MAAMC,MAAK,EAAEC,eAAe,EAAEtC,OAAO,EAAE;YACrC,sCAAsC;YACtCZ,OAAOmD,MAAM,CAAC;YAEd,gEAAgE;YAChE,4EAA4E;YAC5E,MAAMC,eAAexC,QAAQyC,QAAQ,EAAED,gBAAgB;YACvD,MAAME,mBAAmB1C,QAAQyC,QAAQ,EAAEE,SAASC,eAAeC;YACnE,wCAAwC;YACxC,gDAAgD;YAChD,8BAA8B;YAC9B,oCAAoC;YACpC,MAAMC,aAAa,OAAO9C,QAAQ+C,OAAO,KAAK,WAAW/C,QAAQ+C,OAAO,GAAGtD;YAC3E,MAAMuD,UAAUF,YAAYG,WAAW,eAAe;YACtD,MAAMC,mBACJlD,QAAQyC,QAAQ,EAAES,oBAAqBC,CAAAA,QAAQC,GAAG,CAACC,QAAQ,KAAK,gBAAgBL,OAAM;YAExF,IAAIM;YACJ,IAAIZ,kBAAkB;gBACpB,sCAAsC;gBACtCY,oBAAoBJ,mBAAmB,CAAC,SAAS,EAAER,kBAAkB,GAAGA;YAC1E,OAAO;gBACL,yCAAyC;gBACzC,MAAMa,WAAW,GAAGf,aAAa,cAAc,CAAC;gBAChDc,oBAAoBJ,mBAAmB,CAAC,SAAS,EAAEK,UAAU,GAAGA;YAClE;YAEA,6DAA6D;YAC7D,MAAMvE,QAAQwE,GAAG,CAACvG,yBAAyBqG;YAC3C,MAAMlE,OAAOzB,GAAG,CAAC,iBAAiB,CAAC,qBAAqB,EAAE2F,mBAAmB;YAE7E,kCAAkC;YAClC,MAAMvF,QAAQ,IAAIZ,MAChB;gBACE0B;gBACA4E,uBAAuBrG,4BACrBwB,KAAK8E,aAAa,EAClBxE,mBACAC,eACAF;gBAEFqD;gBACAqB,sBAAsBtG,2BAA2BuB,KAAK8E,aAAa,EAAEzE;gBACrEtB,KAAK6B;gBACLT;gBACA6E,mBAAmBtG,wBACjBsB,KAAK8E,aAAa,EAClBxE,mBACAC,eACAF,WACAF;YAEJ,GACA;gBACE,GAAGH,IAAI;gBACP,4EAA4E;gBAC5EiF,WAAW;YACb;YAGF,0BAA0B;YAC1B,MAAMzE,OAAOzB,GAAG,CAAC,QAAQ;YAEzB,8CAA8C;YAC9C,eAAemG;gBACb1E,OAAOmD,MAAM,CAAC;gBACd,MAAMvD,QAAQwE,GAAG,CAACtG,mBAAmB,eAAe6G,OAAOpC,KAAKqC,GAAG;gBACnE,IAAI;oBACF,MAAMjG,MAAMqD,iBAAiB;oBAC7BhC,OAAOmD,MAAM,CAAC;oBACd,4CAA4C;oBAC5C,IAAI7C,wBAAwB;wBAC1BA;wBACAA,yBAAyB;oBAC3B;gBACF,EAAE,OAAOuE,OAAO;oBACd7E,OAAOmD,MAAM,CAAC,iDAAiD0B;oBAC/D,wDAAwD;oBACxD,IAAI,CAACvE,wBAAwB;wBAC3BA,yBAAyBZ,SAASoF,oBAAoB,CAAC,WAAW;4BAChEJ,wBAAwBhD,KAAK,CAAC,CAACqD;gCAC7B/E,OAAOmD,MAAM,CAAC,uBAAuB4B;4BACvC;wBACF;oBACF;gBACF;YACF;YAEA,+EAA+E;YAC/E,MAAMC,eAAe,MAAMpF,QAAQ2B,GAAG,CAACzD,mBAAmB;YAC1D,MAAMmH,UAAU,MAAMrF,QAAQ2B,GAAG,CAACzD,mBAAmB;YACrD,MAAMoH,YAAYF,eAAeG,SAASH,cAAc,MAAM;YAC9D,MAAMI,OAAOH,UAAUE,SAASF,SAAS,MAAM;YAE/C,iCAAiC;YACjCjF,OAAOmD,MAAM,CAAC,iCAAiC;gBAC7CiC,MAAMA,OAAO,IAAI7C,KAAK6C,MAAM5C,WAAW,KAAK;gBAC5C0C,WAAWA,YAAY,IAAI3C,KAAK2C,WAAW1C,WAAW,KAAK;YAC7D;YAEA,IAAI0C,cAAc,MAAM;gBACtB,6BAA6B;gBAC7BlF,OAAOmD,MAAM,CAAC;gBACd7C,yBAAyBZ,SAASoF,oBAAoB,CAAC,WAAW;oBAChEJ,wBAAwBhD,KAAK,CAAC,CAACqD;wBAC7B/E,OAAOmD,MAAM,CAAC,uBAAuB4B;oBACvC;gBACF;YACF,OAAO,IAAIK,SAAS,MAAM;gBACxB,0BAA0B;gBAC1BpF,OAAOmD,MAAM,CAAC;gBACduB,wBAAwBhD,KAAK,CAAC,CAACqD;oBAC7B/E,OAAOmD,MAAM,CAAC,uBAAuB4B;gBACvC;YACF,OAAO,IAAIG,YAAYE,MAAM;gBAC3B,uDAAuD;gBACvDpF,OAAOmD,MAAM,CAAC;gBACduB,wBAAwBhD,KAAK,CAAC,CAACqD;oBAC7B/E,OAAOmD,MAAM,CAAC,eAAe4B;gBAC/B;YACF,OAAO;gBACL,oCAAoC;gBACpC/E,OAAOmD,MAAM,CAAC,wBAAwB;oBACpCkC,UAAU,IAAI9C,KAAK6C,MAAM5C,WAAW;gBACtC;gBACAlC,yBAAyBZ,SAASoF,oBAAoB,CAAC,WAAW;oBAChEJ,wBAAwBhD,KAAK,CAAC,CAACqD;wBAC7B/E,OAAOmD,MAAM,CAAC,uBAAuB4B;oBACvC;gBACF;YACF;YAEA,2EAA2E;YAC3E,MAAMO,kBAAkB5G,sBAAsBC;YAE9C,OAAO;gBACL+B,SAAS;oBAAEkB,mBAAmB;wBAAEjD;oBAAM;gBAAE;gBACxCiC,SAAS;oBACP2E,eAAeD;oBACf,iFAAiF;oBACjF,mFAAmF;oBACnFE,kBAAkB5F;oBAClBhB,MAAM;wBAAE6G,YAAY;4BAAExF,SAAS;wBAAK;oBAAE;gBACxC;YACF;QACF;QACAyF,QAAQ;YACN9G,MAAM;gBACJ+G,QAAQ;oBACN9C,QAAQ;wBACN+C,MAAM;wBACNC,UAAU;oBACZ;gBACF;YACF;QACF;IACF;AACF,EAAC"}
@@ -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
- // Fetch accounts from Better Auth for this user
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":"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"}
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":"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,UAAUd,MAAc,EAAiB;QACrD,MAAMlB,MAAM,IAAI,CAACrB,IAAI,EAAEqB,OAAQ,CAAA,KAAO,CAAA;QACtC,MAAMvB,OAAOyC,OAAOe,EAAE;QAEtB,gDAAgD;QAChD,MAAMC,WAAW,MAAM,IAAI,CAACvD,IAAI,CAAC+C,eAAe,CAACS,YAAY,CAAC1D;QAE9DuB,IAAI,yBAAyB;YAC3BoC,cAAcF,UAAU5B,UAAU;YAClC4B,UAAUA,UAAUG,IAAI,CAACC,IAAO,CAAA;oBAAEL,IAAIK,EAAEL,EAAE;oBAAEM,YAAYD,EAAEC,UAAU;gBAAC,CAAA;YACrE9D;QACF;QAEA,MAAM,IAAI,CAACE,IAAI,CAAC6D,iBAAiB,CAACtB,QAAQgB;IAC5C;IAEA,MAAcO,QAAQlE,CAAO,EAAE;QAC7B,MAAMyB,MAAM,IAAI,CAACrB,IAAI,EAAEqB,OAAQ,CAAA,KAAO,CAAA;QACtC,IAAIzB,EAAEC,IAAI,KAAK,UAAU;YACvBwB,IAAI,gBAAgB;gBAAE0C,UAAUnE,EAAEmE,QAAQ;gBAAEjE,MAAMF,EAAEE,IAAI;YAAC;YACzD,MAAMyC,SAAS3C,EAAE2C,MAAM,IAAI;gBAAEe,IAAI1D,EAAEE,IAAI;YAAC;YACxC,MAAM,IAAI,CAACuD,SAAS,CAACd;YACrB;QACF;QACA,SAAS;QACTlB,IAAI,gBAAgB;YAAE0C,UAAUnE,EAAEmE,QAAQ;YAAEjE,MAAMF,EAAEE,IAAI;QAAC;QACzD,MAAM,IAAI,CAACE,IAAI,CAACgE,qBAAqB,CAACpE,EAAEE,IAAI;IAC9C;IAEQmE,wBAAwB;QAC9B,IAAI,IAAI,CAACvD,gBAAgB,EAAE;YACzBwD,aAAa,IAAI,CAACxD,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,OAAOiD,QAAQ;gBACf,+CAA+C;gBACjD,SAAU;oBACR,IAAI,CAACxD,WAAW,GAAG;oBACnB,uDAAuD;oBACvD,IAAI,CAACsD,qBAAqB;gBAC5B;YACF;QACF,GAAG,IAAI,CAACxD,gBAAgB;QAExB,2EAA2E;QAC3E,IAAI,WAAW,IAAI,CAACC,gBAAgB,IAAI,OAAO,IAAI,CAACA,gBAAgB,CAAC0D,KAAK,KAAK,YAAY;YACzF,IAAI,CAAC1D,gBAAgB,CAAC0D,KAAK;QAC7B;IACF;IACA,0EAA0E,GAC1E,MAAcC,2BAA2BC,WAAmB,EAAE;QAC5D,MAAMjD,MAAM,IAAI,CAACrB,IAAI,EAAEqB,OAAQ,CAAA,KAAO,CAAA;QACtC,MAAMkD,WAAW;QACjB,IAAIC,UAA8B;QAElC,kEAAkE;QAClE,IAAI,IAAI,CAACxE,IAAI,CAACyE,mBAAmB,EAAE;YACjCD,UAAU,IAAIE;YACd,IAAIC,WAAW;YACf,IAAIC,UAAU;YAEd,GAAG;gBACD,MAAM,EAAE9B,KAAK,EAAEG,OAAO4B,OAAO,EAAE,GAAG,MAAM,IAAI,CAAClC,eAAe,CAAC;oBAC3DC,OAAO2B;oBACP1B,QAAQ8B;gBACV;gBACAC,UAAU9B;gBAEV,gEAAgE;gBAChE,KAAK,MAAMgC,KAAKD,QAAS;oBACvB,IAAI,CAACE,aAAa,CAACD,GAAG,OAAO,kBAAkBR;oBAC/CE,QAAQQ,GAAG,CAACF,EAAExB,EAAE;gBAClB;gBAEAqB,YAAYE,QAAQlD,MAAM;gBAC1BN,IAAI,0BAA0B;oBAAEf,WAAWqE;oBAAUL;oBAAaxB,OAAO8B;gBAAQ;YACnF,QAASD,WAAWC,QAAQ;QAC9B,OAAO;YACL,2EAA2E;YAC3E,IAAID,WAAW;YACf,IAAIC,UAAU;YAEd,GAAG;gBACD,4EAA4E;gBAC5E,6DAA6D;gBAC7D,MAAM,EAAE9B,KAAK,EAAEG,OAAO4B,OAAO,EAAE,GAAG,MAAM,IAAI,CAAClC,eAAe,CAAC;oBAC3DC,OAAO2B;oBACP1B,QAAQ8B;gBACV;gBACAC,UAAU9B;gBAEV,gEAAgE;gBAChE,KAAK,MAAMgC,KAAKD,QAAS;oBACvB,IAAI,CAACE,aAAa,CAACD,GAAG,OAAO,kBAAkBR;gBACjD;gBAEAK,YAAYE,QAAQlD,MAAM;gBAC1BN,IAAI,0BAA0B;oBAAEf,WAAWqE;oBAAUL;oBAAaxB,OAAO8B;gBAAQ;YACnF,QAASD,WAAWC,QAAQ;QAC9B;QAEA,wDAAwD;QACxD,IAAI,IAAI,CAAC5E,IAAI,CAACyE,mBAAmB,IAAID,SAAS;YAC5C,IAAIS,cAAc;YAClB,IAAIC,cAAc;YAElB,MAAOA,YAAa;gBAClB,MAAM,EAAEA,aAAaC,QAAQ,EAAElC,OAAOmC,MAAM,EAAE,GAAG,MAAM,IAAI,CAACpF,IAAI,CAACqF,oBAAoB,CACnFd,UACAU;gBAEFC,cAAcC;gBAEd,KAAK,MAAMG,MAAMF,OAAQ;oBACvB,MAAMtF,OAAOwF,GAAGC,QAAQ,EAAEC;oBAC1B,IAAI1F,QAAQ,CAAC0E,QAAQiB,GAAG,CAAC3F,OAAO;wBAC9B,IAAI,CAAC4F,aAAa,CAAC5F,MAAM,OAAO,kBAAkBwE;oBACpD;gBACF;gBAEAW;gBACA5D,IAAI,+BAA+B;oBAAEsE,MAAMV,cAAc;oBAAGX;gBAAY;YAC1E;QACF;IACF;IAEA,MAAcsB,OAAO;QACnB,IAAI,IAAI,CAACrF,UAAU,EAAE;YACnB;QACF;QACA,MAAMsF,MAAMC,KAAKD,GAAG;QACpB,MAAME,MAAM,IAAI,CAACvF,CAAC,CAACwF,SAAS,CAAC,CAACpG,IAAMA,EAAEqG,MAAM,IAAIJ;QAChD,IAAIE,QAAQ,CAAC,GAAG;YACd;QACF;QACA,MAAMxE,OAAO,IAAI,CAACf,CAAC,CAACuF,IAAI;QACxB,IAAI,CAACxF,UAAU,GAAG;QAClB,IAAI;YACF,MAAM,IAAI,CAACuD,OAAO,CAACvC;YACnB,IAAI,CAACf,CAAC,CAAC0F,MAAM,CAACH,KAAK;YACnB,IAAI,CAAC7F,IAAI,CAAC4B,MAAM,CAACnC,IAAI4B;YACrB,IAAI,CAACjB,SAAS;QAChB,EAAE,OAAO6F,GAAY;YACnB,IAAI,CAAClG,MAAM;YACX,IAAI,CAACG,SAAS,GAAG+F,aAAaC,QAAQD,EAAEE,OAAO,GAAGC,OAAOH;YACzD5E,KAAKwC,QAAQ,IAAI;YACjB,MAAMwC,QACJC,KAAKC,GAAG,CAAC,QAAQD,KAAKE,GAAG,CAAC,GAAGnF,KAAKwC,QAAQ,IAAI,QAAQyC,KAAKG,KAAK,CAACH,KAAKI,MAAM,KAAK;YACnFrF,KAAK0E,MAAM,GAAGJ,MAAMU;QACtB,SAAU;YACR,IAAI,CAAChG,UAAU,GAAG;QACpB;IACF;IAEAmF,cACE5F,IAAY,EACZqC,WAAW,KAAK,EAChBN,SAAqB,gBAAgB,EACrCyC,WAAoB,EACpB;QACA,IAAI,CAACpC,OAAO,CACV;YAAE6B,UAAU;YAAGjE;YAAMD,MAAM;YAAUoG,QAAQH,KAAKD,GAAG;YAAIvB;YAAazC;QAAO,GAC7EM;IAEJ;IAEA,6BAA6B;IAC7B4C,cACE8B,IAAY,EACZ1E,WAAW,KAAK,EAChBN,SAAqB,gBAAgB,EACrCyC,WAAoB,EACpB;QACA,IAAI,CAACpC,OAAO,CACV;YACE6B,UAAU;YACVjE,MAAM+G,KAAKvD,EAAE;YACbf,QAAQsE;YACRhH,MAAM;YACNoG,QAAQH,KAAKD,GAAG;YAChBvB;YACAzC;QACF,GACAM;IAEJ;IAEA;;;;GAIC,GACD,MAAM2E,aAAaD,IAAY,EAAiB;QAC9C,MAAM,IAAI,CAACxD,SAAS,CAACwD;IACvB;IAEA,wEAAwE,GACxE,MAAM3F,oBAAoB;QACxB,MAAMG,MAAM,IAAI,CAACrB,IAAI,EAAEqB,OAAQ,CAAA,KAAO,CAAA;QACtC,IAAI,CAAChB,UAAU,GAAG,IAAIyF,OAAOiB,WAAW;QACxC,MAAMzC,cAAc,CAAC,UAAU,EAAEwB,KAAKD,GAAG,GAAG,CAAC,EAAEW,KAAKI,MAAM,GAAGpB,QAAQ,CAAC,IAAIwB,MAAM,CAAC,GAAG,IAAI;QAExF3F,IAAI,wBAAwB;YAAEiD;QAAY;QAE1C,6EAA6E;QAC7E,IAAI,CAAC7C,uBAAuB;QAE5B,MAAM,IAAI,CAAC4C,0BAA0B,CAACC;QAEtCjD,IAAI,uBAAuB,IAAI,CAAC4F,MAAM;IACxC;IAEAnG,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,GAAGsG,YAAY,IAAM,IAAI,CAACtB,IAAI,IAAI7E;YAChD,2EAA2E;YAC3E,IAAI,WAAW,IAAI,CAACH,SAAS,IAAI,OAAO,IAAI,CAACA,SAAS,CAACwD,KAAK,KAAK,YAAY;gBAC3E,IAAI,CAACxD,SAAS,CAACwD,KAAK;YACtB;QACF;QAEA,+BAA+B;QAC/B,IAAI,CAACH,qBAAqB;IAC5B;IAEAgD,SAAS;QACP,MAAME,cAAc,IAAI,CAAC3G,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;YACnBmH,oBAAoBxF;YACpBxB,WAAW,IAAI,CAACA,SAAS;YACzBC,YAAY,IAAI,CAACA,UAAU;YAC3BC,WAAW,IAAI,CAACA,SAAS;YACzBC,YAAY,IAAI,CAACA,UAAU;YAC3B8G,WAAW,IAAI,CAAC7G,CAAC,CAACmB,MAAM;YACxBhB,aAAa,IAAI,CAACA,WAAW;YAC7B2G,YAAYC,MAAMC,IAAI,CAAC,IAAI,CAACtH,IAAI,CAACA,IAAI,IAAIuH,KAAK,CAAC,GAAG;YAClDC,oBAAoBP;QACtB;IACF;AACF"}
@@ -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/auth/methods`;
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/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,sBAAsB,CAAC;IAExD,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"}
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.1.1",
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:3000/admin >/dev/null; do sleep 0.25; done; npm run test:int'\""
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
- return Promise.resolve()
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
- return Promise.resolve()
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
- '/auth/methods',
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 runTask(t: Task) {
163
+ private async runEnsure(baUser: BAUser): Promise<void> {
164
164
  const log = this.deps?.log ?? (() => {})
165
- if (t.kind === 'ensure') {
166
- log('queue.ensure', { attempts: t.attempts, baId: t.baId })
165
+ const baId = baUser.id
167
166
 
168
- // Get user data (either from task or fetch from BA)
169
- const baUser = t.baUser ?? { id: t.baId }
167
+ // Fetch accounts from Better Auth for this user
168
+ const accounts = await this.deps.internalAdapter.findAccounts(baId)
170
169
 
171
- // Fetch accounts from Better Auth for this user
172
- const accounts = await this.deps.internalAdapter.findAccounts(t.baId)
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
- // Debug: log what accounts were found
175
- log('queue.ensure.accounts', {
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
- // Sync user with accounts to Payload
182
- await this.deps.syncUserToPayload(baUser, accounts as BetterAuthAccount[])
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/auth/methods`
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')