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,667 @@
|
|
|
1
|
+
/* eslint-disable global-require, max-classes-per-file */
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
5
|
+
import { CustomError, formatError, getStatusFromError } from '@blocklet/error';
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
8
|
+
import { isProduction, blockletAppDir } from './libs/env';
|
|
9
|
+
|
|
10
|
+
import logger from './libs/logger';
|
|
11
|
+
import { context as requestContext } from './libs/context';
|
|
12
|
+
import { TENANT_CONTEXT_MISSING, TenantError, getTenantMode, getDefaultInstanceDid } from './libs/tenant';
|
|
13
|
+
import type { LocksDriver, QueueHostHooks, CronDriver, IdentityDriver, SecretsDriver } from './libs/drivers';
|
|
14
|
+
import type { PaymentFetchOptions, FetchHandler } from './libs/http-fetch-adapter';
|
|
15
|
+
|
|
16
|
+
export type { PaymentFetchOptions } from './libs/http-fetch-adapter';
|
|
17
|
+
|
|
18
|
+
// Phase 7 (W2-0): the embedded payment service factory.
|
|
19
|
+
//
|
|
20
|
+
// Assembly happens HERE (factory call time), never at module import time —
|
|
21
|
+
// the blocklet server shell (./index.ts) listens itself and calls
|
|
22
|
+
// lifecycle.start(); other hosts (arc, standalone worker) mount `handler`
|
|
23
|
+
// under their own prefix and own the lifecycle explicitly.
|
|
24
|
+
//
|
|
25
|
+
// Slot semantics in this phase: validated and carried, with implementations
|
|
26
|
+
// passing through to the current internals (db -> the global sequelize
|
|
27
|
+
// instance, queue/cron/locks -> existing libs). The `config` slot is now
|
|
28
|
+
// authoritative (Phase 8 W2′): setCoreConfig makes the injected config the
|
|
29
|
+
// source of truth for every core read via the libs/env.ts boundary, with
|
|
30
|
+
// process.env as the fallback; the api/src core-env whitelist is zero. NOTE:
|
|
31
|
+
// two factory calls in one process still share the module-level singletons
|
|
32
|
+
// (models, sequelize, queues) — documented transition state, not the end contract.
|
|
33
|
+
|
|
34
|
+
export type TenancySlot =
|
|
35
|
+
| { mode: 'single'; instanceDid: string }
|
|
36
|
+
| {
|
|
37
|
+
mode: 'multi';
|
|
38
|
+
/**
|
|
39
|
+
* D6 (optional): a host-provided enumeration of all known tenant
|
|
40
|
+
* instanceDids. When given, start() bootstraps each at startup; otherwise
|
|
41
|
+
* the host drives bootstrapTenant() per tenant (e.g. on first request).
|
|
42
|
+
* This is the ONLY sanctioned tenant-registry hook — the IdentityDriver
|
|
43
|
+
* (host->tenant + getAppEk) must not be repurposed for enumeration.
|
|
44
|
+
*/
|
|
45
|
+
listInstanceDids?: () => Promise<string[]> | string[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface PaymentCoreSlots {
|
|
49
|
+
/** all runtime configuration, explicit (Phase 12 makes this exhaustive) */
|
|
50
|
+
config: Record<string, any>;
|
|
51
|
+
/** SQL driver slot — currently the sequelize instance to bind models to */
|
|
52
|
+
db: { sequelize: any };
|
|
53
|
+
queue?: QueueHostHooks;
|
|
54
|
+
cron?: CronDriver;
|
|
55
|
+
locks?: LocksDriver;
|
|
56
|
+
identity?: IdentityDriver;
|
|
57
|
+
secrets?: SecretsDriver;
|
|
58
|
+
tenancy?: TenancySlot;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PaymentCoreLifecycle {
|
|
62
|
+
start: () => Promise<void>;
|
|
63
|
+
stop: () => Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PaymentCoreService {
|
|
67
|
+
/**
|
|
68
|
+
* The full hono app: forked app-shell middleware + DID-Connect handlers +
|
|
69
|
+
* resource routes + static/SPA fallback + unified error handling. For node
|
|
70
|
+
* hosts (blocklet server, arc-node). Built lazily on first access — a workerd
|
|
71
|
+
* host that never touches it never runs the node-only app shell (static
|
|
72
|
+
* serving, fallback), which the CF worker serves through its own Hono pipeline
|
|
73
|
+
* instead. Phase 4: this is the SAME hono instance exposed as `fetch`.
|
|
74
|
+
*/
|
|
75
|
+
handler: Hono;
|
|
76
|
+
/**
|
|
77
|
+
* The embeddable HTTP surface for hosts that own their own app shell. The CF
|
|
78
|
+
* worker mounts `resourceRoutes` into its Hono pipeline (Option 3 seam:
|
|
79
|
+
* resource routes only — no app shell static/fallback, no DID-Connect
|
|
80
|
+
* handlers, which the worker registers via its own Hono `attachDIDConnectRoutes`).
|
|
81
|
+
* Phase 4: a hono app (`/api/healthz` + the migrated resource domains, each
|
|
82
|
+
* scoped to its own app-shell pipeline), mounted by the worker via `app.route`.
|
|
83
|
+
*/
|
|
84
|
+
http: {
|
|
85
|
+
resourceRoutes: Hono;
|
|
86
|
+
/**
|
|
87
|
+
* D5: the Web-Fetch entry over the FULL hono app. A Fetch-native host
|
|
88
|
+
* (arc-node registerPrefixHandler) forwards a Web Request and gets a Web
|
|
89
|
+
* Response — base-strip, raw-body, multi Set-Cookie / redirect passthrough
|
|
90
|
+
* are all handled inside. The host writes zero express/req-res bridge. Never
|
|
91
|
+
* touched by the workerd host.
|
|
92
|
+
*/
|
|
93
|
+
fetch: (request: Request, opts?: PaymentFetchOptions) => Promise<Response>;
|
|
94
|
+
};
|
|
95
|
+
rpc: {
|
|
96
|
+
entitlements: {
|
|
97
|
+
check: (input: { customerDid: string; featureKey: string; livemode?: boolean }) => Promise<any>;
|
|
98
|
+
};
|
|
99
|
+
meterEvents: {
|
|
100
|
+
report: (input: Record<string, any>) => Promise<any>;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
lifecycle: PaymentCoreLifecycle;
|
|
104
|
+
/**
|
|
105
|
+
* D6 — idempotent per-tenant bootstrap (currency logos, overdraft prices,
|
|
106
|
+
* exchange-rate-health schedule, per-tenant integrations), run inside the
|
|
107
|
+
* tenant context. Multi-mode hosts MUST call this per tenant (single mode
|
|
108
|
+
* bootstraps the default tenant from lifecycle.start automatically).
|
|
109
|
+
*/
|
|
110
|
+
bootstrapTenant: (instanceDid: string) => Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Phase 4 (express→hono): the factory return is the published PaymentCoreService
|
|
115
|
+
* PLUS a top-level `fetch` — the hono app the blocklet server shell serves through
|
|
116
|
+
* `@hono/node-server` `serve({ fetch })`. `fetch` is the same hono instance as
|
|
117
|
+
* `handler` (kept as a convenience entry); `http.fetch` is the base-strip variant
|
|
118
|
+
* for arc consumers (same Request/Response shape, strips the host mount prefix).
|
|
119
|
+
*/
|
|
120
|
+
export type EmbeddedPaymentService = PaymentCoreService & {
|
|
121
|
+
fetch: (request: Request) => Response | Promise<Response>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export class PaymentCoreSlotError extends Error {
|
|
125
|
+
code = 'MISSING_SLOT';
|
|
126
|
+
|
|
127
|
+
slot: string;
|
|
128
|
+
|
|
129
|
+
constructor(slot: string) {
|
|
130
|
+
super(`createEmbeddedPaymentService: required slot "${slot}" is missing`);
|
|
131
|
+
this.name = 'PaymentCoreSlotError';
|
|
132
|
+
this.slot = slot;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Phase 8 (W2′): a REQUIRED config field is absent from the injected config (and
|
|
137
|
+
// not present in the process.env fallback). Distinct from a missing slot so the
|
|
138
|
+
// host can tell "you forgot a whole driver" from "you forgot one config key".
|
|
139
|
+
export class MissingConfigError extends Error {
|
|
140
|
+
code = 'MISSING_CONFIG_FIELD';
|
|
141
|
+
|
|
142
|
+
field: string;
|
|
143
|
+
|
|
144
|
+
constructor(field: string) {
|
|
145
|
+
super(`createEmbeddedPaymentService: required config field "${field}" is missing`);
|
|
146
|
+
this.name = 'MissingConfigError';
|
|
147
|
+
this.field = field;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// D1 (S3.0): the tenancy slot is the authoritative source of the tenant mode.
|
|
152
|
+
// A tenancy.mode that is neither "single" nor "multi", or that disagrees with an
|
|
153
|
+
// explicit config.PAYMENT_TENANT_MODE, is a construction-time fail-fast (the
|
|
154
|
+
// factory never silently picks one side). Distinct from a missing slot/config
|
|
155
|
+
// field so the host can tell "your tenancy slot is malformed/contradictory"
|
|
156
|
+
// from "you forgot a driver / a config key".
|
|
157
|
+
export class TenancySlotError extends Error {
|
|
158
|
+
code: 'INVALID_TENANCY_MODE' | 'TENANCY_MODE_CONFLICT';
|
|
159
|
+
|
|
160
|
+
constructor(code: 'INVALID_TENANCY_MODE' | 'TENANCY_MODE_CONFLICT', message: string) {
|
|
161
|
+
super(message);
|
|
162
|
+
this.name = 'TenancySlotError';
|
|
163
|
+
this.code = code;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const VALID_TENANT_MODES = ['single', 'multi'] as const;
|
|
168
|
+
|
|
169
|
+
// Phase 4 (express→hono): the /api surface is now two hono layers so an embedding
|
|
170
|
+
// host can take only what fits its runtime.
|
|
171
|
+
//
|
|
172
|
+
// buildResourceRoutesHono() — /api/healthz + the migrated resource domains, each
|
|
173
|
+
// scoped to its own forked app-shell pipeline. No DID-Connect handlers, no
|
|
174
|
+
// static/SPA fallback. This is the layer the CF worker mounts into its own
|
|
175
|
+
// Hono pipeline via `app.route('/', resourceRoutes)`.
|
|
176
|
+
// buildHonoApp() — the full node hono app (app-shell pipeline +
|
|
177
|
+
// migrated resources + DID-Connect + static/SPA fallback + error handling).
|
|
178
|
+
// For node hosts (blocklet server, arc-node via svc.http.fetch).
|
|
179
|
+
//
|
|
180
|
+
// All route modules are required lazily so importing THIS module stays
|
|
181
|
+
// side-effect-free for hosts that never build a layer.
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* /api/healthz + the migrated resource domains on a standalone hono app — the
|
|
185
|
+
* embeddable surface (no app shell static/fallback, no DID-Connect). The CF
|
|
186
|
+
* worker mounts this whole app via `app.route('/', resourceRoutes)`. Each domain
|
|
187
|
+
* carries its own scoped app-shell pipeline (cors/xss/csrf/cdn/context + livemode
|
|
188
|
+
* + optional baseCurrency) via mountMigratedResources / mountResourceGroup.
|
|
189
|
+
*/
|
|
190
|
+
function buildResourceRoutesHono(): Hono {
|
|
191
|
+
const { mountMigratedResources } = require('./routes/hono');
|
|
192
|
+
const app = new Hono();
|
|
193
|
+
app.get('/api/healthz', (c) => c.json({ ok: true }));
|
|
194
|
+
// No appShell arg → mountResourceGroup defaults to the LITE app-shell (xss for
|
|
195
|
+
// sanitizedBody + livemode + baseCurrency). The CF worker (the only consumer of
|
|
196
|
+
// resourceRoutes) owns its own cors + tenant and never ran csrf/cdn/i18n. Keeping
|
|
197
|
+
// this path free of pipeline.ts (csrf/cdn/context) keeps the worker bundle clear
|
|
198
|
+
// of @blocklet/sdk/lib/util/* subpaths its shim does not map.
|
|
199
|
+
mountMigratedResources(app);
|
|
200
|
+
return app;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The 14 DID-Connect handler modules (each carries its own `action` + callbacks). */
|
|
204
|
+
function connectHandlerModules(): any[] {
|
|
205
|
+
return [
|
|
206
|
+
require('./routes/connect/collect').default,
|
|
207
|
+
require('./routes/connect/collect-batch').default,
|
|
208
|
+
require('./routes/connect/pay').default,
|
|
209
|
+
require('./routes/connect/setup').default,
|
|
210
|
+
require('./routes/connect/subscribe').default,
|
|
211
|
+
require('./routes/connect/change-payment').default,
|
|
212
|
+
require('./routes/connect/change-plan').default,
|
|
213
|
+
require('./routes/connect/recharge').default,
|
|
214
|
+
require('./routes/connect/recharge-account').default,
|
|
215
|
+
require('./routes/connect/delegation').default,
|
|
216
|
+
require('./routes/connect/overdraft-protection').default,
|
|
217
|
+
require('./routes/connect/re-stake').default,
|
|
218
|
+
require('./routes/connect/auto-recharge-auth').default,
|
|
219
|
+
require('./routes/connect/change-payer').default,
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The DID-Connect handlers attached to a hono app. did-connect-js v4 dispatches
|
|
225
|
+
* by isHonoApp() → native attachHono (design §3.3), so the 14 handlers run with
|
|
226
|
+
* zero rewrite. Mounted on the main hono app alongside the native resource group
|
|
227
|
+
* (NOT under it — DID-Connect carries its own ensureSignedJson, no xss/csrf/cors).
|
|
228
|
+
*/
|
|
229
|
+
export function buildConnectRoutesHono(): Hono {
|
|
230
|
+
const { handlers } = require('./libs/auth');
|
|
231
|
+
const connectApp = new Hono();
|
|
232
|
+
for (const h of connectHandlerModules()) handlers.attach(Object.assign({ app: connectApp }, h));
|
|
233
|
+
return connectApp;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Phase 4 (express→hono) — the full node hono app. The loopback bridge + express
|
|
238
|
+
* app shell are gone; this hono app is the only entry.
|
|
239
|
+
*
|
|
240
|
+
* Structure (registration order is load-bearing — hono matches in registration
|
|
241
|
+
* order):
|
|
242
|
+
* ① /api/healthz — bare health route (BS liveness probe).
|
|
243
|
+
* ② native — the migrated resource domains + their scoped forked app-shell
|
|
244
|
+
* pipeline (cors/xss/csrf/cdn/context + livemode/baseCurrency),
|
|
245
|
+
* populated by `configureNative`.
|
|
246
|
+
* ③ connectApp — the DID-Connect sub-app, mounted alongside `native` (NOT under
|
|
247
|
+
* it — it carries its own ensureSignedJson, no xss/csrf/cors).
|
|
248
|
+
* ④ static + SPA fallback (production only) — what the express buildNodeHandler
|
|
249
|
+
* served; now hono-native. `fallback` runs first and skips asset
|
|
250
|
+
* paths (RESOURCE_PATTERN) + non-html, so real files fall through
|
|
251
|
+
* to serveStatic and html navigations get the injected index.html.
|
|
252
|
+
*
|
|
253
|
+
* Static/fallback modules are required lazily so importing this module stays
|
|
254
|
+
* side-effect-free for the workerd host (which never builds this app).
|
|
255
|
+
*/
|
|
256
|
+
export function buildHonoApp(configureNative?: (native: Hono) => void, getConnectApp?: () => Hono): Hono {
|
|
257
|
+
const app = new Hono();
|
|
258
|
+
|
|
259
|
+
// Unified error handling — the hono equivalent of express-async-errors + the
|
|
260
|
+
// express ErrorRequestHandler: CustomError → its mapped status + formatError;
|
|
261
|
+
// otherwise 500 JSON.
|
|
262
|
+
app.onError((err, c) => {
|
|
263
|
+
logger.error('handle router error', err);
|
|
264
|
+
if (err instanceof CustomError) {
|
|
265
|
+
// getStatusFromError returns a plain number; hono's c.json wants a
|
|
266
|
+
// StatusCode union — cast through the contentful-status type.
|
|
267
|
+
return c.json({ error: formatError(err) }, getStatusFromError(err) as ContentfulStatusCode);
|
|
268
|
+
}
|
|
269
|
+
return c.json({ error: (err as Error).message }, 500);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ① /api/healthz — was on the express resource router; now a bare hono route.
|
|
273
|
+
app.get('/api/healthz', (c) => c.json({ ok: true }));
|
|
274
|
+
|
|
275
|
+
const native = new Hono();
|
|
276
|
+
configureNative?.(native);
|
|
277
|
+
app.route('/', native); // ② native group (forked middleware + migrated resources)
|
|
278
|
+
if (getConnectApp) {
|
|
279
|
+
app.route('/', getConnectApp()); // ③ DID-Connect
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ④ production static + SPA fallback (the express buildNodeHandler served these;
|
|
283
|
+
// there is no more bridge to delegate to). Lazily required to keep this
|
|
284
|
+
// module import side-effect-free for the workerd host.
|
|
285
|
+
if (isProduction()) {
|
|
286
|
+
const { serveStatic } = require('@hono/node-server/serve-static');
|
|
287
|
+
const { fallback } = require('./middlewares/hono/fallback');
|
|
288
|
+
const staticDir = path.resolve(blockletAppDir()!, 'dist');
|
|
289
|
+
// serveStatic resolves `root` relative to process.cwd(); map the absolute
|
|
290
|
+
// app dist dir back to a cwd-relative path so it resolves to the same place.
|
|
291
|
+
const staticRoot = path.relative(process.cwd(), staticDir) || '.';
|
|
292
|
+
app.use('*', fallback('index.html', { root: staticDir })); // injected index.html for html GET (skips assets)
|
|
293
|
+
app.use('*', serveStatic({ root: staticRoot })); // real asset files
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return app;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** rpc calls demand a tenant: explicit context or single-mode default. */
|
|
300
|
+
function requireTenant(): string {
|
|
301
|
+
return requestContext.getInstanceDid(); // throws TENANT_CONTEXT_MISSING in multi mode without context
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function buildRpc(): PaymentCoreService['rpc'] {
|
|
305
|
+
return {
|
|
306
|
+
entitlements: {
|
|
307
|
+
check(input) {
|
|
308
|
+
// validation errors must surface as rejections, not sync throws
|
|
309
|
+
return Promise.resolve().then(() => {
|
|
310
|
+
const instanceDid = requireTenant();
|
|
311
|
+
const { checkEntitlement } = require('./libs/entitlement');
|
|
312
|
+
return requestContext.withTenant(instanceDid, () =>
|
|
313
|
+
checkEntitlement({
|
|
314
|
+
customer_did: input.customerDid,
|
|
315
|
+
product_id: input.featureKey,
|
|
316
|
+
livemode: input.livemode,
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
meterEvents: {
|
|
323
|
+
report(input) {
|
|
324
|
+
return Promise.resolve().then(() => {
|
|
325
|
+
const instanceDid = requireTenant();
|
|
326
|
+
const { MeterEvent } = require('./store/models');
|
|
327
|
+
return requestContext.withTenant(instanceDid, () => MeterEvent.create(input as any));
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let servicesStarted = false;
|
|
335
|
+
|
|
336
|
+
// D6: a host-provided tenant enumeration hook (tenancy.listInstanceDids). When a
|
|
337
|
+
// multi-mode host wants every known tenant bootstrapped at start(), it injects
|
|
338
|
+
// this; otherwise the host drives bootstrapTenant() per provisioned tenant (e.g.
|
|
339
|
+
// arc-node on first payment request). We do NOT abuse the IdentityDriver as a
|
|
340
|
+
// tenant registry — it only does host->tenant + getAppEk.
|
|
341
|
+
let listInstanceDidsHook: (() => Promise<string[]> | string[]) | undefined;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* D6 — idempotent per-tenant bootstrap. The tenant-scoped startup work that used
|
|
345
|
+
* to run (unscoped) inside startBackgroundServices lives here, executed INSIDE
|
|
346
|
+
* `withTenant(instanceDid)` so every query/push is correctly tenant-scoped:
|
|
347
|
+
* - syncCurrencyLogo() (PaymentMethod logo backfill)
|
|
348
|
+
* - ensureCreateOverdraftProtectionPrices (Price upsert)
|
|
349
|
+
* - scheduleHealthChecks(instanceDid) (exchange-rate-health, tenant in payload)
|
|
350
|
+
* - ensureStakedForGas() / ensureWebhookRegistered() (per-tenant integrations)
|
|
351
|
+
*
|
|
352
|
+
* Multi mode has no default tenant, so a host MUST bootstrap each tenant
|
|
353
|
+
* explicitly (per provisioned host / first request, or all-at-once via
|
|
354
|
+
* tenancy.listInstanceDids). Single mode bootstraps the default tenant from
|
|
355
|
+
* start() automatically (original behavior preserved).
|
|
356
|
+
*/
|
|
357
|
+
async function bootstrapTenant(instanceDid: string): Promise<void> {
|
|
358
|
+
if (!instanceDid || typeof instanceDid !== 'string') {
|
|
359
|
+
throw new TenantError(TENANT_CONTEXT_MISSING, 'bootstrapTenant requires an instanceDid');
|
|
360
|
+
}
|
|
361
|
+
const { syncCurrencyLogo } = require('./crons/currency');
|
|
362
|
+
const { ensureStakedForGas } = require('./integrations/arcblock/stake');
|
|
363
|
+
const { ensureWebhookRegistered } = require('./integrations/stripe/setup');
|
|
364
|
+
const { ensureCreateOverdraftProtectionPrices } = require('./libs/overdraft-protection');
|
|
365
|
+
const { scheduleHealthChecks } = require('./queues/exchange-rate-health');
|
|
366
|
+
|
|
367
|
+
// Run the whole bootstrap inside the tenant context. We AWAIT each op so its
|
|
368
|
+
// async continuation stays in-scope (a fire-and-forget would lose the ALS
|
|
369
|
+
// context after the callback returns).
|
|
370
|
+
await requestContext.withTenant(instanceDid, async () => {
|
|
371
|
+
await Promise.resolve(syncCurrencyLogo()).catch((error: unknown) =>
|
|
372
|
+
logger.error('bootstrapTenant: syncCurrencyLogo failed', { instanceDid, error })
|
|
373
|
+
);
|
|
374
|
+
await Promise.resolve(ensureCreateOverdraftProtectionPrices()).catch((error: unknown) =>
|
|
375
|
+
logger.error('bootstrapTenant: ensureCreateOverdraftProtectionPrices failed', { instanceDid, error })
|
|
376
|
+
);
|
|
377
|
+
// exchange-rate-health: tenant carried in the job payload (re-schedule safe)
|
|
378
|
+
scheduleHealthChecks(instanceDid);
|
|
379
|
+
if (isProduction()) {
|
|
380
|
+
await ensureWebhookRegistered().catch((error: unknown) =>
|
|
381
|
+
logger.error('bootstrapTenant: ensureWebhookRegistered failed', { instanceDid, error })
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
await Promise.resolve(ensureStakedForGas()).catch((error: unknown) =>
|
|
385
|
+
logger.error('bootstrapTenant: ensureStakedForGas failed', { instanceDid, error })
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function startBackgroundServices(): Promise<void> {
|
|
391
|
+
if (servicesStarted) {
|
|
392
|
+
logger.info('payment core background services already started, skipping');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
servicesStarted = true;
|
|
396
|
+
|
|
397
|
+
const crons = require('./crons/index').default;
|
|
398
|
+
const { initResourceHandler } = require('./integrations/blocklet/resource');
|
|
399
|
+
const { initUserHandler } = require('./integrations/blocklet/user');
|
|
400
|
+
const { initEventBroadcast } = require('./libs/ws');
|
|
401
|
+
|
|
402
|
+
// Tenant-AGNOSTIC engine registration: these create the queues + register the
|
|
403
|
+
// consumer handlers. Jobs carry their own tenant (instance_did), so no startup
|
|
404
|
+
// tenant context is needed. The exchange-rate-health SCHEDULE moved to
|
|
405
|
+
// bootstrapTenant (it pushes a tenant-scoped job); the queue itself is created
|
|
406
|
+
// at module import.
|
|
407
|
+
const starters: [string, () => Promise<any> | void][] = [
|
|
408
|
+
['payment', require('./queues/payment').startPaymentQueue],
|
|
409
|
+
['invoice', require('./queues/invoice').startInvoiceQueue],
|
|
410
|
+
['subscription', require('./queues/subscription').startSubscriptionQueue],
|
|
411
|
+
['event', require('./queues/event').startEventQueue],
|
|
412
|
+
['payout', require('./queues/payout').startPayoutQueue],
|
|
413
|
+
['vendor commission', require('./queues/vendors/commission').startVendorCommissionQueue],
|
|
414
|
+
['vendor fulfillment', require('./queues/vendors/fulfillment').startVendorFulfillmentQueue],
|
|
415
|
+
['coordinated fulfillment', require('./queues/vendors/fulfillment-coordinator').startCoordinatedFulfillmentQueue],
|
|
416
|
+
['checkoutSession', require('./queues/checkout-session').startCheckoutSessionQueue],
|
|
417
|
+
['notification', require('./queues/notification').startNotificationQueue],
|
|
418
|
+
['refund', require('./queues/refund').startRefundQueue],
|
|
419
|
+
['credit', require('./queues/credit-consume').startCreditConsumeQueue],
|
|
420
|
+
['credit grant', require('./queues/credit-grant').startCreditGrantQueue],
|
|
421
|
+
['token transfer', require('./queues/token-transfer').startTokenTransferQueue],
|
|
422
|
+
['credit reconciliation', require('./queues/credit-reconciliation').startReconciliationQueue],
|
|
423
|
+
['discount status', require('./queues/discount-status').startDiscountStatusQueue],
|
|
424
|
+
];
|
|
425
|
+
for (const [name, start] of starters) {
|
|
426
|
+
Promise.resolve(start()).then(() => logger.info(`${name} queue started`));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// cron + global handlers (tenant-agnostic; runOnInit is skipped in multi mode)
|
|
430
|
+
crons.init();
|
|
431
|
+
initEventBroadcast();
|
|
432
|
+
initResourceHandler();
|
|
433
|
+
initUserHandler();
|
|
434
|
+
|
|
435
|
+
// Per-tenant bootstrap. Single mode: the default tenant always exists, so
|
|
436
|
+
// bootstrap it now (preserves the original startup behavior). Multi mode: the
|
|
437
|
+
// host drives bootstrapTenant per tenant — either eagerly via the optional
|
|
438
|
+
// tenancy.listInstanceDids hook, or lazily (per provisioned host / first req).
|
|
439
|
+
if (getTenantMode() === 'single') {
|
|
440
|
+
await bootstrapTenant(getDefaultInstanceDid());
|
|
441
|
+
} else if (listInstanceDidsHook) {
|
|
442
|
+
const dids = (await listInstanceDidsHook()) || [];
|
|
443
|
+
for (const did of dids) {
|
|
444
|
+
// eslint-disable-next-line no-await-in-loop -- bootstraps run sequentially
|
|
445
|
+
await bootstrapTenant(did).catch((error: unknown) =>
|
|
446
|
+
logger.error('startup bootstrapTenant failed', { instanceDid: did, error })
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// eslint-disable-next-line require-await -- async contract; teardown is synchronous today
|
|
453
|
+
async function stopBackgroundServices(): Promise<void> {
|
|
454
|
+
if (!servicesStarted) return;
|
|
455
|
+
|
|
456
|
+
// D2 teardown surface. In-flight jobs are never lost: the jobs table is the
|
|
457
|
+
// source of truth and re-delivery happens on the next start (queue recovery
|
|
458
|
+
// scan). We tear down the two background trigger sources so the process has no
|
|
459
|
+
// dangling handle after stop (the spec's "active handles 归零"):
|
|
460
|
+
// 1. cron timers — the node-cron driver stops its @abtnode/cron scheduler
|
|
461
|
+
// 2. queue poll loops — each scheduled queue's node loop sleep timer
|
|
462
|
+
// Both are idempotent on the next start(): cron register() clears + restarts,
|
|
463
|
+
// and the queue registry dedupes by name + the old loops are already stopped,
|
|
464
|
+
// so a start()/stop()/start() cycle never double-registers.
|
|
465
|
+
try {
|
|
466
|
+
const { getCronDriver } = require('./libs/drivers/cron');
|
|
467
|
+
getCronDriver().stop();
|
|
468
|
+
} catch (err) {
|
|
469
|
+
logger.error('cron teardown failed on stop', { error: err });
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const { stopAllQueues } = require('./libs/queue/runtime');
|
|
473
|
+
stopAllQueues();
|
|
474
|
+
} catch (err) {
|
|
475
|
+
logger.error('queue teardown failed on stop', { error: err });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// clear the guard so a later start() rebuilds the background services cleanly.
|
|
479
|
+
servicesStarted = false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The W2 §1 factory. Synchronous on purpose: hosts get a fully assembled
|
|
484
|
+
* service object and decide when to listen / start. No partial
|
|
485
|
+
* initialization on validation failure.
|
|
486
|
+
*/
|
|
487
|
+
export function createEmbeddedPaymentService(slots: PaymentCoreSlots): EmbeddedPaymentService {
|
|
488
|
+
if (!slots || typeof slots !== 'object') throw new PaymentCoreSlotError('config');
|
|
489
|
+
if (!slots.config) throw new PaymentCoreSlotError('config');
|
|
490
|
+
if (!slots.db || !slots.db.sequelize) throw new PaymentCoreSlotError('db');
|
|
491
|
+
// D1 (S3.0): validate the tenancy slot and derive the effective config BEFORE
|
|
492
|
+
// any setCoreConfig — so a malformed/contradictory slot fails fast without
|
|
493
|
+
// writing a half-baked mode into the config boundary.
|
|
494
|
+
let effectiveConfig = slots.config;
|
|
495
|
+
if (slots.tenancy) {
|
|
496
|
+
const slotMode = (slots.tenancy as { mode?: unknown }).mode;
|
|
497
|
+
if (typeof slotMode !== 'string' || !VALID_TENANT_MODES.includes(slotMode as any)) {
|
|
498
|
+
throw new TenancySlotError(
|
|
499
|
+
'INVALID_TENANCY_MODE',
|
|
500
|
+
`tenancy.mode "${String(slotMode)}" is invalid — expected one of: ${VALID_TENANT_MODES.join(', ')}`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
// slotMode is validated above; use slots.tenancy.mode for the branches below
|
|
504
|
+
// so TypeScript narrows the TenancySlot union (instanceDid only on single).
|
|
505
|
+
if (slots.tenancy.mode === 'single' && !slots.tenancy.instanceDid) {
|
|
506
|
+
throw new TenantError(TENANT_CONTEXT_MISSING, 'tenancy.mode=single requires instanceDid');
|
|
507
|
+
}
|
|
508
|
+
// intending multi must never silently degrade to single: the default
|
|
509
|
+
// identity driver resolves every host to the deployment app DID and the
|
|
510
|
+
// default secrets driver uses a single process key — both are single-tenant
|
|
511
|
+
// behaviors. A multi-mode host that omits either slot fails closed.
|
|
512
|
+
if (slots.tenancy.mode === 'multi') {
|
|
513
|
+
if (!slots.identity) throw new PaymentCoreSlotError('identity');
|
|
514
|
+
if (!slots.secrets) throw new PaymentCoreSlotError('secrets');
|
|
515
|
+
}
|
|
516
|
+
// config.PAYMENT_TENANT_MODE that disagrees with the slot is a conflict —
|
|
517
|
+
// fail fast rather than silently honoring one source.
|
|
518
|
+
const configMode = slots.config.PAYMENT_TENANT_MODE;
|
|
519
|
+
if (configMode !== undefined && configMode !== null && String(configMode) !== slotMode) {
|
|
520
|
+
throw new TenancySlotError(
|
|
521
|
+
'TENANCY_MODE_CONFLICT',
|
|
522
|
+
`tenancy.mode "${slotMode}" conflicts with config.PAYMENT_TENANT_MODE "${String(configMode)}" — set only one`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
// the slot wins: effectiveConfig carries the slot mode through the boundary
|
|
526
|
+
// so getTenantMode() sees it with no env.
|
|
527
|
+
effectiveConfig = { ...slots.config, PAYMENT_TENANT_MODE: slotMode };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Phase 8 (W2′): make the config slot authoritative — every core read goes
|
|
531
|
+
// through libs/env.ts, which now prefers this injected config over process.env.
|
|
532
|
+
const { setCoreConfig, readConfig } = require('./libs/env');
|
|
533
|
+
setCoreConfig(effectiveConfig);
|
|
534
|
+
|
|
535
|
+
// Fail-fast on a missing REQUIRED config field (not a silent default): a
|
|
536
|
+
// single-mode deployment cannot establish its default tenant without an app
|
|
537
|
+
// DID. Multi mode resolves the tenant per request via the identity slot, so
|
|
538
|
+
// BLOCKLET_APP_PID is not required there.
|
|
539
|
+
const singleMode = !slots.tenancy || slots.tenancy.mode === 'single';
|
|
540
|
+
const hasSingleTenantId = Boolean((slots.tenancy as any)?.instanceDid) || Boolean(readConfig('BLOCKLET_APP_PID'));
|
|
541
|
+
if (singleMode && !hasSingleTenantId) {
|
|
542
|
+
throw new MissingConfigError('BLOCKLET_APP_PID');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// model binding — only after validation passed (no partial init)
|
|
546
|
+
const { initialize } = require('./store/models');
|
|
547
|
+
initialize(slots.db.sequelize);
|
|
548
|
+
|
|
549
|
+
// locks slot (Phase 8): inject the locks driver if the host provides one;
|
|
550
|
+
// otherwise the lock facade keeps its default in-process memory driver. The
|
|
551
|
+
// worker passes the D1 locks driver here.
|
|
552
|
+
if (slots.locks) {
|
|
553
|
+
const { setLocksDriver } = require('./libs/lock');
|
|
554
|
+
setLocksDriver(slots.locks);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// queue / cron slots (Phase 9): inject the host flush hook + cron driver if
|
|
558
|
+
// provided; otherwise the Node defaults apply (no-op flush, node-cron
|
|
559
|
+
// registry). The worker injects flush-before-response hooks and the cf-cron
|
|
560
|
+
// driver here.
|
|
561
|
+
const { setQueueHostHooks, setCronDriver, setIdentityDriver, setSecretsDriver } = require('./libs/drivers');
|
|
562
|
+
if (slots.queue) setQueueHostHooks(slots.queue);
|
|
563
|
+
if (slots.cron) setCronDriver(slots.cron);
|
|
564
|
+
|
|
565
|
+
// secrets slot (Phase 11): the host injects a per-tenant keyring driver for
|
|
566
|
+
// multi-tenant; the default driver wraps the process @blocklet/sdk security
|
|
567
|
+
// (single key — Blocklet Server unchanged, existing ciphertext decryptable).
|
|
568
|
+
if (slots.secrets) setSecretsDriver(slots.secrets);
|
|
569
|
+
|
|
570
|
+
// identity slot (Phase 10): the host injects a Host->tenant resolver for
|
|
571
|
+
// multi-tenant; the default driver resolves every host to the deployment app
|
|
572
|
+
// DID (single mode, unchanged Blocklet Server behavior).
|
|
573
|
+
if (slots.identity) setIdentityDriver(slots.identity);
|
|
574
|
+
|
|
575
|
+
// tenancy slot (Phase 10): a single-mode host declares its tenant identity
|
|
576
|
+
// explicitly; wire it into the default-tenant getter so it is not silently
|
|
577
|
+
// ignored (env app DID remains the fallback when no slot value is given).
|
|
578
|
+
if (slots.tenancy && slots.tenancy.mode === 'single' && slots.tenancy.instanceDid) {
|
|
579
|
+
const { setDefaultInstanceDid } = require('./libs/tenant');
|
|
580
|
+
setDefaultInstanceDid(slots.tenancy.instanceDid);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// D6: wire the optional multi-mode tenant enumeration hook (start() bootstraps
|
|
584
|
+
// each enumerated tenant). Absent => the host bootstraps per tenant on demand.
|
|
585
|
+
listInstanceDidsHook = slots.tenancy && slots.tenancy.mode === 'multi' ? slots.tenancy.listInstanceDids : undefined;
|
|
586
|
+
|
|
587
|
+
// Lazy, memoized layer construction (Option 3 seam). Assembly above already
|
|
588
|
+
// ran (config/db/slots) — that is the factory's eager contract. The HTTP
|
|
589
|
+
// layers are built on first access so a workerd host that only reads
|
|
590
|
+
// `http.resourceRoutes` never constructs the full node app (static/fallback),
|
|
591
|
+
// and a node host that reads `handler`/`fetch` reuses the same hono instance.
|
|
592
|
+
const memo = <T>(build: () => T): (() => T) => {
|
|
593
|
+
let value: T | undefined;
|
|
594
|
+
return () => {
|
|
595
|
+
value ??= build();
|
|
596
|
+
return value;
|
|
597
|
+
};
|
|
598
|
+
};
|
|
599
|
+
// Phase 4: the CF worker's embeddable surface — /api/healthz + migrated
|
|
600
|
+
// resources on a standalone hono app (no app-shell static/fallback, no connect).
|
|
601
|
+
const getResourceRoutes = memo(buildResourceRoutesHono);
|
|
602
|
+
// The DID-Connect handlers on a hono app for the full node shell.
|
|
603
|
+
const getConnectRoutesHono = memo(buildConnectRoutesHono);
|
|
604
|
+
// Phase 4: the full node hono app — the only entry. Memoized so `service.fetch`
|
|
605
|
+
// /`handler` are a stable instance for serve({ fetch }). configureNativePipeline
|
|
606
|
+
// + mountMigratedResources (lazily required to keep this module's import
|
|
607
|
+
// side-effect-free) build the native resource group; getConnectRoutesHono mounts
|
|
608
|
+
// DID-Connect; production static/SPA fallback are wired inside buildHonoApp.
|
|
609
|
+
const getHonoApp = memo(() => {
|
|
610
|
+
// eslint-disable-next-line global-require
|
|
611
|
+
const { configureNativePipeline, fullPipeline } = require('./middlewares/hono/pipeline');
|
|
612
|
+
// eslint-disable-next-line global-require
|
|
613
|
+
const { mountMigratedResources } = require('./routes/hono');
|
|
614
|
+
return buildHonoApp((native: Hono) => {
|
|
615
|
+
configureNativePipeline(native); // forked app-shell middleware (+ test stub)
|
|
616
|
+
mountMigratedResources(native, { appShell: fullPipeline() }); // full app-shell on the node host
|
|
617
|
+
}, getConnectRoutesHono);
|
|
618
|
+
});
|
|
619
|
+
// D5: the base-strip Fetch adapter wraps the full hono app (arc consumer). Lazy
|
|
620
|
+
// require + memo so the adapter is built only on the first http.fetch call.
|
|
621
|
+
let fetchHandler: FetchHandler | null = null;
|
|
622
|
+
const getFetchHandler = (): FetchHandler => {
|
|
623
|
+
if (fetchHandler) return fetchHandler;
|
|
624
|
+
// eslint-disable-next-line global-require
|
|
625
|
+
const { createFetchHandler } = require('./libs/http-fetch-adapter');
|
|
626
|
+
const h: FetchHandler = createFetchHandler(getHonoApp());
|
|
627
|
+
fetchHandler = h;
|
|
628
|
+
return h;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const rpc = buildRpc();
|
|
632
|
+
const lifecycle: PaymentCoreLifecycle = {
|
|
633
|
+
start: startBackgroundServices,
|
|
634
|
+
async stop() {
|
|
635
|
+
await stopBackgroundServices();
|
|
636
|
+
// Phase 4: the loopback server is gone, so there is no socket to close. The
|
|
637
|
+
// base-strip adapter's close() is a no-op; reset the memo for symmetry.
|
|
638
|
+
if (fetchHandler) {
|
|
639
|
+
const h = fetchHandler;
|
|
640
|
+
fetchHandler = null;
|
|
641
|
+
await h.close();
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
// Phase 4: handler IS the full node hono app (same instance as `fetch`).
|
|
648
|
+
get handler() {
|
|
649
|
+
return getHonoApp();
|
|
650
|
+
},
|
|
651
|
+
// The hono app the blocklet server shell serves via @hono/node-server.
|
|
652
|
+
fetch(request: Request): Response | Promise<Response> {
|
|
653
|
+
return getHonoApp().fetch(request);
|
|
654
|
+
},
|
|
655
|
+
http: {
|
|
656
|
+
get resourceRoutes() {
|
|
657
|
+
return getResourceRoutes();
|
|
658
|
+
},
|
|
659
|
+
fetch(request: Request, opts?: PaymentFetchOptions): Promise<Response> {
|
|
660
|
+
return getFetchHandler()(request, opts);
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
rpc,
|
|
664
|
+
lifecycle,
|
|
665
|
+
bootstrapTenant,
|
|
666
|
+
};
|
|
667
|
+
}
|