@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,234 @@
1
+ /**
2
+ * 서버 단위 테스트 — rules 평가 로직 + database-live broadcast
3
+ * 1-14 rules.test.ts — 80개 (순수 로직 파트)
4
+ * 1-19 broadcast.test.ts — 20개
5
+ *
6
+ * 실행: cd packages/server && npx vitest run src/__tests__/rules.test.ts
7
+ *
8
+ * NOTE: DB 규칙 평가는 DatabaseDO 내부에서 이루어지지만,
9
+ * 규칙 함수 자체(JS 함수)의 동작 로직은 여기서 순수 테스트
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+
14
+ // ─── 규칙 평가 로직 (DatabaseDO rules 동일 패턴) ─────────────────────────────
15
+
16
+ type AuthCtx = { id: string; role: string | null; email: string | null } | null;
17
+ type ResourceCtx = Record<string, unknown>;
18
+ type ContextCtx = Record<string, unknown>;
19
+
20
+ type RuleValue = boolean | string | ((auth: AuthCtx, resource: ResourceCtx, ctx: ContextCtx) => boolean);
21
+
22
+ function evaluateRule(
23
+ rule: RuleValue,
24
+ auth: AuthCtx,
25
+ resource: ResourceCtx,
26
+ ctx: ContextCtx,
27
+ ): boolean {
28
+ if (rule === true) return true;
29
+ if (rule === false) return false;
30
+ if (typeof rule === 'function') {
31
+ try {
32
+ return Boolean(rule(auth, resource, ctx));
33
+ } catch {
34
+ return false; // 규칙 평가 오류 → 거부
35
+ }
36
+ }
37
+ // 문자열 규칙은 DB 레벨에서 평가; 여기선 다루지 않음
38
+ return false;
39
+ }
40
+
41
+ // ─── A. true / false 규칙 ────────────────────────────────────────────────────
42
+
43
+ describe('evaluateRule — boolean', () => {
44
+ it('true → always allow', () => {
45
+ expect(evaluateRule(true, null, {}, {})).toBe(true);
46
+ });
47
+
48
+ it('false → always deny', () => {
49
+ expect(evaluateRule(false, null, {}, {})).toBe(false);
50
+ });
51
+
52
+ it('true with authenticated user → allow', () => {
53
+ const auth: AuthCtx = { id: 'user-1', role: null, email: null };
54
+ expect(evaluateRule(true, auth, {}, {})).toBe(true);
55
+ });
56
+
57
+ it('false with authenticated user → deny', () => {
58
+ const auth: AuthCtx = { id: 'user-1', role: null, email: null };
59
+ expect(evaluateRule(false, auth, {}, {})).toBe(false);
60
+ });
61
+ });
62
+
63
+ // ─── B. auth!=null (인증 여부) ────────────────────────────────────────────────
64
+
65
+ describe('evaluateRule — auth != null', () => {
66
+ const rule: RuleValue = (auth) => auth !== null;
67
+
68
+ it('no auth → deny', () => {
69
+ expect(evaluateRule(rule, null, {}, {})).toBe(false);
70
+ });
71
+
72
+ it('with valid auth → allow', () => {
73
+ const auth: AuthCtx = { id: 'user-1', role: null, email: null };
74
+ expect(evaluateRule(rule, auth, {}, {})).toBe(true);
75
+ });
76
+ });
77
+
78
+ // ─── C. auth.id == resource.authorId (소유자 확인) ───────────────────────────
79
+
80
+ describe('evaluateRule — ownership check', () => {
81
+ const rule: RuleValue = (auth, resource) => auth !== null && auth.id === resource['authorId'];
82
+
83
+ it('owner → allow', () => {
84
+ const auth: AuthCtx = { id: 'user-1', role: null, email: null };
85
+ expect(evaluateRule(rule, auth, { authorId: 'user-1' }, {})).toBe(true);
86
+ });
87
+
88
+ it('non-owner → deny', () => {
89
+ const auth: AuthCtx = { id: 'user-2', role: null, email: null };
90
+ expect(evaluateRule(rule, auth, { authorId: 'user-1' }, {})).toBe(false);
91
+ });
92
+
93
+ it('no auth → deny for any resource', () => {
94
+ expect(evaluateRule(rule, null, { authorId: 'user-1' }, {})).toBe(false);
95
+ });
96
+
97
+ it('resource without authorId → deny', () => {
98
+ const auth: AuthCtx = { id: 'user-1', role: null, email: null };
99
+ expect(evaluateRule(rule, auth, {}, {})).toBe(false);
100
+ });
101
+ });
102
+
103
+ // ─── D. auth.role 비교 ───────────────────────────────────────────────────────
104
+
105
+ describe('evaluateRule — role check', () => {
106
+ const adminOnlyRule: RuleValue = (auth) => auth?.role === 'admin';
107
+
108
+ it('admin role → allow', () => {
109
+ const auth: AuthCtx = { id: 'u-1', role: 'admin', email: null };
110
+ expect(evaluateRule(adminOnlyRule, auth, {}, {})).toBe(true);
111
+ });
112
+
113
+ it('user role → deny', () => {
114
+ const auth: AuthCtx = { id: 'u-1', role: 'user', email: null };
115
+ expect(evaluateRule(adminOnlyRule, auth, {}, {})).toBe(false);
116
+ });
117
+
118
+ it('null role → deny', () => {
119
+ const auth: AuthCtx = { id: 'u-1', role: null, email: null };
120
+ expect(evaluateRule(adminOnlyRule, auth, {}, {})).toBe(false);
121
+ });
122
+
123
+ it('no auth → deny', () => {
124
+ expect(evaluateRule(adminOnlyRule, null, {}, {})).toBe(false);
125
+ });
126
+ });
127
+
128
+ // ─── E. in 연산자 (배열 포함 여부) ───────────────────────────────────────────
129
+
130
+ describe('evaluateRule — in operator', () => {
131
+ const allowedRoles = ['admin', 'editor', 'moderator'];
132
+ const rule: RuleValue = (auth) => auth !== null && allowedRoles.includes(auth.role ?? '');
133
+
134
+ it('admin → allow', () => {
135
+ const auth: AuthCtx = { id: 'u', role: 'admin', email: null };
136
+ expect(evaluateRule(rule, auth, {}, {})).toBe(true);
137
+ });
138
+
139
+ it('editor → allow', () => {
140
+ const auth: AuthCtx = { id: 'u', role: 'editor', email: null };
141
+ expect(evaluateRule(rule, auth, {}, {})).toBe(true);
142
+ });
143
+
144
+ it('viewer → deny', () => {
145
+ const auth: AuthCtx = { id: 'u', role: 'viewer', email: null };
146
+ expect(evaluateRule(rule, auth, {}, {})).toBe(false);
147
+ });
148
+ });
149
+
150
+ // ─── F. contains (문자열 포함) ───────────────────────────────────────────────
151
+
152
+ describe('evaluateRule — contains', () => {
153
+ const rule: RuleValue = (auth) => auth?.email?.includes('@company.com') ?? false;
154
+
155
+ it('company email → allow', () => {
156
+ const auth: AuthCtx = { id: 'u', role: null, email: 'user@company.com' };
157
+ expect(evaluateRule(rule, auth, {}, {})).toBe(true);
158
+ });
159
+
160
+ it('personal email → deny', () => {
161
+ const auth: AuthCtx = { id: 'u', role: null, email: 'user@gmail.com' };
162
+ expect(evaluateRule(rule, auth, {}, {})).toBe(false);
163
+ });
164
+
165
+ it('null email → deny', () => {
166
+ const auth: AuthCtx = { id: 'u', role: null, email: null };
167
+ expect(evaluateRule(rule, auth, {}, {})).toBe(false);
168
+ });
169
+ });
170
+
171
+ // ─── G. null-safety sentinel (#109) ─────────────────────────────────────────
172
+
173
+ describe('evaluateRule — null-safety', () => {
174
+ it('rule with nested null propagation (safe)', () => {
175
+ // auth?.metadata?.tier === 'pro' pattern
176
+ const rule: RuleValue = (auth) => {
177
+ const tier = (auth as any)?.metadata?.tier;
178
+ return tier === 'pro';
179
+ };
180
+ const auth: AuthCtx = { id: 'u', role: null, email: null };
181
+ // metadata undefined → safe false
182
+ expect(evaluateRule(rule, auth, {}, {})).toBe(false);
183
+ });
184
+
185
+ it('rule throwing exception → deny (graceful)', () => {
186
+ const badRule: RuleValue = () => { throw new Error('Rule error'); };
187
+ expect(evaluateRule(badRule, null, {}, {})).toBe(false);
188
+ });
189
+
190
+ it('rule returning null → deny (falsy)', () => {
191
+ const nullRule: RuleValue = () => null as any;
192
+ expect(evaluateRule(nullRule, null, {}, {})).toBe(false);
193
+ });
194
+
195
+ it('rule returning undefined → deny (falsy)', () => {
196
+ const undefinedRule: RuleValue = () => undefined as any;
197
+ expect(evaluateRule(undefinedRule, null, {}, {})).toBe(false);
198
+ });
199
+ });
200
+
201
+ // ─── H. context.workspaceId 참조 ─────────────────────────────────────────────
202
+
203
+ describe('evaluateRule — context access', () => {
204
+ it('ctx.workspaceId match → allow', () => {
205
+ const rule: RuleValue = (auth, resource, ctx) =>
206
+ ctx['workspaceId'] === resource['workspaceId'];
207
+ const ctx = { workspaceId: 'ws-1' };
208
+ expect(evaluateRule(rule, null, { workspaceId: 'ws-1' }, ctx)).toBe(true);
209
+ });
210
+
211
+ it('ctx.workspaceId mismatch → deny', () => {
212
+ const rule: RuleValue = (auth, resource, ctx) =>
213
+ ctx['workspaceId'] === resource['workspaceId'];
214
+ const ctx = { workspaceId: 'ws-1' };
215
+ expect(evaluateRule(rule, null, { workspaceId: 'ws-2' }, ctx)).toBe(false);
216
+ });
217
+ });
218
+
219
+ // ─── I. 규칙 미정의 → false (default deny) ───────────────────────────────────
220
+
221
+ describe('evaluateRule — default deny', () => {
222
+ it('string rule (non-boolean, non-function) → deny by default', () => {
223
+ // String rules are DB-evaluated; here we treat as deny
224
+ expect(evaluateRule('auth!=null' as any, null, {}, {})).toBe(false);
225
+ });
226
+
227
+ it('undefined rule → treated as false', () => {
228
+ expect(evaluateRule(undefined as any, null, {}, {})).toBe(false);
229
+ });
230
+
231
+ it('null rule → treated as false', () => {
232
+ expect(evaluateRule(null as any, null, {}, {})).toBe(false);
233
+ });
234
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Runtime surface accounting.
3
+ *
4
+ * Guards against route / middleware / durable object files silently drifting
5
+ * outside the test suite's field of view. A runtime file must either be
6
+ * mentioned in unit/integration tests or be explicitly tracked here as
7
+ * indirectly covered.
8
+ */
9
+ import { readFileSync, readdirSync } from 'fs';
10
+ import { resolve } from 'path';
11
+ import { describe, expect, it } from 'vitest';
12
+
13
+ const SRC_ROOT = resolve(new URL('..', import.meta.url).pathname);
14
+ const PACKAGE_ROOT = resolve(SRC_ROOT, '..');
15
+ const RUNTIME_DIRS = [
16
+ resolve(SRC_ROOT, 'routes'),
17
+ resolve(SRC_ROOT, 'durable-objects'),
18
+ resolve(SRC_ROOT, 'middleware'),
19
+ ];
20
+ const TEST_DIRS = [
21
+ resolve(SRC_ROOT, '__tests__'),
22
+ resolve(PACKAGE_ROOT, 'test/integration'),
23
+ ];
24
+ const THIS_TEST_PATH = resolve(new URL(import.meta.url).pathname);
25
+
26
+ // Files below are exercised indirectly, but the test sources do not mention the
27
+ // exact filename/stem yet. Tracking them here keeps the gap reviewable.
28
+ const KNOWN_INDIRECT_RUNTIME_COVERAGE = new Map<string, string>([
29
+ [
30
+ 'routes/schema-endpoint.ts',
31
+ 'Covered via /api/schema integration tests and OpenAPI/spec checks, but the file name is not referenced directly.',
32
+ ],
33
+ [
34
+ 'durable-objects/logs-do.ts',
35
+ 'Covered indirectly via analytics/logging flows; keep explicit until a direct file reference lands in tests.',
36
+ ],
37
+ [
38
+ 'durable-objects/room-runtime-base.ts',
39
+ 'Covered through RoomsDO and room protocol/state tests, but the shared runtime base is not referenced by filename.',
40
+ ],
41
+ ]);
42
+
43
+ function collectFiles(dir: string, predicate: (path: string) => boolean): string[] {
44
+ const results: string[] = [];
45
+
46
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
47
+ const entryPath = resolve(dir, entry.name);
48
+ if (entry.isDirectory()) {
49
+ results.push(...collectFiles(entryPath, predicate));
50
+ continue;
51
+ }
52
+ if (predicate(entryPath)) {
53
+ results.push(entryPath);
54
+ }
55
+ }
56
+
57
+ return results;
58
+ }
59
+
60
+ function toRuntimeKey(absPath: string): string {
61
+ for (const dir of RUNTIME_DIRS) {
62
+ if (absPath.startsWith(dir)) {
63
+ return absPath.slice(dir.length + 1).replace(/^/, `${dir.split('/').at(-1)}/`);
64
+ }
65
+ }
66
+ throw new Error(`Unexpected runtime path: ${absPath}`);
67
+ }
68
+
69
+ function fileMentionsRuntime(testSource: string, runtimeKey: string): boolean {
70
+ const fileName = runtimeKey.split('/').at(-1) ?? runtimeKey;
71
+ const stem = fileName.replace(/\.ts$/, '');
72
+ return testSource.includes(fileName) || testSource.includes(stem);
73
+ }
74
+
75
+ describe('runtime surface accounting', () => {
76
+ const runtimeFiles = RUNTIME_DIRS
77
+ .flatMap((dir) => collectFiles(dir, (path) => path.endsWith('.ts')))
78
+ .map(toRuntimeKey)
79
+ .sort();
80
+
81
+ const allTestSource = TEST_DIRS
82
+ .flatMap((dir) => collectFiles(dir, (path) => path.endsWith('.test.ts')))
83
+ .filter((path) => path !== THIS_TEST_PATH)
84
+ .map((path) => readFileSync(path, 'utf-8'))
85
+ .join('\n');
86
+
87
+ it('every runtime file is referenced by tests or tracked as indirect coverage', () => {
88
+ const unaccounted = runtimeFiles.filter((runtimeKey) => (
89
+ !fileMentionsRuntime(allTestSource, runtimeKey)
90
+ && !KNOWN_INDIRECT_RUNTIME_COVERAGE.has(runtimeKey)
91
+ ));
92
+
93
+ if (unaccounted.length > 0) {
94
+ expect.fail(
95
+ `Found runtime files that are not referenced by tests.\n` +
96
+ `Mention them in a test (comment/import/assertion) or add them to KNOWN_INDIRECT_RUNTIME_COVERAGE with a reason.\n\n` +
97
+ unaccounted.map((file) => ` - ${file}`).join('\n'),
98
+ );
99
+ }
100
+ });
101
+
102
+ it('indirect coverage entries still point to real runtime files', () => {
103
+ for (const runtimeKey of KNOWN_INDIRECT_RUNTIME_COVERAGE.keys()) {
104
+ expect(
105
+ runtimeFiles.includes(runtimeKey),
106
+ `KNOWN_INDIRECT_RUNTIME_COVERAGE contains '${runtimeKey}' but that runtime file no longer exists.`,
107
+ ).toBe(true);
108
+ }
109
+ });
110
+
111
+ it('indirect coverage entries are removed once tests mention the runtime file', () => {
112
+ for (const [runtimeKey, reason] of KNOWN_INDIRECT_RUNTIME_COVERAGE.entries()) {
113
+ expect(reason.trim().length, `Provide a reason for '${runtimeKey}'.`).toBeGreaterThan(0);
114
+ expect(
115
+ fileMentionsRuntime(allTestSource, runtimeKey),
116
+ `Tests now mention '${runtimeKey}'. Remove it from KNOWN_INDIRECT_RUNTIME_COVERAGE.`,
117
+ ).toBe(false);
118
+ }
119
+ });
120
+ });
@@ -0,0 +1,80 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const {
4
+ cleanExpiredSessionsMock,
5
+ cleanStaleAnonymousAccountsMock,
6
+ ensureAuthSchemaMock,
7
+ deleteAnonMock,
8
+ resolveAuthDbMock,
9
+ } = vi.hoisted(() => ({
10
+ cleanExpiredSessionsMock: vi.fn(),
11
+ cleanStaleAnonymousAccountsMock: vi.fn(),
12
+ ensureAuthSchemaMock: vi.fn(),
13
+ deleteAnonMock: vi.fn(),
14
+ resolveAuthDbMock: vi.fn(),
15
+ }));
16
+
17
+ vi.mock('cloudflare:workers', () => ({
18
+ DurableObject: class DurableObject {},
19
+ }));
20
+
21
+ vi.mock('../lib/auth-d1-service.js', async () => {
22
+ const actual = await vi.importActual<typeof import('../lib/auth-d1-service.js')>('../lib/auth-d1-service.js');
23
+ return {
24
+ ...actual,
25
+ cleanExpiredSessions: cleanExpiredSessionsMock,
26
+ cleanStaleAnonymousAccounts: cleanStaleAnonymousAccountsMock,
27
+ };
28
+ });
29
+
30
+ vi.mock('../lib/auth-d1.js', async () => {
31
+ const actual = await vi.importActual<typeof import('../lib/auth-d1.js')>('../lib/auth-d1.js');
32
+ return {
33
+ ...actual,
34
+ ensureAuthSchema: ensureAuthSchemaMock,
35
+ deleteAnon: deleteAnonMock,
36
+ };
37
+ });
38
+
39
+ vi.mock('../lib/auth-db-adapter.js', async () => {
40
+ const actual = await vi.importActual<typeof import('../lib/auth-db-adapter.js')>('../lib/auth-db-adapter.js');
41
+ return {
42
+ ...actual,
43
+ resolveAuthDb: resolveAuthDbMock,
44
+ };
45
+ });
46
+
47
+ describe('scheduled handler', () => {
48
+ beforeEach(() => {
49
+ vi.resetModules();
50
+ cleanExpiredSessionsMock.mockReset().mockResolvedValue(undefined);
51
+ cleanStaleAnonymousAccountsMock.mockReset().mockResolvedValue([]);
52
+ ensureAuthSchemaMock.mockReset().mockResolvedValue(undefined);
53
+ deleteAnonMock.mockReset().mockResolvedValue(undefined);
54
+ resolveAuthDbMock.mockReset().mockReturnValue({ kind: 'auth-db' });
55
+ });
56
+
57
+ it('runs system cleanup even when no user schedule functions are registered', async () => {
58
+ const worker = (await import('../index.js')).default;
59
+ const pending: Promise<unknown>[] = [];
60
+ const ctx = {
61
+ waitUntil: vi.fn((promise: Promise<unknown>) => {
62
+ pending.push(Promise.resolve(promise));
63
+ }),
64
+ };
65
+
66
+ await worker.scheduled(
67
+ { scheduledTime: Date.parse('2026-03-07T03:00:00Z') } as never,
68
+ {} as never,
69
+ ctx as never,
70
+ );
71
+
72
+ expect(ctx.waitUntil).toHaveBeenCalledTimes(1);
73
+ await Promise.all(pending);
74
+ expect(resolveAuthDbMock).toHaveBeenCalledTimes(1);
75
+ expect(ensureAuthSchemaMock).toHaveBeenCalledWith({ kind: 'auth-db' });
76
+ expect(cleanExpiredSessionsMock).toHaveBeenCalledWith({ kind: 'auth-db' });
77
+ expect(cleanStaleAnonymousAccountsMock.mock.calls[0]?.[0]).toEqual({ kind: 'auth-db' });
78
+ expect(deleteAnonMock).not.toHaveBeenCalled();
79
+ }, 15_000);
80
+ });