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
|
@@ -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,209 @@
|
|
|
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
|
+
import { isDevelopmentEnv, enableDevFakeAuth } from '../../libs/env';
|
|
20
|
+
// The embed-auth wallet (verifies the embed authToken signature) is the dynamic
|
|
21
|
+
// business wallet (libs/auth.ts): the active tenant's wallet on arc/CF, the env
|
|
22
|
+
// wallet on blocklet-server. The embed branch runs inside the warmed request
|
|
23
|
+
// scope, so synchronous access resolves the tenant wallet.
|
|
24
|
+
import { wallet } from '../../libs/auth';
|
|
25
|
+
import { Customer } from '../../store/models/customer';
|
|
26
|
+
|
|
27
|
+
type PermissionSpec<T extends Model> = {
|
|
28
|
+
component?: boolean;
|
|
29
|
+
roles?: string[];
|
|
30
|
+
record?: {
|
|
31
|
+
model: T;
|
|
32
|
+
field: string;
|
|
33
|
+
findById?: (id: string) => Promise<T | null>;
|
|
34
|
+
};
|
|
35
|
+
mine?: boolean;
|
|
36
|
+
embed?: boolean;
|
|
37
|
+
ensureLogin?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function authenticate<T extends Model>({
|
|
41
|
+
component,
|
|
42
|
+
roles,
|
|
43
|
+
record,
|
|
44
|
+
mine,
|
|
45
|
+
embed,
|
|
46
|
+
ensureLogin,
|
|
47
|
+
}: PermissionSpec<T>): MiddlewareHandler {
|
|
48
|
+
return async (c, next) => {
|
|
49
|
+
// Dev-only bypass (NODE_ENV=development AND ENABLE_DEV_FAKE_AUTH=1 AND the
|
|
50
|
+
// x-dev-fake-did header). Production never sets ENABLE_DEV_FAKE_AUTH.
|
|
51
|
+
if (isDevelopmentEnv() && enableDevFakeAuth()) {
|
|
52
|
+
const devDid = c.req.header('x-dev-fake-did');
|
|
53
|
+
if (devDid) {
|
|
54
|
+
c.set('user', {
|
|
55
|
+
did: devDid,
|
|
56
|
+
role: 'owner',
|
|
57
|
+
provider: 'dev',
|
|
58
|
+
fullName: 'dev-fake-user',
|
|
59
|
+
walletOS: '',
|
|
60
|
+
via: 'dev',
|
|
61
|
+
});
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Identity carried in LOCALS (hono headers are immutable). Seed from the
|
|
67
|
+
// BS-injected x-user-* headers; the Bearer branch may override.
|
|
68
|
+
let userDid = c.req.header('x-user-did');
|
|
69
|
+
let userRole = c.req.header('x-user-role');
|
|
70
|
+
let userProvider = c.req.header('x-user-provider');
|
|
71
|
+
let userFullname = c.req.header('x-user-fullname');
|
|
72
|
+
let userWalletOs = c.req.header('x-user-wallet-os');
|
|
73
|
+
|
|
74
|
+
// Authenticate by Authorization: Bearer <login-token> (local JWT verify, no
|
|
75
|
+
// HTTP callback) when BS did not inject x-user-did (tunnel bypass).
|
|
76
|
+
const authHeader = c.req.header('authorization');
|
|
77
|
+
if (authHeader && /^Bearer\s+/i.test(authHeader) && !userDid) {
|
|
78
|
+
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
79
|
+
if (token) {
|
|
80
|
+
const session = await verifyLoginToken({ token, strictMode: false }).catch(() => null);
|
|
81
|
+
if (session?.did) {
|
|
82
|
+
const canonicalDid = session.did.startsWith('did:abt:') ? session.did : `did:abt:${session.did}`;
|
|
83
|
+
userDid = canonicalDid;
|
|
84
|
+
userRole = `blocklet-${session.role || 'user'}`;
|
|
85
|
+
userProvider = session.provider || 'wallet';
|
|
86
|
+
userFullname = encodeURIComponent(session.fullName || '');
|
|
87
|
+
userWalletOs = session.walletOS || '';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// authenticate by component call
|
|
93
|
+
const sig = c.req.header('x-component-sig');
|
|
94
|
+
if (component && sig) {
|
|
95
|
+
const url = new URL(c.req.url);
|
|
96
|
+
const shimReq = {
|
|
97
|
+
get: (h: string) => c.req.header(h),
|
|
98
|
+
body: c.get('sanitizedBody') ?? {},
|
|
99
|
+
method: c.req.method,
|
|
100
|
+
originalUrl: url.pathname + url.search,
|
|
101
|
+
query: c.req.query(),
|
|
102
|
+
};
|
|
103
|
+
const { data } = getVerifyData(shimReq as any, 'component');
|
|
104
|
+
const verified = await verify(data, sig);
|
|
105
|
+
if (!verified) {
|
|
106
|
+
return c.json({ error: 'Invalid signature for component call' }, 401);
|
|
107
|
+
}
|
|
108
|
+
const componentDid = c.req.header('x-component-did') as string;
|
|
109
|
+
c.set('user', {
|
|
110
|
+
did: componentDid,
|
|
111
|
+
role: 'owner',
|
|
112
|
+
provider: 'wallet',
|
|
113
|
+
fullName: componentDid,
|
|
114
|
+
walletOS: '',
|
|
115
|
+
via: 'api',
|
|
116
|
+
});
|
|
117
|
+
return next();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// authenticate by authToken for embed
|
|
121
|
+
const embedToken = c.req.query('authToken') || '';
|
|
122
|
+
const embedId = c.req.param('id') || c.req.query('subscription_id') || '';
|
|
123
|
+
if (embed && embedToken && embedId) {
|
|
124
|
+
// next() is intentionally OUTSIDE the try: the try only guards the
|
|
125
|
+
// signature verification, never the downstream route (awaiting next here
|
|
126
|
+
// would wrongly report route errors as embed-auth failures — parity with
|
|
127
|
+
// the express version, which does not await next()).
|
|
128
|
+
let embedOk = false;
|
|
129
|
+
try {
|
|
130
|
+
const w = wallet;
|
|
131
|
+
const verified = await w.verify(embedId, embedToken);
|
|
132
|
+
if (!verified) {
|
|
133
|
+
return c.json({ error: `Invalid signature for embed: ${embedId}` }, 401);
|
|
134
|
+
}
|
|
135
|
+
c.set('user', {
|
|
136
|
+
did: w.address,
|
|
137
|
+
role: 'owner',
|
|
138
|
+
provider: 'wallet',
|
|
139
|
+
fullName: 'embed',
|
|
140
|
+
walletOS: '',
|
|
141
|
+
via: 'embed',
|
|
142
|
+
});
|
|
143
|
+
embedOk = true;
|
|
144
|
+
} catch (err: any) {
|
|
145
|
+
return c.json({ error: `Invalid signature for embed: ${embedId}: ${err.message}` }, 401);
|
|
146
|
+
}
|
|
147
|
+
if (embedOk) return next();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (userDid) {
|
|
151
|
+
const role = (userRole || '').replace('blocklet-', '') || 'guest';
|
|
152
|
+
const user = {
|
|
153
|
+
did: userDid,
|
|
154
|
+
role,
|
|
155
|
+
provider: userProvider as string,
|
|
156
|
+
fullName: decodeURIComponent(userFullname || ''),
|
|
157
|
+
walletOS: userWalletOs as string,
|
|
158
|
+
via: 'dashboard',
|
|
159
|
+
};
|
|
160
|
+
c.set('user', user);
|
|
161
|
+
|
|
162
|
+
// authenticate by session user role
|
|
163
|
+
if (roles) {
|
|
164
|
+
if (roles.includes(user.role)) {
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (ensureLogin) {
|
|
170
|
+
user.via = 'api';
|
|
171
|
+
c.set('user', user);
|
|
172
|
+
return next();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (mine) {
|
|
176
|
+
const customer = await Customer.findOne({ where: { did: user.did } });
|
|
177
|
+
if (customer) {
|
|
178
|
+
c.set('customer', customer);
|
|
179
|
+
// hono query is immutable — inject the VERIFIED id into context so a
|
|
180
|
+
// forged ?customer_id=<other> cannot bypass the mine check.
|
|
181
|
+
c.set('customer_id', customer.id);
|
|
182
|
+
return next();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// authenticate by record owner
|
|
187
|
+
if (record) {
|
|
188
|
+
const { model, field = 'customer_id', findById } = record;
|
|
189
|
+
const id = c.req.param('id') as string;
|
|
190
|
+
const doc: T | null =
|
|
191
|
+
findById && typeof findById === 'function' ? await findById(id) : await (model as any).findByPk(id);
|
|
192
|
+
if (doc && doc[field as keyof T]) {
|
|
193
|
+
const customer = await Customer.findOne({ where: { did: user.did } });
|
|
194
|
+
c.set('doc', doc);
|
|
195
|
+
c.set('customer', customer);
|
|
196
|
+
if (customer && customer.id === doc[field as keyof T]) {
|
|
197
|
+
user.via = 'portal';
|
|
198
|
+
c.set('user', user);
|
|
199
|
+
return next();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return c.json({ error: 'Not authorized to perform this action' }, 403);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default authenticate;
|