payment-kit 1.29.1 → 1.29.2

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 (310) 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 +36 -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 +27 -24
  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/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  236. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  237. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  238. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  239. package/cloudflare/queue-runtime-mode.ts +13 -0
  240. package/cloudflare/run-build.js +10 -56
  241. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  242. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  243. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  244. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  245. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  246. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  247. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  248. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  249. package/cloudflare/shims/cron.ts +38 -158
  250. package/cloudflare/shims/events.ts +124 -0
  251. package/cloudflare/shims/fastq.ts +15 -1
  252. package/cloudflare/shims/nedb-storage.ts +16 -8
  253. package/cloudflare/shims/xss.ts +8 -0
  254. package/cloudflare/tenant-middleware.ts +36 -0
  255. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  256. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  257. package/cloudflare/worker.ts +204 -433
  258. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  259. package/jest.config.js +3 -1
  260. package/package.json +33 -38
  261. package/scripts/core-env-whitelist.json +1 -0
  262. package/scripts/e2e-12b-runtime.ts +149 -0
  263. package/scripts/e2e-core-config.ts +125 -0
  264. package/scripts/e2e-d1-tenancy.ts +116 -0
  265. package/scripts/e2e-d2-cron-queue.ts +139 -0
  266. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  267. package/scripts/e2e-hono-s2.ts +125 -0
  268. package/scripts/e2e-hono-s3e.ts +135 -0
  269. package/scripts/e2e-hono-s4.ts +114 -0
  270. package/scripts/e2e-migration-contract.ts +100 -0
  271. package/scripts/e2e-s0.ts +61 -0
  272. package/scripts/e2e-s1.ts +107 -0
  273. package/scripts/e2e-s2.ts +178 -0
  274. package/scripts/e2e-s3.ts +110 -0
  275. package/scripts/e2e-s4.ts +191 -0
  276. package/scripts/e2e-s5.ts +139 -0
  277. package/scripts/e2e-s6.ts +127 -0
  278. package/scripts/e2e-tenant-model.ts +119 -0
  279. package/scripts/e2e-tenant-worker.ts +199 -0
  280. package/scripts/gen-sql-migrations.js +46 -0
  281. package/scripts/phase8-codemod.js +219 -0
  282. package/scripts/phase9a-env-getters-codemod.js +82 -0
  283. package/scripts/scan-core-env.js +109 -0
  284. package/scripts/scan-tenant-queries.js +235 -0
  285. package/scripts/schema-drift-guard.ts +210 -0
  286. package/scripts/tenant-scan-whitelist.json +1 -0
  287. package/src/env.d.ts +13 -1
  288. package/tsconfig.json +1 -1
  289. package/api/src/libs/did-space.ts +0 -235
  290. package/api/src/libs/middleware.ts +0 -50
  291. package/api/src/libs/security.ts +0 -192
  292. package/api/src/queues/space.ts +0 -662
  293. package/api/src/routes/credit-tokens.ts +0 -38
  294. package/api/src/routes/exchange-rates.ts +0 -87
  295. package/api/src/routes/index.ts +0 -142
  296. package/api/src/routes/integrations/stripe.ts +0 -61
  297. package/api/src/routes/meters.ts +0 -274
  298. package/api/src/routes/passports.ts +0 -68
  299. package/api/src/routes/redirect.ts +0 -20
  300. package/api/src/routes/tool.ts +0 -65
  301. package/api/src/routes/webhook-endpoints.ts +0 -126
  302. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  303. package/cloudflare/shims/did-space-js.ts +0 -17
  304. package/cloudflare/shims/did-space.ts +0 -11
  305. package/cloudflare/shims/express-compat/index.ts +0 -80
  306. package/cloudflare/shims/express-compat/types.ts +0 -41
  307. package/cloudflare/shims/lock.ts +0 -115
  308. package/cloudflare/shims/queue.ts +0 -611
  309. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  310. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -1,53 +1,57 @@
1
- const { EventEmitter } = require('events');
2
-
3
- export class Lock {
4
- name: string;
5
- locked: boolean;
6
- events: typeof EventEmitter;
7
-
8
- constructor(name: string) {
9
- this.name = name;
10
- this.locked = false;
11
- this.events = new EventEmitter();
12
- }
13
-
14
- acquire() {
15
- return new Promise((resolve) => {
16
- // If somebody has the lock, wait until he/she releases the lock and try again
17
- if (this.locked) {
18
- const tryAcquire = () => {
19
- if (!this.locked) {
20
- this.locked = true;
21
- this.events.removeListener('release', tryAcquire);
22
- resolve(true);
23
- }
24
- };
1
+ // Phase 8 (W2-1a): lock facade over the locks slot driver.
2
+ //
3
+ // Call sites keep using `getLock(name)` unchanged. Behind the facade:
4
+ // - the active locks driver is injectable (`setLocksDriver`) — default is the
5
+ // in-process memory driver (Blocklet Server / Node). The worker injects the
6
+ // D1 driver through the locks slot.
7
+ // - lock names are tenant-prefixed (W1 §2.2) by default. Per-resource locks
8
+ // are tenant-scoped; process-level singleton guards (queue start guards,
9
+ // recovery) opt out with `{ scope: 'global' }`. In single-tenant mode the
10
+ // prefix is the constant deployment app DID, so lock identity is unchanged.
11
+
12
+ import { getInstanceDid } from './context';
13
+ import {
14
+ createMemoryLocksDriver,
15
+ scopedLockName,
16
+ MemoryLock,
17
+ type LockHandle,
18
+ type LocksDriver,
19
+ type LockScope,
20
+ } from './drivers';
21
+
22
+ let activeDriver: LocksDriver = createMemoryLocksDriver();
23
+
24
+ /** Inject the locks driver (worker wires the D1 driver here). */
25
+ export function setLocksDriver(driver: LocksDriver): void {
26
+ activeDriver = driver;
27
+ }
25
28
 
26
- this.events.on('release', tryAcquire);
27
- } else {
28
- // Otherwise, take the lock and resolve immediately
29
- this.locked = true;
30
- resolve(true);
31
- }
32
- });
33
- }
29
+ export function getLocksDriver(): LocksDriver {
30
+ return activeDriver;
31
+ }
34
32
 
35
- release() {
36
- // Release the lock immediately
37
- this.locked = false;
38
- setImmediate(() => this.events.emit('release'));
39
- }
33
+ export interface GetLockOptions {
34
+ ttl?: number;
35
+ /** 'tenant' (default) prefixes the lock name with the current tenant; 'global' keeps the bare name. */
36
+ scope?: LockScope;
40
37
  }
41
38
 
42
- const locks = new Map<string, Lock>();
43
- export function getLock(name: string): Lock {
44
- const exist = locks.get(name);
45
- if (exist instanceof Lock) {
46
- return exist;
39
+ /**
40
+ * Acquire a handle to a named lock. Tenant-scoped by default; resolving the
41
+ * tenant fails closed in multi-tenant mode when no context is present (matching
42
+ * the scoped-query helpers from Phase 3).
43
+ */
44
+ export function getLock(name: string, options: GetLockOptions = {}): LockHandle {
45
+ if (!name || typeof name !== 'string') {
46
+ throw new Error('getLock: a non-empty lock name is required');
47
47
  }
48
-
49
- const lock = new Lock(name);
50
- locks.set(name, lock);
51
-
52
- return lock;
48
+ const scope: LockScope = options.scope ?? 'tenant';
49
+ const instanceDid = scope === 'tenant' ? getInstanceDid() : null;
50
+ const resolvedName = scopedLockName(name, instanceDid, scope);
51
+ return activeDriver.getLock(resolvedName, options.ttl !== undefined ? { ttl: options.ttl } : undefined);
53
52
  }
53
+
54
+ // `Lock` is the in-process memory lock class — kept as a public export so the
55
+ // standalone constructor form (`new Lock(name)`) and `LockHandle` annotations
56
+ // both keep working.
57
+ export { MemoryLock as Lock };
@@ -1,4 +1,13 @@
1
- const createLogger = require('@blocklet/logger');
1
+ // Phase 13b: lazy logging boundary.
2
+ //
3
+ // @blocklet/logger throws at REQUIRE time when BLOCKLET_LOG_DIR is absent
4
+ // ("valid BLOCKLET_LOG_DIR env is required by logger"). That made importing the
5
+ // core — and therefore createEmbeddedPaymentService / rpc.entitlements.check,
6
+ // which transitively import this module — demand a blocklet log env even for a
7
+ // bare host (arc embedding before its blocklet runtime is wired). Defer the
8
+ // require to first use and fall back to console when no blocklet log env exists.
9
+ // The blocklet server (BLOCKLET_LOG_DIR set) and the CF worker (aliased shim)
10
+ // still get the real logger on first call — behavior unchanged for them.
2
11
 
3
12
  interface Logger {
4
13
  debug: (...args: any[]) => void;
@@ -7,14 +16,45 @@ interface Logger {
7
16
  warn: (...args: any[]) => void;
8
17
  }
9
18
 
10
- const init = (label: string): Logger => {
11
- const instance = createLogger(label || '');
12
- return instance;
13
- };
19
+ let resolved: Logger | undefined;
14
20
 
15
- const logger = init('app');
21
+ function resolveLogger(): Logger {
22
+ if (resolved) return resolved;
23
+ try {
24
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
25
+ const createLogger = require('@blocklet/logger');
26
+ resolved = createLogger('app') as Logger;
27
+ } catch {
28
+ // bare host without a blocklet log env — console fallback.
29
+ /* eslint-disable no-console */
30
+ resolved = {
31
+ debug: (...args: any[]) => console.debug(...args),
32
+ info: (...args: any[]) => console.info(...args),
33
+ error: (...args: any[]) => console.error(...args),
34
+ warn: (...args: any[]) => console.warn(...args),
35
+ };
36
+ /* eslint-enable no-console */
37
+ }
38
+ return resolved;
39
+ }
40
+
41
+ const logger: Logger = {
42
+ debug: (...args: any[]) => resolveLogger().debug(...args),
43
+ info: (...args: any[]) => resolveLogger().info(...args),
44
+ error: (...args: any[]) => resolveLogger().error(...args),
45
+ warn: (...args: any[]) => resolveLogger().warn(...args),
46
+ };
16
47
 
17
48
  export default logger;
18
49
 
19
- const { setupAccessLogger } = createLogger;
20
- export { setupAccessLogger };
50
+ // Express access logger also lazy. A bare host that never mounts the node
51
+ // handler (where setupAccessLogger runs) never needs the blocklet log env.
52
+ export function setupAccessLogger(app: any): void {
53
+ try {
54
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
55
+ const createLogger = require('@blocklet/logger');
56
+ createLogger.setupAccessLogger?.(app);
57
+ } catch {
58
+ // bare host: no access logging
59
+ }
60
+ }
@@ -1,4 +1,4 @@
1
- import { Notification as BlockletNotification } from '@blocklet/sdk';
1
+ import BlockletNotification from '@blocklet/sdk/service/notification';
2
2
 
3
3
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './template/base';
4
4
  import { CheckoutSession, CreditGrant, Invoice, Meter, MeterEvent, Subscription } from '../../store/models';
@@ -2,6 +2,7 @@
2
2
  /* eslint-disable @typescript-eslint/indent */
3
3
  import { withQuery } from 'ufo';
4
4
  import { BN } from '@ocap/util';
5
+ import { creditLowBalanceThresholdPercentage } from '../../env';
5
6
  import { getUserLocale } from '../../../integrations/blocklet/notification';
6
7
  import { translate } from '../../../locales';
7
8
  import { Customer, PaymentCurrency, CreditGrant } from '../../../store/models';
@@ -35,7 +36,7 @@ export class CustomerCreditLowBalanceEmailTemplate implements BaseEmailTemplate<
35
36
  * @returns threshold percentage (default: 10)
36
37
  */
37
38
  static getLowBalanceThresholdPercent(): number {
38
- return parseInt(process.env.CREDIT_LOW_BALANCE_THRESHOLD_PERCENTAGE || '10', 10);
39
+ return creditLowBalanceThresholdPercentage();
39
40
  }
40
41
 
41
42
  /**
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { getUrl } from '@blocklet/sdk';
3
+ import { getUrl } from '@blocklet/sdk/lib/component';
4
4
  import { getUserLocale } from '../../../integrations/blocklet/notification';
5
5
  import { translate } from '../../../locales';
6
6
  import { CheckoutSession, Customer, PaymentLink, PaymentMethod, Payout } from '../../../store/models';
@@ -4,7 +4,7 @@ import isEmpty from 'lodash/isEmpty';
4
4
  import pWaitFor from 'p-wait-for';
5
5
  import type { LiteralUnion } from 'type-fest';
6
6
 
7
- import { getUrl } from '@blocklet/sdk';
7
+ import { getUrl } from '@blocklet/sdk/lib/component';
8
8
  import { getUserLocale } from '../../../integrations/blocklet/notification';
9
9
  import { translate } from '../../../locales';
10
10
  import {
@@ -1,4 +1,4 @@
1
- import { createProductAndPrices } from '../routes/products';
1
+ import { createProductAndPrices } from '../routes/hono/products';
2
2
  import { PaymentCurrency, PaymentMethod, Price, Product } from '../store/models';
3
3
  import logger from './logger';
4
4
 
@@ -1,4 +1,4 @@
1
- import { component } from '@blocklet/sdk';
1
+ import component from '@blocklet/sdk/lib/component';
2
2
  import type { LiteralUnion } from 'type-fest';
3
3
  import { withQuery } from 'ufo';
4
4
  import { getConnectQueryParam } from './util';
@@ -6,15 +6,91 @@ import fastq from 'fastq';
6
6
  import { nanoid } from 'nanoid';
7
7
 
8
8
  import { AsyncLocalStorage } from 'async_hooks';
9
+ import { isTestEnv } from '../env';
9
10
  import logger from '../logger';
10
- import { sleep, tryWithTimeout } from '../util';
11
+ import { context, withTenant } from '../context';
12
+ import {
13
+ TENANT_CONTEXT_MISSING,
14
+ TENANT_MISMATCH,
15
+ TenantError,
16
+ assertValidInstanceDid,
17
+ getDefaultInstanceDid,
18
+ getTenantMode,
19
+ resolveRowTenant,
20
+ } from '../tenant';
21
+ import { tryWithTimeout } from '../util';
11
22
  import dayjs from '../dayjs';
12
23
  import createQueueStore from './store';
24
+ import { registerQueue, getQueueRuntimeMode, trackPending } from './runtime';
13
25
  import { Job } from '../../store/models/job';
14
26
  import { sequelize } from '../../store/sequelize';
15
27
 
16
28
  const CANCELLED = '__CANCELLED__';
17
- const MIN_DELAY = process.env.NODE_ENV === 'test' ? 2 : 8;
29
+ // lazy so the injected config (set after import) is honored at call time
30
+ const minDelay = (): number => (isTestEnv() ? 2 : 8);
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Phase 5 (W1-4a): generic queue tenant layer.
34
+ // Every queue gets this uniformly — see docs/arc-integration/planning/w1-w2/
35
+ // queue-matrix.md for the per-queue conclusions built on top of it.
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Stamp the active tenant into a job payload at enqueue time.
40
+ * - payload already carries instance_did: validated (and in multi mode it
41
+ * must be a legal DID — a forged/illegal value is refused at the gate)
42
+ * - otherwise: the context tenant (single mode = app DID); multi mode
43
+ * without context -> the push itself is rejected (fail-closed)
44
+ */
45
+ function injectJobTenant(job: any): any {
46
+ if (!job || typeof job !== 'object') return job;
47
+ if (job.instance_did) {
48
+ assertValidInstanceDid(job.instance_did);
49
+ return job;
50
+ }
51
+ return { ...job, instance_did: context.getInstanceDid() };
52
+ }
53
+
54
+ /**
55
+ * Run a job handler inside its payload tenant. Legacy jobs persisted before
56
+ * Phase 5 have no instance_did: single mode falls back to the deployment
57
+ * default, multi mode refuses permanently (non-retryable + structured alert).
58
+ */
59
+ function runJobWithTenant<T>(job: any, onJob: (job: T) => Promise<any>): Promise<any> {
60
+ const tenant = job?.instance_did;
61
+ if (tenant) {
62
+ return withTenant(tenant, () => onJob(job));
63
+ }
64
+ if (getTenantMode() === 'single') {
65
+ return withTenant(getDefaultInstanceDid(), () => onJob(job));
66
+ }
67
+ const err = new TenantError(TENANT_CONTEXT_MISSING, 'legacy job without tenant refused in multi mode');
68
+ (err as any).nonRetryable = true;
69
+ logger.error('[queue] legacy job without tenant refused', { code: TENANT_CONTEXT_MISSING, job });
70
+ return Promise.reject(err);
71
+ }
72
+
73
+ /**
74
+ * Handler-entry invariant for object-bound queues: the object loaded by id
75
+ * must belong to the payload tenant the handler is running under. A forged
76
+ * payload (tenant A, object of B) dies here permanently — nothing executes.
77
+ */
78
+ export function assertJobObjectTenant(row: { instance_did?: string | null } | null | undefined): void {
79
+ if (!row) return; // absent objects are the handler's own no-op/warn path
80
+ const rowTenant = resolveRowTenant(row);
81
+ const jobTenant = context.getInstanceDid();
82
+ if (rowTenant !== jobTenant) {
83
+ // Phase 4 (W1-3): emit a structured violation alert BEFORE throwing, so a
84
+ // forged/mixed cross-tenant job is observable in logs (same dedicated code
85
+ // as the W1 §4.1 event-rejection path) even if the queue swallows the
86
+ // thrown error. Loading the object cross-tenant (systemFindByPk) is what
87
+ // makes this violation visible instead of folding into a scoped null.
88
+ logger.error('TENANT_VIOLATION', { code: TENANT_MISMATCH, rowTenant, jobTenant });
89
+ const err = new TenantError(TENANT_MISMATCH, `job object belongs to ${rowTenant} but payload says ${jobTenant}`);
90
+ (err as any).nonRetryable = true;
91
+ throw err;
92
+ }
93
+ }
18
94
 
19
95
  type QueueOptions<T> = {
20
96
  id?: (job: T) => string;
@@ -46,6 +122,18 @@ type PushParams<T> = {
46
122
  delay?: number; // in seconds
47
123
  runAt?: number; // unix timestamp in seconds
48
124
  skipDuplicateCheck?: boolean; // Q1: skip addJob's findOne when caller guarantees no duplicate
125
+ /**
126
+ * Internal: re-delivery of a row already persisted in the jobs table
127
+ * (startup/scheduled recovery). Skips the push-side tenant gate so legacy
128
+ * pre-tenant rows reach runJobWithTenant, which owns the legacy strategy
129
+ * (single -> default tenant, multi -> structured non-retryable refusal).
130
+ *
131
+ * NEVER set this from application code: it bypasses the enqueue tenant
132
+ * gate. Object-bound handlers are still protected by
133
+ * assertJobObjectTenant, but system-operation queues (no object load)
134
+ * would run under whatever tenant the payload claims.
135
+ */
136
+ fromStore?: boolean;
49
137
  };
50
138
 
51
139
  export default function createQueue<T = any>({ name, onJob, options = defaults }: QueueParams<T>) {
@@ -98,7 +186,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
98
186
  }
99
187
 
100
188
  try {
101
- const result = await tryWithTimeout(() => onJob(job), maxTimeout);
189
+ const result = await tryWithTimeout(() => runJobWithTenant(job, onJob), maxTimeout);
102
190
  logger.info('job finished', { id, result });
103
191
  cb(null, result);
104
192
  } catch (err) {
@@ -108,7 +196,15 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
108
196
  // @ts-ignore
109
197
  }, concurrency);
110
198
 
111
- const push = ({ job, id, persist = true, delay, runAt, skipDuplicateCheck = false }: PushParams<T>) => {
199
+ const push = ({
200
+ job: rawJob,
201
+ id,
202
+ persist = true,
203
+ delay,
204
+ runAt,
205
+ skipDuplicateCheck = false,
206
+ fromStore = false,
207
+ }: PushParams<T>) => {
112
208
  const jobEvents = new EventEmitter();
113
209
  const emit = (e: string, data: any) => {
114
210
  queueEvents.emit(e, data);
@@ -116,20 +212,24 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
116
212
  };
117
213
  const now = dayjs().unix();
118
214
 
119
- if (!job) {
215
+ if (!rawJob) {
120
216
  throw new Error('Can not queue empty job');
121
217
  }
122
218
 
219
+ // fail-closed gate: every NEW payload carries its tenant from here on;
220
+ // store re-deliveries pass through so the execution-side legacy strategy applies
221
+ const job = (fromStore ? rawJob : injectJobTenant(rawJob)) as T;
222
+
123
223
  const jobId = getJobId(id, job);
124
224
 
125
- if ((delay && delay >= MIN_DELAY) || (runAt && runAt > now)) {
225
+ if ((delay && delay >= minDelay()) || (runAt && runAt > now)) {
126
226
  if (!enableScheduledJob) {
127
227
  throw new Error('Must set options.enableScheduledJob to true to run delay jobs');
128
228
  }
129
229
 
130
230
  // 这里不是精确的 delay, 延迟的时间太短没有意义,所以这里限制了最小 delay
131
- if (delay && delay < MIN_DELAY) {
132
- throw new Error(`minimum delay is ${MIN_DELAY}s`);
231
+ if (delay && delay < minDelay()) {
232
+ throw new Error(`minimum delay is ${minDelay()}s`);
133
233
  }
134
234
 
135
235
  const attrs: { delay?: number; will_run_at?: number } = {};
@@ -221,12 +321,31 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
221
321
  queue.push({ id: jobId, job, persist }, onJobComplete);
222
322
  });
223
323
 
324
+ // Phase 12b: workerd flush. Track this immediate execution so a frozen-
325
+ // isolate host can drain it before returning the response — otherwise the
326
+ // fire-and-forget setImmediate/fastq work is dropped when the isolate is
327
+ // torn down. Tracked ONLY on a workerd host: zero overhead and no extra
328
+ // listeners on a long-lived node process.
329
+ let resolveSettled: () => void = () => {};
330
+ if (getQueueRuntimeMode() === 'workerd') {
331
+ const settled = new Promise<void>((resolve) => {
332
+ resolveSettled = resolve;
333
+ });
334
+ jobEvents.on('finished', resolveSettled);
335
+ jobEvents.on('failed', resolveSettled);
336
+ jobEvents.on('cancelled', resolveSettled);
337
+ trackPending(settled);
338
+ }
339
+
224
340
  if (persist) {
225
341
  store
226
342
  .addJob(jobId, job, {}, skipDuplicateCheck)
227
343
  .then(queueJob)
228
344
  .catch((err) => {
229
345
  logger.error('Can not add job to store', { error: err });
346
+ // never enqueued (e.g. duplicate id) → emits nothing; release the
347
+ // workerd flush tracker so flushQueueWork() cannot hang on it.
348
+ resolveSettled();
230
349
  });
231
350
  } else {
232
351
  queueJob();
@@ -246,7 +365,11 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
246
365
 
247
366
  const job = push(params);
248
367
  job.on('finished', (data: { id: string; job: T; result: any }) => resolve(data));
249
- job.on('canceled', (data: { id: string; job: T }) => resolve(data));
368
+ // Phase 12b: the engine emits 'cancelled' (two L); the old 'canceled'
369
+ // listener never fired, so a cancelled job left pushAndWait hanging
370
+ // forever — and the CF queue() consumer now runs through pushAndWait,
371
+ // where that hang would stall the queue batch until CF kills it.
372
+ job.on('cancelled', (data: { id: string; job: T }) => resolve(data));
250
373
  job.on('failed', (data: { id: string; job: T; error: Error }) => reject(data));
251
374
  } catch (err) {
252
375
  reject(err);
@@ -296,60 +419,128 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
296
419
  return updatedJob;
297
420
  };
298
421
 
299
- // Populate the queue on startup
300
- process.nextTick(async () => {
301
- try {
302
- if (!Job.isInitialized()) {
303
- Job.initialize(sequelize);
422
+ // Populate the queue on startup. Phase 12b: this is a long-lived-process
423
+ // recovery — re-queue persisted immediate rows on boot. A workerd host spins
424
+ // up a fresh isolate per request and MUST NOT re-run every persisted row on
425
+ // each cold start; it drives due-job re-dispatch through dispatchDueJobs()
426
+ // from scheduled() instead. So the boot recovery is node-mode only.
427
+ if (getQueueRuntimeMode() === 'node') {
428
+ process.nextTick(async () => {
429
+ try {
430
+ if (!Job.isInitialized()) {
431
+ Job.initialize(sequelize);
432
+ }
433
+ const jobs = await store.getJobs();
434
+ jobs.forEach((x) => {
435
+ if (x.job && x.id) {
436
+ try {
437
+ push({ job: x.job, id: x.id, persist: false, fromStore: true });
438
+ } catch (err: any) {
439
+ // one bad row must never break recovery of the rest
440
+ logger.error('failed to re-queue stored job', { id: x.id, code: err?.code, message: err?.message });
441
+ }
442
+ } else {
443
+ logger.info('skip invalid job from db', { job: x });
444
+ }
445
+ });
446
+ // eslint-disable-next-line no-shadow
447
+ } catch (err) {
448
+ console.error(err);
449
+ logger.error(`Can not load existing ${name} jobs`, { error: err });
304
450
  }
305
- const jobs = await store.getJobs();
306
- jobs.forEach((x) => {
307
- if (x.job && x.id) {
308
- push({ job: x.job, id: x.id, persist: false });
309
- } else {
310
- logger.info('skip invalid job from db', { job: x });
451
+ });
452
+ }
453
+
454
+ // Re-deliver this queue's due delayed rows once — the body shared by the node
455
+ // loop() (timer-driven) and the host dispatchDueJobs() (CF scheduled()-driven).
456
+ // Same cancel-then-replay-from-store path either way, so worker and node agree
457
+ // on exactly how a due delayed job runs.
458
+ const redispatchDue = async (): Promise<{ dispatched: number; failed: number }> => {
459
+ let dispatched = 0;
460
+ let failed = 0;
461
+ if (enableScheduledJob !== true) return { dispatched, failed };
462
+ const jobs = await store.getScheduledJobs();
463
+ for (const x of jobs) {
464
+ if (x.job && x.id) {
465
+ // fix: https://github.com/blocklet/payment-kit/issues/287
466
+ // Intentional cancel-then-replay: marking the row cancelled=true keeps
467
+ // the NEXT due-poll (loop tick / dispatchDueJobs) from re-picking it
468
+ // while this run is in flight; the immediate re-push below clears the
469
+ // in-memory cancel flag for THIS execution, and onJobComplete deletes
470
+ // the row on success. A reader watching raw DB state mid-dispatch will
471
+ // briefly see cancelled=true — that is the de-dupe latch, not an error.
472
+ // eslint-disable-next-line no-await-in-loop
473
+ await cancel(x.id);
474
+ logger.info('reschedule delayed or scheduled job', { id: x.id, job: x.job });
475
+ try {
476
+ push({ job: x.job, id: x.id, persist: false, fromStore: true });
477
+ dispatched += 1;
478
+ } catch (err: any) {
479
+ failed += 1;
480
+ logger.error('failed to reschedule stored job', { id: x.id, code: err?.code, message: err?.message });
311
481
  }
312
- });
313
- // eslint-disable-next-line no-shadow
314
- } catch (err) {
315
- console.error(err);
316
- logger.error(`Can not load existing ${name} jobs`, { error: err });
482
+ } else {
483
+ logger.info('skip invalid job from db', { job: x });
484
+ }
317
485
  }
318
- });
486
+ return { dispatched, failed };
487
+ };
319
488
 
320
- const loop = async () => {
321
- if (enableScheduledJob === true) {
322
- // eslint-disable-next-line no-constant-condition
323
- while (true) {
324
- await sleep((MIN_DELAY * 1000) / 2);
489
+ // D2 teardown: the node poll loop is cancelable. `stopLoop()` flips the flag
490
+ // AND clears the pending sleep timer (so the process has no dangling handle on
491
+ // stop — the spec's "active handles 归零") and resolves the in-flight sleep so
492
+ // the loop observes the flag and returns immediately.
493
+ let loopStopped = false;
494
+ let loopTimer: ReturnType<typeof setTimeout> | null = null;
495
+ let loopWake: (() => void) | null = null;
496
+ const cancelableSleep = (ms: number): Promise<void> =>
497
+ new Promise<void>((resolve) => {
498
+ loopWake = resolve;
499
+ loopTimer = setTimeout(() => {
500
+ loopTimer = null;
501
+ loopWake = null;
502
+ resolve();
503
+ }, ms);
504
+ });
505
+ const stopLoop = (): void => {
506
+ loopStopped = true;
507
+ if (loopTimer) {
508
+ clearTimeout(loopTimer);
509
+ loopTimer = null;
510
+ }
511
+ if (loopWake) {
512
+ const wake = loopWake;
513
+ loopWake = null;
514
+ wake();
515
+ }
516
+ };
325
517
 
326
- try {
327
- /* eslint-disable no-await-in-loop */
328
- const jobs = await store.getScheduledJobs();
329
- for (const x of jobs) {
330
- if (x.job && x.id) {
331
- // fix: https://github.com/blocklet/payment-kit/issues/287
332
- await cancel(x.id);
333
- logger.info('reschedule delayed or scheduled job', {
334
- id: x.id,
335
- job: x.job,
336
- });
337
- push({ job: x.job, id: x.id, persist: false });
338
- } else {
339
- logger.info('skip invalid job from db', { job: x });
340
- }
341
- }
342
- } catch (err) {
343
- console.error(err);
344
- logger.error(`Can not load scheduled ${name} jobs`, { error: err });
345
- }
518
+ // The node poll loop. Disabled on a workerd host: a frozen isolate cannot run
519
+ // a background timer, so the host calls dispatchDueJobs() from scheduled()
520
+ // instead (which runs redispatchDue() above — the same code path).
521
+ const loop = async () => {
522
+ if (enableScheduledJob !== true) return;
523
+ if (getQueueRuntimeMode() !== 'node') return;
524
+ while (!loopStopped) {
525
+ // eslint-disable-next-line no-await-in-loop
526
+ await cancelableSleep((minDelay() * 1000) / 2);
527
+ // stopped during the sleep (teardown) — exit before doing any work.
528
+ if (loopStopped) return;
529
+ // mode can flip after assembly (a host that sets workerd late); stop then.
530
+ if (getQueueRuntimeMode() !== 'node') return;
531
+ try {
532
+ // eslint-disable-next-line no-await-in-loop
533
+ await redispatchDue();
534
+ } catch (err) {
535
+ console.error(err);
536
+ logger.error(`Can not load scheduled ${name} jobs`, { error: err });
346
537
  }
347
538
  }
348
539
  };
349
540
 
350
541
  loop();
351
542
 
352
- return Object.assign(queueEvents, {
543
+ const queueInstance = Object.assign(queueEvents, {
353
544
  store,
354
545
  push,
355
546
  pushAndWait,
@@ -361,6 +552,8 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
361
552
  delete: deleteJob,
362
553
  cancel,
363
554
  update: updateJob,
555
+ /** D2 teardown: stop this queue's node poll loop (no-op if not scheduled). */
556
+ stop: stopLoop,
364
557
  options: {
365
558
  concurrency,
366
559
  maxRetries,
@@ -369,4 +562,18 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
369
562
  enableScheduledJob,
370
563
  },
371
564
  });
565
+
566
+ // Phase 12b: register into the host-facing queue runtime surface so the CF
567
+ // worker can look the handle up by name (queue() consumer) and drive due
568
+ // re-dispatch (scheduled()) through the service/slot boundary — no more
569
+ // direct cloudflare/shims/queue.ts imports.
570
+ registerQueue({
571
+ name,
572
+ enableScheduledJob: enableScheduledJob === true,
573
+ handle: queueInstance,
574
+ redispatchDue,
575
+ stop: stopLoop,
576
+ });
577
+
578
+ return queueInstance;
372
579
  }