payment-kit 1.29.1 → 1.29.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +47 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +41 -37
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/crons/tenant-fanout.ts +82 -0
- package/api/src/host-node/did-connect-runtime-node.ts +33 -0
- package/api/src/host-node/serve-static-arc.ts +68 -0
- package/api/src/host-node/serve-static.ts +41 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +247 -47
- package/api/src/libs/context.ts +89 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
- package/api/src/libs/did-connect/tenant-identity.ts +221 -0
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +142 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +60 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +271 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +80 -0
- package/api/src/middlewares/hono/csrf.ts +83 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +209 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +38 -21
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +41 -11
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +64 -37
- package/api/src/queues/payout.ts +37 -21
- package/api/src/queues/refund.ts +36 -18
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +814 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +82 -23
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
- package/api/tests/crons/tenant-fanout.spec.ts +158 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
- package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/service-host.spec.ts +37 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +292 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
- package/api/tests/service/fail-closed-http.spec.ts +79 -0
- package/api/tests/service/static-arc-handler.spec.ts +101 -0
- package/api/tests/service/static-externalized.spec.ts +48 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
- package/cloudflare/README.md +34 -27
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
- package/cloudflare/build.ts +33 -13
- package/cloudflare/cf-adapter.ts +419 -0
- package/cloudflare/did-connect-runtime.ts +96 -0
- package/cloudflare/did-connect-token-storage.ts +151 -0
- package/cloudflare/esbuild-cf-config.cjs +407 -0
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +33 -403
- package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
- package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/cf-adapter.spec.ts +244 -0
- package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
- package/cloudflare/vite.config.ts +53 -45
- package/cloudflare/worker.ts +261 -448
- package/cloudflare/wrangler.json +0 -6
- package/cloudflare/wrangler.jsonc +0 -6
- package/cloudflare/wrangler.local-e2e.jsonc +25 -0
- package/cloudflare/wrangler.staging.json +0 -6
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/bootstrap-inject.ts +166 -0
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/app.tsx +2 -1
- package/src/env.d.ts +13 -1
- package/src/libs/service-host.ts +13 -0
- package/tsconfig.json +1 -1
- package/vite.arc.config.ts +159 -0
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/did-connect-auth.ts +0 -527
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -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,80 @@
|
|
|
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
|
+
import { warmTenantIdentity } from '../../libs/did-connect/tenant-identity';
|
|
21
|
+
|
|
22
|
+
export function ensureI18n(): MiddlewareHandler {
|
|
23
|
+
return (c, next) => {
|
|
24
|
+
c.set('locale', String(c.req.query('locale') || 'en'));
|
|
25
|
+
c.set('t', translate);
|
|
26
|
+
return next();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function contextMiddleware(): MiddlewareHandler {
|
|
31
|
+
return async (c, next) => {
|
|
32
|
+
const requestId = c.req.header('x-request-id') || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
33
|
+
let requestedBy = 'system';
|
|
34
|
+
|
|
35
|
+
// component signature — verify against the SANITIZED body (xss ran first)
|
|
36
|
+
const sig = c.req.header('x-component-sig');
|
|
37
|
+
const componentDid = c.req.header('x-component-did');
|
|
38
|
+
if (sig && componentDid) {
|
|
39
|
+
const data = c.get('sanitizedBody') ?? {};
|
|
40
|
+
const verified = await verify(data, sig);
|
|
41
|
+
if (verified) {
|
|
42
|
+
requestedBy = componentDid;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// user DID from headers
|
|
47
|
+
const userDid = c.req.header('x-user-did');
|
|
48
|
+
if (userDid) {
|
|
49
|
+
requestedBy = userDid;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// authenticated user (security middleware set this upstream)
|
|
53
|
+
const user = c.get('user');
|
|
54
|
+
if (user?.did) {
|
|
55
|
+
requestedBy = user.did;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Resolve tenant from the raw Host (single point). Multi-mode unknown host →
|
|
59
|
+
// 4xx fail-closed, no default-tenant fallback.
|
|
60
|
+
let instanceDid: string;
|
|
61
|
+
try {
|
|
62
|
+
instanceDid = await resolveTenantForHost(c.req.header('host'));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err instanceof TenantError && err.code === TENANT_HOST_UNRESOLVED) {
|
|
65
|
+
return c.json({ error: { code: err.code, message: err.message } }, 400);
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return context.run({ requestId, requestedBy, instanceDid }, async () => {
|
|
71
|
+
// Warm the tenant's signing identity (arc/CF dynamic runtime only — no-op on
|
|
72
|
+
// blocklet-server) so the synchronous business wallet proxies (libs/auth.ts:
|
|
73
|
+
// `wallet`/`ethWallet`) resolve to THIS tenant's wallet inside the handler.
|
|
74
|
+
// Best-effort: a request that never touches a wallet is not blocked; one that
|
|
75
|
+
// does fails-closed at getCachedTenantIdentity.
|
|
76
|
+
await warmTenantIdentity(instanceDid);
|
|
77
|
+
await next();
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono fork of @blocklet/sdk/lib/middlewares/csrf.js.
|
|
2
|
+
//
|
|
3
|
+
// The crypto core (sign / verify / getCsrfSecret) is framework-agnostic and is
|
|
4
|
+
// REUSED VERBATIM from the SDK — tokens are byte-identical and interchangeable
|
|
5
|
+
// across the express and hono engines (proven in spikes/csrf-parity.mjs, §3.2).
|
|
6
|
+
// Only the express plumbing (req.cookies / res.cookie / res.status().send) is
|
|
7
|
+
// replaced with hono/cookie helpers + c.text(403). The shouldGenerateToken
|
|
8
|
+
// (GET) / shouldVerifyToken (mutating + cookie present + non-/mcp + non-didwallet)
|
|
9
|
+
// semantics are preserved faithfully.
|
|
10
|
+
import type { MiddlewareHandler } from 'hono';
|
|
11
|
+
import { getCookie, setCookie } from 'hono/cookie';
|
|
12
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
13
|
+
import { sign, verify, getCsrfSecret } from '@blocklet/sdk/lib/util/csrf';
|
|
14
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
15
|
+
import { isDidWalletConnect } from '@blocklet/sdk/lib/util/wallet';
|
|
16
|
+
import { readConfig } from '../../libs/env';
|
|
17
|
+
|
|
18
|
+
const isEmpty = (v: unknown): boolean => v === undefined || v === null || v === '';
|
|
19
|
+
|
|
20
|
+
// The CSRF secret is HOST-GLOBAL, not per-tenant: this middleware runs BEFORE the
|
|
21
|
+
// tenant context is established (pipeline order cors→xss→csrf→…→context), so it
|
|
22
|
+
// has no instanceDid, and it binds the host-global login_token anyway. An embedded
|
|
23
|
+
// host (arc) that has no BLOCKLET_APP_SK env injects a dedicated secret via the
|
|
24
|
+
// config slot (`PAYMENT_CSRF_SECRET`); the standard blocklet server falls back to
|
|
25
|
+
// the SDK's env-based secret. See docs/architecture/payment-credential-sourcing.md.
|
|
26
|
+
function csrfSecret(): string {
|
|
27
|
+
return readConfig('PAYMENT_CSRF_SECRET') || getCsrfSecret();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Express SDK: shouldGenerateToken === GET; shouldVerifyToken === mutating
|
|
31
|
+
// method AND an x-csrf-token cookie already exists AND path is not /mcp AND the
|
|
32
|
+
// caller is not a DID Wallet connect request.
|
|
33
|
+
const MUTATING = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* hono csrf middleware. Faithful to the SDK express version:
|
|
37
|
+
* - GET: if a login_token cookie exists, (re)issue x-csrf-token = sign(secret,
|
|
38
|
+
* login_token) when it differs from the current cookie. {sameSite:'Strict',
|
|
39
|
+
* secure:true} matches the express res.cookie attributes.
|
|
40
|
+
* - mutating: only ENFORCED when a login_token AND an x-csrf-token cookie are
|
|
41
|
+
* both present and the path is not /mcp and the caller is not a DID Wallet
|
|
42
|
+
* (parity with the SDK — absent cookie => skip, never reject). The header
|
|
43
|
+
* must equal the cookie and verify() against the login_token, else 403.
|
|
44
|
+
*/
|
|
45
|
+
export function csrf(): MiddlewareHandler {
|
|
46
|
+
// async (no await): the SDK crypto core is synchronous, but the handler mixes a
|
|
47
|
+
// sync Response (c.text 403) with next()'s promise — async unifies the return
|
|
48
|
+
// type to the MiddlewareHandler contract.
|
|
49
|
+
// eslint-disable-next-line require-await
|
|
50
|
+
return async (c, next) => {
|
|
51
|
+
const method = c.req.method.toUpperCase();
|
|
52
|
+
const loginToken = getCookie(c, 'login_token');
|
|
53
|
+
const existingCsrf = getCookie(c, 'x-csrf-token');
|
|
54
|
+
|
|
55
|
+
if (method === 'GET') {
|
|
56
|
+
if (loginToken) {
|
|
57
|
+
const newCsrf = sign(csrfSecret(), loginToken);
|
|
58
|
+
if (newCsrf !== existingCsrf) {
|
|
59
|
+
setCookie(c, 'x-csrf-token', newCsrf, { sameSite: 'Strict', secure: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (MUTATING.includes(method)) {
|
|
66
|
+
// shouldVerifyToken parity: skip (do NOT reject) when the SDK would skip.
|
|
67
|
+
if (c.req.path.includes('/mcp')) return next();
|
|
68
|
+
if (isEmpty(loginToken)) return next();
|
|
69
|
+
if (isEmpty(existingCsrf)) return next();
|
|
70
|
+
if (isDidWalletConnect(c.req.header())) return next();
|
|
71
|
+
|
|
72
|
+
const headerCsrf = c.req.header('x-csrf-token');
|
|
73
|
+
if (existingCsrf === headerCsrf && verify(csrfSecret(), existingCsrf as string, loginToken as string)) {
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
return c.text('Invalid request: csrf token mismatch, please refresh the page try again', 403);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return next();
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default csrf;
|