@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,612 @@
1
+ /**
2
+ * Plugin Migration Engine (Phase 2)
3
+ *
4
+ * Executes plugin onInstall / version-keyed migrations on first request
5
+ * after deploy. Versioned plugins are tracked in CONTROL_DB D1 via
6
+ * `plugin_version:{pluginName}`, while in-flight execution is deduplicated
7
+ * with a module-level promise latch.
8
+ *
9
+ * Provider-aware: if a plugin's dbBlock uses a PostgreSQL provider,
10
+ * the admin context routes CRUD and sql() through PostgreSQL directly
11
+ * instead of DO calls.
12
+ */
13
+
14
+ import type { Env } from '../types.js';
15
+ import type { EdgeBaseConfig, PluginInstance } from '@edge-base/shared';
16
+ import { parseConfig } from './do-router.js';
17
+ import { executePostgresQuery } from './postgres-executor.js';
18
+ import { getProviderBindingName } from './postgres-executor.js';
19
+ import { ensurePgSchema } from './postgres-schema-init.js';
20
+ import { ensureControlSchema, resolveControlDb, type ControlDb } from './control-db.js';
21
+ import { validateInsert, validateUpdate } from './validation.js';
22
+ import {
23
+ escapePgIdentifier,
24
+ preparePgInsertData,
25
+ preparePgUpdateData,
26
+ stripInternalPgFields,
27
+ } from './postgres-table-utils.js';
28
+ import {
29
+ buildAdminDbProxy,
30
+ buildFunctionKvProxy,
31
+ buildFunctionD1Proxy,
32
+ buildFunctionVectorizeProxy,
33
+ buildFunctionPushProxy,
34
+ buildAdminAuthContext,
35
+ } from './functions.js';
36
+ import { executeDoSql } from './do-sql.js';
37
+ import { resolveRootServiceKey } from './service-key.js';
38
+
39
+ /**
40
+ * Promise-based latch: deduplicates concurrent migration requests.
41
+ */
42
+ let migrationPromise: Promise<void> | null = null;
43
+ const versionlessPluginsExecuted = new Set<string>();
44
+ const currentVersionedPlugins = new Map<string, string>();
45
+ let controlSchemaReady = false;
46
+ let currentStateCacheExpiresAt = 0;
47
+
48
+ const DEFAULT_PLUGIN_MIGRATION_TIMEOUT_MS = 30_000;
49
+ const CURRENT_STATE_CACHE_TTL_MS = 30_000;
50
+
51
+ /**
52
+ * Run pending plugin migrations (lazy, once per cold-start).
53
+ *
54
+ * For each plugin that declares `version`:
55
+ * 1. Read stored version from CONTROL_DB (`plugin_version:{name}`)
56
+ * 2. If no stored version → run `onInstall` and pending migrations up to current version
57
+ * 3. If stored < current → run pending migrations in semver order
58
+ * 4. Update stored version
59
+ */
60
+ export async function executePluginMigrations(
61
+ plugins: PluginInstance[],
62
+ env: Env,
63
+ config: EdgeBaseConfig,
64
+ workerUrl?: string,
65
+ ): Promise<void> {
66
+ if (arePluginMigrationsCurrentInMemory(plugins)) {
67
+ return;
68
+ }
69
+
70
+ const controlDb = resolveControlDb(env as unknown as Record<string, unknown>);
71
+ await ensureControlSchemaOnce(controlDb);
72
+
73
+ if (await arePluginMigrationsCurrent(controlDb, plugins)) {
74
+ markPluginsCurrent(plugins);
75
+ return;
76
+ }
77
+
78
+ if (!migrationPromise) {
79
+ migrationPromise = runMigrationsWithTimeout(plugins, env, config, controlDb, workerUrl)
80
+ .then(() => {
81
+ markPluginsCurrent(plugins);
82
+ })
83
+ .catch((error) => {
84
+ throw error;
85
+ })
86
+ .finally(() => {
87
+ migrationPromise = null;
88
+ });
89
+ }
90
+ return migrationPromise;
91
+ }
92
+
93
+ export function resetPluginMigrationState(): void {
94
+ migrationPromise = null;
95
+ versionlessPluginsExecuted.clear();
96
+ currentVersionedPlugins.clear();
97
+ controlSchemaReady = false;
98
+ currentStateCacheExpiresAt = 0;
99
+ }
100
+
101
+ async function ensureControlSchemaOnce(controlDb: ControlDb): Promise<void> {
102
+ if (controlSchemaReady) return;
103
+ await ensureControlSchema(controlDb);
104
+ controlSchemaReady = true;
105
+ }
106
+
107
+ function arePluginMigrationsCurrentInMemory(plugins: PluginInstance[]): boolean {
108
+ if (Date.now() > currentStateCacheExpiresAt) {
109
+ return false;
110
+ }
111
+
112
+ const versionedPlugins = plugins.filter((plugin) => plugin.version);
113
+ const versionlessPlugins = plugins.filter(
114
+ (plugin) => !plugin.version && (plugin.onInstall || plugin.migrations),
115
+ );
116
+
117
+ if (versionlessPlugins.some((plugin) => !versionlessPluginsExecuted.has(plugin.name))) {
118
+ return false;
119
+ }
120
+
121
+ return versionedPlugins.every((plugin) => (
122
+ currentVersionedPlugins.get(plugin.name) === plugin.version
123
+ ));
124
+ }
125
+
126
+ function markPluginsCurrent(plugins: PluginInstance[]): void {
127
+ for (const plugin of plugins) {
128
+ if (plugin.version) {
129
+ currentVersionedPlugins.set(plugin.name, plugin.version);
130
+ }
131
+ }
132
+ currentStateCacheExpiresAt = Date.now() + CURRENT_STATE_CACHE_TTL_MS;
133
+ }
134
+
135
+ async function doMigrations(
136
+ plugins: PluginInstance[],
137
+ env: Env,
138
+ config: EdgeBaseConfig,
139
+ controlDb: ControlDb,
140
+ workerUrl?: string,
141
+ ): Promise<void> {
142
+ const dbNamespace = env.DATABASE;
143
+
144
+ for (const plugin of plugins) {
145
+ if (!plugin.version && !plugin.onInstall && !plugin.migrations) continue;
146
+
147
+ // 1. Read stored version from CONTROL_DB D1 _meta table
148
+ const metaKey = `plugin_version:${plugin.name}`;
149
+ const row = await controlDb.first<{ value: string }>('SELECT value FROM _meta WHERE key = ?', [
150
+ metaKey,
151
+ ]);
152
+ const storedVersion = row?.value ?? null;
153
+
154
+ // Build admin context for migration handlers (provider-aware)
155
+ const adminCtx = buildMigrationAdminContext(
156
+ dbNamespace,
157
+ config,
158
+ env as Env,
159
+ resolveRootServiceKey(config, env),
160
+ workerUrl,
161
+ );
162
+
163
+ if (storedVersion === null) {
164
+ // ─── First install ───
165
+ if (plugin.onInstall) {
166
+ await plugin.onInstall({
167
+ pluginConfig: plugin.config,
168
+ admin: adminCtx,
169
+ previousVersion: null,
170
+ });
171
+ }
172
+
173
+ await runPendingMigrations(plugin, adminCtx, null);
174
+
175
+ if (plugin.version) {
176
+ await setPluginVersion(controlDb, plugin.name, plugin.version);
177
+ } else {
178
+ versionlessPluginsExecuted.add(plugin.name);
179
+ }
180
+ } else if (plugin.version && storedVersion !== plugin.version) {
181
+ // ─── Pending migrations (semver-sorted) ───
182
+ await runPendingMigrations(plugin, adminCtx, storedVersion);
183
+
184
+ // Update stored version even when no version-keyed migrations exist.
185
+ await setPluginVersion(controlDb, plugin.name, plugin.version);
186
+ }
187
+ }
188
+ }
189
+
190
+ async function runMigrationsWithTimeout(
191
+ plugins: PluginInstance[],
192
+ env: Env,
193
+ config: EdgeBaseConfig,
194
+ controlDb: ControlDb,
195
+ workerUrl?: string,
196
+ ): Promise<void> {
197
+ const timeoutMs = resolvePluginMigrationTimeoutMs();
198
+ let timer: ReturnType<typeof setTimeout> | null = null;
199
+
200
+ try {
201
+ await Promise.race([
202
+ doMigrations(plugins, env, config, controlDb, workerUrl),
203
+ new Promise<never>((_, reject) => {
204
+ timer = setTimeout(() => {
205
+ reject(new Error(`Plugin migrations timed out (${timeoutMs}ms)`));
206
+ }, timeoutMs);
207
+ }),
208
+ ]);
209
+ } catch (error) {
210
+ console.error('[EdgeBase] Plugin migrations failed:', error);
211
+ throw error;
212
+ } finally {
213
+ if (timer) {
214
+ clearTimeout(timer);
215
+ }
216
+ }
217
+ }
218
+
219
+ function resolvePluginMigrationTimeoutMs(): number {
220
+ const raw = typeof process !== 'undefined' ? process.env.EDGEBASE_PLUGIN_MIGRATIONS_TIMEOUT_MS : undefined;
221
+ const parsed = Number(raw);
222
+ if (Number.isFinite(parsed) && parsed > 0) {
223
+ return parsed;
224
+ }
225
+ return DEFAULT_PLUGIN_MIGRATION_TIMEOUT_MS;
226
+ }
227
+
228
+ async function arePluginMigrationsCurrent(
229
+ controlDb: ControlDb,
230
+ plugins: PluginInstance[],
231
+ ): Promise<boolean> {
232
+ const versionedPlugins = plugins.filter((plugin) => plugin.version);
233
+ const versionlessPlugins = plugins.filter(
234
+ (plugin) => !plugin.version && (plugin.onInstall || plugin.migrations),
235
+ );
236
+
237
+ if (versionlessPlugins.some((plugin) => !versionlessPluginsExecuted.has(plugin.name))) {
238
+ return false;
239
+ }
240
+
241
+ if (versionedPlugins.length === 0) {
242
+ return true;
243
+ }
244
+
245
+ const keys = versionedPlugins.map((plugin) => `plugin_version:${plugin.name}`);
246
+ const placeholders = keys.map(() => '?').join(', ');
247
+ const rows = await controlDb.query<{ key: string; value: string }>(
248
+ `SELECT key, value FROM _meta WHERE key IN (${placeholders})`,
249
+ keys,
250
+ );
251
+ const versions = new Map(rows.map((row) => [row.key, row.value]));
252
+
253
+ return versionedPlugins.every((plugin) => (
254
+ versions.get(`plugin_version:${plugin.name}`) === plugin.version
255
+ ));
256
+ }
257
+
258
+ // ─── Helpers ───
259
+
260
+ async function setPluginVersion(db: ControlDb, pluginName: string, version: string): Promise<void> {
261
+ await db.run('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)', [
262
+ `plugin_version:${pluginName}`,
263
+ version,
264
+ ]);
265
+ }
266
+
267
+ async function runPendingMigrations(
268
+ plugin: PluginInstance,
269
+ admin: ReturnType<typeof buildMigrationAdminContext>,
270
+ previousVersion: string | null,
271
+ ): Promise<void> {
272
+ if (!plugin.migrations) return;
273
+
274
+ const pending = Object.keys(plugin.migrations)
275
+ .filter((version) => !plugin.version || semverCompare(version, plugin.version) <= 0)
276
+ .filter((version) => previousVersion === null || semverCompare(version, previousVersion) > 0)
277
+ .sort(semverCompare);
278
+
279
+ for (const version of pending) {
280
+ const migrationFn = plugin.migrations[version];
281
+ if (!migrationFn) continue;
282
+
283
+ await migrationFn({
284
+ pluginConfig: plugin.config,
285
+ admin,
286
+ previousVersion,
287
+ });
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Build a lightweight admin context for migration/onInstall handlers.
293
+ * Provides `db().table()` (CRUD proxy via DO calls or PostgreSQL) and `sql()`.
294
+ *
295
+ * Provider-aware: checks the dbBlock's provider for each namespace.
296
+ * - provider='do' (default) → routes through DO calls (existing behavior)
297
+ * - provider='neon'|'postgres' → routes through PostgreSQL directly
298
+ */
299
+ function buildMigrationAdminContext(
300
+ dbNamespace: DurableObjectNamespace,
301
+ _config: EdgeBaseConfig,
302
+ env: Env,
303
+ serviceKey?: string,
304
+ workerUrl?: string,
305
+ ) {
306
+ const config = parseConfig(env);
307
+ const doAdminDb = buildAdminDbProxy({
308
+ databaseNamespace: dbNamespace,
309
+ config,
310
+ workerUrl,
311
+ serviceKey,
312
+ env,
313
+ });
314
+
315
+ /**
316
+ * Resolve connection string for a PostgreSQL-backed namespace.
317
+ * Returns null if not PostgreSQL or binding not available.
318
+ */
319
+ function resolvePgConnString(namespace: string): string | null {
320
+ const dbBlock = config.databases?.[namespace];
321
+ const provider = dbBlock?.provider ?? 'do';
322
+ if (provider === 'do') return null;
323
+
324
+ const bindingName = getProviderBindingName(namespace);
325
+ const envRecord = env as unknown as Record<string, unknown>;
326
+
327
+ // 1. Hyperdrive binding (production)
328
+ const hyperdrive = envRecord[bindingName] as { connectionString: string } | undefined;
329
+ if (hyperdrive?.connectionString) return hyperdrive.connectionString;
330
+
331
+ // 2. Direct URL string (local dev)
332
+ const envKey = dbBlock?.connectionString ?? `${bindingName}_URL`;
333
+ const directUrl = envRecord[envKey] as string | undefined;
334
+ return directUrl ?? null;
335
+ }
336
+
337
+ return {
338
+ db(namespace: string, id?: string) {
339
+ const pgConnStr = resolvePgConnString(namespace);
340
+
341
+ // ─── PostgreSQL path ───
342
+ if (pgConnStr) {
343
+ return {
344
+ table(tableName: string) {
345
+ return buildPgTableOps(pgConnStr, namespace, tableName, config);
346
+ },
347
+ };
348
+ }
349
+
350
+ return doAdminDb(namespace, id);
351
+ },
352
+
353
+ async sql(
354
+ namespace: string,
355
+ id: string | undefined,
356
+ query: string,
357
+ params?: unknown[],
358
+ ): Promise<unknown[]> {
359
+ const dbBlock = config.databases?.[namespace];
360
+ const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
361
+ if (isDynamicNamespace && !id) {
362
+ throw new Error(`admin.sql() requires an id for dynamic namespace '${namespace}'.`);
363
+ }
364
+
365
+ const pgConnStr = resolvePgConnString(namespace);
366
+
367
+ // ─── PostgreSQL path ───
368
+ if (pgConnStr) {
369
+ // Ensure schema is initialized before raw SQL
370
+ const dbBlock = config.databases?.[namespace];
371
+ if (dbBlock?.tables) {
372
+ await ensurePgSchema(pgConnStr, namespace, dbBlock.tables);
373
+ }
374
+ const result = await executePostgresQuery(pgConnStr, query, params ?? []);
375
+ return result.rows as unknown[];
376
+ }
377
+
378
+ // ─── DO path (existing) ───
379
+ return executeDoSql({
380
+ databaseNamespace: dbNamespace,
381
+ namespace,
382
+ id,
383
+ query,
384
+ params: params ?? [],
385
+ internal: true,
386
+ });
387
+ },
388
+
389
+ // ─── Convenience shortcut: table(name) → db('shared').table(name) ───
390
+ table(name: string) {
391
+ // Delegate to db('shared') which handles provider-aware routing
392
+ return this.db('shared').table(name);
393
+ },
394
+
395
+ // ─── KV / D1 / Vectorize / Push proxies (HTTP-based, require workerUrl) ───
396
+ kv(namespace: string) {
397
+ return buildFunctionKvProxy(namespace, config, env, workerUrl, serviceKey);
398
+ },
399
+ d1(database: string) {
400
+ return buildFunctionD1Proxy(database, config, env, workerUrl, serviceKey);
401
+ },
402
+ vector(index: string) {
403
+ return buildFunctionVectorizeProxy(index, config, env, workerUrl, serviceKey);
404
+ },
405
+ push: buildFunctionPushProxy(workerUrl, serviceKey),
406
+
407
+ // ─── Auth context (D1-backed) ───
408
+ auth: buildAdminAuthContext({
409
+ d1Database: env.AUTH_DB,
410
+ serviceKey,
411
+ workerUrl,
412
+ kvNamespace: env.KV,
413
+ }),
414
+
415
+ // ─── Broadcast (HTTP → Worker → DatabaseLiveDO) ───
416
+ async broadcast(
417
+ channel: string,
418
+ event: string,
419
+ payload?: Record<string, unknown>,
420
+ ): Promise<void> {
421
+ if (!workerUrl || !serviceKey) {
422
+ throw new Error('admin.broadcast() requires workerUrl and serviceKey.');
423
+ }
424
+ const res = await fetch(`${workerUrl}/api/db/broadcast`, {
425
+ method: 'POST',
426
+ headers: {
427
+ 'Content-Type': 'application/json',
428
+ 'X-EdgeBase-Service-Key': serviceKey,
429
+ },
430
+ body: JSON.stringify({ channel, event, payload: payload ?? {} }),
431
+ });
432
+ if (!res.ok) throw new Error(`admin.broadcast() failed: ${res.status}`);
433
+ },
434
+
435
+ // ─── Inter-function calls (HTTP → Worker → function handler) ───
436
+ functions: {
437
+ async call(name: string, data?: unknown): Promise<unknown> {
438
+ if (!workerUrl || !serviceKey) {
439
+ throw new Error('admin.functions.call() requires workerUrl and serviceKey.');
440
+ }
441
+ const safeName = name.split('/').map(encodeURIComponent).join('/');
442
+ const res = await fetch(`${workerUrl}/api/functions/${safeName}`, {
443
+ method: 'POST',
444
+ headers: {
445
+ 'Content-Type': 'application/json',
446
+ 'X-EdgeBase-Service-Key': serviceKey,
447
+ },
448
+ body: JSON.stringify(data ?? {}),
449
+ });
450
+ if (!res.ok) {
451
+ const err = (await res
452
+ .json()
453
+ .catch(() => ({ message: `Function call '${name}' failed` }))) as { message: string };
454
+ throw new Error(err.message);
455
+ }
456
+ return res.json();
457
+ },
458
+ },
459
+ };
460
+ }
461
+
462
+ /**
463
+ * Build PostgreSQL CRUD operations for plugin migration admin context.
464
+ * Simplified CRUD — no rules/hooks (migrations are internal/admin-level).
465
+ */
466
+ function buildPgTableOps(
467
+ connectionString: string,
468
+ namespace: string,
469
+ tableName: string,
470
+ config: EdgeBaseConfig,
471
+ ) {
472
+ const tableConfig = config.databases?.[namespace]?.tables?.[tableName];
473
+ if (!tableConfig) {
474
+ throw new Error(`Migration table '${tableName}' not found in database '${namespace}'.`);
475
+ }
476
+
477
+ // Helper: ensure schema before first operation
478
+ let schemaReady = false;
479
+ async function ensureSchema(): Promise<void> {
480
+ if (schemaReady) return;
481
+ const dbBlock = config.databases?.[namespace];
482
+ if (dbBlock?.tables) {
483
+ await ensurePgSchema(connectionString, namespace, dbBlock.tables);
484
+ }
485
+ schemaReady = true;
486
+ }
487
+
488
+ return {
489
+ async insert(data: Record<string, unknown>): Promise<Record<string, unknown>> {
490
+ await ensureSchema();
491
+ const validation = validateInsert(data, tableConfig.schema);
492
+ if (!validation.valid) {
493
+ throw new Error(`Migration insert validation failed: ${JSON.stringify(validation.errors)}`);
494
+ }
495
+
496
+ const prepared = preparePgInsertData(data, tableConfig).data;
497
+ const cols = Object.keys(prepared);
498
+ const vals = Object.values(prepared);
499
+ const placeholders = cols.map((_, i) => `$${i + 1}`);
500
+ const sql = `INSERT INTO ${escapePgIdentifier(tableName)} (${cols.map(escapePgIdentifier).join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
501
+ const result = await executePostgresQuery(connectionString, sql, vals);
502
+ return result.rows[0]
503
+ ? stripInternalPgFields(result.rows[0] as Record<string, unknown>)
504
+ : prepared;
505
+ },
506
+
507
+ async upsert(
508
+ data: Record<string, unknown>,
509
+ options?: { conflictTarget?: string },
510
+ ): Promise<Record<string, unknown>> {
511
+ await ensureSchema();
512
+ const validation = validateInsert(data, tableConfig.schema);
513
+ if (!validation.valid) {
514
+ throw new Error(`Migration upsert validation failed: ${JSON.stringify(validation.errors)}`);
515
+ }
516
+
517
+ const payload = preparePgInsertData(data, tableConfig).data;
518
+ const conflictTarget = options?.conflictTarget ?? 'id';
519
+ const cols = Object.keys(payload);
520
+ const vals = Object.values(payload);
521
+ const placeholders = cols.map((_, i) => `$${i + 1}`);
522
+ const updatableCols = cols.filter((col) => col !== conflictTarget);
523
+ const setClauses = (updatableCols.length > 0 ? updatableCols : [conflictTarget]).map(
524
+ (col) => `${escapePgIdentifier(col)} = EXCLUDED.${escapePgIdentifier(col)}`,
525
+ );
526
+ const sql = `INSERT INTO ${escapePgIdentifier(tableName)} (${cols.map(escapePgIdentifier).join(', ')}) VALUES (${placeholders.join(', ')})` +
527
+ ` ON CONFLICT (${escapePgIdentifier(conflictTarget)}) DO UPDATE SET ${setClauses.join(', ')} RETURNING *`;
528
+ const result = await executePostgresQuery(connectionString, sql, vals);
529
+ return result.rows[0]
530
+ ? stripInternalPgFields(result.rows[0] as Record<string, unknown>)
531
+ : payload;
532
+ },
533
+
534
+ async update(rowId: string, data: Record<string, unknown>): Promise<Record<string, unknown>> {
535
+ await ensureSchema();
536
+ const validation = validateUpdate(data, tableConfig.schema);
537
+ if (!validation.valid) {
538
+ throw new Error(`Migration update validation failed: ${JSON.stringify(validation.errors)}`);
539
+ }
540
+
541
+ const prepared = preparePgUpdateData(data, tableConfig).data;
542
+ if (Object.keys(prepared).length === 0) {
543
+ throw new Error(`Migration update: no valid fields to update in ${tableName}`);
544
+ }
545
+
546
+ const cols = Object.keys(prepared);
547
+ const vals = Object.values(prepared);
548
+ const setClauses = cols.map((c, i) => `${escapePgIdentifier(c)} = $${i + 1}`);
549
+ const sql = `UPDATE ${escapePgIdentifier(tableName)} SET ${setClauses.join(', ')} WHERE "id" = $${cols.length + 1} RETURNING *`;
550
+ const result = await executePostgresQuery(connectionString, sql, [...vals, rowId]);
551
+ if (result.rows.length === 0)
552
+ throw new Error(`Migration update: row '${rowId}' not found in ${tableName}`);
553
+ return stripInternalPgFields(result.rows[0] as Record<string, unknown>);
554
+ },
555
+
556
+ async delete(rowId: string): Promise<{ deleted: boolean }> {
557
+ await ensureSchema();
558
+ const sql = `DELETE FROM ${escapePgIdentifier(tableName)} WHERE "id" = $1`;
559
+ const result = await executePostgresQuery(connectionString, sql, [rowId]);
560
+ return { deleted: result.rowCount > 0 };
561
+ },
562
+
563
+ async get(rowId: string): Promise<Record<string, unknown>> {
564
+ await ensureSchema();
565
+ const sql = `SELECT * FROM ${escapePgIdentifier(tableName)} WHERE "id" = $1`;
566
+ const result = await executePostgresQuery(connectionString, sql, [rowId]);
567
+ if (result.rows.length === 0)
568
+ throw new Error(`Migration get: row '${rowId}' not found in ${tableName}`);
569
+ return stripInternalPgFields(result.rows[0] as Record<string, unknown>);
570
+ },
571
+
572
+ async list(opts?: {
573
+ limit?: number;
574
+ filter?: unknown;
575
+ }): Promise<{ items: Record<string, unknown>[] }> {
576
+ await ensureSchema();
577
+ let sql = `SELECT * FROM ${escapePgIdentifier(tableName)}`;
578
+ const params: unknown[] = [];
579
+ let paramIdx = 1;
580
+
581
+ // Simple filter support: { column: value }
582
+ if (opts?.filter && typeof opts.filter === 'object') {
583
+ const filterEntries = Object.entries(opts.filter as Record<string, unknown>);
584
+ if (filterEntries.length > 0) {
585
+ const whereClauses = filterEntries.map(([col, val]) => {
586
+ params.push(val);
587
+ return `${escapePgIdentifier(col)} = $${paramIdx++}`;
588
+ });
589
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
590
+ }
591
+ }
592
+
593
+ if (opts?.limit) {
594
+ sql += ` LIMIT $${paramIdx}`;
595
+ params.push(opts.limit);
596
+ }
597
+
598
+ const result = await executePostgresQuery(connectionString, sql, params);
599
+ return { items: result.rows.map((row) => stripInternalPgFields(row as Record<string, unknown>)) };
600
+ },
601
+ };
602
+ }
603
+
604
+ /** Simple semver comparison: returns negative/0/positive like Array.sort compareFn. */
605
+ function semverCompare(a: string, b: string): number {
606
+ const pa = a.split('.').map(Number);
607
+ const pb = b.split('.').map(Number);
608
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
609
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
610
+ }
611
+ return 0;
612
+ }