@edge-base/server 0.1.1

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 (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,367 @@
1
+ /**
2
+ * 서버 단위 테스트 — lib/password.ts
3
+ * 1-10 auth-password.test.ts — 20개
4
+ *
5
+ * 실행: cd packages/server && npx vitest run src/__tests__/auth-password.test.ts
6
+ *
7
+ * 테스트 대상:
8
+ * hashPassword / verifyPassword
9
+ * Format: pbkdf2:sha256:{iterations}:{salt_b64}:{hash_b64}
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import bcrypt from 'bcryptjs';
14
+ import { hashPassword, verifyPassword, needsRehash, isPasswordHash } from '../lib/password.js';
15
+
16
+ // ─── A. hashPassword ─────────────────────────────────────────────────────────
17
+
18
+ describe('hashPassword', () => {
19
+ it('returns a string', async () => {
20
+ const hash = await hashPassword('MyP@ssword1!');
21
+ expect(typeof hash).toBe('string');
22
+ });
23
+
24
+ it('format is pbkdf2:sha256:{iterations}:{salt}:{hash}', async () => {
25
+ const hash = await hashPassword('MyP@ssword1!');
26
+ const parts = hash.split(':');
27
+ expect(parts).toHaveLength(5);
28
+ expect(parts[0]).toBe('pbkdf2');
29
+ expect(parts[1]).toBe('sha256');
30
+ expect(parts[2]).toBe('100000');
31
+ // salt and hash are base64 — non-empty
32
+ expect(parts[3].length).toBeGreaterThan(0);
33
+ expect(parts[4].length).toBeGreaterThan(0);
34
+ });
35
+
36
+ it('iterations is 100000 (Cloudflare WebCrypto ceiling)', async () => {
37
+ const hash = await hashPassword('test');
38
+ const iterations = parseInt(hash.split(':')[2], 10);
39
+ expect(iterations).toBe(100000);
40
+ });
41
+
42
+ it('different hashes for same password (random salt)', async () => {
43
+ const hash1 = await hashPassword('identical');
44
+ const hash2 = await hashPassword('identical');
45
+ expect(hash1).not.toBe(hash2);
46
+ });
47
+
48
+ it('salt segment is base64 encoded (16-byte → ~24 chars)', async () => {
49
+ const hash = await hashPassword('test');
50
+ const salt = hash.split(':')[3];
51
+ // 16 bytes base64 should be 24 chars (with padding)
52
+ expect(salt.length).toBeGreaterThanOrEqual(20);
53
+ });
54
+
55
+ it('hash segment is base64 encoded (32-byte → ~44 chars)', async () => {
56
+ const hash = await hashPassword('test');
57
+ const hashPart = hash.split(':')[4];
58
+ // 32 bytes base64 should be 44 chars (with padding)
59
+ expect(hashPart.length).toBeGreaterThanOrEqual(40);
60
+ });
61
+ });
62
+
63
+ // ─── B. verifyPassword ───────────────────────────────────────────────────────
64
+
65
+ describe('verifyPassword', () => {
66
+ it('correct password → true', async () => {
67
+ const hash = await hashPassword('CorrectPw1!');
68
+ const result = await verifyPassword('CorrectPw1!', hash);
69
+ expect(result).toBe(true);
70
+ });
71
+
72
+ it('wrong password → false', async () => {
73
+ const hash = await hashPassword('CorrectPw1!');
74
+ const result = await verifyPassword('WrongPw1!', hash);
75
+ expect(result).toBe(false);
76
+ });
77
+
78
+ it('empty password → false against normal hash', async () => {
79
+ const hash = await hashPassword('CorrectPw1!');
80
+ const result = await verifyPassword('', hash);
81
+ expect(result).toBe(false);
82
+ });
83
+
84
+ it('case-sensitive: lowercase ≠ uppercase', async () => {
85
+ const hash = await hashPassword('Password1!');
86
+ const result = await verifyPassword('password1!', hash);
87
+ expect(result).toBe(false);
88
+ });
89
+
90
+ it('malformed hash (too few parts) → false', async () => {
91
+ const result = await verifyPassword('test', 'pbkdf2:sha256:100000:salt');
92
+ expect(result).toBe(false);
93
+ });
94
+
95
+ it('wrong algorithm prefix → false', async () => {
96
+ const result = await verifyPassword('test', 'bcrypt:sha256:100000:salt:hash');
97
+ expect(result).toBe(false);
98
+ });
99
+
100
+ it('different hash algorithm (sha512) → false', async () => {
101
+ const result = await verifyPassword('test', 'pbkdf2:sha512:100000:salt:hash');
102
+ expect(result).toBe(false);
103
+ });
104
+
105
+ it('blank hash → false', async () => {
106
+ const result = await verifyPassword('test', '');
107
+ expect(result).toBe(false);
108
+ });
109
+
110
+ it('correct unicode password → true', async () => {
111
+ const pw = '한국어비밀번호';
112
+ const hash = await hashPassword(pw);
113
+ expect(await verifyPassword(pw, hash)).toBe(true);
114
+ });
115
+
116
+ it('trailing space in password treated as different password', async () => {
117
+ const hash = await hashPassword('Password1!');
118
+ expect(await verifyPassword('Password1! ', hash)).toBe(false);
119
+ });
120
+
121
+ it('very long password → true', async () => {
122
+ const longPw = 'a'.repeat(1000);
123
+ const hash = await hashPassword(longPw);
124
+ expect(await verifyPassword(longPw, hash)).toBe(true);
125
+ });
126
+
127
+ it('timing-safe: wrong hash still evaluates same path', async () => {
128
+ // Just verify it doesn't throw on wrong secret comparison
129
+ const hash = await hashPassword('correct');
130
+ const result = await verifyPassword('wrong', hash);
131
+ expect(typeof result).toBe('boolean');
132
+ });
133
+
134
+ it('special characters in password', async () => {
135
+ const pw = '!@#$%^&*()_+-=[]{};\':"\\|,.<>/?`~';
136
+ const hash = await hashPassword(pw);
137
+ expect(await verifyPassword(pw, hash)).toBe(true);
138
+ });
139
+
140
+ it('empty stored hash parts[3] invalid salt → false', async () => {
141
+ const result = await verifyPassword('test', 'pbkdf2:sha256:100000::');
142
+ // Should return false (empty salt) or throw — either way not true
143
+ expect(result).not.toBe(true);
144
+ });
145
+
146
+ it('tampered iteration count → false', async () => {
147
+ const hash = await hashPassword('correct');
148
+ const parts = hash.split(':');
149
+ // Replace 100000 with 100 — different iterations produce different derived key
150
+ const tampered = [parts[0], parts[1], '100', parts[3], parts[4]].join(':');
151
+ expect(await verifyPassword('correct', tampered)).toBe(false);
152
+ });
153
+
154
+ it('swapped salt and hash parts → false', async () => {
155
+ const hash = await hashPassword('correct');
156
+ const parts = hash.split(':');
157
+ // Swap parts[3] (salt) and parts[4] (hash)
158
+ const swapped = [parts[0], parts[1], parts[2], parts[4], parts[3]].join(':');
159
+ expect(await verifyPassword('correct', swapped)).toBe(false);
160
+ });
161
+
162
+ it('truncated hash (length mismatch) → false', async () => {
163
+ const hash = await hashPassword('correct');
164
+ const parts = hash.split(':');
165
+ // Shorten the hash part to trigger length mismatch in constant-time comparison
166
+ const truncated = [parts[0], parts[1], parts[2], parts[3], 'AA'].join(':');
167
+ expect(await verifyPassword('correct', truncated)).toBe(false);
168
+ });
169
+
170
+ it('extra parts in hash (6 segments) → false', async () => {
171
+ const hash = await hashPassword('correct');
172
+ const result = await verifyPassword('correct', hash + ':extra');
173
+ expect(result).toBe(false);
174
+ });
175
+
176
+ it('unknown hash format → false', async () => {
177
+ expect(await verifyPassword('test', 'scrypt:sha256:100:salt:hash')).toBe(false);
178
+ });
179
+
180
+ it('$2c$ prefix (not recognized bcrypt variant) → false', async () => {
181
+ expect(await verifyPassword('test', '$2c$10$abcdefghijklmnopqrstuuxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')).toBe(false);
182
+ });
183
+ });
184
+
185
+ // ─── C. needsRehash ──────────────────────────────────────────────────────────
186
+
187
+ describe('needsRehash', () => {
188
+ it('pbkdf2 hash → false (no rehash needed)', async () => {
189
+ const hash = await hashPassword('test');
190
+ expect(needsRehash(hash)).toBe(false);
191
+ });
192
+
193
+ it('bcrypt $2a$ hash → true (needs upgrade)', () => {
194
+ expect(needsRehash('$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')).toBe(true);
195
+ });
196
+
197
+ it('bcrypt $2b$ hash → true', () => {
198
+ expect(needsRehash('$2b$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')).toBe(true);
199
+ });
200
+
201
+ it('bcrypt $2y$ hash → true', () => {
202
+ expect(needsRehash('$2y$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')).toBe(true);
203
+ });
204
+
205
+ it('empty string → true', () => {
206
+ expect(needsRehash('')).toBe(true);
207
+ });
208
+
209
+ it('random string → true', () => {
210
+ expect(needsRehash('not-a-real-hash')).toBe(true);
211
+ });
212
+ });
213
+
214
+ // ─── D. isPasswordHash ──────────────────────────────────────────────────────
215
+
216
+ describe('isPasswordHash', () => {
217
+ it('pbkdf2:sha256: prefix → true', async () => {
218
+ const hash = await hashPassword('test');
219
+ expect(isPasswordHash(hash)).toBe(true);
220
+ });
221
+
222
+ it('$2a$ bcrypt → true', () => {
223
+ expect(isPasswordHash('$2a$10$abcdefghijklmnop')).toBe(true);
224
+ });
225
+
226
+ it('$2b$ bcrypt → true', () => {
227
+ expect(isPasswordHash('$2b$10$abcdefghijklmnop')).toBe(true);
228
+ });
229
+
230
+ it('$2y$ bcrypt → true', () => {
231
+ expect(isPasswordHash('$2y$10$abcdefghijklmnop')).toBe(true);
232
+ });
233
+
234
+ it('plaintext password → false', () => {
235
+ expect(isPasswordHash('MyPassword123!')).toBe(false);
236
+ });
237
+
238
+ it('empty string → false', () => {
239
+ expect(isPasswordHash('')).toBe(false);
240
+ });
241
+
242
+ it('pbkdf2: without sha256 → false', () => {
243
+ expect(isPasswordHash('pbkdf2:sha512:100000:salt:hash')).toBe(false);
244
+ });
245
+
246
+ it('$2c$ not recognized → false', () => {
247
+ expect(isPasswordHash('$2c$10$abcdefghijklmnop')).toBe(false);
248
+ });
249
+ });
250
+
251
+ // ─── E. verifyPassword — bcrypt mutation-killing ────────────────────────────
252
+ // Kills 11 survived mutants on line 78 (bcrypt prefix detection) and 2 on line 73
253
+
254
+ describe('verifyPassword — real bcrypt hashes', () => {
255
+ // Use low cost (4) for speed — these test dispatch logic, not bcrypt strength
256
+ const COST = 4;
257
+
258
+ it('$2b$ real hash + correct password → true', async () => {
259
+ const hash = bcrypt.hashSync('TestPw1!', COST);
260
+ expect(hash).toMatch(/^\$2[ab]\$/);
261
+ expect(await verifyPassword('TestPw1!', hash)).toBe(true);
262
+ });
263
+
264
+ it('$2b$ real hash + wrong password → false', async () => {
265
+ const hash = bcrypt.hashSync('TestPw1!', COST);
266
+ expect(await verifyPassword('WrongPw1!', hash)).toBe(false);
267
+ });
268
+
269
+ it('$2a$ real hash + correct password → true', async () => {
270
+ // bcryptjs v3 defaults to $2b$; convert to $2a$ (compatible variant)
271
+ const hash = bcrypt.hashSync('BcryptA!', COST);
272
+ const a2Hash = hash.replace('$2b$', '$2a$');
273
+ expect(a2Hash).toMatch(/^\$2a\$/);
274
+ expect(await verifyPassword('BcryptA!', a2Hash)).toBe(true);
275
+ });
276
+
277
+ it('pbkdf2 hash dispatches to PBKDF2 path (not bcrypt)', async () => {
278
+ // If line 73 is mutated to `if (true)`, ALL hashes go through verifyPBKDF2.
279
+ // A bcrypt hash in PBKDF2 path → always false.
280
+ // So we need: pbkdf2 correct + bcrypt correct in same test suite.
281
+ const pbkdf2Hash = await hashPassword('TestPw1!');
282
+ expect(await verifyPassword('TestPw1!', pbkdf2Hash)).toBe(true);
283
+
284
+ const bcryptHash = bcrypt.hashSync('TestPw1!', COST);
285
+ expect(await verifyPassword('TestPw1!', bcryptHash)).toBe(true);
286
+ });
287
+
288
+ it('bcrypt hash is NOT handled by PBKDF2 verifier', async () => {
289
+ // Specifically ensures line 73 `startsWith("pbkdf2:")` check matters
290
+ const bcryptHash = bcrypt.hashSync('hello', COST);
291
+ // bcrypt hash does NOT start with 'pbkdf2:', so must go through bcrypt path
292
+ expect(bcryptHash.startsWith('pbkdf2:')).toBe(false);
293
+ expect(await verifyPassword('hello', bcryptHash)).toBe(true);
294
+ });
295
+ });
296
+
297
+ // ─── F. verifyPBKDF2 — parts validation mutation-killing ────────────────────
298
+ // Kills 2 survived mutants on line 108 (independent parts[0] and parts[1] checks)
299
+
300
+ describe('verifyPBKDF2 — independent parts validation', () => {
301
+ it('parts[0] = "argon2" but parts[1] = "sha256" → false', async () => {
302
+ // Kills mutation: parts[0] !== "pbkdf2" check → if (false)
303
+ const hash = await hashPassword('test');
304
+ const parts = hash.split(':');
305
+ const tampered = ['argon2', parts[1], parts[2], parts[3], parts[4]].join(':');
306
+ expect(await verifyPassword('test', tampered)).toBe(false);
307
+ });
308
+
309
+ it('parts[0] = "pbkdf2" but parts[1] = "sha384" → false', async () => {
310
+ // Kills mutation: parts[1] !== "sha256" check → if (false)
311
+ const hash = await hashPassword('test');
312
+ const parts = hash.split(':');
313
+ const tampered = [parts[0], 'sha384', parts[2], parts[3], parts[4]].join(':');
314
+ expect(await verifyPassword('test', tampered)).toBe(false);
315
+ });
316
+
317
+ it('parts[0] changed only → false (parts[1] intact)', async () => {
318
+ const hash = await hashPassword('pw');
319
+ const parts = hash.split(':');
320
+ parts[0] = 'scrypt';
321
+ // Starts with 'scrypt:' not 'pbkdf2:' — falls through to unknown format
322
+ expect(await verifyPassword('pw', parts.join(':'))).toBe(false);
323
+ });
324
+ });
325
+
326
+ // ─── G. Constant-time comparison — mutation-killing ─────────────────────────
327
+ // Kills survived mutant on line 137 (length check → if (false))
328
+
329
+ describe('verifyPBKDF2 — constant-time comparison edge cases', () => {
330
+ it('half-length hash part (correct password) → false', async () => {
331
+ // Construct a hash where the hash portion is exactly half length (16 bytes).
332
+ // With line 137 mutation (if (false)), XOR loop would read undefined bytes
333
+ // from expectedHash, producing NaN→0 in bitwise ops → false positive.
334
+ const hash = await hashPassword('correct');
335
+ const parts = hash.split(':');
336
+ // 32 bytes base64 → 44 chars. Take first 24 chars = 16 bytes.
337
+ const halfHash = parts[4].substring(0, 24);
338
+ // Pad to valid base64 if needed
339
+ const padded = halfHash.length % 4 === 0 ? halfHash : halfHash + '='.repeat(4 - (halfHash.length % 4));
340
+ const truncated = [parts[0], parts[1], parts[2], parts[3], padded].join(':');
341
+ expect(await verifyPassword('correct', truncated)).toBe(false);
342
+ });
343
+
344
+ it('extended hash part (48 bytes vs 32) → false', async () => {
345
+ const hash = await hashPassword('correct');
346
+ const parts = hash.split(':');
347
+ // 48 bytes = valid base64 of 64 chars. Build a standalone valid base64 string.
348
+ const extended = btoa(String.fromCharCode(...new Uint8Array(48)));
349
+ const oversized = [parts[0], parts[1], parts[2], parts[3], extended].join(':');
350
+ expect(await verifyPassword('correct', oversized)).toBe(false);
351
+ });
352
+
353
+ it('single-byte hash part → false', async () => {
354
+ const hash = await hashPassword('correct');
355
+ const parts = hash.split(':');
356
+ // Single byte base64: 'QQ==' = [0x41]
357
+ const single = [parts[0], parts[1], parts[2], parts[3], 'QQ=='].join(':');
358
+ expect(await verifyPassword('correct', single)).toBe(false);
359
+ });
360
+
361
+ it('empty base64 hash part → false', async () => {
362
+ const hash = await hashPassword('correct');
363
+ const parts = hash.split(':');
364
+ const empty = [parts[0], parts[1], parts[2], parts[3], ''].join(':');
365
+ expect(await verifyPassword('correct', empty)).toBe(false);
366
+ });
367
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defineConfig } from '@edge-base/shared';
3
+ import {
4
+ appendRedirectParams,
5
+ buildEmailActionUrl,
6
+ parseClientRedirectInput,
7
+ parseClientRedirectState,
8
+ parseClientRedirectUrl,
9
+ } from '../lib/auth-redirect.js';
10
+
11
+ const envWithAllowList = {
12
+ EDGEBASE_CONFIG: defineConfig({
13
+ auth: {
14
+ allowedRedirectUrls: [
15
+ 'https://app.example.com',
16
+ 'https://preview.example.com/*',
17
+ ],
18
+ },
19
+ }),
20
+ } as const;
21
+
22
+ describe('auth redirect helpers', () => {
23
+ it('appends only present redirect params', () => {
24
+ expect(
25
+ appendRedirectParams('https://app.example.com/auth/callback', {
26
+ token: 'tok_123',
27
+ type: 'magic-link',
28
+ state: '',
29
+ }),
30
+ ).toBe('https://app.example.com/auth/callback#token=tok_123&type=magic-link');
31
+ });
32
+
33
+ it('accepts exact and wildcard allowed redirect URLs', () => {
34
+ expect(
35
+ parseClientRedirectUrl(envWithAllowList as never, 'https://app.example.com/auth/callback'),
36
+ ).toBe('https://app.example.com/auth/callback');
37
+ expect(
38
+ parseClientRedirectUrl(envWithAllowList as never, 'https://preview.example.com/review/123'),
39
+ ).toBe('https://preview.example.com/review/123');
40
+ });
41
+
42
+ it('rejects redirect URLs outside the allow list', () => {
43
+ expect(() =>
44
+ parseClientRedirectUrl(envWithAllowList as never, 'https://evil.example.com/auth/callback'),
45
+ ).toThrow('redirect_url is not allowed');
46
+ });
47
+
48
+ it('validates client redirect state length and null handling', () => {
49
+ expect(parseClientRedirectState(null)).toBeNull();
50
+ expect(() => parseClientRedirectState('x'.repeat(1025))).toThrow('state must not exceed 1024 characters');
51
+ });
52
+
53
+ it('parses redirect input and preserves state', () => {
54
+ expect(
55
+ parseClientRedirectInput(envWithAllowList as never, {
56
+ redirectUrl: 'https://app.example.com/auth/callback',
57
+ state: 'return-to-dashboard',
58
+ }),
59
+ ).toEqual({
60
+ redirectUrl: 'https://app.example.com/auth/callback',
61
+ state: 'return-to-dashboard',
62
+ });
63
+ });
64
+
65
+ it('builds redirect URLs only when a client redirect is present', () => {
66
+ expect(
67
+ buildEmailActionUrl({
68
+ redirectUrl: null,
69
+ fallbackUrl: 'https://edgebase.example.com/auth/fallback',
70
+ token: 'tok_123',
71
+ type: 'verify-email',
72
+ }),
73
+ ).toBe('https://edgebase.example.com/auth/fallback');
74
+
75
+ expect(
76
+ buildEmailActionUrl({
77
+ redirectUrl: 'https://app.example.com/auth/callback',
78
+ fallbackUrl: 'https://edgebase.example.com/auth/fallback',
79
+ token: 'tok_123',
80
+ type: 'verify-email',
81
+ state: 'from-test',
82
+ }),
83
+ ).toBe(
84
+ 'https://app.example.com/auth/callback#token=tok_123&type=verify-email&state=from-test',
85
+ );
86
+ });
87
+ });