@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,337 @@
1
+ /**
2
+ * Unit tests for lib/oauth-providers.ts
3
+ *
4
+ * Covers parseOIDCIdToken (pure JWT decode) and prefetchOIDCDiscovery (fetch mock).
5
+ * Also tests parseAppleIdToken, isSupportedProvider, getOAuthProviderConfig, getAllowedOAuthProviders.
6
+ */
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import {
9
+ parseOIDCIdToken,
10
+ prefetchOIDCDiscovery,
11
+ parseAppleIdToken,
12
+ isSupportedProvider,
13
+ getOAuthProviderConfig,
14
+ getAllowedOAuthProviders,
15
+ } from '../lib/oauth-providers.js';
16
+
17
+ // ─── Helpers ───
18
+
19
+ /** Create a fake JWT with given payload (no signature verification needed) */
20
+ function fakeJwt(payload: Record<string, unknown>): string {
21
+ const header = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
22
+ const body = btoa(JSON.stringify(payload));
23
+ const sig = 'fake-signature';
24
+ return `${header}.${body}.${sig}`;
25
+ }
26
+
27
+ /** Create a fake JWT using base64url (with - and _ instead of + and /) */
28
+ function fakeJwtUrl(payload: Record<string, unknown>): string {
29
+ const header = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' }))
30
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
31
+ const body = btoa(JSON.stringify(payload))
32
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
33
+ const sig = 'fake-signature';
34
+ return `${header}.${body}.${sig}`;
35
+ }
36
+
37
+ // ─── parseOIDCIdToken ───
38
+
39
+ describe('parseOIDCIdToken', () => {
40
+ it('decodes a valid JWT payload', () => {
41
+ const token = fakeJwt({
42
+ sub: 'user-123',
43
+ email: 'test@example.com',
44
+ email_verified: true,
45
+ name: 'Test User',
46
+ picture: 'https://example.com/avatar.png',
47
+ });
48
+ const result = parseOIDCIdToken(token);
49
+
50
+ expect(result.providerUserId).toBe('user-123');
51
+ expect(result.email).toBe('test@example.com');
52
+ expect(result.emailVerified).toBe(true);
53
+ expect(result.displayName).toBe('Test User');
54
+ expect(result.avatarUrl).toBe('https://example.com/avatar.png');
55
+ expect(result.raw.sub).toBe('user-123');
56
+ });
57
+
58
+ it('falls back to preferred_username when name is absent', () => {
59
+ const token = fakeJwt({
60
+ sub: 'u-456',
61
+ preferred_username: 'jdoe',
62
+ });
63
+ const result = parseOIDCIdToken(token);
64
+ expect(result.displayName).toBe('jdoe');
65
+ });
66
+
67
+ it('returns null for missing optional fields', () => {
68
+ const token = fakeJwt({ sub: 'u-789' });
69
+ const result = parseOIDCIdToken(token);
70
+
71
+ expect(result.providerUserId).toBe('u-789');
72
+ expect(result.email).toBeNull();
73
+ expect(result.emailVerified).toBe(false);
74
+ expect(result.displayName).toBeNull();
75
+ expect(result.avatarUrl).toBeNull();
76
+ });
77
+
78
+ it('handles base64url encoded tokens (- and _ chars)', () => {
79
+ const token = fakeJwtUrl({
80
+ sub: 'url-user',
81
+ email: 'url@test.com',
82
+ email_verified: true,
83
+ });
84
+ const result = parseOIDCIdToken(token);
85
+ expect(result.providerUserId).toBe('url-user');
86
+ expect(result.email).toBe('url@test.com');
87
+ });
88
+
89
+ it('throws on invalid token (not 3 parts)', () => {
90
+ expect(() => parseOIDCIdToken('only-two.parts')).toThrow('Invalid OIDC id_token');
91
+ expect(() => parseOIDCIdToken('')).toThrow('Invalid OIDC id_token');
92
+ expect(() => parseOIDCIdToken('a.b.c.d')).toThrow('Invalid OIDC id_token');
93
+ });
94
+
95
+ it('preserves raw payload with all original fields', () => {
96
+ const token = fakeJwt({
97
+ sub: 'u1',
98
+ iss: 'https://issuer.example.com',
99
+ aud: 'client-id',
100
+ exp: 1234567890,
101
+ custom_field: 'custom_value',
102
+ });
103
+ const result = parseOIDCIdToken(token);
104
+ expect(result.raw.iss).toBe('https://issuer.example.com');
105
+ expect(result.raw.custom_field).toBe('custom_value');
106
+ });
107
+ });
108
+
109
+ // ─── prefetchOIDCDiscovery ───
110
+
111
+ describe('prefetchOIDCDiscovery', () => {
112
+ const originalFetch = globalThis.fetch;
113
+
114
+ beforeEach(() => {
115
+ // Clear the module-level discovery cache by importing fresh
116
+ vi.stubGlobal('fetch', vi.fn());
117
+ });
118
+
119
+ afterEach(() => {
120
+ globalThis.fetch = originalFetch;
121
+ vi.restoreAllMocks();
122
+ });
123
+
124
+ it('fetches .well-known/openid-configuration from issuer', async () => {
125
+ const mockDiscovery = {
126
+ authorization_endpoint: 'https://issuer.example.com/authorize',
127
+ token_endpoint: 'https://issuer.example.com/token',
128
+ userinfo_endpoint: 'https://issuer.example.com/userinfo',
129
+ issuer: 'https://issuer.example.com',
130
+ };
131
+
132
+ (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
133
+ new Response(JSON.stringify(mockDiscovery), { status: 200 }),
134
+ );
135
+
136
+ // Should not throw
137
+ await prefetchOIDCDiscovery('https://issuer.example.com');
138
+
139
+ expect(globalThis.fetch).toHaveBeenCalledWith(
140
+ 'https://issuer.example.com/.well-known/openid-configuration',
141
+ { headers: { Accept: 'application/json' } },
142
+ );
143
+ });
144
+
145
+ it('strips trailing slash from issuer URL', async () => {
146
+ const mockDiscovery = {
147
+ authorization_endpoint: 'https://issuer.example.com/authorize',
148
+ token_endpoint: 'https://issuer.example.com/token',
149
+ issuer: 'https://issuer.example.com',
150
+ };
151
+
152
+ (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
153
+ new Response(JSON.stringify(mockDiscovery), { status: 200 }),
154
+ );
155
+
156
+ await prefetchOIDCDiscovery('https://issuer.example.com/');
157
+
158
+ expect(globalThis.fetch).toHaveBeenCalledWith(
159
+ 'https://issuer.example.com/.well-known/openid-configuration',
160
+ expect.any(Object),
161
+ );
162
+ });
163
+
164
+ it('throws on non-OK response', async () => {
165
+ (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
166
+ new Response('Not Found', { status: 404, statusText: 'Not Found' }),
167
+ );
168
+
169
+ await expect(
170
+ prefetchOIDCDiscovery('https://bad-issuer.example.com'),
171
+ ).rejects.toThrow('OIDC discovery failed');
172
+ });
173
+
174
+ it('throws when discovery doc is missing required endpoints', async () => {
175
+ const incomplete = { issuer: 'https://issuer.example.com' };
176
+
177
+ (globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
178
+ new Response(JSON.stringify(incomplete), { status: 200 }),
179
+ );
180
+
181
+ await expect(
182
+ prefetchOIDCDiscovery('https://incomplete.example.com'),
183
+ ).rejects.toThrow('missing required endpoints');
184
+ });
185
+ });
186
+
187
+ // ─── parseAppleIdToken ───
188
+
189
+ describe('parseAppleIdToken', () => {
190
+ it('decodes Apple JWT payload', () => {
191
+ const token = fakeJwt({
192
+ sub: 'apple-user-001',
193
+ email: 'apple@privaterelay.appleid.com',
194
+ email_verified: true,
195
+ });
196
+ const result = parseAppleIdToken(token);
197
+
198
+ expect(result.providerUserId).toBe('apple-user-001');
199
+ expect(result.email).toBe('apple@privaterelay.appleid.com');
200
+ expect(result.emailVerified).toBe(true);
201
+ expect(result.displayName).toBeNull(); // Apple sends name only on first sign-in
202
+ expect(result.avatarUrl).toBeNull();
203
+ });
204
+
205
+ it('throws on invalid token', () => {
206
+ expect(() => parseAppleIdToken('invalid')).toThrow('Invalid Apple id_token');
207
+ });
208
+ });
209
+
210
+ // ─── isSupportedProvider ───
211
+
212
+ describe('isSupportedProvider', () => {
213
+ it.each([
214
+ 'google', 'github', 'apple', 'discord',
215
+ 'microsoft', 'facebook', 'kakao', 'naver',
216
+ 'x', 'reddit', 'line', 'slack', 'spotify', 'twitch',
217
+ ])('returns true for %s', (provider) => {
218
+ expect(isSupportedProvider(provider)).toBe(true);
219
+ });
220
+
221
+ it('returns true for oidc: prefixed providers', () => {
222
+ expect(isSupportedProvider('oidc:okta')).toBe(true);
223
+ expect(isSupportedProvider('oidc:auth0')).toBe(true);
224
+ });
225
+
226
+ it('returns false for oidc: with no name', () => {
227
+ expect(isSupportedProvider('oidc:')).toBe(false);
228
+ });
229
+
230
+ it('returns false for unknown providers', () => {
231
+ expect(isSupportedProvider('myspace')).toBe(false);
232
+ expect(isSupportedProvider('')).toBe(false);
233
+ });
234
+ });
235
+
236
+ // ─── getOAuthProviderConfig ───
237
+
238
+ describe('getOAuthProviderConfig', () => {
239
+ it('returns null for undefined config', () => {
240
+ expect(getOAuthProviderConfig(undefined, 'google')).toBeNull();
241
+ });
242
+
243
+ it('returns null for invalid JSON', () => {
244
+ expect(getOAuthProviderConfig('not-json', 'google')).toBeNull();
245
+ });
246
+
247
+ it('returns config for built-in provider', () => {
248
+ const config = JSON.stringify({
249
+ auth: { oauth: { google: { clientId: 'gid', clientSecret: 'gsec' } } },
250
+ });
251
+ const result = getOAuthProviderConfig(config, 'google');
252
+ expect(result).toEqual({ clientId: 'gid', clientSecret: 'gsec' });
253
+ });
254
+
255
+ it('returns null when provider config is missing clientId', () => {
256
+ const config = JSON.stringify({
257
+ auth: { oauth: { google: { clientSecret: 'gsec' } } },
258
+ });
259
+ expect(getOAuthProviderConfig(config, 'google')).toBeNull();
260
+ });
261
+
262
+ it('returns OIDC config with issuer', () => {
263
+ const config = JSON.stringify({
264
+ auth: {
265
+ oauth: {
266
+ oidc: {
267
+ okta: {
268
+ clientId: 'oid',
269
+ clientSecret: 'osec',
270
+ issuer: 'https://okta.example.com',
271
+ scopes: ['openid', 'profile'],
272
+ },
273
+ },
274
+ },
275
+ },
276
+ });
277
+ const result = getOAuthProviderConfig(config, 'oidc:okta');
278
+ expect(result).toEqual({
279
+ clientId: 'oid',
280
+ clientSecret: 'osec',
281
+ issuer: 'https://okta.example.com',
282
+ scopes: ['openid', 'profile'],
283
+ });
284
+ });
285
+
286
+ it('returns null for OIDC missing issuer', () => {
287
+ const config = JSON.stringify({
288
+ auth: { oauth: { oidc: { okta: { clientId: 'oid', clientSecret: 'osec' } } } },
289
+ });
290
+ expect(getOAuthProviderConfig(config, 'oidc:okta')).toBeNull();
291
+ });
292
+ });
293
+
294
+ describe('getOAuthProviderConfig', () => {
295
+ it('accepts already-materialized config objects', () => {
296
+ const config = {
297
+ auth: { oauth: { google: { clientId: 'runtime-id', clientSecret: 'runtime-secret' } } },
298
+ };
299
+
300
+ expect(getOAuthProviderConfig(config, 'google')).toEqual({
301
+ clientId: 'runtime-id',
302
+ clientSecret: 'runtime-secret',
303
+ });
304
+ });
305
+
306
+ it('returns null when provider is missing from config object', () => {
307
+ const config = {
308
+ auth: { oauth: { github: { clientId: 'gh', clientSecret: 'gs' } } },
309
+ };
310
+
311
+ expect(getOAuthProviderConfig(config, 'google')).toBeNull();
312
+ });
313
+ });
314
+
315
+ // ─── getAllowedOAuthProviders ───
316
+
317
+ describe('getAllowedOAuthProviders', () => {
318
+ it('returns empty array for undefined config', () => {
319
+ expect(getAllowedOAuthProviders(undefined)).toEqual([]);
320
+ });
321
+
322
+ it('returns empty array for invalid JSON', () => {
323
+ expect(getAllowedOAuthProviders('bad')).toEqual([]);
324
+ });
325
+
326
+ it('returns empty array when allowedOAuthProviders is not an array', () => {
327
+ const config = JSON.stringify({ auth: { allowedOAuthProviders: 'google' } });
328
+ expect(getAllowedOAuthProviders(config)).toEqual([]);
329
+ });
330
+
331
+ it('filters out unsupported providers', () => {
332
+ const config = JSON.stringify({
333
+ auth: { allowedOAuthProviders: ['google', 'myspace', 'github', 'oidc:okta'] },
334
+ });
335
+ expect(getAllowedOAuthProviders(config)).toEqual(['google', 'github', 'oidc:okta']);
336
+ });
337
+ });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * OpenAPI route coverage — static source scan.
3
+ *
4
+ * Ensures every route registration in src/routes/ uses .openapi() instead of
5
+ * plain .get()/.post() etc. Non-openapi routes won't appear in /openapi.json,
6
+ * creating a silent gap between the spec and actual server behavior.
7
+ *
8
+ * Intentionally excluded routes (WebSocket upgrades, wildcard proxies, etc.)
9
+ * must be listed in ALLOWED_NON_OPENAPI so the exclusion is explicit and
10
+ * reviewed. Adding a new non-openapi route without updating the allowlist
11
+ * will fail CI.
12
+ */
13
+ import { readFileSync, readdirSync } from 'fs';
14
+ import { resolve } from 'path';
15
+ import { describe, it, expect } from 'vitest';
16
+ import { normalizeOpenApiDocument, type OpenApiSpec } from '../lib/openapi.js';
17
+
18
+ const ROUTES_DIR = resolve(new URL('../routes', import.meta.url).pathname);
19
+
20
+ // ─── Intentionally non-OpenAPI route registrations ───────────────────────────
21
+ // Format: "filename.ts:<line-content-substring>"
22
+ // Each entry must include enough of the line to be unique.
23
+ // When you remove a non-openapi route, remove the entry here too.
24
+ const ALLOWED_NON_OPENAPI = new Set([
25
+ // User-defined function HTTP trigger — wildcard .all(), cannot be expressed as a fixed OpenAPI path
26
+ 'functions.ts:functionsRoute.all(',
27
+ ]);
28
+
29
+ // ─── Detect .get/.post/.put/.delete/.patch/.all route registrations ──────────
30
+ // Matches: varName.get('/path' or varName.post("/path" etc.
31
+ // Excludes: c.get('auth'), formData.get('file'), etc. by requiring a path-like string (starts with /)
32
+ const ROUTE_REG_PATTERN = /\w+\.(get|post|put|delete|patch|all)\(\s*['"][/]/g;
33
+
34
+ // Exclude .openapi( calls — these are already in the spec
35
+ const OPENAPI_CALL_PATTERN = /\.openapi\s*\(/;
36
+
37
+ function extractNonOpenapiRoutes(source: string, filename: string) {
38
+ const lines = source.split('\n');
39
+ const violations: { file: string; line: number; text: string }[] = [];
40
+
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i];
43
+ // Skip lines that are openapi registrations
44
+ if (OPENAPI_CALL_PATTERN.test(line)) continue;
45
+ // Skip comments
46
+ if (line.trimStart().startsWith('//') || line.trimStart().startsWith('*')) continue;
47
+
48
+ if (ROUTE_REG_PATTERN.test(line)) {
49
+ // Check if this is in the allowlist
50
+ const isAllowed = [...ALLOWED_NON_OPENAPI].some(
51
+ (entry) => {
52
+ const [file, substr] = entry.split(':');
53
+ return filename === file && line.includes(substr);
54
+ },
55
+ );
56
+ if (!isAllowed) {
57
+ violations.push({ file: filename, line: i + 1, text: line.trim() });
58
+ }
59
+ }
60
+ // Reset regex lastIndex
61
+ ROUTE_REG_PATTERN.lastIndex = 0;
62
+ }
63
+
64
+ return violations;
65
+ }
66
+
67
+ describe('OpenAPI route coverage', () => {
68
+ const routeFiles = readdirSync(ROUTES_DIR).filter((f) => f.endsWith('.ts'));
69
+
70
+ it('all route registrations use .openapi() or are in ALLOWED_NON_OPENAPI', () => {
71
+ const allViolations: { file: string; line: number; text: string }[] = [];
72
+
73
+ for (const file of routeFiles) {
74
+ const source = readFileSync(resolve(ROUTES_DIR, file), 'utf-8');
75
+ const violations = extractNonOpenapiRoutes(source, file);
76
+ allViolations.push(...violations);
77
+ }
78
+
79
+ if (allViolations.length > 0) {
80
+ const report = allViolations
81
+ .map((v) => ` ${v.file}:${v.line} → ${v.text}`)
82
+ .join('\n');
83
+ expect.fail(
84
+ `Found ${allViolations.length} route registration(s) not using .openapi().\n` +
85
+ `These routes will NOT appear in /openapi.json.\n` +
86
+ `Either convert to createRoute() + .openapi(), or add to ALLOWED_NON_OPENAPI.\n\n${report}`,
87
+ );
88
+ }
89
+ });
90
+
91
+ it('ALLOWED_NON_OPENAPI entries are still valid', () => {
92
+ // Ensure every entry in the allowlist still matches a real line in the source.
93
+ // Stale entries must be removed.
94
+ for (const entry of ALLOWED_NON_OPENAPI) {
95
+ const [file, substr] = entry.split(':');
96
+ const filePath = resolve(ROUTES_DIR, file);
97
+ let source: string;
98
+ try {
99
+ source = readFileSync(filePath, 'utf-8');
100
+ } catch {
101
+ expect.fail(
102
+ `ALLOWED_NON_OPENAPI contains '${entry}' but file '${file}' does not exist. Remove it.`,
103
+ );
104
+ return;
105
+ }
106
+ expect(
107
+ source.includes(substr),
108
+ `ALLOWED_NON_OPENAPI contains '${entry}' but '${substr}' is no longer found in ${file}. Remove it.`,
109
+ ).toBe(true);
110
+ }
111
+ });
112
+
113
+ it('normalizes security schemes and path-level auth requirements', () => {
114
+ const spec: OpenApiSpec = {
115
+ paths: {
116
+ '/api/auth/me': { get: {} },
117
+ '/api/sql': { post: {} },
118
+ '/admin/api/setup': { get: {} },
119
+ '/admin/api/data/users': { get: {} },
120
+ '/api/room/media/realtime/session': { post: {} },
121
+ },
122
+ };
123
+
124
+ const normalized = normalizeOpenApiDocument(spec, 'https://edgebase.example');
125
+ const schemes = normalized.components?.securitySchemes ?? {};
126
+
127
+ expect(normalized.servers).toEqual([
128
+ { url: 'https://edgebase.example', description: 'Current EdgeBase instance' },
129
+ ]);
130
+ expect(schemes).toHaveProperty('adminBearerAuth');
131
+ expect(schemes).toHaveProperty('userBearerAuth');
132
+ expect(schemes).toHaveProperty('serviceKeyAuth');
133
+ expect((normalized.paths?.['/api/auth/me'] as Record<string, { security?: unknown }>).get.security)
134
+ .toEqual([{ userBearerAuth: [] }]);
135
+ expect((normalized.paths?.['/api/room/media/realtime/session'] as Record<string, { security?: unknown }>).post.security)
136
+ .toEqual([{ userBearerAuth: [] }]);
137
+ expect((normalized.paths?.['/api/sql'] as Record<string, { security?: unknown }>).post.security)
138
+ .toEqual([{ serviceKeyAuth: [] }]);
139
+ expect((normalized.paths?.['/admin/api/setup'] as Record<string, { security?: unknown }>).get.security)
140
+ .toBeUndefined();
141
+ expect((normalized.paths?.['/admin/api/data/users'] as Record<string, { security?: unknown }>).get.security)
142
+ .toEqual([{ adminBearerAuth: [] }, { serviceKeyAuth: [] }]);
143
+ });
144
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Regression tests for pagination parameter validation.
3
+ *
4
+ * Key regression: unvalidated parseInt() allowed NaN, negative,
5
+ * and absurdly large values to reach SQL queries.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import { parsePagination } from '../lib/pagination.js';
9
+
10
+ describe('parsePagination', () => {
11
+ // ── Defaults ──
12
+ it('returns defaults when no params provided', () => {
13
+ expect(parsePagination(undefined, undefined)).toEqual({ limit: 20, offset: 0 });
14
+ });
15
+
16
+ // ── Valid values ──
17
+ it('accepts valid limit and offset', () => {
18
+ expect(parsePagination('50', '10')).toEqual({ limit: 50, offset: 10 });
19
+ });
20
+
21
+ it('accepts limit=1 (minimum)', () => {
22
+ expect(parsePagination('1', '0')).toEqual({ limit: 1, offset: 0 });
23
+ });
24
+
25
+ // ── Limit clamping ──
26
+ it('clamps limit to 100', () => {
27
+ expect(parsePagination('999', '0')).toEqual({ limit: 100, offset: 0 });
28
+ });
29
+
30
+ it('rejects limit=0 (falls back to default)', () => {
31
+ expect(parsePagination('0', '0')).toEqual({ limit: 20, offset: 0 });
32
+ });
33
+
34
+ // ── REGRESSION: negative and NaN values ──
35
+ it('rejects negative limit', () => {
36
+ expect(parsePagination('-5', '0')).toEqual({ limit: 20, offset: 0 });
37
+ });
38
+
39
+ it('rejects NaN limit', () => {
40
+ expect(parsePagination('abc', '0')).toEqual({ limit: 20, offset: 0 });
41
+ });
42
+
43
+ it('rejects negative offset', () => {
44
+ expect(parsePagination('20', '-10')).toEqual({ limit: 20, offset: 0 });
45
+ });
46
+
47
+ it('rejects NaN offset', () => {
48
+ expect(parsePagination('20', 'xyz')).toEqual({ limit: 20, offset: 0 });
49
+ });
50
+
51
+ it('rejects Infinity limit', () => {
52
+ expect(parsePagination('Infinity', '0')).toEqual({ limit: 20, offset: 0 });
53
+ });
54
+
55
+ // ── Empty string (same as undefined) ──
56
+ it('treats empty strings as defaults', () => {
57
+ expect(parsePagination('', '')).toEqual({ limit: 20, offset: 0 });
58
+ });
59
+ });