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,120 @@
|
|
|
1
|
+
// Phase 5 (W1′) — the tenant scanner is now a STRUCTURAL backstop, not a
|
|
2
|
+
// fail-open denylist. These tests pin the new behavior:
|
|
3
|
+
// - the live api/src scan passes with ZERO violations and an EMPTY whitelist
|
|
4
|
+
// (the 151-file fail-open whitelist collapsed because Phase 3/4 made the
|
|
5
|
+
// invariants load-bearing);
|
|
6
|
+
// - assertion ② (raw query on a tenant table must bind a tenant) fires on an
|
|
7
|
+
// unguarded raw read and stays quiet on a guarded one / a non-tenant table.
|
|
8
|
+
// Assertion ① (model must extend TenantModel) is exercised by the test-the-test
|
|
9
|
+
// in logs/s5-e2e.log (revert a model to `extends Model` -> scanner exits 1).
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
|
|
15
|
+
const SCANNER = path.join(__dirname, '../../../scripts/scan-tenant-queries.js');
|
|
16
|
+
const WHITELIST = path.join(__dirname, '../../../scripts/tenant-scan-whitelist.json');
|
|
17
|
+
|
|
18
|
+
function runScanner(args: string[], env: Record<string, string> = {}): { exit: number; result: any } {
|
|
19
|
+
try {
|
|
20
|
+
const out = execFileSync('node', [SCANNER, '--json', ...args], {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
env: { ...process.env, ...env },
|
|
23
|
+
});
|
|
24
|
+
return { exit: 0, result: JSON.parse(out) };
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
return { exit: err.status ?? 1, result: JSON.parse(err.stdout || '{}') };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeFixture(name: string, content: string): string {
|
|
31
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scan-'));
|
|
32
|
+
const file = path.join(dir, name);
|
|
33
|
+
fs.writeFileSync(file, content);
|
|
34
|
+
return file;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('tenant scanner — structural backstop (phase 5)', () => {
|
|
38
|
+
it('the live api/src scan passes with zero violations and an empty whitelist', () => {
|
|
39
|
+
const { exit, result } = runScanner([]);
|
|
40
|
+
expect(exit).toBe(0);
|
|
41
|
+
expect(result.violations).toEqual([]);
|
|
42
|
+
expect(result.staleWhitelistEntries).toEqual([]);
|
|
43
|
+
// the fail-open 151-file whitelist has collapsed
|
|
44
|
+
const whitelist = JSON.parse(fs.readFileSync(WHITELIST, 'utf8'));
|
|
45
|
+
expect(whitelist).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('assertion ②: an unguarded raw read on a tenant table is flagged', () => {
|
|
49
|
+
const file = writeFixture(
|
|
50
|
+
'unguarded.ts',
|
|
51
|
+
`export const f = (s: any) => s.query("SELECT * FROM coupons WHERE livemode = 1");`
|
|
52
|
+
);
|
|
53
|
+
const { exit, result } = runScanner([file]);
|
|
54
|
+
expect(exit).toBe(1);
|
|
55
|
+
expect(result.violations[0].kind).toBe('raw-unguarded');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('assertion ②: a tenant-bound raw read is NOT flagged', () => {
|
|
59
|
+
const file = writeFixture(
|
|
60
|
+
'guarded.ts',
|
|
61
|
+
`export const f = (s: any) => s.query("SELECT * FROM coupons WHERE instance_did = :instance_did");`
|
|
62
|
+
);
|
|
63
|
+
const { exit, result } = runScanner([file]);
|
|
64
|
+
expect(exit).toBe(0);
|
|
65
|
+
expect(result.violations).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('assertion ②: a DDL statement on a tenant table is exempt (not flagged)', () => {
|
|
69
|
+
const file = writeFixture(
|
|
70
|
+
'ddl.ts',
|
|
71
|
+
`export const f = (s: any) => s.query('ALTER TABLE "coupons" ADD COLUMN foo TEXT');`
|
|
72
|
+
);
|
|
73
|
+
const { exit } = runScanner([file]);
|
|
74
|
+
expect(exit).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('assertion ②: a raw read on a NON-tenant table is not flagged', () => {
|
|
78
|
+
const file = writeFixture('jobs.ts', `export const f = (s: any) => s.query("SELECT * FROM jobs WHERE id = 1");`);
|
|
79
|
+
const { exit } = runScanner([file]);
|
|
80
|
+
expect(exit).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('Security: DDL is judged by the statement verb — a SELECT cannot hide behind /* CREATE */', () => {
|
|
84
|
+
const file = writeFixture(
|
|
85
|
+
'evasion.ts',
|
|
86
|
+
`export const f = (s: any) => s.query("SELECT * FROM coupons WHERE id=1 /* CREATE */");`
|
|
87
|
+
);
|
|
88
|
+
const { exit, result } = runScanner([file]);
|
|
89
|
+
expect(exit).toBe(1);
|
|
90
|
+
expect(result.violations[0].kind).toBe('raw-unguarded');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('Bad input: an unparseable (binary / non-utf8) file fails loudly, never a silent skip', () => {
|
|
94
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scan-'));
|
|
95
|
+
const file = path.join(dir, 'binary.ts');
|
|
96
|
+
fs.writeFileSync(file, Buffer.from([0x53, 0x45, 0x00, 0x01, 0xff, 0xfe]));
|
|
97
|
+
const { exit, result } = runScanner([file]);
|
|
98
|
+
expect(exit).toBe(1);
|
|
99
|
+
expect(result.violations[0].kind).toBe('unreadable');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('Bad input: a whitelist entry pointing at a non-existent file is reported stale (exit 1)', () => {
|
|
103
|
+
const wl = writeFixture('wl.json', JSON.stringify([{ file: 'api/src/store/models/does-not-exist.ts', reason: 'x' }]));
|
|
104
|
+
const { exit, result } = runScanner([], { TENANT_SCAN_WHITELIST: wl });
|
|
105
|
+
expect(exit).toBe(1);
|
|
106
|
+
expect(result.staleWhitelistEntries).toContain('api/src/store/models/does-not-exist.ts');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('Security: a wildcard whitelist entry cannot swallow a real violation (exact-path match only)', () => {
|
|
110
|
+
const violating = writeFixture(
|
|
111
|
+
'leak.ts',
|
|
112
|
+
`export const f = (s: any) => s.query("SELECT * FROM coupons WHERE id=1");`
|
|
113
|
+
);
|
|
114
|
+
// a glob-looking entry must NOT match the violating file by prefix/wildcard
|
|
115
|
+
const wl = writeFixture('wl2.json', JSON.stringify([{ file: 'api/src/**', reason: 'wildcard attempt' }]));
|
|
116
|
+
const { exit, result } = runScanner([violating], { TENANT_SCAN_WHITELIST: wl });
|
|
117
|
+
expect(exit).toBe(1);
|
|
118
|
+
expect(result.violations.some((v: any) => v.kind === 'raw-unguarded')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono cdn fork. Rewrites asset URLs to the CDN host on
|
|
2
|
+
// outgoing HTML in production; inert for JSON /api responses (the only surface in
|
|
3
|
+
// Phases 1-3). The transform core (AssetHostTransformer) is the real SDK util;
|
|
4
|
+
// only the config env (assetCdnHost / componentDid) is mocked here.
|
|
5
|
+
import { Hono } from 'hono';
|
|
6
|
+
|
|
7
|
+
jest.mock('@blocklet/sdk/lib/config', () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
env: { assetCdnHost: 'cdn.example.com', componentDid: 'z-comp' },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line import/first
|
|
13
|
+
import { cdn } from '../../../src/middlewares/hono/cdn';
|
|
14
|
+
|
|
15
|
+
const ASSET = '/.blocklet/proxy/z-comp/app.js';
|
|
16
|
+
const HTML = `<html><head><script src="${ASSET}"></script></head><body>hi</body></html>`;
|
|
17
|
+
|
|
18
|
+
const prevServiceEnv = process.env.ABT_NODE_SERVICE_ENV;
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
process.env.ABT_NODE_SERVICE_ENV = 'production';
|
|
21
|
+
});
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
if (prevServiceEnv === undefined) delete process.env.ABT_NODE_SERVICE_ENV;
|
|
24
|
+
else process.env.ABT_NODE_SERVICE_ENV = prevServiceEnv;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function buildApp() {
|
|
28
|
+
const app = new Hono();
|
|
29
|
+
app.use('*', cdn());
|
|
30
|
+
app.get('/page', (c) => c.html(HTML));
|
|
31
|
+
app.get('/api/data', (c) => c.json({ asset: ASSET })); // JSON must NOT be rewritten
|
|
32
|
+
return app;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const get = (app: Hono, p: string, accept = 'text/html') =>
|
|
36
|
+
app.fetch(new Request(`http://x${p}`, { headers: { accept } }));
|
|
37
|
+
|
|
38
|
+
describe('hono cdn — happy path', () => {
|
|
39
|
+
it('rewrites HTML asset URLs to the CDN host in production', async () => {
|
|
40
|
+
const res = await get(buildApp(), '/page');
|
|
41
|
+
const body = await res.text();
|
|
42
|
+
expect(body).toContain('//cdn.example.com/.blocklet/proxy/z-comp/app.js');
|
|
43
|
+
expect(body).not.toMatch(/"\/\.blocklet\/proxy\/z-comp\/app\.js"/); // original quoted path is gone
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('hono cdn — security / inert for API', () => {
|
|
48
|
+
it('does NOT rewrite a JSON response (content-type guard — inert for /api)', async () => {
|
|
49
|
+
const res = await get(buildApp(), '/api/data', 'application/json');
|
|
50
|
+
const body = await res.json();
|
|
51
|
+
expect(body.asset).toBe(ASSET); // untouched
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does NOT rewrite when the client does not accept html', async () => {
|
|
55
|
+
const res = await get(buildApp(), '/page', 'application/json');
|
|
56
|
+
const body = await res.text();
|
|
57
|
+
// not html-accepting → shouldProcess false → original asset path retained
|
|
58
|
+
expect(body).toContain(`"${ASSET}"`);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('hono cdn — bad input (non-production inert)', () => {
|
|
63
|
+
it('does NOT rewrite outside production', async () => {
|
|
64
|
+
delete process.env.ABT_NODE_SERVICE_ENV;
|
|
65
|
+
const res = await get(buildApp(), '/page');
|
|
66
|
+
const body = await res.text();
|
|
67
|
+
expect(body).toContain(`"${ASSET}"`); // original retained
|
|
68
|
+
process.env.ABT_NODE_SERVICE_ENV = 'production';
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono ensureI18n + contextMiddleware fork. Mirrors the
|
|
2
|
+
// express tenant-middleware spec: single-point Host→tenant resolution, raw Host
|
|
3
|
+
// only, multi-mode fail-closed. Driven via app.fetch with an explicit host
|
|
4
|
+
// header (the fork reads c.req.header('host') — never a proxy header).
|
|
5
|
+
import { Hono } from 'hono';
|
|
6
|
+
import { ensureI18n, contextMiddleware } from '../../../src/middlewares/hono/context';
|
|
7
|
+
import { getInstanceDid } from '../../../src/libs/context';
|
|
8
|
+
import { getDefaultInstanceDid } from '../../../src/libs/tenant';
|
|
9
|
+
import {
|
|
10
|
+
setIdentityDriver,
|
|
11
|
+
createDefaultIdentityDriver,
|
|
12
|
+
type IdentityDriver,
|
|
13
|
+
} from '../../../src/libs/drivers/identity';
|
|
14
|
+
|
|
15
|
+
const TENANT_A = 'did:abt:zHOSTA';
|
|
16
|
+
const TENANT_B = 'did:abt:zHOSTB';
|
|
17
|
+
|
|
18
|
+
function buildApp() {
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
app.use('*', ensureI18n());
|
|
21
|
+
app.use('*', contextMiddleware());
|
|
22
|
+
app.get('/whoami', (c) => c.json({ tenant: getInstanceDid(), locale: c.get('locale') }));
|
|
23
|
+
return app;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const get = (app: Hono, host: string, extra: Record<string, string> = {}, path = '/whoami') =>
|
|
27
|
+
app.fetch(new Request(`http://${host}${path}`, { headers: { host, ...extra } }));
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
31
|
+
setIdentityDriver(createDefaultIdentityDriver());
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('hono ensureI18n', () => {
|
|
35
|
+
it('sets locale from ?locale and defaults to en', async () => {
|
|
36
|
+
const app = buildApp();
|
|
37
|
+
expect((await (await get(app, 'x.example.com', {}, '/whoami?locale=zh')).json()).locale).toBe('zh');
|
|
38
|
+
expect((await (await get(app, 'x.example.com')).json()).locale).toBe('en');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('hono contextMiddleware — single mode (default test env)', () => {
|
|
43
|
+
it('resolves to the default tenant regardless of host', async () => {
|
|
44
|
+
const app = buildApp();
|
|
45
|
+
const expected = getDefaultInstanceDid();
|
|
46
|
+
expect((await (await get(app, 'a.example.com')).json()).tenant).toBe(expected);
|
|
47
|
+
expect((await (await get(app, 'other.example.com')).json()).tenant).toBe(expected);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('hono contextMiddleware — multi mode (Host→tenant, fail-closed)', () => {
|
|
52
|
+
const identity: IdentityDriver = {
|
|
53
|
+
resolveInstanceDidForHost(host) {
|
|
54
|
+
if (host === 'a.example.com') return TENANT_A;
|
|
55
|
+
if (host === 'b.example.com') return TENANT_B;
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
62
|
+
setIdentityDriver(identity);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('two hosts see two tenants (data isolation)', async () => {
|
|
66
|
+
const app = buildApp();
|
|
67
|
+
expect((await (await get(app, 'a.example.com')).json()).tenant).toBe(TENANT_A);
|
|
68
|
+
expect((await (await get(app, 'b.example.com')).json()).tenant).toBe(TENANT_B);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('unknown host fails closed 400 + error code, no tenant leak', async () => {
|
|
72
|
+
const r = await get(buildApp(), 'unknown.example.com');
|
|
73
|
+
expect(r.status).toBe(400);
|
|
74
|
+
const body = await r.json();
|
|
75
|
+
expect(body.error.code).toBe('TENANT_HOST_UNRESOLVED');
|
|
76
|
+
expect(body.tenant).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('SECURITY: a forged X-Forwarded-Host cannot change resolution (raw Host only)', async () => {
|
|
80
|
+
const r = await get(buildApp(), 'a.example.com', { 'x-forwarded-host': 'b.example.com' });
|
|
81
|
+
expect((await r.json()).tenant).toBe(TENANT_A);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('SECURITY: X-Forwarded-Host pointing at a known tenant cannot rescue an unknown raw Host', async () => {
|
|
85
|
+
const r = await get(buildApp(), 'unknown.example.com', { 'x-forwarded-host': 'a.example.com' });
|
|
86
|
+
expect(r.status).toBe(400);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('an empty / malformed Host fails closed without crashing', async () => {
|
|
90
|
+
// host supplied via the Host header (fixed URL) so an empty/garbage value
|
|
91
|
+
// does not break URL construction; the fork reads c.req.header('host').
|
|
92
|
+
const app = buildApp();
|
|
93
|
+
const empty = await app.fetch(new Request('http://req.local/whoami', { headers: { host: '' } }));
|
|
94
|
+
expect(empty.status).toBe(400);
|
|
95
|
+
const garbage = await app.fetch(new Request('http://req.local/whoami', { headers: { host: ':::garbage:::' } }));
|
|
96
|
+
expect(garbage.status).toBe(400);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('a 4xx-rejected request never reaches the route handler (no write)', async () => {
|
|
100
|
+
let handlerRuns = 0;
|
|
101
|
+
const app = new Hono();
|
|
102
|
+
app.use('*', contextMiddleware());
|
|
103
|
+
app.get('/whoami', (c) => {
|
|
104
|
+
handlerRuns += 1;
|
|
105
|
+
return c.json({ tenant: getInstanceDid() });
|
|
106
|
+
});
|
|
107
|
+
const r = await app.fetch(
|
|
108
|
+
new Request('http://unknown.example.com/whoami', { headers: { host: 'unknown.example.com' } })
|
|
109
|
+
);
|
|
110
|
+
expect(r.status).toBe(400);
|
|
111
|
+
expect(handlerRuns).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono csrf fork parity with @blocklet/sdk csrf.
|
|
2
|
+
// The crypto core is reused verbatim, so tokens are interchangeable across
|
|
3
|
+
// engines. BLOCKLET_APP_SK (csrf secret) is set by tools/jest-setup.js, so we
|
|
4
|
+
// assert COMPUTED consistency (response token == sign(secret, loginToken)),
|
|
5
|
+
// never a fixed byte string.
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
8
|
+
import { sign, getCsrfSecret } from '@blocklet/sdk/lib/util/csrf';
|
|
9
|
+
import { csrf } from '../../../src/middlewares/hono/csrf';
|
|
10
|
+
|
|
11
|
+
const LOGIN = 'login_token_abc.payload.sig';
|
|
12
|
+
|
|
13
|
+
function buildApp() {
|
|
14
|
+
const app = new Hono();
|
|
15
|
+
app.use('*', csrf());
|
|
16
|
+
app.get('/api/ping', (c) => c.json({ ok: true }));
|
|
17
|
+
app.post('/api/mutate', (c) => c.json({ mutated: true }));
|
|
18
|
+
app.post('/api/mcp/call', (c) => c.json({ mcp: true }));
|
|
19
|
+
return app;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const parseCsrfCookie = (res: Response): string | null => {
|
|
23
|
+
const raw = res.headers.get('set-cookie') || '';
|
|
24
|
+
const m = raw.match(/x-csrf-token=([^;]+)/);
|
|
25
|
+
return m ? decodeURIComponent(m[1] as string) : null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('hono csrf — happy path', () => {
|
|
29
|
+
it('GET with login_token issues x-csrf-token == SDK sign(secret, loginToken)', async () => {
|
|
30
|
+
const app = buildApp();
|
|
31
|
+
const res = await app.fetch(new Request('http://x/api/ping', { headers: { cookie: `login_token=${LOGIN}` } }));
|
|
32
|
+
expect(res.status).toBe(200);
|
|
33
|
+
expect(parseCsrfCookie(res)).toBe(sign(getCsrfSecret(), LOGIN));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('POST with matching cookie + header is allowed', async () => {
|
|
37
|
+
const token = sign(getCsrfSecret(), LOGIN);
|
|
38
|
+
const app = buildApp();
|
|
39
|
+
const res = await app.fetch(
|
|
40
|
+
new Request('http://x/api/mutate', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { cookie: `login_token=${LOGIN}; x-csrf-token=${token}`, 'x-csrf-token': token },
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
expect(res.status).toBe(200);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('hono csrf — bad input + security', () => {
|
|
50
|
+
it('POST with a mismatched header is rejected 403', async () => {
|
|
51
|
+
const token = sign(getCsrfSecret(), LOGIN);
|
|
52
|
+
const app = buildApp();
|
|
53
|
+
const res = await app.fetch(
|
|
54
|
+
new Request('http://x/api/mutate', {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { cookie: `login_token=${LOGIN}; x-csrf-token=${token}`, 'x-csrf-token': 'tampered' },
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
expect(res.status).toBe(403);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('POST without login_token is skipped (parity with SDK shouldVerifyToken)', async () => {
|
|
63
|
+
const app = buildApp();
|
|
64
|
+
const res = await app.fetch(new Request('http://x/api/mutate', { method: 'POST' }));
|
|
65
|
+
expect(res.status).toBe(200);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('POST with login_token but no existing x-csrf-token cookie is skipped (SDK only enforces when cookie present)', async () => {
|
|
69
|
+
const app = buildApp();
|
|
70
|
+
const res = await app.fetch(
|
|
71
|
+
new Request('http://x/api/mutate', { method: 'POST', headers: { cookie: `login_token=${LOGIN}` } })
|
|
72
|
+
);
|
|
73
|
+
expect(res.status).toBe(200);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('a /mcp path is never verified (skip)', async () => {
|
|
77
|
+
const token = sign(getCsrfSecret(), LOGIN);
|
|
78
|
+
const app = buildApp();
|
|
79
|
+
const res = await app.fetch(
|
|
80
|
+
new Request('http://x/api/mcp/call', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { cookie: `login_token=${LOGIN}; x-csrf-token=${token}`, 'x-csrf-token': 'tampered' },
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
expect(res.status).toBe(200);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('a DID Wallet connect request (arcwallet user-agent) is skipped', async () => {
|
|
89
|
+
const token = sign(getCsrfSecret(), LOGIN);
|
|
90
|
+
const app = buildApp();
|
|
91
|
+
const res = await app.fetch(
|
|
92
|
+
new Request('http://x/api/mutate', {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
cookie: `login_token=${LOGIN}; x-csrf-token=${token}`,
|
|
96
|
+
'x-csrf-token': 'tampered',
|
|
97
|
+
'user-agent': 'ArcWallet/2.9.0 (iOS)',
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
expect(res.status).toBe(200);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('a token an express SDK issued verifies under the hono port (interoperable)', async () => {
|
|
105
|
+
const expressIssued = sign(getCsrfSecret(), LOGIN);
|
|
106
|
+
const app = buildApp();
|
|
107
|
+
const res = await app.fetch(
|
|
108
|
+
new Request('http://x/api/mutate', {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { cookie: `login_token=${LOGIN}; x-csrf-token=${expressIssued}`, 'x-csrf-token': expressIssued },
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
expect(res.status).toBe(200);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('hono csrf — data damage (cookie attributes)', () => {
|
|
118
|
+
it('the issued cookie carries SameSite=Strict and Secure (parity with express res.cookie)', async () => {
|
|
119
|
+
const app = buildApp();
|
|
120
|
+
const res = await app.fetch(new Request('http://x/api/ping', { headers: { cookie: `login_token=${LOGIN}` } }));
|
|
121
|
+
const raw = res.headers.get('set-cookie') || '';
|
|
122
|
+
expect(raw).toMatch(/SameSite=Strict/i);
|
|
123
|
+
expect(raw).toMatch(/Secure/i);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('hono csrf — data leak (secret never serialized)', () => {
|
|
128
|
+
it('the raw csrf secret never appears in any response header or body', async () => {
|
|
129
|
+
const secret = getCsrfSecret();
|
|
130
|
+
const app = buildApp();
|
|
131
|
+
const res = await app.fetch(new Request('http://x/api/ping', { headers: { cookie: `login_token=${LOGIN}` } }));
|
|
132
|
+
const headerDump = [...res.headers.entries()].map(([k, v]) => `${k}:${v}`).join('\n');
|
|
133
|
+
expect(headerDump).not.toContain(secret); // only the signed token (an HMAC of it) is exposed
|
|
134
|
+
expect(await res.text()).not.toContain(secret);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono fallback fork. SPA index.html with OG meta +
|
|
2
|
+
// theme injection. Ported verbatim from the SDK; only req/res → hono. Inert
|
|
3
|
+
// until SPA serving moves off the bridge (Phase 4); unit-tested here.
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import { fallback } from '../../../src/middlewares/hono/fallback';
|
|
9
|
+
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hono-fallback-'));
|
|
11
|
+
const INDEX = path.join(dir, 'index.html');
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
fs.writeFileSync(INDEX, '<html><head><title>orig</title></head><body>app</body></html>');
|
|
15
|
+
});
|
|
16
|
+
afterAll(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
17
|
+
|
|
18
|
+
function buildApp() {
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
app.use('*', fallback('index.html', { root: dir }));
|
|
21
|
+
app.get('*', (c) => c.text('not-fallback', 404));
|
|
22
|
+
return app;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const get = (app: Hono, p: string, accept = 'text/html') =>
|
|
26
|
+
app.fetch(new Request(`http://x${p}`, { headers: { accept } }));
|
|
27
|
+
|
|
28
|
+
describe('hono fallback — happy path', () => {
|
|
29
|
+
it('serves the index.html with OG + theme injection for an html GET', async () => {
|
|
30
|
+
const res = await get(buildApp(), '/some/spa/route');
|
|
31
|
+
expect(res.status).toBe(200);
|
|
32
|
+
expect(res.headers.get('content-type')).toMatch(/text\/html/);
|
|
33
|
+
const body = await res.text();
|
|
34
|
+
expect(body).toContain('<meta property="og:image"'); // OG injected
|
|
35
|
+
expect(body).toContain('app'); // original body preserved
|
|
36
|
+
expect(res.headers.get('X-Cache')).toBe('MISS');
|
|
37
|
+
expect(res.headers.get('ETag')).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('a second identical request is served from cache (X-Cache HIT)', async () => {
|
|
41
|
+
const app = buildApp();
|
|
42
|
+
await get(app, '/cached/route');
|
|
43
|
+
const res2 = await get(app, '/cached/route');
|
|
44
|
+
expect(res2.headers.get('X-Cache')).toBe('HIT');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('hono fallback — bad input (does not take over non-html / resource)', () => {
|
|
49
|
+
it('passes through (next) when the client does not accept html', async () => {
|
|
50
|
+
const res = await get(buildApp(), '/spa', 'application/json');
|
|
51
|
+
expect(res.status).toBe(404);
|
|
52
|
+
expect(await res.text()).toBe('not-fallback');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('passes through for a resource path (e.g. .png)', async () => {
|
|
56
|
+
const res = await get(buildApp(), '/logo.png');
|
|
57
|
+
expect(res.status).toBe(404);
|
|
58
|
+
expect(await res.text()).toBe('not-fallback');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('passes through for a non-GET/HEAD method', async () => {
|
|
62
|
+
const res = await buildApp().fetch(
|
|
63
|
+
new Request('http://x/spa', { method: 'POST', headers: { accept: 'text/html' } })
|
|
64
|
+
);
|
|
65
|
+
expect(res.status).toBe(404);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// express→hono — native pipeline INTEGRATION test.
|
|
2
|
+
//
|
|
3
|
+
// Drives the REAL buildHonoApp + configureNativePipeline (the test stub
|
|
4
|
+
// /api/__e2e/echo is mounted under NODE_ENV=test) and proves native routes get
|
|
5
|
+
// the full app-shell chain (cors→xss→csrf→ensureI18n→cdn→context): csrf issues a
|
|
6
|
+
// token, i18n sets locale, xss sanitizes body but NOT query.
|
|
7
|
+
//
|
|
8
|
+
// Phase 4: the catch-all loopback bridge is gone (hono is the only entry), so the
|
|
9
|
+
// former "bridge isolation" assertions were removed along with the express
|
|
10
|
+
// backend they exercised.
|
|
11
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
12
|
+
import { sign, getCsrfSecret } from '@blocklet/sdk/lib/util/csrf';
|
|
13
|
+
import { buildHonoApp } from '../../../src/service';
|
|
14
|
+
import { configureNativePipeline } from '../../../src/middlewares/hono/pipeline';
|
|
15
|
+
|
|
16
|
+
let app: ReturnType<typeof buildHonoApp>;
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
app = buildHonoApp(configureNativePipeline);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const call = (path: string, init?: RequestInit) => app.fetch(new Request(`http://app.local${path}`, init));
|
|
23
|
+
|
|
24
|
+
describe('native pipeline — full chain on a native route', () => {
|
|
25
|
+
it('csrf issues a token + i18n sets locale on GET /api/__e2e/echo', async () => {
|
|
26
|
+
const login = 'login_token_pipeline.aaa.bbb';
|
|
27
|
+
const res = await call('/api/__e2e/echo?locale=fr', { headers: { cookie: `login_token=${login}` } });
|
|
28
|
+
expect(res.status).toBe(200);
|
|
29
|
+
// csrf ran: Set-Cookie x-csrf-token == sign(secret, login)
|
|
30
|
+
const setCookie = res.headers.get('set-cookie') || '';
|
|
31
|
+
expect(setCookie).toContain(`x-csrf-token=${encodeURIComponent(sign(getCsrfSecret(), login))}`);
|
|
32
|
+
// i18n ran: locale echoed
|
|
33
|
+
expect((await res.json()).locale).toBe('fr');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('xss sanitizes the body but NOT the query (locked §7 narrowing)', async () => {
|
|
37
|
+
const res = await call('/api/__e2e/echo?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'content-type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ name: '<script>alert(1)</script>hi' }),
|
|
41
|
+
});
|
|
42
|
+
const body = await res.json();
|
|
43
|
+
expect(body.body.name).not.toContain('<script>');
|
|
44
|
+
expect(body.body.name).toContain('hi');
|
|
45
|
+
expect(body.query).toBe('<script>alert(1)</script>'); // query un-sanitized
|
|
46
|
+
});
|
|
47
|
+
});
|