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.
- package/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +47 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +41 -37
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/crons/tenant-fanout.ts +82 -0
- package/api/src/host-node/did-connect-runtime-node.ts +33 -0
- package/api/src/host-node/serve-static-arc.ts +68 -0
- package/api/src/host-node/serve-static.ts +41 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +247 -47
- package/api/src/libs/context.ts +89 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
- package/api/src/libs/did-connect/tenant-identity.ts +221 -0
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +142 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +60 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +271 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +80 -0
- package/api/src/middlewares/hono/csrf.ts +83 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +209 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +38 -21
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +41 -11
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +64 -37
- package/api/src/queues/payout.ts +37 -21
- package/api/src/queues/refund.ts +36 -18
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +814 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +82 -23
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
- package/api/tests/crons/tenant-fanout.spec.ts +158 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
- package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/service-host.spec.ts +37 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +292 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
- package/api/tests/service/fail-closed-http.spec.ts +79 -0
- package/api/tests/service/static-arc-handler.spec.ts +101 -0
- package/api/tests/service/static-externalized.spec.ts +48 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
- package/cloudflare/README.md +34 -27
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
- package/cloudflare/build.ts +33 -13
- package/cloudflare/cf-adapter.ts +419 -0
- package/cloudflare/did-connect-runtime.ts +96 -0
- package/cloudflare/did-connect-token-storage.ts +151 -0
- package/cloudflare/esbuild-cf-config.cjs +407 -0
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +33 -403
- package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
- package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/cf-adapter.spec.ts +244 -0
- package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
- package/cloudflare/vite.config.ts +53 -45
- package/cloudflare/worker.ts +261 -448
- package/cloudflare/wrangler.json +0 -6
- package/cloudflare/wrangler.jsonc +0 -6
- package/cloudflare/wrangler.local-e2e.jsonc +25 -0
- package/cloudflare/wrangler.staging.json +0 -6
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/bootstrap-inject.ts +166 -0
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/app.tsx +2 -1
- package/src/env.d.ts +13 -1
- package/src/libs/service-host.ts +13 -0
- package/tsconfig.json +1 -1
- package/vite.arc.config.ts +159 -0
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/did-connect-auth.ts +0 -527
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -6,15 +6,103 @@ 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 {
|
|
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
|
-
|
|
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
|
+
// Warm the tenant identity then run the handler — the queue analogue of the HTTP
|
|
60
|
+
// contextMiddleware warm. Signing queues (payment/refund/payout/...) access the
|
|
61
|
+
// business wallet synchronously; warming inside the tenant span makes `wallet`/
|
|
62
|
+
// `ethWallet` resolve to the job's tenant (no-op on blocklet-server).
|
|
63
|
+
async function warmThenRun<T>(onJob: (job: T) => Promise<any>, job: T): Promise<any> {
|
|
64
|
+
const { warmTenantIdentity } =
|
|
65
|
+
// eslint-disable-next-line global-require
|
|
66
|
+
require('../did-connect/tenant-identity') as typeof import('../did-connect/tenant-identity');
|
|
67
|
+
await warmTenantIdentity();
|
|
68
|
+
return onJob(job);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runJobWithTenant<T>(job: any, onJob: (job: T) => Promise<any>): Promise<any> {
|
|
72
|
+
const tenant = job?.instance_did;
|
|
73
|
+
if (tenant) {
|
|
74
|
+
return withTenant(tenant, () => warmThenRun(onJob, job));
|
|
75
|
+
}
|
|
76
|
+
if (getTenantMode() === 'single') {
|
|
77
|
+
return withTenant(getDefaultInstanceDid(), () => warmThenRun(onJob, job));
|
|
78
|
+
}
|
|
79
|
+
const err = new TenantError(TENANT_CONTEXT_MISSING, 'legacy job without tenant refused in multi mode');
|
|
80
|
+
(err as any).nonRetryable = true;
|
|
81
|
+
logger.error('[queue] legacy job without tenant refused', { code: TENANT_CONTEXT_MISSING, job });
|
|
82
|
+
return Promise.reject(err);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handler-entry invariant for object-bound queues: the object loaded by id
|
|
87
|
+
* must belong to the payload tenant the handler is running under. A forged
|
|
88
|
+
* payload (tenant A, object of B) dies here permanently — nothing executes.
|
|
89
|
+
*/
|
|
90
|
+
export function assertJobObjectTenant(row: { instance_did?: string | null } | null | undefined): void {
|
|
91
|
+
if (!row) return; // absent objects are the handler's own no-op/warn path
|
|
92
|
+
const rowTenant = resolveRowTenant(row);
|
|
93
|
+
const jobTenant = context.getInstanceDid();
|
|
94
|
+
if (rowTenant !== jobTenant) {
|
|
95
|
+
// Phase 4 (W1-3): emit a structured violation alert BEFORE throwing, so a
|
|
96
|
+
// forged/mixed cross-tenant job is observable in logs (same dedicated code
|
|
97
|
+
// as the W1 §4.1 event-rejection path) even if the queue swallows the
|
|
98
|
+
// thrown error. Loading the object cross-tenant (systemFindByPk) is what
|
|
99
|
+
// makes this violation visible instead of folding into a scoped null.
|
|
100
|
+
logger.error('TENANT_VIOLATION', { code: TENANT_MISMATCH, rowTenant, jobTenant });
|
|
101
|
+
const err = new TenantError(TENANT_MISMATCH, `job object belongs to ${rowTenant} but payload says ${jobTenant}`);
|
|
102
|
+
(err as any).nonRetryable = true;
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
18
106
|
|
|
19
107
|
type QueueOptions<T> = {
|
|
20
108
|
id?: (job: T) => string;
|
|
@@ -46,6 +134,18 @@ type PushParams<T> = {
|
|
|
46
134
|
delay?: number; // in seconds
|
|
47
135
|
runAt?: number; // unix timestamp in seconds
|
|
48
136
|
skipDuplicateCheck?: boolean; // Q1: skip addJob's findOne when caller guarantees no duplicate
|
|
137
|
+
/**
|
|
138
|
+
* Internal: re-delivery of a row already persisted in the jobs table
|
|
139
|
+
* (startup/scheduled recovery). Skips the push-side tenant gate so legacy
|
|
140
|
+
* pre-tenant rows reach runJobWithTenant, which owns the legacy strategy
|
|
141
|
+
* (single -> default tenant, multi -> structured non-retryable refusal).
|
|
142
|
+
*
|
|
143
|
+
* NEVER set this from application code: it bypasses the enqueue tenant
|
|
144
|
+
* gate. Object-bound handlers are still protected by
|
|
145
|
+
* assertJobObjectTenant, but system-operation queues (no object load)
|
|
146
|
+
* would run under whatever tenant the payload claims.
|
|
147
|
+
*/
|
|
148
|
+
fromStore?: boolean;
|
|
49
149
|
};
|
|
50
150
|
|
|
51
151
|
export default function createQueue<T = any>({ name, onJob, options = defaults }: QueueParams<T>) {
|
|
@@ -98,7 +198,7 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
98
198
|
}
|
|
99
199
|
|
|
100
200
|
try {
|
|
101
|
-
const result = await tryWithTimeout(() =>
|
|
201
|
+
const result = await tryWithTimeout(() => runJobWithTenant(job, onJob), maxTimeout);
|
|
102
202
|
logger.info('job finished', { id, result });
|
|
103
203
|
cb(null, result);
|
|
104
204
|
} catch (err) {
|
|
@@ -108,7 +208,15 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
108
208
|
// @ts-ignore
|
|
109
209
|
}, concurrency);
|
|
110
210
|
|
|
111
|
-
const push = ({
|
|
211
|
+
const push = ({
|
|
212
|
+
job: rawJob,
|
|
213
|
+
id,
|
|
214
|
+
persist = true,
|
|
215
|
+
delay,
|
|
216
|
+
runAt,
|
|
217
|
+
skipDuplicateCheck = false,
|
|
218
|
+
fromStore = false,
|
|
219
|
+
}: PushParams<T>) => {
|
|
112
220
|
const jobEvents = new EventEmitter();
|
|
113
221
|
const emit = (e: string, data: any) => {
|
|
114
222
|
queueEvents.emit(e, data);
|
|
@@ -116,20 +224,24 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
116
224
|
};
|
|
117
225
|
const now = dayjs().unix();
|
|
118
226
|
|
|
119
|
-
if (!
|
|
227
|
+
if (!rawJob) {
|
|
120
228
|
throw new Error('Can not queue empty job');
|
|
121
229
|
}
|
|
122
230
|
|
|
231
|
+
// fail-closed gate: every NEW payload carries its tenant from here on;
|
|
232
|
+
// store re-deliveries pass through so the execution-side legacy strategy applies
|
|
233
|
+
const job = (fromStore ? rawJob : injectJobTenant(rawJob)) as T;
|
|
234
|
+
|
|
123
235
|
const jobId = getJobId(id, job);
|
|
124
236
|
|
|
125
|
-
if ((delay && delay >=
|
|
237
|
+
if ((delay && delay >= minDelay()) || (runAt && runAt > now)) {
|
|
126
238
|
if (!enableScheduledJob) {
|
|
127
239
|
throw new Error('Must set options.enableScheduledJob to true to run delay jobs');
|
|
128
240
|
}
|
|
129
241
|
|
|
130
242
|
// 这里不是精确的 delay, 延迟的时间太短没有意义,所以这里限制了最小 delay
|
|
131
|
-
if (delay && delay <
|
|
132
|
-
throw new Error(`minimum delay is ${
|
|
243
|
+
if (delay && delay < minDelay()) {
|
|
244
|
+
throw new Error(`minimum delay is ${minDelay()}s`);
|
|
133
245
|
}
|
|
134
246
|
|
|
135
247
|
const attrs: { delay?: number; will_run_at?: number } = {};
|
|
@@ -221,12 +333,31 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
221
333
|
queue.push({ id: jobId, job, persist }, onJobComplete);
|
|
222
334
|
});
|
|
223
335
|
|
|
336
|
+
// Phase 12b: workerd flush. Track this immediate execution so a frozen-
|
|
337
|
+
// isolate host can drain it before returning the response — otherwise the
|
|
338
|
+
// fire-and-forget setImmediate/fastq work is dropped when the isolate is
|
|
339
|
+
// torn down. Tracked ONLY on a workerd host: zero overhead and no extra
|
|
340
|
+
// listeners on a long-lived node process.
|
|
341
|
+
let resolveSettled: () => void = () => {};
|
|
342
|
+
if (getQueueRuntimeMode() === 'workerd') {
|
|
343
|
+
const settled = new Promise<void>((resolve) => {
|
|
344
|
+
resolveSettled = resolve;
|
|
345
|
+
});
|
|
346
|
+
jobEvents.on('finished', resolveSettled);
|
|
347
|
+
jobEvents.on('failed', resolveSettled);
|
|
348
|
+
jobEvents.on('cancelled', resolveSettled);
|
|
349
|
+
trackPending(settled);
|
|
350
|
+
}
|
|
351
|
+
|
|
224
352
|
if (persist) {
|
|
225
353
|
store
|
|
226
354
|
.addJob(jobId, job, {}, skipDuplicateCheck)
|
|
227
355
|
.then(queueJob)
|
|
228
356
|
.catch((err) => {
|
|
229
357
|
logger.error('Can not add job to store', { error: err });
|
|
358
|
+
// never enqueued (e.g. duplicate id) → emits nothing; release the
|
|
359
|
+
// workerd flush tracker so flushQueueWork() cannot hang on it.
|
|
360
|
+
resolveSettled();
|
|
230
361
|
});
|
|
231
362
|
} else {
|
|
232
363
|
queueJob();
|
|
@@ -246,7 +377,11 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
246
377
|
|
|
247
378
|
const job = push(params);
|
|
248
379
|
job.on('finished', (data: { id: string; job: T; result: any }) => resolve(data));
|
|
249
|
-
|
|
380
|
+
// Phase 12b: the engine emits 'cancelled' (two L); the old 'canceled'
|
|
381
|
+
// listener never fired, so a cancelled job left pushAndWait hanging
|
|
382
|
+
// forever — and the CF queue() consumer now runs through pushAndWait,
|
|
383
|
+
// where that hang would stall the queue batch until CF kills it.
|
|
384
|
+
job.on('cancelled', (data: { id: string; job: T }) => resolve(data));
|
|
250
385
|
job.on('failed', (data: { id: string; job: T; error: Error }) => reject(data));
|
|
251
386
|
} catch (err) {
|
|
252
387
|
reject(err);
|
|
@@ -296,60 +431,128 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
296
431
|
return updatedJob;
|
|
297
432
|
};
|
|
298
433
|
|
|
299
|
-
// Populate the queue on startup
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
434
|
+
// Populate the queue on startup. Phase 12b: this is a long-lived-process
|
|
435
|
+
// recovery — re-queue persisted immediate rows on boot. A workerd host spins
|
|
436
|
+
// up a fresh isolate per request and MUST NOT re-run every persisted row on
|
|
437
|
+
// each cold start; it drives due-job re-dispatch through dispatchDueJobs()
|
|
438
|
+
// from scheduled() instead. So the boot recovery is node-mode only.
|
|
439
|
+
if (getQueueRuntimeMode() === 'node') {
|
|
440
|
+
process.nextTick(async () => {
|
|
441
|
+
try {
|
|
442
|
+
if (!Job.isInitialized()) {
|
|
443
|
+
Job.initialize(sequelize);
|
|
444
|
+
}
|
|
445
|
+
const jobs = await store.getJobs();
|
|
446
|
+
jobs.forEach((x) => {
|
|
447
|
+
if (x.job && x.id) {
|
|
448
|
+
try {
|
|
449
|
+
push({ job: x.job, id: x.id, persist: false, fromStore: true });
|
|
450
|
+
} catch (err: any) {
|
|
451
|
+
// one bad row must never break recovery of the rest
|
|
452
|
+
logger.error('failed to re-queue stored job', { id: x.id, code: err?.code, message: err?.message });
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
logger.info('skip invalid job from db', { job: x });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
// eslint-disable-next-line no-shadow
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.error(err);
|
|
461
|
+
logger.error(`Can not load existing ${name} jobs`, { error: err });
|
|
304
462
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Re-deliver this queue's due delayed rows once — the body shared by the node
|
|
467
|
+
// loop() (timer-driven) and the host dispatchDueJobs() (CF scheduled()-driven).
|
|
468
|
+
// Same cancel-then-replay-from-store path either way, so worker and node agree
|
|
469
|
+
// on exactly how a due delayed job runs.
|
|
470
|
+
const redispatchDue = async (): Promise<{ dispatched: number; failed: number }> => {
|
|
471
|
+
let dispatched = 0;
|
|
472
|
+
let failed = 0;
|
|
473
|
+
if (enableScheduledJob !== true) return { dispatched, failed };
|
|
474
|
+
const jobs = await store.getScheduledJobs();
|
|
475
|
+
for (const x of jobs) {
|
|
476
|
+
if (x.job && x.id) {
|
|
477
|
+
// fix: https://github.com/blocklet/payment-kit/issues/287
|
|
478
|
+
// Intentional cancel-then-replay: marking the row cancelled=true keeps
|
|
479
|
+
// the NEXT due-poll (loop tick / dispatchDueJobs) from re-picking it
|
|
480
|
+
// while this run is in flight; the immediate re-push below clears the
|
|
481
|
+
// in-memory cancel flag for THIS execution, and onJobComplete deletes
|
|
482
|
+
// the row on success. A reader watching raw DB state mid-dispatch will
|
|
483
|
+
// briefly see cancelled=true — that is the de-dupe latch, not an error.
|
|
484
|
+
// eslint-disable-next-line no-await-in-loop
|
|
485
|
+
await cancel(x.id);
|
|
486
|
+
logger.info('reschedule delayed or scheduled job', { id: x.id, job: x.job });
|
|
487
|
+
try {
|
|
488
|
+
push({ job: x.job, id: x.id, persist: false, fromStore: true });
|
|
489
|
+
dispatched += 1;
|
|
490
|
+
} catch (err: any) {
|
|
491
|
+
failed += 1;
|
|
492
|
+
logger.error('failed to reschedule stored job', { id: x.id, code: err?.code, message: err?.message });
|
|
311
493
|
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.error(err);
|
|
316
|
-
logger.error(`Can not load existing ${name} jobs`, { error: err });
|
|
494
|
+
} else {
|
|
495
|
+
logger.info('skip invalid job from db', { job: x });
|
|
496
|
+
}
|
|
317
497
|
}
|
|
318
|
-
|
|
498
|
+
return { dispatched, failed };
|
|
499
|
+
};
|
|
319
500
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
501
|
+
// D2 teardown: the node poll loop is cancelable. `stopLoop()` flips the flag
|
|
502
|
+
// AND clears the pending sleep timer (so the process has no dangling handle on
|
|
503
|
+
// stop — the spec's "active handles 归零") and resolves the in-flight sleep so
|
|
504
|
+
// the loop observes the flag and returns immediately.
|
|
505
|
+
let loopStopped = false;
|
|
506
|
+
let loopTimer: ReturnType<typeof setTimeout> | null = null;
|
|
507
|
+
let loopWake: (() => void) | null = null;
|
|
508
|
+
const cancelableSleep = (ms: number): Promise<void> =>
|
|
509
|
+
new Promise<void>((resolve) => {
|
|
510
|
+
loopWake = resolve;
|
|
511
|
+
loopTimer = setTimeout(() => {
|
|
512
|
+
loopTimer = null;
|
|
513
|
+
loopWake = null;
|
|
514
|
+
resolve();
|
|
515
|
+
}, ms);
|
|
516
|
+
});
|
|
517
|
+
const stopLoop = (): void => {
|
|
518
|
+
loopStopped = true;
|
|
519
|
+
if (loopTimer) {
|
|
520
|
+
clearTimeout(loopTimer);
|
|
521
|
+
loopTimer = null;
|
|
522
|
+
}
|
|
523
|
+
if (loopWake) {
|
|
524
|
+
const wake = loopWake;
|
|
525
|
+
loopWake = null;
|
|
526
|
+
wake();
|
|
527
|
+
}
|
|
528
|
+
};
|
|
325
529
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
530
|
+
// The node poll loop. Disabled on a workerd host: a frozen isolate cannot run
|
|
531
|
+
// a background timer, so the host calls dispatchDueJobs() from scheduled()
|
|
532
|
+
// instead (which runs redispatchDue() above — the same code path).
|
|
533
|
+
const loop = async () => {
|
|
534
|
+
if (enableScheduledJob !== true) return;
|
|
535
|
+
if (getQueueRuntimeMode() !== 'node') return;
|
|
536
|
+
while (!loopStopped) {
|
|
537
|
+
// eslint-disable-next-line no-await-in-loop
|
|
538
|
+
await cancelableSleep((minDelay() * 1000) / 2);
|
|
539
|
+
// stopped during the sleep (teardown) — exit before doing any work.
|
|
540
|
+
if (loopStopped) return;
|
|
541
|
+
// mode can flip after assembly (a host that sets workerd late); stop then.
|
|
542
|
+
if (getQueueRuntimeMode() !== 'node') return;
|
|
543
|
+
try {
|
|
544
|
+
// eslint-disable-next-line no-await-in-loop
|
|
545
|
+
await redispatchDue();
|
|
546
|
+
} catch (err) {
|
|
547
|
+
console.error(err);
|
|
548
|
+
logger.error(`Can not load scheduled ${name} jobs`, { error: err });
|
|
346
549
|
}
|
|
347
550
|
}
|
|
348
551
|
};
|
|
349
552
|
|
|
350
553
|
loop();
|
|
351
554
|
|
|
352
|
-
|
|
555
|
+
const queueInstance = Object.assign(queueEvents, {
|
|
353
556
|
store,
|
|
354
557
|
push,
|
|
355
558
|
pushAndWait,
|
|
@@ -361,6 +564,8 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
361
564
|
delete: deleteJob,
|
|
362
565
|
cancel,
|
|
363
566
|
update: updateJob,
|
|
567
|
+
/** D2 teardown: stop this queue's node poll loop (no-op if not scheduled). */
|
|
568
|
+
stop: stopLoop,
|
|
364
569
|
options: {
|
|
365
570
|
concurrency,
|
|
366
571
|
maxRetries,
|
|
@@ -369,4 +574,18 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
|
|
|
369
574
|
enableScheduledJob,
|
|
370
575
|
},
|
|
371
576
|
});
|
|
577
|
+
|
|
578
|
+
// Phase 12b: register into the host-facing queue runtime surface so the CF
|
|
579
|
+
// worker can look the handle up by name (queue() consumer) and drive due
|
|
580
|
+
// re-dispatch (scheduled()) through the service/slot boundary — no more
|
|
581
|
+
// direct cloudflare/shims/queue.ts imports.
|
|
582
|
+
registerQueue({
|
|
583
|
+
name,
|
|
584
|
+
enableScheduledJob: enableScheduledJob === true,
|
|
585
|
+
handle: queueInstance,
|
|
586
|
+
redispatchDue,
|
|
587
|
+
stop: stopLoop,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
return queueInstance;
|
|
372
591
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Phase 12b (W2′): the core queue RUNTIME surface.
|
|
2
|
+
//
|
|
3
|
+
// Decision (2026-06-12, build-phases 12b, option A): the node engine
|
|
4
|
+
// (api/src/libs/queue) is the ONE canonical queue semantics for every runtime.
|
|
5
|
+
// A host swaps only the TRIGGER (CF scheduled() vs the in-process poll loop),
|
|
6
|
+
// the EXECUTOR (fastq shim vs real fastq) and FLUSH (drain-before-response vs
|
|
7
|
+
// no-op). It NEVER forks the engine — the old cloudflare/shims/queue.ts
|
|
8
|
+
// duplicate engine is dead under the canonical build.ts (nothing populates its
|
|
9
|
+
// registry there) and is removed in 12c.
|
|
10
|
+
//
|
|
11
|
+
// This module is the seam the worker drives instead of reaching into
|
|
12
|
+
// shims/queue internals:
|
|
13
|
+
// - a queue REGISTRY (each createQueue registers its handle + due-dispatch)
|
|
14
|
+
// - dispatchDueJobs() — host-driven scheduled re-dispatch (workerd trigger =
|
|
15
|
+
// CF scheduled(); on node the per-queue loop() does the same on a timer)
|
|
16
|
+
// - flushQueueWork() — drain in-flight push/execution work before the
|
|
17
|
+
// isolate freezes (no-op on a long-lived node process)
|
|
18
|
+
//
|
|
19
|
+
// Runtime modes:
|
|
20
|
+
// 'node' — long-lived process: each scheduled queue's loop() polls D1.
|
|
21
|
+
// 'workerd' — frozen isolate: loop() is disabled; the host calls
|
|
22
|
+
// dispatchDueJobs() from scheduled() and flushQueueWork() before
|
|
23
|
+
// returning the response.
|
|
24
|
+
|
|
25
|
+
export type QueueRuntimeMode = 'node' | 'workerd';
|
|
26
|
+
|
|
27
|
+
let mode: QueueRuntimeMode = 'node';
|
|
28
|
+
|
|
29
|
+
export function setQueueRuntimeMode(next: QueueRuntimeMode): void {
|
|
30
|
+
mode = next;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getQueueRuntimeMode(): QueueRuntimeMode {
|
|
34
|
+
return mode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RegisteredQueue {
|
|
38
|
+
name: string;
|
|
39
|
+
/** whether this queue accepts delayed/scheduled jobs (gates due-dispatch) */
|
|
40
|
+
enableScheduledJob: boolean;
|
|
41
|
+
/** the public queue handle (push / pushAndWait / cancel / get / ...) */
|
|
42
|
+
handle: any;
|
|
43
|
+
/**
|
|
44
|
+
* Re-deliver this queue's due delayed rows once — the body of the node
|
|
45
|
+
* loop(). The host (CF scheduled()) calls this through dispatchDueJobs();
|
|
46
|
+
* the node loop() calls it on its own timer. Same code path either way.
|
|
47
|
+
*/
|
|
48
|
+
redispatchDue: () => Promise<{ dispatched: number; failed: number }>;
|
|
49
|
+
/**
|
|
50
|
+
* D2 teardown: stop this queue's node poll loop (clears its sleep timer). The
|
|
51
|
+
* host (arc/Node) calls stopAllQueues() on lifecycle.stop() so no poll timer
|
|
52
|
+
* survives a stop / ARC_PAYMENT toggle. No-op on workerd (no loop runs).
|
|
53
|
+
*/
|
|
54
|
+
stop?: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const registry = new Map<string, RegisteredQueue>();
|
|
58
|
+
|
|
59
|
+
export function registerQueue(entry: RegisteredQueue): void {
|
|
60
|
+
registry.set(entry.name, entry);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** the queue handle the worker's queue() consumer looks up by name (no more shim registry) */
|
|
64
|
+
export function getQueueHandler(name: string): any | undefined {
|
|
65
|
+
return registry.get(name)?.handle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getAllQueueNames(): string[] {
|
|
69
|
+
return Array.from(registry.keys());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Host-driven scheduled dispatch (the workerd trigger is CF scheduled()). Runs
|
|
74
|
+
* every registered scheduled queue's due-row re-dispatch once. On a node host
|
|
75
|
+
* the per-queue loop() does exactly this on a timer; a workerd host calls this
|
|
76
|
+
* explicitly because it cannot run a background timer in a frozen isolate.
|
|
77
|
+
*/
|
|
78
|
+
export async function dispatchDueJobs(): Promise<{ dispatched: number; failed: number; queues: string[] }> {
|
|
79
|
+
let dispatched = 0;
|
|
80
|
+
let failed = 0;
|
|
81
|
+
const queues: string[] = [];
|
|
82
|
+
for (const entry of registry.values()) {
|
|
83
|
+
// eslint-disable-next-line no-continue -- skip non-scheduled queues in the dispatch loop
|
|
84
|
+
if (!entry.enableScheduledJob) continue;
|
|
85
|
+
try {
|
|
86
|
+
// eslint-disable-next-line no-await-in-loop -- queues dispatch sequentially within a tick
|
|
87
|
+
const r = await entry.redispatchDue();
|
|
88
|
+
dispatched += r.dispatched;
|
|
89
|
+
failed += r.failed;
|
|
90
|
+
if (r.dispatched > 0 || r.failed > 0) queues.push(entry.name);
|
|
91
|
+
} catch {
|
|
92
|
+
failed += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { dispatched, failed, queues };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* D2 teardown: stop every registered queue's node poll loop. The Node host
|
|
100
|
+
* (arc) calls this from lifecycle.stop() so no background poll timer survives a
|
|
101
|
+
* stop / ARC_PAYMENT toggle. The registry entries stay (a later start() rebuilds
|
|
102
|
+
* the loops by recreating the queues). Idempotent — safe to call when stopped.
|
|
103
|
+
*/
|
|
104
|
+
export function stopAllQueues(): void {
|
|
105
|
+
for (const entry of registry.values()) {
|
|
106
|
+
try {
|
|
107
|
+
entry.stop?.();
|
|
108
|
+
} catch {
|
|
109
|
+
/* a queue already stopped — ignore */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** stop every queue loop AND clear the registry (full reset). */
|
|
115
|
+
export function disposeQueues(): void {
|
|
116
|
+
stopAllQueues();
|
|
117
|
+
registry.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- in-flight push tracking (workerd flush) ---
|
|
121
|
+
// On node the process stays alive and fastq drains naturally; nothing is
|
|
122
|
+
// tracked (zero overhead, byte-identical behavior). On workerd the isolate is
|
|
123
|
+
// torn down after the response, so the host must await in-flight executions
|
|
124
|
+
// before it returns — otherwise an immediate push is silently dropped.
|
|
125
|
+
const pending = new Set<Promise<any>>();
|
|
126
|
+
|
|
127
|
+
export function trackPending(p: Promise<any>): void {
|
|
128
|
+
if (mode !== 'workerd') return;
|
|
129
|
+
const wrapped = Promise.resolve(p).catch(() => undefined);
|
|
130
|
+
pending.add(wrapped);
|
|
131
|
+
wrapped.then(() => {
|
|
132
|
+
pending.delete(wrapped);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Drain in-flight push/execution work. The CF worker calls this before
|
|
138
|
+
* returning the HTTP response and at the end of scheduled()/queue() so no
|
|
139
|
+
* immediate push is lost when the isolate freezes. No-op on a node host.
|
|
140
|
+
*/
|
|
141
|
+
export async function flushQueueWork(): Promise<void> {
|
|
142
|
+
if (mode !== 'workerd') return;
|
|
143
|
+
const MAX_ITERATIONS = 10;
|
|
144
|
+
// Wall-clock guard: a misbehaving handler whose fastq callback never fires
|
|
145
|
+
// must not block the isolate's response forever. The D1 jobs row is the
|
|
146
|
+
// source of truth, so anything still pending past the budget is re-dispatched
|
|
147
|
+
// on the next scheduled() tick rather than lost.
|
|
148
|
+
const deadline = Date.now() + 5000;
|
|
149
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
150
|
+
if (pending.size === 0) break;
|
|
151
|
+
const remaining = deadline - Date.now();
|
|
152
|
+
if (remaining <= 0) break;
|
|
153
|
+
const batch = Array.from(pending);
|
|
154
|
+
// eslint-disable-next-line no-await-in-loop
|
|
155
|
+
await Promise.race([
|
|
156
|
+
Promise.allSettled(batch),
|
|
157
|
+
new Promise<void>((resolve) => {
|
|
158
|
+
setTimeout(resolve, remaining);
|
|
159
|
+
}),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Exported for unit tests only — not part of the host-facing surface.
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention -- test-only export sentinel
|
|
166
|
+
export const __test__ = {
|
|
167
|
+
reset() {
|
|
168
|
+
registry.clear();
|
|
169
|
+
pending.clear();
|
|
170
|
+
mode = 'node';
|
|
171
|
+
},
|
|
172
|
+
registrySize() {
|
|
173
|
+
return registry.size;
|
|
174
|
+
},
|
|
175
|
+
};
|
package/api/src/libs/resource.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { fromTokenToUnit } from '@ocap/util';
|
|
|
10
10
|
|
|
11
11
|
import { updatePassportExtra } from '../integrations/blocklet/passport';
|
|
12
12
|
import { replace } from '../locales';
|
|
13
|
-
import { createPaymentLink } from '../routes/payment-links';
|
|
14
|
-
import { createPrice } from '../routes/prices';
|
|
15
|
-
import { createProductAndPrices } from '../routes/products';
|
|
13
|
+
import { createPaymentLink } from '../routes/hono/payment-links';
|
|
14
|
+
import { createPrice } from '../routes/hono/prices';
|
|
15
|
+
import { createProductAndPrices } from '../routes/hono/products';
|
|
16
16
|
import { PaymentCurrency, PaymentLink, Price, Product, nextPriceId } from '../store/models';
|
|
17
17
|
|
|
18
18
|
export async function getPackResource(type: string) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Phase 11 (W2-3): tenant-aware secrets facade.
|
|
2
|
+
//
|
|
3
|
+
// Replaces direct `security.encrypt/decrypt` calls in the payment hot path. The
|
|
4
|
+
// tenant is resolved from the TenantContext (single point), so call sites do
|
|
5
|
+
// not change signature. single mode -> the default secrets driver (process
|
|
6
|
+
// key), unchanged; multi mode -> the keyring driver keyed per tenant.
|
|
7
|
+
//
|
|
8
|
+
// Sync surface: the payment loop (PaymentMethod.encrypt/decrypt Settings,
|
|
9
|
+
// getStripeClient, ~50 sync callers) stays synchronous. The keyring driver's
|
|
10
|
+
// sync path requires the tenant key to be warmed (the worker shell warms it at
|
|
11
|
+
// request entry); the default driver's process key is always available.
|
|
12
|
+
|
|
13
|
+
import { getInstanceDid } from './context';
|
|
14
|
+
import { getSecretsDriver } from './drivers/secrets';
|
|
15
|
+
|
|
16
|
+
/** Encrypt a value under the current tenant's key (sync hot path). */
|
|
17
|
+
export function encryptSecret(value: string): string {
|
|
18
|
+
return getSecretsDriver().encryptSync(getInstanceDid(), value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Decrypt a value under the current tenant's key (sync hot path). */
|
|
22
|
+
export function decryptSecret(value: string): string {
|
|
23
|
+
return getSecretsDriver().decryptSync(getInstanceDid(), value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Async per-tenant variants (lazy EK fetch + retry) for non-hot-path callers. */
|
|
27
|
+
export function encryptSecretAsync(value: string): Promise<string> {
|
|
28
|
+
return getSecretsDriver().encrypt(getInstanceDid(), value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function decryptSecretAsync(value: string): Promise<string> {
|
|
32
|
+
return getSecretsDriver().decrypt(getInstanceDid(), value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pre-resolve the current tenant's key so the sync path is a cache hit. */
|
|
36
|
+
export function warmupSecrets(instanceDid?: string): Promise<void> {
|
|
37
|
+
return getSecretsDriver().warmup(instanceDid ?? getInstanceDid());
|
|
38
|
+
}
|
package/api/src/libs/session.ts
CHANGED
|
@@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|
|
6
6
|
import isEqual from 'lodash/isEqual';
|
|
7
7
|
import pAll from 'p-all';
|
|
8
8
|
import omit from 'lodash/omit';
|
|
9
|
+
import { paymentBillingThreshold, paymentMinStakeAmount } from './env';
|
|
9
10
|
import dayjs from './dayjs';
|
|
10
11
|
import { validCoupon } from './discount/coupon';
|
|
11
12
|
import { getPriceUintAmountByCurrency, getPriceCurrencyOptions } from './price';
|
|
@@ -425,7 +426,7 @@ export function getBillingThreshold(config: Record<string, any> = {}) {
|
|
|
425
426
|
}
|
|
426
427
|
}
|
|
427
428
|
|
|
428
|
-
const threshold =
|
|
429
|
+
const threshold = paymentBillingThreshold();
|
|
429
430
|
if (threshold > 0) {
|
|
430
431
|
return threshold;
|
|
431
432
|
}
|
|
@@ -441,7 +442,7 @@ export function getMinStakeAmount(config: Record<string, any> = {}) {
|
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
444
|
|
|
444
|
-
const threshold =
|
|
445
|
+
const threshold = paymentMinStakeAmount();
|
|
445
446
|
if (threshold > 0) {
|
|
446
447
|
return threshold;
|
|
447
448
|
}
|