@nexpress/core 0.3.7 → 0.3.9
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/dist/{audit-43OLHR3U.js → audit-ZLNKBIDO.js} +3 -3
- package/dist/auth.js +4 -4
- package/dist/{can-UJ2NAOIR.js → can-U5F4JBZ7.js} +2 -2
- package/dist/{chunk-OMGQZ4Q5.js → chunk-2OWUHCFY.js} +2 -2
- package/dist/{chunk-OMGQZ4Q5.js.map → chunk-2OWUHCFY.js.map} +1 -1
- package/dist/{chunk-ELK6AVW5.js → chunk-2X3GBJOT.js} +2 -2
- package/dist/{chunk-HNX7COHQ.js → chunk-3SW4L3DL.js} +12 -12
- package/dist/chunk-3SW4L3DL.js.map +1 -0
- package/dist/{chunk-ML2E3P3X.js → chunk-5C22NDW4.js} +2 -2
- package/dist/chunk-5C22NDW4.js.map +1 -0
- package/dist/{chunk-RKM4GDWM.js → chunk-6MRTH734.js} +1 -1
- package/dist/chunk-6MRTH734.js.map +1 -0
- package/dist/{chunk-PW43RCJK.js → chunk-6OUWW6JF.js} +2 -2
- package/dist/{chunk-QBIJZZ5V.js → chunk-CGLJBRRX.js} +2 -2
- package/dist/chunk-CGLJBRRX.js.map +1 -0
- package/dist/{chunk-2VZZ7M26.js → chunk-EAYUAXW3.js} +3 -3
- package/dist/chunk-EAYUAXW3.js.map +1 -0
- package/dist/{chunk-2N53KKIL.js → chunk-EWVXP3GP.js} +2 -2
- package/dist/{chunk-CAS4Z6IN.js → chunk-I4FSVEJK.js} +1 -1
- package/dist/chunk-I4FSVEJK.js.map +1 -0
- package/dist/{chunk-LN6NTH6E.js → chunk-K4CJ3KXB.js} +3 -3
- package/dist/chunk-K4CJ3KXB.js.map +1 -0
- package/dist/{chunk-B7DTNT4O.js → chunk-MWLSXK6Y.js} +2 -2
- package/dist/{chunk-NFHS7CFV.js → chunk-Q7MK5ZKG.js} +2 -2
- package/dist/{chunk-PUV3VZPD.js → chunk-QZ52U4ET.js} +2 -2
- package/dist/{chunk-2GXH7566.js → chunk-SJ7M2VCC.js} +10 -10
- package/dist/chunk-SJ7M2VCC.js.map +1 -0
- package/dist/{chunk-6UV2P5MW.js → chunk-TIWJVQOO.js} +3 -3
- package/dist/chunk-TIWJVQOO.js.map +1 -0
- package/dist/{chunk-MLXKZK6G.js → chunk-TSCXXBOM.js} +76 -28
- package/dist/chunk-TSCXXBOM.js.map +1 -0
- package/dist/{chunk-L6VG7IK6.js → chunk-VBVLYFSZ.js} +2 -2
- package/dist/chunk-VBVLYFSZ.js.map +1 -0
- package/dist/{chunk-RDTTK27V.js → chunk-XPD7EQML.js} +3 -3
- package/dist/chunk-XPD7EQML.js.map +1 -0
- package/dist/{chunk-RJ76SKWQ.js → chunk-XU2GJJ6Z.js} +1 -1
- package/dist/chunk-XU2GJJ6Z.js.map +1 -0
- package/dist/{chunk-WJJ5MBH5.js → chunk-YEOQJ7WW.js} +1 -1
- package/dist/chunk-YEOQJ7WW.js.map +1 -0
- package/dist/community.js +14 -14
- package/dist/{config-YHUEYQ66.js → config-YDGNUDKP.js} +5 -5
- package/dist/{digest-ZODDTXA2.js → digest-IWHMJPXI.js} +4 -4
- package/dist/{host-XBGYIQEE.js → host-HG4QGD3L.js} +4 -4
- package/dist/i18n.js +2 -2
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/{job-log-N3IGI4NA.js → job-log-UY6ERPQZ.js} +3 -3
- package/dist/jobs.js +3 -3
- package/dist/{logger-2WUTTELV.js → logger-6ZGEKEMK.js} +2 -2
- package/dist/media.js +3 -3
- package/dist/{mentions-U4JACYI6.js → mentions-LQRZWAGO.js} +2 -2
- package/dist/{mutes-MNQP6ACF.js → mutes-PQA6U5X7.js} +2 -2
- package/dist/{notification-prefs-H4HFVCL7.js → notification-prefs-62NX2GBF.js} +2 -2
- package/dist/observability.js +2 -2
- package/dist/{reputation-ICIXDGPM.js → reputation-5DJLDBZY.js} +3 -3
- package/dist/{scheduled-S6IO47JD.js → scheduled-C2IKVZVK.js} +5 -5
- package/dist/seo.js +4 -4
- package/dist/{settings-OZWM6L2K.js → settings-NBAP7E5E.js} +2 -2
- package/dist/{strings-4EWJYDOG.js → strings-O2M7VSKV.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-2GXH7566.js.map +0 -1
- package/dist/chunk-2VZZ7M26.js.map +0 -1
- package/dist/chunk-6UV2P5MW.js.map +0 -1
- package/dist/chunk-CAS4Z6IN.js.map +0 -1
- package/dist/chunk-HNX7COHQ.js.map +0 -1
- package/dist/chunk-L6VG7IK6.js.map +0 -1
- package/dist/chunk-LN6NTH6E.js.map +0 -1
- package/dist/chunk-ML2E3P3X.js.map +0 -1
- package/dist/chunk-MLXKZK6G.js.map +0 -1
- package/dist/chunk-QBIJZZ5V.js.map +0 -1
- package/dist/chunk-RDTTK27V.js.map +0 -1
- package/dist/chunk-RJ76SKWQ.js.map +0 -1
- package/dist/chunk-RKM4GDWM.js.map +0 -1
- package/dist/chunk-WJJ5MBH5.js.map +0 -1
- /package/dist/{audit-43OLHR3U.js.map → audit-ZLNKBIDO.js.map} +0 -0
- /package/dist/{can-UJ2NAOIR.js.map → can-U5F4JBZ7.js.map} +0 -0
- /package/dist/{chunk-ELK6AVW5.js.map → chunk-2X3GBJOT.js.map} +0 -0
- /package/dist/{chunk-PW43RCJK.js.map → chunk-6OUWW6JF.js.map} +0 -0
- /package/dist/{chunk-2N53KKIL.js.map → chunk-EWVXP3GP.js.map} +0 -0
- /package/dist/{chunk-B7DTNT4O.js.map → chunk-MWLSXK6Y.js.map} +0 -0
- /package/dist/{chunk-NFHS7CFV.js.map → chunk-Q7MK5ZKG.js.map} +0 -0
- /package/dist/{chunk-PUV3VZPD.js.map → chunk-QZ52U4ET.js.map} +0 -0
- /package/dist/{config-YHUEYQ66.js.map → config-YDGNUDKP.js.map} +0 -0
- /package/dist/{digest-ZODDTXA2.js.map → digest-IWHMJPXI.js.map} +0 -0
- /package/dist/{host-XBGYIQEE.js.map → host-HG4QGD3L.js.map} +0 -0
- /package/dist/{job-log-N3IGI4NA.js.map → job-log-UY6ERPQZ.js.map} +0 -0
- /package/dist/{logger-2WUTTELV.js.map → logger-6ZGEKEMK.js.map} +0 -0
- /package/dist/{mentions-U4JACYI6.js.map → mentions-LQRZWAGO.js.map} +0 -0
- /package/dist/{mutes-MNQP6ACF.js.map → mutes-PQA6U5X7.js.map} +0 -0
- /package/dist/{notification-prefs-H4HFVCL7.js.map → notification-prefs-62NX2GBF.js.map} +0 -0
- /package/dist/{reputation-ICIXDGPM.js.map → reputation-5DJLDBZY.js.map} +0 -0
- /package/dist/{scheduled-S6IO47JD.js.map → scheduled-C2IKVZVK.js.map} +0 -0
- /package/dist/{settings-OZWM6L2K.js.map → settings-NBAP7E5E.js.map} +0 -0
- /package/dist/{strings-4EWJYDOG.js.map → strings-O2M7VSKV.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/jobs/handlers.ts","../src/email/templates.ts","../src/jobs/builtin-handlers.ts","../src/jobs/heartbeat.ts","../src/jobs/pause-state.ts","../src/jobs/pg-boss-adapter.ts","../src/jobs/worker.ts"],"sourcesContent":["import { type NpJobType } from \"../config/types.js\";\n\nexport type NpJobHandler = (data: unknown) => Promise<void>;\n\nconst handlers = new Map<NpJobType, NpJobHandler>();\n\nexport function registerJobHandler(type: NpJobType, handler: NpJobHandler): void {\n handlers.set(type, handler);\n}\n\nexport function getJobHandler(type: NpJobType): NpJobHandler | undefined {\n return handlers.get(type);\n}\n\nexport function getAllJobHandlers(): ReadonlyMap<NpJobType, NpJobHandler> {\n return handlers;\n}\n","export interface NpPasswordResetTemplateData {\n siteName: string;\n name: string;\n resetUrl: string;\n /** When the link expires (ISO string), for display only. */\n expiresAt?: string;\n}\n\nexport interface NpEmailTemplate {\n subject: string;\n text: string;\n html: string;\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nfunction wrap(siteName: string, contentHtml: string): string {\n // Table-based layout is the safest cross-client default. Keeps styles\n // inline so most webmail clients don't rewrite them away.\n return `<!doctype html>\n<html>\n<body style=\"margin:0;padding:24px;background:#f5f5f5;font-family:system-ui,-apple-system,Segoe UI,sans-serif;color:#111;\">\n <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:560px;margin:0 auto;background:#fff;border-radius:12px;padding:32px;border:1px solid #e5e5e5;\">\n <tr>\n <td>\n <h1 style=\"margin:0 0 16px;font-size:20px;font-weight:600;\">${escapeHtml(siteName)}</h1>\n ${contentHtml}\n <p style=\"margin-top:32px;font-size:12px;color:#777;\">If you didn't expect this email you can ignore it.</p>\n </td>\n </tr>\n </table>\n</body>\n</html>`;\n}\n\nexport function buildInviteEmail(data: NpPasswordResetTemplateData): NpEmailTemplate {\n const subject = `You're invited to ${data.siteName}`;\n const text =\n `Hi ${data.name},\\n\\n` +\n `You've been invited to ${data.siteName}. Set your password to activate your account:\\n\\n` +\n `${data.resetUrl}\\n\\n` +\n `This link expires in 7 days.`;\n\n const html = wrap(\n data.siteName,\n `\n <p style=\"margin:0 0 16px;\">Hi ${escapeHtml(data.name)},</p>\n <p style=\"margin:0 0 24px;\">You've been invited to ${escapeHtml(data.siteName)}. Set your password to activate your account:</p>\n <p style=\"margin:0 0 24px;\"><a href=\"${escapeHtml(data.resetUrl)}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;padding:10px 20px;border-radius:8px;font-weight:500;\">Set my password</a></p>\n <p style=\"margin:0 0 8px;font-size:13px;color:#555;\">Or copy the link:</p>\n <p style=\"margin:0;font-size:13px;color:#555;word-break:break-all;\">${escapeHtml(data.resetUrl)}</p>\n <p style=\"margin-top:24px;font-size:13px;color:#555;\">This link expires in 7 days.</p>\n `,\n );\n\n return { subject, text, html };\n}\n\nexport interface NpMemberVerifyTemplateData {\n siteName: string;\n displayName: string;\n verifyUrl: string;\n}\n\n/**\n * Email a brand-new member to confirm their address. Different copy\n * from the staff invite (members self-register, no admin invited them)\n * but reuses the same wrapper styling.\n */\nexport function buildMemberVerifyEmail(data: NpMemberVerifyTemplateData): NpEmailTemplate {\n const subject = `Confirm your ${data.siteName} account`;\n const text =\n `Hi ${data.displayName},\\n\\n` +\n `Welcome to ${data.siteName}. Confirm your email so we can activate your account:\\n\\n` +\n `${data.verifyUrl}\\n\\n` +\n `This link expires in 24 hours. If you didn't sign up, you can ignore this email.`;\n\n const html = wrap(\n data.siteName,\n `\n <p style=\"margin:0 0 16px;\">Hi ${escapeHtml(data.displayName)},</p>\n <p style=\"margin:0 0 24px;\">Welcome to ${escapeHtml(data.siteName)}. Confirm your email so we can activate your account:</p>\n <p style=\"margin:0 0 24px;\"><a href=\"${escapeHtml(data.verifyUrl)}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;padding:10px 20px;border-radius:8px;font-weight:500;\">Confirm my email</a></p>\n <p style=\"margin:0 0 8px;font-size:13px;color:#555;\">Or copy the link:</p>\n <p style=\"margin:0;font-size:13px;color:#555;word-break:break-all;\">${escapeHtml(data.verifyUrl)}</p>\n <p style=\"margin-top:24px;font-size:13px;color:#555;\">This link expires in 24 hours. If you didn't sign up, you can ignore this email.</p>\n `,\n );\n\n return { subject, text, html };\n}\n\nexport function buildResetEmail(data: NpPasswordResetTemplateData): NpEmailTemplate {\n const subject = `Reset your ${data.siteName} password`;\n const text =\n `Hi ${data.name},\\n\\n` +\n `Someone requested a password reset for your ${data.siteName} account. ` +\n `If that was you, use this link to set a new one:\\n\\n` +\n `${data.resetUrl}\\n\\n` +\n `This link expires in 1 hour. If you didn't request it, you can ignore this email.`;\n\n const html = wrap(\n data.siteName,\n `\n <p style=\"margin:0 0 16px;\">Hi ${escapeHtml(data.name)},</p>\n <p style=\"margin:0 0 24px;\">Someone requested a password reset for your ${escapeHtml(data.siteName)} account. If that was you, use this link to set a new one:</p>\n <p style=\"margin:0 0 24px;\"><a href=\"${escapeHtml(data.resetUrl)}\" style=\"display:inline-block;background:#111;color:#fff;text-decoration:none;padding:10px 20px;border-radius:8px;font-weight:500;\">Reset password</a></p>\n <p style=\"margin:0 0 8px;font-size:13px;color:#555;\">Or copy the link:</p>\n <p style=\"margin:0;font-size:13px;color:#555;word-break:break-all;\">${escapeHtml(data.resetUrl)}</p>\n <p style=\"margin-top:24px;font-size:13px;color:#555;\">This link expires in 1 hour. If you didn't request it you can ignore this email.</p>\n `,\n );\n\n return { subject, text, html };\n}\n","import {\n type NpAuthUser,\n type NpCollectionConfig,\n type NpCollectionHook,\n type NpHookPrincipal,\n type NpJobType,\n} from \"../config/types.js\";\nimport { getEmailAdapter } from \"../email/service.js\";\nimport { buildInviteEmail, buildMemberVerifyEmail, buildResetEmail } from \"../email/templates.js\";\nimport { registerJobHandler } from \"./handlers.js\";\n\ninterface ContentJobData {\n collection: string;\n documentId: string;\n operation: \"create\" | \"update\";\n userId: string;\n}\n\ninterface ContentDeleteJobData {\n collection: string;\n documentId: string;\n userId: string;\n}\n\ninterface ResolvedHookContext {\n collectionConfig: NpCollectionConfig;\n data: Record<string, unknown>;\n /**\n * Resolved staff session, or `null` when the originating actor\n * was a member (Phase 9.7o widened the hook surface so member\n * writes also fire `afterCreate` / `afterUpdate`).\n */\n user: NpAuthUser | null;\n /**\n * Polymorphic actor reference. Resolvers should derive this\n * from whatever actor metadata they recorded with the job —\n * e.g. by checking whether the saved `userId` is null\n * (member-authored) and looking up the member id separately.\n */\n principal: NpHookPrincipal;\n originalDoc?: Record<string, unknown> | null;\n}\n\ninterface ResolvedDeleteHookContext {\n collectionConfig: NpCollectionConfig;\n data: Record<string, unknown>;\n user: NpAuthUser | null;\n principal: NpHookPrincipal;\n}\n\ninterface BuiltinJobContext {\n resolveContentAfterSaveContext?: (\n data: ContentJobData,\n ) => Promise<ResolvedHookContext | null> | ResolvedHookContext | null;\n resolveContentAfterDeleteContext?: (\n data: ContentDeleteJobData,\n ) => Promise<ResolvedDeleteHookContext | null> | ResolvedDeleteHookContext | null;\n processImage?: (data: unknown) => Promise<void> | void;\n cleanupMedia?: (data: unknown) => Promise<void> | void;\n runScheduledPluginTask?: (data: unknown) => Promise<void> | void;\n pruneRevisions?: () => Promise<void> | void;\n cleanupSessions?: () => Promise<void> | void;\n sendPasswordReset?: (data: unknown) => Promise<void> | void;\n sendMemberVerifyEmail?: (data: unknown) => Promise<void> | void;\n sendMemberPasswordReset?: (data: unknown) => Promise<void> | void;\n}\n\nconst builtinJobContext: BuiltinJobContext = {};\n\nexport function configureBuiltinJobContext(context: Partial<BuiltinJobContext>): void {\n Object.assign(builtinJobContext, context);\n}\n\nexport function registerBuiltinHandlers(): void {\n registerJobHandler(\"content:afterSave\", handleContentAfterSave);\n registerJobHandler(\"content:afterDelete\", handleContentAfterDelete);\n registerJobHandler(\"content:publishScheduled\", handleContentPublishScheduled);\n registerJobHandler(\"media:processImage\", handleMediaProcessImage);\n registerJobHandler(\"media:cleanup\", handleMediaCleanup);\n registerJobHandler(\"plugin:scheduledTask\", handlePluginScheduledTask);\n registerJobHandler(\"system:revisionPrune\", handleRevisionPrune);\n registerJobHandler(\"system:sessionCleanup\", handleSessionCleanup);\n registerJobHandler(\"system:jobLogPrune\", handleJobLogPrune);\n registerJobHandler(\"auth:sendPasswordReset\", handleAuthSendPasswordReset);\n registerJobHandler(\"members:sendVerifyEmail\", handleMemberSendVerifyEmail);\n registerJobHandler(\"members:sendPasswordReset\", handleMemberSendPasswordReset);\n registerJobHandler(\"notifications:sendDigest\", handleNotificationsSendDigest);\n}\n\nasync function handleContentPublishScheduled(_: unknown): Promise<void> {\n const { publishScheduledDocuments } = await import(\"../collections/scheduled.js\");\n const result = await publishScheduledDocuments();\n if (result.published > 0) {\n console.info(\n `[nexpress] content:publishScheduled flipped ${result.published} document(s)`,\n result.byCollection,\n );\n }\n}\n\nasync function handleContentAfterSave(data: unknown): Promise<void> {\n const jobData = asContentJobData(data);\n\n await revalidateCollectionTags(jobData.collection, jobData.documentId);\n\n const context = await builtinJobContext.resolveContentAfterSaveContext?.(jobData);\n\n if (!context) {\n return;\n }\n\n const hooks =\n jobData.operation === \"create\"\n ? context.collectionConfig.hooks?.afterCreate\n : context.collectionConfig.hooks?.afterUpdate;\n\n await runCollectionHooks(hooks, {\n data: context.data,\n user: context.user,\n principal: context.principal,\n collection: context.collectionConfig.slug,\n originalDoc: context.originalDoc,\n });\n}\n\nasync function handleContentAfterDelete(data: unknown): Promise<void> {\n const jobData = asContentDeleteJobData(data);\n\n await revalidateCollectionTags(jobData.collection, jobData.documentId);\n\n const context = await builtinJobContext.resolveContentAfterDeleteContext?.(jobData);\n\n if (!context) {\n return;\n }\n\n await runCollectionHooks(context.collectionConfig.hooks?.afterDelete, {\n data: context.data,\n user: context.user,\n principal: context.principal,\n collection: context.collectionConfig.slug,\n });\n}\n\nasync function handleMediaProcessImage(data: unknown): Promise<void> {\n await builtinJobContext.processImage?.(data);\n}\n\nasync function handleMediaCleanup(data: unknown): Promise<void> {\n await builtinJobContext.cleanupMedia?.(data);\n}\n\nasync function handlePluginScheduledTask(data: unknown): Promise<void> {\n // Phase 19 — first prefer the inline handler registered via\n // `definePlugin({ scheduled })`. Falls back to the legacy\n // `builtinJobContext.runScheduledPluginTask` resolver for\n // sites that wired their own dispatcher pre-Phase-19.\n if (isRecord(data) && typeof data.pluginId === \"string\" && typeof data.taskId === \"string\") {\n try {\n const { runPluginScheduledTask } = await import(\"../plugins/host.js\");\n await runPluginScheduledTask(data.pluginId, data.taskId);\n return;\n } catch (err) {\n // No registered schedule with this id — fall through to\n // the legacy resolver. If that's also absent we re-throw\n // so the worker's retry policy surfaces the misconfig.\n const message = err instanceof Error ? err.message : String(err);\n if (!/no scheduled task/.test(message) && !/is not registered/.test(message)) {\n throw err;\n }\n }\n }\n await builtinJobContext.runScheduledPluginTask?.(data);\n}\n\nasync function handleRevisionPrune(_: unknown): Promise<void> {\n await builtinJobContext.pruneRevisions?.();\n}\n\nasync function handleSessionCleanup(_: unknown): Promise<void> {\n await builtinJobContext.cleanupSessions?.();\n}\n\n/**\n * Phase 20.3 — keep `np_job_logs` from growing unbounded.\n * Default retention is 14 days; the cron registration in\n * `pg-boss-adapter.scheduleRecurring()` runs this at 03:30 UTC\n * daily (offset from `system:revisionPrune` at 03:00 so the two\n * cleanup jobs don't pile DB load on the same minute).\n */\nasync function handleJobLogPrune(_: unknown): Promise<void> {\n const { pruneJobLogsOlderThan, DEFAULT_JOB_LOG_RETENTION_MS } = await import(\"./job-log.js\");\n const cutoff = new Date(Date.now() - DEFAULT_JOB_LOG_RETENTION_MS);\n const deleted = await pruneJobLogsOlderThan(cutoff);\n if (deleted > 0) {\n console.info(`[nexpress] system:jobLogPrune deleted ${deleted} log row(s)`);\n }\n}\n\ninterface PasswordResetJobData {\n email: string;\n name: string;\n token: string;\n purpose: \"invite\" | \"reset\";\n resetUrl: string;\n /** Optional — producer may pass a site-display name for the template. */\n siteName?: string;\n}\n\n/**\n * Default handler for password-reset / invite emails. Routes the message\n * through the configured email adapter (noop by default — see\n * `NoopEmailAdapter`). Apps override either by installing a real adapter\n * (`setEmailAdapter(new SmtpEmailAdapter(...))`) or by providing a fully\n * custom handler via `configureBuiltinJobContext({ sendPasswordReset })`.\n */\nasync function handleAuthSendPasswordReset(data: unknown): Promise<void> {\n if (builtinJobContext.sendPasswordReset) {\n await builtinJobContext.sendPasswordReset(data);\n return;\n }\n\n const payload = asPasswordResetJobData(data);\n const templateData = {\n siteName: payload.siteName ?? \"your site\",\n name: payload.name,\n resetUrl: payload.resetUrl,\n };\n const template =\n payload.purpose === \"invite\" ? buildInviteEmail(templateData) : buildResetEmail(templateData);\n\n await getEmailAdapter().send({\n to: payload.email,\n subject: template.subject,\n text: template.text,\n html: template.html,\n });\n}\n\nfunction asPasswordResetJobData(data: unknown): PasswordResetJobData {\n if (!isRecord(data)) {\n throw new Error(\"Invalid auth:sendPasswordReset job payload.\");\n }\n\n return {\n email: asString(data.email, \"email\"),\n name: asString(data.name, \"name\"),\n token: asString(data.token, \"token\"),\n siteName:\n typeof data.siteName === \"string\" && data.siteName.length > 0 ? data.siteName : undefined,\n purpose: asResetPurpose(data.purpose),\n resetUrl: asString(data.resetUrl, \"resetUrl\"),\n };\n}\n\nfunction asResetPurpose(value: unknown): \"invite\" | \"reset\" {\n if (value === \"invite\" || value === \"reset\") {\n return value;\n }\n\n throw new Error(\"Invalid password reset purpose.\");\n}\n\nasync function runCollectionHooks(\n hooks: NpCollectionHook[] | undefined,\n args: Parameters<NpCollectionHook>[0],\n): Promise<void> {\n if (!hooks || hooks.length === 0) {\n return;\n }\n\n for (const hook of hooks) {\n await hook(args);\n }\n}\n\nasync function revalidateCollectionTags(collection: string, documentId: string): Promise<void> {\n try {\n const revalidateTag = await loadRevalidateTag();\n\n if (!revalidateTag) {\n return;\n }\n\n revalidateTag(`nx:${collection}`);\n revalidateTag(`nx:${collection}:${documentId}`);\n } catch {\n return;\n }\n}\n\nasync function loadRevalidateTag(): Promise<((tag: string) => void) | null> {\n // Indirect specifier so TypeScript doesn't try to resolve\n // `next/cache` at compile time — `@nexpress/core` doesn't\n // depend on Next.js, the cache helpers are only available\n // when this code runs inside a Next runtime.\n const moduleId: string = \"next/cache\";\n let importedModule: unknown;\n try {\n importedModule = await import(moduleId);\n } catch {\n return null;\n }\n\n if (!isRecord(importedModule)) {\n return null;\n }\n\n const revalidateTag = importedModule.revalidateTag as\n | ((tag: string) => void)\n | ((tag: string, profile: string) => void);\n\n if (typeof revalidateTag !== \"function\") {\n return null;\n }\n\n // Next 16 widened the signature to `(tag, profile)`. Detect\n // the runtime arity so this helper works against both 15.x\n // and 16.x without a hard pin: pre-16 ignores extra args.\n return (tag: string) => {\n if (revalidateTag.length >= 2) {\n (revalidateTag)(tag, \"default\");\n } else {\n (revalidateTag as (tag: string) => void)(tag);\n }\n };\n}\n\nfunction asContentJobData(data: unknown): ContentJobData {\n if (!isRecord(data)) {\n throw new Error(\"Invalid content:afterSave job payload.\");\n }\n\n return {\n collection: asString(data.collection, \"collection\"),\n documentId: asString(data.documentId, \"documentId\"),\n operation: asContentOperation(data.operation),\n userId: asString(data.userId, \"userId\"),\n };\n}\n\nfunction asContentDeleteJobData(data: unknown): ContentDeleteJobData {\n if (!isRecord(data)) {\n throw new Error(\"Invalid content:afterDelete job payload.\");\n }\n\n return {\n collection: asString(data.collection, \"collection\"),\n documentId: asString(data.documentId, \"documentId\"),\n userId: asString(data.userId, \"userId\"),\n };\n}\n\nfunction asContentOperation(value: unknown): ContentJobData[\"operation\"] {\n if (value === \"create\" || value === \"update\") {\n return value;\n }\n\n throw new Error(\"Invalid content operation.\");\n}\n\nfunction asString(value: unknown, fieldName: string): string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new Error(`Invalid ${fieldName} field.`);\n }\n\n return value;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\ninterface MemberVerifyJobData {\n email: string;\n displayName: string;\n verifyUrl: string;\n siteName?: string;\n}\n\ninterface MemberResetJobData {\n email: string;\n displayName: string;\n resetUrl: string;\n siteName?: string;\n}\n\nasync function handleMemberSendVerifyEmail(data: unknown): Promise<void> {\n if (builtinJobContext.sendMemberVerifyEmail) {\n await builtinJobContext.sendMemberVerifyEmail(data);\n return;\n }\n if (!isRecord(data)) throw new Error(\"Invalid members:sendVerifyEmail job payload.\");\n const payload: MemberVerifyJobData = {\n email: asString(data.email, \"email\"),\n displayName: asString(data.displayName, \"displayName\"),\n verifyUrl: asString(data.verifyUrl, \"verifyUrl\"),\n siteName:\n typeof data.siteName === \"string\" && data.siteName.length > 0 ? data.siteName : undefined,\n };\n const template = buildMemberVerifyEmail({\n siteName: payload.siteName ?? \"your site\",\n displayName: payload.displayName,\n verifyUrl: payload.verifyUrl,\n });\n await getEmailAdapter().send({\n to: payload.email,\n subject: template.subject,\n text: template.text,\n html: template.html,\n });\n}\n\nasync function handleMemberSendPasswordReset(data: unknown): Promise<void> {\n if (builtinJobContext.sendMemberPasswordReset) {\n await builtinJobContext.sendMemberPasswordReset(data);\n return;\n }\n if (!isRecord(data)) throw new Error(\"Invalid members:sendPasswordReset job payload.\");\n const payload: MemberResetJobData = {\n email: asString(data.email, \"email\"),\n displayName: asString(data.displayName, \"displayName\"),\n resetUrl: asString(data.resetUrl, \"resetUrl\"),\n siteName:\n typeof data.siteName === \"string\" && data.siteName.length > 0 ? data.siteName : undefined,\n };\n // Reuse the staff `buildResetEmail` template — copy is identical from\n // the user's POV (\"reset your <site> password\"). When templating\n // diverges we'll fork the function, not the dispatcher.\n const template = buildResetEmail({\n siteName: payload.siteName ?? \"your site\",\n name: payload.displayName,\n resetUrl: payload.resetUrl,\n });\n await getEmailAdapter().send({\n to: payload.email,\n subject: template.subject,\n text: template.text,\n html: template.html,\n });\n}\n\nasync function handleNotificationsSendDigest(data: unknown): Promise<void> {\n const cadence =\n isRecord(data) && (data.cadence === \"daily\" || data.cadence === \"weekly\")\n ? data.cadence\n : \"daily\";\n const siteName = isRecord(data) && typeof data.siteName === \"string\" ? data.siteName : undefined;\n const { runDigestSweep } = await import(\"../community/digest.js\");\n const result = await runDigestSweep({ cadence, siteName });\n\n console.info(\n `[nexpress] notifications:sendDigest cadence=${cadence}` +\n ` considered=${result.considered} sent=${result.sent}` +\n ` skipped=${result.skipped} failed=${result.failed}`,\n );\n}\n\nexport type { BuiltinJobContext, ContentDeleteJobData, ContentJobData, NpJobType };\n","import { hostname } from \"node:os\";\nimport { randomUUID } from \"node:crypto\";\n\nimport { desc, eq, gt, lt } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { readEnvPositiveInt } from \"../config/env.js\";\nimport { npWorkerHeartbeats } from \"../db/schema/system.js\";\nimport { getLogger } from \"../observability/logger.js\";\n\n/**\n * Phase 19 — worker liveness signal.\n *\n * The worker process upserts its row every\n * `WORKER_HEARTBEAT_INTERVAL_MS` so admins can tell whether the\n * pg-boss queue actually has a draining process attached.\n * Without this the only signal was \"Pending stays high while\n * Completed doesn't grow,\" which a stuck DB or a stopped\n * worker look identical from outside.\n *\n * Stale rows (no heartbeat in `WORKER_STALE_THRESHOLD_MS`) are\n * reported `unhealthy`; the row stays in place until an\n * operator GCs it or a worker with the same id rejoins. The\n * id is `hostname:pid` so a restarted process on the same host\n * naturally reclaims its row instead of stacking duplicates.\n */\n\n/**\n * How often a running worker pings its row. Tightening lets\n * `lastSeenAt` track wall-clock more closely; loosening cuts\n * write traffic on idle workers. `NP_WORKER_HEARTBEAT_SECONDS`.\n */\nexport const WORKER_HEARTBEAT_INTERVAL_MS =\n readEnvPositiveInt(\"NP_WORKER_HEARTBEAT_SECONDS\", 30) * 1_000;\n\n/**\n * After how long with no heartbeat a worker is treated as\n * unhealthy in the admin UI / health check. Default 90s is\n * `3 × HEARTBEAT_INTERVAL` so a single missed beat doesn't trip\n * the alarm. `NP_WORKER_STALE_THRESHOLD_SECONDS`.\n */\nexport const WORKER_STALE_THRESHOLD_MS =\n readEnvPositiveInt(\"NP_WORKER_STALE_THRESHOLD_SECONDS\", 90) * 1_000;\n\nexport interface NpWorkerHeartbeat {\n id: string;\n status: string;\n startedAt: Date;\n lastSeenAt: Date;\n meta: Record<string, unknown>;\n}\n\nexport interface NpWorkerHealthSummary {\n workers: Array<NpWorkerHeartbeat & { alive: boolean; lastSeenAgoMs: number }>;\n aliveCount: number;\n totalCount: number;\n /** ISO timestamp of the most recent heartbeat across all workers. */\n newestHeartbeat: string | null;\n}\n\nfunction generateWorkerId(): string {\n // Hostname is shared across pods on the same VM but differs\n // across containers. Adding the PID + a short random suffix\n // keeps the id stable across short crashes (same PID under\n // same hostname overwrites the row) while still differing\n // between fresh process starts. Falls back to a UUID when\n // hostname / pid aren't readable (rare; mostly for\n // non-Node runtimes).\n try {\n const host = hostname();\n return `${host}:${process.pid}`;\n } catch {\n return randomUUID();\n }\n}\n\n/**\n * Stamp a single heartbeat row. Used by `startHeartbeatLoop`\n * and exposed for tests so they can inject fake worker rows\n * without spinning up a real interval.\n */\nexport async function recordHeartbeat(\n workerId: string,\n meta: Record<string, unknown> = {},\n): Promise<void> {\n const db = getDb();\n const now = new Date();\n await db\n .insert(npWorkerHeartbeats)\n .values({\n id: workerId,\n status: \"running\",\n startedAt: now,\n lastSeenAt: now,\n meta,\n })\n .onConflictDoUpdate({\n target: npWorkerHeartbeats.id,\n set: { lastSeenAt: now, status: \"running\", meta },\n });\n}\n\n/**\n * Mark the row as `stopped` so the admin sees a graceful\n * shutdown rather than the row drifting into `unhealthy`.\n */\nexport async function markWorkerStopped(workerId: string): Promise<void> {\n const db = getDb();\n await db\n .update(npWorkerHeartbeats)\n .set({ status: \"stopped\", lastSeenAt: new Date() })\n .where(eq(npWorkerHeartbeats.id, workerId));\n}\n\n/**\n * Read every worker row, decorate with `alive` + `lastSeenAgoMs`\n * relative to `now`. Sorted with the most recent heartbeat\n * first so the admin's first row is the freshest worker.\n */\nexport async function listWorkerHealth(now: Date = new Date()): Promise<NpWorkerHealthSummary> {\n const db = getDb();\n const rows = (await db\n .select()\n .from(npWorkerHeartbeats)\n .orderBy(desc(npWorkerHeartbeats.lastSeenAt))) as NpWorkerHeartbeat[];\n\n let aliveCount = 0;\n const decorated = rows.map((row) => {\n const lastSeenAgoMs = Math.max(0, now.getTime() - row.lastSeenAt.getTime());\n const alive = row.status === \"running\" && lastSeenAgoMs < WORKER_STALE_THRESHOLD_MS;\n if (alive) aliveCount += 1;\n return { ...row, alive, lastSeenAgoMs };\n });\n\n return {\n workers: decorated,\n aliveCount,\n totalCount: rows.length,\n newestHeartbeat: rows[0]?.lastSeenAt.toISOString() ?? null,\n };\n}\n\ninterface HeartbeatLoopHandle {\n workerId: string;\n stop(): Promise<void>;\n}\n\n/**\n * Spin up a recurring heartbeat. Returns a handle the caller\n * keeps so they can stop it on shutdown. Errors inside the\n * loop are logged and continued — a transient DB blip\n * shouldn't crash the worker, the next tick recovers.\n */\nexport function startHeartbeatLoop(\n meta: Record<string, unknown> = {},\n intervalMs: number = WORKER_HEARTBEAT_INTERVAL_MS,\n): HeartbeatLoopHandle {\n const workerId = generateWorkerId();\n const log = getLogger();\n\n const beat = async (): Promise<void> => {\n try {\n await recordHeartbeat(workerId, meta);\n } catch (err) {\n log.warn(\"worker heartbeat failed\", {\n workerId,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n };\n\n // Beat immediately so the row exists from t=0; subsequent\n // beats happen on the interval. `setInterval` returns a\n // Timeout we keep so we can clear it on stop.\n void beat();\n const timer = setInterval(() => {\n void beat();\n }, intervalMs);\n // Don't keep the event loop alive on the heartbeat alone —\n // the worker has its own keep-alive (pg-boss); the heartbeat\n // is bookkeeping.\n if (typeof timer.unref === \"function\") timer.unref();\n\n return {\n workerId,\n async stop() {\n clearInterval(timer);\n try {\n await markWorkerStopped(workerId);\n } catch (err) {\n log.warn(\"worker heartbeat stop failed to mark row\", {\n workerId,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n },\n };\n}\n\n/**\n * Manual GC hook — purge worker rows whose `last_seen_at` is\n * older than `olderThan`. Operators can call this from a\n * cron / admin action when the table accumulates ghosts.\n */\nexport async function purgeStaleWorkers(\n olderThan: Date = new Date(Date.now() - WORKER_STALE_THRESHOLD_MS * 10),\n): Promise<number> {\n const db = getDb();\n const deleted = (await db\n .delete(npWorkerHeartbeats)\n .where(lt(npWorkerHeartbeats.lastSeenAt, olderThan))\n .returning({ id: npWorkerHeartbeats.id })) as Array<{ id: string }>;\n return deleted.length;\n}\n\n/** Return only the rows currently considered alive. Cheap probe for boot health. */\nexport async function countAliveWorkers(now: Date = new Date()): Promise<number> {\n const db = getDb();\n const cutoff = new Date(now.getTime() - WORKER_STALE_THRESHOLD_MS);\n const rows = (await db\n .select({ id: npWorkerHeartbeats.id })\n .from(npWorkerHeartbeats)\n .where(gt(npWorkerHeartbeats.lastSeenAt, cutoff))) as Array<{ id: string }>;\n return rows.length;\n}\n","import { and, eq } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npSettings } from \"../db/schema/system.js\";\nimport { getLogger } from \"../observability/logger.js\";\nimport { reportError } from \"../observability/error-reporter.js\";\nimport { type NpJobQueue } from \"./queue.js\";\n\n/**\n * Phase 20.2 — global pause / resume for job processing.\n *\n * Stored in `np_settings` under a deliberately reserved\n * `siteId=\"_system\"` key. The pause flag is process-wide\n * (a maintenance window pauses everything, not per-tenant)\n * so it sits outside the per-site settings space. Reads /\n * writes go through this module rather than `getSetting`\n * because the latter scopes by request site.\n */\nconst SYSTEM_SITE_ID = \"_system\";\nconst JOBS_PAUSED_KEY = \"jobs.paused\";\n\nexport interface NpJobsPauseState {\n paused: boolean;\n /** ISO timestamp captured the last time the flag flipped. */\n changedAt: string;\n /** User id (staff) who flipped the flag, when known. */\n changedByUserId: string | null;\n /** Optional note an operator can leave for the next person. */\n reason: string | null;\n}\n\nconst DEFAULT_STATE: NpJobsPauseState = {\n paused: false,\n changedAt: new Date(0).toISOString(),\n changedByUserId: null,\n reason: null,\n};\n\nexport async function getJobsPauseState(): Promise<NpJobsPauseState> {\n const db = getDb();\n const rows = await db\n .select()\n .from(npSettings)\n .where(and(eq(npSettings.siteId, SYSTEM_SITE_ID), eq(npSettings.key, JOBS_PAUSED_KEY)))\n .limit(1);\n\n const row = rows[0];\n if (!row) return DEFAULT_STATE;\n\n const value = row.value as Partial<NpJobsPauseState> | null;\n if (!value || typeof value.paused !== \"boolean\") return DEFAULT_STATE;\n\n return {\n paused: value.paused,\n changedAt: typeof value.changedAt === \"string\" ? value.changedAt : DEFAULT_STATE.changedAt,\n changedByUserId: typeof value.changedByUserId === \"string\" ? value.changedByUserId : null,\n reason: typeof value.reason === \"string\" ? value.reason : null,\n };\n}\n\nexport interface SetJobsPauseStateInput {\n paused: boolean;\n changedByUserId?: string | null;\n reason?: string | null;\n}\n\nexport async function setJobsPauseState(input: SetJobsPauseStateInput): Promise<NpJobsPauseState> {\n const db = getDb();\n const next: NpJobsPauseState = {\n paused: input.paused,\n changedAt: new Date().toISOString(),\n changedByUserId: input.changedByUserId ?? null,\n reason: input.reason ?? null,\n };\n\n await db\n .insert(npSettings)\n .values({\n siteId: SYSTEM_SITE_ID,\n key: JOBS_PAUSED_KEY,\n value: next,\n })\n .onConflictDoUpdate({\n target: [npSettings.siteId, npSettings.key],\n set: {\n value: next,\n updatedAt: new Date(),\n },\n });\n\n return next;\n}\n\nexport const PAUSE_SYNC_INTERVAL_MS = 30_000;\n\n/**\n * Number of consecutive failures before the loop escalates from a\n * `warn` log to the error reporter. Three ticks at 30s = ~90s of\n * sync drift before an operator gets a tracked alert; tunable here\n * if real-world fault profiles call for tighter or looser\n * thresholds.\n */\nexport const PAUSE_SYNC_ESCALATE_AFTER = 3;\n\nexport interface PauseSyncLoopHandle {\n stop(): void;\n}\n\n/**\n * Phase 20.2 — multi-pod pause sync. Each worker pod polls the\n * persisted flag on this cadence (default 30 s, matching the\n * heartbeat) and applies any state change locally. So an\n * operator pausing on pod A also stops pod B within roughly\n * one tick, instead of waiting for pod B to restart.\n *\n * Returns a handle whose `stop()` clears the interval. Read\n * errors are logged at warn — we don't want a transient DB\n * blip to wedge the worker. After\n * `PAUSE_SYNC_ESCALATE_AFTER` consecutive failures (#312),\n * the next failure is also reported via `reportError` so an\n * operator monitoring Sentry / their tracker sees the pod has\n * been silently out of sync. The counter resets on the next\n * successful tick.\n */\nexport function startPauseSyncLoop(\n queue: NpJobQueue,\n intervalMs: number = PAUSE_SYNC_INTERVAL_MS,\n): PauseSyncLoopHandle {\n const log = getLogger();\n let consecutiveFailures = 0;\n let escalated = false;\n\n const tick = async (): Promise<void> => {\n try {\n const persisted = await getJobsPauseState();\n const localPaused =\n typeof queue.isProcessingPaused === \"function\" ? queue.isProcessingPaused() : false;\n\n if (persisted.paused && !localPaused && typeof queue.pauseProcessing === \"function\") {\n await queue.pauseProcessing();\n log.info(\"Pause sync: applied paused=true from settings\", {\n changedAt: persisted.changedAt,\n });\n } else if (!persisted.paused && localPaused && typeof queue.resumeProcessing === \"function\") {\n await queue.resumeProcessing();\n log.info(\"Pause sync: applied paused=false from settings\", {\n changedAt: persisted.changedAt,\n });\n }\n\n // Successful tick — clear the run of failures so a single\n // recovery resets the escalation gate.\n if (consecutiveFailures > 0) {\n log.info(\"Pause sync: recovered after consecutive failures\", {\n previousFailures: consecutiveFailures,\n });\n consecutiveFailures = 0;\n escalated = false;\n }\n } catch (err) {\n consecutiveFailures += 1;\n const errorMessage = err instanceof Error ? err.message : String(err);\n log.warn(\"Pause sync tick failed\", {\n error: errorMessage,\n consecutiveFailures,\n });\n\n // After `PAUSE_SYNC_ESCALATE_AFTER` consecutive failures,\n // surface the error to the configured reporter so silent\n // out-of-sync state is visible to operators. Escalate\n // exactly once per failure run; the success branch above\n // resets `escalated` so a subsequent run can re-alert.\n if (consecutiveFailures >= PAUSE_SYNC_ESCALATE_AFTER && !escalated) {\n escalated = true;\n const reportable = err instanceof Error ? err : new Error(errorMessage);\n await reportError(reportable, {\n tags: { source: \"worker\", subsystem: \"pause-sync\" },\n extra: { consecutiveFailures },\n });\n }\n }\n };\n\n // Tick once immediately so a worker booted just before a\n // pause API call doesn't process up to one full interval's\n // worth of jobs before it sees the flag. Mirrors the pattern\n // in `startHeartbeatLoop`.\n void tick();\n const timer = setInterval(() => {\n void tick();\n }, intervalMs);\n if (typeof timer.unref === \"function\") timer.unref();\n\n return {\n stop() {\n clearInterval(timer);\n },\n };\n}\n","import { PgBoss, type ConstructorOptions, type Job } from \"pg-boss\";\nimport { type NpJobType } from \"../config/types.js\";\nimport { getLogger } from \"../observability/logger.js\";\nimport { reportError } from \"../observability/error-reporter.js\";\nimport { getAllJobHandlers } from \"./handlers.js\";\nimport { recordJobLog, runInJobContext } from \"./job-log.js\";\nimport {\n type NpJobCountOptions,\n type NpJobListOptions,\n type NpJobListResult,\n type NpJobQueue,\n type NpJobState,\n type NpJobStateCounts,\n type NpJobSummary,\n type NpPluginScheduleStats,\n type NpReconcileSchedulesResult,\n type NpScheduleSummary,\n} from \"./queue.js\";\n\n/**\n * pg-boss 12+ rejects queue names containing `:`, but NexPress job types use\n * `:` as a namespace separator (e.g. \"content:afterSave\"). Translate here so\n * the external API keeps its readable form while pg-boss sees an allowed\n * name.\n */\nfunction toQueueName(type: NpJobType): string {\n return type.replace(/:/g, \".\");\n}\n\nexport class PgBossAdapter implements NpJobQueue {\n private readonly boss: PgBoss;\n /**\n * Phase 20.2 — every queue we've called `boss.work()` on, plus\n * the function that re-registers it. We need both because\n * `pauseProcessing()` calls `boss.offWork(name)` (drops the\n * worker) and `resumeProcessing()` has to re-call the original\n * `boss.work(...)` to bring it back. Order is preserved so\n * resume registers in the same order as start did.\n */\n private readonly workRegistrations: Array<{\n queueName: string;\n register: () => Promise<void>;\n }> = [];\n private paused = false;\n /**\n * Flips `true` after `start()` runs (full worker mode). `startProducer()`\n * doesn't set it. Used by `reconcilePluginSchedules()` to tell admins\n * whether this process owns the `boss.work()` loops for plugin schedules\n * — the same boss instance can act as producer-only in the web server\n * and full worker in the worker process.\n */\n private workerStarted = false;\n\n constructor(connectionString: string, options?: ConstructorOptions) {\n this.boss = new PgBoss({ connectionString, ...options });\n }\n\n async enqueue(type: NpJobType, data: unknown): Promise<string> {\n const jobId = await this.boss.send(toQueueName(type), asJobPayload(data));\n\n if (!jobId) {\n throw new Error(`Failed to enqueue job: ${type}`);\n }\n\n return jobId;\n }\n\n /**\n * Opens the pg-boss connection and runs its migrations. Safe to call from a\n * non-worker process (e.g. the Next.js server) so it can enqueue jobs.\n */\n async startProducer(): Promise<void> {\n await this.boss.start();\n }\n\n /**\n * Full start: opens the connection (idempotent with startProducer) and\n * registers `boss.work()` loops for every handler in the registry. Call\n * this from the dedicated worker process.\n */\n async start(): Promise<void> {\n await this.boss.start();\n\n for (const [type, handler] of getAllJobHandlers()) {\n const queueName = toQueueName(type);\n await this.boss.createQueue(queueName);\n const register = async () => {\n await this.boss.work(queueName, async (jobs: Job<unknown>[]) => {\n for (const job of jobs) {\n // Phase 20.3 — every handler invocation runs inside an\n // AsyncLocalStorage context keyed on the pg-boss job id\n // so `recordJobLog()` calls (from the framework or\n // plugin code) get stamped automatically.\n await runInJobContext(job.id, async () => {\n try {\n await handler(job.data);\n } catch (error) {\n // Surface job failures to logs + the configured error reporter.\n // Re-throw so pg-boss applies its retry/dead-letter policy.\n const err = error instanceof Error ? error : new Error(String(error));\n getLogger().error(\"Job handler threw\", {\n type,\n jobId: job.id,\n error: err.message,\n stack: err.stack,\n });\n // Phase 20.3 — capture the failure on the job's\n // own log stream too. Operator opens the row in\n // the admin → sees the error message inline.\n await recordJobLog(\"error\", `Job handler threw: ${err.message}`, {\n type,\n stack: err.stack,\n });\n void reportError(err, {\n tags: { source: \"worker\", jobType: type },\n extra: { jobId: job.id },\n });\n throw err;\n }\n });\n }\n });\n };\n this.workRegistrations.push({ queueName, register });\n await register();\n }\n\n // Phase 19 — register one queue + worker per plugin schedule.\n // pg-boss enforces a 1:1 mapping between schedule name and\n // queue, so each `definePlugin({ scheduled })` entry needs\n // its own queue. The dispatcher inside the handler delegates\n // to the registered handler via `runPluginScheduledTask`.\n const { getRegisteredPluginSchedules, runPluginScheduledTask } =\n await import(\"../plugins/host.js\");\n for (const schedule of getRegisteredPluginSchedules()) {\n const queueName = `${toQueueName(\"plugin:scheduledTask\")}.${schedule.pluginId}.${schedule.taskId}`;\n await this.boss.createQueue(queueName);\n const register = async () => {\n await this.boss.work(queueName, async (jobs: Job<unknown>[]) => {\n for (const job of jobs) {\n await runInJobContext(job.id, async () => {\n try {\n await runPluginScheduledTask(schedule.pluginId, schedule.taskId);\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n getLogger().error(\"Plugin scheduled task threw\", {\n pluginId: schedule.pluginId,\n taskId: schedule.taskId,\n jobId: job.id,\n error: err.message,\n stack: err.stack,\n });\n await recordJobLog(\"error\", `Plugin scheduled task threw: ${err.message}`, {\n pluginId: schedule.pluginId,\n taskId: schedule.taskId,\n stack: err.stack,\n });\n void reportError(err, {\n tags: {\n source: \"worker\",\n pluginId: schedule.pluginId,\n taskId: schedule.taskId,\n },\n extra: { jobId: job.id },\n });\n throw err;\n }\n });\n }\n });\n };\n this.workRegistrations.push({ queueName, register });\n await register();\n }\n this.workerStarted = true;\n }\n\n /**\n * Phase 20.2 — drop every registered worker so the boss stops\n * claiming new jobs. The pg-boss connection stays open; the\n * producer can keep enqueueing while paused. In-flight jobs\n * picked up before pause finish normally because pg-boss only\n * cancels the polling loop, not the fetch already in flight.\n */\n async pauseProcessing(): Promise<void> {\n if (this.paused) return;\n for (const { queueName } of this.workRegistrations) {\n await this.boss.offWork(queueName);\n }\n this.paused = true;\n getLogger().info(\"Job processing paused\", {\n queues: this.workRegistrations.length,\n });\n }\n\n /** Phase 20.2 — re-run every captured `boss.work()` registration. Idempotent. */\n async resumeProcessing(): Promise<void> {\n if (!this.paused) return;\n for (const { register } of this.workRegistrations) {\n await register();\n }\n this.paused = false;\n getLogger().info(\"Job processing resumed\", {\n queues: this.workRegistrations.length,\n });\n }\n\n isProcessingPaused(): boolean {\n return this.paused;\n }\n\n /**\n * Phase 22.4 — readiness probe round-trip. `boss.isInstalled()`\n * issues a single SELECT against `pgboss.version`, so a true\n * answer proves both that the DB connection is alive AND that\n * pg-boss's schema migrations have applied. Any throw — pool\n * dead, schema missing, permissions revoked — is caught and\n * reported as `false`; the readiness probe never sees an\n * exception bubble out of the queue check.\n */\n async isHealthy(): Promise<boolean> {\n try {\n return await this.boss.isInstalled();\n } catch {\n return false;\n }\n }\n\n async stop(): Promise<void> {\n await this.boss.stop({ graceful: true, timeout: 30000 });\n }\n\n async scheduleRecurring(): Promise<void> {\n await this.boss.schedule(toQueueName(\"system:revisionPrune\"), \"0 3 * * *\", {});\n await this.boss.schedule(toQueueName(\"system:sessionCleanup\"), \"0 * * * *\", {});\n // Phase 20.3 — daily np_job_logs retention sweep at 03:30 UTC.\n // Offset 30 min from revisionPrune so the two cleanup jobs\n // don't pile DB load on the same minute.\n await this.boss.schedule(toQueueName(\"system:jobLogPrune\"), \"30 3 * * *\", {});\n // Phase 16.4 — daily digest at 08:00 UTC, weekly digest Mondays\n // 08:00 UTC. Members opt in via their notification prefs;\n // the handler short-circuits when nobody matches.\n //\n // Issue #217 — pg-boss `pgboss.schedule` rows are uniquely\n // keyed by `(name, key)`. Without an explicit `key`, both\n // calls write to `(notifications:sendDigest, '')` and the\n // weekly upsert silently overwrites the daily one. Pass\n // `{ key }` so the two cadences live in distinct rows;\n // both schedules still fire jobs into the same queue name\n // (handler discriminates by `data.cadence`). The empty-key\n // legacy row from earlier deploys is removed below so a\n // re-deploy converges on the correct shape.\n const digestQueue = toQueueName(\"notifications:sendDigest\");\n await this.boss.unschedule(digestQueue).catch(() => {\n // First-deploy systems have no row to remove. Older\n // deploys may have one; clear it so the new\n // `(name, key='daily')` and `(name, key='weekly')` rows\n // don't coexist alongside an orphan empty-key row.\n });\n await this.boss.schedule(digestQueue, \"0 8 * * *\", { cadence: \"daily\" }, { key: \"daily\" });\n await this.boss.schedule(digestQueue, \"0 8 * * 1\", { cadence: \"weekly\" }, { key: \"weekly\" });\n // Phase 19 — first-class plugin cron schedules. Each entry\n // declared via `definePlugin({ scheduled: [...] })` becomes\n // one row in `pgboss.schedule`. We share the `plugin:scheduledTask`\n // queue and dispatch by `(pluginId, taskId)` in the handler;\n // the schedule's pg-boss `name` is stable per task so a re-\n // boot doesn't accumulate duplicates.\n const { getRegisteredPluginSchedules } = await import(\"../plugins/host.js\");\n for (const schedule of getRegisteredPluginSchedules()) {\n const pgBossName = `${toQueueName(\"plugin:scheduledTask\")}.${schedule.pluginId}.${schedule.taskId}`;\n await this.boss.schedule(pgBossName, schedule.cron, {\n pluginId: schedule.pluginId,\n taskId: schedule.taskId,\n });\n }\n }\n\n getBoss(): PgBoss {\n return this.boss;\n }\n\n /**\n * Phase 13 — admin job introspection. Joins pgboss.job\n * (pending / active / retry) and pgboss.archive (completed\n * / failed / expired) into one unified list.\n *\n * Phase 13.2 — `since` filter for time-bounded queries\n * (\"last 24 hours\") and accurate `total` via a parallel\n * COUNT(*) so the admin pagination shows the right count.\n * The COUNT runs against the same UNION; the per-page\n * SELECT still gets the row data.\n */\n async listJobs(options: NpJobListOptions): Promise<NpJobListResult> {\n const limit = Math.min(Math.max(1, options.limit ?? 50), 200);\n const offset = Math.max(0, options.offset ?? 0);\n\n const db = (\n this.boss as unknown as {\n db: { executeSql: (sql: string, params?: unknown[]) => Promise<{ rows: PgBossRow[] }> };\n }\n ).db;\n const params: unknown[] = [];\n const where: string[] = [];\n if (options.name) {\n params.push(options.name);\n where.push(`name = $${params.length}`);\n }\n if (options.state) {\n params.push(options.state);\n where.push(`state = $${params.length}`);\n }\n if (options.since) {\n params.push(options.since.toISOString());\n where.push(`created_on >= $${params.length}`);\n }\n const whereSql = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n\n // Schema name defaults to `pgboss` but can be overridden\n // via constructor options. We didn't pass `schema`, so the\n // default is in effect.\n //\n // Phase 20.4 — when `options.source` is set we narrow the\n // UNION to that single table; otherwise we keep the\n // historical \"show everything\" union. The `source` column on\n // each row is what the admin uses to split live vs archive\n // visually without an extra round trip.\n const liveSelect = `\n SELECT id, name, state, data, retry_count,\n output::text AS output, created_on, started_on, completed_on,\n 'live' AS source\n FROM pgboss.job`;\n const archiveSelect = `\n SELECT id, name, state, data, retry_count,\n output::text AS output, created_on, started_on, completed_on,\n 'archive' AS source\n FROM pgboss.archive`;\n const innerUnion =\n options.source === \"live\"\n ? liveSelect\n : options.source === \"archive\"\n ? archiveSelect\n : `${liveSelect}\\n UNION ALL${archiveSelect}`;\n const listSql = `\n SELECT id, name, state::text AS state, data, retry_count,\n output, created_on, started_on, completed_on, source\n FROM (\n ${innerUnion}\n ) jobs\n ${whereSql}\n ORDER BY created_on DESC\n LIMIT ${limit} OFFSET ${offset}\n `;\n const liveCount = `SELECT id, name, state, data, created_on, 'live' AS source FROM pgboss.job`;\n const archiveCount = `SELECT id, name, state, data, created_on, 'archive' AS source FROM pgboss.archive`;\n const countUnion =\n options.source === \"live\"\n ? liveCount\n : options.source === \"archive\"\n ? archiveCount\n : `${liveCount} UNION ALL ${archiveCount}`;\n const countSql = `\n SELECT COUNT(*)::bigint AS total\n FROM (\n ${countUnion}\n ) jobs\n ${whereSql}\n `;\n const [listResult, countResult] = await Promise.all([\n db.executeSql(listSql, params),\n db.executeSql(countSql, params) as unknown as Promise<{\n rows: Array<{ total: string | number }>;\n }>,\n ]);\n const rows = listResult.rows ?? [];\n const totalRaw = countResult.rows?.[0]?.total;\n const total =\n typeof totalRaw === \"number\"\n ? totalRaw\n : typeof totalRaw === \"string\"\n ? Number.parseInt(totalRaw, 10)\n : 0;\n\n return {\n jobs: rows.map(rowToSummary),\n total: Number.isFinite(total) ? total : 0,\n };\n }\n\n /**\n * Phase 13.2 — list every cron schedule registered in the\n * queue. Reads from `pgboss.schedule`, which is the table\n * pg-boss writes to on each `boss.schedule()` call. Sorted\n * by name for stable display.\n */\n async listSchedules(): Promise<NpScheduleSummary[]> {\n const db = (\n this.boss as unknown as {\n db: {\n executeSql: (sql: string, params?: unknown[]) => Promise<{ rows: PgBossScheduleRow[] }>;\n };\n }\n ).db;\n const result = await db.executeSql(\n `SELECT name, key, cron, timezone, data, created_on, updated_on\n FROM pgboss.schedule\n ORDER BY name ASC, key ASC`,\n );\n return (result.rows ?? []).map(scheduleRowToSummary);\n }\n\n /**\n * Phase 4.2 — pulls per-(pluginId, taskId) execution stats from the\n * union of `pgboss.job` (in-flight + recently-completed) and\n * `pgboss.archive` (rolled-over history). One row per taskId so the\n * caller can index without a second pass.\n *\n * The window default is 7 days because longer windows force the\n * archive table into the hot path and admins typically want recent\n * health, not lifetime totals. Increase via `windowDays` if surfacing\n * a \"30-day reliability\" widget.\n */\n async getPluginScheduleStats(\n pluginId: string,\n options?: { windowDays?: number },\n ): Promise<NpPluginScheduleStats[]> {\n const windowDays = Math.max(1, Math.min(365, options?.windowDays ?? 7));\n const db = (\n this.boss as unknown as {\n db: {\n executeSql: (\n sql: string,\n params?: unknown[],\n ) => Promise<{\n rows: Array<{\n task_id: string | null;\n last_run: Date | string | null;\n last_success: Date | string | null;\n last_failure: Date | string | null;\n completed_count: string | number;\n failed_count: string | number;\n }>;\n }>;\n };\n }\n ).db;\n\n // Plugin schedule jobs land in pg-boss under two name shapes:\n // - `plugin.scheduledTask` — `schedulePluginTask()` enqueues\n // here for one-shot \"Run now\" invocations (handlePluginScheduledTask\n // dispatches by `(pluginId, taskId)` from the payload).\n // - `plugin.scheduledTask.<pluginId>.<taskId>` — cron schedules. Each entry\n // declared via `definePlugin({ scheduled: [...] })` gets its own queue +\n // row in `pgboss.schedule` (Phase 19).\n // Both share the `(pluginId, taskId)` payload shape, so we filter by name\n // prefix and join on `data->>'pluginId'` to collect either source. The\n // earlier `name = 'plugin.scheduledTask'` filter only matched the first\n // shape, leaving cron-scheduled stats permanently at zero.\n const result = await db.executeSql(\n `WITH plugin_jobs AS (\n SELECT state, completed_on, data\n FROM pgboss.job\n WHERE (name = 'plugin.scheduledTask' OR name LIKE 'plugin.scheduledTask.%')\n AND data->>'pluginId' = $1\n AND completed_on > NOW() - ($2 || ' days')::interval\n UNION ALL\n SELECT state, completed_on, data\n FROM pgboss.archive\n WHERE (name = 'plugin.scheduledTask' OR name LIKE 'plugin.scheduledTask.%')\n AND data->>'pluginId' = $1\n AND completed_on > NOW() - ($2 || ' days')::interval\n )\n SELECT data->>'taskId' AS task_id,\n MAX(completed_on) AS last_run,\n MAX(CASE WHEN state = 'completed' THEN completed_on END) AS last_success,\n MAX(CASE WHEN state = 'failed' THEN completed_on END) AS last_failure,\n SUM(CASE WHEN state = 'completed' THEN 1 ELSE 0 END) AS completed_count,\n SUM(CASE WHEN state = 'failed' THEN 1 ELSE 0 END) AS failed_count\n FROM plugin_jobs\n WHERE data->>'taskId' IS NOT NULL\n GROUP BY data->>'taskId'`,\n [pluginId, String(windowDays)],\n );\n\n return (result.rows ?? [])\n .filter((row): row is typeof row & { task_id: string } => typeof row.task_id === \"string\")\n .map((row) => ({\n taskId: row.task_id,\n lastRunAt: toIso(row.last_run),\n lastSuccessAt: toIso(row.last_success),\n lastFailureAt: toIso(row.last_failure),\n completedCount: Number(row.completed_count) || 0,\n failedCount: Number(row.failed_count) || 0,\n windowDays,\n }));\n }\n\n /**\n * Issue #461 — diff the in-memory plugin schedule registry against the\n * `pgboss.schedule` rows whose name starts with `plugin.scheduledTask.*`\n * and bring pg-boss in line. Without this, `reloadPlugins()` only\n * rebuilt the in-process registry and pg-boss kept firing the old set\n * of crons until the worker process restarted — the admin \"Reload all\"\n * toast was promising behavior the system didn't deliver.\n *\n * Worker `boss.work()` registrations stay untouched. In production the\n * worker is a separate process with its own boss instance; the web\n * process can't add or drop work loops there. We surface that via\n * `workerOwnsRegistrations` so the admin UI can warn the operator.\n */\n async reconcilePluginSchedules(): Promise<NpReconcileSchedulesResult> {\n // Pull the current registry inline — same dynamic-import pattern\n // `start()` uses to dodge a core ↔ jobs cycle.\n const { getRegisteredPluginSchedules } = await import(\"../plugins/host.js\");\n const wantedList = getRegisteredPluginSchedules();\n const wantedByName = new Map<\n string,\n { pluginId: string; taskId: string; cron: string }\n >();\n for (const schedule of wantedList) {\n const name = `${toQueueName(\"plugin:scheduledTask\")}.${schedule.pluginId}.${schedule.taskId}`;\n wantedByName.set(name, {\n pluginId: schedule.pluginId,\n taskId: schedule.taskId,\n cron: schedule.cron,\n });\n }\n\n // Existing schedule rows for the plugin namespace only — the framework\n // owns its built-in schedules (`system.revisionPrune` etc.) elsewhere\n // and we mustn't touch them here.\n const existingAll = await this.listSchedules();\n const existingByName = new Map<string, NpScheduleSummary>();\n for (const entry of existingAll) {\n if (entry.name.startsWith(\"plugin.scheduledTask.\")) {\n existingByName.set(entry.name, entry);\n }\n }\n\n let added = 0;\n let updated = 0;\n let removed = 0;\n\n // Add or update.\n for (const [name, want] of wantedByName) {\n const existing = existingByName.get(name);\n if (!existing) {\n await this.boss.schedule(name, want.cron, {\n pluginId: want.pluginId,\n taskId: want.taskId,\n });\n added++;\n continue;\n }\n if (existing.cron !== want.cron) {\n await this.boss.unschedule(name).catch(() => {\n // Race: another reconcile call could have removed the row in\n // parallel. Either way, the next `schedule()` below installs\n // the new cron from a clean slate.\n });\n await this.boss.schedule(name, want.cron, {\n pluginId: want.pluginId,\n taskId: want.taskId,\n });\n updated++;\n }\n }\n\n // Remove stale rows.\n for (const [name] of existingByName) {\n if (!wantedByName.has(name)) {\n await this.boss.unschedule(name).catch(() => {\n // Concurrent removal — fine; the row is gone either way.\n });\n removed++;\n }\n }\n\n return {\n added,\n updated,\n removed,\n workerOwnsRegistrations: this.workerStarted,\n };\n }\n\n /**\n * Phase 23.5 — `GROUP BY state` across the union of pgboss.job\n * (live) and pgboss.archive (rolled). Returns a fully-populated\n * record so callers can index without optional chaining.\n *\n * Uses `created_on` for the optional `since` filter. Both tables\n * carry the same column, so the union pre-filter is a single\n * predicate.\n */\n async countByState(options?: NpJobCountOptions): Promise<NpJobStateCounts> {\n const db = (\n this.boss as unknown as {\n db: {\n executeSql: (\n sql: string,\n params?: unknown[],\n ) => Promise<{ rows: Array<{ state: string; count: string | number }> }>;\n };\n }\n ).db;\n const params: unknown[] = [];\n let whereSql = \"\";\n if (options?.since) {\n params.push(options.since.toISOString());\n whereSql = `WHERE created_on >= $${params.length}`;\n }\n const result = await db.executeSql(\n `SELECT state::text AS state, COUNT(*)::bigint AS count\n FROM (\n SELECT state, created_on FROM pgboss.job\n UNION ALL\n SELECT state, created_on FROM pgboss.archive\n ) jobs\n ${whereSql}\n GROUP BY state`,\n params,\n );\n const counts: NpJobStateCounts = {\n created: 0,\n active: 0,\n completed: 0,\n failed: 0,\n retry: 0,\n cancelled: 0,\n expired: 0,\n };\n for (const row of result.rows ?? []) {\n const key = row.state as keyof NpJobStateCounts;\n if (key in counts) {\n const value =\n typeof row.count === \"number\" ? row.count : Number.parseInt(row.count, 10);\n counts[key] = Number.isFinite(value) ? value : 0;\n }\n }\n return counts;\n }\n\n async retryJob(id: string): Promise<string> {\n // Look up the original payload + queue name first so we\n // can re-enqueue with the same shape. Could be in either\n // pgboss.job (still pending/active/retry) or pgboss.archive\n // (already terminal); UNION handles both.\n const db = (\n this.boss as unknown as {\n db: { executeSql: (sql: string, params?: unknown[]) => Promise<{ rows: PgBossRow[] }> };\n }\n ).db;\n const result = await db.executeSql(\n `SELECT id, name, state::text AS state, data, retry_count,\n output::text AS output, created_on, started_on, completed_on\n FROM pgboss.job WHERE id = $1\n UNION ALL\n SELECT id, name, state, data, retry_count,\n output::text AS output, created_on, started_on, completed_on\n FROM pgboss.archive WHERE id = $1\n LIMIT 1`,\n [id],\n );\n const row = result.rows?.[0];\n if (!row) {\n throw new Error(`Job ${id} not found`);\n }\n const newId = await this.boss.send(row.name, row.data ?? {});\n if (!newId) {\n throw new Error(`Failed to re-enqueue ${row.name}`);\n }\n return newId;\n }\n\n async cancelJob(id: string): Promise<void> {\n // pg-boss's cancel API requires the queue name; look it up\n // from pgboss.job. Already-archived (terminal) jobs can't\n // be cancelled, which matches user intuition.\n const db = (\n this.boss as unknown as {\n db: {\n executeSql: (sql: string, params?: unknown[]) => Promise<{ rows: { name: string }[] }>;\n };\n }\n ).db;\n const result = await db.executeSql(`SELECT name FROM pgboss.job WHERE id = $1`, [id]);\n const row = result.rows?.[0];\n if (!row) {\n throw new Error(`Job ${id} not found or already terminal`);\n }\n await this.boss.cancel(row.name, id);\n }\n}\n\ninterface PgBossRow {\n id: string;\n name: string;\n state: string;\n data: unknown;\n retry_count?: number;\n output?: string | null;\n created_on?: Date | string | null;\n started_on?: Date | string | null;\n completed_on?: Date | string | null;\n /** Phase 20.4 — `live` (pgboss.job) or `archive` (pgboss.archive). */\n source?: string;\n}\n\ninterface PgBossScheduleRow {\n name: string;\n key?: string | null;\n cron: string;\n timezone?: string | null;\n data?: unknown;\n created_on?: Date | string | null;\n updated_on?: Date | string | null;\n}\n\nfunction scheduleRowToSummary(row: PgBossScheduleRow): NpScheduleSummary {\n return {\n name: row.name,\n key: row.key ?? \"\",\n cron: row.cron,\n timezone: row.timezone ?? null,\n data: row.data ?? null,\n createdOn: toIso(row.created_on) ?? new Date(0).toISOString(),\n updatedOn: toIso(row.updated_on),\n };\n}\n\nfunction rowToSummary(row: PgBossRow): NpJobSummary {\n return {\n id: row.id,\n name: row.name,\n state: row.state as NpJobState,\n data: row.data,\n retryCount: typeof row.retry_count === \"number\" ? row.retry_count : undefined,\n output: row.output ?? null,\n createdOn: toIso(row.created_on) ?? new Date(0).toISOString(),\n startedOn: toIso(row.started_on),\n completedOn: toIso(row.completed_on),\n source: row.source === \"archive\" ? \"archive\" : row.source === \"live\" ? \"live\" : undefined,\n };\n}\n\nfunction toIso(value: Date | string | null | undefined): string | null {\n if (!value) return null;\n if (value instanceof Date) return value.toISOString();\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();\n}\n\nfunction asJobPayload(data: unknown): object {\n if (!data || typeof data !== \"object\" || Array.isArray(data)) {\n return { payload: data };\n }\n\n return data;\n}\n","import { registerBuiltinHandlers } from \"./builtin-handlers.js\";\nimport { startHeartbeatLoop } from \"./heartbeat.js\";\nimport { getJobsPauseState, startPauseSyncLoop, type PauseSyncLoopHandle } from \"./pause-state.js\";\nimport { PgBossAdapter } from \"./pg-boss-adapter.js\";\nimport { setJobQueue } from \"./queue.js\";\nimport { getLogger } from \"../observability/logger.js\";\n\nlet workerAdapter: PgBossAdapter | null = null;\nlet producerAdapter: PgBossAdapter | null = null;\nlet heartbeatHandle: { stop(): Promise<void> } | null = null;\nlet pauseSyncHandle: PauseSyncLoopHandle | null = null;\nlet signalHandlersInstalled = false;\nconst installedSignalHandlers = new Map<NodeJS.Signals, () => void>();\n\nfunction installShutdownSignalHandlers(): void {\n if (signalHandlersInstalled) return;\n signalHandlersInstalled = true;\n\n // Ensure the heartbeat row flips to `stopped` synchronously\n // before the process exits, even on signal-driven shutdown.\n // Without this a SIGTERM-driven shutdown raced with the\n // event-loop stopping and the row drifted into `unhealthy`\n // for a full WORKER_STALE_THRESHOLD_MS window.\n for (const signal of [\"SIGINT\", \"SIGTERM\"] as const) {\n const handler = (): void => {\n void (async () => {\n try {\n await stopWorker();\n } catch (err) {\n getLogger().warn(\"Worker shutdown handler failed\", {\n signal,\n error: err instanceof Error ? err.message : String(err),\n });\n } finally {\n process.exit(0);\n }\n })();\n };\n process.on(signal, handler);\n installedSignalHandlers.set(signal, handler);\n }\n}\n\nfunction removeShutdownSignalHandlers(): void {\n if (!signalHandlersInstalled) return;\n for (const [signal, handler] of installedSignalHandlers) {\n process.off(signal, handler);\n }\n installedSignalHandlers.clear();\n signalHandlersInstalled = false;\n}\n\nexport async function startWorker(\n connectionString: string,\n options?: {\n schema?: string;\n heartbeat?: boolean | { meta?: Record<string, unknown> };\n /**\n * When true (default), startWorker installs SIGINT / SIGTERM\n * listeners that call `stopWorker()` and `process.exit(0)`.\n * Set false in environments that manage their own shutdown\n * sequencing (custom supervisors, embedded test harnesses).\n */\n installSignalHandlers?: boolean;\n },\n): Promise<void> {\n if (workerAdapter) {\n return;\n }\n\n registerBuiltinHandlers();\n\n workerAdapter = new PgBossAdapter(connectionString, {\n schema: options?.schema ?? \"public\",\n });\n\n setJobQueue(workerAdapter);\n\n // #318 — wrap the post-init steps so a throw partway through\n // doesn't strand a half-set-up worker (signal handlers armed\n // on null state, dangling heartbeat row, etc). The catch\n // unwinds whatever did succeed before re-throwing, so the\n // orchestrator sees the original error and a subsequent\n // `startWorker()` retry starts from a clean slate.\n try {\n await workerAdapter.start();\n await workerAdapter.scheduleRecurring();\n\n // Phase 20.2 — if the operator paused processing while the\n // worker was offline, honor it on boot. The flag is global\n // (in `np_settings` siteId=\"_system\") so it survives worker\n // restarts. We swallow read errors because a pre-migrate DB\n // would otherwise stop the worker from starting at all —\n // safer to default to \"running\" than to refuse to boot.\n try {\n const pauseState = await getJobsPauseState();\n if (pauseState.paused) {\n await workerAdapter.pauseProcessing();\n getLogger().info(\"Worker booted in paused state\", {\n changedAt: pauseState.changedAt,\n reason: pauseState.reason,\n });\n }\n } catch (err) {\n getLogger().warn(\"Could not read jobs pause state on worker boot\", {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n\n // Phase 19 — start the heartbeat loop AFTER pg-boss is up so\n // a misconfigured DB surfaces as a `boss.start()` throw\n // rather than a heartbeat row that lies about a worker that\n // never really booted. Tests can pass `heartbeat: false` to\n // skip the recurring interval.\n const heartbeatOpt = options?.heartbeat ?? true;\n if (heartbeatOpt !== false) {\n const meta = typeof heartbeatOpt === \"object\" ? (heartbeatOpt.meta ?? {}) : {};\n heartbeatHandle = startHeartbeatLoop(meta);\n // Phase 20.2 — multi-pod pause sync. Each tick reads the\n // persisted flag and applies any divergence to the local\n // adapter, so an operator pause on one pod propagates to\n // the rest within ~30 s. Same gate as the heartbeat — if\n // the operator opted out of background loops (tests), we\n // skip this too.\n pauseSyncHandle = startPauseSyncLoop(workerAdapter);\n }\n\n // Install signal handlers LAST — if any earlier step threw,\n // the catch below has already unwound; we don't want to arm\n // shutdown handlers that would then fire against null state.\n if (options?.installSignalHandlers !== false) {\n installShutdownSignalHandlers();\n }\n } catch (err) {\n // Best-effort cleanup of whatever did succeed. Each step is\n // its own try so one failure here doesn't mask the original.\n if (heartbeatHandle) {\n try {\n await heartbeatHandle.stop();\n } catch {\n /* swallow — original error matters more */\n }\n heartbeatHandle = null;\n }\n if (pauseSyncHandle) {\n try {\n pauseSyncHandle.stop();\n } catch {\n /* swallow */\n }\n pauseSyncHandle = null;\n }\n if (workerAdapter) {\n try {\n await workerAdapter.stop();\n } catch {\n /* swallow */\n }\n workerAdapter = null;\n }\n removeShutdownSignalHandlers();\n throw err;\n }\n}\n\n/**\n * Enqueue-only setup for the web/API process. Wires pg-boss as the job queue\n * singleton without attaching any `boss.work()` loops — those belong in the\n * dedicated worker process. Calling `enqueueJob` after this will actually\n * send jobs instead of no-op'ing.\n */\nexport async function startProducer(\n connectionString: string,\n options?: { schema?: string },\n): Promise<void> {\n if (producerAdapter) {\n return;\n }\n\n producerAdapter = new PgBossAdapter(connectionString, {\n schema: options?.schema ?? \"public\",\n });\n\n setJobQueue(producerAdapter);\n\n await producerAdapter.startProducer();\n}\n\nexport async function stopWorker(): Promise<void> {\n if (!workerAdapter) {\n return;\n }\n\n // Phase 19 — stop the heartbeat first so the row flips to\n // `stopped` while the DB is still reachable. The pg-boss\n // shutdown then clears the queue lock cleanly.\n if (heartbeatHandle) {\n await heartbeatHandle.stop();\n heartbeatHandle = null;\n }\n\n // Phase 20.2 — pause sync interval is just an in-memory\n // timer; clear it before tearing down the queue so a\n // late-firing tick doesn't try to call pauseProcessing on a\n // half-shut-down adapter.\n if (pauseSyncHandle) {\n pauseSyncHandle.stop();\n pauseSyncHandle = null;\n }\n\n await workerAdapter.stop();\n workerAdapter = null;\n removeShutdownSignalHandlers();\n}\n\nexport async function stopProducer(): Promise<void> {\n if (!producerAdapter) {\n return;\n }\n\n await producerAdapter.stop();\n producerAdapter = null;\n}\n"],"mappings":"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","names":["pruneJobLogsOlderThan","DEFAULT_JOB_LOG_RETENTION_MS","eq","eq"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
recordAuditEvent
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-5C22NDW4.js";
|
|
4
4
|
import {
|
|
5
5
|
getCommunitySettings
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-6MRTH734.js";
|
|
7
7
|
import {
|
|
8
8
|
NpAuthError,
|
|
9
9
|
NpForbiddenError,
|
|
@@ -855,4 +855,4 @@ export {
|
|
|
855
855
|
requestMemberPasswordReset,
|
|
856
856
|
consumeMemberPasswordReset
|
|
857
857
|
};
|
|
858
|
-
//# sourceMappingURL=chunk-
|
|
858
|
+
//# sourceMappingURL=chunk-TIWJVQOO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/access.ts","../src/auth/token.ts","../src/auth/users.ts","../src/auth/password.ts","../src/auth/csrf.ts","../src/auth/oauth-providers.ts","../src/auth/oauth-resolve.ts","../src/auth/oauth-resolve-member.ts","../src/auth/oauth-state.ts","../src/auth/oauth-arctic.ts","../src/auth/session.ts","../src/auth/identities-admin.ts","../src/auth/reset-token.ts","../src/auth/member-token.ts","../src/auth/member-session.ts","../src/auth/member-credentials.ts"],"sourcesContent":["import { type NpAccessFunction } from \"./types.js\";\n\nexport const authenticated: NpAccessFunction = ({ user }) => !!user;\n\nexport const isAdmin: NpAccessFunction = ({ user }) => user?.role === \"admin\";\n\nexport const isEditorOrAbove: NpAccessFunction = ({ user }) =>\n !!user && (user.role === \"admin\" || user.role === \"editor\");\n\nexport const isOwnerOrAdmin: NpAccessFunction = ({ user, doc }) =>\n user?.role === \"admin\" || doc?.createdBy === user?.id;\n","import { randomBytes } from \"node:crypto\";\nimport { jwtVerify, SignJWT, errors as joseErrors, type JWTPayload } from \"jose\";\n\nimport type { NpUserRole } from \"../config/types.js\";\nimport { NpAuthError } from \"../errors.js\";\n\n/**\n * Staff-side JWT helpers. Both access (`np-session`) and refresh\n * (`np-refresh`) cookies are signed with this module; the\n * `use: \"access\" | \"refresh\"` claim separates them so a stolen\n * refresh JWT cannot be replayed as a session cookie. Without this\n * separation a leaked 7-day refresh became a 7-day admin bearer\n * because both cookies decoded to the same `{ sub, role, ver }`\n * payload through `verifyToken` (#94).\n *\n * The fix mirrors the member-side fix from #92/#93: the `use` claim\n * is required, no legacy fallback for tokens missing the claim. The\n * cost is one forced re-login for staff sessions issued before the\n * deploy; bounded by the 7-day refresh TTL.\n */\nexport type NpTokenUse = \"access\" | \"refresh\";\n\nexport interface NpTokenPayload {\n sub: string;\n role: NpUserRole;\n ver: number;\n /** Required. `verifyToken` refuses tokens missing this claim so\n * legacy refresh JWTs cannot be smuggled into the session\n * cookie path. */\n use: NpTokenUse;\n /** Random per-token id — needed if rotation lands on the staff\n * side (mirrors the member-side `jti` for #45). Optional today\n * but populated on every newly-minted token. */\n jti?: string;\n iat: number;\n exp: number;\n}\n\nconst textEncoder = new TextEncoder();\n\nexport async function signToken(\n user: { id: string; role: NpUserRole; tokenVersion: number },\n secret: string,\n expirationSeconds: number = 7200,\n tokenUse: NpTokenUse = \"access\",\n): Promise<string> {\n const secretKey = textEncoder.encode(secret);\n\n return new SignJWT({\n sub: user.id,\n role: user.role,\n ver: user.tokenVersion,\n use: tokenUse,\n })\n .setProtectedHeader({ alg: \"HS256\" })\n .setJti(randomBytes(16).toString(\"base64url\"))\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + expirationSeconds)\n .sign(secretKey);\n}\n\n/**\n * Verify a staff JWT. When `expectedUse` is provided, refuses tokens\n * whose `use` claim doesn't match — that's how `getSessionUser`\n * rejects a refresh token used as a session cookie and how the\n * refresh route rejects an access token as a refresh trigger.\n *\n * Tokens minted before the `use` claim landed have NO `use` payload\n * field. We refuse those outright rather than treating them as\n * `access` — the prior fallback would let still-live legacy refresh\n * JWTs be smuggled into the session cookie and pass the access\n * check. Cost: staff logged in before this deploy must log in once.\n * Bounded by the refresh-token TTL (default 7 days).\n */\nexport async function verifyToken(\n token: string,\n secret: string,\n expectedUse?: NpTokenUse,\n): Promise<NpTokenPayload> {\n const secretKey = textEncoder.encode(secret);\n const { payload } = await jwtVerify(token, secretKey);\n const typed = payload as JWTPayload & {\n sub: string;\n role: NpUserRole;\n ver: number;\n iat: number;\n exp: number;\n use?: NpTokenUse;\n };\n if (typed.use !== \"access\" && typed.use !== \"refresh\") {\n throw new NpAuthError(\"Staff token missing `use` claim\");\n }\n const use: NpTokenUse = typed.use;\n if (expectedUse && use !== expectedUse) {\n throw new NpAuthError(\n `Staff token use mismatch: expected ${expectedUse}, got ${use}`,\n );\n }\n return { ...typed, use };\n}\n\n/**\n * True when `err` represents a token-verification failure rather than\n * an unrelated runtime fault (DB outage, misconfiguration, …). Auth\n * helpers use this to keep the existing \"bad token → 401\" behavior\n * silent while letting infrastructure failures surface as 5xx.\n *\n * Covers:\n * - `NpAuthError` — `verifyToken` / `verifyMemberToken` rejecting a\n * missing or wrong `use` claim, or `verifyCsrf` failing.\n * - `jose.errors.JOSEError` — every JWT signature / format /\n * expiration failure, including subclasses like `JWTExpired`,\n * `JWSSignatureVerificationFailed`, `JWTInvalid`.\n */\nexport function isTokenVerificationError(err: unknown): boolean {\n if (err instanceof NpAuthError) return true;\n if (err instanceof joseErrors.JOSEError) return true;\n return false;\n}\n","import { eq } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npUsers } from \"../db/schema/system.js\";\n\n/**\n * Minimal public projection of a user row — `id` + `name` + `email`.\n * Themes / plugins reach for this when they need to display a byline\n * (post.author → user) without pulling in session machinery. Password\n * hash + tokenVersion + reset state stay private to the auth module.\n */\nexport interface NpUserBasic {\n id: string;\n name: string;\n email: string;\n}\n\n/**\n * Look up a user by id. Returns `null` when the id doesn't exist\n * (caller handles missing-author UI). UUID validation lives at the\n * caller — Postgres rejects malformed ids inside `eq()` and the\n * surfacing error is already informative.\n *\n * This is the supported entry point for theme code that needs to\n * render a byline from `posts.author: relationTo(\"users\")`. Direct\n * drizzle reads against `np_users` are private to the framework.\n */\nexport async function getUserById(id: string): Promise<NpUserBasic | null> {\n const db = getDb();\n const [user] = await db\n .select({\n id: npUsers.id,\n name: npUsers.name,\n email: npUsers.email,\n })\n .from(npUsers)\n .where(eq(npUsers.id, id))\n .limit(1);\n return user ?? null;\n}\n","import { hash, verify, type Options } from \"@node-rs/argon2\";\n\nexport const ARGON2_OPTIONS: Options = {\n memoryCost: 19456,\n timeCost: 2,\n outputLen: 32,\n parallelism: 1,\n};\n\n// Test-only weak params — drops a hash from ~75ms to <1ms. Only kicks in\n// when NP_TEST_FAST_HASH=1 is explicitly set (vitest's setup-env.ts does\n// this) so production / dev never see weakened security.\nconst TEST_ARGON2_OPTIONS: Options = {\n memoryCost: 8,\n timeCost: 1,\n outputLen: 32,\n parallelism: 1,\n};\n\nexport function hashPassword(password: string): Promise<string> {\n return hash(\n password,\n process.env.NP_TEST_FAST_HASH === \"1\" ? TEST_ARGON2_OPTIONS : ARGON2_OPTIONS,\n );\n}\n\nexport function verifyPassword(\n passwordHash: string,\n password: string,\n): Promise<boolean> {\n return verify(passwordHash, password);\n}\n","const SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\nexport function verifyCsrf(\n method: string,\n cookieToken: string | undefined,\n headerToken: string | undefined,\n): boolean {\n if (SAFE_METHODS.has(method.toUpperCase())) {\n return true;\n }\n\n return Boolean(cookieToken && headerToken && cookieToken === headerToken);\n}\n","/**\n * OAuth provider registry — extension point for SSO. A provider plugin\n * (e.g. `@nexpress/plugin-oauth-github`) registers itself at startup\n * via `registerOAuthProvider()`; the framework's `/api/auth/oauth/{id}`\n * routes look it up by id.\n *\n * The provider is responsible for:\n * - Building the authorize URL (`authorize`).\n * - Exchanging the callback code for a normalized profile (`exchange`).\n *\n * The framework owns state-cookie signing, identity ↔ user resolution,\n * session minting, and audit. Providers must NOT touch cookies, the DB,\n * or response objects directly.\n */\n\n/**\n * Profile returned from a successful `exchange()`. The framework uses\n * `providerUserId` as the durable identifier — `email` may change at the\n * provider but `providerUserId` should not. If the provider doesn't\n * surface `email`, the framework falls back to creating a synthetic\n * placeholder (`<providerUserId>@<provider>.oauth.local`) so the\n * `np_users.email NOT NULL UNIQUE` constraint is still satisfied.\n */\nexport interface OAuthProfile {\n /** Stable per-user id from the provider. Required. */\n providerUserId: string;\n /** Optional — falls back to synthetic if missing. */\n email?: string | null;\n /** Optional — defaults to email local-part on user creation. */\n name?: string | null;\n /** Optional — written into `np_user_oauth_identities.metadata`. */\n avatarUrl?: string | null;\n /** Optional — full payload the provider wants to remember (e.g. scopes). */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Inputs the provider receives at the two callback boundaries. The\n * framework picks `redirectUri` from `SITE_URL` (or the request origin\n * in dev) so the provider doesn't have to know its own deployment URL.\n */\nexport interface OAuthAuthorizeParams {\n state: string;\n redirectUri: string;\n /**\n * PKCE code verifier (32+ char URL-safe random). The framework\n * generates one for every login and threads it through the state\n * cookie. Providers that don't support PKCE (e.g. GitHub) ignore it;\n * providers that require it (e.g. Google) hash it into the\n * `code_challenge` query param.\n */\n codeVerifier: string;\n}\n\nexport interface OAuthExchangeParams {\n code: string;\n state: string;\n redirectUri: string;\n /** Same verifier minted at /start, recovered from the state cookie. */\n codeVerifier: string;\n}\n\nexport interface OAuthProvider {\n /** Stable id used in route paths and `np_user_oauth_identities.provider`. */\n id: string;\n /** Human-readable label for admin UI / login buttons. */\n label?: string;\n /**\n * Returns a fully-qualified URL the framework should redirect the\n * browser to. Async to allow providers that need to mint per-request\n * client credentials.\n */\n authorize(params: OAuthAuthorizeParams): Promise<string> | string;\n /**\n * Validates the callback and returns the normalized profile.\n * Throwing here aborts the login with `OAUTH_EXCHANGE_FAILED`.\n */\n exchange(params: OAuthExchangeParams): Promise<OAuthProfile>;\n}\n\nconst providers = new Map<string, OAuthProvider>();\n\n/**\n * Register a provider. Idempotent: re-registering with the same id\n * overwrites — useful in dev when a plugin's `setup()` runs again on\n * reload.\n */\nexport function registerOAuthProvider(provider: OAuthProvider): void {\n if (!provider.id || typeof provider.id !== \"string\") {\n throw new Error(\"OAuth provider must have a non-empty string id\");\n }\n if (typeof provider.authorize !== \"function\" || typeof provider.exchange !== \"function\") {\n throw new Error(\n `OAuth provider \"${provider.id}\" must implement authorize() and exchange()`,\n );\n }\n providers.set(provider.id, provider);\n}\n\nexport function getOAuthProvider(id: string): OAuthProvider | undefined {\n return providers.get(id);\n}\n\nexport function listOAuthProviders(): OAuthProvider[] {\n return Array.from(providers.values());\n}\n\n/** Reset the registry — tests use this between cases. Not for runtime use. */\nexport function resetOAuthProviders(): void {\n providers.clear();\n}\n","import { eq, and, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { npUserOAuthIdentities, npUsers } from \"../db/schema/system.js\";\nimport type { NpUserRole } from \"../config/types.js\";\n\nimport { hashPassword } from \"./password.js\";\nimport type { OAuthProfile } from \"./oauth-providers.js\";\n\n/**\n * Resolves an `OAuthProfile` to a real `np_users` row, in this order:\n *\n * 1. Lookup by `(provider, provider_user_id)` — the durable link. This\n * is the only path that survives an email change at the provider.\n * 2. Email-match — if the provider gave us an email and an existing\n * user has it, link the OAuth identity to that user. Lets a staff\n * member who originally signed up with a password later \"sign in\n * with Google\" and have it just work, without an explicit linking\n * UI.\n * 3. Create — auto-provision a new user with the provider's profile,\n * default role `viewer`. The password column is filled with an\n * unrecoverable Argon2 hash of a random secret so the column\n * constraints are satisfied; the user can later run the\n * forgot-password flow to set a real password if they want one.\n *\n * Side effects: writes a row into `np_user_oauth_identities` for paths\n * 2 and 3, updates `metadata` for path 1.\n */\nexport interface ResolveOAuthLoginResult {\n user: ResolvedOAuthUser;\n /** Tells the caller whether this login created the underlying user. */\n created: boolean;\n /** Tells the caller whether this login linked a new identity row. */\n linked: boolean;\n}\n\nexport interface ResolvedOAuthUser {\n id: string;\n email: string;\n name: string;\n role: NpUserRole;\n tokenVersion: number;\n}\n\nexport interface ResolveOAuthLoginInput {\n provider: string;\n profile: OAuthProfile;\n /** Default role for auto-created users. Defaults to `\"viewer\"`. */\n defaultRole?: NpUserRole;\n}\n\nconst SYNTHETIC_EMAIL_SUFFIX = \".oauth.local\";\n\nfunction syntheticEmail(provider: string, providerUserId: string): string {\n // Stable, namespaced, doesn't collide with real provider domains.\n return `${providerUserId}@${provider}${SYNTHETIC_EMAIL_SUFFIX}`;\n}\n\nfunction deriveName(profile: OAuthProfile, fallbackEmail: string): string {\n if (profile.name && profile.name.trim().length > 0) return profile.name.trim();\n const localPart = fallbackEmail.split(\"@\")[0];\n return localPart && localPart.length > 0 ? localPart : \"Member\";\n}\n\nexport async function resolveOAuthLogin(\n input: ResolveOAuthLoginInput,\n): Promise<ResolveOAuthLoginResult> {\n const db = getDb();\n const provider = input.provider;\n const profile = input.profile;\n const role: NpUserRole = input.defaultRole ?? \"viewer\";\n\n // Step 1: lookup by durable provider link.\n const [existingLink] = (await db\n .select({\n userId: npUserOAuthIdentities.userId,\n identityId: npUserOAuthIdentities.id,\n })\n .from(npUserOAuthIdentities)\n .where(\n and(\n eq(npUserOAuthIdentities.provider, provider),\n eq(npUserOAuthIdentities.providerUserId, profile.providerUserId),\n ),\n )\n .limit(1)) as Array<{ userId: string; identityId: string }>;\n\n if (existingLink) {\n // Refresh metadata so the most recent provider info is captured.\n const metadata = mergeMetadata(profile);\n await db\n .update(npUserOAuthIdentities)\n .set({ metadata, updatedAt: new Date() })\n .where(eq(npUserOAuthIdentities.id, existingLink.identityId));\n\n const user = await loadUser(db, existingLink.userId);\n return { user, created: false, linked: false };\n }\n\n // Step 2: email match. Skipped when the provider doesn't surface an\n // email — we can't risk linking by guesswork.\n if (profile.email) {\n const normalizedEmail = profile.email.trim().toLowerCase();\n const [existingUser] = (await db\n .select({\n id: npUsers.id,\n email: npUsers.email,\n name: npUsers.name,\n role: npUsers.role,\n tokenVersion: npUsers.tokenVersion,\n })\n .from(npUsers)\n .where(eq(sql`lower(${npUsers.email})`, normalizedEmail))\n .limit(1)) as ResolvedOAuthUser[];\n\n if (existingUser) {\n await db.insert(npUserOAuthIdentities).values({\n userId: existingUser.id,\n provider,\n providerUserId: profile.providerUserId,\n metadata: mergeMetadata(profile),\n });\n return { user: existingUser, created: false, linked: true };\n }\n }\n\n // Step 3: auto-provision.\n const email =\n profile.email && profile.email.trim().length > 0\n ? profile.email.trim().toLowerCase()\n : syntheticEmail(provider, profile.providerUserId);\n const name = deriveName(profile, email);\n const placeholderPassword = await hashPassword(\n crypto.randomUUID() + crypto.randomUUID(),\n );\n\n const [created] = (await db\n .insert(npUsers)\n .values({\n email,\n name,\n password: placeholderPassword,\n role,\n })\n .returning({\n id: npUsers.id,\n email: npUsers.email,\n name: npUsers.name,\n role: npUsers.role,\n tokenVersion: npUsers.tokenVersion,\n })) as ResolvedOAuthUser[];\n\n await db.insert(npUserOAuthIdentities).values({\n userId: created.id,\n provider,\n providerUserId: profile.providerUserId,\n metadata: mergeMetadata(profile),\n });\n\n return { user: created, created: true, linked: true };\n}\n\nfunction mergeMetadata(profile: OAuthProfile): Record<string, unknown> {\n const base: Record<string, unknown> = {};\n if (profile.avatarUrl) base.avatarUrl = profile.avatarUrl;\n if (profile.email) base.email = profile.email;\n if (profile.name) base.name = profile.name;\n if (profile.metadata) Object.assign(base, profile.metadata);\n return base;\n}\n\nasync function loadUser(\n db: NodePgDatabase<Record<string, unknown>>,\n userId: string,\n): Promise<ResolvedOAuthUser> {\n const [row] = (await db\n .select({\n id: npUsers.id,\n email: npUsers.email,\n name: npUsers.name,\n role: npUsers.role,\n tokenVersion: npUsers.tokenVersion,\n })\n .from(npUsers)\n .where(eq(npUsers.id, userId))\n .limit(1)) as ResolvedOAuthUser[];\n if (!row) {\n throw new Error(`User ${userId} referenced by oauth identity is missing`);\n }\n return row;\n}\n","import { and, eq, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { getCommunitySettings } from \"../community/settings.js\";\nimport { npMemberIdentities, npMembers } from \"../db/schema/community.js\";\nimport { NpForbiddenError } from \"../errors.js\";\n\nimport { hashPassword } from \"./password.js\";\nimport type { OAuthProfile } from \"./oauth-providers.js\";\n\n/**\n * Member-side mirror of `resolveOAuthLogin` (the staff resolver in\n * `oauth-resolve.ts`). Walks the same three-step ladder:\n *\n * 1. Lookup by `(provider, subject)` in `np_member_identities` —\n * durable provider link.\n * 2. Email match — if the profile carries an email, link the\n * identity to the existing `np_members` row.\n * 3. Auto-provision a new member with status=`active`, default\n * password = unrecoverable Argon2 of a random secret. The user\n * can later run forgot-password to set a real password if they\n * want one (or stay SSO-only).\n *\n * Members are kept distinct from staff users at every layer\n * (different table, different cookies, different audience claim on\n * the JWT). This resolver intentionally never touches `np_users`.\n */\nexport interface ResolvedOAuthMember {\n id: string;\n email: string;\n handle: string;\n displayName: string;\n status: \"active\" | \"pending\" | \"suspended\" | \"deleted\";\n tokenVersion: number;\n}\n\nexport interface ResolveMemberOAuthLoginInput {\n provider: string;\n profile: OAuthProfile;\n}\n\nexport interface ResolveMemberOAuthLoginResult {\n member: ResolvedOAuthMember;\n /** True when this login auto-provisioned the underlying member. */\n created: boolean;\n /** True when this login linked a new identity row (covers steps 2 + 3). */\n linked: boolean;\n}\n\nconst SYNTHETIC_EMAIL_SUFFIX = \".oauth.local\";\nconst HANDLE_FALLBACK = \"user\";\nconst HANDLE_RANDOM_SUFFIX_BYTES = 4;\n\nfunction syntheticEmail(provider: string, providerUserId: string): string {\n return `${providerUserId}@${provider}${SYNTHETIC_EMAIL_SUFFIX}`;\n}\n\n/**\n * Members have a unique `handle` field. Build a candidate from the\n * provider's profile, sanitize to the project's handle regex, and add\n * a short random suffix to dodge collisions on common values like\n * \"alice\" / \"octocat\".\n *\n * Handle regex (per `register/route.ts`):\n * /^[a-z0-9][a-z0-9_-]{2,29}$/\n */\nfunction generateHandle(profile: OAuthProfile, fallbackEmail: string): string {\n const seed =\n (profile.metadata && typeof profile.metadata.login === \"string\" && profile.metadata.login) ||\n profile.name ||\n fallbackEmail.split(\"@\")[0] ||\n HANDLE_FALLBACK;\n const sanitized = String(seed)\n .toLowerCase()\n .replace(/[^a-z0-9_-]/g, \"-\")\n .replace(/^[-_]+/, \"\")\n .slice(0, 20);\n const base = sanitized.length >= 3 ? sanitized : HANDLE_FALLBACK;\n // Random suffix keeps handles unique across the OAuth user pool —\n // accept the cost of \"alice-9k2x\" rather than fighting a tight loop\n // of insert-and-retry on every collision.\n const suffix = Math.random()\n .toString(36)\n .slice(2, 2 + HANDLE_RANDOM_SUFFIX_BYTES);\n return `${base}-${suffix}`.slice(0, 30);\n}\n\nfunction deriveDisplayName(profile: OAuthProfile, fallbackEmail: string): string {\n if (profile.name && profile.name.trim().length > 0) return profile.name.trim();\n const localPart = fallbackEmail.split(\"@\")[0];\n return localPart && localPart.length > 0 ? localPart : \"Member\";\n}\n\nfunction mergeMetadata(profile: OAuthProfile): Record<string, unknown> {\n const base: Record<string, unknown> = {};\n if (profile.avatarUrl) base.avatarUrl = profile.avatarUrl;\n if (profile.email) base.email = profile.email;\n if (profile.name) base.name = profile.name;\n if (profile.metadata) Object.assign(base, profile.metadata);\n return base;\n}\n\nasync function loadMember(\n db: NodePgDatabase<Record<string, unknown>>,\n memberId: string,\n): Promise<ResolvedOAuthMember> {\n const [row] = (await db\n .select({\n id: npMembers.id,\n email: npMembers.email,\n handle: npMembers.handle,\n displayName: npMembers.displayName,\n status: npMembers.status,\n tokenVersion: npMembers.tokenVersion,\n })\n .from(npMembers)\n .where(eq(npMembers.id, memberId))\n .limit(1)) as ResolvedOAuthMember[];\n if (!row) {\n throw new Error(`Member ${memberId} referenced by oauth identity is missing`);\n }\n return row;\n}\n\nexport async function resolveMemberOAuthLogin(\n input: ResolveMemberOAuthLoginInput,\n): Promise<ResolveMemberOAuthLoginResult> {\n const db = getDb();\n const { provider, profile } = input;\n\n // Step 1: durable lookup.\n const [existingLink] = (await db\n .select({ memberId: npMemberIdentities.memberId, identityId: npMemberIdentities.id })\n .from(npMemberIdentities)\n .where(\n and(\n eq(npMemberIdentities.provider, provider),\n eq(npMemberIdentities.subject, profile.providerUserId),\n ),\n )\n .limit(1)) as Array<{ memberId: string; identityId: string }>;\n\n if (existingLink) {\n await db\n .update(npMemberIdentities)\n .set({ metadata: mergeMetadata(profile), updatedAt: new Date() })\n .where(eq(npMemberIdentities.id, existingLink.identityId));\n const member = await loadMember(db, existingLink.memberId);\n return { member, created: false, linked: false };\n }\n\n // Step 2: email match.\n if (profile.email) {\n const normalizedEmail = profile.email.trim().toLowerCase();\n const [existingMember] = (await db\n .select({\n id: npMembers.id,\n email: npMembers.email,\n handle: npMembers.handle,\n displayName: npMembers.displayName,\n status: npMembers.status,\n tokenVersion: npMembers.tokenVersion,\n })\n .from(npMembers)\n .where(eq(sql`lower(${npMembers.email})`, normalizedEmail))\n .limit(1)) as ResolvedOAuthMember[];\n\n if (existingMember) {\n // Refuse to auto-link an OAuth identity to a non-active member.\n // Without this guard an attacker who controls an OAuth account\n // with a victim's email could pre-link an identity to the\n // victim's pending (unverified) row; once the victim later\n // activates, the attacker's identity is already attached and\n // they can sign in as the victim. The callback would still\n // refuse the immediate login (status check below), but the\n // dangling link would persist.\n //\n // Active members are the only ones we'll cross-link\n // automatically — pending / suspended / deleted are returned\n // as-is and the route's status check refuses the login.\n if (existingMember.status !== \"active\") {\n return { member: existingMember, created: false, linked: false };\n }\n await db.insert(npMemberIdentities).values({\n memberId: existingMember.id,\n provider,\n subject: profile.providerUserId,\n email: profile.email,\n metadata: mergeMetadata(profile),\n });\n return { member: existingMember, created: false, linked: true };\n }\n }\n\n // Step 3: auto-provision a brand-new member account. This is the\n // step the `community.registrationEnabled` site setting gates —\n // an invite-only site that disables password sign-up via\n // `/api/members/register` would otherwise be joined through OAuth\n // (the password endpoint and OAuth callback both create new\n // member rows from an unauthenticated request, so they're the\n // same surface from a policy point of view).\n //\n // Steps 1 and 2 are NOT gated: durable links and email matches\n // log an EXISTING member back in, which isn't a new\n // registration. An admin who flips `registrationEnabled = false`\n // expects existing members to keep working — only new accounts\n // should be refused.\n const settings = await getCommunitySettings();\n if (!settings.registrationEnabled) {\n throw new NpForbiddenError(\"members\", \"register\");\n }\n\n const email =\n profile.email && profile.email.trim().length > 0\n ? profile.email.trim().toLowerCase()\n : syntheticEmail(provider, profile.providerUserId);\n const displayName = deriveDisplayName(profile, email);\n const handle = generateHandle(profile, email);\n const placeholderPassword = await hashPassword(\n crypto.randomUUID() + crypto.randomUUID(),\n );\n\n const [created] = (await db\n .insert(npMembers)\n .values({\n email,\n handle,\n displayName,\n password: placeholderPassword,\n // OAuth verifies the address out-of-band (the provider showed the\n // user a real login screen for it), so skip the email-verify\n // dance that password registration goes through.\n emailVerified: true,\n status: \"active\",\n })\n .returning({\n id: npMembers.id,\n email: npMembers.email,\n handle: npMembers.handle,\n displayName: npMembers.displayName,\n status: npMembers.status,\n tokenVersion: npMembers.tokenVersion,\n })) as ResolvedOAuthMember[];\n\n await db.insert(npMemberIdentities).values({\n memberId: created.id,\n provider,\n subject: profile.providerUserId,\n email: profile.email ?? null,\n metadata: mergeMetadata(profile),\n });\n\n return { member: created, created: true, linked: true };\n}\n","import { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\n\nimport { readEnvPositiveInt } from \"../config/env.js\";\n\n/**\n * HMAC-signed state tokens for the OAuth start ↔ callback handshake.\n * The framework (not the provider) issues + verifies these — providers\n * only see them as opaque strings.\n *\n * Token shape: `<base64url(payload)>.<base64url(hmac)>` where payload is\n * JSON `{ providerId, nonce, expSeconds, codeVerifier }`. Using an HMAC\n * instead of a JWT keeps this self-contained — no jose import, no key\n * rotation surface — and the payload stays comfortably under the\n * cookie size cap.\n *\n * The `codeVerifier` is a 32-byte URL-safe random string that providers\n * supporting PKCE (Google, etc.) hash into the authorize URL. Providers\n * that don't use PKCE (GitHub) ignore it. We always generate one so the\n * flow is uniform.\n *\n * Default state TTL is 10 minutes — long enough for slow IdP redirects\n * (corporate SSO with MFA prompts), short enough that a stale state\n * cookie doesn't sit around forever. Override via\n * `NP_OAUTH_STATE_TTL_SECONDS`.\n */\n\nconst STATE_TTL_SECONDS = readEnvPositiveInt(\"NP_OAUTH_STATE_TTL_SECONDS\", 600);\nconst CODE_VERIFIER_BYTES = 32;\n\nexport interface OAuthStatePayload {\n providerId: string;\n nonce: string;\n expSeconds: number;\n codeVerifier: string;\n}\n\nexport interface IssuedOAuthState {\n /** The serialized state token (cookie + redirect query value). */\n token: string;\n /** The PKCE verifier — also embedded in the token, surfaced here so\n * the route can pass it to `provider.authorize()` without re-parsing. */\n codeVerifier: string;\n}\n\nfunction b64url(input: string | Buffer): string {\n return Buffer.from(input).toString(\"base64url\");\n}\n\nfunction sign(payload: string, secret: string): string {\n return createHmac(\"sha256\", secret).update(payload).digest(\"base64url\");\n}\n\nexport function issueOAuthState(providerId: string, secret: string): IssuedOAuthState {\n const nonce = randomBytes(16).toString(\"base64url\");\n const codeVerifier = randomBytes(CODE_VERIFIER_BYTES).toString(\"base64url\");\n const expSeconds = Math.floor(Date.now() / 1000) + STATE_TTL_SECONDS;\n const payload: OAuthStatePayload = { providerId, nonce, expSeconds, codeVerifier };\n const encoded = b64url(JSON.stringify(payload));\n const sig = sign(encoded, secret);\n return { token: `${encoded}.${sig}`, codeVerifier };\n}\n\nexport interface VerifyOAuthStateResult {\n ok: boolean;\n payload?: OAuthStatePayload;\n reason?: \"format\" | \"signature\" | \"expired\";\n}\n\n/**\n * Strict verification:\n * - Format must be `<payload>.<sig>` with two segments.\n * - HMAC must match (constant-time compare).\n * - `expSeconds` must be in the future.\n * - `providerId` in the payload must match the route's expected provider.\n * - `codeVerifier` must be a non-empty string.\n */\nexport function verifyOAuthState(\n token: string,\n expectedProviderId: string,\n secret: string,\n): VerifyOAuthStateResult {\n if (typeof token !== \"string\" || !token.includes(\".\")) {\n return { ok: false, reason: \"format\" };\n }\n const [encoded, sig] = token.split(\".\") as [string, string];\n if (!encoded || !sig) {\n return { ok: false, reason: \"format\" };\n }\n const expectedSig = sign(encoded, secret);\n const sigBuf = Buffer.from(sig);\n const expectedBuf = Buffer.from(expectedSig);\n if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {\n return { ok: false, reason: \"signature\" };\n }\n\n let payload: OAuthStatePayload;\n try {\n payload = JSON.parse(Buffer.from(encoded, \"base64url\").toString(\"utf8\"));\n } catch {\n return { ok: false, reason: \"format\" };\n }\n\n if (\n !payload ||\n typeof payload.providerId !== \"string\" ||\n typeof payload.nonce !== \"string\" ||\n typeof payload.expSeconds !== \"number\" ||\n typeof payload.codeVerifier !== \"string\" ||\n payload.codeVerifier.length === 0\n ) {\n return { ok: false, reason: \"format\" };\n }\n\n if (payload.providerId !== expectedProviderId) {\n return { ok: false, reason: \"signature\" };\n }\n\n if (payload.expSeconds <= Math.floor(Date.now() / 1000)) {\n return { ok: false, reason: \"expired\" };\n }\n\n return { ok: true, payload };\n}\n","import type { OAuthProfile, OAuthProvider } from \"./oauth-providers.js\";\n\n/**\n * Adapter that bridges any [arctic](https://arctic.js.org/) provider\n * (`new GitHub(...)`, `new Google(...)`, `new Apple(...)`, etc.) to\n * NexPress's `OAuthProvider` interface.\n *\n * Why this exists: arctic ships ~25 maintained providers and handles\n * the OAuth dance — token exchange, PKCE hashing, refresh-token\n * support — so plugin authors only have to write the **profile fetch**\n * (the part that varies most by provider). Our framework still owns\n * state cookies, identity ↔ user resolution, and session minting; this\n * adapter just lets users skip the boilerplate token POST.\n *\n * Usage from a plugin:\n *\n * import { Apple } from \"arctic\";\n * import { fromArctic, registerOAuthProvider } from \"@nexpress/core\";\n *\n * registerOAuthProvider(fromArctic(\n * // Factory: framework calls this each request with the freshly-\n * // resolved redirectUri (matters in dev when Next.js may bind a\n * // non-default port).\n * (redirectUri) => new Apple(clientId, teamId, keyId, privateKey, redirectUri),\n * {\n * id: \"apple\",\n * scopes: [\"name\", \"email\"],\n * fetchProfile: async (accessToken, tokens) => {\n * // Apple returns the user payload INSIDE the token response\n * // (not a separate userinfo endpoint) — pull it from\n * // `tokens.idToken()` here and parse the JWT body.\n * return { providerUserId: parseAppleSub(tokens.idToken()), email: null };\n * },\n * },\n * ));\n */\n\n/**\n * Minimal slice of arctic's provider classes that the adapter actually\n * needs. Both `GitHub` (no PKCE) and `Google` (PKCE-required) match\n * this — the third positional arg is \"second positional\" for\n * non-PKCE providers (just unused) and \"code verifier\" for PKCE ones.\n *\n * Declared structurally so we don't drag arctic into the public type\n * graph of `@nexpress/core`. Plugins that import a real arctic class\n * pass it directly; the structural match keeps the signature lined up.\n */\nexport interface ArcticLikeProvider {\n createAuthorizationURL(state: string, ...rest: never[]): URL;\n validateAuthorizationCode(code: string, ...rest: never[]): Promise<ArcticLikeTokens>;\n}\n\nexport interface ArcticLikeTokens {\n accessToken(): string;\n hasRefreshToken?(): boolean;\n refreshToken?(): string;\n idToken?(): string;\n}\n\nexport interface FromArcticOptions {\n /** Provider id used in route paths and `np_user_oauth_identities.provider`. */\n id: string;\n /** Human label for admin UI / login buttons. */\n label?: string;\n /** Scopes passed to `createAuthorizationURL`. Most providers default\n * to nothing useful — set this. */\n scopes?: string[];\n /**\n * Whether the underlying arctic provider expects a PKCE code verifier\n * as the second arg to `createAuthorizationURL` and\n * `validateAuthorizationCode`. Default `true` (Google, Apple, etc.).\n * Set `false` for non-PKCE providers like GitHub.\n */\n pkce?: boolean;\n /**\n * Turns an access token (and the full token response, useful for\n * providers like Apple that return the profile in the token) into the\n * normalized `OAuthProfile` consumed by `resolveOAuthLogin`.\n *\n * Throwing aborts the login with `oauth_error=exchange_failed`.\n */\n fetchProfile: (\n accessToken: string,\n tokens: ArcticLikeTokens,\n ) => Promise<OAuthProfile>;\n}\n\n/**\n * Wraps an arctic provider into the framework's `OAuthProvider`\n * shape. The framework calls `authorize` and `exchange`; this adapter\n * builds a fresh arctic instance per request via `factory(redirectUri)`\n * so the redirect URI always matches what the framework computed for\n * THIS request — critical in dev where Next.js may fall back to a\n * non-3000 port and a setup-time-frozen redirectUri would diverge.\n *\n * Arctic provider classes are cheap to construct (just hold the three\n * credential strings), so the per-request factory call has no\n * meaningful cost.\n */\nexport function fromArctic(\n factory: (redirectUri: string) => ArcticLikeProvider,\n opts: FromArcticOptions,\n): OAuthProvider {\n const usePkce = opts.pkce !== false;\n const scopes = opts.scopes ?? [];\n\n return {\n id: opts.id,\n label: opts.label,\n authorize({ state, redirectUri, codeVerifier }) {\n const arctic = factory(redirectUri);\n // Arctic's signatures vary: `(state, scopes)` for non-PKCE,\n // `(state, codeVerifier, scopes)` for PKCE. The structural type\n // hides this; do the dispatch here so plugin code stays clean.\n const url = usePkce\n ? (arctic.createAuthorizationURL as unknown as (\n state: string,\n verifier: string,\n scopes: string[],\n ) => URL)(state, codeVerifier, scopes)\n : (arctic.createAuthorizationURL as unknown as (\n state: string,\n scopes: string[],\n ) => URL)(state, scopes);\n return url.toString();\n },\n async exchange({ code, redirectUri, codeVerifier }) {\n const arctic = factory(redirectUri);\n const tokens = usePkce\n ? await (arctic.validateAuthorizationCode as unknown as (\n code: string,\n verifier: string,\n ) => Promise<ArcticLikeTokens>)(code, codeVerifier)\n : await (arctic.validateAuthorizationCode)(code);\n return opts.fetchProfile(tokens.accessToken(), tokens);\n },\n };\n}\n","import { webcrypto } from \"node:crypto\";\n\nimport { eq, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport type { NpAuthUser } from \"../config/types.js\";\nimport { verifyToken, type NpTokenUse } from \"./token.js\";\nimport { npSessions, npUsers } from \"../db/schema/system.js\";\n\n/**\n * Loose Drizzle handle type — every staff-auth caller passes\n * the same NodePgDatabase, but TS over-narrows when the\n * generated schema record is folded in. Using\n * `Record<string, unknown>` keeps the helper portable across\n * schema generations without surfacing as `any`.\n */\ntype SessionDb = NodePgDatabase<Record<string, unknown>>;\n\nexport async function sha256(input: string): Promise<string> {\n const digest = await webcrypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(input),\n );\n\n return Array.from(new Uint8Array(digest), (byte) =>\n byte.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n}\n\n/**\n * Verify a staff JWT and resolve the active user.\n *\n * `expectedUse` defaults to `\"access\"` because every caller of this\n * helper outside the rotation endpoint reads `np-session` (server\n * components, route handlers, the bootstrap layout). Defaulting\n * means a fresh route or RSC page can't accidentally tolerate a\n * refresh JWT in the session cookie just by forgetting the\n * argument. The rotation route explicitly passes `\"refresh\"` for\n * its `np-refresh` read.\n *\n * Tokens missing the `use` claim throw via `verifyToken`; we let\n * that propagate so a `NpAuthError` surfaces as 401 at the API\n * layer.\n */\nexport async function verifyTokenFull(\n token: string,\n secret: string,\n db: SessionDb,\n expectedUse: NpTokenUse = \"access\",\n): Promise<NpAuthUser | null> {\n const payload = await verifyToken(token, secret, expectedUse);\n const [user] = await db\n .select({\n id: npUsers.id,\n email: npUsers.email,\n name: npUsers.name,\n role: npUsers.role,\n tokenVersion: npUsers.tokenVersion,\n })\n .from(npUsers)\n .where(eq(npUsers.id, payload.sub))\n .limit(1);\n\n if (!user || user.tokenVersion !== payload.ver) {\n return null;\n }\n\n return user;\n}\n\nexport async function invalidateAllSessions(\n userId: string,\n db: SessionDb,\n): Promise<void> {\n await db.transaction(async (tx) => {\n await tx\n .update(npUsers)\n .set({\n tokenVersion: sql`${npUsers.tokenVersion} + 1`,\n })\n .where(eq(npUsers.id, userId));\n\n await tx.delete(npSessions).where(eq(npSessions.userId, userId));\n });\n}\n","import { and, desc, eq } from \"drizzle-orm\";\n\nimport { getDb } from \"../db/runtime.js\";\nimport { recordAuditEvent } from \"../community/audit.js\";\nimport {\n npMemberIdentities,\n npMembers,\n} from \"../db/schema/community.js\";\nimport { npUserOAuthIdentities, npUsers } from \"../db/schema/system.js\";\nimport { NpNotFoundError } from \"../errors.js\";\n\n/**\n * Admin-side helpers for listing and revoking OAuth identity links.\n * Both staff (`np_user_oauth_identities`) and member\n * (`np_member_identities`) tables use the same shape: one row per\n * (account, provider) pair, holding the durable provider subject\n * plus arbitrary metadata. These helpers are the source of truth for\n * `/api/admin/users/[id]/identities` and the member equivalent.\n *\n * Revoking does not invalidate sessions — the user / member can\n * re-link by signing in via OAuth again, which creates a fresh\n * identity row through the resolver. Revocation is intentionally\n * reversible because the durable link is the only thing dropped;\n * the underlying account remains.\n */\n\nexport interface NpUserIdentityRow {\n id: string;\n userId: string;\n provider: string;\n providerUserId: string;\n metadata: Record<string, unknown>;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface NpMemberIdentityRow {\n id: string;\n memberId: string;\n provider: string;\n subject: string;\n email: string | null;\n metadata: Record<string, unknown>;\n createdAt: Date;\n updatedAt: Date;\n}\n\nasync function assertUserExists(userId: string): Promise<void> {\n const db = getDb();\n const [row] = (await db\n .select({ id: npUsers.id })\n .from(npUsers)\n .where(eq(npUsers.id, userId))\n .limit(1)) as Array<{ id: string }>;\n if (!row) throw new NpNotFoundError(\"user\", userId);\n}\n\nasync function assertMemberExists(memberId: string): Promise<void> {\n const db = getDb();\n const [row] = (await db\n .select({ id: npMembers.id })\n .from(npMembers)\n .where(eq(npMembers.id, memberId))\n .limit(1)) as Array<{ id: string }>;\n if (!row) throw new NpNotFoundError(\"member\", memberId);\n}\n\nexport async function listUserIdentities(userId: string): Promise<NpUserIdentityRow[]> {\n await assertUserExists(userId);\n const db = getDb();\n const rows = (await db\n .select()\n .from(npUserOAuthIdentities)\n .where(eq(npUserOAuthIdentities.userId, userId))\n .orderBy(desc(npUserOAuthIdentities.createdAt))) as NpUserIdentityRow[];\n return rows;\n}\n\nexport async function listMemberIdentities(memberId: string): Promise<NpMemberIdentityRow[]> {\n await assertMemberExists(memberId);\n const db = getDb();\n const rows = (await db\n .select()\n .from(npMemberIdentities)\n .where(eq(npMemberIdentities.memberId, memberId))\n .orderBy(desc(npMemberIdentities.createdAt))) as NpMemberIdentityRow[];\n return rows;\n}\n\nexport interface RevokeIdentityInput {\n /** Staff user id whose identity is being revoked (`actorKind: \"staff\"`). */\n staffUserId: string;\n}\n\nexport async function revokeUserIdentity(\n userId: string,\n identityId: string,\n actor: RevokeIdentityInput,\n): Promise<void> {\n const db = getDb();\n // Fetch the row first so the audit event captures the provider /\n // subject — once deleted we'd lose the forensic context.\n const [existing] = (await db\n .select()\n .from(npUserOAuthIdentities)\n .where(\n and(\n eq(npUserOAuthIdentities.id, identityId),\n eq(npUserOAuthIdentities.userId, userId),\n ),\n )\n .limit(1)) as NpUserIdentityRow[];\n if (!existing) {\n // Either the identity doesn't exist or it belongs to a different\n // user — both surface as 404 to avoid leaking cross-user\n // existence to staff who don't have the right grants.\n throw new NpNotFoundError(\"identity\", identityId);\n }\n // Use `.returning()` so we can tell whether OUR call did the\n // delete. Two concurrent revokes both pass the select check\n // above; if we record an audit event unconditionally we'd\n // double-log the revocation. The second caller's delete returns\n // zero rows — we skip the audit there.\n const deleted = (await db\n .delete(npUserOAuthIdentities)\n .where(eq(npUserOAuthIdentities.id, identityId))\n .returning({ id: npUserOAuthIdentities.id })) as Array<{ id: string }>;\n if (deleted.length === 0) return;\n await recordAuditEvent({\n actor: { kind: \"staff\", userId: actor.staffUserId },\n action: \"user.identity.revoke\",\n targetType: \"user\",\n targetId: userId,\n payload: {\n identityId,\n provider: existing.provider,\n providerUserId: existing.providerUserId,\n },\n });\n}\n\nexport async function revokeMemberIdentity(\n memberId: string,\n identityId: string,\n actor: RevokeIdentityInput,\n): Promise<void> {\n const db = getDb();\n const [existing] = (await db\n .select()\n .from(npMemberIdentities)\n .where(\n and(\n eq(npMemberIdentities.id, identityId),\n eq(npMemberIdentities.memberId, memberId),\n ),\n )\n .limit(1)) as NpMemberIdentityRow[];\n if (!existing) throw new NpNotFoundError(\"identity\", identityId);\n const deleted = (await db\n .delete(npMemberIdentities)\n .where(eq(npMemberIdentities.id, identityId))\n .returning({ id: npMemberIdentities.id })) as Array<{ id: string }>;\n if (deleted.length === 0) return;\n await recordAuditEvent({\n actor: { kind: \"staff\", userId: actor.staffUserId },\n action: \"member.identity.revoke\",\n targetType: \"member\",\n targetId: memberId,\n payload: {\n identityId,\n provider: existing.provider,\n subject: existing.subject,\n },\n });\n}\n","import { randomBytes } from \"node:crypto\";\n\nimport { and, eq, gt, isNotNull, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { NpValidationError } from \"../errors.js\";\nimport { npSessions, npUsers } from \"../db/schema/system.js\";\nimport { hashPassword } from \"./password.js\";\nimport { sha256 } from \"./session.js\";\n\nexport type NpPasswordResetPurpose = \"invite\" | \"reset\";\n\nexport interface NpIssuedResetToken {\n /** The raw token — deliver to the user, never persist. */\n token: string;\n /** Matches `np_users.password_reset_expires_at`. */\n expiresAt: Date;\n purpose: NpPasswordResetPurpose;\n}\n\nexport interface NpCreateResetTokenOptions {\n userId: string;\n purpose: NpPasswordResetPurpose;\n ttlMs: number;\n}\n\nconst MIN_PASSWORD_LENGTH = 8;\n\nfunction generateRawToken(): string {\n // 32 bytes → 64 hex chars. Wide enough that brute force is hopeless.\n return randomBytes(32).toString(\"hex\");\n}\n\n/**\n * Issues a new password reset token for `userId`. Stores the **hash** of the\n * token in the `np_users` row alongside the expiry and purpose, then returns\n * the raw token for the caller to deliver (email/link).\n *\n * Any previously-outstanding reset token for the user is replaced.\n */\nexport async function createPasswordResetToken(\n db: NodePgDatabase<Record<string, unknown>>,\n options: NpCreateResetTokenOptions,\n): Promise<NpIssuedResetToken> {\n const token = generateRawToken();\n const tokenHash = await sha256(token);\n const expiresAt = new Date(Date.now() + options.ttlMs);\n\n await db\n .update(npUsers)\n .set({\n passwordResetTokenHash: tokenHash,\n passwordResetExpiresAt: expiresAt,\n passwordResetPurpose: options.purpose,\n updatedAt: new Date(),\n })\n .where(eq(npUsers.id, options.userId));\n\n return { token, expiresAt, purpose: options.purpose };\n}\n\nexport interface NpResetRequestResult {\n userId: string | null;\n name: string | null;\n email: string | null;\n issued: NpIssuedResetToken | null;\n}\n\n/**\n * Handles the \"forgot password\" flow. If the email matches a user, issues a\n * reset token and returns their name so the mailer can personalise the email.\n * If not, silently returns nulls so callers can respond with a constant\n * message and avoid email enumeration.\n */\nexport async function requestPasswordReset(\n db: NodePgDatabase<Record<string, unknown>>,\n email: string,\n ttlMs: number,\n): Promise<NpResetRequestResult> {\n const normalizedEmail = email.trim().toLowerCase();\n const [user] = await db\n .select({\n id: npUsers.id,\n email: npUsers.email,\n name: npUsers.name,\n })\n .from(npUsers)\n .where(eq(npUsers.email, normalizedEmail))\n .limit(1);\n\n if (!user) {\n return { userId: null, name: null, email: null, issued: null };\n }\n\n const issued = await createPasswordResetToken(db, {\n userId: user.id,\n purpose: \"reset\",\n ttlMs,\n });\n\n return { userId: user.id, name: user.name, email: user.email, issued };\n}\n\nexport interface NpConsumeResetTokenOptions {\n token: string;\n newPassword: string;\n}\n\nexport interface NpConsumeResetTokenResult {\n userId: string;\n email: string;\n purpose: NpPasswordResetPurpose;\n}\n\n/**\n * Verifies a password reset token and atomically:\n * - sets the new password hash\n * - bumps `tokenVersion` and deletes all sessions (force logout everywhere)\n * - clears the reset columns on the user row\n *\n * Throws `NpValidationError` when the token is unknown, expired, or the\n * password is too short. Uses a single DB transaction for atomicity.\n */\nexport async function consumePasswordResetToken(\n db: NodePgDatabase<Record<string, unknown>>,\n options: NpConsumeResetTokenOptions,\n): Promise<NpConsumeResetTokenResult> {\n if (!options.token || typeof options.token !== \"string\") {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"token\", message: \"Reset token is required.\" },\n ]);\n }\n\n if (!options.newPassword || options.newPassword.length < MIN_PASSWORD_LENGTH) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"password\",\n message: `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n },\n ]);\n }\n\n const tokenHash = await sha256(options.token);\n const now = new Date();\n\n const [user] = await db\n .select({\n id: npUsers.id,\n email: npUsers.email,\n purpose: npUsers.passwordResetPurpose,\n })\n .from(npUsers)\n .where(\n and(\n eq(npUsers.passwordResetTokenHash, tokenHash),\n isNotNull(npUsers.passwordResetExpiresAt),\n gt(npUsers.passwordResetExpiresAt, now),\n ),\n )\n .limit(1);\n\n if (!user) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"token\", message: \"Reset link is invalid or has expired.\" },\n ]);\n }\n\n const newPasswordHash = await hashPassword(options.newPassword);\n\n // We inline the tokenVersion bump + session delete instead of calling\n // invalidateAllSessions because we need them to land in the same\n // transaction as the password write + reset-column clear. Splitting into\n // two transactions could leave the user with a new password but still-\n // valid old JWTs if the second call failed.\n await db.transaction(async (tx) => {\n await tx\n .update(npUsers)\n .set({\n password: newPasswordHash,\n passwordResetTokenHash: null,\n passwordResetExpiresAt: null,\n passwordResetPurpose: null,\n loginAttempts: 0,\n lockUntil: null,\n tokenVersion: sql`${npUsers.tokenVersion} + 1`,\n updatedAt: new Date(),\n })\n .where(eq(npUsers.id, user.id));\n\n await tx.delete(npSessions).where(eq(npSessions.userId, user.id));\n });\n\n return {\n userId: user.id,\n email: user.email,\n purpose: (user.purpose ?? \"reset\"),\n };\n}\n","import { randomBytes } from \"node:crypto\";\nimport { jwtVerify, SignJWT, type JWTPayload } from \"jose\";\n\nimport { NpAuthError } from \"../errors.js\";\n\n/**\n * Member-side JWT helpers. Mirrors `signToken` / `verifyToken` for\n * staff but adds a fixed `aud: \"member\"` claim so a forged JWT signed\n * for a staff user can't be replayed against member-only routes (and\n * vice-versa).\n *\n * The signing secret is the same `NP_SECRET`; rotating it invalidates\n * both staff and member sessions, which is the desired behavior.\n *\n * Every token gets a random `jti` so two tokens minted within the\n * same second for the same member produce DIFFERENT JWT strings —\n * needed for refresh-token rotation: without it, the rotated token\n * hash would collide with the prior token hash and revocation by\n * tokenHash would still resolve the rotated row.\n *\n * `use: \"access\" | \"refresh\"` separates the two token purposes. A\n * refresh JWT cannot be presented as the `np-mb-session` cookie and\n * a session JWT cannot drive the rotation endpoint — without this\n * separation a leaked refresh token effectively became a long-lived\n * bearer access token because both kinds were stored as fungible\n * rows in `np_member_sessions` with no row-level kind column.\n */\nexport type NpMemberTokenUse = \"access\" | \"refresh\";\n\nexport interface NpMemberTokenPayload {\n sub: string;\n aud: \"member\";\n ver: number;\n /** Required. `verifyMemberToken` refuses tokens missing this claim\n * so legacy refresh JWTs from before #92 cannot be smuggled into\n * the session cookie path (#91 reopen). */\n use: NpMemberTokenUse;\n /** Optional only for the deploy window; new tokens always carry\n * one. */\n jti?: string;\n iat: number;\n exp: number;\n}\n\nconst textEncoder = new TextEncoder();\nconst MEMBER_AUDIENCE = \"member\";\n\nexport async function signMemberToken(\n member: { id: string; tokenVersion: number },\n secret: string,\n expirationSeconds: number = 7200,\n tokenUse: NpMemberTokenUse = \"access\",\n): Promise<string> {\n const secretKey = textEncoder.encode(secret);\n return new SignJWT({ sub: member.id, ver: member.tokenVersion, use: tokenUse })\n .setProtectedHeader({ alg: \"HS256\" })\n .setAudience(MEMBER_AUDIENCE)\n .setJti(randomBytes(16).toString(\"base64url\"))\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + expirationSeconds)\n .sign(secretKey);\n}\n\n/**\n * Verify a member JWT and return the parsed payload. When\n * `expectedUse` is provided, refuses tokens whose `use` claim doesn't\n * match — that's how `getSessionMember` rejects a refresh token used\n * as a session cookie and how the refresh route rejects an access\n * token as a refresh trigger.\n *\n * Tokens minted before the `use` claim landed have NO `use` payload\n * field. We refuse those outright rather than treating them as\n * `access` — the prior fallback let still-live legacy refresh JWTs\n * (already persisted in `np_member_sessions` per #45's fix) be\n * smuggled into the session cookie and pass the access check (#91\n * reopen). The cost: members logged in before this deploy must log\n * in once. That's bounded by the access-token TTL (default 2h);\n * legacy session rows that don't match a new login age out via\n * `expiresAt` within 7 days regardless.\n */\nexport async function verifyMemberToken(\n token: string,\n secret: string,\n expectedUse?: NpMemberTokenUse,\n): Promise<NpMemberTokenPayload> {\n const secretKey = textEncoder.encode(secret);\n const { payload } = await jwtVerify(token, secretKey, { audience: MEMBER_AUDIENCE });\n // jwtVerify already validated `aud === MEMBER_AUDIENCE`; cast through\n // JWTPayload to lock in the fields we know land on member tokens.\n const typed = payload as JWTPayload & {\n sub: string;\n ver: number;\n iat: number;\n exp: number;\n use?: NpMemberTokenUse;\n };\n if (typed.use !== \"access\" && typed.use !== \"refresh\") {\n throw new NpAuthError(\"Member token missing `use` claim\");\n }\n const use: NpMemberTokenUse = typed.use;\n if (expectedUse && use !== expectedUse) {\n // Throw `NpAuthError` so the response mapper emits 401 instead of\n // a plain 500 — this is an auth failure, not a server failure.\n throw new NpAuthError(\n `Member token use mismatch: expected ${expectedUse}, got ${use}`,\n );\n }\n return { ...typed, aud: MEMBER_AUDIENCE, use };\n}\n","import { and, eq, gt, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { npMemberSessions, npMembers } from \"../db/schema/community.js\";\nimport { sha256 } from \"./session.js\";\n\n/**\n * Member-side session lookups, mirroring the staff helpers in session.ts\n * but for `np_members` / `np_member_sessions`. The sha256 helper is\n * reused (sessions store hashed tokens regardless of the principal kind).\n */\n\nexport interface NpMemberAuthRow {\n id: string;\n email: string;\n handle: string;\n displayName: string;\n status: \"active\" | \"pending\" | \"suspended\" | \"deleted\";\n tokenVersion: number;\n}\n\n/**\n * Resolve a member from a verified JWT payload AND the raw access\n * token. We hash the token and require a live row in\n * `np_member_sessions` — without that row check, deleting a session in\n * `/api/members/logout` had no effect and a stolen token kept working\n * until JWT expiry. (#45)\n *\n * Backward-compat: when no `accessToken` is passed (legacy callers in\n * tests / older routes), we fall back to the previous tokenVersion\n * check only. New paths should always pass the token.\n */\nexport async function getMemberFromTokenPayload(\n db: NodePgDatabase<Record<string, unknown>>,\n payload: { sub: string; ver: number },\n accessToken?: string,\n): Promise<NpMemberAuthRow | null> {\n const [row] = await db\n .select({\n id: npMembers.id,\n email: npMembers.email,\n handle: npMembers.handle,\n displayName: npMembers.displayName,\n status: npMembers.status,\n tokenVersion: npMembers.tokenVersion,\n })\n .from(npMembers)\n .where(eq(npMembers.id, payload.sub))\n .limit(1);\n\n if (!row) return null;\n if (row.tokenVersion !== payload.ver) return null;\n\n if (accessToken) {\n const tokenHash = await sha256(accessToken);\n const now = new Date();\n const [session] = (await db\n .select({ id: npMemberSessions.id })\n .from(npMemberSessions)\n .where(\n and(\n eq(npMemberSessions.memberId, row.id),\n eq(npMemberSessions.tokenHash, tokenHash),\n gt(npMemberSessions.expiresAt, now),\n ),\n )\n .limit(1)) as Array<{ id: string }>;\n if (!session) return null;\n }\n\n return row as NpMemberAuthRow;\n}\n\n/**\n * Bumps a member's tokenVersion + drops every session row, force-logging\n * them out everywhere. Call inside the same transaction as a password\n * change / soft-delete so a leaked old JWT can't outlive the change.\n */\nexport async function invalidateAllMemberSessions(\n db: NodePgDatabase<Record<string, unknown>>,\n memberId: string,\n): Promise<void> {\n await db.transaction(async (tx) => {\n await tx\n .update(npMembers)\n .set({\n tokenVersion: sql`${npMembers.tokenVersion} + 1`,\n updatedAt: new Date(),\n })\n .where(eq(npMembers.id, memberId));\n await tx.delete(npMemberSessions).where(eq(npMemberSessions.memberId, memberId));\n });\n}\n","import { randomBytes } from \"node:crypto\";\n\nimport { and, eq, gt, isNotNull, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\nimport { NpValidationError } from \"../errors.js\";\nimport { npMemberSessions, npMembers } from \"../db/schema/community.js\";\nimport { hashPassword } from \"./password.js\";\nimport { sha256 } from \"./session.js\";\n\n/**\n * Member-side credential flows: email verification on registration,\n * password reset, password change. Mirrors the staff equivalents in\n * `reset-token.ts` but writes to `np_members` and uses dedicated\n * verify columns (`email_verify_token_hash` / `email_verify_expires_at`)\n * so a verify and a reset can coexist on the same member row.\n */\n\nconst MIN_PASSWORD_LENGTH = 8;\n\nexport interface NpIssuedMemberToken {\n /** The raw token to ship to the user. Never persist. */\n token: string;\n expiresAt: Date;\n}\n\nfunction generateRawToken(): string {\n return randomBytes(32).toString(\"hex\");\n}\n\n// ── Email verification ────────────────────────────────────────────────\n\nexport async function createMemberEmailVerifyToken(\n db: NodePgDatabase<Record<string, unknown>>,\n memberId: string,\n ttlMs: number,\n): Promise<NpIssuedMemberToken> {\n const token = generateRawToken();\n const tokenHash = await sha256(token);\n const expiresAt = new Date(Date.now() + ttlMs);\n\n await db\n .update(npMembers)\n .set({\n emailVerifyTokenHash: tokenHash,\n emailVerifyExpiresAt: expiresAt,\n updatedAt: new Date(),\n })\n .where(eq(npMembers.id, memberId));\n\n return { token, expiresAt };\n}\n\nexport interface NpConsumeMemberEmailVerifyResult {\n memberId: string;\n email: string;\n handle: string;\n displayName: string;\n}\n\nexport async function consumeMemberEmailVerifyToken(\n db: NodePgDatabase<Record<string, unknown>>,\n token: string,\n): Promise<NpConsumeMemberEmailVerifyResult> {\n if (!token || typeof token !== \"string\") {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"token\", message: \"Verification token is required.\" },\n ]);\n }\n const tokenHash = await sha256(token);\n const now = new Date();\n\n const [member] = await db\n .select({\n id: npMembers.id,\n email: npMembers.email,\n handle: npMembers.handle,\n displayName: npMembers.displayName,\n })\n .from(npMembers)\n .where(\n and(\n eq(npMembers.emailVerifyTokenHash, tokenHash),\n isNotNull(npMembers.emailVerifyExpiresAt),\n gt(npMembers.emailVerifyExpiresAt, now),\n ),\n )\n .limit(1);\n\n if (!member) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"token\", message: \"Verification link is invalid or has expired.\" },\n ]);\n }\n\n await db\n .update(npMembers)\n .set({\n emailVerified: true,\n // Pending → active on first verify so login can succeed afterwards.\n // Suspended/deleted members stay where they are; the mod UI flips\n // those statuses, never the verify endpoint.\n status: sql`case when ${npMembers.status} = 'pending' then 'active' else ${npMembers.status} end`,\n emailVerifyTokenHash: null,\n emailVerifyExpiresAt: null,\n updatedAt: now,\n })\n .where(eq(npMembers.id, member.id));\n\n return {\n memberId: member.id,\n email: member.email,\n handle: member.handle,\n displayName: member.displayName,\n };\n}\n\n// ── Password reset ────────────────────────────────────────────────────\n\nexport interface NpMemberResetRequestResult {\n memberId: string | null;\n displayName: string | null;\n email: string | null;\n issued: NpIssuedMemberToken | null;\n}\n\nexport async function requestMemberPasswordReset(\n db: NodePgDatabase<Record<string, unknown>>,\n email: string,\n ttlMs: number,\n): Promise<NpMemberResetRequestResult> {\n const normalizedEmail = email.trim().toLowerCase();\n const [member] = await db\n .select({\n id: npMembers.id,\n email: npMembers.email,\n displayName: npMembers.displayName,\n status: npMembers.status,\n })\n .from(npMembers)\n .where(eq(npMembers.email, normalizedEmail))\n .limit(1);\n\n if (!member || member.status === \"deleted\") {\n return { memberId: null, displayName: null, email: null, issued: null };\n }\n\n const token = generateRawToken();\n const tokenHash = await sha256(token);\n const expiresAt = new Date(Date.now() + ttlMs);\n\n await db\n .update(npMembers)\n .set({\n passwordResetTokenHash: tokenHash,\n passwordResetExpiresAt: expiresAt,\n updatedAt: new Date(),\n })\n .where(eq(npMembers.id, member.id));\n\n return {\n memberId: member.id,\n displayName: member.displayName,\n email: member.email,\n issued: { token, expiresAt },\n };\n}\n\nexport interface NpConsumeMemberResetResult {\n memberId: string;\n email: string;\n}\n\nexport async function consumeMemberPasswordReset(\n db: NodePgDatabase<Record<string, unknown>>,\n token: string,\n newPassword: string,\n): Promise<NpConsumeMemberResetResult> {\n if (!token || typeof token !== \"string\") {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"token\", message: \"Reset token is required.\" },\n ]);\n }\n if (!newPassword || newPassword.length < MIN_PASSWORD_LENGTH) {\n throw new NpValidationError(\"Invalid input\", [\n {\n field: \"password\",\n message: `Password must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n },\n ]);\n }\n\n const tokenHash = await sha256(token);\n const now = new Date();\n\n const [member] = await db\n .select({ id: npMembers.id, email: npMembers.email })\n .from(npMembers)\n .where(\n and(\n eq(npMembers.passwordResetTokenHash, tokenHash),\n isNotNull(npMembers.passwordResetExpiresAt),\n gt(npMembers.passwordResetExpiresAt, now),\n ),\n )\n .limit(1);\n\n if (!member) {\n throw new NpValidationError(\"Invalid input\", [\n { field: \"token\", message: \"Reset link is invalid or has expired.\" },\n ]);\n }\n\n const newPasswordHash = await hashPassword(newPassword);\n\n await db.transaction(async (tx) => {\n await tx\n .update(npMembers)\n .set({\n password: newPasswordHash,\n passwordResetTokenHash: null,\n passwordResetExpiresAt: null,\n loginAttempts: 0,\n lockUntil: null,\n // Bump tokenVersion in-place so existing JWTs are invalidated. Also\n // mark email as verified — completing a reset on an unverified\n // account is itself proof of email ownership.\n tokenVersion: sql`${npMembers.tokenVersion} + 1`,\n emailVerified: true,\n status: sql`case when ${npMembers.status} = 'pending' then 'active' else ${npMembers.status} end`,\n updatedAt: new Date(),\n })\n .where(eq(npMembers.id, member.id));\n\n await tx.delete(npMemberSessions).where(eq(npMemberSessions.memberId, member.id));\n });\n\n return { memberId: member.id, email: member.email };\n}\n"],"mappings":"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","names":["eq","eq","and","eq","sql","SYNTHETIC_EMAIL_SUFFIX","syntheticEmail","mergeMetadata","eq","and","sql","randomBytes","randomBytes","eq","sql","eq","sql","and","eq","eq","and","randomBytes","and","eq","sql","randomBytes","eq","and","sql","randomBytes","jwtVerify","SignJWT","textEncoder","SignJWT","randomBytes","jwtVerify","and","eq","gt","sql","eq","and","gt","sql","randomBytes","and","eq","gt","isNotNull","sql","MIN_PASSWORD_LENGTH","generateRawToken","randomBytes","eq","and","isNotNull","gt","sql"]}
|