payment-kit 1.29.0 → 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.
- 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 +36 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +27 -24
- 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/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 +99 -38
- package/api/src/libs/context.ts +78 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- 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 +81 -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 +50 -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 +259 -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 +73 -0
- package/api/src/middlewares/hono/csrf.ts +72 -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 +214 -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 +17 -12
- 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 +12 -4
- 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 +41 -28
- package/api/src/queues/payout.ts +9 -5
- package/api/src/queues/refund.ts +18 -12
- 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} +193 -223
- 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} +96 -80
- 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 +667 -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 +66 -22
- 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/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/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/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 +236 -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/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/README.md +26 -6
- package/cloudflare/build.ts +28 -13
- package/cloudflare/did-connect-auth.ts +0 -217
- package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -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 +31 -56
- 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/util-csrf.ts +13 -0
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
- 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/node-fetch.ts +35 -0
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
- package/cloudflare/worker.ts +204 -433
- package/cloudflare/wrangler.local-e2e.jsonc +26 -0
- package/jest.config.js +3 -1
- package/package.json +33 -38
- 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/env.d.ts +13 -1
- package/tsconfig.json +1 -1
- 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/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
package/api/src/libs/context.ts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
TENANT_CONTEXT_MISSING,
|
|
5
|
+
TenantError,
|
|
6
|
+
assertValidInstanceDid,
|
|
7
|
+
getDefaultInstanceDid,
|
|
8
|
+
getTenantMode,
|
|
9
|
+
} from './tenant';
|
|
10
|
+
|
|
11
|
+
export * from './tenant';
|
|
12
|
+
|
|
3
13
|
interface RequestContext {
|
|
4
14
|
requestedBy?: string;
|
|
5
15
|
requestId?: string;
|
|
16
|
+
instanceDid?: string;
|
|
6
17
|
}
|
|
7
18
|
|
|
8
19
|
class RequestContextManager {
|
|
9
20
|
private storage = new AsyncLocalStorage<RequestContext>();
|
|
10
21
|
private contexts = new Map<string, RequestContext>();
|
|
22
|
+
// System-operation flag: when set, TenantModel bypasses tenant scoping so a
|
|
23
|
+
// legitimate cross-tenant read (queue dispatch, IAP bundle->tenant reverse
|
|
24
|
+
// lookup, event fan-out) can load rows across tenants. Entered ONLY via the
|
|
25
|
+
// system* helpers — a normal route context can never read across tenants.
|
|
26
|
+
private systemStorage = new AsyncLocalStorage<boolean>();
|
|
11
27
|
|
|
12
28
|
getContext(requestId?: string): RequestContext {
|
|
13
29
|
if (requestId && this.contexts.has(requestId)) {
|
|
@@ -20,16 +36,62 @@ class RequestContextManager {
|
|
|
20
36
|
return this.getContext(requestId).requestedBy;
|
|
21
37
|
}
|
|
22
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Run fn with the given tenant. Nested calls shadow the outer tenant and restore it on exit.
|
|
41
|
+
* Other context fields (requestId, requestedBy) are inherited from the enclosing scope.
|
|
42
|
+
*/
|
|
43
|
+
withTenant<T>(instanceDid: string, fn: () => Promise<T> | T): Promise<T> {
|
|
44
|
+
try {
|
|
45
|
+
assertValidInstanceDid(instanceDid);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return Promise.reject(err);
|
|
48
|
+
}
|
|
49
|
+
const parent = this.storage.getStore();
|
|
50
|
+
// frozen so fn cannot mutate the stored context and affect sibling calls
|
|
51
|
+
const next = Object.freeze({ ...parent, instanceDid });
|
|
52
|
+
return this.storage.run(next, () => Promise.resolve(fn()));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Current tenant. Single mode falls back to the deployment app DID;
|
|
57
|
+
* multi mode fails closed with TENANT_CONTEXT_MISSING.
|
|
58
|
+
*/
|
|
59
|
+
getInstanceDid(): string {
|
|
60
|
+
const instanceDid = this.storage.getStore()?.instanceDid;
|
|
61
|
+
if (instanceDid) return instanceDid;
|
|
62
|
+
if (getTenantMode() === 'single') return getDefaultInstanceDid();
|
|
63
|
+
throw new TenantError(TENANT_CONTEXT_MISSING, 'tenant context is missing in multi-tenant mode');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run fn as a system operation: TenantModel scoping is bypassed for the span
|
|
68
|
+
* of fn so legitimate cross-tenant reads can load rows regardless of tenant.
|
|
69
|
+
* The scope ends when fn settles — callers must enforce the row's tenant
|
|
70
|
+
* themselves (e.g. assertJobObjectTenant). Explicit by construction: only the
|
|
71
|
+
* system* helpers enter this, so a normal route can never read across tenants.
|
|
72
|
+
*/
|
|
73
|
+
runAsSystem<T>(fn: () => Promise<T> | T): Promise<T> {
|
|
74
|
+
return this.systemStorage.run(true, () => Promise.resolve(fn()));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** True inside a runAsSystem span — TenantModel checks this to skip scoping. */
|
|
78
|
+
isSystem(): boolean {
|
|
79
|
+
return this.systemStorage.getStore() === true;
|
|
80
|
+
}
|
|
81
|
+
|
|
23
82
|
run<T>(context: RequestContext, fn: () => Promise<T> | T): Promise<T> {
|
|
24
83
|
const requestId = context.requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
84
|
+
// inherit tenant from the enclosing scope unless explicitly provided
|
|
85
|
+
const instanceDid = context.instanceDid ?? this.storage.getStore()?.instanceDid;
|
|
25
86
|
|
|
26
87
|
this.contexts.set(requestId, {
|
|
27
88
|
...context,
|
|
89
|
+
instanceDid,
|
|
28
90
|
requestId,
|
|
29
91
|
});
|
|
30
92
|
|
|
31
93
|
return new Promise((resolve, reject) => {
|
|
32
|
-
this.storage.run({ ...context, requestId }, async () => {
|
|
94
|
+
this.storage.run({ ...context, instanceDid, requestId }, async () => {
|
|
33
95
|
const resource = new AsyncResource('RequestContext');
|
|
34
96
|
try {
|
|
35
97
|
const result = await resource.runInAsyncScope(fn);
|
|
@@ -46,3 +108,18 @@ class RequestContextManager {
|
|
|
46
108
|
}
|
|
47
109
|
|
|
48
110
|
export const context = new RequestContextManager();
|
|
111
|
+
|
|
112
|
+
/** Run fn with the given tenant — also the test injection helper for jest. */
|
|
113
|
+
export function withTenant<T>(instanceDid: string, fn: () => Promise<T> | T): Promise<T> {
|
|
114
|
+
return context.withTenant(instanceDid, fn);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Current tenant; see RequestContextManager#getInstanceDid for mode semantics. */
|
|
118
|
+
export function getInstanceDid(): string {
|
|
119
|
+
return context.getInstanceDid();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** True inside a runAsSystem span; TenantModel uses it to bypass scoping. */
|
|
123
|
+
export function isSystemContext(): boolean {
|
|
124
|
+
return context.isSystem();
|
|
125
|
+
}
|
package/api/src/libs/currency.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
2
|
-
import { getUrl } from '@blocklet/sdk';
|
|
2
|
+
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
3
3
|
import { PaymentCurrency, Price, Product, RechargeConfig } from '../store/models';
|
|
4
4
|
import { trimDecimals } from './math-utils';
|
|
5
|
-
import { createPaymentLink } from '../routes/payment-links';
|
|
5
|
+
import { createPaymentLink } from '../routes/hono/payment-links';
|
|
6
6
|
import logger from './logger';
|
|
7
7
|
|
|
8
8
|
export async function formatCurrencyToken(amount: string, currencyId: string) {
|
package/api/src/libs/dayjs.ts
CHANGED
|
@@ -5,8 +5,14 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
|
|
5
5
|
import timezone from 'dayjs/plugin/timezone'; // dependent on utc plugin
|
|
6
6
|
import utc from 'dayjs/plugin/utc';
|
|
7
7
|
|
|
8
|
-
import(
|
|
9
|
-
|
|
8
|
+
// Use explicit `.js` so the dynamic import resolves under Node ESM (strict
|
|
9
|
+
// resolution: dayjs ships no `exports` map, so the extensionless subpath
|
|
10
|
+
// `dayjs/locale/en` fails when payment-core is embedded in a Node ESM host
|
|
11
|
+
// like arc). Webpack/blocklet-server tolerate the extension too.
|
|
12
|
+
// eslint-disable-next-line import/extensions -- explicit .js required for Node ESM hosts (arc)
|
|
13
|
+
import('dayjs/locale/en.js');
|
|
14
|
+
// eslint-disable-next-line import/extensions -- explicit .js required for Node ESM hosts (arc)
|
|
15
|
+
import('dayjs/locale/zh.js');
|
|
10
16
|
|
|
11
17
|
dayjs.extend(relativeTime);
|
|
12
18
|
dayjs.extend(localizedFormat);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Phase 8 (W2-1a): DID Connect token storage on the db driver.
|
|
2
|
+
//
|
|
3
|
+
// In Blocklet Server the real @arcblock/did-connect-storage-nedb (file-backed)
|
|
4
|
+
// is used unchanged. In the CF worker that package was aliased to a NO-OP stub
|
|
5
|
+
// (cloudflare/shims/nedb-storage.ts) — DID Connect state was silently dropped.
|
|
6
|
+
// This is the from-scratch persistent implementation on top of the db driver
|
|
7
|
+
// contract, so the same code runs on the node (sqlite) and d1 backends and is
|
|
8
|
+
// covered by the driver consistency suite.
|
|
9
|
+
//
|
|
10
|
+
// Records are flexible JSON documents keyed by `token` (the DID-Auth session
|
|
11
|
+
// token, globally unique — no instance_did column needed; isolation is by the
|
|
12
|
+
// unguessable token, matching the original file store's scope).
|
|
13
|
+
|
|
14
|
+
import type { DbDriver } from './db';
|
|
15
|
+
|
|
16
|
+
const { EventEmitter } = require('events');
|
|
17
|
+
|
|
18
|
+
const TABLE = 'did_auth_records';
|
|
19
|
+
|
|
20
|
+
export interface AuthRecord {
|
|
21
|
+
token: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DbAuthStorage extends EventEmitter {
|
|
26
|
+
private driver: DbDriver;
|
|
27
|
+
private ready: Promise<void> | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(driver: DbDriver) {
|
|
30
|
+
super();
|
|
31
|
+
if (!driver) throw new Error('DbAuthStorage requires a db driver');
|
|
32
|
+
this.driver = driver;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// idempotent lazy schema — the original file store self-managed its db file
|
|
36
|
+
// (it was never part of the Umzug/D1 migration chain), so we keep that here.
|
|
37
|
+
private ensureTable(): Promise<void> {
|
|
38
|
+
if (!this.ready) {
|
|
39
|
+
this.ready = this.driver
|
|
40
|
+
.exec(
|
|
41
|
+
`CREATE TABLE IF NOT EXISTS ${TABLE} (` +
|
|
42
|
+
'token TEXT PRIMARY KEY, ' +
|
|
43
|
+
'doc TEXT NOT NULL, ' +
|
|
44
|
+
'created_at INTEGER NOT NULL, ' +
|
|
45
|
+
'updated_at INTEGER NOT NULL)'
|
|
46
|
+
)
|
|
47
|
+
.then(() => undefined)
|
|
48
|
+
.catch((err) => {
|
|
49
|
+
// reset so a transient failure can be retried on next call
|
|
50
|
+
this.ready = null;
|
|
51
|
+
throw err;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return this.ready;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async read(token: string): Promise<AuthRecord | null> {
|
|
58
|
+
if (!token) throw new Error('token is required to read auth record');
|
|
59
|
+
await this.ensureTable();
|
|
60
|
+
const row = await this.driver.get<{ doc: string }>(`SELECT doc FROM ${TABLE} WHERE token = ?`, [token]);
|
|
61
|
+
if (!row) return null;
|
|
62
|
+
return JSON.parse(row.doc) as AuthRecord;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async create(token: string, status = 'created'): Promise<AuthRecord> {
|
|
66
|
+
if (!token) throw new Error('token is required to create auth record');
|
|
67
|
+
await this.ensureTable();
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const doc: AuthRecord = { token, status, createdAt: now, updatedAt: now };
|
|
70
|
+
await this.driver.exec(`INSERT INTO ${TABLE} (token, doc, created_at, updated_at) VALUES (?, ?, ?, ?)`, [
|
|
71
|
+
token,
|
|
72
|
+
JSON.stringify(doc),
|
|
73
|
+
now,
|
|
74
|
+
now,
|
|
75
|
+
]);
|
|
76
|
+
this.emit('create', doc);
|
|
77
|
+
return doc;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async update(token: string, updates: Record<string, any> = {}): Promise<AuthRecord | null> {
|
|
81
|
+
if (!token) throw new Error('token is required to update auth record');
|
|
82
|
+
await this.ensureTable();
|
|
83
|
+
const current = await this.read(token);
|
|
84
|
+
if (!current) return null;
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const merged: AuthRecord = { ...current, ...updates, token, updatedAt: now };
|
|
87
|
+
await this.driver.exec(`UPDATE ${TABLE} SET doc = ?, updated_at = ? WHERE token = ?`, [
|
|
88
|
+
JSON.stringify(merged),
|
|
89
|
+
now,
|
|
90
|
+
token,
|
|
91
|
+
]);
|
|
92
|
+
this.emit('update', merged);
|
|
93
|
+
return merged;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async delete(token: string): Promise<number> {
|
|
97
|
+
if (!token) throw new Error('token is required to delete auth record');
|
|
98
|
+
await this.ensureTable();
|
|
99
|
+
const res = await this.driver.exec(`DELETE FROM ${TABLE} WHERE token = ?`, [token]);
|
|
100
|
+
this.emit('destroy', token);
|
|
101
|
+
return res.changes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async exist(token: string, did: any): Promise<boolean> {
|
|
105
|
+
if (!token) throw new Error('token is required to check auth record');
|
|
106
|
+
const doc = await this.read(token);
|
|
107
|
+
return !!doc && doc.did === did;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async clear(): Promise<void> {
|
|
111
|
+
await this.ensureTable();
|
|
112
|
+
await this.driver.exec(`DELETE FROM ${TABLE}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createAuthStorage(driver: DbDriver): DbAuthStorage {
|
|
117
|
+
return new DbAuthStorage(driver);
|
|
118
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Phase 9 (W2-1b): cron slot driver contract.
|
|
2
|
+
//
|
|
3
|
+
// The cron contract is: register jobs + a due-poll dispatch entry. The HOST
|
|
4
|
+
// provides the trigger — CF `scheduled()` calls runDue() every minute; the Node
|
|
5
|
+
// host uses @abtnode/cron's own scheduler. Both share ONE cron-expression
|
|
6
|
+
// matcher (previously duplicated in cloudflare/shims/cron.ts), so embedded and
|
|
7
|
+
// worker agree on exactly when a job is due.
|
|
8
|
+
|
|
9
|
+
export interface CronJob {
|
|
10
|
+
name: string;
|
|
11
|
+
time: string;
|
|
12
|
+
fn: () => Promise<any> | any;
|
|
13
|
+
/**
|
|
14
|
+
* runOnInit: honored by the node host scheduler (@abtnode/cron) which runs
|
|
15
|
+
* the job once at registration. The shared registry's runDue (used by the
|
|
16
|
+
* cf-cron host) does NOT auto-run it — CF deliberately skips runOnInit and
|
|
17
|
+
* lets the next matching trigger fire it (running at module-init would block
|
|
18
|
+
* the request / risk CPU limits). It is a host-trigger concern, not a matcher
|
|
19
|
+
* concern.
|
|
20
|
+
*/
|
|
21
|
+
options?: { runOnInit?: boolean };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CronDriver {
|
|
25
|
+
kind: 'node-cron' | 'cf-cron';
|
|
26
|
+
/** register jobs (idempotent: clears + re-adds) */
|
|
27
|
+
register(jobs: CronJob[], onError?: (err: Error, name: string) => void): void;
|
|
28
|
+
/** add a single job after init */
|
|
29
|
+
addJob(name: string, time: string, fn: CronJob['fn'], options?: CronJob['options']): void;
|
|
30
|
+
/** due-poll dispatch entry — runs every job whose schedule matches `now` */
|
|
31
|
+
runDue(now?: Date): Promise<{ ran: string[]; skipped: string[] }>;
|
|
32
|
+
/** run one named job regardless of schedule (manual trigger) */
|
|
33
|
+
runJob(name: string): Promise<void>;
|
|
34
|
+
/** registered job descriptions (name + schedule) */
|
|
35
|
+
getJobNames(): string[];
|
|
36
|
+
/**
|
|
37
|
+
* D2 teardown surface. Stop all live timers (the node-cron driver tears down
|
|
38
|
+
* its @abtnode/cron scheduler so the process has no dangling cron timer); the
|
|
39
|
+
* registry is preserved so a later start() can re-register. No-op for the
|
|
40
|
+
* passive cf-cron driver (it never owns a timer — the host drives runDue).
|
|
41
|
+
*/
|
|
42
|
+
stop(): void;
|
|
43
|
+
/** stop + clear the registry (a full reset; start() re-registers from scratch). */
|
|
44
|
+
dispose(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// @abtnode/cron's CronScheduler shape we drive (lib/scheduler.js): addJob wires
|
|
48
|
+
// a self-scheduling `cron` CronJob (start=true) and exposes the live jobs map.
|
|
49
|
+
interface AbtnodeCronScheduler {
|
|
50
|
+
jobs: Record<string, { start(): void; stop(): void }>;
|
|
51
|
+
addJob(name: string, time: string, fn: (...args: any[]) => any, options?: Record<string, any>): unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- shared cron-expression matcher ---
|
|
55
|
+
// 6-field: second minute hour dayOfMonth month dayOfWeek (seconds ignored —
|
|
56
|
+
// CF triggers are minute-level). Supports numbers, *, */N, ranges, lists.
|
|
57
|
+
|
|
58
|
+
function parseField(field: string, min: number, max: number): number[] | null {
|
|
59
|
+
// null means "match all"
|
|
60
|
+
if (field === '*') return null;
|
|
61
|
+
|
|
62
|
+
const values = new Set<number>();
|
|
63
|
+
|
|
64
|
+
for (const part of field.split(',')) {
|
|
65
|
+
const stepMatch = part.match(/^\*\/(\d+)$/);
|
|
66
|
+
const rangeMatch = part.match(/^(\d+)-(\d+)$/);
|
|
67
|
+
if (stepMatch) {
|
|
68
|
+
const step = parseInt(stepMatch[1]!, 10);
|
|
69
|
+
for (let i = min; i <= max; i += step) values.add(i);
|
|
70
|
+
} else if (rangeMatch) {
|
|
71
|
+
const from = parseInt(rangeMatch[1]!, 10);
|
|
72
|
+
const to = parseInt(rangeMatch[2]!, 10);
|
|
73
|
+
for (let i = from; i <= to; i += 1) values.add(i);
|
|
74
|
+
} else {
|
|
75
|
+
const num = parseInt(part, 10);
|
|
76
|
+
if (!Number.isNaN(num)) values.add(num);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return values.size > 0 ? Array.from(values) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether `date` matches a 5- or 6-field cron expression (minute granularity).
|
|
85
|
+
*/
|
|
86
|
+
export function matchesCron(cronExpr: string, date: Date): boolean {
|
|
87
|
+
const fields = cronExpr.trim().split(/\s+/);
|
|
88
|
+
if (fields.length < 5) return true; // can't parse — run it
|
|
89
|
+
|
|
90
|
+
const offset = fields.length >= 6 ? 1 : 0;
|
|
91
|
+
// length >= 5 guaranteed above, so offset..offset+4 are present
|
|
92
|
+
const minuteField = parseField(fields[offset]!, 0, 59);
|
|
93
|
+
const hourField = parseField(fields[offset + 1]!, 0, 23);
|
|
94
|
+
const domField = parseField(fields[offset + 2]!, 1, 31);
|
|
95
|
+
// cron months are 1-12 (matched against getUTCMonth()+1); a 0-11 range here
|
|
96
|
+
// made step patterns like */3 generate {0,3,6,9} and never match real months.
|
|
97
|
+
const monthField = parseField(fields[offset + 3]!, 1, 12);
|
|
98
|
+
const dowField = parseField(fields[offset + 4]!, 0, 6);
|
|
99
|
+
|
|
100
|
+
const m = date.getUTCMinutes();
|
|
101
|
+
const h = date.getUTCHours();
|
|
102
|
+
const dom = date.getUTCDate();
|
|
103
|
+
const month = date.getUTCMonth() + 1; // JS 0-based → cron 1-based
|
|
104
|
+
const dow = date.getUTCDay();
|
|
105
|
+
|
|
106
|
+
if (minuteField && !minuteField.includes(m)) return false;
|
|
107
|
+
if (hourField && !hourField.includes(h)) return false;
|
|
108
|
+
if (domField && !domField.includes(dom)) return false;
|
|
109
|
+
if (monthField && !monthField.includes(month)) return false;
|
|
110
|
+
if (dowField && !dowField.includes(dow)) return false;
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether a cron expression should fire at `date`. Minute-level match — the
|
|
117
|
+
* trigger source (CF scheduled / node cron) fires every minute, so each
|
|
118
|
+
* expression triggers at its designed frequency (see the 2026-04-17 incident
|
|
119
|
+
* note in the prior cloudflare/shims/cron.ts history).
|
|
120
|
+
*/
|
|
121
|
+
export function shouldRunInWindow(cronExpr: string, date: Date): boolean {
|
|
122
|
+
return matchesCron(cronExpr, date);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A cron registry implementing the due-poll dispatch entry on top of the shared
|
|
127
|
+
* matcher. Both the node-cron and cf-cron drivers are built from this — the
|
|
128
|
+
* only difference is who calls runDue (node scheduler vs CF scheduled()).
|
|
129
|
+
*/
|
|
130
|
+
export function createCronRegistry(kind: CronDriver['kind']): CronDriver {
|
|
131
|
+
const jobs: CronJob[] = [];
|
|
132
|
+
let onErrorHandler: ((err: Error, name: string) => void) | undefined;
|
|
133
|
+
|
|
134
|
+
// node-cron self-schedules through @abtnode/cron; cf-cron stays a passive
|
|
135
|
+
// matcher registry whose runDue() is driven by the host's scheduled().
|
|
136
|
+
const selfSchedule = kind === 'node-cron';
|
|
137
|
+
let scheduler: AbtnodeCronScheduler | null = null;
|
|
138
|
+
|
|
139
|
+
// Tear down every live @abtnode/cron timer (each scheduler.jobs[*] is a `cron`
|
|
140
|
+
// CronJob with its own setTimeout). Clears the scheduler so the process has no
|
|
141
|
+
// dangling cron handle and a later start() rebuilds from scratch.
|
|
142
|
+
const stopScheduler = (): void => {
|
|
143
|
+
if (!scheduler) return;
|
|
144
|
+
for (const job of Object.values(scheduler.jobs)) {
|
|
145
|
+
try {
|
|
146
|
+
job.stop();
|
|
147
|
+
} catch {
|
|
148
|
+
/* a job already stopped — ignore */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
scheduler = null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// (Re)build the live scheduler from the current jobs array. Always stops the
|
|
155
|
+
// previous scheduler first so register()/start() is idempotent: no double
|
|
156
|
+
// timers, no double registration. Lazy-requires @abtnode/cron so the cf-cron
|
|
157
|
+
// path and pure imports never load the Node scheduler.
|
|
158
|
+
const startScheduler = (): void => {
|
|
159
|
+
stopScheduler();
|
|
160
|
+
if (!selfSchedule || jobs.length === 0) return;
|
|
161
|
+
// eslint-disable-next-line global-require
|
|
162
|
+
const mod = require('@abtnode/cron');
|
|
163
|
+
// real package (CJS): module.exports = { init }; the CF shim (ESM default)
|
|
164
|
+
// resolves to { default: { init } } under the bundler interop.
|
|
165
|
+
type AbtnodeInit = (params: {
|
|
166
|
+
context: any;
|
|
167
|
+
jobs: any[];
|
|
168
|
+
onError: (e: Error, n: string) => void;
|
|
169
|
+
}) => AbtnodeCronScheduler;
|
|
170
|
+
const init: AbtnodeInit = mod.init ?? mod.default?.init;
|
|
171
|
+
// D2/multi: skip runOnInit in multi-tenant mode — same as the CF host
|
|
172
|
+
// (cloudflare/shims/cron.ts). A runOnInit job fires immediately at register,
|
|
173
|
+
// before any request, so it has NO tenant context; in multi mode that makes
|
|
174
|
+
// a tenant-requiring startup push (e.g. deposit.vault / credit.consumption)
|
|
175
|
+
// throw TENANT_CONTEXT_MISSING and abort lifecycle.start(). The job still
|
|
176
|
+
// runs on its next scheduled tick (where the handler resolves its own
|
|
177
|
+
// tenant). Single mode keeps runOnInit (the default tenant is always present).
|
|
178
|
+
// eslint-disable-next-line global-require
|
|
179
|
+
const { getTenantMode } = require('../tenant');
|
|
180
|
+
const isMulti = getTenantMode() === 'multi';
|
|
181
|
+
scheduler = init({
|
|
182
|
+
context: {},
|
|
183
|
+
jobs: jobs.map((j) => ({
|
|
184
|
+
name: j.name,
|
|
185
|
+
time: j.time,
|
|
186
|
+
fn: j.fn,
|
|
187
|
+
options: isMulti ? { ...(j.options || {}), runOnInit: false } : j.options || {},
|
|
188
|
+
})),
|
|
189
|
+
onError: onErrorHandler ?? (() => {}),
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
kind,
|
|
195
|
+
register(next, onError) {
|
|
196
|
+
jobs.length = 0;
|
|
197
|
+
onErrorHandler = onError;
|
|
198
|
+
for (const job of next || []) {
|
|
199
|
+
if (job.name && job.time && typeof job.fn === 'function') jobs.push(job);
|
|
200
|
+
}
|
|
201
|
+
startScheduler();
|
|
202
|
+
},
|
|
203
|
+
addJob(name, time, fn, options) {
|
|
204
|
+
jobs.push({ name, time, fn, options });
|
|
205
|
+
// keep the live scheduler in sync when one is already running
|
|
206
|
+
if (selfSchedule && scheduler) {
|
|
207
|
+
try {
|
|
208
|
+
scheduler.addJob(name, time, fn, options || {});
|
|
209
|
+
} catch (err: any) {
|
|
210
|
+
onErrorHandler?.(err, name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
stop() {
|
|
215
|
+
stopScheduler();
|
|
216
|
+
},
|
|
217
|
+
dispose() {
|
|
218
|
+
stopScheduler();
|
|
219
|
+
jobs.length = 0;
|
|
220
|
+
},
|
|
221
|
+
async runDue(now = new Date()) {
|
|
222
|
+
const ran: string[] = [];
|
|
223
|
+
const skipped: string[] = [];
|
|
224
|
+
for (const job of jobs) {
|
|
225
|
+
if (shouldRunInWindow(job.time, now)) {
|
|
226
|
+
ran.push(job.name);
|
|
227
|
+
try {
|
|
228
|
+
// eslint-disable-next-line no-await-in-loop -- jobs run sequentially within a tick
|
|
229
|
+
await job.fn();
|
|
230
|
+
} catch (err: any) {
|
|
231
|
+
onErrorHandler?.(err, job.name);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
skipped.push(job.name);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { ran, skipped };
|
|
238
|
+
},
|
|
239
|
+
async runJob(name) {
|
|
240
|
+
const job = jobs.find((j) => j.name === name);
|
|
241
|
+
if (!job) return;
|
|
242
|
+
try {
|
|
243
|
+
await job.fn();
|
|
244
|
+
} catch (err: any) {
|
|
245
|
+
onErrorHandler?.(err, name);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
getJobNames() {
|
|
249
|
+
return jobs.map((j) => `${j.name} (${j.time})`);
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Active cron driver — injectable by the factory's `cron` slot; defaults to a
|
|
255
|
+
// node-cron registry. The worker shell injects the cf-cron driver.
|
|
256
|
+
let activeCronDriver: CronDriver = createCronRegistry('node-cron');
|
|
257
|
+
|
|
258
|
+
export function setCronDriver(driver: CronDriver): void {
|
|
259
|
+
activeCronDriver = driver;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function getCronDriver(): CronDriver {
|
|
263
|
+
return activeCronDriver;
|
|
264
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Phase 8 (W2-1a): db slot driver contract.
|
|
2
|
+
//
|
|
3
|
+
// The db slot carries the SQL layer. Two implementations conform to the same
|
|
4
|
+
// contract:
|
|
5
|
+
// - node driver: wraps the existing Sequelize instance (raw helpers go
|
|
6
|
+
// through `sequelize.query`); `sequelize` is exposed so the factory can
|
|
7
|
+
// bind models to it (unchanged from Phase 7).
|
|
8
|
+
// - d1 driver: wraps a Cloudflare D1 binding (`prepare().bind().run()/all()/
|
|
9
|
+
// first()`), the worker-side implementation that `cloudflare/shims/
|
|
10
|
+
// sequelize-d1` is the model layer for. This phase formalizes the
|
|
11
|
+
// D1Binding contract beneath that model layer; physically relocating the
|
|
12
|
+
// sequelize-d1 files out of cloudflare/shims is part of the Phase 12 §3.1
|
|
13
|
+
// shim cleanup (the small, build-orphan shims — lock.ts, nedb-storage — are
|
|
14
|
+
// handled here since they carried no build-alias risk).
|
|
15
|
+
//
|
|
16
|
+
// The minimal raw surface (`exec/all/get`) is what tenant-agnostic
|
|
17
|
+
// infrastructure (e.g. the did-connect AuthStorage) builds on so a single
|
|
18
|
+
// implementation runs identically on both backends — see auth-storage.ts and
|
|
19
|
+
// the driver consistency suite.
|
|
20
|
+
|
|
21
|
+
export type DbDriverKind = 'node' | 'd1';
|
|
22
|
+
|
|
23
|
+
export interface DbExecResult {
|
|
24
|
+
/** rows affected by an INSERT/UPDATE/DELETE */
|
|
25
|
+
changes: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** a single statement in a batch (the shared transactional primitive) */
|
|
29
|
+
export interface DbBatchOp {
|
|
30
|
+
sql: string;
|
|
31
|
+
params?: any[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DbDriver {
|
|
35
|
+
kind: DbDriverKind;
|
|
36
|
+
/** run a write statement; returns affected row count */
|
|
37
|
+
exec(sql: string, params?: any[]): Promise<DbExecResult>;
|
|
38
|
+
/** run a SELECT; returns all rows */
|
|
39
|
+
all<T = any>(sql: string, params?: any[]): Promise<T[]>;
|
|
40
|
+
/** run a SELECT; returns the first row or null */
|
|
41
|
+
get<T = any>(sql: string, params?: any[]): Promise<T | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Run statements atomically. This is the contract's transactional primitive:
|
|
44
|
+
* D1 offers no interactive transactions, only batch-level atomicity, so the
|
|
45
|
+
* shared surface is "all-or-nothing batch" rather than an interactive
|
|
46
|
+
* transaction(fn). Any statement failing rolls the whole batch back. The node
|
|
47
|
+
* driver wraps a real Sequelize transaction; the d1 driver uses binding.batch.
|
|
48
|
+
*/
|
|
49
|
+
batch(ops: DbBatchOp[]): Promise<any[]>;
|
|
50
|
+
/**
|
|
51
|
+
* The ORM instance, when the backend is Sequelize-based. Present on the node
|
|
52
|
+
* driver (model binding target); absent on the raw d1 binding driver.
|
|
53
|
+
*/
|
|
54
|
+
sequelize?: any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** D1 binding shape (subset actually used). */
|
|
58
|
+
export interface D1Binding {
|
|
59
|
+
prepare(sql: string): {
|
|
60
|
+
bind(...params: any[]): {
|
|
61
|
+
run(): Promise<{ success?: boolean; meta?: { changes?: number; rows_written?: number } }>;
|
|
62
|
+
all(): Promise<{ results?: any[] }>;
|
|
63
|
+
first(): Promise<any | null>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
batch(stmts: any[]): Promise<any[]>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Node driver — wraps an existing Sequelize instance. Raw helpers route through
|
|
71
|
+
* `sequelize.query` with bind replacements (`$1, $2, ...` style); no string
|
|
72
|
+
* interpolation of values, matching the d1 driver's parameter binding.
|
|
73
|
+
*/
|
|
74
|
+
export function createNodeDbDriver(sequelize: any): DbDriver {
|
|
75
|
+
if (!sequelize) throw new Error('createNodeDbDriver: sequelize instance is required');
|
|
76
|
+
// Sequelize positional bind uses `$1`-style markers; callers pass `?` which we
|
|
77
|
+
// translate so the same SQL string works on both drivers.
|
|
78
|
+
const toBind = (sql: string) => {
|
|
79
|
+
let i = 0;
|
|
80
|
+
return sql.replace(/\?/g, () => {
|
|
81
|
+
i += 1;
|
|
82
|
+
return `$${i}`;
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
kind: 'node',
|
|
87
|
+
sequelize,
|
|
88
|
+
async exec(sql, params = []) {
|
|
89
|
+
const [, meta] = await sequelize.query(toBind(sql), { bind: params });
|
|
90
|
+
// sqlite returns affected rows on meta.changes; fall back to 0
|
|
91
|
+
const changes = (meta && (meta.changes ?? meta.rowCount)) ?? 0;
|
|
92
|
+
return { changes };
|
|
93
|
+
},
|
|
94
|
+
async all(sql, params = []) {
|
|
95
|
+
const rows = await sequelize.query(toBind(sql), {
|
|
96
|
+
bind: params,
|
|
97
|
+
type: sequelize.QueryTypes ? sequelize.QueryTypes.SELECT : 'SELECT',
|
|
98
|
+
});
|
|
99
|
+
return rows as any[];
|
|
100
|
+
},
|
|
101
|
+
async get(sql, params = []) {
|
|
102
|
+
const rows = await sequelize.query(toBind(sql), {
|
|
103
|
+
bind: params,
|
|
104
|
+
type: sequelize.QueryTypes ? sequelize.QueryTypes.SELECT : 'SELECT',
|
|
105
|
+
});
|
|
106
|
+
return (rows as any[])[0] ?? null;
|
|
107
|
+
},
|
|
108
|
+
// eslint-disable-next-line require-await -- async contract; returns the transaction promise
|
|
109
|
+
async batch(ops) {
|
|
110
|
+
// real Sequelize transaction — any statement throwing rolls everything back
|
|
111
|
+
return sequelize.transaction(async (t: any) => {
|
|
112
|
+
const results: any[] = [];
|
|
113
|
+
for (const op of ops) {
|
|
114
|
+
// eslint-disable-next-line no-await-in-loop -- ordered within one transaction
|
|
115
|
+
const rows = await sequelize.query(toBind(op.sql), { bind: op.params ?? [], transaction: t });
|
|
116
|
+
results.push(rows);
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* D1 driver — wraps a Cloudflare D1 binding. This is the worker-side db driver;
|
|
126
|
+
* the sequelize-d1 model layer is built on the same binding. Accepts the
|
|
127
|
+
* binding directly or a lazy getter (the worker resolves the binding per
|
|
128
|
+
* request, so it may be absent at construction time).
|
|
129
|
+
*/
|
|
130
|
+
export function createD1DbDriver(binding: D1Binding | (() => D1Binding)): DbDriver {
|
|
131
|
+
if (!binding) throw new Error('createD1DbDriver: D1 binding (or getter) is required');
|
|
132
|
+
const resolve = (): D1Binding => {
|
|
133
|
+
const b = typeof binding === 'function' ? (binding as () => D1Binding)() : binding;
|
|
134
|
+
if (!b) throw new Error('createD1DbDriver: D1 binding is not available');
|
|
135
|
+
return b;
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
kind: 'd1',
|
|
139
|
+
async exec(sql, params = []) {
|
|
140
|
+
const res = await resolve()
|
|
141
|
+
.prepare(sql)
|
|
142
|
+
.bind(...params)
|
|
143
|
+
.run();
|
|
144
|
+
const changes = res?.meta?.changes ?? res?.meta?.rows_written ?? 0;
|
|
145
|
+
return { changes };
|
|
146
|
+
},
|
|
147
|
+
async all(sql, params = []) {
|
|
148
|
+
const res = await resolve()
|
|
149
|
+
.prepare(sql)
|
|
150
|
+
.bind(...params)
|
|
151
|
+
.all();
|
|
152
|
+
return (res?.results ?? []) as any[];
|
|
153
|
+
},
|
|
154
|
+
async get(sql, params = []) {
|
|
155
|
+
const row = await resolve()
|
|
156
|
+
.prepare(sql)
|
|
157
|
+
.bind(...params)
|
|
158
|
+
.first();
|
|
159
|
+
return (row ?? null) as any;
|
|
160
|
+
},
|
|
161
|
+
// eslint-disable-next-line require-await -- async contract; resolve() throws surface as rejection
|
|
162
|
+
async batch(ops) {
|
|
163
|
+
const b = resolve();
|
|
164
|
+
// D1 batch is atomic (implicitly transactional) — a failing statement
|
|
165
|
+
// rolls the batch back, matching the node driver's transaction semantics
|
|
166
|
+
const stmts = ops.map((op) => b.prepare(op.sql).bind(...(op.params ?? [])));
|
|
167
|
+
return b.batch(stmts);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|