payment-kit 1.29.1 → 1.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +10 -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/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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// CF shim for @blocklet/sdk/lib/util/wallet.
|
|
2
|
+
//
|
|
3
|
+
// DEAD on the CF path: the only importer is the csrf middleware (node-shell only;
|
|
4
|
+
// see util-csrf.ts), which reads isDidWalletConnect to skip csrf for DID-wallet
|
|
5
|
+
// requests. Never executes on the worker — a resolving stub is enough.
|
|
6
|
+
export const isDidWalletConnect = (_userAgent?: string): boolean => false;
|
|
7
|
+
|
|
8
|
+
export default { isDidWalletConnect };
|
package/cloudflare/shims/cron.ts
CHANGED
|
@@ -1,182 +1,62 @@
|
|
|
1
|
-
// @abtnode/cron shim for CF Cron Triggers
|
|
1
|
+
// @abtnode/cron shim for CF Cron Triggers (Phase 9, W2-1b).
|
|
2
2
|
//
|
|
3
|
-
// The real @abtnode/cron exports { init } where init({
|
|
4
|
-
// In CF Workers
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
// The real @abtnode/cron exports { init } where init({ jobs, onError }) registers
|
|
4
|
+
// jobs. In CF Workers we store the jobs and execute due ones from scheduled().
|
|
5
|
+
// The cron-expression matcher + registry now live in the shared cron driver
|
|
6
|
+
// (api/src/libs/drivers/cron.ts) so embedded and worker agree on when a job is
|
|
7
|
+
// due; this file is the thin worker adapter that keeps the @abtnode/cron API.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createCronRegistry,
|
|
11
|
+
setCronDriver,
|
|
12
|
+
getCronDriver,
|
|
13
|
+
matchesCron,
|
|
14
|
+
shouldRunInWindow,
|
|
15
|
+
} from '../../api/src/libs/drivers/cron';
|
|
16
|
+
|
|
17
|
+
// D2: crons/index.ts now registers through getCronDriver() instead of importing
|
|
18
|
+
// @abtnode/cron directly. On CF this shim is the active cron driver, so make the
|
|
19
|
+
// cf-cron registry the global driver at module load — BEFORE worker.ts calls
|
|
20
|
+
// crons.init(). That keeps crons.init()'s register() and this shim's runAll() on
|
|
21
|
+
// the SAME passive cf-cron registry (host drives runDue from scheduled(); no
|
|
22
|
+
// @abtnode/cron self-scheduling timer is ever created in the frozen isolate).
|
|
23
|
+
const registry = createCronRegistry('cf-cron');
|
|
24
|
+
setCronDriver(registry);
|
|
15
25
|
|
|
16
26
|
type InitOptions = {
|
|
17
27
|
context?: any;
|
|
18
|
-
jobs:
|
|
28
|
+
jobs: Array<{ name: string; time: string; fn: () => Promise<any> | any; options?: { runOnInit?: boolean } }>;
|
|
19
29
|
onError?: (error: Error, name: string) => void;
|
|
20
30
|
};
|
|
21
31
|
|
|
22
|
-
// Singleton storage for registered cron jobs
|
|
23
|
-
const registeredJobs: CronJob[] = [];
|
|
24
|
-
let onErrorHandler: ((error: Error, name: string) => void) | undefined;
|
|
25
|
-
|
|
26
|
-
// --- Cron expression matcher ---
|
|
27
|
-
// Supports 6-field format: second minute hour dayOfMonth month dayOfWeek
|
|
28
|
-
// Supports: numbers, *, */N, ranges (1-5), lists (1,3,5)
|
|
29
|
-
|
|
30
|
-
function parseField(field: string, min: number, max: number): number[] | null {
|
|
31
|
-
// null means "match all"
|
|
32
|
-
if (field === '*') return null;
|
|
33
|
-
|
|
34
|
-
const values = new Set<number>();
|
|
35
|
-
|
|
36
|
-
for (const part of field.split(',')) {
|
|
37
|
-
// */N — every N
|
|
38
|
-
const stepMatch = part.match(/^\*\/(\d+)$/);
|
|
39
|
-
if (stepMatch) {
|
|
40
|
-
const step = parseInt(stepMatch[1], 10);
|
|
41
|
-
for (let i = min; i <= max; i += step) {
|
|
42
|
-
values.add(i);
|
|
43
|
-
}
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// N-M — range
|
|
48
|
-
const rangeMatch = part.match(/^(\d+)-(\d+)$/);
|
|
49
|
-
if (rangeMatch) {
|
|
50
|
-
const from = parseInt(rangeMatch[1], 10);
|
|
51
|
-
const to = parseInt(rangeMatch[2], 10);
|
|
52
|
-
for (let i = from; i <= to; i++) {
|
|
53
|
-
values.add(i);
|
|
54
|
-
}
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// N — single value
|
|
59
|
-
const num = parseInt(part, 10);
|
|
60
|
-
if (!isNaN(num)) {
|
|
61
|
-
values.add(num);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return values.size > 0 ? Array.from(values) : null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Check if a date matches a 6-field cron expression.
|
|
70
|
-
* Returns true if the date's minute/hour/day/month/weekday match.
|
|
71
|
-
* Seconds field is ignored (CF triggers are minute-level).
|
|
72
|
-
*/
|
|
73
|
-
function matchesCron(cronExpr: string, date: Date): boolean {
|
|
74
|
-
const fields = cronExpr.trim().split(/\s+/);
|
|
75
|
-
if (fields.length < 5) return true; // Can't parse — run it
|
|
76
|
-
|
|
77
|
-
// 6-field: sec min hour dom month dow
|
|
78
|
-
// 5-field: min hour dom month dow
|
|
79
|
-
const offset = fields.length >= 6 ? 1 : 0;
|
|
80
|
-
|
|
81
|
-
const minuteField = parseField(fields[offset], 0, 59);
|
|
82
|
-
const hourField = parseField(fields[offset + 1], 0, 23);
|
|
83
|
-
const domField = parseField(fields[offset + 2], 1, 31);
|
|
84
|
-
const monthField = parseField(fields[offset + 3], 0, 11); // cron months are 1-12, JS is 0-11
|
|
85
|
-
const dowField = parseField(fields[offset + 4], 0, 6);
|
|
86
|
-
|
|
87
|
-
const m = date.getUTCMinutes();
|
|
88
|
-
const h = date.getUTCHours();
|
|
89
|
-
const dom = date.getUTCDate();
|
|
90
|
-
const month = date.getUTCMonth() + 1; // JS 0-based → cron 1-based
|
|
91
|
-
const dow = date.getUTCDay(); // 0=Sunday
|
|
92
|
-
|
|
93
|
-
if (minuteField && !minuteField.includes(m)) return false;
|
|
94
|
-
if (hourField && !hourField.includes(h)) return false;
|
|
95
|
-
if (domField && !domField.includes(dom)) return false;
|
|
96
|
-
if (monthField && !monthField.includes(month)) return false;
|
|
97
|
-
if (dowField && !dowField.includes(dow)) return false;
|
|
98
|
-
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Check if a cron expression should fire at the given date (minute-level match).
|
|
103
|
-
//
|
|
104
|
-
// History: this helper previously used a 5-minute look-ahead window, under the
|
|
105
|
-
// assumption that CF Cron Triggers fire every 5 minutes. Once the deploy config
|
|
106
|
-
// switched to every-minute cron, that window made every stepped expression fire
|
|
107
|
-
// 5x more often than intended, driving CF Queues past the free-tier daily cap
|
|
108
|
-
// (2026-04-17 incident). With CF Scheduled firing every minute, we only check
|
|
109
|
-
// the current minute — each cron expression triggers at its designed frequency.
|
|
110
|
-
// See docs/cf-queues-ops-alert-analysis.md § 改动 B.
|
|
111
|
-
function shouldRunInWindow(cronExpr: string, date: Date): boolean {
|
|
112
|
-
return matchesCron(cronExpr, date);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// --- Cron shim API ---
|
|
116
|
-
|
|
117
32
|
function init(options: InitOptions) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
for (const job of options.jobs || []) {
|
|
122
|
-
if (job.name && job.time && typeof job.fn === 'function') {
|
|
123
|
-
registeredJobs.push(job);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Skip runOnInit jobs in CF Workers — they'll run on the next matching trigger
|
|
128
|
-
// (running them at module init time would block the request and may exceed CPU limits)
|
|
129
|
-
|
|
33
|
+
registry.register(options.jobs || [], options.onError);
|
|
34
|
+
// runOnInit jobs are skipped in CF Workers — they run on the next matching
|
|
35
|
+
// trigger (running them at module init would block the request / risk CPU limits)
|
|
130
36
|
return {
|
|
131
37
|
addJob(name: string, time: string, fn: Function, opts?: any) {
|
|
132
|
-
|
|
38
|
+
registry.addJob(name, time, fn as any, opts);
|
|
39
|
+
},
|
|
40
|
+
start() {
|
|
41
|
+
/* no-op — CF scheduled() drives runAll */
|
|
133
42
|
},
|
|
134
|
-
start() { /* no-op */ },
|
|
135
43
|
};
|
|
136
44
|
}
|
|
137
45
|
|
|
138
|
-
// Called by worker.ts scheduled handler
|
|
139
|
-
//
|
|
140
|
-
// intended trigger minute) instead of relying on wall-clock at execution time.
|
|
141
|
-
// CF may deliver scheduled events with a small delay that crosses a minute
|
|
142
|
-
// boundary; matching on `scheduledTime` keeps exact-minute cron reliable.
|
|
46
|
+
// Called by worker.ts scheduled handler. Accepts an optional `now` so the caller
|
|
47
|
+
// can pass `event.scheduledTime` (the intended trigger minute).
|
|
143
48
|
async function runAll(now: Date = new Date()) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for (const job of registeredJobs) {
|
|
148
|
-
if (shouldRunInWindow(job.time, now)) {
|
|
149
|
-
matched.push(job.name);
|
|
150
|
-
try {
|
|
151
|
-
await job.fn();
|
|
152
|
-
} catch (err: any) {
|
|
153
|
-
console.error(`[Cron] ${job.name} failed:`, err?.message || err);
|
|
154
|
-
onErrorHandler?.(err, job.name);
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
skipped.push(job.name);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
console.log(`[Cron] Ran ${matched.length} jobs: [${matched.join(', ')}]. Skipped ${skipped.length}.`);
|
|
49
|
+
const { ran, skipped } = await getCronDriver().runDue(now);
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log(`[Cron] Ran ${ran.length} jobs: [${ran.join(', ')}]. Skipped ${skipped.length}.`);
|
|
162
52
|
}
|
|
163
53
|
|
|
164
54
|
async function runJob(name: string) {
|
|
165
|
-
|
|
166
|
-
if (job) {
|
|
167
|
-
try {
|
|
168
|
-
await job.fn();
|
|
169
|
-
} catch (err: any) {
|
|
170
|
-
console.error(`[Cron] ${name} failed:`, err?.message || err);
|
|
171
|
-
onErrorHandler?.(err, name);
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
console.warn(`[Cron] Job ${name} not found`);
|
|
175
|
-
}
|
|
55
|
+
await getCronDriver().runJob(name);
|
|
176
56
|
}
|
|
177
57
|
|
|
178
58
|
function getJobNames(): string[] {
|
|
179
|
-
return
|
|
59
|
+
return getCronDriver().getJobNames();
|
|
180
60
|
}
|
|
181
61
|
|
|
182
62
|
// Export matching @abtnode/cron API: default export is { init }
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// EventEmitter shim for CF Workers.
|
|
2
|
+
//
|
|
3
|
+
// workerd's nodejs_compat provides `node:events` for ESM `import from 'events'`,
|
|
4
|
+
// but the esbuild banner's globalThis.require polyfill (build.ts) does NOT list
|
|
5
|
+
// `events`, so CJS `const { EventEmitter } = require('events')` resolves to an
|
|
6
|
+
// empty object and `class X extends EventEmitter` throws at global init
|
|
7
|
+
// ("Class extends value undefined"). Two core drivers hit this:
|
|
8
|
+
// - api/src/libs/drivers/locks.ts (MemoryLock)
|
|
9
|
+
// - api/src/libs/drivers/auth-storage.ts (DbAuthStorage extends EventEmitter)
|
|
10
|
+
//
|
|
11
|
+
// Aliasing `events` to this shim (build.ts) makes BOTH the CJS-require and
|
|
12
|
+
// ESM-import forms resolve to one deterministic implementation, removing the
|
|
13
|
+
// fragile split between nodejs_compat (ESM) and the banner (CJS). Only the core
|
|
14
|
+
// queue/lock/auth-storage event buses use it — emit / on / once / removeListener
|
|
15
|
+
// are the methods exercised (verified by grep), so a compact but correct
|
|
16
|
+
// implementation suffices.
|
|
17
|
+
|
|
18
|
+
type Listener = (...args: any[]) => void;
|
|
19
|
+
|
|
20
|
+
export class EventEmitter {
|
|
21
|
+
private _events: Map<string | symbol, Listener[]> = new Map();
|
|
22
|
+
|
|
23
|
+
private _maxListeners = 10;
|
|
24
|
+
|
|
25
|
+
addListener(event: string | symbol, listener: Listener): this {
|
|
26
|
+
return this.on(event, listener);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
on(event: string | symbol, listener: Listener): this {
|
|
30
|
+
const list = this._events.get(event);
|
|
31
|
+
if (list) {
|
|
32
|
+
list.push(listener);
|
|
33
|
+
} else {
|
|
34
|
+
this._events.set(event, [listener]);
|
|
35
|
+
}
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
prependListener(event: string | symbol, listener: Listener): this {
|
|
40
|
+
const list = this._events.get(event);
|
|
41
|
+
if (list) {
|
|
42
|
+
list.unshift(listener);
|
|
43
|
+
} else {
|
|
44
|
+
this._events.set(event, [listener]);
|
|
45
|
+
}
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
once(event: string | symbol, listener: Listener): this {
|
|
50
|
+
const wrapper = (...args: any[]) => {
|
|
51
|
+
this.removeListener(event, wrapper);
|
|
52
|
+
listener.apply(this, args);
|
|
53
|
+
};
|
|
54
|
+
// keep a handle to the original so removeListener(event, listener) still works
|
|
55
|
+
(wrapper as any).listener = listener;
|
|
56
|
+
return this.on(event, wrapper);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
prependOnceListener(event: string | symbol, listener: Listener): this {
|
|
60
|
+
const wrapper = (...args: any[]) => {
|
|
61
|
+
this.removeListener(event, wrapper);
|
|
62
|
+
listener.apply(this, args);
|
|
63
|
+
};
|
|
64
|
+
(wrapper as any).listener = listener;
|
|
65
|
+
return this.prependListener(event, wrapper);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeListener(event: string | symbol, listener: Listener): this {
|
|
69
|
+
const list = this._events.get(event);
|
|
70
|
+
if (!list) return this;
|
|
71
|
+
const idx = list.findIndex((l) => l === listener || (l as any).listener === listener);
|
|
72
|
+
if (idx >= 0) {
|
|
73
|
+
list.splice(idx, 1);
|
|
74
|
+
if (list.length === 0) this._events.delete(event);
|
|
75
|
+
}
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
off(event: string | symbol, listener: Listener): this {
|
|
80
|
+
return this.removeListener(event, listener);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
removeAllListeners(event?: string | symbol): this {
|
|
84
|
+
if (event === undefined) {
|
|
85
|
+
this._events.clear();
|
|
86
|
+
} else {
|
|
87
|
+
this._events.delete(event);
|
|
88
|
+
}
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
emit(event: string | symbol, ...args: any[]): boolean {
|
|
93
|
+
const list = this._events.get(event);
|
|
94
|
+
if (!list || list.length === 0) return false;
|
|
95
|
+
// copy so once()-removals during iteration don't skip listeners
|
|
96
|
+
for (const listener of [...list]) {
|
|
97
|
+
listener.apply(this, args);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
listeners(event: string | symbol): Listener[] {
|
|
103
|
+
return [...(this._events.get(event) || [])];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
listenerCount(event: string | symbol): number {
|
|
107
|
+
return this._events.get(event)?.length || 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
eventNames(): (string | symbol)[] {
|
|
111
|
+
return [...this._events.keys()];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setMaxListeners(n: number): this {
|
|
115
|
+
this._maxListeners = n;
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getMaxListeners(): number {
|
|
120
|
+
return this._maxListeners;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default EventEmitter;
|
|
@@ -12,7 +12,21 @@
|
|
|
12
12
|
|
|
13
13
|
type Task = { data: any; cb?: Function };
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Phase 9 (W2-1b): faithful fastq executor driver for the worker.
|
|
16
|
+
//
|
|
17
|
+
// Match the real fastq calling convention. The Node queue engine
|
|
18
|
+
// (api/src/libs/queue) calls `fastq(workerFn, concurrency)` (2-arg form);
|
|
19
|
+
// real fastq detects a function first arg and shifts (context => null). The
|
|
20
|
+
// previous shim assumed the 3-arg `fastq(context, worker, concurrency)` form,
|
|
21
|
+
// so the 2-arg call landed `worker` on `context` and every job execution threw
|
|
22
|
+
// "worker is not a function". This shift makes the shim a drop-in executor so
|
|
23
|
+
// the SAME engine runs identically on Node (real fastq) and the worker (shim).
|
|
24
|
+
export default function fastq(context: any, worker: Function, concurrency: number) {
|
|
25
|
+
if (typeof context === 'function') {
|
|
26
|
+
concurrency = worker as unknown as number;
|
|
27
|
+
worker = context;
|
|
28
|
+
context = null;
|
|
29
|
+
}
|
|
16
30
|
const pending: Task[] = [];
|
|
17
31
|
let running = false;
|
|
18
32
|
let drainFn: (() => void) | null = null;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
// NeDB → D1 storage shim for DID Connect sessions
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// NeDB → D1 storage shim for DID Connect sessions (Phase 8, W2-1a).
|
|
2
|
+
//
|
|
3
|
+
// Was a no-op stub (DID Connect state silently dropped in the worker). Now a
|
|
4
|
+
// thin worker adapter: it builds a D1 db driver from the bound D1 database and
|
|
5
|
+
// delegates to the real, contract-tested DbAuthStorage. The `{ dbPath }` option
|
|
6
|
+
// is ignored — persistence is the D1 binding, not a disk file.
|
|
7
|
+
|
|
8
|
+
import { createD1DbDriver, DbAuthStorage } from '../../api/src/libs/drivers';
|
|
9
|
+
|
|
10
|
+
import { getDB } from './sequelize-d1/model';
|
|
11
|
+
|
|
12
|
+
export default class AuthStorage extends DbAuthStorage {
|
|
13
|
+
constructor(_opts?: any) {
|
|
14
|
+
// lazy getter — the D1 binding is set per request, not at module import
|
|
15
|
+
super(createD1DbDriver(() => getDB()));
|
|
16
|
+
}
|
|
9
17
|
}
|
package/cloudflare/shims/xss.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export function xss(_opts?: any) {
|
|
2
2
|
return (_req: any, _res: any, next: any) => next();
|
|
3
3
|
}
|
|
4
|
+
|
|
5
|
+
// Phase 4 (express→hono): the hono xss middleware (LITE app-shell on CF) calls
|
|
6
|
+
// initSanitize(...)(body) to produce sanitizedBody. The old express-compat worker
|
|
7
|
+
// path set req.body WITHOUT xss sanitization, so an identity sanitizer preserves
|
|
8
|
+
// the worker's exact behavior (the node host uses the real @blocklet/xss).
|
|
9
|
+
export function initSanitize(_opts?: any) {
|
|
10
|
+
return <T>(body: T): T => body;
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Phase 7 (W1′): worker tenant-context wiring.
|
|
2
|
+
//
|
|
3
|
+
// The CF worker's `*` middleware only set up DB/env — it never established a
|
|
4
|
+
// tenant context, so every TenantModel query in the worker fell back to the
|
|
5
|
+
// single-mode default. This Hono middleware mirrors the Express
|
|
6
|
+
// `contextMiddleware`: it resolves the request's raw Host to a tenant via the
|
|
7
|
+
// SINGLE-POINT resolver (`resolveTenantForHost`) and wraps the downstream chain
|
|
8
|
+
// in `context.withTenant` so the resolved tenant flows through AsyncLocalStorage
|
|
9
|
+
// into every handler/query.
|
|
10
|
+
//
|
|
11
|
+
// single mode: resolves to the deployment app DID (standalone worker unchanged).
|
|
12
|
+
// multi mode : unknown/missing Host -> 400 fail-closed, no default-tenant
|
|
13
|
+
// fallback, and the handler never runs.
|
|
14
|
+
|
|
15
|
+
import type { Context, Next } from 'hono';
|
|
16
|
+
|
|
17
|
+
import { context } from '../api/src/libs/context';
|
|
18
|
+
import { TenantError, TENANT_HOST_UNRESOLVED } from '../api/src/libs/tenant';
|
|
19
|
+
import { resolveTenantForHost } from '../api/src/libs/drivers/identity';
|
|
20
|
+
|
|
21
|
+
export function tenantMiddleware() {
|
|
22
|
+
return async (c: Context, next: Next) => {
|
|
23
|
+
let instanceDid: string;
|
|
24
|
+
try {
|
|
25
|
+
// raw Host header only — never a proxy header (X-Forwarded-Host); the CF
|
|
26
|
+
// route is responsible for ensuring it cannot be forged by the client.
|
|
27
|
+
instanceDid = await resolveTenantForHost(c.req.header('host'));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err instanceof TenantError && err.code === TENANT_HOST_UNRESOLVED) {
|
|
30
|
+
return c.json({ error: { code: err.code, message: err.message } }, 400);
|
|
31
|
+
}
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return context.withTenant(instanceDid, () => next());
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Phase 7 (W1′): worker `withTenant` wiring. The CF worker had no tenant
|
|
2
|
+
// context — its `*` middleware only `setDB`. This drives the real Hono tenant
|
|
3
|
+
// middleware (the same factory mounted in worker.ts) over a tiny Hono app so
|
|
4
|
+
// the actual Host -> instanceDid resolution, multi-mode fail-closed 4xx, and
|
|
5
|
+
// `context.withTenant` ALS wrapping are exercised end to end.
|
|
6
|
+
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
|
|
9
|
+
import { tenantMiddleware } from '../tenant-middleware';
|
|
10
|
+
import { getInstanceDid, context } from '../../api/src/libs/context';
|
|
11
|
+
import { getDefaultInstanceDid } from '../../api/src/libs/tenant';
|
|
12
|
+
import { setIdentityDriver, createDefaultIdentityDriver, type IdentityDriver } from '../../api/src/libs/drivers/identity';
|
|
13
|
+
|
|
14
|
+
const TENANT_A = 'did:abt:zHOSTA';
|
|
15
|
+
const TENANT_B = 'did:abt:zHOSTB';
|
|
16
|
+
|
|
17
|
+
// Build a Hono app wired exactly like worker.ts: tenant middleware on /api/*,
|
|
18
|
+
// then a handler that reports the tenant resolved from the ALS context.
|
|
19
|
+
function buildApp() {
|
|
20
|
+
const app = new Hono();
|
|
21
|
+
app.use('/api/*', tenantMiddleware());
|
|
22
|
+
app.get('/api/whoami', (c) => c.json({ tenant: getInstanceDid() }));
|
|
23
|
+
// /health is OUTSIDE /api/* — must never require a tenant (infra health check)
|
|
24
|
+
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
25
|
+
return app;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function get(app: Hono, path: string, headers: Record<string, string> = {}) {
|
|
29
|
+
const res = await app.request(path, { headers });
|
|
30
|
+
let body: any;
|
|
31
|
+
try {
|
|
32
|
+
body = await res.json();
|
|
33
|
+
} catch {
|
|
34
|
+
body = await res.text();
|
|
35
|
+
}
|
|
36
|
+
return { status: res.status, body };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ORIGINAL_MODE = process.env.PAYMENT_TENANT_MODE;
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
if (ORIGINAL_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
|
|
43
|
+
else process.env.PAYMENT_TENANT_MODE = ORIGINAL_MODE;
|
|
44
|
+
setIdentityDriver(createDefaultIdentityDriver());
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('single mode (standalone worker legacy) — unchanged', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
process.env.PAYMENT_TENANT_MODE = 'single';
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('any host resolves to the deployment app DID', async () => {
|
|
53
|
+
const expected = getDefaultInstanceDid();
|
|
54
|
+
const app = buildApp();
|
|
55
|
+
const r1 = await get(app, '/api/whoami', { Host: 'anything.example.com' });
|
|
56
|
+
expect(r1.status).toBe(200);
|
|
57
|
+
expect(r1.body.tenant).toBe(expected);
|
|
58
|
+
const r2 = await get(app, '/api/whoami', { Host: 'other.example.com' });
|
|
59
|
+
expect(r2.body.tenant).toBe(expected); // default regardless of host
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('a request with no Host still resolves (single-mode default, no crash)', async () => {
|
|
63
|
+
const expected = getDefaultInstanceDid();
|
|
64
|
+
const app = buildApp();
|
|
65
|
+
const r = await get(app, '/api/whoami');
|
|
66
|
+
expect(r.status).toBe(200);
|
|
67
|
+
expect(r.body.tenant).toBe(expected);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('multi mode — Host maps to tenant, fail-closed on unknown', () => {
|
|
72
|
+
const identity: IdentityDriver = {
|
|
73
|
+
resolveInstanceDidForHost(host) {
|
|
74
|
+
if (host === 'a.example.com') return TENANT_A;
|
|
75
|
+
if (host === 'b.example.com') return TENANT_B;
|
|
76
|
+
return null;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
82
|
+
setIdentityDriver(identity);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Happy path — two hosts see two tenants (data isolation)
|
|
86
|
+
it('two hosts resolve to two tenants', async () => {
|
|
87
|
+
const app = buildApp();
|
|
88
|
+
const a = await get(app, '/api/whoami', { Host: 'a.example.com' });
|
|
89
|
+
const b = await get(app, '/api/whoami', { Host: 'b.example.com' });
|
|
90
|
+
expect(a.body.tenant).toBe(TENANT_A);
|
|
91
|
+
expect(b.body.tenant).toBe(TENANT_B);
|
|
92
|
+
expect(a.body.tenant).not.toBe(b.body.tenant);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Bad input — missing Host fails closed (no default-tenant fallback)
|
|
96
|
+
it('missing Host fails closed with 4xx (no APP_PID fallback)', async () => {
|
|
97
|
+
const app = buildApp();
|
|
98
|
+
const r = await get(app, '/api/whoami');
|
|
99
|
+
expect(r.status).toBe(400);
|
|
100
|
+
expect(r.body.error.code).toBe('TENANT_HOST_UNRESOLVED');
|
|
101
|
+
expect(r.body.tenant).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Data leak — unknown (unregistered) host must never fall into a tenant
|
|
105
|
+
it('unknown host fails closed with 4xx + error code', async () => {
|
|
106
|
+
const app = buildApp();
|
|
107
|
+
const r = await get(app, '/api/whoami', { Host: 'unknown.example.com' });
|
|
108
|
+
expect(r.status).toBe(400);
|
|
109
|
+
expect(r.body.error.code).toBe('TENANT_HOST_UNRESOLVED');
|
|
110
|
+
expect(r.body.tenant).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Security — a forged X-Forwarded-Host must not change resolution (raw Host only)
|
|
114
|
+
it('SECURITY: X-Forwarded-Host cannot override the raw Host', async () => {
|
|
115
|
+
const app = buildApp();
|
|
116
|
+
const r = await get(app, '/api/whoami', { Host: 'a.example.com', 'X-Forwarded-Host': 'b.example.com' });
|
|
117
|
+
expect(r.body.tenant).toBe(TENANT_A);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('SECURITY: X-Forwarded-Host pointing at a known tenant cannot rescue an unknown raw Host', async () => {
|
|
121
|
+
const app = buildApp();
|
|
122
|
+
const r = await get(app, '/api/whoami', { Host: 'unknown.example.com', 'X-Forwarded-Host': 'a.example.com' });
|
|
123
|
+
expect(r.status).toBe(400);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Data loss — a 4xx-rejected request must never reach the route handler
|
|
127
|
+
it('a 4xx-rejected request never runs the handler (no write)', async () => {
|
|
128
|
+
const app = new Hono();
|
|
129
|
+
app.use('/api/*', tenantMiddleware());
|
|
130
|
+
let handlerRuns = 0;
|
|
131
|
+
app.get('/api/whoami', (c) => {
|
|
132
|
+
handlerRuns += 1;
|
|
133
|
+
return c.json({ tenant: getInstanceDid() });
|
|
134
|
+
});
|
|
135
|
+
const r = await get(app, '/api/whoami', { Host: 'unknown.example.com' });
|
|
136
|
+
expect(r.status).toBe(400);
|
|
137
|
+
expect(handlerRuns).toBe(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ALS propagation — the handler runs INSIDE the withTenant context
|
|
141
|
+
it('the handler observes the resolved tenant via context ALS', async () => {
|
|
142
|
+
const app = new Hono();
|
|
143
|
+
app.use('/api/*', tenantMiddleware());
|
|
144
|
+
let seen: string | undefined;
|
|
145
|
+
app.get('/api/whoami', (c) => {
|
|
146
|
+
seen = context.getInstanceDid();
|
|
147
|
+
return c.json({ ok: true });
|
|
148
|
+
});
|
|
149
|
+
await get(app, '/api/whoami', { Host: 'b.example.com' });
|
|
150
|
+
expect(seen).toBe(TENANT_B);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// /health is outside /api/* — tenant middleware must not gate it
|
|
154
|
+
it('/health is reachable without a Host even in multi mode', async () => {
|
|
155
|
+
const app = buildApp();
|
|
156
|
+
const r = await get(app, '/health');
|
|
157
|
+
expect(r.status).toBe(200);
|
|
158
|
+
expect(r.body.status).toBe('ok');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Phase 12c HARD GATE (scan): the CF worker is a host adapter. It must mount the
|
|
2
|
+
// embedded service's resource-route surface (svc.http.resourceRoutes) and NEVER
|
|
3
|
+
// access svc.handler — whose lazy getter builds the node-only Express app shell
|
|
4
|
+
// that does not belong under workerd. It also must not import the deleted legacy
|
|
5
|
+
// shims/queue.ts duplicate engine. This statically scans the worker source so a
|
|
6
|
+
// regression fails the suite (the runtime Proxy in ensurePaymentService is the
|
|
7
|
+
// second line of defense).
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
const workerSrc = fs.readFileSync(path.join(__dirname, '../worker.ts'), 'utf8');
|
|
12
|
+
|
|
13
|
+
describe('Phase 12c — worker host-adapter hard gate', () => {
|
|
14
|
+
it('mounts the resource-route surface (svc.http.resourceRoutes)', () => {
|
|
15
|
+
expect(workerSrc).toMatch(/service\.http\.resourceRoutes/);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('never reads .handler on the payment service', () => {
|
|
19
|
+
const offending = workerSrc
|
|
20
|
+
.split('\n')
|
|
21
|
+
.map((line, n) => ({ line: line.trim(), n: n + 1 }))
|
|
22
|
+
// ignore comment lines — only actual code may not read svc.handler
|
|
23
|
+
.filter(({ line }) => !line.startsWith('//') && !line.startsWith('*') && !line.startsWith('/*'))
|
|
24
|
+
.filter(({ line }) => /\b(service|paymentService|svc)\.handler\b/.test(line))
|
|
25
|
+
// the hard-gate trap string + the Proxy guard line are the gate itself
|
|
26
|
+
.filter(({ line }) => !/forbidden in the CF worker/.test(line) && !/prop === 'handler'/.test(line));
|
|
27
|
+
expect(offending).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('installs the runtime hard-gate Proxy that throws on handler access', () => {
|
|
31
|
+
expect(workerSrc).toMatch(/new Proxy\(svc,/);
|
|
32
|
+
expect(workerSrc).toMatch(/svc\.handler is forbidden in the CF worker/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does not import the deleted legacy shims/queue engine', () => {
|
|
36
|
+
expect(workerSrc).not.toMatch(/from '\.\/shims\/queue'/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('drives the core queue runtime surface instead', () => {
|
|
40
|
+
expect(workerSrc).toMatch(/from '\.\.\/api\/src\/libs\/queue\/runtime'/);
|
|
41
|
+
expect(workerSrc).toMatch(/dispatchDueJobs\(\)/);
|
|
42
|
+
expect(workerSrc).toMatch(/flushQueueWork\(\)/);
|
|
43
|
+
});
|
|
44
|
+
});
|