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,72 @@
|
|
|
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
|
+
|
|
17
|
+
const isEmpty = (v: unknown): boolean => v === undefined || v === null || v === '';
|
|
18
|
+
|
|
19
|
+
// Express SDK: shouldGenerateToken === GET; shouldVerifyToken === mutating
|
|
20
|
+
// method AND an x-csrf-token cookie already exists AND path is not /mcp AND the
|
|
21
|
+
// caller is not a DID Wallet connect request.
|
|
22
|
+
const MUTATING = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* hono csrf middleware. Faithful to the SDK express version:
|
|
26
|
+
* - GET: if a login_token cookie exists, (re)issue x-csrf-token = sign(secret,
|
|
27
|
+
* login_token) when it differs from the current cookie. {sameSite:'Strict',
|
|
28
|
+
* secure:true} matches the express res.cookie attributes.
|
|
29
|
+
* - mutating: only ENFORCED when a login_token AND an x-csrf-token cookie are
|
|
30
|
+
* both present and the path is not /mcp and the caller is not a DID Wallet
|
|
31
|
+
* (parity with the SDK — absent cookie => skip, never reject). The header
|
|
32
|
+
* must equal the cookie and verify() against the login_token, else 403.
|
|
33
|
+
*/
|
|
34
|
+
export function csrf(): MiddlewareHandler {
|
|
35
|
+
// async (no await): the SDK crypto core is synchronous, but the handler mixes a
|
|
36
|
+
// sync Response (c.text 403) with next()'s promise — async unifies the return
|
|
37
|
+
// type to the MiddlewareHandler contract.
|
|
38
|
+
// eslint-disable-next-line require-await
|
|
39
|
+
return async (c, next) => {
|
|
40
|
+
const method = c.req.method.toUpperCase();
|
|
41
|
+
const loginToken = getCookie(c, 'login_token');
|
|
42
|
+
const existingCsrf = getCookie(c, 'x-csrf-token');
|
|
43
|
+
|
|
44
|
+
if (method === 'GET') {
|
|
45
|
+
if (loginToken) {
|
|
46
|
+
const newCsrf = sign(getCsrfSecret(), loginToken);
|
|
47
|
+
if (newCsrf !== existingCsrf) {
|
|
48
|
+
setCookie(c, 'x-csrf-token', newCsrf, { sameSite: 'Strict', secure: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (MUTATING.includes(method)) {
|
|
55
|
+
// shouldVerifyToken parity: skip (do NOT reject) when the SDK would skip.
|
|
56
|
+
if (c.req.path.includes('/mcp')) return next();
|
|
57
|
+
if (isEmpty(loginToken)) return next();
|
|
58
|
+
if (isEmpty(existingCsrf)) return next();
|
|
59
|
+
if (isDidWalletConnect(c.req.header())) return next();
|
|
60
|
+
|
|
61
|
+
const headerCsrf = c.req.header('x-csrf-token');
|
|
62
|
+
if (existingCsrf === headerCsrf && verify(getCsrfSecret(), existingCsrf as string, loginToken as string)) {
|
|
63
|
+
return next();
|
|
64
|
+
}
|
|
65
|
+
return c.text('Invalid request: csrf token mismatch, please refresh the page try again', 403);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return next();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default csrf;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono fork of @blocklet/sdk/lib/middlewares/fallback.js.
|
|
2
|
+
//
|
|
3
|
+
// SPA HTML fallback: serve index.html for html GET/HEAD requests with OG meta +
|
|
4
|
+
// theme styles/script + blocklet.js injection. The injection logic is ported
|
|
5
|
+
// VERBATIM from the SDK (same helpers: getBlockletSettings / getBlockletJs / env,
|
|
6
|
+
// @blocklet/theme buildThemeStyles+buildThemeScript, lodash escape) — only the
|
|
7
|
+
// express req/res plumbing becomes hono (c.req / c.html). The express-only test
|
|
8
|
+
// hook (`next(source)` in NODE_ENV=test) is dropped. did-pay calls this with no
|
|
9
|
+
// getPageData, so pageData defaults to {} (title/description from env). Inert
|
|
10
|
+
// until SPA serving moves off the bridge (Phase 4); forked + unit-tested now.
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import type { MiddlewareHandler, Context } from 'hono';
|
|
15
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
16
|
+
import { joinURL } from 'ufo';
|
|
17
|
+
import escape from 'lodash/escape';
|
|
18
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
19
|
+
import { RESOURCE_PATTERN } from '@blocklet/constant';
|
|
20
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
21
|
+
import { buildThemeStyles, buildThemeScript } from '@blocklet/theme';
|
|
22
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
23
|
+
import { env, getBlockletSettings, getBlockletJs } from '@blocklet/sdk/lib/config';
|
|
24
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
25
|
+
import { SERVICE_PREFIX } from '@blocklet/sdk/lib/util/constants';
|
|
26
|
+
import { readConfig, isTestEnv } from '../../libs/env';
|
|
27
|
+
|
|
28
|
+
interface PageData {
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
ogImage?: string;
|
|
32
|
+
embed?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FallbackOptions {
|
|
36
|
+
root?: string;
|
|
37
|
+
getPageData?: (c: Context) => Promise<PageData> | PageData;
|
|
38
|
+
maxLength?: number;
|
|
39
|
+
timeout?: number;
|
|
40
|
+
cacheTtl?: number;
|
|
41
|
+
injectBlockletJs?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_CACHE_TTL = 60 * 1000;
|
|
45
|
+
const cache = new Map<
|
|
46
|
+
string,
|
|
47
|
+
{ html: string; timestamp: number; etag: string; pageGroup: string; pathPrefix: string }
|
|
48
|
+
>();
|
|
49
|
+
const cacheEnabled = readConfig('FALLBACK_CACHE_ENABLED') === 'true' || isTestEnv();
|
|
50
|
+
|
|
51
|
+
const TITLE_TAG_REGEX = /<title>(.+)<\/title>/;
|
|
52
|
+
const HEAD_END_TAG = '</head>';
|
|
53
|
+
|
|
54
|
+
const buildOpenGraph = (
|
|
55
|
+
pageData: Required<Pick<PageData, 'title' | 'description' | 'ogImage'>>,
|
|
56
|
+
appUrl: string
|
|
57
|
+
): string =>
|
|
58
|
+
[
|
|
59
|
+
`<meta property="og:title" content="${pageData.title}" data-react-helmet="true" />`,
|
|
60
|
+
`<meta property="og:description" content="${pageData.description}" data-react-helmet="true" />`,
|
|
61
|
+
'<meta property="og:type" content="website" data-react-helmet="true" />',
|
|
62
|
+
`<meta property="og:url" content="${appUrl}" data-react-helmet="true" />`,
|
|
63
|
+
`<meta property="og:image" content="${pageData.ogImage}" data-react-helmet="true" />`,
|
|
64
|
+
'<meta name="twitter:card" content="summary_large_image" data-react-helmet="true" />',
|
|
65
|
+
].join('\n');
|
|
66
|
+
|
|
67
|
+
const validatePageData = (data: PageData, maxLength: number): void => {
|
|
68
|
+
if (data.title && data.title.length > maxLength) throw new Error('Title too long');
|
|
69
|
+
if (data.description && data.description.length > maxLength) throw new Error('Description too long');
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const generateETag = (content: string): string => `W/"${crypto.createHash('sha1').update(content).digest('base64')}"`;
|
|
73
|
+
|
|
74
|
+
const getCacheKey = (pathname: string, filePath: string, pageGroup: string, pathPrefix: string): string =>
|
|
75
|
+
crypto.createHash('sha1').update(`${pathname}:${filePath}:${pageGroup}:${pathPrefix}`).digest('base64');
|
|
76
|
+
|
|
77
|
+
const tryWithTimeout = <T>(asyncFn: () => Promise<T> | T, timeout: number): Promise<T> =>
|
|
78
|
+
new Promise<T>((resolve, reject) => {
|
|
79
|
+
const timer = setTimeout(() => reject(new Error(`Operation timed out after ${timeout} ms`)), timeout);
|
|
80
|
+
Promise.resolve()
|
|
81
|
+
.then(asyncFn)
|
|
82
|
+
.then((result) => resolve(result))
|
|
83
|
+
.catch((err) => reject(err))
|
|
84
|
+
.finally(() => clearTimeout(timer));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function acceptsHtml(accept: string): boolean {
|
|
88
|
+
if (!accept) return true;
|
|
89
|
+
return accept.includes('text/html') || accept.includes('application/xhtml+xml') || accept.includes('*/*');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function fallback(file: string, options: FallbackOptions = {}): MiddlewareHandler {
|
|
93
|
+
const filePath = options.root ? join(options.root, file) : file;
|
|
94
|
+
if (!fs.existsSync(filePath)) {
|
|
95
|
+
throw new Error(`Fallback file not found at: ${filePath}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return async (c, next) => {
|
|
99
|
+
const method = c.req.method.toUpperCase();
|
|
100
|
+
if (
|
|
101
|
+
(method !== 'GET' && method !== 'HEAD') ||
|
|
102
|
+
!acceptsHtml(c.req.header('accept') || '') ||
|
|
103
|
+
RESOURCE_PATTERN.test(c.req.path)
|
|
104
|
+
) {
|
|
105
|
+
return next();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const pageGroup = c.req.header('x-page-group') || '';
|
|
109
|
+
const pathPrefix = c.req.header('x-path-prefix') || '';
|
|
110
|
+
const cacheKey = getCacheKey(c.req.path, filePath, pageGroup, pathPrefix);
|
|
111
|
+
const { theme } = getBlockletSettings();
|
|
112
|
+
|
|
113
|
+
if (cacheEnabled) {
|
|
114
|
+
const cached = cache.get(cacheKey);
|
|
115
|
+
const cacheTtl = options.cacheTtl || DEFAULT_CACHE_TTL;
|
|
116
|
+
if (
|
|
117
|
+
cached &&
|
|
118
|
+
Date.now() - cached.timestamp < cacheTtl &&
|
|
119
|
+
cached.pageGroup === pageGroup &&
|
|
120
|
+
cached.pathPrefix === pathPrefix
|
|
121
|
+
) {
|
|
122
|
+
c.header('X-Cache', 'HIT');
|
|
123
|
+
c.header('ETag', cached.etag);
|
|
124
|
+
return c.html(cached.html);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pageData: PageData = await tryWithTimeout(
|
|
129
|
+
options.getPageData ? () => options.getPageData!(c) : () => Promise.resolve({}),
|
|
130
|
+
options.timeout || 5000
|
|
131
|
+
);
|
|
132
|
+
validatePageData(pageData, options.maxLength || 1000);
|
|
133
|
+
const title = escape(pageData.title || (env as any).appName);
|
|
134
|
+
const description = escape(pageData.description || (env as any).appDescription);
|
|
135
|
+
const ogImage = pageData.ogImage || joinURL((env as any).appUrl || '/', SERVICE_PREFIX, '/blocklet/og.png');
|
|
136
|
+
|
|
137
|
+
let source = await fs.promises.readFile(filePath, 'utf8');
|
|
138
|
+
if (title) {
|
|
139
|
+
source = source.includes('<title>')
|
|
140
|
+
? source.replace(TITLE_TAG_REGEX, `<title>${title}</title>`)
|
|
141
|
+
: source.replace(HEAD_END_TAG, `<title>${title}</title>${HEAD_END_TAG}`);
|
|
142
|
+
}
|
|
143
|
+
if (description && !source.includes('<meta name="description"')) {
|
|
144
|
+
source = source.replace(
|
|
145
|
+
HEAD_END_TAG,
|
|
146
|
+
`<meta name="description" content="${description}" data-react-helmet="true" />${HEAD_END_TAG}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (!source.includes('meta property="og:image"')) {
|
|
150
|
+
source = source.replace(
|
|
151
|
+
HEAD_END_TAG,
|
|
152
|
+
`${buildOpenGraph({ title, description, ogImage }, (env as any).appUrl || '/')}\n${HEAD_END_TAG}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (pageData.embed) {
|
|
156
|
+
source = source.replace(
|
|
157
|
+
HEAD_END_TAG,
|
|
158
|
+
`<link rel="blocklet-open-embed" type="application/json" href="${pageData.embed}" />${HEAD_END_TAG}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const blockletJs = getBlockletJs(pageGroup, pathPrefix);
|
|
163
|
+
if (blockletJs && options.injectBlockletJs !== false) {
|
|
164
|
+
source = source
|
|
165
|
+
.replace('<script src="__blocklet__.js"></script>', `<script>${blockletJs}</script>`)
|
|
166
|
+
.replace('<script src="__meta__.js"></script>', `<script>${blockletJs}</script>`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const themeStyles = buildThemeStyles(theme);
|
|
170
|
+
const themeScript = buildThemeScript(theme);
|
|
171
|
+
if (!source.includes('<style id="blocklet-theme">')) {
|
|
172
|
+
source = source.replace(HEAD_END_TAG, `${themeStyles}${HEAD_END_TAG}`);
|
|
173
|
+
}
|
|
174
|
+
if (!source.includes('<script id="blocklet-theme-script">')) {
|
|
175
|
+
source = source.replace(HEAD_END_TAG, `${themeScript}${HEAD_END_TAG}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const etag = generateETag(source);
|
|
179
|
+
cache.set(cacheKey, { html: source, timestamp: Date.now(), etag, pageGroup, pathPrefix });
|
|
180
|
+
|
|
181
|
+
if (options.cacheTtl) {
|
|
182
|
+
c.header('Cache-Control', `public, max-age=${options.cacheTtl}`);
|
|
183
|
+
} else {
|
|
184
|
+
c.header('Surrogate-Control', 'no-store');
|
|
185
|
+
c.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
186
|
+
c.header('Expires', '0');
|
|
187
|
+
}
|
|
188
|
+
c.header('X-Cache', 'MISS');
|
|
189
|
+
c.header('ETag', etag);
|
|
190
|
+
return c.html(source);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export default fallback;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — the native route group middleware pipeline.
|
|
2
|
+
//
|
|
3
|
+
// Assembled in the SAME order as the legacy express app shell
|
|
4
|
+
// (service.ts buildNodeHandler: cookie→json→urlencoded→cors→xss→csrf→ensureI18n
|
|
5
|
+
// →cdn→context). hono parses cookies (hono/cookie) and the body (xss is the
|
|
6
|
+
// single body read-point) on demand, so there is no separate cookie/json/
|
|
7
|
+
// urlencoded middleware.
|
|
8
|
+
//
|
|
9
|
+
// IMPORTANT — scoping (design §2 + §7 risk "回环桥 catch-all 与 native 路由遮蔽"):
|
|
10
|
+
// the pipeline is applied SCOPED to each migrated route prefix
|
|
11
|
+
// (`native.use('/api/<domain>/*', mw)`), NOT globally (`use('*')`). A global
|
|
12
|
+
// `use('*')` would run for EVERY request — including those that fall through to
|
|
13
|
+
// the catch-all bridge — double-sourcing cors, consuming the raw body (breaking
|
|
14
|
+
// the Stripe webhook), and resolving the tenant twice. Scoping keeps the bridge
|
|
15
|
+
// path entirely free of native middleware (verified empirically + by the Phase 0
|
|
16
|
+
// single-cors / raw-body invariants). As Phase 3 migrates a domain, it calls
|
|
17
|
+
// mountNativeGroup(native, '/api/<domain>', register) so the SAME pipeline covers
|
|
18
|
+
// that domain's routes and nothing else.
|
|
19
|
+
import type { Hono, MiddlewareHandler } from 'hono';
|
|
20
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
21
|
+
import { cors } from 'hono/cors';
|
|
22
|
+
import { xss } from './xss';
|
|
23
|
+
import { csrf } from './csrf';
|
|
24
|
+
import { cdn } from './cdn';
|
|
25
|
+
import { ensureI18n, contextMiddleware } from './context';
|
|
26
|
+
import { isTestEnv } from '../../libs/env';
|
|
27
|
+
|
|
28
|
+
// The full app-shell pipeline (single instances, reused across prefixes). This
|
|
29
|
+
// module — and therefore csrf/cdn/context (which import @blocklet/sdk/lib/util/*
|
|
30
|
+
// the CF worker shim does not map) — is only reachable from the NODE host
|
|
31
|
+
// (service.ts getHonoApp); the CF worker mounts resources via the import-light
|
|
32
|
+
// resource-mount.ts, so it never pulls these in.
|
|
33
|
+
const sharedPipeline: MiddlewareHandler[] = [cors(), xss(), csrf(), ensureI18n(), cdn(), contextMiddleware()];
|
|
34
|
+
|
|
35
|
+
/** The full app-shell middleware array — the node host's appShell for mountResourceGroup. */
|
|
36
|
+
export function fullPipeline(): MiddlewareHandler[] {
|
|
37
|
+
return sharedPipeline;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Apply the native app-shell pipeline scoped to a route prefix, then register
|
|
42
|
+
* that group's routes. `prefix` is a path like '/api/customers' (no trailing
|
|
43
|
+
* slash); the pipeline is bound to `${prefix}/*`, which hono matches for both
|
|
44
|
+
* the bare prefix and its sub-paths but for nothing outside it.
|
|
45
|
+
*/
|
|
46
|
+
export function mountNativeGroup(native: Hono, prefix: string, register: (native: Hono) => void): void {
|
|
47
|
+
for (const mw of sharedPipeline) {
|
|
48
|
+
native.use(`${prefix}/*`, mw);
|
|
49
|
+
}
|
|
50
|
+
register(native);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function configureNativePipeline(native: Hono): void {
|
|
54
|
+
// Phase 1 production: the native group has no business routes yet (DID-Connect
|
|
55
|
+
// in Phase 2, resource routes in Phase 3); every request still falls through
|
|
56
|
+
// to the express bridge. The only native route is a test-only diagnostic that
|
|
57
|
+
// exercises the full chain (cors→xss→csrf→ensureI18n→cdn→context) end-to-end.
|
|
58
|
+
if (isTestEnv()) {
|
|
59
|
+
mountNativeGroup(native, '/api/__e2e', (n) => {
|
|
60
|
+
const echo = (c: any) =>
|
|
61
|
+
c.json({
|
|
62
|
+
body: c.get('sanitizedBody') ?? null,
|
|
63
|
+
query: c.req.query('q') ?? null,
|
|
64
|
+
user: c.get('user') ?? null,
|
|
65
|
+
locale: c.get('locale') ?? null,
|
|
66
|
+
});
|
|
67
|
+
n.get('/api/__e2e/echo', echo);
|
|
68
|
+
n.post('/api/__e2e/echo', echo);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default configureNativePipeline;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Phase 4 (express→hono) — resource-domain mounting, split out of pipeline.ts so
|
|
2
|
+
// it stays import-light for the CF worker bundle.
|
|
3
|
+
//
|
|
4
|
+
// The CF worker reaches this module via service.http.resourceRoutes →
|
|
5
|
+
// routes/hono → mountResourceGroup. It must NOT statically import the full
|
|
6
|
+
// app-shell middleware (csrf/cdn/ensureI18n/contextMiddleware), because those pull
|
|
7
|
+
// @blocklet/sdk/lib/util/* subpaths the worker's blocklet-sdk shim does not map —
|
|
8
|
+
// and the worker runs a LITE app-shell anyway (it owns cors + tenant). So this
|
|
9
|
+
// module imports ONLY xss (the routes depend on it to populate sanitizedBody) plus
|
|
10
|
+
// the resource-level livemode/baseCurrency middleware. The node host injects its
|
|
11
|
+
// full app-shell array via the `appShell` option (built in pipeline.ts).
|
|
12
|
+
import type { Hono, MiddlewareHandler } from 'hono';
|
|
13
|
+
import { xss } from './xss';
|
|
14
|
+
|
|
15
|
+
// LITE app-shell — the worker default. Only xss (self-skips the Stripe raw-body
|
|
16
|
+
// path via RAW_BODY_PREFIXES); cors/csrf/cdn/i18n/context are provided by the
|
|
17
|
+
// worker (cors + tenant) or intentionally absent (the worker never ran csrf/cdn).
|
|
18
|
+
const litePipeline: MiddlewareHandler[] = [xss()];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mount a RESOURCE domain — the app-shell middleware (default LITE; the node host
|
|
22
|
+
* passes its full pipeline via `opts.appShell`) + the resource-level livemode
|
|
23
|
+
* middleware (routes/index.ts:49 parity) + (for the 5 domains that need it)
|
|
24
|
+
* loadBaseCurrency, all scoped to `${prefix}/*`, then the routes. Middleware order
|
|
25
|
+
* mirrors express: app-shell → livemode → [baseCurrency] → route.
|
|
26
|
+
*/
|
|
27
|
+
export function mountResourceGroup(
|
|
28
|
+
native: Hono,
|
|
29
|
+
prefix: string,
|
|
30
|
+
subApp: Hono,
|
|
31
|
+
opts: { baseCurrency?: boolean; appShell?: MiddlewareHandler[] } = {}
|
|
32
|
+
): void {
|
|
33
|
+
// eslint-disable-next-line global-require
|
|
34
|
+
const { livemode, loadBaseCurrency } = require('./resource');
|
|
35
|
+
const appShell = opts.appShell ?? litePipeline;
|
|
36
|
+
for (const mw of appShell) native.use(`${prefix}/*`, mw);
|
|
37
|
+
native.use(`${prefix}/*`, livemode());
|
|
38
|
+
if (opts.baseCurrency) native.use(`${prefix}/*`, loadBaseCurrency());
|
|
39
|
+
native.route(prefix, subApp); // sub-app routes are relative to the prefix
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default mountResourceGroup;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Phase 3 (express→hono) — resource-route prerequisite middleware, the hono fork
|
|
2
|
+
// of the two router-level middlewares in routes/index.ts:
|
|
3
|
+
// ① livemode (GLOBAL): routes/index.ts:49 — req.livemode = … for every resource
|
|
4
|
+
// route. Honors ?livemode, then a CF-prefilled value, then PAYMENT_LIVEMODE.
|
|
5
|
+
// ② loadBaseCurrency (SELECTIVE): routes/index.ts:86 — only 5 domains
|
|
6
|
+
// (checkout-sessions / donations / payment-links / prices / products).
|
|
7
|
+
// The baseCurrencyCache + lazy PaymentCurrency.addHook registration are
|
|
8
|
+
// framework-agnostic and preserved verbatim.
|
|
9
|
+
import type { MiddlewareHandler } from 'hono';
|
|
10
|
+
import { paymentLivemode } from '../../libs/env';
|
|
11
|
+
import { PaymentCurrency } from '../../store/models/payment-currency';
|
|
12
|
+
|
|
13
|
+
/** ① livemode — global across all native resource routes (parity with routes/index.ts:49). */
|
|
14
|
+
export function livemode(): MiddlewareHandler {
|
|
15
|
+
return (c, next) => {
|
|
16
|
+
const q = c.req.query('livemode');
|
|
17
|
+
if (q !== undefined && q !== '') {
|
|
18
|
+
try {
|
|
19
|
+
c.set('livemode', !!JSON.parse(String(q)));
|
|
20
|
+
} catch {
|
|
21
|
+
c.set('livemode', true);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
// No upstream prefill on hono (unlike CF createExpressReq); fall back to the
|
|
25
|
+
// env var, defaulting to livemode=true unless PAYMENT_LIVEMODE=false.
|
|
26
|
+
c.set('livemode', paymentLivemode());
|
|
27
|
+
}
|
|
28
|
+
return next();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Lazy base-currency cache with TTL (invalidated on PaymentCurrency update/destroy).
|
|
33
|
+
const baseCurrencyCache = new Map<string, { data: any; expires: number }>();
|
|
34
|
+
const BASE_CURRENCY_TTL = 5 * 60_000;
|
|
35
|
+
let baseCurrencyHooksRegistered = false;
|
|
36
|
+
function ensureBaseCurrencyHooks() {
|
|
37
|
+
if (baseCurrencyHooksRegistered) return;
|
|
38
|
+
baseCurrencyHooksRegistered = true;
|
|
39
|
+
PaymentCurrency.addHook('afterUpdate', 'invalidateBaseCurrencyCache', () => baseCurrencyCache.clear());
|
|
40
|
+
PaymentCurrency.addHook('afterDestroy', 'invalidateBaseCurrencyCacheOnDelete', () => baseCurrencyCache.clear());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** ② loadBaseCurrency — only for the 5 domains that read c.get('baseCurrency'). */
|
|
44
|
+
export function loadBaseCurrency(): MiddlewareHandler {
|
|
45
|
+
return async (c, next) => {
|
|
46
|
+
ensureBaseCurrencyHooks();
|
|
47
|
+
const livemodeVal = c.get('livemode');
|
|
48
|
+
const key = `base_${livemodeVal}`;
|
|
49
|
+
const cached = baseCurrencyCache.get(key);
|
|
50
|
+
if (cached && cached.expires > Date.now()) {
|
|
51
|
+
c.set('baseCurrency', cached.data);
|
|
52
|
+
} else {
|
|
53
|
+
const baseCurrency = await PaymentCurrency.findOne({
|
|
54
|
+
where: { is_base_currency: true, livemode: livemodeVal },
|
|
55
|
+
});
|
|
56
|
+
c.set('baseCurrency', baseCurrency);
|
|
57
|
+
if (baseCurrency) {
|
|
58
|
+
baseCurrencyCache.set(key, { data: baseCurrency, expires: Date.now() + BASE_CURRENCY_TTL });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return next();
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono fork of api/src/libs/security.ts authenticate().
|
|
2
|
+
//
|
|
3
|
+
// Behavior is identical to the express version; the plumbing changes:
|
|
4
|
+
// - req.user= / req.customer= / req.doc= → c.set('user'|'customer'|'doc', ...)
|
|
5
|
+
// - mine mode: req.query.customer_id = id → c.set('customer_id', id) (hono query
|
|
6
|
+
// is immutable; routes read c.get('customer_id') ?? c.req.query('customer_id')
|
|
7
|
+
// so a forged ?customer_id cannot bypass the verified injection — design §7).
|
|
8
|
+
// - the express Bearer branch MUTATED req.headers['x-user-did'] then re-read it;
|
|
9
|
+
// hono headers are immutable, so the resolved identity is carried in LOCALS
|
|
10
|
+
// (userDid/userRole/...) instead — same cascade, no header mutation.
|
|
11
|
+
// - component-sig verify needs an express-req shape for getVerifyData(); a thin
|
|
12
|
+
// shim is built from the context (body = the sanitized body, xss ran first).
|
|
13
|
+
import type { MiddlewareHandler } from 'hono';
|
|
14
|
+
import type { Model } from 'sequelize';
|
|
15
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
16
|
+
import { getVerifyData, verify } from '@blocklet/sdk/lib/util/verify-sign';
|
|
17
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
18
|
+
import { verifyLoginToken } from '@blocklet/sdk/lib/util/verify-session';
|
|
19
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
20
|
+
import { getWallet } from '@blocklet/sdk/lib/wallet';
|
|
21
|
+
import { isDevelopmentEnv, enableDevFakeAuth } from '../../libs/env';
|
|
22
|
+
import { Customer } from '../../store/models/customer';
|
|
23
|
+
|
|
24
|
+
// Phase 13b parity: lazy wallet — getWallet() needs BLOCKLET_APP_PK which a bare
|
|
25
|
+
// host lacks; only the embed branch uses it.
|
|
26
|
+
let cachedWallet: ReturnType<typeof getWallet> | undefined;
|
|
27
|
+
const wallet = () => {
|
|
28
|
+
cachedWallet ??= getWallet();
|
|
29
|
+
return cachedWallet;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type PermissionSpec<T extends Model> = {
|
|
33
|
+
component?: boolean;
|
|
34
|
+
roles?: string[];
|
|
35
|
+
record?: {
|
|
36
|
+
model: T;
|
|
37
|
+
field: string;
|
|
38
|
+
findById?: (id: string) => Promise<T | null>;
|
|
39
|
+
};
|
|
40
|
+
mine?: boolean;
|
|
41
|
+
embed?: boolean;
|
|
42
|
+
ensureLogin?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function authenticate<T extends Model>({
|
|
46
|
+
component,
|
|
47
|
+
roles,
|
|
48
|
+
record,
|
|
49
|
+
mine,
|
|
50
|
+
embed,
|
|
51
|
+
ensureLogin,
|
|
52
|
+
}: PermissionSpec<T>): MiddlewareHandler {
|
|
53
|
+
return async (c, next) => {
|
|
54
|
+
// Dev-only bypass (NODE_ENV=development AND ENABLE_DEV_FAKE_AUTH=1 AND the
|
|
55
|
+
// x-dev-fake-did header). Production never sets ENABLE_DEV_FAKE_AUTH.
|
|
56
|
+
if (isDevelopmentEnv() && enableDevFakeAuth()) {
|
|
57
|
+
const devDid = c.req.header('x-dev-fake-did');
|
|
58
|
+
if (devDid) {
|
|
59
|
+
c.set('user', {
|
|
60
|
+
did: devDid,
|
|
61
|
+
role: 'owner',
|
|
62
|
+
provider: 'dev',
|
|
63
|
+
fullName: 'dev-fake-user',
|
|
64
|
+
walletOS: '',
|
|
65
|
+
via: 'dev',
|
|
66
|
+
});
|
|
67
|
+
return next();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Identity carried in LOCALS (hono headers are immutable). Seed from the
|
|
72
|
+
// BS-injected x-user-* headers; the Bearer branch may override.
|
|
73
|
+
let userDid = c.req.header('x-user-did');
|
|
74
|
+
let userRole = c.req.header('x-user-role');
|
|
75
|
+
let userProvider = c.req.header('x-user-provider');
|
|
76
|
+
let userFullname = c.req.header('x-user-fullname');
|
|
77
|
+
let userWalletOs = c.req.header('x-user-wallet-os');
|
|
78
|
+
|
|
79
|
+
// Authenticate by Authorization: Bearer <login-token> (local JWT verify, no
|
|
80
|
+
// HTTP callback) when BS did not inject x-user-did (tunnel bypass).
|
|
81
|
+
const authHeader = c.req.header('authorization');
|
|
82
|
+
if (authHeader && /^Bearer\s+/i.test(authHeader) && !userDid) {
|
|
83
|
+
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
84
|
+
if (token) {
|
|
85
|
+
const session = await verifyLoginToken({ token, strictMode: false }).catch(() => null);
|
|
86
|
+
if (session?.did) {
|
|
87
|
+
const canonicalDid = session.did.startsWith('did:abt:') ? session.did : `did:abt:${session.did}`;
|
|
88
|
+
userDid = canonicalDid;
|
|
89
|
+
userRole = `blocklet-${session.role || 'user'}`;
|
|
90
|
+
userProvider = session.provider || 'wallet';
|
|
91
|
+
userFullname = encodeURIComponent(session.fullName || '');
|
|
92
|
+
userWalletOs = session.walletOS || '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// authenticate by component call
|
|
98
|
+
const sig = c.req.header('x-component-sig');
|
|
99
|
+
if (component && sig) {
|
|
100
|
+
const url = new URL(c.req.url);
|
|
101
|
+
const shimReq = {
|
|
102
|
+
get: (h: string) => c.req.header(h),
|
|
103
|
+
body: c.get('sanitizedBody') ?? {},
|
|
104
|
+
method: c.req.method,
|
|
105
|
+
originalUrl: url.pathname + url.search,
|
|
106
|
+
query: c.req.query(),
|
|
107
|
+
};
|
|
108
|
+
const { data } = getVerifyData(shimReq as any, 'component');
|
|
109
|
+
const verified = await verify(data, sig);
|
|
110
|
+
if (!verified) {
|
|
111
|
+
return c.json({ error: 'Invalid signature for component call' }, 401);
|
|
112
|
+
}
|
|
113
|
+
const componentDid = c.req.header('x-component-did') as string;
|
|
114
|
+
c.set('user', {
|
|
115
|
+
did: componentDid,
|
|
116
|
+
role: 'owner',
|
|
117
|
+
provider: 'wallet',
|
|
118
|
+
fullName: componentDid,
|
|
119
|
+
walletOS: '',
|
|
120
|
+
via: 'api',
|
|
121
|
+
});
|
|
122
|
+
return next();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// authenticate by authToken for embed
|
|
126
|
+
const embedToken = c.req.query('authToken') || '';
|
|
127
|
+
const embedId = c.req.param('id') || c.req.query('subscription_id') || '';
|
|
128
|
+
if (embed && embedToken && embedId) {
|
|
129
|
+
// next() is intentionally OUTSIDE the try: the try only guards the
|
|
130
|
+
// signature verification, never the downstream route (awaiting next here
|
|
131
|
+
// would wrongly report route errors as embed-auth failures — parity with
|
|
132
|
+
// the express version, which does not await next()).
|
|
133
|
+
let embedOk = false;
|
|
134
|
+
try {
|
|
135
|
+
const w = wallet();
|
|
136
|
+
const verified = await w.verify(embedId, embedToken);
|
|
137
|
+
if (!verified) {
|
|
138
|
+
return c.json({ error: `Invalid signature for embed: ${embedId}` }, 401);
|
|
139
|
+
}
|
|
140
|
+
c.set('user', {
|
|
141
|
+
did: w.address,
|
|
142
|
+
role: 'owner',
|
|
143
|
+
provider: 'wallet',
|
|
144
|
+
fullName: 'embed',
|
|
145
|
+
walletOS: '',
|
|
146
|
+
via: 'embed',
|
|
147
|
+
});
|
|
148
|
+
embedOk = true;
|
|
149
|
+
} catch (err: any) {
|
|
150
|
+
return c.json({ error: `Invalid signature for embed: ${embedId}: ${err.message}` }, 401);
|
|
151
|
+
}
|
|
152
|
+
if (embedOk) return next();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (userDid) {
|
|
156
|
+
const role = (userRole || '').replace('blocklet-', '') || 'guest';
|
|
157
|
+
const user = {
|
|
158
|
+
did: userDid,
|
|
159
|
+
role,
|
|
160
|
+
provider: userProvider as string,
|
|
161
|
+
fullName: decodeURIComponent(userFullname || ''),
|
|
162
|
+
walletOS: userWalletOs as string,
|
|
163
|
+
via: 'dashboard',
|
|
164
|
+
};
|
|
165
|
+
c.set('user', user);
|
|
166
|
+
|
|
167
|
+
// authenticate by session user role
|
|
168
|
+
if (roles) {
|
|
169
|
+
if (roles.includes(user.role)) {
|
|
170
|
+
return next();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (ensureLogin) {
|
|
175
|
+
user.via = 'api';
|
|
176
|
+
c.set('user', user);
|
|
177
|
+
return next();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (mine) {
|
|
181
|
+
const customer = await Customer.findOne({ where: { did: user.did } });
|
|
182
|
+
if (customer) {
|
|
183
|
+
c.set('customer', customer);
|
|
184
|
+
// hono query is immutable — inject the VERIFIED id into context so a
|
|
185
|
+
// forged ?customer_id=<other> cannot bypass the mine check.
|
|
186
|
+
c.set('customer_id', customer.id);
|
|
187
|
+
return next();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// authenticate by record owner
|
|
192
|
+
if (record) {
|
|
193
|
+
const { model, field = 'customer_id', findById } = record;
|
|
194
|
+
const id = c.req.param('id') as string;
|
|
195
|
+
const doc: T | null =
|
|
196
|
+
findById && typeof findById === 'function' ? await findById(id) : await (model as any).findByPk(id);
|
|
197
|
+
if (doc && doc[field as keyof T]) {
|
|
198
|
+
const customer = await Customer.findOne({ where: { did: user.did } });
|
|
199
|
+
c.set('doc', doc);
|
|
200
|
+
c.set('customer', customer);
|
|
201
|
+
if (customer && customer.id === doc[field as keyof T]) {
|
|
202
|
+
user.via = 'portal';
|
|
203
|
+
c.set('user', user);
|
|
204
|
+
return next();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return c.json({ error: 'Not authorized to perform this action' }, 403);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default authenticate;
|