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,245 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { Sequelize } from 'sequelize';
|
|
5
|
+
import { SequelizeStorage, Umzug } from 'umzug';
|
|
6
|
+
|
|
7
|
+
import { withTenant } from '../../src/libs/context';
|
|
8
|
+
import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
|
|
9
|
+
import { systemFindByPk } from '../../src/store/scoped';
|
|
10
|
+
import { TENANT_A, TENANT_B } from '../fixtures/tenants';
|
|
11
|
+
|
|
12
|
+
jest.mock('../../src/libs/logger', () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const STORE_DIR = path.join(__dirname, '../../src/store');
|
|
18
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tenant-matrix-a-'));
|
|
19
|
+
const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
|
|
20
|
+
const umzug = new Umzug({
|
|
21
|
+
migrations: {
|
|
22
|
+
glob: ['migrations/*.ts', { cwd: STORE_DIR }],
|
|
23
|
+
resolve: ({ name, path: p, context }) => {
|
|
24
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
25
|
+
const migration = require(p!);
|
|
26
|
+
return {
|
|
27
|
+
name: name.replace(/\.ts$/, '.js'),
|
|
28
|
+
up: () => migration.up({ context }),
|
|
29
|
+
down: () => migration.down({ context }),
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
context: sequelize.getQueryInterface(),
|
|
34
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
35
|
+
logger: undefined,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let models: any;
|
|
39
|
+
let createQueue: any;
|
|
40
|
+
let assertJobObjectTenant: any;
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
await umzug.up();
|
|
44
|
+
// eslint-disable-next-line global-require
|
|
45
|
+
models = require('../../src/store/models');
|
|
46
|
+
models.initialize(sequelize);
|
|
47
|
+
// eslint-disable-next-line global-require
|
|
48
|
+
const queueModule = require('../../src/libs/queue');
|
|
49
|
+
createQueue = queueModule.default;
|
|
50
|
+
assertJobObjectTenant = queueModule.assertJobObjectTenant;
|
|
51
|
+
}, 120000);
|
|
52
|
+
|
|
53
|
+
afterAll(async () => {
|
|
54
|
+
await sequelize.close();
|
|
55
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const waitFor = (emitter: any, events: string[]): Promise<{ event: string; data: any }> =>
|
|
59
|
+
new Promise((resolve) => {
|
|
60
|
+
for (const event of events) {
|
|
61
|
+
emitter.on(event, (data: any) => resolve({ event, data }));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('queue tenant layer — first batch (phase 5)', () => {
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
await sequelize.query('DELETE FROM customers');
|
|
68
|
+
await sequelize.query('DELETE FROM jobs');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('happy path', () => {
|
|
72
|
+
it('push stamps the active tenant into the payload, job row included, handler runs in that tenant', async () => {
|
|
73
|
+
const seen: string[] = [];
|
|
74
|
+
const queue = createQueue({
|
|
75
|
+
name: `tm-happy-${Date.now()}`,
|
|
76
|
+
onJob: async (job: any) => {
|
|
77
|
+
// eslint-disable-next-line global-require
|
|
78
|
+
const { getInstanceDid } = require('../../src/libs/context');
|
|
79
|
+
seen.push(job.instance_did, getInstanceDid());
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const handle = await withTenant(TENANT_A, async () => queue.push({ job: { businessId: 'x1' }, persist: true }));
|
|
84
|
+
await waitFor(handle, ['finished', 'failed']);
|
|
85
|
+
expect(seen).toEqual([TENANT_A, TENANT_A]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('handler object guard passes for matching tenants', async () => {
|
|
89
|
+
const customer = await withTenant(TENANT_A, () =>
|
|
90
|
+
models.Customer.create({ livemode: false, did: 'z-m-a', delinquent: false, instance_did: TENANT_A })
|
|
91
|
+
);
|
|
92
|
+
await withTenant(TENANT_A, async () => {
|
|
93
|
+
expect(() => assertJobObjectTenant(customer)).not.toThrow();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('bad input', () => {
|
|
99
|
+
it('multi mode push without tenant context is rejected at the gate', async () => {
|
|
100
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
101
|
+
try {
|
|
102
|
+
const queue = createQueue({ name: `tm-bad-${Date.now()}`, onJob: async () => {} });
|
|
103
|
+
expect(() => queue.push({ job: { businessId: 'x' } })).toThrow(
|
|
104
|
+
expect.objectContaining({ code: TENANT_CONTEXT_MISSING })
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('illegal tenant DID in the payload is refused', () => {
|
|
112
|
+
const queue = createQueue({ name: `tm-bad2-${Date.now()}`, onJob: async () => {} });
|
|
113
|
+
expect(() => queue.push({ job: { businessId: 'x', instance_did: 'a b' } })).toThrow(
|
|
114
|
+
expect.objectContaining({ code: TENANT_CONTEXT_MISSING })
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('security: forged payload tenant vs object tenant', () => {
|
|
120
|
+
it('handler refuses to act on another tenant object; the object is untouched', async () => {
|
|
121
|
+
const victim = await withTenant(TENANT_B, () =>
|
|
122
|
+
models.Customer.create({ livemode: false, did: 'z-victim', delinquent: false, instance_did: TENANT_B })
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
let observedError: any;
|
|
126
|
+
const queue = createQueue({
|
|
127
|
+
name: `tm-forged-${Date.now()}`,
|
|
128
|
+
onJob: async (job: any) => {
|
|
129
|
+
// handlers load the tenant-stamped object cross-tenant (system) then
|
|
130
|
+
// enforce its tenant — mirrors the real queue handlers so a forged
|
|
131
|
+
// payload surfaces an observable TENANT_MISMATCH rather than folding
|
|
132
|
+
// into a scoped null (tenant-design §10).
|
|
133
|
+
const row: any = await systemFindByPk(models.Customer, job.customerId);
|
|
134
|
+
assertJobObjectTenant(row); // throws TENANT_MISMATCH
|
|
135
|
+
await row.update({ name: 'pwned' });
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
// forge: payload claims TENANT_A but targets B's customer
|
|
139
|
+
const handle = await withTenant(TENANT_A, async () =>
|
|
140
|
+
queue.push({ job: { customerId: victim.id }, persist: true })
|
|
141
|
+
);
|
|
142
|
+
const outcome = await waitFor(handle, ['failed', 'finished']);
|
|
143
|
+
observedError = (outcome.data as any).error;
|
|
144
|
+
|
|
145
|
+
expect(outcome.event).toBe('failed');
|
|
146
|
+
expect(observedError?.code).toBe(TENANT_MISMATCH);
|
|
147
|
+
const reloaded: any = await systemFindByPk(models.Customer, victim.id);
|
|
148
|
+
expect(reloaded.name).toBeNull(); // not 'pwned'
|
|
149
|
+
expect(reloaded.instance_did).toBe(TENANT_B);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('data loss: refused jobs surface as failures, not silence', () => {
|
|
154
|
+
it('the failed event fires with the tenant error attached', async () => {
|
|
155
|
+
const queue = createQueue({
|
|
156
|
+
name: `tm-fail-${Date.now()}`,
|
|
157
|
+
onJob: async () => {
|
|
158
|
+
const err: any = new Error('refused');
|
|
159
|
+
err.code = TENANT_MISMATCH;
|
|
160
|
+
err.nonRetryable = true;
|
|
161
|
+
throw err;
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const handle = await withTenant(TENANT_A, async () => queue.push({ job: { businessId: 'x' }, persist: true }));
|
|
165
|
+
const outcome = await waitFor(handle, ['failed', 'finished']);
|
|
166
|
+
expect(outcome.event).toBe('failed');
|
|
167
|
+
expect((outcome.data as any).error?.code).toBe(TENANT_MISMATCH);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('data damage: retries keep the original tenant', () => {
|
|
172
|
+
it('a retried job re-executes under the same tenant, never a different one', async () => {
|
|
173
|
+
const tenantsSeen: string[] = [];
|
|
174
|
+
const queue = createQueue({
|
|
175
|
+
name: `tm-retry-${Date.now()}`,
|
|
176
|
+
onJob: async (job: any) => {
|
|
177
|
+
tenantsSeen.push(job.instance_did);
|
|
178
|
+
if (tenantsSeen.length < 2) throw new Error('transient');
|
|
179
|
+
},
|
|
180
|
+
options: { maxRetries: 2, retryDelay: 1 },
|
|
181
|
+
});
|
|
182
|
+
const handle = await withTenant(TENANT_B, async () => queue.push({ job: { businessId: 'r1' }, persist: true }));
|
|
183
|
+
await waitFor(handle, ['finished', 'failed']);
|
|
184
|
+
expect(tenantsSeen.length).toBeGreaterThanOrEqual(2);
|
|
185
|
+
expect(new Set(tenantsSeen)).toEqual(new Set([TENANT_B]));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// NOTE: spec's "channel-identifier job id prefix" row converts to a no-op
|
|
190
|
+
// for Phase 5 queues — all job ids are internal globally-unique business
|
|
191
|
+
// ids (see queue-matrix.md); channel-identifier dedup moves to Phase 6
|
|
192
|
+
// integrations as (instance_did, identifier) composite keys.
|
|
193
|
+
describe('data leak: legacy job strategy', () => {
|
|
194
|
+
it('a pre-tenant job (no instance_did) executes under the default tenant in single mode', async () => {
|
|
195
|
+
const seen: string[] = [];
|
|
196
|
+
const queue = createQueue({
|
|
197
|
+
name: `tm-legacy-${Date.now()}`,
|
|
198
|
+
onJob: async () => {
|
|
199
|
+
// eslint-disable-next-line global-require
|
|
200
|
+
const { getInstanceDid } = require('../../src/libs/context');
|
|
201
|
+
seen.push(getInstanceDid());
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
// simulate a legacy row: bypass push's injection by writing the store directly
|
|
205
|
+
await queue.store.addJob('legacy-1', { businessId: 'legacy' }, {});
|
|
206
|
+
const row = await queue.get('legacy-1');
|
|
207
|
+
expect(row.instance_did).toBeUndefined();
|
|
208
|
+
// re-deliver it the way startup recovery does (push with persist=false keeps payload as-is? no — push injects)
|
|
209
|
+
// execution path: runJobWithTenant falls back to the default tenant
|
|
210
|
+
const handle = queue.push({
|
|
211
|
+
job: row,
|
|
212
|
+
id: 'legacy-1',
|
|
213
|
+
persist: false,
|
|
214
|
+
skipDuplicateCheck: true,
|
|
215
|
+
fromStore: true,
|
|
216
|
+
});
|
|
217
|
+
await waitFor(handle, ['finished', 'failed']);
|
|
218
|
+
// eslint-disable-next-line global-require
|
|
219
|
+
const { getDefaultInstanceDid } = require('../../src/libs/tenant');
|
|
220
|
+
expect(seen).toEqual([getDefaultInstanceDid()]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('a legacy job is refused permanently in multi mode (structured, non-retryable)', async () => {
|
|
224
|
+
const queue = createQueue({
|
|
225
|
+
name: `tm-legacy-multi-${Date.now()}`,
|
|
226
|
+
onJob: async () => 'should-not-run',
|
|
227
|
+
});
|
|
228
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
229
|
+
try {
|
|
230
|
+
const handle = queue.push({
|
|
231
|
+
job: { businessId: 'legacy-multi' },
|
|
232
|
+
id: 'legacy-multi-1',
|
|
233
|
+
persist: false,
|
|
234
|
+
fromStore: true, // store re-delivery path: gate skipped, execution-side refuses
|
|
235
|
+
});
|
|
236
|
+
const outcome = await waitFor(handle, ['failed', 'finished']);
|
|
237
|
+
expect(outcome.event).toBe('failed');
|
|
238
|
+
expect((outcome.data as any).error?.code).toBe(TENANT_CONTEXT_MISSING);
|
|
239
|
+
expect((outcome.data as any).error?.nonRetryable).toBe(true);
|
|
240
|
+
} finally {
|
|
241
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { Sequelize } from 'sequelize';
|
|
5
|
+
import { SequelizeStorage, Umzug } from 'umzug';
|
|
6
|
+
|
|
7
|
+
import { withTenant } from '../../src/libs/context';
|
|
8
|
+
import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
|
|
9
|
+
import { systemFindByPk } from '../../src/store/scoped';
|
|
10
|
+
import { TENANT_A, TENANT_B } from '../fixtures/tenants';
|
|
11
|
+
|
|
12
|
+
jest.mock('../../src/libs/logger', () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const STORE_DIR = path.join(__dirname, '../../src/store');
|
|
18
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tenant-matrix-b-'));
|
|
19
|
+
const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
|
|
20
|
+
const umzug = new Umzug({
|
|
21
|
+
migrations: {
|
|
22
|
+
glob: ['migrations/*.ts', { cwd: STORE_DIR }],
|
|
23
|
+
resolve: ({ name, path: p, context }) => {
|
|
24
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
25
|
+
const migration = require(p!);
|
|
26
|
+
return {
|
|
27
|
+
name: name.replace(/\.ts$/, '.js'),
|
|
28
|
+
up: () => migration.up({ context }),
|
|
29
|
+
down: () => migration.down({ context }),
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
context: sequelize.getQueryInterface(),
|
|
34
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
35
|
+
logger: undefined,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let models: any;
|
|
39
|
+
let createQueue: any;
|
|
40
|
+
let assertJobObjectTenant: any;
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
await umzug.up();
|
|
44
|
+
// eslint-disable-next-line global-require
|
|
45
|
+
models = require('../../src/store/models');
|
|
46
|
+
models.initialize(sequelize);
|
|
47
|
+
// eslint-disable-next-line global-require
|
|
48
|
+
const queueModule = require('../../src/libs/queue');
|
|
49
|
+
createQueue = queueModule.default;
|
|
50
|
+
assertJobObjectTenant = queueModule.assertJobObjectTenant;
|
|
51
|
+
}, 120000);
|
|
52
|
+
|
|
53
|
+
afterAll(async () => {
|
|
54
|
+
await sequelize.close();
|
|
55
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const waitFor = (emitter: any, events: string[]): Promise<{ event: string; data: any }> =>
|
|
59
|
+
new Promise((resolve) => {
|
|
60
|
+
for (const event of events) {
|
|
61
|
+
emitter.on(event, (data: any) => resolve({ event, data }));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('queue tenant layer — remaining queues (phase 6)', () => {
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
await sequelize.query('DELETE FROM coupons');
|
|
68
|
+
await sequelize.query('DELETE FROM jobs');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('happy path + data damage: phase 6 object-bound handler pattern', () => {
|
|
72
|
+
it('a coupon-bound job (discount-status shape) executes only for its own tenant', async () => {
|
|
73
|
+
const coupon = await withTenant(TENANT_B, () =>
|
|
74
|
+
models.Coupon.create({
|
|
75
|
+
livemode: false,
|
|
76
|
+
instance_did: TENANT_B,
|
|
77
|
+
name: 'b-coupon',
|
|
78
|
+
duration: 'once',
|
|
79
|
+
valid: true,
|
|
80
|
+
created_via: 'api',
|
|
81
|
+
percent_off: 10,
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const executed: string[] = [];
|
|
86
|
+
const queue = createQueue({
|
|
87
|
+
name: `tmb-coupon-${Date.now()}`,
|
|
88
|
+
onJob: async (job: any) => {
|
|
89
|
+
// load cross-tenant (system) then enforce — mirrors real handlers
|
|
90
|
+
const row: any = await systemFindByPk(models.Coupon, job.id);
|
|
91
|
+
assertJobObjectTenant(row);
|
|
92
|
+
executed.push(row.id);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// same tenant: executes
|
|
97
|
+
const ok = await withTenant(TENANT_B, async () => queue.push({ job: { id: coupon.id }, persist: true }));
|
|
98
|
+
const okOutcome = await waitFor(ok, ['finished', 'failed']);
|
|
99
|
+
expect(okOutcome.event).toBe('finished');
|
|
100
|
+
expect(executed).toEqual([coupon.id]);
|
|
101
|
+
|
|
102
|
+
// forged tenant: refused
|
|
103
|
+
const forged = await withTenant(TENANT_A, async () =>
|
|
104
|
+
queue.push({ job: { id: coupon.id }, id: `forged-${coupon.id}`, persist: true })
|
|
105
|
+
);
|
|
106
|
+
const forgedOutcome = await waitFor(forged, ['finished', 'failed']);
|
|
107
|
+
expect(forgedOutcome.event).toBe('failed');
|
|
108
|
+
expect((forgedOutcome.data as any).error?.code).toBe(TENANT_MISMATCH);
|
|
109
|
+
expect(executed).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('data loss: in-flight legacy jobs across an upgrade window', () => {
|
|
114
|
+
it('single mode consumes a pre-tenant job normally (default tenant)', async () => {
|
|
115
|
+
const seen: string[] = [];
|
|
116
|
+
const queue = createQueue({
|
|
117
|
+
name: `tmb-legacy-${Date.now()}`,
|
|
118
|
+
onJob: async () => {
|
|
119
|
+
// eslint-disable-next-line global-require
|
|
120
|
+
const { getInstanceDid } = require('../../src/libs/context');
|
|
121
|
+
seen.push(getInstanceDid());
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
await queue.store.addJob('legacy-b-1', { anything: true }, {});
|
|
125
|
+
const row = await queue.get('legacy-b-1');
|
|
126
|
+
const handle = queue.push({
|
|
127
|
+
job: row,
|
|
128
|
+
id: 'legacy-b-1',
|
|
129
|
+
persist: false,
|
|
130
|
+
skipDuplicateCheck: true,
|
|
131
|
+
fromStore: true,
|
|
132
|
+
});
|
|
133
|
+
const outcome = await waitFor(handle, ['finished', 'failed']);
|
|
134
|
+
expect(outcome.event).toBe('finished');
|
|
135
|
+
// eslint-disable-next-line global-require
|
|
136
|
+
const { getDefaultInstanceDid } = require('../../src/libs/tenant');
|
|
137
|
+
expect(seen).toEqual([getDefaultInstanceDid()]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('data leak: multi mode never falls back to a default tenant', () => {
|
|
142
|
+
it('a legacy job is refused with a structured non-retryable error and never executes', async () => {
|
|
143
|
+
const executed: any[] = [];
|
|
144
|
+
const queue = createQueue({
|
|
145
|
+
name: `tmb-legacy-multi-${Date.now()}`,
|
|
146
|
+
onJob: async (job: any) => {
|
|
147
|
+
executed.push(job);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
151
|
+
try {
|
|
152
|
+
const handle = queue.push({
|
|
153
|
+
job: { anything: true },
|
|
154
|
+
id: 'legacy-multi-b',
|
|
155
|
+
persist: false,
|
|
156
|
+
fromStore: true,
|
|
157
|
+
});
|
|
158
|
+
const outcome = await waitFor(handle, ['finished', 'failed']);
|
|
159
|
+
expect(outcome.event).toBe('failed');
|
|
160
|
+
expect((outcome.data as any).error?.code).toBe(TENANT_CONTEXT_MISSING);
|
|
161
|
+
expect((outcome.data as any).error?.nonRetryable).toBe(true);
|
|
162
|
+
expect(executed).toHaveLength(0);
|
|
163
|
+
} finally {
|
|
164
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Phase 2 (express→hono) — DID-Connect surface migration. Proves the SAME
|
|
2
|
+
// @blocklet/sdk WalletHandlers attach to a hono app via did-connect-js v4 native
|
|
3
|
+
// attachHono (isHonoApp dispatch, design §3.3): all routes register and a request
|
|
4
|
+
// is dispatched to the generateSession handler over app.fetch. The 14 handlers
|
|
5
|
+
// are unchanged (framework-agnostic CallbackArgs); only the app type changes.
|
|
6
|
+
//
|
|
7
|
+
// NOTE: the session-DEPENDENT spec-table categories (Security ensureSignedJson,
|
|
8
|
+
// Data-loss deep-link, Data-damage appInfo, Data-leak token isolation) and the
|
|
9
|
+
// happy-path session SHAPE are validated over a REAL socket in
|
|
10
|
+
// scripts/e2e-hono-s2.ts (10 self-validating checks, logs/s2-e2e.log). They
|
|
11
|
+
// cannot run in jest: generateSession needs a valid appInfo.icon URI, which jest's
|
|
12
|
+
// bare blocklet env does not provide (the SDK authenticator rejects it). The E2E
|
|
13
|
+
// uses the spike's testSetup env (parsed blocklet.yml → valid appInfo).
|
|
14
|
+
import { buildConnectRoutesHono } from '../../../src/service';
|
|
15
|
+
|
|
16
|
+
const PREFIX = '/api/did';
|
|
17
|
+
// a representative subset of the 14 actions
|
|
18
|
+
const ACTIONS = ['collect', 'payment', 'subscription'];
|
|
19
|
+
const ALL_ACTIONS = [
|
|
20
|
+
'collect',
|
|
21
|
+
'collect-batch',
|
|
22
|
+
'payment',
|
|
23
|
+
'setup',
|
|
24
|
+
'subscription',
|
|
25
|
+
'change-payment',
|
|
26
|
+
'change-plan',
|
|
27
|
+
'recharge',
|
|
28
|
+
'recharge-account',
|
|
29
|
+
'delegation',
|
|
30
|
+
'overdraft-protection',
|
|
31
|
+
're-stake',
|
|
32
|
+
'auto-recharge-auth',
|
|
33
|
+
'change-payer',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
let connectApp: ReturnType<typeof buildConnectRoutesHono>;
|
|
37
|
+
|
|
38
|
+
beforeAll(() => {
|
|
39
|
+
connectApp = buildConnectRoutesHono();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const registeredPaths = (): Set<string> => new Set(((connectApp as any).routes || []).map((r: any) => r.path));
|
|
43
|
+
|
|
44
|
+
describe('Phase 2 — attachHono route registration (all 14 handlers)', () => {
|
|
45
|
+
it('registers token/status/timeout/auth/auth-submit for each action', () => {
|
|
46
|
+
const paths = registeredPaths();
|
|
47
|
+
for (const action of ACTIONS) {
|
|
48
|
+
for (const sub of ['token', 'status', 'timeout', 'auth', 'auth/submit']) {
|
|
49
|
+
expect(paths.has(`${PREFIX}/${action}/${sub}`)).toBe(true);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('registers the full route set (token/status/timeout/auth/auth-submit) for ALL 14 actions', () => {
|
|
55
|
+
const paths = registeredPaths();
|
|
56
|
+
expect(ALL_ACTIONS.length).toBe(14);
|
|
57
|
+
for (const action of ALL_ACTIONS) {
|
|
58
|
+
for (const sub of ['token', 'status', 'timeout', 'auth', 'auth/submit']) {
|
|
59
|
+
expect(paths.has(`${PREFIX}/${action}/${sub}`)).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('a POST to /auth is registered (the signed authInfo endpoint) and dispatched to the handler', async () => {
|
|
65
|
+
const paths = registeredPaths();
|
|
66
|
+
expect(paths.has(`${PREFIX}/payment/auth`)).toBe(true); // POST exists too
|
|
67
|
+
// an unsigned POST with a forged token reaches the handler (500/4xx, never a
|
|
68
|
+
// hono 404) — proving attachHono wired the POST body adapter; the actual
|
|
69
|
+
// ensureSignedJson rejection on a VALID session is in the E2E.
|
|
70
|
+
const res = await connectApp.fetch(
|
|
71
|
+
new Request(`http://app.local${PREFIX}/payment/auth?_t_=deadbeef`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'content-type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ userDid: 'did:abt:zForged', claims: [] }),
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
expect(res.status).not.toBe(404);
|
|
78
|
+
const body = await res.text();
|
|
79
|
+
expect(body).not.toContain('"status":"created"'); // never resolves a session for a forged token
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('Phase 2 — attachHono dispatches GET .../token to the handler (over app.fetch)', () => {
|
|
84
|
+
it('reaches the generateSession handler (200 JSON), not a hono 404', async () => {
|
|
85
|
+
// The full successful session (valid appInfo) is validated over a real socket
|
|
86
|
+
// in scripts/e2e-hono-s2.ts (a valid appInfo.icon URI is an env detail jest's
|
|
87
|
+
// bare blocklet env lacks). Here we prove attachHono ROUTED the request to the
|
|
88
|
+
// did-connect handler: a 200 JSON response, never hono's 404 Not Found.
|
|
89
|
+
const res = await connectApp.fetch(new Request(`http://app.local${PREFIX}/collect/token`));
|
|
90
|
+
expect(res.status).toBe(200);
|
|
91
|
+
const body = await res.json(); // handler output is JSON (session OR a did-connect error)
|
|
92
|
+
expect(typeof body).toBe('object');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Phase 2 — bad input: an invalid/forged session token does not resolve', () => {
|
|
97
|
+
it('GET .../payment/auth with a non-existent _t_ does not return a valid authInfo/session', async () => {
|
|
98
|
+
const res = await connectApp.fetch(
|
|
99
|
+
new Request(`http://app.local${PREFIX}/payment/auth?_t_=deadbeef-not-a-session`)
|
|
100
|
+
);
|
|
101
|
+
// attachHono surfaces the did-connect error path (4xx or an error-status body),
|
|
102
|
+
// never a created session for a forged token.
|
|
103
|
+
const body = await res.json().catch(() => ({}));
|
|
104
|
+
const looksLikeValidSession = res.status === 200 && (body.status === 'created' || body.authInfo);
|
|
105
|
+
expect(looksLikeValidSession).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Phase 4 (express→hono) — adapter collapse.
|
|
2
|
+
//
|
|
3
|
+
// The loopback http.Server + express app shell are gone: svc.http.fetch is now a
|
|
4
|
+
// thin base-strip wrapper over honoApp.fetch, and the full node app is the hono
|
|
5
|
+
// app itself. This spec locks the two things the collapse must preserve byte-for-
|
|
6
|
+
// byte (the arc consumer contract): the base-strip semantics and raw-body
|
|
7
|
+
// fidelity through the strip, plus the hono app's healthz + onError.
|
|
8
|
+
import { Hono } from 'hono';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
import { createFetchHandler } from '../../src/libs/http-fetch-adapter';
|
|
11
|
+
import { buildHonoApp } from '../../src/service';
|
|
12
|
+
|
|
13
|
+
// A stand-in "core" hono app: echoes the internal path + the raw body bytes so we
|
|
14
|
+
// can assert exactly what the adapter forwarded after base-stripping.
|
|
15
|
+
function buildEchoApp(): Hono {
|
|
16
|
+
const app = new Hono();
|
|
17
|
+
app.get('/api/echo', (c) => c.json({ path: new URL(c.req.url).pathname }));
|
|
18
|
+
app.post('/api/raw', async (c) => {
|
|
19
|
+
const buf = Buffer.from(await c.req.arrayBuffer());
|
|
20
|
+
return c.json({ len: buf.length, sig: crypto.createHmac('sha256', 'k').update(buf).digest('hex') });
|
|
21
|
+
});
|
|
22
|
+
return app;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('Phase 4 collapse — svc.http.fetch base-strip over app.fetch', () => {
|
|
26
|
+
const handler = createFetchHandler(buildEchoApp());
|
|
27
|
+
|
|
28
|
+
it('happy: strips the host mount prefix to reach the internal /api/*', async () => {
|
|
29
|
+
const res = await handler(new Request('http://app.local/.well-known/payment/api/echo'), {
|
|
30
|
+
basePath: '/.well-known/payment',
|
|
31
|
+
});
|
|
32
|
+
expect(res.status).toBe(200);
|
|
33
|
+
expect((await res.json()).path).toBe('/api/echo');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('happy: no basePath → request passes through unchanged', async () => {
|
|
37
|
+
const res = await handler(new Request('http://app.local/api/echo'));
|
|
38
|
+
expect((await res.json()).path).toBe('/api/echo');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('bad input: exact segment-boundary strip — basePath itself maps to "/"', async () => {
|
|
42
|
+
// /.well-known/payment (=== basePath) → "/", which the echo app 404s; the
|
|
43
|
+
// point is the strip is applied (not left as the prefix).
|
|
44
|
+
const res = await handler(new Request('http://app.local/.well-known/payment'), {
|
|
45
|
+
basePath: '/.well-known/payment',
|
|
46
|
+
});
|
|
47
|
+
expect(res.status).toBe(404); // "/" has no route — strip happened, didn't match
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('bad input: segment-boundary negative — "/mntbeta" is NOT stripped by basePath "/mnt"', async () => {
|
|
51
|
+
// a naive startsWith would wrongly strip "/mnt" from "/mntbeta/..."; the precise
|
|
52
|
+
// === / "+ /" check must leave it intact (→ /mntbeta/api/echo, 404).
|
|
53
|
+
const res = await handler(new Request('http://app.local/mntbeta/api/echo'), { basePath: '/mnt' });
|
|
54
|
+
expect(res.status).toBe(404);
|
|
55
|
+
// the correctly-prefixed twin strips to /api/echo and resolves
|
|
56
|
+
const ok = await handler(new Request('http://app.local/mnt/api/echo'), { basePath: '/mnt' });
|
|
57
|
+
expect(ok.status).toBe(200);
|
|
58
|
+
expect((await ok.json()).path).toBe('/api/echo');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('security/data loss: raw body bytes survive the strip intact (Stripe webhook fidelity)', async () => {
|
|
62
|
+
const payload = JSON.stringify({ evt: 'x', nested: { a: 1, b: '<b>' } });
|
|
63
|
+
const res = await handler(
|
|
64
|
+
new Request('http://app.local/.well-known/payment/api/raw', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'content-type': 'application/json' },
|
|
67
|
+
body: payload,
|
|
68
|
+
}),
|
|
69
|
+
{ basePath: '/.well-known/payment' }
|
|
70
|
+
);
|
|
71
|
+
const body = await res.json();
|
|
72
|
+
expect(body.len).toBe(Buffer.byteLength(payload)); // every byte forwarded
|
|
73
|
+
expect(body.sig).toBe(crypto.createHmac('sha256', 'k').update(payload).digest('hex'));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Phase 4 collapse — buildHonoApp surface', () => {
|
|
78
|
+
const app = buildHonoApp();
|
|
79
|
+
|
|
80
|
+
it('serves /api/healthz natively (was on the express resource router)', async () => {
|
|
81
|
+
const res = await app.fetch(new Request('http://app.local/api/healthz'));
|
|
82
|
+
expect(res.status).toBe(200);
|
|
83
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('onError maps a thrown error to 500 JSON (express-async-errors equivalent)', async () => {
|
|
87
|
+
const a = buildHonoApp((native) => {
|
|
88
|
+
native.get('/api/boom', () => {
|
|
89
|
+
throw new Error('kaboom');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
const res = await a.fetch(new Request('http://app.local/api/boom'));
|
|
93
|
+
expect(res.status).toBe(500);
|
|
94
|
+
expect((await res.json()).error).toBe('kaboom');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// S3-CF Phase 1 inversion ③ — DID-Connect token storage slot.
|
|
2
|
+
//
|
|
3
|
+
// The DID-Connect token handshake store is now a host-injected slot (same
|
|
4
|
+
// reversal as db/queue/cron). The CF worker injects a D1-backed store so the
|
|
5
|
+
// token state lands in PAYMENT_DB; node hosts omit it and keep the file-backed
|
|
6
|
+
// nedb default. This spec locks the factory→libs/auth wiring and the default.
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
import { Sequelize } from 'sequelize';
|
|
12
|
+
|
|
13
|
+
import * as auth from '../../src/libs/auth';
|
|
14
|
+
import { createEmbeddedPaymentService } from '../../src/service';
|
|
15
|
+
|
|
16
|
+
function freshSequelize(): Sequelize {
|
|
17
|
+
const dbFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'pay-storage-slot-')), 'test.db');
|
|
18
|
+
return new Sequelize({ dialect: 'sqlite', storage: dbFile, logging: false });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const baseConfig = { BLOCKLET_APP_PID: 'zMOCK_STORAGE_SLOT' };
|
|
22
|
+
|
|
23
|
+
describe('Phase 1 (S3-CF) ③ — DID-Connect token storage slot', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
// reset the module-level injection so specs don't leak into each other
|
|
26
|
+
auth.setDidConnectTokenStorage(null);
|
|
27
|
+
jest.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('wires a host-provided storage slot into libs/auth (setDidConnectTokenStorage)', () => {
|
|
31
|
+
const spy = jest.spyOn(auth, 'setDidConnectTokenStorage');
|
|
32
|
+
const sentinel: auth.DidConnectTokenStorage = {
|
|
33
|
+
read: async () => null,
|
|
34
|
+
create: async () => undefined,
|
|
35
|
+
update: async () => undefined,
|
|
36
|
+
delete: async () => undefined,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
createEmbeddedPaymentService({
|
|
40
|
+
config: baseConfig,
|
|
41
|
+
db: { sequelize: freshSequelize() },
|
|
42
|
+
tenancy: { mode: 'single', instanceDid: 'zMOCK_STORAGE_SLOT' },
|
|
43
|
+
storage: sentinel,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(spy).toHaveBeenCalledWith(sentinel);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('omitting the slot leaves the node nedb default (setter never called)', () => {
|
|
50
|
+
const spy = jest.spyOn(auth, 'setDidConnectTokenStorage');
|
|
51
|
+
|
|
52
|
+
createEmbeddedPaymentService({
|
|
53
|
+
config: baseConfig,
|
|
54
|
+
db: { sequelize: freshSequelize() },
|
|
55
|
+
tenancy: { mode: 'single', instanceDid: 'zMOCK_STORAGE_SLOT' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(spy).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|