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,175 @@
|
|
|
1
|
+
// Phase 12b (W2′): the core queue RUNTIME surface.
|
|
2
|
+
//
|
|
3
|
+
// Decision (2026-06-12, build-phases 12b, option A): the node engine
|
|
4
|
+
// (api/src/libs/queue) is the ONE canonical queue semantics for every runtime.
|
|
5
|
+
// A host swaps only the TRIGGER (CF scheduled() vs the in-process poll loop),
|
|
6
|
+
// the EXECUTOR (fastq shim vs real fastq) and FLUSH (drain-before-response vs
|
|
7
|
+
// no-op). It NEVER forks the engine — the old cloudflare/shims/queue.ts
|
|
8
|
+
// duplicate engine is dead under the canonical build.ts (nothing populates its
|
|
9
|
+
// registry there) and is removed in 12c.
|
|
10
|
+
//
|
|
11
|
+
// This module is the seam the worker drives instead of reaching into
|
|
12
|
+
// shims/queue internals:
|
|
13
|
+
// - a queue REGISTRY (each createQueue registers its handle + due-dispatch)
|
|
14
|
+
// - dispatchDueJobs() — host-driven scheduled re-dispatch (workerd trigger =
|
|
15
|
+
// CF scheduled(); on node the per-queue loop() does the same on a timer)
|
|
16
|
+
// - flushQueueWork() — drain in-flight push/execution work before the
|
|
17
|
+
// isolate freezes (no-op on a long-lived node process)
|
|
18
|
+
//
|
|
19
|
+
// Runtime modes:
|
|
20
|
+
// 'node' — long-lived process: each scheduled queue's loop() polls D1.
|
|
21
|
+
// 'workerd' — frozen isolate: loop() is disabled; the host calls
|
|
22
|
+
// dispatchDueJobs() from scheduled() and flushQueueWork() before
|
|
23
|
+
// returning the response.
|
|
24
|
+
|
|
25
|
+
export type QueueRuntimeMode = 'node' | 'workerd';
|
|
26
|
+
|
|
27
|
+
let mode: QueueRuntimeMode = 'node';
|
|
28
|
+
|
|
29
|
+
export function setQueueRuntimeMode(next: QueueRuntimeMode): void {
|
|
30
|
+
mode = next;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getQueueRuntimeMode(): QueueRuntimeMode {
|
|
34
|
+
return mode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RegisteredQueue {
|
|
38
|
+
name: string;
|
|
39
|
+
/** whether this queue accepts delayed/scheduled jobs (gates due-dispatch) */
|
|
40
|
+
enableScheduledJob: boolean;
|
|
41
|
+
/** the public queue handle (push / pushAndWait / cancel / get / ...) */
|
|
42
|
+
handle: any;
|
|
43
|
+
/**
|
|
44
|
+
* Re-deliver this queue's due delayed rows once — the body of the node
|
|
45
|
+
* loop(). The host (CF scheduled()) calls this through dispatchDueJobs();
|
|
46
|
+
* the node loop() calls it on its own timer. Same code path either way.
|
|
47
|
+
*/
|
|
48
|
+
redispatchDue: () => Promise<{ dispatched: number; failed: number }>;
|
|
49
|
+
/**
|
|
50
|
+
* D2 teardown: stop this queue's node poll loop (clears its sleep timer). The
|
|
51
|
+
* host (arc/Node) calls stopAllQueues() on lifecycle.stop() so no poll timer
|
|
52
|
+
* survives a stop / ARC_PAYMENT toggle. No-op on workerd (no loop runs).
|
|
53
|
+
*/
|
|
54
|
+
stop?: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const registry = new Map<string, RegisteredQueue>();
|
|
58
|
+
|
|
59
|
+
export function registerQueue(entry: RegisteredQueue): void {
|
|
60
|
+
registry.set(entry.name, entry);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** the queue handle the worker's queue() consumer looks up by name (no more shim registry) */
|
|
64
|
+
export function getQueueHandler(name: string): any | undefined {
|
|
65
|
+
return registry.get(name)?.handle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getAllQueueNames(): string[] {
|
|
69
|
+
return Array.from(registry.keys());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Host-driven scheduled dispatch (the workerd trigger is CF scheduled()). Runs
|
|
74
|
+
* every registered scheduled queue's due-row re-dispatch once. On a node host
|
|
75
|
+
* the per-queue loop() does exactly this on a timer; a workerd host calls this
|
|
76
|
+
* explicitly because it cannot run a background timer in a frozen isolate.
|
|
77
|
+
*/
|
|
78
|
+
export async function dispatchDueJobs(): Promise<{ dispatched: number; failed: number; queues: string[] }> {
|
|
79
|
+
let dispatched = 0;
|
|
80
|
+
let failed = 0;
|
|
81
|
+
const queues: string[] = [];
|
|
82
|
+
for (const entry of registry.values()) {
|
|
83
|
+
// eslint-disable-next-line no-continue -- skip non-scheduled queues in the dispatch loop
|
|
84
|
+
if (!entry.enableScheduledJob) continue;
|
|
85
|
+
try {
|
|
86
|
+
// eslint-disable-next-line no-await-in-loop -- queues dispatch sequentially within a tick
|
|
87
|
+
const r = await entry.redispatchDue();
|
|
88
|
+
dispatched += r.dispatched;
|
|
89
|
+
failed += r.failed;
|
|
90
|
+
if (r.dispatched > 0 || r.failed > 0) queues.push(entry.name);
|
|
91
|
+
} catch {
|
|
92
|
+
failed += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { dispatched, failed, queues };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* D2 teardown: stop every registered queue's node poll loop. The Node host
|
|
100
|
+
* (arc) calls this from lifecycle.stop() so no background poll timer survives a
|
|
101
|
+
* stop / ARC_PAYMENT toggle. The registry entries stay (a later start() rebuilds
|
|
102
|
+
* the loops by recreating the queues). Idempotent — safe to call when stopped.
|
|
103
|
+
*/
|
|
104
|
+
export function stopAllQueues(): void {
|
|
105
|
+
for (const entry of registry.values()) {
|
|
106
|
+
try {
|
|
107
|
+
entry.stop?.();
|
|
108
|
+
} catch {
|
|
109
|
+
/* a queue already stopped — ignore */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** stop every queue loop AND clear the registry (full reset). */
|
|
115
|
+
export function disposeQueues(): void {
|
|
116
|
+
stopAllQueues();
|
|
117
|
+
registry.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- in-flight push tracking (workerd flush) ---
|
|
121
|
+
// On node the process stays alive and fastq drains naturally; nothing is
|
|
122
|
+
// tracked (zero overhead, byte-identical behavior). On workerd the isolate is
|
|
123
|
+
// torn down after the response, so the host must await in-flight executions
|
|
124
|
+
// before it returns — otherwise an immediate push is silently dropped.
|
|
125
|
+
const pending = new Set<Promise<any>>();
|
|
126
|
+
|
|
127
|
+
export function trackPending(p: Promise<any>): void {
|
|
128
|
+
if (mode !== 'workerd') return;
|
|
129
|
+
const wrapped = Promise.resolve(p).catch(() => undefined);
|
|
130
|
+
pending.add(wrapped);
|
|
131
|
+
wrapped.then(() => {
|
|
132
|
+
pending.delete(wrapped);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Drain in-flight push/execution work. The CF worker calls this before
|
|
138
|
+
* returning the HTTP response and at the end of scheduled()/queue() so no
|
|
139
|
+
* immediate push is lost when the isolate freezes. No-op on a node host.
|
|
140
|
+
*/
|
|
141
|
+
export async function flushQueueWork(): Promise<void> {
|
|
142
|
+
if (mode !== 'workerd') return;
|
|
143
|
+
const MAX_ITERATIONS = 10;
|
|
144
|
+
// Wall-clock guard: a misbehaving handler whose fastq callback never fires
|
|
145
|
+
// must not block the isolate's response forever. The D1 jobs row is the
|
|
146
|
+
// source of truth, so anything still pending past the budget is re-dispatched
|
|
147
|
+
// on the next scheduled() tick rather than lost.
|
|
148
|
+
const deadline = Date.now() + 5000;
|
|
149
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
150
|
+
if (pending.size === 0) break;
|
|
151
|
+
const remaining = deadline - Date.now();
|
|
152
|
+
if (remaining <= 0) break;
|
|
153
|
+
const batch = Array.from(pending);
|
|
154
|
+
// eslint-disable-next-line no-await-in-loop
|
|
155
|
+
await Promise.race([
|
|
156
|
+
Promise.allSettled(batch),
|
|
157
|
+
new Promise<void>((resolve) => {
|
|
158
|
+
setTimeout(resolve, remaining);
|
|
159
|
+
}),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Exported for unit tests only — not part of the host-facing surface.
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention -- test-only export sentinel
|
|
166
|
+
export const __test__ = {
|
|
167
|
+
reset() {
|
|
168
|
+
registry.clear();
|
|
169
|
+
pending.clear();
|
|
170
|
+
mode = 'node';
|
|
171
|
+
},
|
|
172
|
+
registrySize() {
|
|
173
|
+
return registry.size;
|
|
174
|
+
},
|
|
175
|
+
};
|
package/api/src/libs/resource.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { fromTokenToUnit } from '@ocap/util';
|
|
|
10
10
|
|
|
11
11
|
import { updatePassportExtra } from '../integrations/blocklet/passport';
|
|
12
12
|
import { replace } from '../locales';
|
|
13
|
-
import { createPaymentLink } from '../routes/payment-links';
|
|
14
|
-
import { createPrice } from '../routes/prices';
|
|
15
|
-
import { createProductAndPrices } from '../routes/products';
|
|
13
|
+
import { createPaymentLink } from '../routes/hono/payment-links';
|
|
14
|
+
import { createPrice } from '../routes/hono/prices';
|
|
15
|
+
import { createProductAndPrices } from '../routes/hono/products';
|
|
16
16
|
import { PaymentCurrency, PaymentLink, Price, Product, nextPriceId } from '../store/models';
|
|
17
17
|
|
|
18
18
|
export async function getPackResource(type: string) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Phase 11 (W2-3): tenant-aware secrets facade.
|
|
2
|
+
//
|
|
3
|
+
// Replaces direct `security.encrypt/decrypt` calls in the payment hot path. The
|
|
4
|
+
// tenant is resolved from the TenantContext (single point), so call sites do
|
|
5
|
+
// not change signature. single mode -> the default secrets driver (process
|
|
6
|
+
// key), unchanged; multi mode -> the keyring driver keyed per tenant.
|
|
7
|
+
//
|
|
8
|
+
// Sync surface: the payment loop (PaymentMethod.encrypt/decrypt Settings,
|
|
9
|
+
// getStripeClient, ~50 sync callers) stays synchronous. The keyring driver's
|
|
10
|
+
// sync path requires the tenant key to be warmed (the worker shell warms it at
|
|
11
|
+
// request entry); the default driver's process key is always available.
|
|
12
|
+
|
|
13
|
+
import { getInstanceDid } from './context';
|
|
14
|
+
import { getSecretsDriver } from './drivers/secrets';
|
|
15
|
+
|
|
16
|
+
/** Encrypt a value under the current tenant's key (sync hot path). */
|
|
17
|
+
export function encryptSecret(value: string): string {
|
|
18
|
+
return getSecretsDriver().encryptSync(getInstanceDid(), value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Decrypt a value under the current tenant's key (sync hot path). */
|
|
22
|
+
export function decryptSecret(value: string): string {
|
|
23
|
+
return getSecretsDriver().decryptSync(getInstanceDid(), value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Async per-tenant variants (lazy EK fetch + retry) for non-hot-path callers. */
|
|
27
|
+
export function encryptSecretAsync(value: string): Promise<string> {
|
|
28
|
+
return getSecretsDriver().encrypt(getInstanceDid(), value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function decryptSecretAsync(value: string): Promise<string> {
|
|
32
|
+
return getSecretsDriver().decrypt(getInstanceDid(), value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pre-resolve the current tenant's key so the sync path is a cache hit. */
|
|
36
|
+
export function warmupSecrets(instanceDid?: string): Promise<void> {
|
|
37
|
+
return getSecretsDriver().warmup(instanceDid ?? getInstanceDid());
|
|
38
|
+
}
|
package/api/src/libs/session.ts
CHANGED
|
@@ -6,6 +6,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|
|
6
6
|
import isEqual from 'lodash/isEqual';
|
|
7
7
|
import pAll from 'p-all';
|
|
8
8
|
import omit from 'lodash/omit';
|
|
9
|
+
import { paymentBillingThreshold, paymentMinStakeAmount } from './env';
|
|
9
10
|
import dayjs from './dayjs';
|
|
10
11
|
import { validCoupon } from './discount/coupon';
|
|
11
12
|
import { getPriceUintAmountByCurrency, getPriceCurrencyOptions } from './price';
|
|
@@ -425,7 +426,7 @@ export function getBillingThreshold(config: Record<string, any> = {}) {
|
|
|
425
426
|
}
|
|
426
427
|
}
|
|
427
428
|
|
|
428
|
-
const threshold =
|
|
429
|
+
const threshold = paymentBillingThreshold();
|
|
429
430
|
if (threshold > 0) {
|
|
430
431
|
return threshold;
|
|
431
432
|
}
|
|
@@ -441,7 +442,7 @@ export function getMinStakeAmount(config: Record<string, any> = {}) {
|
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
444
|
|
|
444
|
-
const threshold =
|
|
445
|
+
const threshold = paymentMinStakeAmount();
|
|
445
446
|
if (threshold > 0) {
|
|
446
447
|
return threshold;
|
|
447
448
|
}
|
|
@@ -8,6 +8,7 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
8
8
|
import { withQuery } from 'ufo';
|
|
9
9
|
|
|
10
10
|
import { Op } from 'sequelize';
|
|
11
|
+
import env, { paymentDaysUntilDue, paymentDaysUntilCancel } from './env';
|
|
11
12
|
import {
|
|
12
13
|
ChainType,
|
|
13
14
|
Customer,
|
|
@@ -27,9 +28,8 @@ import {
|
|
|
27
28
|
TLineItemExpanded,
|
|
28
29
|
UsageRecord,
|
|
29
30
|
} from '../store/models';
|
|
30
|
-
import { createEvent } from './audit';
|
|
31
|
+
import { createEvent, reportAuditFailure } from './audit';
|
|
31
32
|
import dayjs from './dayjs';
|
|
32
|
-
import env from './env';
|
|
33
33
|
import logger from './logger';
|
|
34
34
|
import { getExchangeRateService } from './exchange-rate';
|
|
35
35
|
import { getExchangeRateSymbol } from './exchange-rate/token-address-mapping';
|
|
@@ -107,11 +107,11 @@ export function parseIntegerConfig(alternatives: any[], defaultValue: number) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export function getDaysUntilDue(query: Record<string, any> = {}) {
|
|
110
|
-
return parseIntegerConfig([query.days_until_due,
|
|
110
|
+
return parseIntegerConfig([query.days_until_due, paymentDaysUntilDue()], 6);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export function getDaysUntilCancel(query: Record<string, any> = {}) {
|
|
114
|
-
return parseIntegerConfig([query.days_until_cancel,
|
|
114
|
+
return parseIntegerConfig([query.days_until_cancel, paymentDaysUntilCancel()], 0);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
export const getDueUnit = (interval: string) => {
|
|
@@ -821,7 +821,7 @@ export async function finalizeStripeSubscriptionUpdate({
|
|
|
821
821
|
await Lock.acquire(`${subscription.id}-change-plan`, releaseAt);
|
|
822
822
|
logger.info('subscription plan change lock acquired on finalize', { subscription: subscription.id, releaseAt });
|
|
823
823
|
|
|
824
|
-
createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(
|
|
824
|
+
createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(reportAuditFailure);
|
|
825
825
|
}
|
|
826
826
|
|
|
827
827
|
logger.info('subscription update finalized', { subscription: subscription.id, updates, items });
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { tenantModeRaw, blockletAppPid } from './env';
|
|
2
|
+
|
|
3
|
+
export type TenantMode = 'single' | 'multi';
|
|
4
|
+
|
|
5
|
+
export const TENANT_CONTEXT_MISSING = 'TENANT_CONTEXT_MISSING';
|
|
6
|
+
export const TENANT_MISMATCH = 'TENANT_MISMATCH';
|
|
7
|
+
// programmer error: a scoped helper was pointed at a non-tenant model —
|
|
8
|
+
// distinct from TENANT_MISMATCH so monitoring can tell data races from bugs
|
|
9
|
+
export const INVALID_TENANT_TABLE = 'INVALID_TENANT_TABLE';
|
|
10
|
+
// multi-tenant fail-closed: a request Host did not resolve to any tenant
|
|
11
|
+
// (Phase 10) — the request is refused 4xx with no default-tenant fallback
|
|
12
|
+
export const TENANT_HOST_UNRESOLVED = 'TENANT_HOST_UNRESOLVED';
|
|
13
|
+
|
|
14
|
+
export type TenantErrorCode =
|
|
15
|
+
| typeof TENANT_CONTEXT_MISSING
|
|
16
|
+
| typeof TENANT_MISMATCH
|
|
17
|
+
| typeof INVALID_TENANT_TABLE
|
|
18
|
+
| typeof TENANT_HOST_UNRESOLVED;
|
|
19
|
+
|
|
20
|
+
export class TenantError extends Error {
|
|
21
|
+
code: TenantErrorCode;
|
|
22
|
+
|
|
23
|
+
constructor(code: TenantErrorCode, message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'TenantError';
|
|
26
|
+
this.code = code;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tenant mode of this deployment:
|
|
32
|
+
* - `single` (default, blocklet server legacy): tenant context falls back to the deployment's own app DID
|
|
33
|
+
* - `multi`: no fallback — missing tenant context is a fail-closed error
|
|
34
|
+
*
|
|
35
|
+
* Source of the mode is the existing env mechanism for now; Phase 12 converges it into the config slot.
|
|
36
|
+
*/
|
|
37
|
+
export function getTenantMode(): TenantMode {
|
|
38
|
+
// Phase 8: mode-source reads the injected config (libs/env boundary), falling
|
|
39
|
+
// back to process.env. Not request-tamperable — config is set once at factory
|
|
40
|
+
// init, never from request input.
|
|
41
|
+
return tenantModeRaw() === 'multi' ? 'multi' : 'single';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Single source of truth for "this deployment's app DID" (single-tenant default tenant).
|
|
46
|
+
* Phase 2 backfill values and Phase 10 `tenancy.instanceDid` must reuse this getter.
|
|
47
|
+
*/
|
|
48
|
+
// Phase 10: the single-mode tenancy slot value, when the host supplies one via
|
|
49
|
+
// createEmbeddedPaymentService({ tenancy: { mode:'single', instanceDid } }).
|
|
50
|
+
// Preferred over the env snapshot so a host can declare its single-tenant
|
|
51
|
+
// identity explicitly (and so the slot is not silently ignored).
|
|
52
|
+
let overrideDefaultInstanceDid: string | undefined;
|
|
53
|
+
|
|
54
|
+
/** Wire the single-mode tenancy slot value (factory only). Pass undefined to clear. */
|
|
55
|
+
export function setDefaultInstanceDid(did: string | undefined): void {
|
|
56
|
+
if (did !== undefined) assertValidInstanceDid(did);
|
|
57
|
+
overrideDefaultInstanceDid = did;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getDefaultInstanceDid(): string {
|
|
61
|
+
// explicit tenancy slot value wins; otherwise the app DID from the injected
|
|
62
|
+
// config (libs/env boundary), which itself falls back to process.env's
|
|
63
|
+
// BLOCKLET_APP_PID (the value @blocklet/sdk's env.appPid wrapped before Phase 8).
|
|
64
|
+
const did = overrideDefaultInstanceDid || blockletAppPid() || '';
|
|
65
|
+
if (!did) {
|
|
66
|
+
throw new TenantError(TENANT_CONTEXT_MISSING, 'app DID is not configured for this deployment');
|
|
67
|
+
}
|
|
68
|
+
return did;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Deliberately loose: deployments use both bare addresses (z8iZ...) and
|
|
72
|
+
// did:abt: URIs, so we only reject values that cannot be a DID at all
|
|
73
|
+
// (non-strings, empty/whitespace, embedded whitespace). Strict format
|
|
74
|
+
// enforcement belongs to the identity slot (Phase 10), which resolves
|
|
75
|
+
// Host -> instanceDid from a trusted source.
|
|
76
|
+
/**
|
|
77
|
+
* Tenant of a loaded row, fail-closed: rows written since Phase 2 always
|
|
78
|
+
* carry instance_did; a NULL means pre-backfill data, which is only legal in
|
|
79
|
+
* single mode (where it can only belong to the default tenant).
|
|
80
|
+
*/
|
|
81
|
+
export function resolveRowTenant(row: { instance_did?: string | null } | null | undefined): string {
|
|
82
|
+
const fromRow = row?.instance_did;
|
|
83
|
+
if (fromRow) return fromRow;
|
|
84
|
+
if (getTenantMode() === 'single') return getDefaultInstanceDid();
|
|
85
|
+
throw new TenantError(TENANT_CONTEXT_MISSING, 'row has no tenant and deployment is in multi mode');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function assertValidInstanceDid(instanceDid: unknown): asserts instanceDid is string {
|
|
89
|
+
if (typeof instanceDid !== 'string' || !/^\S{3,}$/.test(instanceDid)) {
|
|
90
|
+
throw new TenantError(TENANT_CONTEXT_MISSING, `invalid instanceDid: ${JSON.stringify(instanceDid)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/api/src/libs/url.ts
CHANGED
|
@@ -30,7 +30,7 @@ export async function formatToShortUrl({
|
|
|
30
30
|
validUntil?: string;
|
|
31
31
|
maxVisits?: number;
|
|
32
32
|
}): Promise<string> {
|
|
33
|
-
const apiKey = shortUrlApiKey;
|
|
33
|
+
const apiKey = shortUrlApiKey();
|
|
34
34
|
|
|
35
35
|
if (!apiKey) {
|
|
36
36
|
return url;
|
|
@@ -43,14 +43,14 @@ export async function formatToShortUrl({
|
|
|
43
43
|
maxVisits,
|
|
44
44
|
tags: [],
|
|
45
45
|
shortCodeLength: 8,
|
|
46
|
-
domain: shortUrlDomain,
|
|
46
|
+
domain: shortUrlDomain(),
|
|
47
47
|
findIfExists: true,
|
|
48
48
|
validateUrl: true,
|
|
49
49
|
forwardQuery: true,
|
|
50
50
|
crawlable: true,
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const response = await fetch(`https://${shortUrlDomain}/rest/v3/short-urls`, {
|
|
53
|
+
const response = await fetch(`https://${shortUrlDomain()}/rest/v3/short-urls`, {
|
|
54
54
|
method: 'POST',
|
|
55
55
|
headers: {
|
|
56
56
|
'Content-Type': 'application/json',
|
package/api/src/libs/util.ts
CHANGED
|
@@ -10,10 +10,10 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
10
10
|
import { joinURL, withQuery, withTrailingSlash } from 'ufo';
|
|
11
11
|
|
|
12
12
|
import axios from 'axios';
|
|
13
|
-
import { ethers } from 'ethers';
|
|
14
13
|
import { fromUnitToToken } from '@ocap/util';
|
|
15
14
|
import get from 'lodash/get';
|
|
16
15
|
import trimEnd from 'lodash/trimEnd';
|
|
16
|
+
import { googlePlayWebhookUrl, blockletAppUrl, blockletMountPoints, blockletAppId, blockletAppName } from './env';
|
|
17
17
|
import dayjs from './dayjs';
|
|
18
18
|
import { blocklet, wallet } from './auth';
|
|
19
19
|
import type { PaymentCurrency, PaymentMethod, Subscription } from '../store/models';
|
|
@@ -37,7 +37,7 @@ export const STRIPE_ENDPOINT: string = getUrl('/api/integrations/stripe/webhook'
|
|
|
37
37
|
// Lazy-eval (function not constant) because dotenv loads env AFTER this module
|
|
38
38
|
// is imported — a constant captured at module-load would only see BLOCKLET_APP_URL.
|
|
39
39
|
export const googlePlayEndpoint = (): string =>
|
|
40
|
-
|
|
40
|
+
googlePlayWebhookUrl() || getUrl('/api/integrations/google-play/webhook');
|
|
41
41
|
|
|
42
42
|
// Back-compat constant for any caller that captures it at module-load.
|
|
43
43
|
// Prefer googlePlayEndpoint() going forward.
|
|
@@ -262,7 +262,7 @@ const cachedBlockletJsonResult = new Map<string, { data: any; expiry: number }>(
|
|
|
262
262
|
const CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
263
263
|
|
|
264
264
|
export async function getBlockletJson(url?: string) {
|
|
265
|
-
const blockletKey = url ||
|
|
265
|
+
const blockletKey = url || blockletAppUrl() || 'default';
|
|
266
266
|
const now = Date.now();
|
|
267
267
|
|
|
268
268
|
if (cachedBlockletJsonResult.has(blockletKey)) {
|
|
@@ -271,7 +271,7 @@ export async function getBlockletJson(url?: string) {
|
|
|
271
271
|
return cached.data;
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
const baseUrl = url ||
|
|
274
|
+
const baseUrl = url || blockletAppUrl();
|
|
275
275
|
if (!baseUrl) {
|
|
276
276
|
return null;
|
|
277
277
|
}
|
|
@@ -282,14 +282,14 @@ export async function getBlockletJson(url?: string) {
|
|
|
282
282
|
return blockletMeta;
|
|
283
283
|
} catch (err) {
|
|
284
284
|
logger.error(`getBlockletJson error for ${scriptUrl}`, err);
|
|
285
|
-
if (
|
|
286
|
-
const BLOCKLET_MOUNT_POINTS = safeJsonParse(
|
|
285
|
+
if (blockletMountPoints()) {
|
|
286
|
+
const BLOCKLET_MOUNT_POINTS = safeJsonParse(blockletMountPoints(), []);
|
|
287
287
|
return {
|
|
288
288
|
componentMountPoints: BLOCKLET_MOUNT_POINTS,
|
|
289
|
-
appId:
|
|
290
|
-
appName:
|
|
289
|
+
appId: blockletAppId(),
|
|
290
|
+
appName: blockletAppName(),
|
|
291
291
|
appLogo: '/.well-known/service/blocklet/logo',
|
|
292
|
-
appUrl:
|
|
292
|
+
appUrl: blockletAppUrl(),
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
return null;
|
|
@@ -313,9 +313,9 @@ export async function getUserOrAppInfo(
|
|
|
313
313
|
if (appInfo) {
|
|
314
314
|
return {
|
|
315
315
|
name: appInfo.name,
|
|
316
|
-
avatar: joinURL(
|
|
316
|
+
avatar: joinURL(blockletAppUrl()!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),
|
|
317
317
|
type: 'dapp',
|
|
318
|
-
url: joinURL(
|
|
318
|
+
url: joinURL(blockletAppUrl()!, appInfo.mountPoint),
|
|
319
319
|
};
|
|
320
320
|
}
|
|
321
321
|
}
|
|
@@ -324,7 +324,7 @@ export async function getUserOrAppInfo(
|
|
|
324
324
|
const locale = get(user, 'locale', 'en');
|
|
325
325
|
return {
|
|
326
326
|
name: user?.fullName,
|
|
327
|
-
avatar: joinURL(
|
|
327
|
+
avatar: joinURL(blockletAppUrl()!, user?.avatar),
|
|
328
328
|
type: 'user',
|
|
329
329
|
url: getCustomerProfileUrl({ userDid: address, locale }),
|
|
330
330
|
};
|
|
@@ -608,7 +608,15 @@ export async function isUserInBlocklist(did: string, paymentMethod: PaymentMetho
|
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
export function resolveAddressChainTypes(address: string): LiteralUnion<'ethereum' | 'base' | 'arcblock', string>[] {
|
|
611
|
-
|
|
611
|
+
// Phase 13b2: lazy ethers. An eager top-level `import 'ethers'` loaded ethers
|
|
612
|
+
// during createEmbeddedPaymentService assembly (libs/util is pulled in at factory
|
|
613
|
+
// time), so a host that force-resolves an incompatible @noble/hashes (e.g. arc's
|
|
614
|
+
// `@noble/hashes:^2.2.0` override vs ethers@6.16's declared 1.3.2) crashed ethers
|
|
615
|
+
// at require. Deferring to call time keeps the factory + rpc.entitlements.check
|
|
616
|
+
// ethers-free; this fn runs only on EVM address resolution.
|
|
617
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
618
|
+
const { isAddress } = require('ethers');
|
|
619
|
+
if (isAddress(address)) {
|
|
612
620
|
return ['ethereum', 'base', 'arcblock'];
|
|
613
621
|
}
|
|
614
622
|
return ['arcblock'];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono fork of @blocklet/sdk/lib/middlewares/cdn.js.
|
|
2
|
+
//
|
|
3
|
+
// The express version monkeypatches res.send to rewrite asset URLs to the CDN
|
|
4
|
+
// host on outgoing HTML. hono responses are built differently, so this fork
|
|
5
|
+
// rewrites in the RESPONSE phase: run the handler, then if the response is HTML
|
|
6
|
+
// (production GET/HEAD, non-resource, html-accepting), read it once and rebuild
|
|
7
|
+
// the Response with rewritten URLs. The transform core (AssetHostTransformer) is
|
|
8
|
+
// REUSED VERBATIM from the SDK — byte-identical rewriting. Inert for JSON /api
|
|
9
|
+
// routes (content-type is not text/html), which is the only surface in Phases
|
|
10
|
+
// 1-3; it becomes load-bearing when SPA HTML serving moves off the bridge.
|
|
11
|
+
import type { MiddlewareHandler } from 'hono';
|
|
12
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
13
|
+
import { AssetHostTransformer } from '@blocklet/sdk/lib/util/asset-host-transformer';
|
|
14
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
15
|
+
import { env } from '@blocklet/sdk/lib/config';
|
|
16
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
17
|
+
import { BLOCKLET_PROXY_PATH_PREFIX } from '@abtnode/constant';
|
|
18
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
19
|
+
import { RESOURCE_PATTERN } from '@blocklet/constant';
|
|
20
|
+
import { nodeEnv, readConfig } from '../../libs/env';
|
|
21
|
+
|
|
22
|
+
function isProductionRuntime(): boolean {
|
|
23
|
+
return nodeEnv() === 'production' || readConfig('ABT_NODE_SERVICE_ENV') === 'production';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Parity with express req.accepts(['html', ...]) for the html family.
|
|
27
|
+
function acceptsHtml(accept: string): boolean {
|
|
28
|
+
if (!accept) return true; // express treats a missing Accept as accept-all
|
|
29
|
+
return accept.includes('text/html') || accept.includes('application/xhtml+xml') || accept.includes('*/*');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shouldProcess(method: string, path: string, accept: string): boolean {
|
|
33
|
+
if (!isProductionRuntime()) return false;
|
|
34
|
+
if (method !== 'GET' && method !== 'HEAD') return false;
|
|
35
|
+
if (path.includes('/.well-known/service/')) return false;
|
|
36
|
+
if (RESOURCE_PATTERN.test(path)) return false;
|
|
37
|
+
return acceptsHtml(accept);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function cdn(): MiddlewareHandler {
|
|
41
|
+
// Lazy + skip-if-absent: a bare/embedded host without componentDid never
|
|
42
|
+
// rewrites (the SDK throws "did is required" eagerly; the fork stays inert).
|
|
43
|
+
let transformer: AssetHostTransformer | undefined;
|
|
44
|
+
return async (c, next) => {
|
|
45
|
+
await next();
|
|
46
|
+
const assetHost = (env as any).assetCdnHost;
|
|
47
|
+
const did = (env as any).componentDid;
|
|
48
|
+
if (!assetHost || !did) return;
|
|
49
|
+
if (!shouldProcess(c.req.method.toUpperCase(), c.req.path, c.req.header('accept') || '')) return;
|
|
50
|
+
|
|
51
|
+
const contentType = c.res.headers.get('content-type') || '';
|
|
52
|
+
if (!contentType.includes('text/html')) return;
|
|
53
|
+
|
|
54
|
+
transformer ??= new AssetHostTransformer(`${BLOCKLET_PROXY_PATH_PREFIX}/${did}/`);
|
|
55
|
+
const html = await c.res.text();
|
|
56
|
+
const transformed = transformer.transform(html, assetHost);
|
|
57
|
+
const rebuilt = new Response(transformed, c.res);
|
|
58
|
+
rebuilt.headers.delete('content-length'); // body length changed; let the server derive it
|
|
59
|
+
c.res = rebuilt;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default cdn;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono fork of api/src/libs/middleware.ts
|
|
2
|
+
// (ensureI18n + contextMiddleware). Behavior is identical to the express
|
|
3
|
+
// version; only the req/res plumbing changes:
|
|
4
|
+
// - req.query.locale / req.t= → c.req.query('locale') / c.set('t', ...)
|
|
5
|
+
// - req.get('x-component-sig') → c.req.header('x-component-sig')
|
|
6
|
+
// - req.body (component sig verify) → c.get('sanitizedBody') (xss is the single
|
|
7
|
+
// body read-point, already ran upstream)
|
|
8
|
+
// - req.headers.host (tenant) → c.req.header('host') (raw Host only, never
|
|
9
|
+
// a proxy header — single tenant resolution)
|
|
10
|
+
// - res.status(400).json(...) → c.json(..., 400) (fail-closed on unknown
|
|
11
|
+
// host in multi mode)
|
|
12
|
+
// - context.run(..., next) → context.run(..., () => next()) (same ALS)
|
|
13
|
+
import type { MiddlewareHandler } from 'hono';
|
|
14
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
15
|
+
import { verify } from '@blocklet/sdk/lib/util/verify-sign';
|
|
16
|
+
import { translate } from '../../locales';
|
|
17
|
+
import { context } from '../../libs/context';
|
|
18
|
+
import { TenantError, TENANT_HOST_UNRESOLVED } from '../../libs/tenant';
|
|
19
|
+
import { resolveTenantForHost } from '../../libs/drivers/identity';
|
|
20
|
+
|
|
21
|
+
export function ensureI18n(): MiddlewareHandler {
|
|
22
|
+
return (c, next) => {
|
|
23
|
+
c.set('locale', String(c.req.query('locale') || 'en'));
|
|
24
|
+
c.set('t', translate);
|
|
25
|
+
return next();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function contextMiddleware(): MiddlewareHandler {
|
|
30
|
+
return async (c, next) => {
|
|
31
|
+
const requestId = c.req.header('x-request-id') || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
32
|
+
let requestedBy = 'system';
|
|
33
|
+
|
|
34
|
+
// component signature — verify against the SANITIZED body (xss ran first)
|
|
35
|
+
const sig = c.req.header('x-component-sig');
|
|
36
|
+
const componentDid = c.req.header('x-component-did');
|
|
37
|
+
if (sig && componentDid) {
|
|
38
|
+
const data = c.get('sanitizedBody') ?? {};
|
|
39
|
+
const verified = await verify(data, sig);
|
|
40
|
+
if (verified) {
|
|
41
|
+
requestedBy = componentDid;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// user DID from headers
|
|
46
|
+
const userDid = c.req.header('x-user-did');
|
|
47
|
+
if (userDid) {
|
|
48
|
+
requestedBy = userDid;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// authenticated user (security middleware set this upstream)
|
|
52
|
+
const user = c.get('user');
|
|
53
|
+
if (user?.did) {
|
|
54
|
+
requestedBy = user.did;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Resolve tenant from the raw Host (single point). Multi-mode unknown host →
|
|
58
|
+
// 4xx fail-closed, no default-tenant fallback.
|
|
59
|
+
let instanceDid: string;
|
|
60
|
+
try {
|
|
61
|
+
instanceDid = await resolveTenantForHost(c.req.header('host'));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err instanceof TenantError && err.code === TENANT_HOST_UNRESOLVED) {
|
|
64
|
+
return c.json({ error: { code: err.code, message: err.message } }, 400);
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return context.run({ requestId, requestedBy, instanceDid }, async () => {
|
|
70
|
+
await next();
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
}
|