nextly 0.0.1 → 0.0.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +122 -0
  3. package/dist/_dts-chunks/collections-handler.d-DjgO74Wt.d.ts +20540 -0
  4. package/dist/_dts-chunks/config.d-DNwsDnjs.d.ts +2589 -0
  5. package/dist/_dts-chunks/define-component.d-BUgTHmt3.d.ts +1149 -0
  6. package/dist/_dts-chunks/image-processor.d-OO1PmMrv.d.ts +335 -0
  7. package/dist/_dts-chunks/index.d-axCAzZ7m.d.ts +17842 -0
  8. package/dist/_dts-chunks/media.d-DjDOZo4B.d.ts +117 -0
  9. package/dist/_dts-chunks/on-error.d-CHIKWNxd.d.ts +38 -0
  10. package/dist/_dts-chunks/storage.d-BUhQ2we_.d.ts +404 -0
  11. package/dist/actions/index.d.ts +239 -0
  12. package/dist/actions/index.mjs +281 -0
  13. package/dist/api/auth-state.d.ts +5 -0
  14. package/dist/api/auth-state.mjs +131 -0
  15. package/dist/api/collections-schema-detail.d.ts +56 -0
  16. package/dist/api/collections-schema-detail.mjs +244 -0
  17. package/dist/api/collections-schema-export.d.ts +56 -0
  18. package/dist/api/collections-schema-export.mjs +129 -0
  19. package/dist/api/collections-schema.d.ts +59 -0
  20. package/dist/api/collections-schema.mjs +207 -0
  21. package/dist/api/components-detail.d.ts +50 -0
  22. package/dist/api/components-detail.mjs +132 -0
  23. package/dist/api/components.d.ts +69 -0
  24. package/dist/api/components.mjs +144 -0
  25. package/dist/api/email-providers-default.d.ts +40 -0
  26. package/dist/api/email-providers-default.mjs +75 -0
  27. package/dist/api/email-providers-detail.d.ts +81 -0
  28. package/dist/api/email-providers-detail.mjs +109 -0
  29. package/dist/api/email-providers-test.d.ts +43 -0
  30. package/dist/api/email-providers-test.mjs +114 -0
  31. package/dist/api/email-providers.d.ts +69 -0
  32. package/dist/api/email-providers.mjs +110 -0
  33. package/dist/api/email-send-template.d.ts +41 -0
  34. package/dist/api/email-send-template.mjs +58 -0
  35. package/dist/api/email-send.d.ts +42 -0
  36. package/dist/api/email-send.mjs +58 -0
  37. package/dist/api/email-templates-detail.d.ts +74 -0
  38. package/dist/api/email-templates-detail.mjs +112 -0
  39. package/dist/api/email-templates-layout.d.ts +55 -0
  40. package/dist/api/email-templates-layout.mjs +92 -0
  41. package/dist/api/email-templates-preview.d.ts +48 -0
  42. package/dist/api/email-templates-preview.mjs +93 -0
  43. package/dist/api/email-templates.d.ts +61 -0
  44. package/dist/api/email-templates.mjs +118 -0
  45. package/dist/api/health.d.ts +68 -0
  46. package/dist/api/health.mjs +67 -0
  47. package/dist/api/index.d.ts +54 -0
  48. package/dist/api/index.mjs +16 -0
  49. package/dist/api/media-bulk.d.ts +74 -0
  50. package/dist/api/media-bulk.mjs +196 -0
  51. package/dist/api/media-folders.d.ts +112 -0
  52. package/dist/api/media-folders.mjs +187 -0
  53. package/dist/api/media-handlers.d.ts +102 -0
  54. package/dist/api/media-handlers.mjs +437 -0
  55. package/dist/api/media.d.ts +117 -0
  56. package/dist/api/media.mjs +242 -0
  57. package/dist/api/singles-detail.d.ts +87 -0
  58. package/dist/api/singles-detail.mjs +170 -0
  59. package/dist/api/singles-schema-detail.d.ts +54 -0
  60. package/dist/api/singles-schema-detail.mjs +182 -0
  61. package/dist/api/singles.d.ts +34 -0
  62. package/dist/api/singles.mjs +94 -0
  63. package/dist/api/storage-upload-url.d.ts +48 -0
  64. package/dist/api/storage-upload-url.mjs +202 -0
  65. package/dist/api/uploads.d.ts +109 -0
  66. package/dist/api/uploads.mjs +359 -0
  67. package/dist/auth/index.d.ts +425 -0
  68. package/dist/auth/index.mjs +199 -0
  69. package/dist/boot-apply-PQSYLDIN.mjs +7 -0
  70. package/dist/chunk-2OALJTK6.mjs +489 -0
  71. package/dist/chunk-2Q2SX2CS.mjs +365 -0
  72. package/dist/chunk-2TFX4ND3.mjs +13 -0
  73. package/dist/chunk-2TWPDSYD.mjs +87 -0
  74. package/dist/chunk-2W3DVD7S.mjs +647 -0
  75. package/dist/chunk-2ZFKXPQM.mjs +88 -0
  76. package/dist/chunk-3FA7FKAV.mjs +832 -0
  77. package/dist/chunk-3NZ2KMBL.mjs +58 -0
  78. package/dist/chunk-4MJLT6PZ.mjs +0 -0
  79. package/dist/chunk-56WO4WX7.mjs +0 -0
  80. package/dist/chunk-5APFUGAD.mjs +89 -0
  81. package/dist/chunk-5HMZ644B.mjs +108 -0
  82. package/dist/chunk-67GXH6PR.mjs +32 -0
  83. package/dist/chunk-6JNEPWRW.mjs +14368 -0
  84. package/dist/chunk-6NFHQIJD.mjs +45 -0
  85. package/dist/chunk-7P6ASYW6.mjs +9 -0
  86. package/dist/chunk-A3WPLSDT.mjs +1364 -0
  87. package/dist/chunk-AGJ6F2T3.mjs +144 -0
  88. package/dist/chunk-AK6Z23OX.mjs +1464 -0
  89. package/dist/chunk-APKKRD2G.mjs +102 -0
  90. package/dist/chunk-B2GV2BWH.mjs +73 -0
  91. package/dist/chunk-D5HQBNUB.mjs +74 -0
  92. package/dist/chunk-DNNG377Z.mjs +204 -0
  93. package/dist/chunk-DP3G27G5.mjs +135 -0
  94. package/dist/chunk-DV6WVX2Q.mjs +0 -0
  95. package/dist/chunk-DXGGXIUZ.mjs +57 -0
  96. package/dist/chunk-EGXBZCGC.mjs +943 -0
  97. package/dist/chunk-ERCNLX3V.mjs +176 -0
  98. package/dist/chunk-FQULBZ53.mjs +850 -0
  99. package/dist/chunk-G2AA4QLC.mjs +262 -0
  100. package/dist/chunk-GDBJ5JCU.mjs +488 -0
  101. package/dist/chunk-GJNSJU4S.mjs +19 -0
  102. package/dist/chunk-GZ6DCQKC.mjs +69 -0
  103. package/dist/chunk-H26B4FYG.mjs +167 -0
  104. package/dist/chunk-I4JMR3UR.mjs +21 -0
  105. package/dist/chunk-INV7QKLG.mjs +508 -0
  106. package/dist/chunk-IUDOC7N7.mjs +46 -0
  107. package/dist/chunk-IZWPRDC3.mjs +206 -0
  108. package/dist/chunk-KIMNCZGV.mjs +15 -0
  109. package/dist/chunk-L6HW2DA7.mjs +15 -0
  110. package/dist/chunk-LAZXX4HR.mjs +100 -0
  111. package/dist/chunk-LDKCUMHK.mjs +95 -0
  112. package/dist/chunk-LRXMECUA.mjs +0 -0
  113. package/dist/chunk-M52VMPGA.mjs +119 -0
  114. package/dist/chunk-MGUWEEI6.mjs +160 -0
  115. package/dist/chunk-NRUWQ5Z7.mjs +419 -0
  116. package/dist/chunk-NSEFNNU4.mjs +25360 -0
  117. package/dist/chunk-NTHVDFGO.mjs +138 -0
  118. package/dist/chunk-O3QHXMOX.mjs +3166 -0
  119. package/dist/chunk-P7NH2OSC.mjs +2605 -0
  120. package/dist/chunk-PKMABBB5.mjs +184 -0
  121. package/dist/chunk-PWS6XGJK.mjs +76 -0
  122. package/dist/chunk-R6JJQHFC.mjs +20 -0
  123. package/dist/chunk-RJLLGGPG.mjs +0 -0
  124. package/dist/chunk-SBACDPNX.mjs +689 -0
  125. package/dist/chunk-TO5AFLVQ.mjs +124 -0
  126. package/dist/chunk-TS7GHTG2.mjs +5436 -0
  127. package/dist/chunk-UJ2IMJ4W.mjs +133 -0
  128. package/dist/chunk-UOP63Q54.mjs +102 -0
  129. package/dist/chunk-UUOFWCM6.mjs +78 -0
  130. package/dist/chunk-V4EQTOA4.mjs +893 -0
  131. package/dist/chunk-VJ66NCL4.mjs +193 -0
  132. package/dist/chunk-VQJQHVEV.mjs +29 -0
  133. package/dist/chunk-VTJADRO3.mjs +141 -0
  134. package/dist/chunk-VWF3JO32.mjs +0 -0
  135. package/dist/chunk-W4MGXIRR.mjs +27 -0
  136. package/dist/chunk-W5KKPZT5.mjs +1204 -0
  137. package/dist/chunk-WD34YQ6T.mjs +381 -0
  138. package/dist/chunk-WZBYMYVW.mjs +14 -0
  139. package/dist/chunk-X23WKS3Z.mjs +50 -0
  140. package/dist/chunk-X7TXCYYN.mjs +6496 -0
  141. package/dist/chunk-XGI4EMS3.mjs +140 -0
  142. package/dist/chunk-XZKLBMN6.mjs +1153 -0
  143. package/dist/chunk-YB7INWPY.mjs +0 -0
  144. package/dist/chunk-YV4Y7SDL.mjs +83 -0
  145. package/dist/chunk-YZNBLFIW.mjs +1688 -0
  146. package/dist/chunk-YZZCTONM.mjs +263 -0
  147. package/dist/chunk-ZE6A3FYH.mjs +289 -0
  148. package/dist/cli/nextly.mjs +68 -0
  149. package/dist/cli/utils/index.d.ts +449 -0
  150. package/dist/cli/utils/index.mjs +49 -0
  151. package/dist/component-schema-service-5577KVW6.mjs +11 -0
  152. package/dist/config-loader-23YEMC3Z.mjs +23 -0
  153. package/dist/config.d.ts +44 -0
  154. package/dist/config.mjs +109 -0
  155. package/dist/container-ORGFGYSZ.mjs +9 -0
  156. package/dist/database/index.d.ts +12 -0
  157. package/dist/database/index.mjs +40 -0
  158. package/dist/database/seeders/index.d.ts +93 -0
  159. package/dist/database/seeders/index.mjs +47 -0
  160. package/dist/db-sync-demote-LJGKLB3S.mjs +117 -0
  161. package/dist/db-sync-promote-B26VSYQF.mjs +113 -0
  162. package/dist/dev-reload-broadcaster-B73IQ53V.mjs +25 -0
  163. package/dist/dist-M2NOU37V.mjs +19 -0
  164. package/dist/drizzle-kit-lazy-D2M2PXR2.mjs +13 -0
  165. package/dist/dynamic-collection-schema-service-IEXTPIZ7.mjs +8 -0
  166. package/dist/errors/index.d.ts +159 -0
  167. package/dist/errors/index.mjs +10 -0
  168. package/dist/factory-IWMBKUJM.mjs +15 -0
  169. package/dist/first-run-QIVKWJIF.mjs +63 -0
  170. package/dist/fresh-push-NR67DC3R.mjs +8 -0
  171. package/dist/index.d.ts +4175 -0
  172. package/dist/index.mjs +1336 -0
  173. package/dist/local-plugin-PTET4NAT.mjs +7 -0
  174. package/dist/logger-NU46DXNY.mjs +15 -0
  175. package/dist/logger-YE4TC7ZN.mjs +9 -0
  176. package/dist/migration-journal-EP532Y4L.mjs +139 -0
  177. package/dist/migrations/mysql/0000_eager_sentry.sql +174 -0
  178. package/dist/migrations/mysql/0001_soft_giant_girl.sql +27 -0
  179. package/dist/migrations/mysql/0002_media_table.sql +24 -0
  180. package/dist/migrations/mysql/0003_dynamic_singles.sql +37 -0
  181. package/dist/migrations/mysql/0004_dynamic_components.sql +35 -0
  182. package/dist/migrations/mysql/0005_user_management_tables.sql +92 -0
  183. package/dist/migrations/mysql/0006_api_keys.sql +36 -0
  184. package/dist/migrations/mysql/0007_general_settings.sql +20 -0
  185. package/dist/migrations/mysql/0008_site_settings_logo_url.sql +9 -0
  186. package/dist/migrations/mysql/0009_activity_log.sql +30 -0
  187. package/dist/migrations/mysql/0010_site_settings_sidebar.sql +13 -0
  188. package/dist/migrations/mysql/0011_missing_tables_and_columns.sql +54 -0
  189. package/dist/migrations/mysql/0012_image_sizes_and_focal_point.sql +30 -0
  190. package/dist/migrations/mysql/0012_media_folders.sql +43 -0
  191. package/dist/migrations/mysql/0013_user_brute_force_protection.sql +31 -0
  192. package/dist/migrations/mysql/0014_email_template_attachments.sql +12 -0
  193. package/dist/migrations/mysql/0015_media_uploaded_by_nullable.sql +15 -0
  194. package/dist/migrations/mysql/20260429_000000_000_initial_journal.sql +22 -0
  195. package/dist/migrations/mysql/20260501_000000_journal_batch.sql +17 -0
  196. package/dist/migrations/mysql/20260501_000001_audit_log.sql +24 -0
  197. package/dist/migrations/mysql/20260504_000000_nextly_meta.sql +21 -0
  198. package/dist/migrations/mysql/meta/0000_snapshot.json +1005 -0
  199. package/dist/migrations/mysql/meta/0001_snapshot.json +1099 -0
  200. package/dist/migrations/mysql/meta/_journal.json +41 -0
  201. package/dist/migrations/postgresql/0000_misty_king_bedlam.sql +169 -0
  202. package/dist/migrations/postgresql/0001_perpetual_captain_marvel.sql +8 -0
  203. package/dist/migrations/postgresql/0002_sad_spectrum.sql +16 -0
  204. package/dist/migrations/postgresql/0003_hesitant_ultron.sql +17 -0
  205. package/dist/migrations/postgresql/0004_media_table.sql +24 -0
  206. package/dist/migrations/postgresql/0005_media_folders.sql +36 -0
  207. package/dist/migrations/postgresql/0006_dynamic_collections_update.sql +50 -0
  208. package/dist/migrations/postgresql/0007_dynamic_singles.sql +38 -0
  209. package/dist/migrations/postgresql/0008_dynamic_components.sql +37 -0
  210. package/dist/migrations/postgresql/0009_user_management_tables.sql +95 -0
  211. package/dist/migrations/postgresql/0010_api_keys.sql +34 -0
  212. package/dist/migrations/postgresql/0011_general_settings.sql +20 -0
  213. package/dist/migrations/postgresql/0012_site_settings_logo_url.sql +9 -0
  214. package/dist/migrations/postgresql/0013_activity_log.sql +29 -0
  215. package/dist/migrations/postgresql/0014_image_sizes_and_focal_point.sql +33 -0
  216. package/dist/migrations/postgresql/0014_site_settings_sidebar.sql +13 -0
  217. package/dist/migrations/postgresql/0015_user_brute_force_protection.sql +29 -0
  218. package/dist/migrations/postgresql/0016_email_template_attachments.sql +12 -0
  219. package/dist/migrations/postgresql/0017_media_uploaded_by_nullable.sql +15 -0
  220. package/dist/migrations/postgresql/20260429_000000_000_initial_journal.sql +24 -0
  221. package/dist/migrations/postgresql/20260501_000000_journal_batch.sql +17 -0
  222. package/dist/migrations/postgresql/20260501_000001_audit_log.sql +24 -0
  223. package/dist/migrations/postgresql/20260504_000000_nextly_meta.sql +22 -0
  224. package/dist/migrations/postgresql/meta/0000_snapshot.json +1286 -0
  225. package/dist/migrations/postgresql/meta/0001_snapshot.json +1407 -0
  226. package/dist/migrations/postgresql/meta/0002_snapshot.json +1552 -0
  227. package/dist/migrations/postgresql/meta/0003_snapshot.json +1695 -0
  228. package/dist/migrations/postgresql/meta/0010_snapshot.json +2345 -0
  229. package/dist/migrations/postgresql/meta/_journal.json +90 -0
  230. package/dist/migrations/sqlite/0000_api_keys.sql +34 -0
  231. package/dist/migrations/sqlite/0001_general_settings.sql +20 -0
  232. package/dist/migrations/sqlite/0002_site_settings_logo_url.sql +9 -0
  233. package/dist/migrations/sqlite/0003_activity_log.sql +29 -0
  234. package/dist/migrations/sqlite/0004_image_sizes_and_focal_point.sql +29 -0
  235. package/dist/migrations/sqlite/0004_site_settings_sidebar.sql +11 -0
  236. package/dist/migrations/sqlite/0005_user_brute_force_protection.sql +29 -0
  237. package/dist/migrations/sqlite/0006_email_template_attachments.sql +12 -0
  238. package/dist/migrations/sqlite/0007_media_uploaded_by_nullable.sql +111 -0
  239. package/dist/migrations/sqlite/20260429_000000_000_initial_journal.sql +24 -0
  240. package/dist/migrations/sqlite/20260501_000000_journal_batch.sql +19 -0
  241. package/dist/migrations/sqlite/20260501_000001_audit_log.sql +24 -0
  242. package/dist/migrations/sqlite/20260504_000000_nextly_meta.sql +21 -0
  243. package/dist/migrations/sqlite/20260505_000000_user_management_tables.sql +77 -0
  244. package/dist/next.d.ts +57 -0
  245. package/dist/next.mjs +55 -0
  246. package/dist/observability/index.d.ts +87 -0
  247. package/dist/observability/index.mjs +57 -0
  248. package/dist/permissions-3DZZQZMI.mjs +39 -0
  249. package/dist/pipeline-YOML7SWF.mjs +29 -0
  250. package/dist/preview-ZZTR3QGS.mjs +9 -0
  251. package/dist/program-PW6UB2ZC.mjs +5934 -0
  252. package/dist/reconcile-single-tables-7ENVXJGB.mjs +7 -0
  253. package/dist/register-SF6E6FVU.mjs +49 -0
  254. package/dist/reload-config-HWQ4G5MM.mjs +23 -0
  255. package/dist/resolve-single-table-name-JSOMUB3R.mjs +7 -0
  256. package/dist/routeHandler-UNMMJIBM.mjs +77 -0
  257. package/dist/runtime-schema-generator-NRA6A6Z6.mjs +8 -0
  258. package/dist/runtime.d.ts +120 -0
  259. package/dist/runtime.mjs +73 -0
  260. package/dist/schema-hash-FMMG6VPJ.mjs +13 -0
  261. package/dist/schema-registry-EQ36FZDP.mjs +7 -0
  262. package/dist/scripts/load-env.mjs +42 -0
  263. package/dist/storage/index.d.ts +566 -0
  264. package/dist/storage/index.mjs +45 -0
  265. package/dist/super-admin-G5ZK5F4T.mjs +39 -0
  266. package/dist/system-table-service-WGSRVEGT.mjs +17 -0
  267. package/dist/users-7KELGRYJ.mjs +38 -0
  268. package/package.json +308 -9
@@ -0,0 +1,1364 @@
1
+ import {
2
+ rateLimiter
3
+ } from "./chunk-DXGGXIUZ.mjs";
4
+ import {
5
+ getTrustedClientIp
6
+ } from "./chunk-APKKRD2G.mjs";
7
+ import {
8
+ generateRefreshToken,
9
+ generateRefreshTokenId,
10
+ getSession,
11
+ hashRefreshToken
12
+ } from "./chunk-UUOFWCM6.mjs";
13
+ import {
14
+ COOKIE_NAMES,
15
+ clearAccessTokenCookie,
16
+ getCookieOptions,
17
+ parseCookie,
18
+ readAccessTokenCookie,
19
+ serializeClearCookie,
20
+ serializeCookie,
21
+ setAccessTokenCookie
22
+ } from "./chunk-2ZFKXPQM.mjs";
23
+ import {
24
+ buildClaims,
25
+ signAccessToken
26
+ } from "./chunk-X23WKS3Z.mjs";
27
+ import {
28
+ readOrGenerateRequestId
29
+ } from "./chunk-67GXH6PR.mjs";
30
+ import {
31
+ getNextlyLogger
32
+ } from "./chunk-W4MGXIRR.mjs";
33
+ import {
34
+ EmailSchema,
35
+ PasswordSchema,
36
+ validatePasswordStrength,
37
+ verifyPassword
38
+ } from "./chunk-W5KKPZT5.mjs";
39
+ import {
40
+ respondAction,
41
+ respondData
42
+ } from "./chunk-IUDOC7N7.mjs";
43
+ import {
44
+ NextlyError
45
+ } from "./chunk-NRUWQ5Z7.mjs";
46
+
47
+ // src/auth/cookies/refresh-token-cookie.ts
48
+ function setRefreshTokenCookie(token, ttlSeconds, isProduction) {
49
+ const options = getCookieOptions("refreshToken", isProduction, ttlSeconds);
50
+ return serializeCookie(COOKIE_NAMES.refreshToken, token, options);
51
+ }
52
+ function clearRefreshTokenCookie() {
53
+ return serializeClearCookie(
54
+ COOKIE_NAMES.refreshToken,
55
+ "/admin/api/auth/refresh"
56
+ );
57
+ }
58
+ function readRefreshTokenCookie(request) {
59
+ return parseCookie(request.headers.get("cookie"), COOKIE_NAMES.refreshToken);
60
+ }
61
+
62
+ // src/auth/csrf/csrf-cookie.ts
63
+ function setCsrfCookie(token, isProduction) {
64
+ const options = getCookieOptions("csrf", isProduction);
65
+ return serializeCookie(COOKIE_NAMES.csrf, token, options);
66
+ }
67
+ function clearCsrfCookie() {
68
+ return serializeClearCookie(COOKIE_NAMES.csrf, "/admin");
69
+ }
70
+ function readCsrfCookie(request) {
71
+ return parseCookie(request.headers.get("cookie"), COOKIE_NAMES.csrf);
72
+ }
73
+ function readCsrfFromRequest(body, request) {
74
+ const headerToken = request.headers.get("x-csrf-token");
75
+ if (headerToken) return headerToken;
76
+ if (body && typeof body.csrfToken === "string") {
77
+ return body.csrfToken;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ // src/auth/csrf/validate.ts
83
+ import { timingSafeEqual } from "crypto";
84
+ function csrfTokensMatch(cookieToken, requestToken) {
85
+ if (!cookieToken || !requestToken) return false;
86
+ if (cookieToken.length !== requestToken.length) return false;
87
+ try {
88
+ const a = Buffer.from(cookieToken, "utf-8");
89
+ const b = Buffer.from(requestToken, "utf-8");
90
+ return timingSafeEqual(a, b);
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+ function validateOrigin(request, allowedOrigins) {
96
+ const origin = request.headers.get("origin");
97
+ const referer = request.headers.get("referer");
98
+ const requestOrigin = origin || (referer ? new URL(referer).origin : null);
99
+ if (!requestOrigin) {
100
+ return false;
101
+ }
102
+ const requestUrl = new URL(request.url);
103
+ const selfOrigin = requestUrl.origin;
104
+ const allAllowed = [selfOrigin, ...allowedOrigins];
105
+ return allAllowed.some(
106
+ (allowed) => requestOrigin.toLowerCase() === allowed.toLowerCase()
107
+ );
108
+ }
109
+ function validateCsrf(request, cookieToken, requestToken, allowedOrigins = []) {
110
+ if (!cookieToken || !requestToken) {
111
+ return { valid: false, error: "Missing CSRF token" };
112
+ }
113
+ if (!csrfTokensMatch(cookieToken, requestToken)) {
114
+ return { valid: false, error: "Invalid CSRF token" };
115
+ }
116
+ if (!validateOrigin(request, allowedOrigins)) {
117
+ return { valid: false, error: "Invalid request origin" };
118
+ }
119
+ return { valid: true };
120
+ }
121
+
122
+ // src/auth/handlers/handler-utils.ts
123
+ function jsonResponse(status, body, headers) {
124
+ return new Response(JSON.stringify(body), {
125
+ status,
126
+ headers: {
127
+ "Content-Type": "application/json",
128
+ ...headers
129
+ }
130
+ });
131
+ }
132
+ async function stallResponse(startTime, stallMs) {
133
+ const elapsed = Date.now() - startTime;
134
+ if (elapsed < stallMs) {
135
+ await new Promise((resolve) => setTimeout(resolve, stallMs - elapsed));
136
+ }
137
+ }
138
+ function buildCookieHeaders(cookies, extra) {
139
+ const headers = new Headers({
140
+ "Content-Type": "application/json",
141
+ ...extra
142
+ });
143
+ for (const cookie of cookies) {
144
+ headers.append("Set-Cookie", cookie);
145
+ }
146
+ return headers;
147
+ }
148
+ async function parseJsonBody(request) {
149
+ try {
150
+ return await request.json();
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ // src/auth/handlers/change-password.ts
157
+ async function handleChangePassword(request, deps) {
158
+ const sessionResult = await getSession(request, deps.secret);
159
+ if (!sessionResult.authenticated) {
160
+ return jsonResponse(401, {
161
+ error: { code: "AUTH_REQUIRED", message: "Authentication required" }
162
+ });
163
+ }
164
+ const body = await request.json();
165
+ const csrfCookie = readCsrfCookie(request);
166
+ const csrfToken = readCsrfFromRequest(body, request);
167
+ const csrfResult = validateCsrf(
168
+ request,
169
+ csrfCookie,
170
+ csrfToken,
171
+ deps.allowedOrigins
172
+ );
173
+ if (!csrfResult.valid) {
174
+ return jsonResponse(403, {
175
+ error: { code: "CSRF_FAILED", message: csrfResult.error }
176
+ });
177
+ }
178
+ const { currentPassword, newPassword } = body;
179
+ if (!currentPassword || !newPassword) {
180
+ return jsonResponse(400, {
181
+ error: {
182
+ code: "VALIDATION_ERROR",
183
+ message: "Current password and new password are required"
184
+ }
185
+ });
186
+ }
187
+ const result = await deps.changePassword(
188
+ sessionResult.user.id,
189
+ currentPassword,
190
+ newPassword
191
+ );
192
+ if (!result.success) {
193
+ return jsonResponse(400, {
194
+ error: { code: "PASSWORD_CHANGE_FAILED", message: result.error }
195
+ });
196
+ }
197
+ await deps.deleteAllRefreshTokensForUser(sessionResult.user.id);
198
+ await deps.auditLog.write({
199
+ kind: "password-changed",
200
+ actorUserId: sessionResult.user.id,
201
+ targetUserId: sessionResult.user.id,
202
+ ipAddress: getTrustedClientIp(request, {
203
+ trustProxy: deps.trustProxy,
204
+ trustedProxyIps: deps.trustedProxyIps
205
+ }),
206
+ userAgent: request.headers.get("user-agent")
207
+ });
208
+ const clearCookies = [clearAccessTokenCookie(), clearRefreshTokenCookie()];
209
+ return respondAction(
210
+ "Password changed.",
211
+ {},
212
+ { status: 200, headers: buildCookieHeaders(clearCookies) }
213
+ );
214
+ }
215
+
216
+ // src/auth/csrf/generate.ts
217
+ import { randomBytes } from "crypto";
218
+ function generateCsrfToken() {
219
+ return randomBytes(32).toString("hex");
220
+ }
221
+
222
+ // src/auth/handlers/csrf.ts
223
+ async function handleCsrf(_request, deps) {
224
+ const token = generateCsrfToken();
225
+ const cookie = setCsrfCookie(token, deps.isProduction);
226
+ const headers = new Headers();
227
+ headers.append("Set-Cookie", cookie);
228
+ return respondData({ token }, { status: 200, headers });
229
+ }
230
+
231
+ // src/auth/handlers/forgot-password.ts
232
+ var SILENT_MESSAGE = "If an account exists for this email, a password reset link has been sent.";
233
+ function sanitizeRedirectPath(raw) {
234
+ if (typeof raw !== "string" || raw.length === 0) return void 0;
235
+ if (raw.length > 2048 || raw.includes("\0")) {
236
+ console.warn(
237
+ `[nextly/auth] Rejected forgot-password redirectPath: oversized or contained null byte`
238
+ );
239
+ return void 0;
240
+ }
241
+ if (raw.startsWith("/") && !raw.startsWith("//")) {
242
+ if (raw === "/admin" || raw.startsWith("/admin/") || raw.startsWith("/admin?")) {
243
+ return raw;
244
+ }
245
+ console.warn(
246
+ `[nextly/auth] Rejected forgot-password redirectPath outside /admin: ${raw}`
247
+ );
248
+ return void 0;
249
+ }
250
+ let parsed;
251
+ try {
252
+ parsed = new URL(raw);
253
+ } catch {
254
+ console.warn(
255
+ `[nextly/auth] Rejected forgot-password redirectPath: unparseable URL: ${raw}`
256
+ );
257
+ return void 0;
258
+ }
259
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
260
+ console.warn(
261
+ `[nextly/auth] Rejected forgot-password redirectPath: bad protocol ${parsed.protocol}`
262
+ );
263
+ return void 0;
264
+ }
265
+ const allowed = (process.env.ALLOWED_REDIRECT_HOSTS ?? "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
266
+ const host = parsed.host.toLowerCase();
267
+ if (allowed.includes(host)) {
268
+ return raw;
269
+ }
270
+ console.warn(
271
+ `[nextly/auth] Rejected forgot-password redirectPath: host ${host} not in ALLOWED_REDIRECT_HOSTS`
272
+ );
273
+ return void 0;
274
+ }
275
+ function buildForgotErrorResponse(err, requestId) {
276
+ return new Response(
277
+ JSON.stringify({ error: err.toResponseJSON(requestId) }),
278
+ {
279
+ status: err.statusCode,
280
+ headers: {
281
+ "content-type": "application/problem+json",
282
+ "x-request-id": requestId
283
+ }
284
+ }
285
+ );
286
+ }
287
+ async function handleForgotPassword(request, deps) {
288
+ const startTime = Date.now();
289
+ const requestId = readOrGenerateRequestId(request);
290
+ try {
291
+ const body = await request.json();
292
+ const csrfCookie = readCsrfCookie(request);
293
+ const csrfToken = readCsrfFromRequest(body, request);
294
+ const csrfResult = validateCsrf(
295
+ request,
296
+ csrfCookie,
297
+ csrfToken,
298
+ deps.allowedOrigins
299
+ );
300
+ if (!csrfResult.valid) {
301
+ await stallResponse(startTime, deps.loginStallTimeMs);
302
+ return jsonResponse(
303
+ 403,
304
+ { error: { code: "CSRF_FAILED", message: csrfResult.error } },
305
+ { "x-request-id": requestId }
306
+ );
307
+ }
308
+ const { email, redirectPath } = body;
309
+ if (!email) {
310
+ throw NextlyError.validation({
311
+ errors: [{ path: "email", code: "REQUIRED", message: "Required." }]
312
+ });
313
+ }
314
+ const safeRedirectPath = sanitizeRedirectPath(redirectPath);
315
+ try {
316
+ await deps.generatePasswordResetToken(email, safeRedirectPath);
317
+ } catch (err) {
318
+ if (NextlyError.is(err) && (err.code === "NOT_FOUND" || err.code === "AUTH_INVALID_CREDENTIALS" || err.code === "VALIDATION_ERROR")) {
319
+ } else {
320
+ throw err;
321
+ }
322
+ }
323
+ await stallResponse(startTime, deps.loginStallTimeMs);
324
+ return respondAction(
325
+ SILENT_MESSAGE,
326
+ {},
327
+ { status: 200, headers: { "x-request-id": requestId } }
328
+ );
329
+ } catch (err) {
330
+ await stallResponse(startTime, deps.loginStallTimeMs);
331
+ if (NextlyError.is(err)) {
332
+ return buildForgotErrorResponse(err, requestId);
333
+ }
334
+ return buildForgotErrorResponse(
335
+ NextlyError.internal({ cause: err }),
336
+ requestId
337
+ );
338
+ }
339
+ }
340
+
341
+ // src/auth/credentials/verify-credentials.ts
342
+ var DUMMY_HASH = "$2b$12$ML1pr5W9k0ODLs2GFo9gruB/VcQfuby0nAeFo959eFXl0u1ZUmbb6";
343
+ async function verifyCredentials(input, deps) {
344
+ const user = await deps.findUserByEmail(input.email);
345
+ const passwordOk = user ? await verifyPassword(input.password, user.passwordHash) : await verifyPassword(input.password, DUMMY_HASH);
346
+ if (!user || !passwordOk) {
347
+ if (user) {
348
+ const newAttempts = user.failedLoginAttempts + 1;
349
+ if (newAttempts >= deps.maxLoginAttempts) {
350
+ const lockedUntil = new Date(
351
+ Date.now() + deps.lockoutDurationSeconds * 1e3
352
+ );
353
+ await deps.lockAccount(user.id, lockedUntil);
354
+ } else {
355
+ await deps.incrementFailedAttempts(user.id);
356
+ }
357
+ }
358
+ throw NextlyError.invalidCredentials({
359
+ logContext: {
360
+ email: input.email,
361
+ reason: !user ? "user-not-found" : "password-mismatch"
362
+ }
363
+ });
364
+ }
365
+ if (user.lockedUntil && user.lockedUntil > /* @__PURE__ */ new Date()) {
366
+ throw NextlyError.invalidCredentials({
367
+ logContext: {
368
+ userId: user.id,
369
+ reason: "locked",
370
+ lockedUntil: user.lockedUntil
371
+ }
372
+ });
373
+ }
374
+ if (deps.requireEmailVerification && !user.emailVerified) {
375
+ throw NextlyError.invalidCredentials({
376
+ logContext: { userId: user.id, reason: "unverified" }
377
+ });
378
+ }
379
+ if (!user.isActive) {
380
+ throw NextlyError.invalidCredentials({
381
+ logContext: { userId: user.id, reason: "inactive" }
382
+ });
383
+ }
384
+ if (user.failedLoginAttempts > 0) {
385
+ await deps.resetFailedAttempts(user.id);
386
+ }
387
+ return {
388
+ id: user.id,
389
+ email: user.email,
390
+ name: user.name,
391
+ image: user.image,
392
+ emailVerified: user.emailVerified,
393
+ isActive: user.isActive
394
+ };
395
+ }
396
+
397
+ // src/auth/handlers/login.ts
398
+ function buildLoginErrorResponse(err, requestId) {
399
+ return new Response(
400
+ JSON.stringify({ error: err.toResponseJSON(requestId) }),
401
+ {
402
+ status: err.statusCode,
403
+ headers: {
404
+ "content-type": "application/problem+json",
405
+ "x-request-id": requestId
406
+ }
407
+ }
408
+ );
409
+ }
410
+ async function handleLogin(request, deps) {
411
+ const startTime = Date.now();
412
+ const requestId = readOrGenerateRequestId(request);
413
+ try {
414
+ const body = await request.json();
415
+ const csrfCookie = readCsrfCookie(request);
416
+ const csrfToken = readCsrfFromRequest(body, request);
417
+ const csrfResult = validateCsrf(
418
+ request,
419
+ csrfCookie,
420
+ csrfToken,
421
+ deps.allowedOrigins
422
+ );
423
+ if (!csrfResult.valid) {
424
+ await stallResponse(startTime, deps.loginStallTimeMs);
425
+ return jsonResponse(
426
+ 403,
427
+ {
428
+ error: { code: "CSRF_FAILED", message: csrfResult.error }
429
+ },
430
+ { "x-request-id": requestId }
431
+ );
432
+ }
433
+ const verifiedUser = await verifyCredentials(
434
+ { email: body.email, password: body.password },
435
+ {
436
+ findUserByEmail: deps.findUserByEmail,
437
+ incrementFailedAttempts: deps.incrementFailedAttempts,
438
+ lockAccount: deps.lockAccount,
439
+ resetFailedAttempts: deps.resetFailedAttempts,
440
+ maxLoginAttempts: deps.maxLoginAttempts,
441
+ lockoutDurationSeconds: deps.lockoutDurationSeconds,
442
+ requireEmailVerification: deps.requireEmailVerification
443
+ }
444
+ );
445
+ const [roleIds, customFields] = await Promise.all([
446
+ deps.fetchRoleIds(verifiedUser.id),
447
+ deps.fetchCustomFields(verifiedUser.id)
448
+ ]);
449
+ const claims = buildClaims({
450
+ userId: verifiedUser.id,
451
+ email: verifiedUser.email,
452
+ name: verifiedUser.name,
453
+ image: verifiedUser.image,
454
+ roleIds,
455
+ customFields
456
+ });
457
+ const accessToken = await signAccessToken(
458
+ claims,
459
+ deps.secret,
460
+ deps.accessTokenTTL
461
+ );
462
+ const rawRefreshToken = generateRefreshToken();
463
+ const refreshTokenHash = hashRefreshToken(rawRefreshToken);
464
+ await deps.storeRefreshToken({
465
+ id: generateRefreshTokenId(),
466
+ userId: verifiedUser.id,
467
+ tokenHash: refreshTokenHash,
468
+ userAgent: request.headers.get("user-agent"),
469
+ ipAddress: getTrustedClientIp(request, {
470
+ trustProxy: deps.trustProxy,
471
+ trustedProxyIps: deps.trustedProxyIps
472
+ }),
473
+ expiresAt: new Date(Date.now() + deps.refreshTokenTTL * 1e3)
474
+ });
475
+ const cookies = [
476
+ setAccessTokenCookie(
477
+ accessToken,
478
+ deps.refreshTokenTTL,
479
+ deps.isProduction
480
+ ),
481
+ setRefreshTokenCookie(
482
+ rawRefreshToken,
483
+ deps.refreshTokenTTL,
484
+ deps.isProduction
485
+ )
486
+ ];
487
+ await stallResponse(startTime, deps.loginStallTimeMs);
488
+ return respondAction(
489
+ "Logged in.",
490
+ {
491
+ user: {
492
+ id: verifiedUser.id,
493
+ email: verifiedUser.email,
494
+ name: verifiedUser.name,
495
+ image: verifiedUser.image,
496
+ roleIds
497
+ },
498
+ accessToken,
499
+ refreshToken: rawRefreshToken,
500
+ // `expiresAt` reflects the access-token JWT exp claim (the
501
+ // authoritative expiration server-side), not the cookie max-age.
502
+ // signAccessToken uses deps.accessTokenTTL for that.
503
+ expiresAt: new Date(
504
+ Date.now() + deps.accessTokenTTL * 1e3
505
+ ).toISOString()
506
+ },
507
+ {
508
+ status: 200,
509
+ headers: buildCookieHeaders(cookies, { "x-request-id": requestId })
510
+ }
511
+ );
512
+ } catch (err) {
513
+ await stallResponse(startTime, deps.loginStallTimeMs);
514
+ await deps.auditLog.write({
515
+ kind: "login-failed",
516
+ ipAddress: getTrustedClientIp(request, {
517
+ trustProxy: deps.trustProxy,
518
+ trustedProxyIps: deps.trustedProxyIps
519
+ }),
520
+ userAgent: request.headers.get("user-agent"),
521
+ metadata: NextlyError.is(err) ? { code: err.code, ...err.logContext ?? {} } : { code: "INTERNAL_ERROR" }
522
+ });
523
+ if (NextlyError.is(err)) {
524
+ return buildLoginErrorResponse(err, requestId);
525
+ }
526
+ return buildLoginErrorResponse(
527
+ NextlyError.internal({ cause: err }),
528
+ requestId
529
+ );
530
+ }
531
+ }
532
+
533
+ // src/auth/handlers/logout.ts
534
+ async function handleLogout(request, deps) {
535
+ const body = await parseJsonBody(request);
536
+ const csrfCookie = readCsrfCookie(request);
537
+ const csrfToken = readCsrfFromRequest(body, request);
538
+ const csrfResult = validateCsrf(
539
+ request,
540
+ csrfCookie,
541
+ csrfToken,
542
+ deps.allowedOrigins
543
+ );
544
+ if (!csrfResult.valid) {
545
+ return jsonResponse(403, {
546
+ error: { code: "CSRF_FAILED", message: csrfResult.error }
547
+ });
548
+ }
549
+ const refreshToken = readRefreshTokenCookie(request);
550
+ if (refreshToken) {
551
+ const tokenHash = hashRefreshToken(refreshToken);
552
+ await deps.deleteRefreshTokenByHash(tokenHash);
553
+ }
554
+ const clearCookies = [
555
+ clearAccessTokenCookie(),
556
+ clearRefreshTokenCookie(),
557
+ clearCsrfCookie()
558
+ ];
559
+ return respondAction(
560
+ "Logged out.",
561
+ {},
562
+ { status: 200, headers: buildCookieHeaders(clearCookies) }
563
+ );
564
+ }
565
+
566
+ // src/auth/session/refresh-binding.ts
567
+ var IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
568
+ var IPV6_CHAR_RE = /^[0-9a-fA-F:.]+$/;
569
+ function classifyIp(addr) {
570
+ if (IPV4_RE.test(addr)) return 4;
571
+ if (addr.includes(":") && IPV6_CHAR_RE.test(addr) && addr.length <= 45) {
572
+ return 6;
573
+ }
574
+ return 0;
575
+ }
576
+ function ipv4ToInt(ip) {
577
+ const parts = ip.split(".");
578
+ if (parts.length !== 4) return null;
579
+ let acc = 0;
580
+ for (const p of parts) {
581
+ const n = Number(p);
582
+ if (!Number.isInteger(n) || n < 0 || n > 255) return null;
583
+ acc = acc << 8 | n;
584
+ }
585
+ return acc >>> 0;
586
+ }
587
+ function sameIpv4Prefix(a, b, prefix) {
588
+ const ai = ipv4ToInt(a);
589
+ const bi = ipv4ToInt(b);
590
+ if (ai === null || bi === null) return false;
591
+ const mask = prefix === 0 ? 0 : -1 >>> 32 - prefix << 32 - prefix;
592
+ return (ai & mask) >>> 0 === (bi & mask) >>> 0;
593
+ }
594
+ function ipv6PrefixHex(addr, prefixBits) {
595
+ if (prefixBits % 4 !== 0) return null;
596
+ const hexCount = prefixBits / 4;
597
+ let work = addr.toLowerCase();
598
+ const dottedMatch = work.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/);
599
+ if (dottedMatch) {
600
+ const v4 = ipv4ToInt(dottedMatch[2]);
601
+ if (v4 === null) return null;
602
+ const high = (v4 >>> 16 & 65535).toString(16);
603
+ const low = (v4 & 65535).toString(16);
604
+ work = `${dottedMatch[1]}${high}:${low}`;
605
+ }
606
+ const doubleColon = work.split("::");
607
+ if (doubleColon.length > 2) return null;
608
+ let groups;
609
+ if (doubleColon.length === 2) {
610
+ const left = doubleColon[0] === "" ? [] : doubleColon[0].split(":");
611
+ const right = doubleColon[1] === "" ? [] : doubleColon[1].split(":");
612
+ if (left.length + right.length > 8) return null;
613
+ const fill = new Array(8 - left.length - right.length).fill("0");
614
+ groups = [...left, ...fill, ...right];
615
+ } else {
616
+ groups = work.split(":");
617
+ if (groups.length !== 8) return null;
618
+ }
619
+ let out = "";
620
+ for (const g of groups) {
621
+ if (!/^[0-9a-f]{0,4}$/.test(g)) return null;
622
+ out += g.padStart(4, "0");
623
+ if (out.length >= hexCount) break;
624
+ }
625
+ return out.slice(0, hexCount);
626
+ }
627
+ function sameIpv6Prefix(a, b, prefix) {
628
+ const ah = ipv6PrefixHex(a, prefix);
629
+ const bh = ipv6PrefixHex(b, prefix);
630
+ if (ah === null || bh === null) return false;
631
+ return ah === bh;
632
+ }
633
+ function evaluateRefreshBinding(input) {
634
+ const ipResult = compareIps(input.storedIp, input.currentIp);
635
+ if (ipResult !== null) {
636
+ return { kind: "hard", reason: ipResult };
637
+ }
638
+ if (input.storedUserAgent !== null && input.currentUserAgent !== null && input.storedUserAgent !== input.currentUserAgent) {
639
+ return { kind: "soft", reason: "ua-mismatch" };
640
+ }
641
+ return { kind: "ok" };
642
+ }
643
+ function compareIps(stored, current) {
644
+ if (stored === null || current === null) return null;
645
+ if (stored === current) return null;
646
+ const sf = classifyIp(stored);
647
+ const cf = classifyIp(current);
648
+ if (sf === 0 || cf === 0) return null;
649
+ if (sf !== cf) return "ip-family-mismatch";
650
+ if (sf === 4) {
651
+ return sameIpv4Prefix(stored, current, 24) ? null : "ipv4-prefix-mismatch";
652
+ }
653
+ return sameIpv6Prefix(stored, current, 48) ? null : "ipv6-prefix-mismatch";
654
+ }
655
+
656
+ // src/auth/handlers/refresh.ts
657
+ async function handleRefresh(request, deps) {
658
+ const rawToken = readRefreshTokenCookie(request);
659
+ if (!rawToken) {
660
+ return clearAndDeny("No refresh token");
661
+ }
662
+ const tokenHash = hashRefreshToken(rawToken);
663
+ const tokenRecord = await deps.findRefreshTokenByHash(tokenHash);
664
+ if (!tokenRecord) {
665
+ return clearAndDeny("Invalid refresh token");
666
+ }
667
+ if (tokenRecord.expiresAt < /* @__PURE__ */ new Date()) {
668
+ await deps.deleteRefreshToken(tokenRecord.id);
669
+ return clearAndDeny("Refresh token expired");
670
+ }
671
+ const currentUserAgent = request.headers.get("user-agent");
672
+ const currentIp = getTrustedClientIp(request, {
673
+ trustProxy: deps.trustProxy,
674
+ trustedProxyIps: deps.trustedProxyIps
675
+ });
676
+ const binding = evaluateRefreshBinding({
677
+ storedUserAgent: tokenRecord.userAgent,
678
+ currentUserAgent,
679
+ storedIp: tokenRecord.ipAddress,
680
+ currentIp
681
+ });
682
+ if (binding.kind === "hard") {
683
+ await deps.deleteRefreshToken(tokenRecord.id);
684
+ await deps.deleteAllRefreshTokensForUser(tokenRecord.userId);
685
+ getNextlyLogger().warn({
686
+ kind: "refresh-binding-hard-fail",
687
+ reason: binding.reason,
688
+ tokenId: tokenRecord.id,
689
+ userId: tokenRecord.userId
690
+ });
691
+ return clearAndDeny("Session binding mismatch");
692
+ }
693
+ if (binding.kind === "soft") {
694
+ getNextlyLogger().warn({
695
+ kind: "refresh-binding-soft-warn",
696
+ reason: binding.reason,
697
+ tokenId: tokenRecord.id,
698
+ userId: tokenRecord.userId
699
+ });
700
+ }
701
+ await deps.deleteRefreshToken(tokenRecord.id);
702
+ const user = await deps.findUserById(tokenRecord.userId);
703
+ if (!user || !user.isActive) {
704
+ return clearAndDeny("User not found or inactive");
705
+ }
706
+ const [roleIds, customFields] = await Promise.all([
707
+ deps.fetchRoleIds(user.id),
708
+ deps.fetchCustomFields(user.id)
709
+ ]);
710
+ const claims = buildClaims({
711
+ userId: user.id,
712
+ email: user.email,
713
+ name: user.name,
714
+ image: user.image,
715
+ roleIds,
716
+ customFields
717
+ });
718
+ const accessToken = await signAccessToken(
719
+ claims,
720
+ deps.secret,
721
+ deps.accessTokenTTL
722
+ );
723
+ const newRawToken = generateRefreshToken();
724
+ const newTokenHash = hashRefreshToken(newRawToken);
725
+ await deps.storeRefreshToken({
726
+ id: generateRefreshTokenId(),
727
+ userId: user.id,
728
+ tokenHash: newTokenHash,
729
+ userAgent: request.headers.get("user-agent"),
730
+ ipAddress: getTrustedClientIp(request, {
731
+ trustProxy: deps.trustProxy,
732
+ trustedProxyIps: deps.trustedProxyIps
733
+ }),
734
+ expiresAt: new Date(Date.now() + deps.refreshTokenTTL * 1e3)
735
+ });
736
+ const cookies = [
737
+ setAccessTokenCookie(accessToken, deps.refreshTokenTTL, deps.isProduction),
738
+ setRefreshTokenCookie(newRawToken, deps.refreshTokenTTL, deps.isProduction)
739
+ ];
740
+ return respondData(
741
+ {
742
+ user: {
743
+ id: user.id,
744
+ email: user.email,
745
+ name: user.name,
746
+ image: user.image,
747
+ roleIds
748
+ },
749
+ accessToken,
750
+ refreshToken: newRawToken,
751
+ // Authoritative server-side exp lives on the JWT itself (accessTokenTTL).
752
+ expiresAt: new Date(
753
+ Date.now() + deps.accessTokenTTL * 1e3
754
+ ).toISOString()
755
+ },
756
+ { status: 200, headers: buildCookieHeaders(cookies) }
757
+ );
758
+ }
759
+ function clearAndDeny(message) {
760
+ const clearCookies = [
761
+ clearAccessTokenCookie(),
762
+ clearRefreshTokenCookie(),
763
+ clearCsrfCookie()
764
+ ];
765
+ return new Response(
766
+ JSON.stringify({
767
+ error: { code: "REFRESH_FAILED", message }
768
+ }),
769
+ { status: 401, headers: buildCookieHeaders(clearCookies) }
770
+ );
771
+ }
772
+
773
+ // src/auth/handlers/register.ts
774
+ import { z } from "zod";
775
+ var RegisterPayloadSchema = z.object({
776
+ email: EmailSchema,
777
+ password: PasswordSchema,
778
+ name: z.string().trim().min(1, "Name is required.").max(100, "Name must be 100 characters or less.")
779
+ });
780
+ var SILENT_SUCCESS_MESSAGE = "If this email is available, we've sent a confirmation link.";
781
+ function zodIssueToCode(issue) {
782
+ switch (issue.code) {
783
+ case "invalid_type":
784
+ return "REQUIRED";
785
+ case "too_small":
786
+ return issue.minimum === 1 ? "REQUIRED" : "TOO_SHORT";
787
+ case "too_big":
788
+ return "TOO_LONG";
789
+ case "invalid_format":
790
+ return "INVALID_FORMAT";
791
+ default:
792
+ return "INVALID";
793
+ }
794
+ }
795
+ function buildRegisterErrorResponse(err, requestId) {
796
+ return new Response(
797
+ JSON.stringify({ error: err.toResponseJSON(requestId) }),
798
+ {
799
+ status: err.statusCode,
800
+ headers: {
801
+ "content-type": "application/problem+json",
802
+ "x-request-id": requestId
803
+ }
804
+ }
805
+ );
806
+ }
807
+ async function handleRegister(request, deps) {
808
+ const startTime = Date.now();
809
+ const requestId = readOrGenerateRequestId(request);
810
+ try {
811
+ const body = await request.json();
812
+ const csrfCookie = readCsrfCookie(request);
813
+ const csrfToken = readCsrfFromRequest(body, request);
814
+ const csrfResult = validateCsrf(
815
+ request,
816
+ csrfCookie,
817
+ csrfToken,
818
+ deps.allowedOrigins
819
+ );
820
+ if (!csrfResult.valid) {
821
+ await stallResponse(startTime, deps.loginStallTimeMs);
822
+ return jsonResponse(
823
+ 403,
824
+ { error: { code: "CSRF_FAILED", message: csrfResult.error } },
825
+ { "x-request-id": requestId }
826
+ );
827
+ }
828
+ const parsed = RegisterPayloadSchema.safeParse(body);
829
+ if (!parsed.success) {
830
+ throw NextlyError.validation({
831
+ errors: parsed.error.issues.map((issue) => ({
832
+ path: issue.path.join(".") || "root",
833
+ code: zodIssueToCode(issue),
834
+ message: issue.message.endsWith(".") ? issue.message : `${issue.message}.`
835
+ }))
836
+ });
837
+ }
838
+ const { email, password, name } = parsed.data;
839
+ try {
840
+ const user = await deps.registerUser({ email, password, name });
841
+ if (deps.revealRegistrationConflict) {
842
+ await stallResponse(startTime, deps.loginStallTimeMs);
843
+ return respondAction(
844
+ "Account created.",
845
+ { user: { id: user.id, email: user.email, name: user.name } },
846
+ {
847
+ status: 201,
848
+ headers: { "x-request-id": requestId }
849
+ }
850
+ );
851
+ }
852
+ await stallResponse(startTime, deps.loginStallTimeMs);
853
+ return respondAction(
854
+ SILENT_SUCCESS_MESSAGE,
855
+ {},
856
+ {
857
+ status: 200,
858
+ headers: { "x-request-id": requestId }
859
+ }
860
+ );
861
+ } catch (err) {
862
+ if (NextlyError.isCode(err, "DUPLICATE") && !deps.revealRegistrationConflict) {
863
+ getNextlyLogger().info({
864
+ kind: "register-duplicate-swallowed",
865
+ requestId,
866
+ logContext: err.logContext
867
+ });
868
+ await stallResponse(startTime, deps.loginStallTimeMs);
869
+ return respondAction(
870
+ SILENT_SUCCESS_MESSAGE,
871
+ {},
872
+ {
873
+ status: 200,
874
+ headers: { "x-request-id": requestId }
875
+ }
876
+ );
877
+ }
878
+ throw err;
879
+ }
880
+ } catch (err) {
881
+ await stallResponse(startTime, deps.loginStallTimeMs);
882
+ if (NextlyError.is(err)) {
883
+ return buildRegisterErrorResponse(err, requestId);
884
+ }
885
+ return buildRegisterErrorResponse(
886
+ NextlyError.internal({ cause: err }),
887
+ requestId
888
+ );
889
+ }
890
+ }
891
+
892
+ // src/auth/handlers/reset-password.ts
893
+ function buildResetErrorResponse(err, requestId) {
894
+ return new Response(
895
+ JSON.stringify({ error: err.toResponseJSON(requestId) }),
896
+ {
897
+ status: err.statusCode,
898
+ headers: {
899
+ "content-type": "application/problem+json",
900
+ "x-request-id": requestId
901
+ }
902
+ }
903
+ );
904
+ }
905
+ function normaliseTokenFailure(err) {
906
+ if (NextlyError.is(err)) {
907
+ if (err.code === "NOT_FOUND" || err.code === "INVALID_INPUT" || err.code === "TOKEN_EXPIRED" || err.code === "VALIDATION_ERROR") {
908
+ return new NextlyError({
909
+ code: "INVALID_INPUT",
910
+ publicMessage: "This reset link is invalid or has expired.",
911
+ logContext: {
912
+ ...err.logContext ?? {},
913
+ originalCode: err.code
914
+ }
915
+ });
916
+ }
917
+ }
918
+ throw err;
919
+ }
920
+ async function handleResetPassword(request, deps) {
921
+ const requestId = readOrGenerateRequestId(request);
922
+ try {
923
+ const body = await request.json();
924
+ const csrfCookie = readCsrfCookie(request);
925
+ const csrfToken = readCsrfFromRequest(body, request);
926
+ const csrfResult = validateCsrf(
927
+ request,
928
+ csrfCookie,
929
+ csrfToken,
930
+ deps.allowedOrigins
931
+ );
932
+ if (!csrfResult.valid) {
933
+ return jsonResponse(
934
+ 403,
935
+ { error: { code: "CSRF_FAILED", message: csrfResult.error } },
936
+ { "x-request-id": requestId }
937
+ );
938
+ }
939
+ const { token, newPassword } = body;
940
+ if (!token || !newPassword) {
941
+ throw NextlyError.validation({
942
+ errors: [
943
+ ...!token ? [{ path: "token", code: "REQUIRED", message: "Required." }] : [],
944
+ ...!newPassword ? [{ path: "newPassword", code: "REQUIRED", message: "Required." }] : []
945
+ ]
946
+ });
947
+ }
948
+ let result;
949
+ try {
950
+ result = await deps.resetPasswordWithToken(token, newPassword);
951
+ } catch (err) {
952
+ throw normaliseTokenFailure(err);
953
+ }
954
+ if (result.email) {
955
+ const user = await deps.findUserByEmail(result.email);
956
+ if (user) {
957
+ await deps.deleteAllRefreshTokensForUser(user.id);
958
+ }
959
+ }
960
+ return respondAction(
961
+ "Password reset.",
962
+ {},
963
+ { status: 200, headers: { "x-request-id": requestId } }
964
+ );
965
+ } catch (err) {
966
+ if (NextlyError.is(err)) {
967
+ return buildResetErrorResponse(err, requestId);
968
+ }
969
+ return buildResetErrorResponse(
970
+ NextlyError.internal({ cause: err }),
971
+ requestId
972
+ );
973
+ }
974
+ }
975
+
976
+ // src/auth/handlers/session.ts
977
+ var devAutoLoginWarned = /* @__PURE__ */ new Set();
978
+ async function handleSession(request, deps) {
979
+ const result = await getSession(request, deps.secret);
980
+ let failureReason = null;
981
+ if (!result.authenticated) {
982
+ failureReason = result.reason;
983
+ } else if (!deps.isProduction && deps.devAutoLogin) {
984
+ const live = await deps.findUserByEmail(result.user.email);
985
+ if (!live || live.id !== result.user.id) failureReason = "user_gone";
986
+ }
987
+ if (failureReason === null && result.authenticated) {
988
+ const accessToken = readAccessTokenCookie(request);
989
+ return respondData({ user: result.user, accessToken });
990
+ }
991
+ if (!deps.isProduction && deps.devAutoLogin) {
992
+ const autoLoginResponse = await attemptDevAutoLogin(deps);
993
+ if (autoLoginResponse) return autoLoginResponse;
994
+ } else if (deps.isProduction && deps.devAutoLogin) {
995
+ const key = deps.devAutoLogin.email;
996
+ if (!devAutoLoginWarned.has(`prod:${key}`)) {
997
+ devAutoLoginWarned.add(`prod:${key}`);
998
+ console.warn(
999
+ `[nextly] devAutoLogin ignored: NODE_ENV=production. Set this only for development. (configured email: ${key})`
1000
+ );
1001
+ }
1002
+ }
1003
+ const clearCookies = [];
1004
+ if (failureReason === "invalid" || failureReason === "user_gone") {
1005
+ clearCookies.push(clearAccessTokenCookie());
1006
+ }
1007
+ const code = failureReason === "expired" ? "TOKEN_EXPIRED" : "AUTH_REQUIRED";
1008
+ const message = code === "TOKEN_EXPIRED" ? "Session expired" : "Not authenticated";
1009
+ if (clearCookies.length > 0) {
1010
+ return new Response(JSON.stringify({ error: { code, message } }), {
1011
+ status: 401,
1012
+ headers: buildCookieHeaders(clearCookies)
1013
+ });
1014
+ }
1015
+ return jsonResponse(401, { error: { code, message } });
1016
+ }
1017
+ async function attemptDevAutoLogin(deps) {
1018
+ if (!deps.devAutoLogin) return null;
1019
+ const { email } = deps.devAutoLogin;
1020
+ if (!email) return null;
1021
+ const user = await deps.findUserByEmail(email);
1022
+ if (!user || !user.isActive) {
1023
+ if (!devAutoLoginWarned.has(`miss:${email}`)) {
1024
+ devAutoLoginWarned.add(`miss:${email}`);
1025
+ console.warn(
1026
+ `[nextly] devAutoLogin: user "${email}" not found. Auto-login skipped. Either register the user or update admin.devAutoLogin.email in your nextly.config.ts.`
1027
+ );
1028
+ }
1029
+ return null;
1030
+ }
1031
+ if (!devAutoLoginWarned.has(`active:${email}`)) {
1032
+ devAutoLoginWarned.add(`active:${email}`);
1033
+ console.warn(
1034
+ `[nextly] devAutoLogin enabled for ${email}. DO NOT use this in production deployments.`
1035
+ );
1036
+ }
1037
+ const [roleIds, customFields] = await Promise.all([
1038
+ deps.fetchRoleIds(user.id),
1039
+ deps.fetchCustomFields(user.id)
1040
+ ]);
1041
+ const claims = buildClaims({
1042
+ userId: user.id,
1043
+ email: user.email,
1044
+ name: user.name,
1045
+ image: user.image,
1046
+ roleIds,
1047
+ customFields
1048
+ });
1049
+ const accessToken = await signAccessToken(
1050
+ claims,
1051
+ deps.secret,
1052
+ deps.accessTokenTTL
1053
+ );
1054
+ const rawRefreshToken = generateRefreshToken();
1055
+ const refreshTokenHash = hashRefreshToken(rawRefreshToken);
1056
+ await deps.storeRefreshToken({
1057
+ id: generateRefreshTokenId(),
1058
+ userId: user.id,
1059
+ tokenHash: refreshTokenHash,
1060
+ userAgent: null,
1061
+ ipAddress: null,
1062
+ expiresAt: new Date(Date.now() + deps.refreshTokenTTL * 1e3)
1063
+ });
1064
+ const cookies = [
1065
+ setAccessTokenCookie(accessToken, deps.refreshTokenTTL, deps.isProduction),
1066
+ setRefreshTokenCookie(
1067
+ rawRefreshToken,
1068
+ deps.refreshTokenTTL,
1069
+ deps.isProduction
1070
+ )
1071
+ ];
1072
+ return new Response(
1073
+ JSON.stringify({
1074
+ user: {
1075
+ id: user.id,
1076
+ email: user.email,
1077
+ name: user.name,
1078
+ image: user.image
1079
+ },
1080
+ accessToken
1081
+ }),
1082
+ {
1083
+ status: 200,
1084
+ headers: buildCookieHeaders(cookies)
1085
+ }
1086
+ );
1087
+ }
1088
+
1089
+ // src/auth/handlers/setup.ts
1090
+ async function handleSetupStatus(_request, deps) {
1091
+ const count = await deps.getUserCount();
1092
+ const isSetup = count > 0;
1093
+ return respondData({ isSetup, requiresInitialUser: !isSetup });
1094
+ }
1095
+ async function handleSetup(request, deps) {
1096
+ const userCount = await deps.getUserCount();
1097
+ if (userCount > 0) {
1098
+ return jsonResponse(403, {
1099
+ error: {
1100
+ code: "SETUP_COMPLETE",
1101
+ message: "Setup already completed"
1102
+ }
1103
+ });
1104
+ }
1105
+ const body = await request.json();
1106
+ const csrfCookie = readCsrfCookie(request);
1107
+ const csrfToken = readCsrfFromRequest(body, request);
1108
+ const csrfResult = validateCsrf(
1109
+ request,
1110
+ csrfCookie,
1111
+ csrfToken,
1112
+ deps.allowedOrigins
1113
+ );
1114
+ if (!csrfResult.valid) {
1115
+ return jsonResponse(403, {
1116
+ error: { code: "CSRF_FAILED", message: csrfResult.error }
1117
+ });
1118
+ }
1119
+ const { email, name, password } = body;
1120
+ if (!email || !name || !password) {
1121
+ return jsonResponse(400, {
1122
+ error: {
1123
+ code: "VALIDATION_ERROR",
1124
+ message: "Email, name, and password are required"
1125
+ }
1126
+ });
1127
+ }
1128
+ const strengthResult = validatePasswordStrength(password);
1129
+ if (!strengthResult.ok) {
1130
+ return jsonResponse(400, {
1131
+ error: {
1132
+ code: "WEAK_PASSWORD",
1133
+ message: "Password does not meet requirements",
1134
+ details: strengthResult.errors
1135
+ }
1136
+ });
1137
+ }
1138
+ const user = await deps.createSuperAdmin({ email, name, password });
1139
+ const roleIds = await deps.fetchRoleIds(user.id);
1140
+ const claims = buildClaims({
1141
+ userId: user.id,
1142
+ email: user.email,
1143
+ name: user.name,
1144
+ image: null,
1145
+ roleIds
1146
+ });
1147
+ const accessToken = await signAccessToken(
1148
+ claims,
1149
+ deps.secret,
1150
+ deps.accessTokenTTL
1151
+ );
1152
+ const rawRefreshToken = generateRefreshToken();
1153
+ await deps.storeRefreshToken({
1154
+ id: generateRefreshTokenId(),
1155
+ userId: user.id,
1156
+ tokenHash: hashRefreshToken(rawRefreshToken),
1157
+ userAgent: request.headers.get("user-agent"),
1158
+ ipAddress: getTrustedClientIp(request, {
1159
+ trustProxy: deps.trustProxy,
1160
+ trustedProxyIps: deps.trustedProxyIps
1161
+ }),
1162
+ expiresAt: new Date(Date.now() + deps.refreshTokenTTL * 1e3)
1163
+ });
1164
+ const cookies = [
1165
+ setAccessTokenCookie(accessToken, deps.refreshTokenTTL, deps.isProduction),
1166
+ setRefreshTokenCookie(
1167
+ rawRefreshToken,
1168
+ deps.refreshTokenTTL,
1169
+ deps.isProduction
1170
+ )
1171
+ ];
1172
+ return respondAction(
1173
+ "Setup complete.",
1174
+ {
1175
+ user: { id: user.id, email: user.email, name: user.name, roleIds },
1176
+ accessToken,
1177
+ refreshToken: rawRefreshToken,
1178
+ // Authoritative server-side exp = accessToken JWT exp claim, not cookie max-age.
1179
+ expiresAt: new Date(
1180
+ Date.now() + deps.accessTokenTTL * 1e3
1181
+ ).toISOString()
1182
+ },
1183
+ { status: 201, headers: buildCookieHeaders(cookies) }
1184
+ );
1185
+ }
1186
+
1187
+ // src/auth/handlers/verify-email.ts
1188
+ async function handleVerifyEmail(request, deps) {
1189
+ const body = await request.json();
1190
+ const { token } = body;
1191
+ if (!token) {
1192
+ return jsonResponse(400, {
1193
+ error: { code: "VALIDATION_ERROR", message: "Token is required" }
1194
+ });
1195
+ }
1196
+ const result = await deps.verifyEmail(token);
1197
+ if (!result.success) {
1198
+ return jsonResponse(400, {
1199
+ error: {
1200
+ code: "VERIFICATION_FAILED",
1201
+ message: result.error || "Invalid or expired token"
1202
+ }
1203
+ });
1204
+ }
1205
+ return respondAction("Email verified.", { email: result.email });
1206
+ }
1207
+ async function handleResendVerification(request, deps) {
1208
+ const body = await request.json();
1209
+ const csrfCookie = readCsrfCookie(request);
1210
+ const csrfToken = readCsrfFromRequest(body, request);
1211
+ const csrfResult = validateCsrf(
1212
+ request,
1213
+ csrfCookie,
1214
+ csrfToken,
1215
+ deps.allowedOrigins
1216
+ );
1217
+ if (!csrfResult.valid) {
1218
+ return jsonResponse(403, {
1219
+ error: { code: "CSRF_FAILED", message: csrfResult.error }
1220
+ });
1221
+ }
1222
+ const { email } = body;
1223
+ if (!email) {
1224
+ return jsonResponse(400, {
1225
+ error: { code: "VALIDATION_ERROR", message: "Email is required" }
1226
+ });
1227
+ }
1228
+ await deps.resendVerificationEmail(email);
1229
+ return respondAction("Verification email sent.");
1230
+ }
1231
+
1232
+ // src/auth/handlers/router.ts
1233
+ var RATE_LIMITED_AUTH_PATHS = /* @__PURE__ */ new Set([
1234
+ "login",
1235
+ "register",
1236
+ "forgot-password",
1237
+ "reset-password"
1238
+ ]);
1239
+ async function routeAuthRequest(request, authPath, deps) {
1240
+ const response = await dispatchAuthRequest(request, authPath, deps);
1241
+ return applyNoStoreCache(response);
1242
+ }
1243
+ async function dispatchAuthRequest(request, authPath, deps) {
1244
+ const method = request.method.toUpperCase();
1245
+ if (method === "GET") {
1246
+ switch (authPath) {
1247
+ case "setup-status":
1248
+ return handleSetupStatus(request, deps);
1249
+ case "session":
1250
+ return handleSession(request, deps);
1251
+ case "csrf":
1252
+ return handleCsrf(request, deps);
1253
+ default:
1254
+ return null;
1255
+ }
1256
+ }
1257
+ if (method === "POST") {
1258
+ if (RATE_LIMITED_AUTH_PATHS.has(authPath)) {
1259
+ const limited = checkAuthIpRateLimit(request, deps);
1260
+ if (limited) return limited;
1261
+ }
1262
+ return auditCsrfFailure(request, authPath, deps, () => {
1263
+ switch (authPath) {
1264
+ case "login":
1265
+ return handleLogin(request, deps);
1266
+ case "logout":
1267
+ return handleLogout(request, deps);
1268
+ case "refresh":
1269
+ return handleRefresh(request, deps);
1270
+ case "setup":
1271
+ return handleSetup(request, deps);
1272
+ case "register":
1273
+ return handleRegister(request, deps);
1274
+ case "forgot-password":
1275
+ return handleForgotPassword(request, deps);
1276
+ case "reset-password":
1277
+ return handleResetPassword(request, deps);
1278
+ case "verify-email":
1279
+ return handleVerifyEmail(request, deps);
1280
+ case "verify-email/resend":
1281
+ return handleResendVerification(request, deps);
1282
+ default:
1283
+ return Promise.resolve(null);
1284
+ }
1285
+ });
1286
+ }
1287
+ if (method === "PATCH") {
1288
+ return auditCsrfFailure(request, authPath, deps, () => {
1289
+ switch (authPath) {
1290
+ case "change-password":
1291
+ return handleChangePassword(request, deps);
1292
+ default:
1293
+ return Promise.resolve(null);
1294
+ }
1295
+ });
1296
+ }
1297
+ return null;
1298
+ }
1299
+ function applyNoStoreCache(response) {
1300
+ if (!response) return response;
1301
+ response.headers.set("Cache-Control", "no-store");
1302
+ response.headers.set("Pragma", "no-cache");
1303
+ return response;
1304
+ }
1305
+ async function auditCsrfFailure(request, authPath, deps, dispatch) {
1306
+ const response = await dispatch();
1307
+ if (!response || response.status !== 403) return response;
1308
+ try {
1309
+ const body = await response.clone().json();
1310
+ if (body?.error?.code !== "CSRF_FAILED") return response;
1311
+ await deps.auditLog.write({
1312
+ kind: "csrf-failed",
1313
+ ipAddress: getTrustedClientIp(request, {
1314
+ trustProxy: deps.trustProxy,
1315
+ trustedProxyIps: deps.trustedProxyIps
1316
+ }),
1317
+ userAgent: request.headers.get("user-agent"),
1318
+ metadata: { path: authPath, method: request.method.toUpperCase() }
1319
+ });
1320
+ } catch {
1321
+ }
1322
+ return response;
1323
+ }
1324
+ function checkAuthIpRateLimit(request, deps) {
1325
+ const limit = deps.authRateLimit.requestsPerHour;
1326
+ if (limit <= 0) return null;
1327
+ const ip = getTrustedClientIp(request, {
1328
+ trustProxy: deps.trustProxy,
1329
+ trustedProxyIps: deps.trustedProxyIps
1330
+ }) ?? "unknown";
1331
+ const result = rateLimiter.check(
1332
+ `auth-ip:${ip}`,
1333
+ limit,
1334
+ deps.authRateLimit.windowMs
1335
+ );
1336
+ if (result.allowed) return null;
1337
+ const retryAfter = Math.max(
1338
+ 1,
1339
+ Math.ceil((result.resetAt.getTime() - Date.now()) / 1e3)
1340
+ );
1341
+ return new Response(
1342
+ JSON.stringify({
1343
+ error: {
1344
+ code: "RATE_LIMIT_EXCEEDED",
1345
+ message: "Too many auth requests from this IP. Please try again later.",
1346
+ retryAfter
1347
+ }
1348
+ }),
1349
+ {
1350
+ status: 429,
1351
+ headers: {
1352
+ "content-type": "application/json",
1353
+ "retry-after": String(retryAfter),
1354
+ "x-ratelimit-limit": String(limit),
1355
+ "x-ratelimit-remaining": "0",
1356
+ "x-ratelimit-reset": String(result.resetAt.getTime())
1357
+ }
1358
+ }
1359
+ );
1360
+ }
1361
+
1362
+ export {
1363
+ routeAuthRequest
1364
+ };