payment-kit 1.29.1 → 1.29.3

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 (343) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +47 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +41 -37
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/crons/tenant-fanout.ts +82 -0
  12. package/api/src/host-node/did-connect-runtime-node.ts +33 -0
  13. package/api/src/host-node/serve-static-arc.ts +68 -0
  14. package/api/src/host-node/serve-static.ts +41 -0
  15. package/api/src/index.ts +22 -161
  16. package/api/src/integrations/app-store/client.ts +3 -4
  17. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  18. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  19. package/api/src/integrations/arcblock/token.ts +21 -7
  20. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  21. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  22. package/api/src/integrations/google-play/verify.ts +3 -2
  23. package/api/src/integrations/iap-reconcile.ts +3 -5
  24. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  25. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  26. package/api/src/libs/archive/query.ts +19 -0
  27. package/api/src/libs/audit.ts +61 -4
  28. package/api/src/libs/auth.ts +247 -47
  29. package/api/src/libs/context.ts +89 -1
  30. package/api/src/libs/currency.ts +2 -2
  31. package/api/src/libs/dayjs.ts +8 -2
  32. package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
  33. package/api/src/libs/did-connect/tenant-identity.ts +221 -0
  34. package/api/src/libs/drivers/auth-storage.ts +118 -0
  35. package/api/src/libs/drivers/cron.ts +264 -0
  36. package/api/src/libs/drivers/db.ts +170 -0
  37. package/api/src/libs/drivers/identity.ts +142 -0
  38. package/api/src/libs/drivers/index.ts +40 -0
  39. package/api/src/libs/drivers/locks.ts +226 -0
  40. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  41. package/api/src/libs/drivers/queue.ts +104 -0
  42. package/api/src/libs/drivers/secrets.ts +194 -0
  43. package/api/src/libs/env.ts +170 -54
  44. package/api/src/libs/exchange-rate/service.ts +7 -6
  45. package/api/src/libs/http-fetch-adapter.ts +60 -0
  46. package/api/src/libs/invoice.ts +1 -1
  47. package/api/src/libs/lock.ts +51 -47
  48. package/api/src/libs/logger.ts +48 -8
  49. package/api/src/libs/notification/index.ts +1 -1
  50. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  51. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  52. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  53. package/api/src/libs/overdraft-protection.ts +1 -1
  54. package/api/src/libs/payout.ts +1 -1
  55. package/api/src/libs/queue/index.ts +271 -52
  56. package/api/src/libs/queue/runtime.ts +175 -0
  57. package/api/src/libs/resource.ts +3 -3
  58. package/api/src/libs/secrets.ts +38 -0
  59. package/api/src/libs/session.ts +3 -2
  60. package/api/src/libs/subscription.ts +5 -5
  61. package/api/src/libs/tenant.ts +92 -0
  62. package/api/src/libs/url.ts +3 -3
  63. package/api/src/libs/util.ts +21 -13
  64. package/api/src/middlewares/hono/cdn.ts +63 -0
  65. package/api/src/middlewares/hono/context.ts +80 -0
  66. package/api/src/middlewares/hono/csrf.ts +83 -0
  67. package/api/src/middlewares/hono/fallback.ts +194 -0
  68. package/api/src/middlewares/hono/pipeline.ts +73 -0
  69. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  70. package/api/src/middlewares/hono/resource.ts +63 -0
  71. package/api/src/middlewares/hono/security.ts +209 -0
  72. package/api/src/middlewares/hono/session.ts +114 -0
  73. package/api/src/middlewares/hono/xss.ts +61 -0
  74. package/api/src/queues/auto-recharge.ts +12 -10
  75. package/api/src/queues/checkout-session.ts +38 -21
  76. package/api/src/queues/credit-consume.ts +40 -36
  77. package/api/src/queues/credit-grant.ts +25 -18
  78. package/api/src/queues/credit-reconciliation.ts +7 -5
  79. package/api/src/queues/discount-status.ts +9 -6
  80. package/api/src/queues/event.ts +41 -11
  81. package/api/src/queues/exchange-rate-health.ts +49 -30
  82. package/api/src/queues/invoice.ts +18 -15
  83. package/api/src/queues/notification.ts +14 -7
  84. package/api/src/queues/payment.ts +64 -37
  85. package/api/src/queues/payout.ts +37 -21
  86. package/api/src/queues/refund.ts +36 -18
  87. package/api/src/queues/subscription.ts +83 -53
  88. package/api/src/queues/token-transfer.ts +15 -10
  89. package/api/src/queues/usage-record.ts +8 -5
  90. package/api/src/queues/vendors/commission.ts +7 -5
  91. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  92. package/api/src/queues/vendors/fulfillment.ts +4 -2
  93. package/api/src/queues/vendors/return-processor.ts +5 -3
  94. package/api/src/queues/vendors/return-scanner.ts +5 -4
  95. package/api/src/queues/vendors/status-check.ts +10 -7
  96. package/api/src/queues/webhook.ts +60 -32
  97. package/api/src/routes/connect/shared.ts +1 -2
  98. package/api/src/routes/connect/subscribe.ts +3 -3
  99. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  100. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  101. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  102. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  103. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  104. package/api/src/routes/hono/credit-tokens.ts +43 -0
  105. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  106. package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
  107. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  108. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  109. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  110. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  111. package/api/src/routes/hono/exchange-rates.ts +77 -0
  112. package/api/src/routes/hono/index.ts +115 -0
  113. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  114. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  115. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  116. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  117. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  118. package/api/src/routes/hono/meters.ts +288 -0
  119. package/api/src/routes/hono/passports.ts +73 -0
  120. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  121. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  122. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  123. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  124. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  125. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  126. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  127. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  128. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  129. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  130. package/api/src/routes/hono/redirect.ts +24 -0
  131. package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
  132. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  133. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  134. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  135. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  136. package/api/src/routes/hono/tool.ts +69 -0
  137. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  138. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  139. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  140. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  141. package/api/src/service.ts +814 -0
  142. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  143. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  144. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  145. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  146. package/api/src/store/models/auto-recharge-config.ts +22 -10
  147. package/api/src/store/models/checkout-session.ts +15 -14
  148. package/api/src/store/models/coupon.ts +29 -20
  149. package/api/src/store/models/credit-grant.ts +38 -29
  150. package/api/src/store/models/credit-transaction.ts +32 -21
  151. package/api/src/store/models/customer.ts +19 -17
  152. package/api/src/store/models/discount.ts +11 -2
  153. package/api/src/store/models/entitlement-grant.ts +21 -9
  154. package/api/src/store/models/entitlement-product.ts +21 -9
  155. package/api/src/store/models/entitlement.ts +19 -10
  156. package/api/src/store/models/event.ts +18 -9
  157. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  158. package/api/src/store/models/invoice-item.ts +18 -9
  159. package/api/src/store/models/invoice.ts +16 -8
  160. package/api/src/store/models/meter-event.ts +27 -9
  161. package/api/src/store/models/meter.ts +31 -22
  162. package/api/src/store/models/payment-currency.ts +25 -8
  163. package/api/src/store/models/payment-intent.ts +15 -6
  164. package/api/src/store/models/payment-link.ts +15 -6
  165. package/api/src/store/models/payment-method.ts +38 -22
  166. package/api/src/store/models/payment-stat.ts +18 -9
  167. package/api/src/store/models/payout.ts +15 -6
  168. package/api/src/store/models/price-quote.ts +17 -8
  169. package/api/src/store/models/price.ts +24 -12
  170. package/api/src/store/models/pricing-table.ts +29 -20
  171. package/api/src/store/models/product-vendor.ts +20 -10
  172. package/api/src/store/models/product.ts +15 -6
  173. package/api/src/store/models/promotion-code.ts +14 -6
  174. package/api/src/store/models/refund.ts +15 -6
  175. package/api/src/store/models/revenue-snapshot.ts +21 -9
  176. package/api/src/store/models/setting.ts +18 -9
  177. package/api/src/store/models/setup-intent.ts +36 -27
  178. package/api/src/store/models/subscription-item.ts +21 -9
  179. package/api/src/store/models/subscription-schedule.ts +21 -9
  180. package/api/src/store/models/subscription.ts +21 -10
  181. package/api/src/store/models/tax-rate.ts +29 -21
  182. package/api/src/store/models/usage-record.ts +11 -2
  183. package/api/src/store/models/webhook-attempt.ts +18 -9
  184. package/api/src/store/models/webhook-endpoint.ts +18 -9
  185. package/api/src/store/scoped-core.ts +55 -0
  186. package/api/src/store/scoped.ts +247 -0
  187. package/api/src/store/sequelize.ts +82 -23
  188. package/api/src/store/sql-migrations.ts +20 -0
  189. package/api/src/store/tenant-backfill.ts +260 -0
  190. package/api/src/store/tenant-model.ts +124 -0
  191. package/api/src/store/tenant-tables.ts +50 -0
  192. package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
  193. package/api/tests/crons/tenant-fanout.spec.ts +158 -0
  194. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  195. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  196. package/api/tests/fixtures/core-env-violation.ts +10 -0
  197. package/api/tests/fixtures/host-read-violation.ts +19 -0
  198. package/api/tests/fixtures/tenants.ts +4 -0
  199. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  200. package/api/tests/libs/archive-query.spec.ts +26 -0
  201. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  202. package/api/tests/libs/context.spec.ts +204 -0
  203. package/api/tests/libs/core-config.spec.ts +115 -0
  204. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  205. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  206. package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
  207. package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
  208. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  209. package/api/tests/libs/scoped.spec.ts +222 -0
  210. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  211. package/api/tests/libs/service-host.spec.ts +37 -0
  212. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  213. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  214. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  215. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  216. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  217. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  218. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  219. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  220. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  221. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  222. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  223. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  224. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  225. package/api/tests/models/tenant-columns.spec.ts +161 -0
  226. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  227. package/api/tests/queues/credit-consume.spec.ts +8 -1
  228. package/api/tests/queues/event-tenant.spec.ts +292 -0
  229. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  230. package/api/tests/queues/queue-parity.spec.ts +249 -0
  231. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  232. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  233. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  234. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  235. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  236. package/api/tests/service/collapse.spec.ts +96 -0
  237. package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
  238. package/api/tests/service/fail-closed-http.spec.ts +79 -0
  239. package/api/tests/service/static-arc-handler.spec.ts +101 -0
  240. package/api/tests/service/static-externalized.spec.ts +48 -0
  241. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  242. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  243. package/api/tests/store/tenant-model.spec.ts +162 -0
  244. package/api/tests/store/tenant-residual.spec.ts +196 -0
  245. package/api/third.d.ts +4 -0
  246. package/blocklet.yml +1 -1
  247. package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
  248. package/cloudflare/README.md +34 -27
  249. package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
  250. package/cloudflare/build.ts +33 -13
  251. package/cloudflare/cf-adapter.ts +419 -0
  252. package/cloudflare/did-connect-runtime.ts +96 -0
  253. package/cloudflare/did-connect-token-storage.ts +151 -0
  254. package/cloudflare/esbuild-cf-config.cjs +407 -0
  255. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  256. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  257. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  258. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  259. package/cloudflare/queue-runtime-mode.ts +13 -0
  260. package/cloudflare/run-build.js +33 -403
  261. package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
  262. package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
  263. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  264. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  265. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  266. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  267. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  268. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  269. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
  270. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
  271. package/cloudflare/shims/cron.ts +38 -158
  272. package/cloudflare/shims/events.ts +124 -0
  273. package/cloudflare/shims/fastq.ts +15 -1
  274. package/cloudflare/shims/nedb-storage.ts +16 -8
  275. package/cloudflare/shims/xss.ts +8 -0
  276. package/cloudflare/tenant-middleware.ts +36 -0
  277. package/cloudflare/tests/cf-adapter.spec.ts +244 -0
  278. package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
  279. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  280. package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
  281. package/cloudflare/vite.config.ts +53 -45
  282. package/cloudflare/worker.ts +261 -448
  283. package/cloudflare/wrangler.json +0 -6
  284. package/cloudflare/wrangler.jsonc +0 -6
  285. package/cloudflare/wrangler.local-e2e.jsonc +25 -0
  286. package/cloudflare/wrangler.staging.json +0 -6
  287. package/jest.config.js +3 -1
  288. package/package.json +33 -38
  289. package/scripts/bootstrap-inject.ts +166 -0
  290. package/scripts/core-env-whitelist.json +1 -0
  291. package/scripts/e2e-12b-runtime.ts +149 -0
  292. package/scripts/e2e-core-config.ts +125 -0
  293. package/scripts/e2e-d1-tenancy.ts +116 -0
  294. package/scripts/e2e-d2-cron-queue.ts +139 -0
  295. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  296. package/scripts/e2e-hono-s2.ts +125 -0
  297. package/scripts/e2e-hono-s3e.ts +135 -0
  298. package/scripts/e2e-hono-s4.ts +114 -0
  299. package/scripts/e2e-migration-contract.ts +100 -0
  300. package/scripts/e2e-s0.ts +61 -0
  301. package/scripts/e2e-s1.ts +107 -0
  302. package/scripts/e2e-s2.ts +178 -0
  303. package/scripts/e2e-s3.ts +110 -0
  304. package/scripts/e2e-s4.ts +191 -0
  305. package/scripts/e2e-s5.ts +139 -0
  306. package/scripts/e2e-s6.ts +127 -0
  307. package/scripts/e2e-tenant-model.ts +119 -0
  308. package/scripts/e2e-tenant-worker.ts +199 -0
  309. package/scripts/gen-sql-migrations.js +46 -0
  310. package/scripts/phase8-codemod.js +219 -0
  311. package/scripts/phase9a-env-getters-codemod.js +82 -0
  312. package/scripts/scan-core-env.js +109 -0
  313. package/scripts/scan-tenant-queries.js +235 -0
  314. package/scripts/schema-drift-guard.ts +210 -0
  315. package/scripts/tenant-scan-whitelist.json +1 -0
  316. package/src/app.tsx +2 -1
  317. package/src/env.d.ts +13 -1
  318. package/src/libs/service-host.ts +13 -0
  319. package/tsconfig.json +1 -1
  320. package/vite.arc.config.ts +159 -0
  321. package/api/src/libs/did-space.ts +0 -235
  322. package/api/src/libs/middleware.ts +0 -50
  323. package/api/src/libs/security.ts +0 -192
  324. package/api/src/queues/space.ts +0 -662
  325. package/api/src/routes/credit-tokens.ts +0 -38
  326. package/api/src/routes/exchange-rates.ts +0 -87
  327. package/api/src/routes/index.ts +0 -142
  328. package/api/src/routes/integrations/stripe.ts +0 -61
  329. package/api/src/routes/meters.ts +0 -274
  330. package/api/src/routes/passports.ts +0 -68
  331. package/api/src/routes/redirect.ts +0 -20
  332. package/api/src/routes/tool.ts +0 -65
  333. package/api/src/routes/webhook-endpoints.ts +0 -126
  334. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  335. package/cloudflare/did-connect-auth.ts +0 -527
  336. package/cloudflare/shims/did-space-js.ts +0 -17
  337. package/cloudflare/shims/did-space.ts +0 -11
  338. package/cloudflare/shims/express-compat/index.ts +0 -80
  339. package/cloudflare/shims/express-compat/types.ts +0 -41
  340. package/cloudflare/shims/lock.ts +0 -115
  341. package/cloudflare/shims/queue.ts +0 -611
  342. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  343. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,226 @@
1
+ // Phase 8 (W2-1a): locks slot driver contract.
2
+ //
3
+ // W1 §2.2 exempts the `locks` table from an instance_did column: tenant
4
+ // isolation for locks is achieved by prefixing the lock NAME, not by a column.
5
+ // `scopedLockName` is the single place that prefix is applied, so both drivers
6
+ // (and the libs/lock facade) stay consistent.
7
+ //
8
+ // Two implementations conform to the contract:
9
+ // - memory driver: the original in-process EventEmitter lock (Blocklet Server
10
+ // single-process Node.js). Semantics unchanged — Phase 8 wraps, never
11
+ // rewrites.
12
+ // - d1 driver: the Cloudflare D1-backed lock (atomic INSERT OR IGNORE + TTL
13
+ // expiry + owner token), relocated here from the previously-orphaned
14
+ // cloudflare/shims/lock.ts so the worker wires it through the locks slot.
15
+
16
+ // the two lock classes are the two driver implementations — cohesive in one module
17
+ /* eslint-disable max-classes-per-file */
18
+
19
+ import type { D1Binding } from './db';
20
+
21
+ export interface LockHandle {
22
+ name: string;
23
+ /** whether this handle currently holds the lock — used by singleton start guards */
24
+ locked: boolean;
25
+ /**
26
+ * Acquire the lock. `maxWaitMs` is a bounded-wait hint: the d1 driver enforces
27
+ * it as a hard timeout (and rejects on expiry) because a holder lives in a
28
+ * different isolate and may crash; the in-process memory driver waits until
29
+ * release and does not time out — single process means there is no
30
+ * crashed-holder / cross-isolate-wait scenario. The shared parity cases
31
+ * (acquire-when-free, block-until-release) hold on both; TTL-expiry and
32
+ * timeout are d1-only capabilities (see drivers/locks.spec.ts).
33
+ */
34
+ acquire(maxWaitMs?: number): Promise<true>;
35
+ release(): void;
36
+ }
37
+
38
+ export type LocksDriverKind = 'memory' | 'd1';
39
+
40
+ export interface LocksDriver {
41
+ kind: LocksDriverKind;
42
+ getLock(name: string, options?: { ttl?: number }): LockHandle;
43
+ }
44
+
45
+ export type LockScope = 'tenant' | 'global';
46
+
47
+ /**
48
+ * Apply the tenant prefix to a lock name. Tenant-scoped locks (the default for
49
+ * per-resource locks) are isolated per deployment/tenant; global-scoped locks
50
+ * (process-level singleton guards like queue start guards) are intentionally
51
+ * shared and keep their bare name. In single-tenant mode the prefix is the
52
+ * constant deployment app DID, so lock identity is unchanged in practice.
53
+ */
54
+ export function scopedLockName(name: string, instanceDid: string | null, scope: LockScope): string {
55
+ if (scope === 'global') return name;
56
+ if (!instanceDid) {
57
+ // tenant scope requires a tenant — callers resolve it fail-closed before
58
+ // reaching here (see libs/lock.ts). Guard anyway so a programming error is
59
+ // loud rather than silently producing a cross-tenant-shared lock.
60
+ throw new Error('scopedLockName: tenant-scoped lock requires a non-empty instanceDid');
61
+ }
62
+ return `tenant:${instanceDid}::${name}`;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // memory driver — original in-process lock semantics
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const { EventEmitter } = require('events');
70
+
71
+ export class MemoryLock implements LockHandle {
72
+ name: string;
73
+ locked: boolean;
74
+ private events: any;
75
+
76
+ constructor(name: string) {
77
+ this.name = name;
78
+ this.locked = false;
79
+ this.events = new EventEmitter();
80
+ }
81
+
82
+ acquire(): Promise<true> {
83
+ return new Promise((resolve) => {
84
+ if (this.locked) {
85
+ const tryAcquire = () => {
86
+ if (!this.locked) {
87
+ this.locked = true;
88
+ this.events.removeListener('release', tryAcquire);
89
+ resolve(true);
90
+ }
91
+ };
92
+ this.events.on('release', tryAcquire);
93
+ } else {
94
+ this.locked = true;
95
+ resolve(true);
96
+ }
97
+ });
98
+ }
99
+
100
+ release(): void {
101
+ this.locked = false;
102
+ setImmediate(() => this.events.emit('release'));
103
+ }
104
+ }
105
+
106
+ export function createMemoryLocksDriver(): LocksDriver {
107
+ const locks = new Map<string, MemoryLock>();
108
+ return {
109
+ kind: 'memory',
110
+ getLock(name: string): LockHandle {
111
+ const exist = locks.get(name);
112
+ if (exist instanceof MemoryLock) return exist;
113
+ const lock = new MemoryLock(name);
114
+ locks.set(name, lock);
115
+ return lock;
116
+ },
117
+ };
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // d1 driver — D1-backed cross-isolate lock (relocated from shims/lock.ts)
122
+ // ---------------------------------------------------------------------------
123
+
124
+ let nanoidCounter = 0;
125
+ function simpleId(): string {
126
+ nanoidCounter += 1;
127
+ return `${Date.now().toString(36)}-${nanoidCounter.toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
128
+ }
129
+
130
+ class D1Lock implements LockHandle {
131
+ name: string;
132
+ owner: string;
133
+ locked: boolean;
134
+ ttl: number;
135
+ private getBinding: () => D1Binding;
136
+
137
+ constructor(getBinding: () => D1Binding, name: string, options?: { ttl?: number }) {
138
+ this.getBinding = getBinding;
139
+ this.name = name;
140
+ this.owner = simpleId();
141
+ this.locked = false;
142
+ this.ttl = options?.ttl || 5000;
143
+ }
144
+
145
+ async acquire(maxWaitMs = 10000): Promise<true> {
146
+ const db: any = this.getBinding();
147
+ const deadline = Date.now() + maxWaitMs;
148
+ let delay = 30;
149
+
150
+ while (Date.now() < deadline) {
151
+ const now = Date.now();
152
+ try {
153
+ // eslint-disable-next-line no-await-in-loop -- polling retry until acquired or timed out
154
+ const batchResult = await db.batch([
155
+ db.prepare('DELETE FROM _locks WHERE name = ? AND expires_at < ?').bind(this.name, now),
156
+ db
157
+ .prepare('INSERT OR IGNORE INTO _locks (name, owner, expires_at) VALUES (?, ?, ?)')
158
+ .bind(this.name, this.owner, now + this.ttl),
159
+ db.prepare('SELECT owner FROM _locks WHERE name = ?').bind(this.name),
160
+ ]);
161
+ const row = batchResult[2]?.results?.[0] as { owner: string } | undefined;
162
+ if (row?.owner === this.owner) {
163
+ this.locked = true;
164
+ return true;
165
+ }
166
+ } catch (err: any) {
167
+ // eslint-disable-next-line no-console
168
+ console.error(`[D1Lock] acquire error for "${this.name}":`, err?.message || err);
169
+ }
170
+ // backoff with jitter, capped — captured into a const so the closure does
171
+ // not reference the mutated `delay`
172
+ const waitMs = delay + Math.random() * delay * 0.3;
173
+ // eslint-disable-next-line no-await-in-loop, no-promise-executor-return
174
+ await new Promise((r) => setTimeout(r, waitMs));
175
+ delay = Math.min(delay * 2, 500);
176
+ }
177
+ throw new Error(`[D1Lock] Failed to acquire lock "${this.name}" within ${maxWaitMs}ms`);
178
+ }
179
+
180
+ release(): void {
181
+ if (!this.locked) return;
182
+ this.locked = false;
183
+ try {
184
+ const db: any = this.getBinding();
185
+ const promise = db
186
+ .prepare('DELETE FROM _locks WHERE name = ? AND owner = ?')
187
+ .bind(this.name, this.owner)
188
+ .run()
189
+ // eslint-disable-next-line no-console
190
+ .catch((err: any) => console.error(`[D1Lock] release error for "${this.name}":`, err?.message || err));
191
+
192
+ // release is fire-and-forget; the worker shell flushes the pending delete
193
+ // before responding. This depends on ambient worker globals
194
+ // (__cfHttpContext__ / __cfWaitUntil__ / __cfPendingJobs__) set by the CF
195
+ // request handler — a host injecting the d1 locks driver must provide the
196
+ // same flush hooks (Phase 9 formalizes the flush contract). Outside a
197
+ // worker (e.g. the consistency suite) the delete still runs; it is simply
198
+ // not registered for flushing.
199
+ const isHttp = (globalThis as any).__cfHttpContext__;
200
+ if (isHttp) {
201
+ const waitUntil = (globalThis as any).__cfWaitUntil__;
202
+ if (typeof waitUntil === 'function') waitUntil(promise);
203
+ } else {
204
+ const pending = (globalThis as any).__cfPendingJobs__;
205
+ if (Array.isArray(pending)) pending.push(promise);
206
+ }
207
+ } catch (err: any) {
208
+ // eslint-disable-next-line no-console
209
+ console.error(`[D1Lock] release setup error for "${this.name}":`, err?.message || err);
210
+ }
211
+ }
212
+ }
213
+
214
+ /**
215
+ * D1-backed locks driver. `getBinding` is a getter (the binding is resolved
216
+ * lazily per request in the worker). Each call returns a fresh lock — isolates
217
+ * never share instances, matching the original shim's behavior.
218
+ */
219
+ export function createD1LocksDriver(getBinding: () => D1Binding): LocksDriver {
220
+ return {
221
+ kind: 'd1',
222
+ getLock(name: string, options?: { ttl?: number }): LockHandle {
223
+ return new D1Lock(getBinding, name, options);
224
+ },
225
+ };
226
+ }
@@ -0,0 +1,70 @@
1
+ // Phase 11 (W2′): runtime-neutral SQL migration runner — the migration-driver
2
+ // boundary. The library provisions the embedded D1 schema by applying the D1 SQL
3
+ // migrations through the host's db driver (its `exec`), NEVER by shelling out to
4
+ // the wrangler CLI. `wrangler d1 migrations apply` stays the CF-native local-dev
5
+ // / deploy provisioning mechanism for the SAME .sql files; this runner is the
6
+ // in-process path arc-node (and any non-CF host) uses via lifecycle/host wiring.
7
+ //
8
+ // Idempotent: applied migrations are tracked in `_sql_migrations`, so re-running
9
+ // is a no-op (the .sql contain non-IF-NOT-EXISTS DDL like ALTER ... ADD COLUMN).
10
+ //
11
+ // NOTE: this tracker (`_sql_migrations`) is independent of wrangler's own
12
+ // `d1_migrations` table. That is safe because the two provisioning paths target
13
+ // DIFFERENT databases — arc-node's embedded SQLite (this runner) vs the CF
14
+ // worker's bound D1 (wrangler) — and never share one. Do NOT run both paths
15
+ // against the SAME database: the runner is blind to `d1_migrations`, so it would
16
+ // re-apply everything and the non-idempotent ALTER ... ADD COLUMN would fail.
17
+
18
+ import type { DbDriver, DbBatchOp } from './db';
19
+
20
+ export interface SqlMigration {
21
+ /** stable id (the migration filename) — the idempotency key */
22
+ name: string;
23
+ /** the migration body; may contain multiple `;`-separated statements */
24
+ sql: string;
25
+ }
26
+
27
+ /** Strip line comments and split a migration body into individual statements. */
28
+ export function splitStatements(sql: string): string[] {
29
+ return sql
30
+ .split('\n')
31
+ .filter((line) => !line.trim().startsWith('--'))
32
+ .join('\n')
33
+ .split(';')
34
+ .map((s) => s.trim())
35
+ .filter((s) => s.length > 0);
36
+ }
37
+
38
+ /**
39
+ * Apply pending SQL migrations through the db driver, in order. Returns the
40
+ * names actually applied this run (empty on a fully-migrated db). No wrangler.
41
+ *
42
+ * Each migration is applied ATOMICALLY: all of its statements PLUS the tracker
43
+ * insert run in a single `driver.batch(...)` (the contract's all-or-nothing
44
+ * transactional primitive). So a mid-migration failure rolls the whole migration
45
+ * back AND leaves the tracker unwritten — a re-run retries that migration from a
46
+ * clean slate, never replaying half-applied (non-IF-NOT-EXISTS) DDL like
47
+ * `ALTER ... ADD COLUMN`. This satisfies "中断后重跑不丢已迁移状态(幂等 + 事务边界)".
48
+ */
49
+ export async function applySqlMigrations(driver: DbDriver, migrations: SqlMigration[]): Promise<string[]> {
50
+ await driver.exec('CREATE TABLE IF NOT EXISTS _sql_migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)');
51
+ const rows = await driver.all<{ name: string }>('SELECT name FROM _sql_migrations');
52
+ const applied = new Set(rows.map((r) => r.name));
53
+
54
+ const ran: string[] = [];
55
+ const pending = migrations.filter((m) => !applied.has(m.name));
56
+ /* eslint-disable no-await-in-loop -- migrations are ordered + must apply sequentially */
57
+ for (const m of pending) {
58
+ const ops: DbBatchOp[] = splitStatements(m.sql).map((sql) => ({ sql }));
59
+ // tracker insert is the LAST op in the same batch — committed iff every
60
+ // schema statement of this migration committed (atomic boundary).
61
+ ops.push({
62
+ sql: 'INSERT INTO _sql_migrations (name, applied_at) VALUES (?, ?)',
63
+ params: [m.name, new Date().toISOString()],
64
+ });
65
+ await driver.batch(ops);
66
+ ran.push(m.name);
67
+ }
68
+ /* eslint-enable no-await-in-loop */
69
+ return ran;
70
+ }
@@ -0,0 +1,104 @@
1
+ // Phase 9 (W2-1b): queue slot driver contract.
2
+ //
3
+ // The queue contract is the WHOLE semantics the two engines share, not just
4
+ // enqueue:
5
+ // - jobs table (createQueueStore) = persistent scheduler / source of truth
6
+ // - immediate vs delayed dispatch (delayed rows wait for the due-poll)
7
+ // - consumer ack + failure re-delivery (retry_count up to maxRetries, then
8
+ // failed; nonRetryable errors fail immediately)
9
+ // - pushAndWait inline execution (caller awaits the result)
10
+ // - host flush hook (CF flushes pending push/timer work before responding;
11
+ // the Node long-lived process is a no-op)
12
+ // - tenant is carried in the payload (Phase 5/6); the contract layer passes
13
+ // it through and NEVER resolves Host on the background path
14
+ //
15
+ // ONE engine (api/src/libs/queue) serves every runtime (Phase 12b, option A):
16
+ // the host swaps only the EXECUTOR (real fastq on node, cloudflare/shims/fastq
17
+ // in the worker) and the TRIGGER (in-process poll loop on node, CF scheduled()
18
+ // calling the engine's dispatchDueJobs() in the worker — see
19
+ // api/src/libs/queue/runtime.ts). Persistence, retry policy and tenant handling
20
+ // are shared because there is no second engine. The old cloudflare/shims/queue.ts
21
+ // duplicate engine was removed in Phase 12c.
22
+
23
+ import type EventEmitter from 'events';
24
+
25
+ export interface QueueOptions<T> {
26
+ id?: (job: T) => string;
27
+ concurrency?: number;
28
+ maxRetries?: number;
29
+ maxTimeout?: number;
30
+ retryDelay?: number;
31
+ enableScheduledJob?: boolean;
32
+ }
33
+
34
+ export interface PushParams<T> {
35
+ job: T;
36
+ id?: string;
37
+ persist?: boolean;
38
+ /** seconds */
39
+ delay?: number;
40
+ /** unix timestamp in seconds */
41
+ runAt?: number;
42
+ skipDuplicateCheck?: boolean;
43
+ /**
44
+ * Internal re-delivery flag for rows already persisted (startup / scheduled
45
+ * recovery). Skips the enqueue tenant gate so the execution-side legacy
46
+ * strategy applies. NEVER set from application code.
47
+ */
48
+ fromStore?: boolean;
49
+ }
50
+
51
+ /** the per-job event channel returned by push (emits queued/finished/failed/retry/cancelled) */
52
+ export type JobEvents = EventEmitter & { id?: string };
53
+
54
+ export interface QueueHandle<T = any> {
55
+ push(params: PushParams<T>): JobEvents;
56
+ pushAndWait(params: PushParams<T>): Promise<{ id: string; job: T; result: any }>;
57
+ get(id: string): Promise<T | null>;
58
+ delete(id: string, knownExists?: boolean): Promise<boolean>;
59
+ cancel(id: string): Promise<T | null>;
60
+ update(id: string, updates: any): Promise<any>;
61
+ /**
62
+ * D2 teardown: stop this queue's node poll loop (clears its sleep timer).
63
+ * No-op when the queue is not scheduled or on a workerd host (no loop runs).
64
+ * The Node host tears every queue down via lifecycle.stop() → stopAllQueues().
65
+ */
66
+ stop?(): void;
67
+ options: Required<Omit<QueueOptions<T>, 'id'>>;
68
+ }
69
+
70
+ /** factory shape both engines export as default */
71
+ export type QueueFactory = <T = any>(params: {
72
+ name: string;
73
+ onJob: (job: T) => Promise<any>;
74
+ options?: QueueOptions<T>;
75
+ }) => QueueHandle<T> & EventEmitter;
76
+
77
+ /**
78
+ * Host flush hook. CF must flush pending push/timer work before returning the
79
+ * response (the isolate is torn down after); a long-lived Node process never
80
+ * needs to and uses the no-op below.
81
+ */
82
+ export interface QueueHostHooks {
83
+ flush(): Promise<void>;
84
+ }
85
+
86
+ /** Node host: long-lived process, nothing to flush before a response. */
87
+ export const nodeQueueHostHooks: QueueHostHooks = {
88
+ async flush() {
89
+ /* no-op — the process stays alive, in-flight jobs continue */
90
+ },
91
+ };
92
+
93
+ // Active host hooks — injectable by the factory's `queue` slot; defaults to the
94
+ // Node no-op. The worker shell injects hooks that flush pending push/timer work
95
+ // before responding.
96
+ let activeQueueHostHooks: QueueHostHooks = nodeQueueHostHooks;
97
+
98
+ export function setQueueHostHooks(hooks: QueueHostHooks): void {
99
+ activeQueueHostHooks = hooks;
100
+ }
101
+
102
+ export function getQueueHostHooks(): QueueHostHooks {
103
+ return activeQueueHostHooks;
104
+ }
@@ -0,0 +1,194 @@
1
+ // Phase 11 (W2-3): secrets slot driver contract — per-tenant keyring.
2
+ //
3
+ // Replaces the process-level single key (cloudflare/shims/blocklet-sdk/security
4
+ // `_password`, initialized once from APP_PID's EK). Each tenant gets its own
5
+ // encryption key derived from its own EK, so one tenant's key can never decrypt
6
+ // another's ciphertext.
7
+ //
8
+ // Two surfaces, because the payment hot path (PaymentMethod.encrypt/decrypt
9
+ // Settings, getStripeClient, ~50 sync call sites) cannot become async without a
10
+ // large, risky ripple:
11
+ // - encrypt/decrypt (async): the full contract — lazy EK fetch + cache + TTL,
12
+ // and decrypt-failure forces one EK re-fetch (tolerates identity-side EK
13
+ // rotation). Hosts use this.
14
+ // - encryptSync/decryptSync: the hot path — uses an already-resolved key
15
+ // (default driver: the process key; keyring driver: the cached password,
16
+ // warmed via warmup()). Throws if the keyring key is cold (fail-closed).
17
+ // - warmup(instanceDid): async pre-resolve so the sync path is a cache hit
18
+ // (no-op for the single-tenant default driver).
19
+ //
20
+ // single mode (Blocklet Server) uses the default driver = the existing process
21
+ // `@blocklet/sdk/lib/security`, so existing ciphertext stays decryptable and
22
+ // behavior is unchanged.
23
+
24
+ /* eslint-disable max-classes-per-file */
25
+ import crypto from 'crypto';
26
+
27
+ import type { IdentityDriver } from './identity';
28
+
29
+ // crypto-js ships no type declarations; required untyped (same AES chain as the
30
+ // prior process security so ciphertext is interchangeable)
31
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-extraneous-dependencies
32
+ const CryptoJS: any = require('crypto-js');
33
+
34
+ export interface SecretsDriver {
35
+ /** async per-tenant encrypt — lazy-resolves the tenant key on first use */
36
+ encrypt(instanceDid: string, value: string): Promise<string>;
37
+ /** async per-tenant decrypt — re-fetches the EK once on failure (EK rotation tolerance) */
38
+ decrypt(instanceDid: string, value: string): Promise<string>;
39
+ /** sync hot path — requires the tenant key already resolved (warmup first for the keyring) */
40
+ encryptSync(instanceDid: string, value: string): string;
41
+ decryptSync(instanceDid: string, value: string): string;
42
+ /** pre-resolve the tenant key so the sync path is a cache hit (no-op for the default driver) */
43
+ warmup(instanceDid: string): Promise<void>;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // default driver — single-tenant, delegates to the process @blocklet/sdk security
48
+ // ---------------------------------------------------------------------------
49
+
50
+ class DefaultSecretsDriver implements SecretsDriver {
51
+ // lazily required so importing this module stays side-effect-free
52
+ private security() {
53
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
54
+ return require('@blocklet/sdk/lib/security').default ?? require('@blocklet/sdk/lib/security');
55
+ }
56
+
57
+ encryptSync(_instanceDid: string, value: string): string {
58
+ return this.security().encrypt(value);
59
+ }
60
+
61
+ decryptSync(_instanceDid: string, value: string): string {
62
+ return this.security().decrypt(value);
63
+ }
64
+
65
+ // eslint-disable-next-line require-await -- async contract; the single key is sync
66
+ async encrypt(instanceDid: string, value: string): Promise<string> {
67
+ return this.encryptSync(instanceDid, value);
68
+ }
69
+
70
+ // eslint-disable-next-line require-await -- async contract; the single key is sync
71
+ async decrypt(instanceDid: string, value: string): Promise<string> {
72
+ return this.decryptSync(instanceDid, value);
73
+ }
74
+
75
+ async warmup(): Promise<void> {
76
+ /* single key is always available — nothing to warm */
77
+ }
78
+ }
79
+
80
+ export function createDefaultSecretsDriver(): SecretsDriver {
81
+ return new DefaultSecretsDriver();
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // keyring driver — per-tenant key derived from each tenant's EK
86
+ // ---------------------------------------------------------------------------
87
+
88
+ const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 min
89
+
90
+ type CacheEntry = { password: string; expiresAt: number };
91
+
92
+ // AES decrypt with a wrong key can throw "Malformed UTF-8" or return empty,
93
+ // depending on the ciphertext bytes. Normalize both to '' so a wrong key is a
94
+ // deterministic failure (never the plaintext, never a crash) — this is what
95
+ // drives the decrypt-retry and what keeps cross-tenant decryption safe.
96
+ function safeUtf8(decrypted: any): string {
97
+ try {
98
+ return decrypted.toString(CryptoJS.enc.Utf8);
99
+ } catch {
100
+ return '';
101
+ }
102
+ }
103
+
104
+ function derivePassword(appEk: string, instanceDid: string): string {
105
+ // identical chain to the prior process security: PBKDF2(EK, salt) -> AES key.
106
+ // salt = the tenant DID (== blockletDid in single mode), so single-mode
107
+ // ciphertext written by the old path stays decryptable when the same EK is
108
+ // returned by identity.getAppEk(instanceDid).
109
+ return crypto.pbkdf2Sync(appEk, instanceDid, 256, 32, 'sha512').toString('hex');
110
+ }
111
+
112
+ class KeyringSecretsDriver implements SecretsDriver {
113
+ private identity: IdentityDriver;
114
+ private ttlMs: number;
115
+ private cache = new Map<string, CacheEntry>();
116
+
117
+ constructor(identity: IdentityDriver, opts?: { ttlMs?: number }) {
118
+ if (typeof identity.getAppEk !== 'function') {
119
+ throw new Error('createKeyringSecretsDriver: identity driver must provide getAppEk(instanceDid)');
120
+ }
121
+ this.identity = identity;
122
+ this.ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
123
+ }
124
+
125
+ private fresh(instanceDid: string): string | null {
126
+ const entry = this.cache.get(instanceDid);
127
+ if (entry && entry.expiresAt > Date.now()) return entry.password;
128
+ return null;
129
+ }
130
+
131
+ private async resolvePassword(instanceDid: string, force = false): Promise<string> {
132
+ if (!force) {
133
+ const cached = this.fresh(instanceDid);
134
+ if (cached) return cached;
135
+ }
136
+ const appEk = await this.identity.getAppEk!(instanceDid);
137
+ if (!appEk) {
138
+ throw new Error(`secrets: no EK for tenant ${instanceDid}`);
139
+ }
140
+ const password = derivePassword(appEk, instanceDid);
141
+ this.cache.set(instanceDid, { password, expiresAt: Date.now() + this.ttlMs });
142
+ return password;
143
+ }
144
+
145
+ async warmup(instanceDid: string): Promise<void> {
146
+ await this.resolvePassword(instanceDid);
147
+ }
148
+
149
+ encryptSync(instanceDid: string, value: string): string {
150
+ const password = this.fresh(instanceDid);
151
+ if (!password) throw new Error(`secrets: key for tenant ${instanceDid} is not warmed (call warmup first)`);
152
+ return CryptoJS.AES.encrypt(value, password).toString();
153
+ }
154
+
155
+ decryptSync(instanceDid: string, value: string): string {
156
+ const password = this.fresh(instanceDid);
157
+ if (!password) throw new Error(`secrets: key for tenant ${instanceDid} is not warmed (call warmup first)`);
158
+ return safeUtf8(CryptoJS.AES.decrypt(value, password));
159
+ }
160
+
161
+ async encrypt(instanceDid: string, value: string): Promise<string> {
162
+ const password = await this.resolvePassword(instanceDid);
163
+ return CryptoJS.AES.encrypt(value, password).toString();
164
+ }
165
+
166
+ async decrypt(instanceDid: string, value: string): Promise<string> {
167
+ let password = await this.resolvePassword(instanceDid);
168
+ let plain = safeUtf8(CryptoJS.AES.decrypt(value, password));
169
+ if (!plain) {
170
+ // empty result == wrong key (likely rotated EK) -> force one re-fetch + retry
171
+ password = await this.resolvePassword(instanceDid, true);
172
+ plain = safeUtf8(CryptoJS.AES.decrypt(value, password));
173
+ }
174
+ return plain;
175
+ }
176
+ }
177
+
178
+ export function createKeyringSecretsDriver(identity: IdentityDriver, opts?: { ttlMs?: number }): SecretsDriver {
179
+ return new KeyringSecretsDriver(identity, opts);
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // active driver + tenant-aware facade (resolves tenant from TenantContext)
184
+ // ---------------------------------------------------------------------------
185
+
186
+ let activeSecretsDriver: SecretsDriver = createDefaultSecretsDriver();
187
+
188
+ export function setSecretsDriver(driver: SecretsDriver): void {
189
+ activeSecretsDriver = driver;
190
+ }
191
+
192
+ export function getSecretsDriver(): SecretsDriver {
193
+ return activeSecretsDriver;
194
+ }