@nexpress/core 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/{audit-43OLHR3U.js → audit-ZLNKBIDO.js} +3 -3
  2. package/dist/auth.js +4 -4
  3. package/dist/{can-UJ2NAOIR.js → can-U5F4JBZ7.js} +2 -2
  4. package/dist/{chunk-UMEFU7Y3.js → chunk-2O2KMHLO.js} +10 -10
  5. package/dist/chunk-2O2KMHLO.js.map +1 -0
  6. package/dist/{chunk-ELK6AVW5.js → chunk-2X3GBJOT.js} +2 -2
  7. package/dist/{chunk-ML2E3P3X.js → chunk-5C22NDW4.js} +2 -2
  8. package/dist/chunk-5C22NDW4.js.map +1 -0
  9. package/dist/{chunk-RKM4GDWM.js → chunk-6MRTH734.js} +1 -1
  10. package/dist/chunk-6MRTH734.js.map +1 -0
  11. package/dist/{chunk-STOLH4V2.js → chunk-6PFUXZJ6.js} +12 -12
  12. package/dist/chunk-6PFUXZJ6.js.map +1 -0
  13. package/dist/{chunk-WRDIRDH7.js → chunk-CD74WQK7.js} +76 -28
  14. package/dist/chunk-CD74WQK7.js.map +1 -0
  15. package/dist/{chunk-QBIJZZ5V.js → chunk-CGLJBRRX.js} +2 -2
  16. package/dist/chunk-CGLJBRRX.js.map +1 -0
  17. package/dist/{chunk-2VZZ7M26.js → chunk-EAYUAXW3.js} +3 -3
  18. package/dist/chunk-EAYUAXW3.js.map +1 -0
  19. package/dist/{chunk-2N53KKIL.js → chunk-EWVXP3GP.js} +2 -2
  20. package/dist/{chunk-CAS4Z6IN.js → chunk-I4FSVEJK.js} +1 -1
  21. package/dist/chunk-I4FSVEJK.js.map +1 -0
  22. package/dist/{chunk-KHTS6Y3E.js → chunk-JKTU67A7.js} +2 -2
  23. package/dist/{chunk-KHTS6Y3E.js.map → chunk-JKTU67A7.js.map} +1 -1
  24. package/dist/{chunk-LN6NTH6E.js → chunk-K4CJ3KXB.js} +3 -3
  25. package/dist/chunk-K4CJ3KXB.js.map +1 -0
  26. package/dist/{chunk-B7DTNT4O.js → chunk-MWLSXK6Y.js} +2 -2
  27. package/dist/{chunk-7KGF7JVJ.js → chunk-PPUHXOWZ.js} +2 -2
  28. package/dist/{chunk-NFHS7CFV.js → chunk-Q7MK5ZKG.js} +2 -2
  29. package/dist/{chunk-6UV2P5MW.js → chunk-TIWJVQOO.js} +3 -3
  30. package/dist/chunk-TIWJVQOO.js.map +1 -0
  31. package/dist/{chunk-L6VG7IK6.js → chunk-VBVLYFSZ.js} +2 -2
  32. package/dist/chunk-VBVLYFSZ.js.map +1 -0
  33. package/dist/{chunk-YYPSQMFY.js → chunk-VX3HM5TF.js} +2 -2
  34. package/dist/{chunk-RDTTK27V.js → chunk-XPD7EQML.js} +3 -3
  35. package/dist/chunk-XPD7EQML.js.map +1 -0
  36. package/dist/{chunk-RJ76SKWQ.js → chunk-XU2GJJ6Z.js} +1 -1
  37. package/dist/chunk-XU2GJJ6Z.js.map +1 -0
  38. package/dist/{chunk-WJJ5MBH5.js → chunk-YEOQJ7WW.js} +1 -1
  39. package/dist/chunk-YEOQJ7WW.js.map +1 -0
  40. package/dist/community.js +14 -14
  41. package/dist/{config-YFGOXHSR.js → config-2CV7KZ3D.js} +5 -5
  42. package/dist/{digest-ZODDTXA2.js → digest-IWHMJPXI.js} +4 -4
  43. package/dist/{host-SUX3SPOX.js → host-C5PGUXX7.js} +4 -4
  44. package/dist/i18n.js +2 -2
  45. package/dist/index.js +21 -21
  46. package/dist/index.js.map +1 -1
  47. package/dist/{job-log-N3IGI4NA.js → job-log-UY6ERPQZ.js} +3 -3
  48. package/dist/jobs.js +3 -3
  49. package/dist/{logger-2WUTTELV.js → logger-6ZGEKEMK.js} +2 -2
  50. package/dist/media.js +3 -3
  51. package/dist/{mentions-U4JACYI6.js → mentions-LQRZWAGO.js} +2 -2
  52. package/dist/{mutes-MNQP6ACF.js → mutes-PQA6U5X7.js} +2 -2
  53. package/dist/{notification-prefs-H4HFVCL7.js → notification-prefs-62NX2GBF.js} +2 -2
  54. package/dist/observability.js +2 -2
  55. package/dist/{reputation-ICIXDGPM.js → reputation-5DJLDBZY.js} +3 -3
  56. package/dist/{scheduled-T5WZ4I6O.js → scheduled-PF2HECSF.js} +5 -5
  57. package/dist/seo.js +4 -4
  58. package/dist/{settings-OZWM6L2K.js → settings-NBAP7E5E.js} +2 -2
  59. package/dist/{strings-4EWJYDOG.js → strings-O2M7VSKV.js} +3 -3
  60. package/package.json +1 -1
  61. package/dist/chunk-2VZZ7M26.js.map +0 -1
  62. package/dist/chunk-6UV2P5MW.js.map +0 -1
  63. package/dist/chunk-CAS4Z6IN.js.map +0 -1
  64. package/dist/chunk-L6VG7IK6.js.map +0 -1
  65. package/dist/chunk-LN6NTH6E.js.map +0 -1
  66. package/dist/chunk-ML2E3P3X.js.map +0 -1
  67. package/dist/chunk-QBIJZZ5V.js.map +0 -1
  68. package/dist/chunk-RDTTK27V.js.map +0 -1
  69. package/dist/chunk-RJ76SKWQ.js.map +0 -1
  70. package/dist/chunk-RKM4GDWM.js.map +0 -1
  71. package/dist/chunk-STOLH4V2.js.map +0 -1
  72. package/dist/chunk-UMEFU7Y3.js.map +0 -1
  73. package/dist/chunk-WJJ5MBH5.js.map +0 -1
  74. package/dist/chunk-WRDIRDH7.js.map +0 -1
  75. /package/dist/{audit-43OLHR3U.js.map → audit-ZLNKBIDO.js.map} +0 -0
  76. /package/dist/{can-UJ2NAOIR.js.map → can-U5F4JBZ7.js.map} +0 -0
  77. /package/dist/{chunk-ELK6AVW5.js.map → chunk-2X3GBJOT.js.map} +0 -0
  78. /package/dist/{chunk-2N53KKIL.js.map → chunk-EWVXP3GP.js.map} +0 -0
  79. /package/dist/{chunk-B7DTNT4O.js.map → chunk-MWLSXK6Y.js.map} +0 -0
  80. /package/dist/{chunk-7KGF7JVJ.js.map → chunk-PPUHXOWZ.js.map} +0 -0
  81. /package/dist/{chunk-NFHS7CFV.js.map → chunk-Q7MK5ZKG.js.map} +0 -0
  82. /package/dist/{chunk-YYPSQMFY.js.map → chunk-VX3HM5TF.js.map} +0 -0
  83. /package/dist/{config-YFGOXHSR.js.map → config-2CV7KZ3D.js.map} +0 -0
  84. /package/dist/{digest-ZODDTXA2.js.map → digest-IWHMJPXI.js.map} +0 -0
  85. /package/dist/{host-SUX3SPOX.js.map → host-C5PGUXX7.js.map} +0 -0
  86. /package/dist/{job-log-N3IGI4NA.js.map → job-log-UY6ERPQZ.js.map} +0 -0
  87. /package/dist/{logger-2WUTTELV.js.map → logger-6ZGEKEMK.js.map} +0 -0
  88. /package/dist/{mentions-U4JACYI6.js.map → mentions-LQRZWAGO.js.map} +0 -0
  89. /package/dist/{mutes-MNQP6ACF.js.map → mutes-PQA6U5X7.js.map} +0 -0
  90. /package/dist/{notification-prefs-H4HFVCL7.js.map → notification-prefs-62NX2GBF.js.map} +0 -0
  91. /package/dist/{reputation-ICIXDGPM.js.map → reputation-5DJLDBZY.js.map} +0 -0
  92. /package/dist/{scheduled-T5WZ4I6O.js.map → scheduled-PF2HECSF.js.map} +0 -0
  93. /package/dist/{settings-OZWM6L2K.js.map → settings-NBAP7E5E.js.map} +0 -0
  94. /package/dist/{strings-4EWJYDOG.js.map → strings-O2M7VSKV.js.map} +0 -0
@@ -6,9 +6,9 @@ import {
6
6
  pruneJobLogsOlderThan,
7
7
  recordJobLog,
8
8
  runInJobContext
9
- } from "./chunk-QBIJZZ5V.js";
9
+ } from "./chunk-CGLJBRRX.js";
10
10
  import "./chunk-OROPGO65.js";
11
- import "./chunk-NFHS7CFV.js";
11
+ import "./chunk-Q7MK5ZKG.js";
12
12
  import "./chunk-XANPEOJC.js";
13
13
  import "./chunk-X7K5F2UI.js";
14
14
  import "./chunk-PZ5AY32C.js";
@@ -21,4 +21,4 @@ export {
21
21
  recordJobLog,
22
22
  runInJobContext
23
23
  };
24
- //# sourceMappingURL=job-log-N3IGI4NA.js.map
24
+ //# sourceMappingURL=job-log-UY6ERPQZ.js.map
package/dist/jobs.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  startWorker,
19
19
  stopProducer,
20
20
  stopWorker
21
- } from "./chunk-UMEFU7Y3.js";
21
+ } from "./chunk-2O2KMHLO.js";
22
22
  import {
23
23
  DEFAULT_JOB_LOG_RETENTION_MS,
24
24
  countJobLogs,
@@ -27,7 +27,7 @@ import {
27
27
  pruneJobLogsOlderThan,
28
28
  recordJobLog,
29
29
  runInJobContext
30
- } from "./chunk-QBIJZZ5V.js";
30
+ } from "./chunk-CGLJBRRX.js";
31
31
  import "./chunk-LSHHRDVR.js";
32
32
  import {
33
33
  enqueueJob,
@@ -37,7 +37,7 @@ import {
37
37
  } from "./chunk-V2UNHGAP.js";
38
38
  import "./chunk-WV272MPW.js";
39
39
  import "./chunk-OROPGO65.js";
40
- import "./chunk-NFHS7CFV.js";
40
+ import "./chunk-Q7MK5ZKG.js";
41
41
  import "./chunk-XANPEOJC.js";
42
42
  import "./chunk-X7K5F2UI.js";
43
43
  import "./chunk-PZ5AY32C.js";
@@ -4,7 +4,7 @@ import {
4
4
  getScopedLogger,
5
5
  resetLogger,
6
6
  setLogger
7
- } from "./chunk-NFHS7CFV.js";
7
+ } from "./chunk-Q7MK5ZKG.js";
8
8
  import "./chunk-PZ5AY32C.js";
9
9
  export {
10
10
  consoleLogger,
@@ -13,4 +13,4 @@ export {
13
13
  resetLogger,
14
14
  setLogger
15
15
  };
16
- //# sourceMappingURL=logger-2WUTTELV.js.map
16
+ //# sourceMappingURL=logger-6ZGEKEMK.js.map
package/dist/media.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-TETTWT56.js";
5
5
  import {
6
6
  getMediaUrl
7
- } from "./chunk-2N53KKIL.js";
7
+ } from "./chunk-EWVXP3GP.js";
8
8
  import {
9
9
  DEFAULT_IMAGE_SIZES,
10
10
  cleanupDeletedMedia,
@@ -16,10 +16,10 @@ import {
16
16
  processMediaImage,
17
17
  setStorageAdapter,
18
18
  uploadMedia
19
- } from "./chunk-2VZZ7M26.js";
19
+ } from "./chunk-EAYUAXW3.js";
20
20
  import "./chunk-V2UNHGAP.js";
21
21
  import "./chunk-OROPGO65.js";
22
- import "./chunk-NFHS7CFV.js";
22
+ import "./chunk-Q7MK5ZKG.js";
23
23
  import "./chunk-XANPEOJC.js";
24
24
  import "./chunk-X7K5F2UI.js";
25
25
  import "./chunk-PZ5AY32C.js";
@@ -5,7 +5,7 @@ import {
5
5
  extractMentionHandlesFromRichText,
6
6
  fanOutMentionNotifications,
7
7
  resolveMentionedMembers
8
- } from "./chunk-RDTTK27V.js";
8
+ } from "./chunk-XPD7EQML.js";
9
9
  import "./chunk-U4QCCLAW.js";
10
10
  import "./chunk-SBCVAC2Z.js";
11
11
  import "./chunk-ZCINJSS4.js";
@@ -20,4 +20,4 @@ export {
20
20
  fanOutMentionNotifications,
21
21
  resolveMentionedMembers
22
22
  };
23
- //# sourceMappingURL=mentions-U4JACYI6.js.map
23
+ //# sourceMappingURL=mentions-LQRZWAGO.js.map
@@ -4,7 +4,7 @@ import {
4
4
  listMutes,
5
5
  muteMember,
6
6
  unmuteMember
7
- } from "./chunk-WJJ5MBH5.js";
7
+ } from "./chunk-YEOQJ7WW.js";
8
8
  import "./chunk-U4QCCLAW.js";
9
9
  import "./chunk-SBCVAC2Z.js";
10
10
  import "./chunk-ZCINJSS4.js";
@@ -18,4 +18,4 @@ export {
18
18
  muteMember,
19
19
  unmuteMember
20
20
  };
21
- //# sourceMappingURL=mutes-MNQP6ACF.js.map
21
+ //# sourceMappingURL=mutes-PQA6U5X7.js.map
@@ -5,7 +5,7 @@ import {
5
5
  recordDigestSent,
6
6
  registerNotificationKind,
7
7
  setMemberNotificationPrefs
8
- } from "./chunk-CAS4Z6IN.js";
8
+ } from "./chunk-I4FSVEJK.js";
9
9
  import "./chunk-ZCINJSS4.js";
10
10
  import "./chunk-XANPEOJC.js";
11
11
  import "./chunk-X7K5F2UI.js";
@@ -18,4 +18,4 @@ export {
18
18
  registerNotificationKind,
19
19
  setMemberNotificationPrefs
20
20
  };
21
- //# sourceMappingURL=notification-prefs-H4HFVCL7.js.map
21
+ //# sourceMappingURL=notification-prefs-62NX2GBF.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  verifyStartupSafety
3
- } from "./chunk-B7DTNT4O.js";
3
+ } from "./chunk-MWLSXK6Y.js";
4
4
  import {
5
5
  getErrorReporter,
6
6
  noopErrorReporter,
@@ -14,7 +14,7 @@ import {
14
14
  getScopedLogger,
15
15
  resetLogger,
16
16
  setLogger
17
- } from "./chunk-NFHS7CFV.js";
17
+ } from "./chunk-Q7MK5ZKG.js";
18
18
  import "./chunk-PZ5AY32C.js";
19
19
  export {
20
20
  consoleLogger,
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  applyReputation
3
- } from "./chunk-L6VG7IK6.js";
4
- import "./chunk-NFHS7CFV.js";
3
+ } from "./chunk-VBVLYFSZ.js";
4
+ import "./chunk-Q7MK5ZKG.js";
5
5
  import "./chunk-XANPEOJC.js";
6
6
  import "./chunk-X7K5F2UI.js";
7
7
  import "./chunk-PZ5AY32C.js";
8
8
  export {
9
9
  applyReputation
10
10
  };
11
- //# sourceMappingURL=reputation-ICIXDGPM.js.map
11
+ //# sourceMappingURL=reputation-5DJLDBZY.js.map
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  publishScheduledDocuments
3
- } from "./chunk-7KGF7JVJ.js";
4
- import "./chunk-WRDIRDH7.js";
5
- import "./chunk-2VZZ7M26.js";
3
+ } from "./chunk-PPUHXOWZ.js";
4
+ import "./chunk-CD74WQK7.js";
5
+ import "./chunk-EAYUAXW3.js";
6
6
  import "./chunk-EFZH6UPY.js";
7
7
  import "./chunk-4ZLMEKFX.js";
8
8
  import "./chunk-U4QCCLAW.js";
@@ -11,11 +11,11 @@ import "./chunk-ZCINJSS4.js";
11
11
  import "./chunk-V2UNHGAP.js";
12
12
  import "./chunk-WV272MPW.js";
13
13
  import "./chunk-OROPGO65.js";
14
- import "./chunk-NFHS7CFV.js";
14
+ import "./chunk-Q7MK5ZKG.js";
15
15
  import "./chunk-XANPEOJC.js";
16
16
  import "./chunk-X7K5F2UI.js";
17
17
  import "./chunk-PZ5AY32C.js";
18
18
  export {
19
19
  publishScheduledDocuments
20
20
  };
21
- //# sourceMappingURL=scheduled-T5WZ4I6O.js.map
21
+ //# sourceMappingURL=scheduled-PF2HECSF.js.map
package/dist/seo.js CHANGED
@@ -12,9 +12,9 @@ import {
12
12
  renderSitemapIndexXml,
13
13
  renderSitemapXml,
14
14
  validateSeoSettingsPatch
15
- } from "./chunk-YYPSQMFY.js";
16
- import "./chunk-WRDIRDH7.js";
17
- import "./chunk-2VZZ7M26.js";
15
+ } from "./chunk-VX3HM5TF.js";
16
+ import "./chunk-CD74WQK7.js";
17
+ import "./chunk-EAYUAXW3.js";
18
18
  import "./chunk-EFZH6UPY.js";
19
19
  import "./chunk-4ZLMEKFX.js";
20
20
  import "./chunk-U4QCCLAW.js";
@@ -23,7 +23,7 @@ import "./chunk-ZCINJSS4.js";
23
23
  import "./chunk-V2UNHGAP.js";
24
24
  import "./chunk-WV272MPW.js";
25
25
  import "./chunk-OROPGO65.js";
26
- import "./chunk-NFHS7CFV.js";
26
+ import "./chunk-Q7MK5ZKG.js";
27
27
  import "./chunk-XANPEOJC.js";
28
28
  import "./chunk-X7K5F2UI.js";
29
29
  import "./chunk-PZ5AY32C.js";
@@ -3,7 +3,7 @@ import {
3
3
  getCommunitySettings,
4
4
  updateCommunitySettings,
5
5
  validateCommunitySettingsPatch
6
- } from "./chunk-RKM4GDWM.js";
6
+ } from "./chunk-6MRTH734.js";
7
7
  import "./chunk-ZCINJSS4.js";
8
8
  import "./chunk-XANPEOJC.js";
9
9
  import "./chunk-X7K5F2UI.js";
@@ -14,4 +14,4 @@ export {
14
14
  updateCommunitySettings,
15
15
  validateCommunitySettingsPatch
16
16
  };
17
- //# sourceMappingURL=settings-OZWM6L2K.js.map
17
+ //# sourceMappingURL=settings-NBAP7E5E.js.map
@@ -7,12 +7,12 @@ import {
7
7
  setStrings,
8
8
  t,
9
9
  tSync
10
- } from "./chunk-ELK6AVW5.js";
10
+ } from "./chunk-2X3GBJOT.js";
11
11
  import "./chunk-4ZLMEKFX.js";
12
12
  import "./chunk-U4QCCLAW.js";
13
13
  import "./chunk-SBCVAC2Z.js";
14
14
  import "./chunk-ZCINJSS4.js";
15
- import "./chunk-NFHS7CFV.js";
15
+ import "./chunk-Q7MK5ZKG.js";
16
16
  import "./chunk-XANPEOJC.js";
17
17
  import "./chunk-X7K5F2UI.js";
18
18
  import "./chunk-PZ5AY32C.js";
@@ -26,4 +26,4 @@ export {
26
26
  t,
27
27
  tSync
28
28
  };
29
- //# sourceMappingURL=strings-4EWJYDOG.js.map
29
+ //# sourceMappingURL=strings-O2M7VSKV.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexpress/core",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Server-side core for NexPress — collections pipeline, auth, jobs, media, plugins, observability.",
5
5
  "license": "MIT",
6
6
  "author": "Nexpress",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/media/processor.ts","../src/media/service.ts"],"sourcesContent":["import sharp from \"sharp\";\n\nimport type { NpImageSize } from \"../config/types.js\";\n\nexport interface NpProcessedImageVariant {\n name: string;\n buffer: Buffer;\n width: number;\n height: number;\n size: number;\n}\n\nexport interface NpProcessedImageSourceMetadata {\n width: number | null;\n height: number | null;\n format: string | null;\n}\n\nexport interface NpProcessedImageResult {\n source: NpProcessedImageSourceMetadata;\n variants: NpProcessedImageVariant[];\n}\n\nexport const DEFAULT_IMAGE_SIZES: NpImageSize[] = [\n { name: \"thumbnail\", width: 300 },\n { name: \"small\", width: 600 },\n { name: \"medium\", width: 900 },\n { name: \"large\", width: 1400 },\n { name: \"xlarge\", width: 1920 },\n { name: \"og\", width: 1200, height: 630, crop: \"center\" },\n];\n\nexport async function processImage(\n inputBuffer: Buffer,\n sizes: NpImageSize[],\n options: { format?: string; quality?: number } = {},\n): Promise<NpProcessedImageResult> {\n const format = options.format ?? \"webp\";\n const quality = options.quality ?? 80;\n const sourceImage = sharp(inputBuffer).autoOrient();\n const metadata = await sourceImage.metadata();\n\n const variants = await Promise.all(\n sizes.map(async (size) => {\n const resized = size.height\n ? sourceImage.clone().resize({\n width: size.width,\n height: size.height,\n fit: \"cover\",\n position: resolveCropPosition(size.crop),\n })\n : sourceImage.clone().resize({\n width: size.width,\n fit: \"inside\",\n withoutEnlargement: true,\n });\n\n const formatted = applyFormat(resized, format, quality);\n const { data, info } = await formatted.toBuffer({ resolveWithObject: true });\n\n return {\n name: size.name,\n buffer: data,\n width: info.width,\n height: info.height,\n size: info.size ?? data.byteLength,\n };\n }),\n );\n\n return {\n source: {\n width: metadata.width ?? null,\n height: metadata.height ?? null,\n format: metadata.format ?? null,\n },\n variants,\n };\n}\n\nfunction applyFormat(\n image: sharp.Sharp,\n format: string,\n quality: number,\n): sharp.Sharp {\n switch (format) {\n case \"avif\":\n return image.avif({ quality });\n case \"jpeg\":\n return image.jpeg({ quality });\n case \"png\":\n return image.png({ quality });\n case \"webp\":\n default:\n return image.webp({ quality });\n }\n}\n\nfunction resolveCropPosition(crop?: NpImageSize[\"crop\"]): sharp.Gravity | number {\n switch (crop) {\n case \"top\":\n return \"top\";\n case \"bottom\":\n return \"bottom\";\n case \"left\":\n return \"left\";\n case \"right\":\n return \"right\";\n case \"center\":\n return \"centre\";\n default:\n return sharp.strategy.attention;\n }\n}\n","import { createHash, randomUUID } from \"node:crypto\";\nimport { extname } from \"node:path\";\nimport { buffer as consumeBuffer } from \"node:stream/consumers\";\nimport { Readable } from \"node:stream\";\n\nimport { and, count, desc, eq, gte, ilike, inArray, isNotNull, isNull, lt, or, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\n\nimport type { NpFindResult, NpImageSize } from \"../config/types.js\";\nimport { readEnvPositiveInt } from \"../config/env.js\";\nimport { npMembers } from \"../db/schema/community.js\";\nimport { npMedia, npMediaRefs } from \"../db/schema/media.js\";\nimport { npUsers } from \"../db/schema/system.js\";\nimport { enqueueJob } from \"../jobs/queue.js\";\nimport { getLogger } from \"../observability/logger.js\";\nimport { getDb } from \"../db/runtime.js\";\nimport {\n DEFAULT_IMAGE_SIZES,\n processImage,\n type NpProcessedImageResult,\n} from \"./processor.js\";\nimport type { NpStorageAdapter } from \"../storage/types.js\";\n\n/**\n * Trailing-window for member upload quotas (`perDay` in\n * `npMemberUploadQuota`). Default 24h matches the historical\n * \"daily quota\" semantics; override via\n * `NP_MEMBER_QUOTA_WINDOW_HOURS` to shift to weekly or hourly\n * caps without touching code.\n */\nconst MEMBER_QUOTA_WINDOW_MS =\n readEnvPositiveInt(\"NP_MEMBER_QUOTA_WINDOW_HOURS\", 24) * 60 * 60 * 1000;\n\ninterface SelectQuery extends Promise<unknown[]> {\n where(condition: ReturnType<typeof and> | ReturnType<typeof isNull>): SelectQuery;\n orderBy(order: ReturnType<typeof desc>): SelectQuery;\n limit(limit: number): SelectQuery;\n offset(offset: number): SelectQuery;\n}\n\ninterface InsertValuesQuery extends Promise<unknown> {\n returning(): Promise<unknown[]>;\n}\n\ninterface DrizzleDatabaseLike {\n insert(table: PgTable): {\n values(values: Record<string, unknown> | Record<string, unknown>[]): InsertValuesQuery;\n };\n update(table: PgTable): {\n set(values: Record<string, unknown>): {\n where(condition: ReturnType<typeof and> | ReturnType<typeof eq>): {\n returning(): Promise<unknown[]>;\n };\n };\n };\n delete(table: PgTable): {\n where(condition: ReturnType<typeof inArray> ): Promise<unknown>;\n };\n select(selection?: Record<string, unknown>): {\n from(table: PgTable): SelectQuery;\n };\n}\n\ninterface MediaRecord {\n id: string;\n filename: string;\n originalFilename: string;\n mimeType: string;\n filesize: number;\n width: number | null;\n height: number | null;\n sizes: Record<string, Record<string, unknown>> | null;\n storageKey: string;\n hash: string;\n status: \"processing\" | \"ready\" | \"error\";\n folderId: string | null;\n uploadedBy: string | null;\n createdAt: Date;\n updatedAt: Date;\n deletedAt: Date | null;\n}\n\nlet storageAdapter: NpStorageAdapter | null = null;\n\nexport function setStorageAdapter(adapter: NpStorageAdapter): void {\n storageAdapter = adapter;\n}\n\nexport function getStorageAdapter(): NpStorageAdapter {\n if (!storageAdapter) {\n throw new Error(\"Storage adapter not initialized. Call setStorageAdapter() first.\");\n }\n\n return storageAdapter;\n}\n\n/**\n * Polymorphic uploader: a row on `np_media` is owned by exactly\n * one of staff (`uploadedBy` → `np_users.id`) or member\n * (`uploadedByMemberId` → `np_members.id`, Phase 9.7j). Pass a\n * `null` value as the second argument to `uploadMedia` for plugin /\n * system uploads with no human owner — both columns stay null and\n * the audit log carries the actor.\n */\nexport type NpMediaUploader =\n | { kind: \"staff\"; userId: string }\n | { kind: \"member\"; memberId: string }\n | null;\n\nexport async function uploadMedia(\n file: { buffer: Buffer; originalFilename: string; mimeType: string },\n uploader: NpMediaUploader | string,\n folderId?: string,\n): Promise<{ id: string; status: string }> {\n // Backwards-compat: the original signature was\n // `uploadMedia(file, userId: string | null, folderId?)`. Existing\n // callers (plugin context, admin bulk uploads, etc.) pass a bare\n // string. Coerce that into the staff variant of the polymorphic\n // shape so the rest of this function only deals with the union.\n const resolvedUploader: NpMediaUploader =\n typeof uploader === \"string\"\n ? { kind: \"staff\", userId: uploader }\n : uploader;\n\n const id = randomUUID();\n const extension = resolveFileExtension(file.originalFilename, file.mimeType);\n const storageKey = `media/${id}/original.${extension}`;\n const now = new Date();\n const insertValues = {\n id,\n filename: file.originalFilename,\n originalFilename: file.originalFilename,\n mimeType: file.mimeType,\n filesize: file.buffer.byteLength,\n storageKey,\n hash: createHash(\"sha256\").update(file.buffer).digest(\"hex\"),\n status: \"processing\" as const,\n folderId,\n uploadedBy:\n resolvedUploader && resolvedUploader.kind === \"staff\"\n ? resolvedUploader.userId\n : null,\n uploadedByMemberId:\n resolvedUploader && resolvedUploader.kind === \"member\"\n ? resolvedUploader.memberId\n : null,\n createdAt: now,\n updatedAt: now,\n };\n\n // Phase 9.7p: per-member upload quota. Staff uploads are never\n // gated. Phase 9.7p-followup (#120) — the count + insert must be\n // atomic per member, otherwise concurrent uploads can both\n // observe the same pre-insert count and both succeed past the\n // cap. Wrap the gated branch in a transaction holding a Postgres\n // advisory lock keyed on the member id; cross-member uploaders\n // don't contend (different lock keys), same-member concurrent\n // uploaders serialize and the second one sees the updated\n // count.\n //\n // Storage upload happens AFTER the DB row commits so the quota\n // count is correct before bytes touch storage. If the upload\n // fails (#138 follow-up), we hard-delete the just-inserted row\n // so it stops counting against quota and doesn't strand the\n // member with a permanent ghost. We do NOT just mark the row\n // `error` here — there's no storage object to inspect, no\n // processor will arrive (the job hasn't been enqueued yet),\n // and the quota count filters by `deletedAt IS NULL`, not\n // `status`. Hard delete is the right semantic.\n if (resolvedUploader && resolvedUploader.kind === \"member\") {\n const memberId = resolvedUploader.memberId;\n const dbPg = getDb() as unknown as NodePgDatabase<Record<string, unknown>>;\n await dbPg.transaction(async (tx) => {\n // `pg_advisory_xact_lock` auto-releases on commit/rollback.\n // `hashtextextended` produces a stable int8 from a UUID\n // string — collisions across different member ids are\n // benign (worst case some unrelated members serialize).\n await tx.execute(\n sql`SELECT pg_advisory_xact_lock(hashtextextended(${memberId}, 0))`,\n );\n await assertMemberUploadQuota(memberId, tx);\n await tx.insert(npMedia).values(insertValues);\n });\n } else {\n const db = getDb() as unknown as DrizzleDatabaseLike;\n await db.insert(npMedia).values(insertValues);\n }\n\n const adapter = getStorageAdapter();\n try {\n await adapter.upload(storageKey, file.buffer, {\n contentType: file.mimeType,\n contentLength: file.buffer.byteLength,\n originalFilename: file.originalFilename,\n });\n } catch (err) {\n // Storage failed after the DB row committed. Roll the row\n // back so it doesn't (a) eat the member's quota allowance\n // for nothing, (b) confuse operators with a permanent\n // `processing` row that never gets a job. Cleanup is\n // best-effort — if the delete itself fails we still surface\n // the original storage error to the caller, since that's\n // what they need to act on.\n try {\n const cleanupDb = getDb() as unknown as DrizzleDatabaseLike;\n await cleanupDb.delete(npMedia).where(eq(npMedia.id, id));\n } catch (cleanupErr) {\n // Swallow so the original storage error reaches the\n // caller — that's what they need to act on. But don't go\n // silent: a failed cleanup leaves a permanent ghost row\n // in `processing` that eats the member's quota with no\n // storage object to inspect and no job ever enqueued.\n // Operators need a signal to find and remediate it.\n getLogger().error(\"media upload cleanup failed\", {\n mediaId: id,\n storageKey,\n error: cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr),\n });\n }\n throw err;\n }\n\n await enqueueJob(\"media:processImage\", { mediaId: id });\n\n return { id, status: \"processing\" };\n}\n\n/**\n * Throws `NpRateLimitError` (429) if the member is at or over\n * their per-day or lifetime upload cap. Both bounds count\n * non-deleted rows, so admin / member deletes free up quota the\n * same way (mirrors the 9.7l purge semantic). When both bounds\n * are `null` (the default), this function is a no-op aside from\n * a single settings read.\n *\n * Defer-loaded `getCommunitySettings` to avoid an import cycle\n * with `community/settings.ts` — that module reads `getDb()`,\n * which is wired by the same bootstrap that wires the media DB,\n * so they sit on the same module layer; deferring keeps a clean\n * one-way edge from media → community for this single call site.\n */\nasync function assertMemberUploadQuota(\n memberId: string,\n txDb?: NodePgDatabase<Record<string, unknown>>,\n): Promise<void> {\n const { getCommunitySettings } = await import(\n \"../community/settings.js\"\n );\n const { NpRateLimitError } = await import(\"../errors.js\");\n const settings = await getCommunitySettings();\n const { perDay, total } = settings.memberUploadQuota;\n if (perDay === null && total === null) return;\n\n // When invoked inside the upload transaction (#120 fix), the\n // count + downstream insert run under the same advisory lock,\n // so the count must use the tx handle to see writes by sibling\n // statements. When called from elsewhere we fall back to the\n // shared media DB.\n const db =\n txDb ??\n (getDb() as unknown as NodePgDatabase<Record<string, unknown>>);\n\n if (total !== null) {\n const [row] = (await db\n .select({ value: count() })\n .from(npMedia)\n .where(\n and(\n eq(npMedia.uploadedByMemberId, memberId),\n isNull(npMedia.deletedAt),\n ),\n )) as Array<{ value: number }>;\n const used = row?.value ?? 0;\n if (used >= total) {\n throw new NpRateLimitError(\n `Upload quota exceeded — this account has reached its lifetime cap of ${total} uploads.`,\n );\n }\n }\n\n if (perDay !== null) {\n const since = new Date(Date.now() - MEMBER_QUOTA_WINDOW_MS);\n const [row] = (await db\n .select({ value: count() })\n .from(npMedia)\n .where(\n and(\n eq(npMedia.uploadedByMemberId, memberId),\n isNull(npMedia.deletedAt),\n gte(npMedia.createdAt, since),\n ),\n )) as Array<{ value: number }>;\n const recent = row?.value ?? 0;\n if (recent >= perDay) {\n throw new NpRateLimitError(\n `Upload rate limit exceeded — try again later (max ${perDay} uploads per 24 hours).`,\n );\n }\n }\n}\n\nexport async function processMediaImage(\n mediaId: string,\n config: { sizes?: NpImageSize[]; format?: string; quality?: number },\n): Promise<void> {\n const db = getDb() as unknown as DrizzleDatabaseLike;\n const adapter = getStorageAdapter();\n const media = await getMediaRecordById(mediaId);\n\n if (!media) {\n throw new Error(`Media '${mediaId}' not found.`);\n }\n\n try {\n const originalStream = await adapter.getStream(media.storageKey);\n const originalBuffer = await consumeBuffer(Readable.fromWeb(originalStream));\n const processed = await processImage(\n originalBuffer,\n config.sizes ?? DEFAULT_IMAGE_SIZES,\n { format: config.format, quality: config.quality },\n );\n const format = config.format ?? \"webp\";\n const mimeType = getFormatMimeType(format);\n const sizes = await uploadImageVariants(adapter, media.id, processed, format, mimeType);\n\n await db\n .update(npMedia)\n .set({\n sizes,\n width: processed.source.width,\n height: processed.source.height,\n status: \"ready\",\n updatedAt: new Date(),\n })\n .where(eq(npMedia.id, media.id))\n .returning();\n } catch (error) {\n await db\n .update(npMedia)\n .set({\n status: \"error\",\n updatedAt: new Date(),\n })\n .where(eq(npMedia.id, media.id))\n .returning();\n\n throw error;\n }\n}\n\nexport async function getMediaById(id: string): Promise<Record<string, unknown> | null> {\n const db = getDb() as unknown as DrizzleDatabaseLike;\n const [media] = await db\n .select()\n .from(npMedia)\n .where(and(eq(npMedia.id, id), isNull(npMedia.deletedAt)))\n .limit(1);\n\n return media ? toRecord(media) : null;\n}\n\nexport async function deleteMedia(\n id: string,\n): Promise<{ deleted: boolean; references?: unknown[] }> {\n const db = getDb() as unknown as DrizzleDatabaseLike;\n const references = await db.select().from(npMediaRefs).where(eq(npMediaRefs.mediaId, id));\n\n if (references.length > 0) {\n return { deleted: false, references };\n }\n\n const [media] = await db\n .select()\n .from(npMedia)\n .where(and(eq(npMedia.id, id), isNull(npMedia.deletedAt)))\n .limit(1);\n\n if (!media) {\n return { deleted: false };\n }\n\n await db\n .update(npMedia)\n .set({\n deletedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(eq(npMedia.id, id))\n .returning();\n\n return { deleted: true };\n}\n\n/**\n * Phase 9.7k uploader filters. `uploaderKind` partitions the\n * library into staff-uploaded rows (`uploaded_by IS NOT NULL`) vs\n * member-uploaded rows (`uploaded_by_member_id IS NOT NULL`) — the\n * two columns are mutually exclusive on every row written through\n * `uploadMedia`. `uploadedByMemberId` narrows to a specific member\n * for \"show me everything @handle uploaded\" investigations after a\n * spam wave.\n */\nexport type NpMediaUploaderKindFilter = \"staff\" | \"member\";\n\nexport async function listMedia(options: {\n page?: number;\n limit?: number;\n folderId?: string;\n mimeType?: string;\n uploaderKind?: NpMediaUploaderKindFilter;\n uploadedByMemberId?: string;\n /**\n * Substring match against `filename` and `alt`. Matches\n * server-side via `ILIKE`, so the page-builder block-image\n * picker can search the whole library without paging through\n * every result client-side. Empty / whitespace-only `q` is\n * treated as no filter.\n */\n q?: string;\n}): Promise<NpFindResult> {\n const db = getDb() as unknown as DrizzleDatabaseLike;\n const page = normalizePage(options.page);\n const limit = normalizeLimit(options.limit);\n const offset = (page - 1) * limit;\n const conditions = [isNull(npMedia.deletedAt)];\n\n if (options.folderId) {\n conditions.push(eq(npMedia.folderId, options.folderId));\n }\n\n if (options.mimeType) {\n conditions.push(eq(npMedia.mimeType, options.mimeType));\n }\n\n if (options.uploaderKind === \"staff\") {\n conditions.push(isNotNull(npMedia.uploadedBy));\n } else if (options.uploaderKind === \"member\") {\n conditions.push(isNotNull(npMedia.uploadedByMemberId));\n }\n\n if (options.uploadedByMemberId) {\n conditions.push(eq(npMedia.uploadedByMemberId, options.uploadedByMemberId));\n }\n\n // Substring search across filename + alt. We match `ILIKE\n // %q%` against both columns and OR them so the picker's\n // search box hits filenames the operator remembers and alt\n // text they wrote. SQL escapes the literal `%` / `_` chars\n // by doubling them so a filename containing them isn't\n // treated as a wildcard.\n if (options.q && options.q.trim().length > 0) {\n const needle = `%${options.q.trim().replace(/[%_]/g, (c) => `\\\\${c}`)}%`;\n const search = or(\n ilike(npMedia.filename, needle),\n ilike(npMedia.alt, needle),\n );\n if (search) conditions.push(search);\n }\n\n const whereClause = combineConditions(conditions);\n // The local `DrizzleDatabaseLike` interface in this file is\n // narrow on purpose (only `select/insert/update/delete`); a\n // proper leftJoin chain would require typing the full Drizzle\n // builder pipeline. Cast through `unknown` for this query —\n // safer than widening the interface and dragging join semantics\n // into every other media call site.\n const joined = (db as unknown as {\n select: (s: Record<string, unknown>) => {\n from: (t: PgTable) => {\n leftJoin: (j: PgTable, c: unknown) => {\n leftJoin: (j: PgTable, c: unknown) => {\n where: (c: unknown) => {\n orderBy: (o: unknown) => {\n limit: (n: number) => {\n offset: (n: number) => Promise<Array<Record<string, unknown>>>;\n };\n };\n };\n };\n };\n };\n };\n })\n .select({\n media: npMedia,\n userName: npUsers.name,\n userEmail: npUsers.email,\n memberHandle: npMembers.handle,\n memberDisplayName: npMembers.displayName,\n })\n .from(npMedia)\n .leftJoin(npUsers, eq(npMedia.uploadedBy, npUsers.id))\n .leftJoin(npMembers, eq(npMedia.uploadedByMemberId, npMembers.id))\n .where(whereClause)\n .orderBy(desc(npMedia.createdAt))\n .limit(limit)\n .offset(offset);\n\n const rows = (await joined) as Array<{\n media: Record<string, unknown>;\n userName: string | null;\n userEmail: string | null;\n memberHandle: string | null;\n memberDisplayName: string | null;\n }>;\n const [{ total }] = (whereClause\n ? await db.select({ total: count() }).from(npMedia).where(whereClause)\n : await db.select({ total: count() }).from(npMedia)) as Array<{ total: number | string }>;\n const totalDocs = Number(total ?? 0);\n const totalPages = totalDocs === 0 ? 0 : Math.ceil(totalDocs / limit);\n\n // Flatten the JOIN result so each doc carries an `uploader`\n // sub-object alongside the standard media columns. Keeps the\n // shape backwards-compatible (the existing media columns are\n // still at the top level).\n const docs = rows.map((row) => ({\n ...row.media,\n uploader: row.userName !== null\n ? {\n kind: \"staff\" as const,\n name: row.userName,\n email: row.userEmail,\n }\n : row.memberHandle !== null\n ? {\n kind: \"member\" as const,\n handle: row.memberHandle,\n displayName: row.memberDisplayName,\n }\n : null,\n }));\n\n return {\n docs: docs as Record<string, unknown>[],\n totalDocs,\n totalPages,\n page,\n limit,\n hasNextPage: page < totalPages,\n hasPrevPage: page > 1 && totalDocs > 0,\n };\n}\n\nexport async function cleanupDeletedMedia(olderThanDays: number): Promise<number> {\n const db = getDb() as unknown as DrizzleDatabaseLike;\n const adapter = getStorageAdapter();\n const threshold = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);\n const rows = await db\n .select()\n .from(npMedia)\n .where(and(isNotNull(npMedia.deletedAt), lt(npMedia.deletedAt, threshold)));\n const mediaRows = rows.map(toMediaRecord);\n\n if (mediaRows.length === 0) {\n return 0;\n }\n\n for (const media of mediaRows) {\n const keys = new Set<string>([\n media.storageKey,\n ...extractVariantStorageKeys(media.sizes),\n ]);\n\n for (const key of keys) {\n try {\n await adapter.delete(key);\n } catch {\n continue;\n }\n }\n }\n\n await db.delete(npMedia).where(inArray(npMedia.id, mediaRows.map((media) => media.id)));\n\n return mediaRows.length;\n}\n\nasync function getMediaRecordById(id: string): Promise<MediaRecord | null> {\n const db = getDb() as unknown as DrizzleDatabaseLike;\n const [media] = await db\n .select()\n .from(npMedia)\n .where(and(eq(npMedia.id, id), isNull(npMedia.deletedAt)))\n .limit(1);\n\n return media ? toMediaRecord(media) : null;\n}\n\nasync function uploadImageVariants(\n adapter: NpStorageAdapter,\n mediaId: string,\n processed: NpProcessedImageResult,\n format: string,\n mimeType: string,\n): Promise<Record<string, Record<string, unknown>>> {\n const entries = await Promise.all(\n processed.variants.map(async (variant) => {\n const filename = `${variant.name}.${format}`;\n const storageKey = `media/${mediaId}/${filename}`;\n\n await adapter.upload(storageKey, variant.buffer, {\n contentType: mimeType,\n contentLength: variant.size,\n originalFilename: filename,\n });\n\n return [\n variant.name,\n {\n filename,\n mimeType,\n filesize: variant.size,\n width: variant.width,\n height: variant.height,\n storageKey,\n url: await adapter.getUrl(storageKey),\n },\n ] as const;\n }),\n );\n\n return Object.fromEntries(entries);\n}\n\nfunction extractVariantStorageKeys(\n sizes: Record<string, Record<string, unknown>> | null,\n): string[] {\n if (!sizes) {\n return [];\n }\n\n return Object.values(sizes)\n .map((size) => size.storageKey)\n .filter((value): value is string => typeof value === \"string\" && value.length > 0);\n}\n\nfunction resolveFileExtension(originalFilename: string, mimeType: string): string {\n const extension = extname(originalFilename).slice(1).toLowerCase();\n\n if (extension) {\n return extension;\n }\n\n switch (mimeType) {\n case \"image/jpeg\":\n return \"jpg\";\n case \"image/png\":\n return \"png\";\n case \"image/webp\":\n return \"webp\";\n case \"image/avif\":\n return \"avif\";\n case \"image/gif\":\n return \"gif\";\n case \"application/pdf\":\n return \"pdf\";\n default:\n return \"bin\";\n }\n}\n\nfunction getFormatMimeType(format: string): string {\n switch (format) {\n case \"avif\":\n return \"image/avif\";\n case \"jpeg\":\n return \"image/jpeg\";\n case \"png\":\n return \"image/png\";\n case \"webp\":\n default:\n return \"image/webp\";\n }\n}\n\nfunction combineConditions(\n conditions: Array<ReturnType<typeof and> | ReturnType<typeof isNull> >,\n): ReturnType<typeof and> | ReturnType<typeof isNull> | undefined {\n if (conditions.length === 0) {\n return undefined;\n }\n\n if (conditions.length === 1) {\n return conditions[0];\n }\n\n return and(...conditions);\n}\n\nfunction normalizePage(page?: number): number {\n if (!page || page < 1) {\n return 1;\n }\n\n return Math.floor(page);\n}\n\nfunction normalizeLimit(limit?: number): number {\n if (!limit || limit < 1) {\n return 10;\n }\n\n return Math.floor(limit);\n}\n\nfunction toMediaRecord(value: unknown): MediaRecord {\n const record = toRecord(value);\n\n return {\n id: asString(record.id, \"id\"),\n filename: asString(record.filename, \"filename\"),\n originalFilename: asString(record.originalFilename, \"originalFilename\"),\n mimeType: asString(record.mimeType, \"mimeType\"),\n filesize: asNumber(record.filesize, \"filesize\"),\n width: asNullableNumber(record.width),\n height: asNullableNumber(record.height),\n sizes: asSizes(record.sizes),\n storageKey: asString(record.storageKey, \"storageKey\"),\n hash: asString(record.hash, \"hash\"),\n status: asMediaStatus(record.status),\n folderId: asNullableString(record.folderId),\n uploadedBy: asNullableString(record.uploadedBy),\n createdAt: asDate(record.createdAt, \"createdAt\"),\n updatedAt: asDate(record.updatedAt, \"updatedAt\"),\n deletedAt: asNullableDate(record.deletedAt),\n };\n}\n\nfunction asSizes(value: unknown): Record<string, Record<string, unknown>> | null {\n if (value == null) {\n return null;\n }\n\n const record = toRecord(value);\n const sizes: Record<string, Record<string, unknown>> = {};\n\n for (const [key, entry] of Object.entries(record)) {\n const sizeRecord = toRecord(entry);\n sizes[key] = sizeRecord;\n }\n\n return sizes;\n}\n\nfunction asMediaStatus(value: unknown): MediaRecord[\"status\"] {\n if (value === \"processing\" || value === \"ready\" || value === \"error\") {\n return value;\n }\n\n throw new Error(\"Invalid media status.\");\n}\n\nfunction asString(value: unknown, field: string): string {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new Error(`Invalid ${field}.`);\n }\n\n return value;\n}\n\nfunction asNullableString(value: unknown): string | null {\n if (value == null) {\n return null;\n }\n\n return asString(value, \"string field\");\n}\n\nfunction asNumber(value: unknown, field: string): number {\n if (typeof value !== \"number\") {\n throw new Error(`Invalid ${field}.`);\n }\n\n return value;\n}\n\nfunction asNullableNumber(value: unknown): number | null {\n if (value == null) {\n return null;\n }\n\n return asNumber(value, \"number field\");\n}\n\nfunction asDate(value: unknown, field: string): Date {\n if (!(value instanceof Date)) {\n throw new Error(`Invalid ${field}.`);\n }\n\n return value;\n}\n\nfunction asNullableDate(value: unknown): Date | null {\n if (value == null) {\n return null;\n }\n\n return asDate(value, \"date field\");\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new Error(\"Expected object record.\");\n }\n\n return value as Record<string, unknown>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,OAAO,WAAW;AAuBX,IAAM,sBAAqC;AAAA,EAChD,EAAE,MAAM,aAAa,OAAO,IAAI;AAAA,EAChC,EAAE,MAAM,SAAS,OAAO,IAAI;AAAA,EAC5B,EAAE,MAAM,UAAU,OAAO,IAAI;AAAA,EAC7B,EAAE,MAAM,SAAS,OAAO,KAAK;AAAA,EAC7B,EAAE,MAAM,UAAU,OAAO,KAAK;AAAA,EAC9B,EAAE,MAAM,MAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,SAAS;AACzD;AAEA,eAAsB,aACpB,aACA,OACA,UAAiD,CAAC,GACjB;AACjC,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,cAAc,MAAM,WAAW,EAAE,WAAW;AAClD,QAAM,WAAW,MAAM,YAAY,SAAS;AAE5C,QAAM,WAAW,MAAM,QAAQ;AAAA,IAC7B,MAAM,IAAI,OAAO,SAAS;AACxB,YAAM,UAAU,KAAK,SACjB,YAAY,MAAM,EAAE,OAAO;AAAA,QACzB,OAAO,KAAK;AAAA,QACZ,QAAQ,KAAK;AAAA,QACb,KAAK;AAAA,QACL,UAAU,oBAAoB,KAAK,IAAI;AAAA,MACzC,CAAC,IACD,YAAY,MAAM,EAAE,OAAO;AAAA,QACzB,OAAO,KAAK;AAAA,QACZ,KAAK;AAAA,QACL,oBAAoB;AAAA,MACtB,CAAC;AAEL,YAAM,YAAY,YAAY,SAAS,QAAQ,OAAO;AACtD,YAAM,EAAE,MAAM,KAAK,IAAI,MAAM,UAAU,SAAS,EAAE,mBAAmB,KAAK,CAAC;AAE3E,aAAO;AAAA,QACL,MAAM,KAAK;AAAA,QACX,QAAQ;AAAA,QACR,OAAO,KAAK;AAAA,QACZ,QAAQ,KAAK;AAAA,QACb,MAAM,KAAK,QAAQ,KAAK;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,OAAO,SAAS,SAAS;AAAA,MACzB,QAAQ,SAAS,UAAU;AAAA,MAC3B,QAAQ,SAAS,UAAU;AAAA,IAC7B;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,YACP,OACA,QACA,SACa;AACb,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO,MAAM,KAAK,EAAE,QAAQ,CAAC;AAAA,IAC/B,KAAK;AACH,aAAO,MAAM,KAAK,EAAE,QAAQ,CAAC;AAAA,IAC/B,KAAK;AACH,aAAO,MAAM,IAAI,EAAE,QAAQ,CAAC;AAAA,IAC9B,KAAK;AAAA,IACL;AACE,aAAO,MAAM,KAAK,EAAE,QAAQ,CAAC;AAAA,EACjC;AACF;AAEA,SAAS,oBAAoB,MAAoD;AAC/E,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO,MAAM,SAAS;AAAA,EAC1B;AACF;;;ACjHA,SAAS,YAAY,kBAAkB;AACvC,SAAS,eAAe;AACxB,SAAS,UAAU,qBAAqB;AACxC,SAAS,gBAAgB;AAEzB,SAAS,KAAK,OAAO,MAAM,IAAI,KAAK,OAAO,SAAS,WAAW,QAAQ,IAAI,IAAI,WAAW;AA0B1F,IAAM,yBACJ,mBAAmB,gCAAgC,EAAE,IAAI,KAAK,KAAK;AAmDrE,IAAI,iBAA0C;AAEvC,SAAS,kBAAkB,SAAiC;AACjE,mBAAiB;AACnB;AAEO,SAAS,oBAAsC;AACpD,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,MAAM,kEAAkE;AAAA,EACpF;AAEA,SAAO;AACT;AAeA,eAAsB,YACpB,MACA,UACA,UACyC;AAMzC,QAAM,mBACJ,OAAO,aAAa,WAChB,EAAE,MAAM,SAAS,QAAQ,SAAS,IAClC;AAEN,QAAM,KAAK,WAAW;AACtB,QAAM,YAAY,qBAAqB,KAAK,kBAAkB,KAAK,QAAQ;AAC3E,QAAM,aAAa,SAAS,EAAE,aAAa,SAAS;AACpD,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,eAAe;AAAA,IACnB;AAAA,IACA,UAAU,KAAK;AAAA,IACf,kBAAkB,KAAK;AAAA,IACvB,UAAU,KAAK;AAAA,IACf,UAAU,KAAK,OAAO;AAAA,IACtB;AAAA,IACA,MAAM,WAAW,QAAQ,EAAE,OAAO,KAAK,MAAM,EAAE,OAAO,KAAK;AAAA,IAC3D,QAAQ;AAAA,IACR;AAAA,IACA,YACE,oBAAoB,iBAAiB,SAAS,UAC1C,iBAAiB,SACjB;AAAA,IACN,oBACE,oBAAoB,iBAAiB,SAAS,WAC1C,iBAAiB,WACjB;AAAA,IACN,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AAqBA,MAAI,oBAAoB,iBAAiB,SAAS,UAAU;AAC1D,UAAM,WAAW,iBAAiB;AAClC,UAAM,OAAO,MAAM;AACnB,UAAM,KAAK,YAAY,OAAO,OAAO;AAKnC,YAAM,GAAG;AAAA,QACP,oDAAoD,QAAQ;AAAA,MAC9D;AACA,YAAM,wBAAwB,UAAU,EAAE;AAC1C,YAAM,GAAG,OAAO,OAAO,EAAE,OAAO,YAAY;AAAA,IAC9C,CAAC;AAAA,EACH,OAAO;AACL,UAAM,KAAK,MAAM;AACjB,UAAM,GAAG,OAAO,OAAO,EAAE,OAAO,YAAY;AAAA,EAC9C;AAEA,QAAM,UAAU,kBAAkB;AAClC,MAAI;AACF,UAAM,QAAQ,OAAO,YAAY,KAAK,QAAQ;AAAA,MAC5C,aAAa,KAAK;AAAA,MAClB,eAAe,KAAK,OAAO;AAAA,MAC3B,kBAAkB,KAAK;AAAA,IACzB,CAAC;AAAA,EACH,SAAS,KAAK;AAQZ,QAAI;AACF,YAAM,YAAY,MAAM;AACxB,YAAM,UAAU,OAAO,OAAO,EAAE,MAAM,GAAG,QAAQ,IAAI,EAAE,CAAC;AAAA,IAC1D,SAAS,YAAY;AAOnB,gBAAU,EAAE,MAAM,+BAA+B;AAAA,QAC/C,SAAS;AAAA,QACT;AAAA,QACA,OAAO,sBAAsB,QAAQ,WAAW,UAAU,OAAO,UAAU;AAAA,MAC7E,CAAC;AAAA,IACH;AACA,UAAM;AAAA,EACR;AAEA,QAAM,WAAW,sBAAsB,EAAE,SAAS,GAAG,CAAC;AAEtD,SAAO,EAAE,IAAI,QAAQ,aAAa;AACpC;AAgBA,eAAe,wBACb,UACA,MACe;AACf,QAAM,EAAE,qBAAqB,IAAI,MAAM,OACrC,wBACF;AACA,QAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,sBAAc;AACxD,QAAM,WAAW,MAAM,qBAAqB;AAC5C,QAAM,EAAE,QAAQ,MAAM,IAAI,SAAS;AACnC,MAAI,WAAW,QAAQ,UAAU,KAAM;AAOvC,QAAM,KACJ,QACC,MAAM;AAET,MAAI,UAAU,MAAM;AAClB,UAAM,CAAC,GAAG,IAAK,MAAM,GAClB,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC,EACzB,KAAK,OAAO,EACZ;AAAA,MACC;AAAA,QACE,GAAG,QAAQ,oBAAoB,QAAQ;AAAA,QACvC,OAAO,QAAQ,SAAS;AAAA,MAC1B;AAAA,IACF;AACF,UAAM,OAAO,KAAK,SAAS;AAC3B,QAAI,QAAQ,OAAO;AACjB,YAAM,IAAI;AAAA,QACR,6EAAwE,KAAK;AAAA,MAC/E;AAAA,IACF;AAAA,EACF;AAEA,MAAI,WAAW,MAAM;AACnB,UAAM,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,sBAAsB;AAC1D,UAAM,CAAC,GAAG,IAAK,MAAM,GAClB,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC,EACzB,KAAK,OAAO,EACZ;AAAA,MACC;AAAA,QACE,GAAG,QAAQ,oBAAoB,QAAQ;AAAA,QACvC,OAAO,QAAQ,SAAS;AAAA,QACxB,IAAI,QAAQ,WAAW,KAAK;AAAA,MAC9B;AAAA,IACF;AACF,UAAM,SAAS,KAAK,SAAS;AAC7B,QAAI,UAAU,QAAQ;AACpB,YAAM,IAAI;AAAA,QACR,0DAAqD,MAAM;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,kBACpB,SACA,QACe;AACf,QAAM,KAAK,MAAM;AACjB,QAAM,UAAU,kBAAkB;AAClC,QAAM,QAAQ,MAAM,mBAAmB,OAAO;AAE9C,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,UAAU,OAAO,cAAc;AAAA,EACjD;AAEA,MAAI;AACF,UAAM,iBAAiB,MAAM,QAAQ,UAAU,MAAM,UAAU;AAC/D,UAAM,iBAAiB,MAAM,cAAc,SAAS,QAAQ,cAAc,CAAC;AAC3E,UAAM,YAAY,MAAM;AAAA,MACtB;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,EAAE,QAAQ,OAAO,QAAQ,SAAS,OAAO,QAAQ;AAAA,IACnD;AACA,UAAM,SAAS,OAAO,UAAU;AAChC,UAAM,WAAW,kBAAkB,MAAM;AACzC,UAAM,QAAQ,MAAM,oBAAoB,SAAS,MAAM,IAAI,WAAW,QAAQ,QAAQ;AAEtF,UAAM,GACH,OAAO,OAAO,EACd,IAAI;AAAA,MACH;AAAA,MACA,OAAO,UAAU,OAAO;AAAA,MACxB,QAAQ,UAAU,OAAO;AAAA,MACzB,QAAQ;AAAA,MACR,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC,EACA,MAAM,GAAG,QAAQ,IAAI,MAAM,EAAE,CAAC,EAC9B,UAAU;AAAA,EACf,SAAS,OAAO;AACd,UAAM,GACH,OAAO,OAAO,EACd,IAAI;AAAA,MACH,QAAQ;AAAA,MACR,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC,EACA,MAAM,GAAG,QAAQ,IAAI,MAAM,EAAE,CAAC,EAC9B,UAAU;AAEb,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,aAAa,IAAqD;AACtF,QAAM,KAAK,MAAM;AACjB,QAAM,CAAC,KAAK,IAAI,MAAM,GACnB,OAAO,EACP,KAAK,OAAO,EACZ,MAAM,IAAI,GAAG,QAAQ,IAAI,EAAE,GAAG,OAAO,QAAQ,SAAS,CAAC,CAAC,EACxD,MAAM,CAAC;AAEV,SAAO,QAAQ,SAAS,KAAK,IAAI;AACnC;AAEA,eAAsB,YACpB,IACuD;AACvD,QAAM,KAAK,MAAM;AACjB,QAAM,aAAa,MAAM,GAAG,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,GAAG,YAAY,SAAS,EAAE,CAAC;AAExF,MAAI,WAAW,SAAS,GAAG;AACzB,WAAO,EAAE,SAAS,OAAO,WAAW;AAAA,EACtC;AAEA,QAAM,CAAC,KAAK,IAAI,MAAM,GACnB,OAAO,EACP,KAAK,OAAO,EACZ,MAAM,IAAI,GAAG,QAAQ,IAAI,EAAE,GAAG,OAAO,QAAQ,SAAS,CAAC,CAAC,EACxD,MAAM,CAAC;AAEV,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAEA,QAAM,GACH,OAAO,OAAO,EACd,IAAI;AAAA,IACH,WAAW,oBAAI,KAAK;AAAA,IACpB,WAAW,oBAAI,KAAK;AAAA,EACtB,CAAC,EACA,MAAM,GAAG,QAAQ,IAAI,EAAE,CAAC,EACxB,UAAU;AAEb,SAAO,EAAE,SAAS,KAAK;AACzB;AAaA,eAAsB,UAAU,SAeN;AACxB,QAAM,KAAK,MAAM;AACjB,QAAM,OAAO,cAAc,QAAQ,IAAI;AACvC,QAAM,QAAQ,eAAe,QAAQ,KAAK;AAC1C,QAAM,UAAU,OAAO,KAAK;AAC5B,QAAM,aAAa,CAAC,OAAO,QAAQ,SAAS,CAAC;AAE7C,MAAI,QAAQ,UAAU;AACpB,eAAW,KAAK,GAAG,QAAQ,UAAU,QAAQ,QAAQ,CAAC;AAAA,EACxD;AAEA,MAAI,QAAQ,UAAU;AACpB,eAAW,KAAK,GAAG,QAAQ,UAAU,QAAQ,QAAQ,CAAC;AAAA,EACxD;AAEA,MAAI,QAAQ,iBAAiB,SAAS;AACpC,eAAW,KAAK,UAAU,QAAQ,UAAU,CAAC;AAAA,EAC/C,WAAW,QAAQ,iBAAiB,UAAU;AAC5C,eAAW,KAAK,UAAU,QAAQ,kBAAkB,CAAC;AAAA,EACvD;AAEA,MAAI,QAAQ,oBAAoB;AAC9B,eAAW,KAAK,GAAG,QAAQ,oBAAoB,QAAQ,kBAAkB,CAAC;AAAA,EAC5E;AAQA,MAAI,QAAQ,KAAK,QAAQ,EAAE,KAAK,EAAE,SAAS,GAAG;AAC5C,UAAM,SAAS,IAAI,QAAQ,EAAE,KAAK,EAAE,QAAQ,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;AACrE,UAAM,SAAS;AAAA,MACb,MAAM,QAAQ,UAAU,MAAM;AAAA,MAC9B,MAAM,QAAQ,KAAK,MAAM;AAAA,IAC3B;AACA,QAAI,OAAQ,YAAW,KAAK,MAAM;AAAA,EACpC;AAEA,QAAM,cAAc,kBAAkB,UAAU;AAOhD,QAAM,SAAU,GAiBb,OAAO;AAAA,IACN,OAAO;AAAA,IACP,UAAU,QAAQ;AAAA,IAClB,WAAW,QAAQ;AAAA,IACnB,cAAc,UAAU;AAAA,IACxB,mBAAmB,UAAU;AAAA,EAC/B,CAAC,EACA,KAAK,OAAO,EACZ,SAAS,SAAS,GAAG,QAAQ,YAAY,QAAQ,EAAE,CAAC,EACpD,SAAS,WAAW,GAAG,QAAQ,oBAAoB,UAAU,EAAE,CAAC,EAChE,MAAM,WAAW,EACjB,QAAQ,KAAK,QAAQ,SAAS,CAAC,EAC/B,MAAM,KAAK,EACX,OAAO,MAAM;AAEhB,QAAM,OAAQ,MAAM;AAOpB,QAAM,CAAC,EAAE,MAAM,CAAC,IAAK,cACjB,MAAM,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,IACnE,MAAM,GAAG,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC,EAAE,KAAK,OAAO;AACpD,QAAM,YAAY,OAAO,SAAS,CAAC;AACnC,QAAM,aAAa,cAAc,IAAI,IAAI,KAAK,KAAK,YAAY,KAAK;AAMpE,QAAM,OAAO,KAAK,IAAI,CAAC,SAAS;AAAA,IAC9B,GAAG,IAAI;AAAA,IACP,UAAU,IAAI,aAAa,OACvB;AAAA,MACE,MAAM;AAAA,MACN,MAAM,IAAI;AAAA,MACV,OAAO,IAAI;AAAA,IACb,IACA,IAAI,iBAAiB,OACrB;AAAA,MACE,MAAM;AAAA,MACN,QAAQ,IAAI;AAAA,MACZ,aAAa,IAAI;AAAA,IACnB,IACA;AAAA,EACN,EAAE;AAEF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,OAAO;AAAA,IACpB,aAAa,OAAO,KAAK,YAAY;AAAA,EACvC;AACF;AAEA,eAAsB,oBAAoB,eAAwC;AAChF,QAAM,KAAK,MAAM;AACjB,QAAM,UAAU,kBAAkB;AAClC,QAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,gBAAgB,KAAK,KAAK,KAAK,GAAI;AAC3E,QAAM,OAAO,MAAM,GAChB,OAAO,EACP,KAAK,OAAO,EACZ,MAAM,IAAI,UAAU,QAAQ,SAAS,GAAG,GAAG,QAAQ,WAAW,SAAS,CAAC,CAAC;AAC5E,QAAM,YAAY,KAAK,IAAI,aAAa;AAExC,MAAI,UAAU,WAAW,GAAG;AAC1B,WAAO;AAAA,EACT;AAEA,aAAW,SAAS,WAAW;AAC7B,UAAM,OAAO,oBAAI,IAAY;AAAA,MAC3B,MAAM;AAAA,MACN,GAAG,0BAA0B,MAAM,KAAK;AAAA,IAC1C,CAAC;AAED,eAAW,OAAO,MAAM;AACtB,UAAI;AACF,cAAM,QAAQ,OAAO,GAAG;AAAA,MAC1B,QAAQ;AACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,GAAG,OAAO,OAAO,EAAE,MAAM,QAAQ,QAAQ,IAAI,UAAU,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC,CAAC;AAEtF,SAAO,UAAU;AACnB;AAEA,eAAe,mBAAmB,IAAyC;AACzE,QAAM,KAAK,MAAM;AACjB,QAAM,CAAC,KAAK,IAAI,MAAM,GACnB,OAAO,EACP,KAAK,OAAO,EACZ,MAAM,IAAI,GAAG,QAAQ,IAAI,EAAE,GAAG,OAAO,QAAQ,SAAS,CAAC,CAAC,EACxD,MAAM,CAAC;AAEV,SAAO,QAAQ,cAAc,KAAK,IAAI;AACxC;AAEA,eAAe,oBACb,SACA,SACA,WACA,QACA,UACkD;AAClD,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,UAAU,SAAS,IAAI,OAAO,YAAY;AACxC,YAAM,WAAW,GAAG,QAAQ,IAAI,IAAI,MAAM;AAC1C,YAAM,aAAa,SAAS,OAAO,IAAI,QAAQ;AAE/C,YAAM,QAAQ,OAAO,YAAY,QAAQ,QAAQ;AAAA,QAC/C,aAAa;AAAA,QACb,eAAe,QAAQ;AAAA,QACvB,kBAAkB;AAAA,MACpB,CAAC;AAED,aAAO;AAAA,QACL,QAAQ;AAAA,QACR;AAAA,UACE;AAAA,UACA;AAAA,UACA,UAAU,QAAQ;AAAA,UAClB,OAAO,QAAQ;AAAA,UACf,QAAQ,QAAQ;AAAA,UAChB;AAAA,UACA,KAAK,MAAM,QAAQ,OAAO,UAAU;AAAA,QACtC;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO,OAAO,YAAY,OAAO;AACnC;AAEA,SAAS,0BACP,OACU;AACV,MAAI,CAAC,OAAO;AACV,WAAO,CAAC;AAAA,EACV;AAEA,SAAO,OAAO,OAAO,KAAK,EACvB,IAAI,CAAC,SAAS,KAAK,UAAU,EAC7B,OAAO,CAAC,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC;AACrF;AAEA,SAAS,qBAAqB,kBAA0B,UAA0B;AAChF,QAAM,YAAY,QAAQ,gBAAgB,EAAE,MAAM,CAAC,EAAE,YAAY;AAEjE,MAAI,WAAW;AACb,WAAO;AAAA,EACT;AAEA,UAAQ,UAAU;AAAA,IAChB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,kBAAkB,QAAwB;AACjD,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAEA,SAAS,kBACP,YACkE;AAClE,MAAI,WAAW,WAAW,GAAG;AAC3B,WAAO;AAAA,EACT;AAEA,MAAI,WAAW,WAAW,GAAG;AAC3B,WAAO,WAAW,CAAC;AAAA,EACrB;AAEA,SAAO,IAAI,GAAG,UAAU;AAC1B;AAEA,SAAS,cAAc,MAAuB;AAC5C,MAAI,CAAC,QAAQ,OAAO,GAAG;AACrB,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,MAAM,IAAI;AACxB;AAEA,SAAS,eAAe,OAAwB;AAC9C,MAAI,CAAC,SAAS,QAAQ,GAAG;AACvB,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,MAAM,KAAK;AACzB;AAEA,SAAS,cAAc,OAA6B;AAClD,QAAM,SAAS,SAAS,KAAK;AAE7B,SAAO;AAAA,IACL,IAAI,SAAS,OAAO,IAAI,IAAI;AAAA,IAC5B,UAAU,SAAS,OAAO,UAAU,UAAU;AAAA,IAC9C,kBAAkB,SAAS,OAAO,kBAAkB,kBAAkB;AAAA,IACtE,UAAU,SAAS,OAAO,UAAU,UAAU;AAAA,IAC9C,UAAU,SAAS,OAAO,UAAU,UAAU;AAAA,IAC9C,OAAO,iBAAiB,OAAO,KAAK;AAAA,IACpC,QAAQ,iBAAiB,OAAO,MAAM;AAAA,IACtC,OAAO,QAAQ,OAAO,KAAK;AAAA,IAC3B,YAAY,SAAS,OAAO,YAAY,YAAY;AAAA,IACpD,MAAM,SAAS,OAAO,MAAM,MAAM;AAAA,IAClC,QAAQ,cAAc,OAAO,MAAM;AAAA,IACnC,UAAU,iBAAiB,OAAO,QAAQ;AAAA,IAC1C,YAAY,iBAAiB,OAAO,UAAU;AAAA,IAC9C,WAAW,OAAO,OAAO,WAAW,WAAW;AAAA,IAC/C,WAAW,OAAO,OAAO,WAAW,WAAW;AAAA,IAC/C,WAAW,eAAe,OAAO,SAAS;AAAA,EAC5C;AACF;AAEA,SAAS,QAAQ,OAAgE;AAC/E,MAAI,SAAS,MAAM;AACjB,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,SAAS,KAAK;AAC7B,QAAM,QAAiD,CAAC;AAExD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,UAAM,aAAa,SAAS,KAAK;AACjC,UAAM,GAAG,IAAI;AAAA,EACf;AAEA,SAAO;AACT;AAEA,SAAS,cAAc,OAAuC;AAC5D,MAAI,UAAU,gBAAgB,UAAU,WAAW,UAAU,SAAS;AACpE,WAAO;AAAA,EACT;AAEA,QAAM,IAAI,MAAM,uBAAuB;AACzC;AAEA,SAAS,SAAS,OAAgB,OAAuB;AACvD,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,UAAM,IAAI,MAAM,WAAW,KAAK,GAAG;AAAA,EACrC;AAEA,SAAO;AACT;AAEA,SAAS,iBAAiB,OAA+B;AACvD,MAAI,SAAS,MAAM;AACjB,WAAO;AAAA,EACT;AAEA,SAAO,SAAS,OAAO,cAAc;AACvC;AAEA,SAAS,SAAS,OAAgB,OAAuB;AACvD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,IAAI,MAAM,WAAW,KAAK,GAAG;AAAA,EACrC;AAEA,SAAO;AACT;AAEA,SAAS,iBAAiB,OAA+B;AACvD,MAAI,SAAS,MAAM;AACjB,WAAO;AAAA,EACT;AAEA,SAAO,SAAS,OAAO,cAAc;AACvC;AAEA,SAAS,OAAO,OAAgB,OAAqB;AACnD,MAAI,EAAE,iBAAiB,OAAO;AAC5B,UAAM,IAAI,MAAM,WAAW,KAAK,GAAG;AAAA,EACrC;AAEA,SAAO;AACT;AAEA,SAAS,eAAe,OAA6B;AACnD,MAAI,SAAS,MAAM;AACjB,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,OAAO,YAAY;AACnC;AAEA,SAAS,SAAS,OAAyC;AACzD,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AAC/D,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AAEA,SAAO;AACT;","names":[]}
@@ -1 +0,0 @@
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() as unknown as NodePgDatabase<Record<string, unknown>>;\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() as unknown as NodePgDatabase<Record<string, unknown>>;\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 as unknown as (\n code: string,\n ) => Promise<ArcticLikeTokens>)(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\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\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() as unknown as NodePgDatabase<Record<string, unknown>>;\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() as unknown as NodePgDatabase<Record<string, unknown>>;\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() as unknown as NodePgDatabase<Record<string, unknown>>;\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() as unknown as NodePgDatabase<Record<string, unknown>>;\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() as unknown as NodePgDatabase<Record<string, unknown>>;\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() as unknown as NodePgDatabase<Record<string, unknown>>;\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\") as NpPasswordResetPurpose,\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"]}