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,115 @@
|
|
|
1
|
+
// Phase 8 (W2′): the injected-config slot is authoritative. libs/env.ts is the
|
|
2
|
+
// single boundary; the factory wires setCoreConfig(config) and core reads prefer
|
|
3
|
+
// the injected config over process.env (the worker mirror / blocklet server
|
|
4
|
+
// native fallback). getTenantMode's mode-source reads the injected config too.
|
|
5
|
+
|
|
6
|
+
import { setCoreConfig, readConfig, hasConfig } from '../../src/libs/env';
|
|
7
|
+
import { getTenantMode, getDefaultInstanceDid, setDefaultInstanceDid } from '../../src/libs/tenant';
|
|
8
|
+
import { createDefaultSecretsDriver } from '../../src/libs/drivers';
|
|
9
|
+
import { createEmbeddedPaymentService, MissingConfigError } from '../../src/service';
|
|
10
|
+
|
|
11
|
+
const ORIG_MODE = process.env.PAYMENT_TENANT_MODE;
|
|
12
|
+
const ORIG_PID = process.env.BLOCKLET_APP_PID;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
setCoreConfig(undefined); // clear the injected config singleton between tests
|
|
16
|
+
setDefaultInstanceDid(undefined); // clear any tenancy-slot override
|
|
17
|
+
if (ORIG_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
|
|
18
|
+
else process.env.PAYMENT_TENANT_MODE = ORIG_MODE;
|
|
19
|
+
if (ORIG_PID === undefined) delete process.env.BLOCKLET_APP_PID;
|
|
20
|
+
else process.env.BLOCKLET_APP_PID = ORIG_PID;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Phase 8 — injected config is authoritative (happy path)', () => {
|
|
24
|
+
it('readConfig prefers injected config over process.env', () => {
|
|
25
|
+
process.env.PHASE8_FOO = 'from-env';
|
|
26
|
+
setCoreConfig({ PHASE8_FOO: 'from-config' });
|
|
27
|
+
expect(readConfig('PHASE8_FOO')).toBe('from-config');
|
|
28
|
+
delete process.env.PHASE8_FOO;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('readConfig falls back to process.env when the key is absent from injected config', () => {
|
|
32
|
+
process.env.PHASE8_BAR = 'env-only';
|
|
33
|
+
setCoreConfig({ OTHER: 'x' });
|
|
34
|
+
expect(readConfig('PHASE8_BAR')).toBe('env-only');
|
|
35
|
+
delete process.env.PHASE8_BAR;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('hasConfig is true for injected and env keys, false for absent', () => {
|
|
39
|
+
setCoreConfig({ PHASE8_PRESENT: 'v' });
|
|
40
|
+
expect(hasConfig('PHASE8_PRESENT')).toBe(true);
|
|
41
|
+
expect(hasConfig('PHASE8_DEFINITELY_ABSENT_KEY')).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('Phase 8 — getTenantMode mode-source from injected config (data isolation)', () => {
|
|
46
|
+
it('reads tenant mode from the injected config', () => {
|
|
47
|
+
delete process.env.PAYMENT_TENANT_MODE; // no env fallback — prove config is the source
|
|
48
|
+
setCoreConfig({ PAYMENT_TENANT_MODE: 'multi' });
|
|
49
|
+
expect(getTenantMode()).toBe('multi');
|
|
50
|
+
setCoreConfig({ PAYMENT_TENANT_MODE: 'single' });
|
|
51
|
+
expect(getTenantMode()).toBe('single');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('injected config wins over a conflicting process.env (single source of truth)', () => {
|
|
55
|
+
process.env.PAYMENT_TENANT_MODE = 'single';
|
|
56
|
+
setCoreConfig({ PAYMENT_TENANT_MODE: 'multi' });
|
|
57
|
+
expect(getTenantMode()).toBe('multi'); // config wins, not the env mirror
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('SECURITY: getTenantMode takes no request input — only config/env decide the mode', () => {
|
|
61
|
+
setCoreConfig({ PAYMENT_TENANT_MODE: 'single' });
|
|
62
|
+
// getTenantMode() has no parameters; there is no request-level surface to tamper.
|
|
63
|
+
expect(getTenantMode.length).toBe(0);
|
|
64
|
+
expect(getTenantMode()).toBe('single');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('getDefaultInstanceDid reads the app DID from injected config', () => {
|
|
68
|
+
delete process.env.BLOCKLET_APP_PID;
|
|
69
|
+
process.env.PAYMENT_TENANT_MODE = 'single';
|
|
70
|
+
setCoreConfig({ BLOCKLET_APP_PID: 'did:abt:zCONFIGAPP' });
|
|
71
|
+
expect(getDefaultInstanceDid()).toBe('did:abt:zCONFIGAPP');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Phase 8 — fail-fast on a missing required config field (bad input)', () => {
|
|
76
|
+
it('single-mode factory with no BLOCKLET_APP_PID (config + env both absent) throws MissingConfigError', () => {
|
|
77
|
+
delete process.env.BLOCKLET_APP_PID;
|
|
78
|
+
expect(() =>
|
|
79
|
+
createEmbeddedPaymentService({
|
|
80
|
+
config: {}, // no BLOCKLET_APP_PID
|
|
81
|
+
db: { sequelize: {} as any },
|
|
82
|
+
// no tenancy slot -> defaults to single mode
|
|
83
|
+
})
|
|
84
|
+
).toThrow(MissingConfigError);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('the thrown error names the missing field (not a silent default)', () => {
|
|
88
|
+
delete process.env.BLOCKLET_APP_PID;
|
|
89
|
+
try {
|
|
90
|
+
createEmbeddedPaymentService({ config: {}, db: { sequelize: {} as any } });
|
|
91
|
+
throw new Error('expected MissingConfigError');
|
|
92
|
+
} catch (err: any) {
|
|
93
|
+
expect(err).toBeInstanceOf(MissingConfigError);
|
|
94
|
+
expect(err.code).toBe('MISSING_CONFIG_FIELD');
|
|
95
|
+
expect(err.field).toBe('BLOCKLET_APP_PID');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('multi mode does NOT require BLOCKLET_APP_PID (tenant resolved per request)', () => {
|
|
100
|
+
delete process.env.BLOCKLET_APP_PID;
|
|
101
|
+
// multi mode passes the config fail-fast; it fails later on the db slot only
|
|
102
|
+
// if absent — here we give a truthy sequelize so the config check is what we
|
|
103
|
+
// assert. D1: multi also requires identity + secrets slots (else it would
|
|
104
|
+
// silently degrade to single), so provide minimal stubs.
|
|
105
|
+
expect(() =>
|
|
106
|
+
createEmbeddedPaymentService({
|
|
107
|
+
config: { PAYMENT_TENANT_MODE: 'multi' },
|
|
108
|
+
db: { sequelize: {} as any },
|
|
109
|
+
tenancy: { mode: 'multi' },
|
|
110
|
+
identity: { resolveInstanceDidForHost: () => null } as any,
|
|
111
|
+
secrets: createDefaultSecretsDriver(),
|
|
112
|
+
})
|
|
113
|
+
).not.toThrow(MissingConfigError);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// D2 (S3.0) — the node-cron driver self-schedules via @abtnode/cron and exposes
|
|
2
|
+
// a teardown surface (stop/dispose). cf-cron stays a passive matcher registry.
|
|
3
|
+
//
|
|
4
|
+
// "Self-schedule" is proven two ways: a runOnInit job runs once at register
|
|
5
|
+
// (the @abtnode/cron live path), and a 1-second cron fires on its own with NO
|
|
6
|
+
// external runDue() tick. Teardown is proven by: after stop() a scheduled job no
|
|
7
|
+
// longer fires, and a stop()/start() cycle re-registers without double-firing.
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { createCronRegistry } from '../../src/libs/drivers/cron';
|
|
12
|
+
import { setCoreConfig } from '../../src/libs/env';
|
|
13
|
+
|
|
14
|
+
const flush = () => new Promise((r) => setTimeout(r, 20));
|
|
15
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
|
|
17
|
+
// real-timer 1s cron tests give the suite a little headroom
|
|
18
|
+
jest.setTimeout(15000);
|
|
19
|
+
|
|
20
|
+
describe('D2 — node-cron driver self-schedules (no external tick)', () => {
|
|
21
|
+
it('a runOnInit job runs once at register via the live @abtnode/cron path', async () => {
|
|
22
|
+
const driver = createCronRegistry('node-cron');
|
|
23
|
+
let ran = 0;
|
|
24
|
+
driver.register([
|
|
25
|
+
{
|
|
26
|
+
name: 'd2.init',
|
|
27
|
+
time: '0 0 0 1 1 *',
|
|
28
|
+
fn: () => {
|
|
29
|
+
ran += 1;
|
|
30
|
+
},
|
|
31
|
+
options: { runOnInit: true },
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
await flush();
|
|
35
|
+
expect(ran).toBe(1);
|
|
36
|
+
driver.stop();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('a 1-second cron fires by itself with no runDue() call', async () => {
|
|
40
|
+
const driver = createCronRegistry('node-cron');
|
|
41
|
+
let ticks = 0;
|
|
42
|
+
driver.register([
|
|
43
|
+
{
|
|
44
|
+
name: 'd2.every-sec',
|
|
45
|
+
time: '*/1 * * * * *',
|
|
46
|
+
fn: () => {
|
|
47
|
+
ticks += 1;
|
|
48
|
+
},
|
|
49
|
+
options: { runOnInit: false },
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
await wait(2200); // ~2 self-scheduled ticks, zero external runDue
|
|
53
|
+
driver.stop();
|
|
54
|
+
expect(ticks).toBeGreaterThanOrEqual(1);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('D2 — multi mode skips runOnInit (matches CF; avoids tenant-less startup push)', () => {
|
|
59
|
+
afterEach(() => setCoreConfig(undefined));
|
|
60
|
+
|
|
61
|
+
it('a runOnInit job does NOT fire at register in multi mode', async () => {
|
|
62
|
+
setCoreConfig({ PAYMENT_TENANT_MODE: 'multi' });
|
|
63
|
+
const driver = createCronRegistry('node-cron');
|
|
64
|
+
let ran = 0;
|
|
65
|
+
driver.register([
|
|
66
|
+
{
|
|
67
|
+
name: 'm.init',
|
|
68
|
+
time: '0 0 0 1 1 *',
|
|
69
|
+
fn: () => {
|
|
70
|
+
ran += 1;
|
|
71
|
+
},
|
|
72
|
+
options: { runOnInit: true },
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
await flush();
|
|
76
|
+
expect(ran).toBe(0); // skipped in multi — the job runs on its next scheduled tick
|
|
77
|
+
driver.stop();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('single mode STILL honors runOnInit (the default tenant is always present)', async () => {
|
|
81
|
+
setCoreConfig({ PAYMENT_TENANT_MODE: 'single' });
|
|
82
|
+
const driver = createCronRegistry('node-cron');
|
|
83
|
+
let ran = 0;
|
|
84
|
+
driver.register([
|
|
85
|
+
{
|
|
86
|
+
name: 's.init',
|
|
87
|
+
time: '0 0 0 1 1 *',
|
|
88
|
+
fn: () => {
|
|
89
|
+
ran += 1;
|
|
90
|
+
},
|
|
91
|
+
options: { runOnInit: true },
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
await flush();
|
|
95
|
+
expect(ran).toBe(1); // single keeps runOnInit
|
|
96
|
+
driver.stop();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('D2 — register receives all jobs (conservation)', () => {
|
|
101
|
+
it('getJobNames lists every registered job in order', () => {
|
|
102
|
+
const driver = createCronRegistry('node-cron');
|
|
103
|
+
driver.register([
|
|
104
|
+
{ name: 'a', time: '0 0 0 1 1 *', fn: () => {} },
|
|
105
|
+
{ name: 'b', time: '0 0 0 1 1 *', fn: () => {} },
|
|
106
|
+
{ name: 'c', time: '0 0 0 1 1 *', fn: () => {} },
|
|
107
|
+
]);
|
|
108
|
+
expect(driver.getJobNames().map((s) => s.split(' ')[0])).toEqual(['a', 'b', 'c']);
|
|
109
|
+
driver.stop();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('SECURITY/conservation: no bare @abtnode/cron import or Cron.init survives in api/src', () => {
|
|
113
|
+
// the only allowed reference is the lazy require() inside the driver itself.
|
|
114
|
+
const root = path.resolve(__dirname, '../../src');
|
|
115
|
+
const offenders: string[] = [];
|
|
116
|
+
const walk = (dir: string) => {
|
|
117
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
118
|
+
const full = path.join(dir, entry.name);
|
|
119
|
+
if (entry.isDirectory()) walk(full);
|
|
120
|
+
else if (entry.name.endsWith('.ts')) {
|
|
121
|
+
const src = fs.readFileSync(full, 'utf8');
|
|
122
|
+
if (/import\s+[^;]*from\s+['"]@abtnode\/cron['"]/.test(src)) offenders.push(`${full}: import`);
|
|
123
|
+
if (/\bCron\.init\s*\(/.test(src)) offenders.push(`${full}: Cron.init`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
walk(root);
|
|
128
|
+
expect(offenders).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('D2 — a throwing job routes to onError, does not crash the driver', () => {
|
|
133
|
+
it('onError receives (err, name); other jobs keep running', async () => {
|
|
134
|
+
const driver = createCronRegistry('node-cron');
|
|
135
|
+
const errs: Array<{ name: string; message: string }> = [];
|
|
136
|
+
let okRan = 0;
|
|
137
|
+
driver.register(
|
|
138
|
+
[
|
|
139
|
+
{
|
|
140
|
+
name: 'boom',
|
|
141
|
+
time: '0 0 0 1 1 *',
|
|
142
|
+
fn: () => {
|
|
143
|
+
throw new Error('kaboom');
|
|
144
|
+
},
|
|
145
|
+
options: { runOnInit: true },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'ok',
|
|
149
|
+
time: '0 0 0 1 1 *',
|
|
150
|
+
fn: () => {
|
|
151
|
+
okRan += 1;
|
|
152
|
+
},
|
|
153
|
+
options: { runOnInit: true },
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
(err, name) => errs.push({ name, message: err.message })
|
|
157
|
+
);
|
|
158
|
+
await flush();
|
|
159
|
+
expect(errs).toEqual([{ name: 'boom', message: 'kaboom' }]);
|
|
160
|
+
expect(okRan).toBe(1);
|
|
161
|
+
driver.stop();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('D2 — teardown: stop() kills the timer, start()/stop() is idempotent', () => {
|
|
166
|
+
it('after stop() a 1-second cron no longer fires', async () => {
|
|
167
|
+
const driver = createCronRegistry('node-cron');
|
|
168
|
+
let ticks = 0;
|
|
169
|
+
driver.register([
|
|
170
|
+
{
|
|
171
|
+
name: 'tear',
|
|
172
|
+
time: '*/1 * * * * *',
|
|
173
|
+
fn: () => {
|
|
174
|
+
ticks += 1;
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
driver.stop();
|
|
179
|
+
const before = ticks;
|
|
180
|
+
await wait(2200);
|
|
181
|
+
expect(ticks).toBe(before); // no fires after stop — timer was cleared
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('stop() then re-register does not double-fire (job set idempotent)', async () => {
|
|
185
|
+
const driver = createCronRegistry('node-cron');
|
|
186
|
+
let ran = 0;
|
|
187
|
+
const jobs = [
|
|
188
|
+
{
|
|
189
|
+
name: 'idem',
|
|
190
|
+
time: '0 0 0 1 1 *',
|
|
191
|
+
fn: () => {
|
|
192
|
+
ran += 1;
|
|
193
|
+
},
|
|
194
|
+
options: { runOnInit: true },
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
driver.register(jobs);
|
|
198
|
+
await flush();
|
|
199
|
+
expect(ran).toBe(1);
|
|
200
|
+
driver.stop();
|
|
201
|
+
driver.register(jobs); // restart
|
|
202
|
+
await flush();
|
|
203
|
+
expect(ran).toBe(2); // exactly one more — not 3+ (old scheduler was torn down)
|
|
204
|
+
driver.stop();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('dispose() clears the registry', () => {
|
|
208
|
+
const driver = createCronRegistry('node-cron');
|
|
209
|
+
driver.register([{ name: 'x', time: '0 0 0 1 1 *', fn: () => {} }]);
|
|
210
|
+
expect(driver.getJobNames().length).toBe(1);
|
|
211
|
+
driver.dispose();
|
|
212
|
+
expect(driver.getJobNames().length).toBe(0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('D2 — cf-cron driver stays passive (no self-scheduling timer)', () => {
|
|
217
|
+
it('cf-cron does not run jobs on register; host drives runDue()', async () => {
|
|
218
|
+
const driver = createCronRegistry('cf-cron');
|
|
219
|
+
let ran = 0;
|
|
220
|
+
driver.register([
|
|
221
|
+
{
|
|
222
|
+
name: 'cf',
|
|
223
|
+
time: '*/1 * * * * *',
|
|
224
|
+
fn: () => {
|
|
225
|
+
ran += 1;
|
|
226
|
+
},
|
|
227
|
+
options: { runOnInit: true },
|
|
228
|
+
},
|
|
229
|
+
]);
|
|
230
|
+
await wait(1200);
|
|
231
|
+
expect(ran).toBe(0); // no self-scheduling, runOnInit skipped on CF
|
|
232
|
+
const result = await driver.runDue(new Date());
|
|
233
|
+
expect(result.ran).toContain('cf');
|
|
234
|
+
driver.stop(); // no-op, must not throw
|
|
235
|
+
expect(ran).toBe(1);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// D2 (S3.0) — conservation: crons/index registers its FULL declared job set
|
|
2
|
+
// through a single getCronDriver().register() call, with no bare @abtnode/cron
|
|
3
|
+
// channel left behind. Verified at the source level (parsing crons/index.ts)
|
|
4
|
+
// rather than by importing it — importing the whole crons graph pulls heavy
|
|
5
|
+
// integration ESM deps outside this test's concern. Combined with the scanner
|
|
6
|
+
// in cron-driver-d2.spec ("no bare @abtnode/cron import / Cron.init anywhere"),
|
|
7
|
+
// this proves: ONE registration channel, carrying the whole declared set.
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
const CRONS_SRC = path.resolve(__dirname, '../../src/crons/index.ts');
|
|
13
|
+
|
|
14
|
+
describe('D2 — crons/index conservation (single driver channel, full job set)', () => {
|
|
15
|
+
const src = fs.readFileSync(CRONS_SRC, 'utf8');
|
|
16
|
+
|
|
17
|
+
it('registers through getCronDriver().register() exactly once (no bare Cron.init)', () => {
|
|
18
|
+
const registerCalls = src.match(/getCronDriver\(\)\.register\(/g) || [];
|
|
19
|
+
expect(registerCalls.length).toBe(1);
|
|
20
|
+
expect(src).not.toMatch(/Cron\.init\s*\(/);
|
|
21
|
+
expect(src).not.toMatch(/from\s+['"]@abtnode\/cron['"]/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('the declared job set reaches the driver in full (>=18 distinct named jobs)', () => {
|
|
25
|
+
const names = [...src.matchAll(/name:\s*'([^']+)'/g)].map((m) => m[1]);
|
|
26
|
+
// every job declared in the register([...]) array is a name: '...' literal
|
|
27
|
+
expect(names).toEqual(
|
|
28
|
+
expect.arrayContaining([
|
|
29
|
+
'subscription.will.renew',
|
|
30
|
+
'subscription.trial.will.end',
|
|
31
|
+
'customer.subscription.will_canceled',
|
|
32
|
+
'subscription.schedule.retry',
|
|
33
|
+
'refund.recovery',
|
|
34
|
+
'checkoutSession.cleanup.expired',
|
|
35
|
+
'stripe.invoice.sync',
|
|
36
|
+
'stripe.payment.sync',
|
|
37
|
+
'stripe.subscription.sync',
|
|
38
|
+
'customer.stake.revoked',
|
|
39
|
+
'iap.reconcile',
|
|
40
|
+
'event.retry',
|
|
41
|
+
'payment.stat',
|
|
42
|
+
'payment.daily.report',
|
|
43
|
+
'deposit.vault',
|
|
44
|
+
'credit.consumption',
|
|
45
|
+
'vendor.status.check',
|
|
46
|
+
'vendor.return.scan',
|
|
47
|
+
])
|
|
48
|
+
);
|
|
49
|
+
expect(names.length).toBeGreaterThanOrEqual(18);
|
|
50
|
+
expect(new Set(names).size).toBe(names.length); // no duplicate names
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// S3-CF (DID convergence) — the real @arcblock/did-connect-js runtime.
|
|
2
|
+
//
|
|
3
|
+
// Acceptance: when a host injects createDidConnectJsRuntime (CF / arc-node), the
|
|
4
|
+
// core buildConnectRoutesHono registers the 14 payment DID-Connect actions, and
|
|
5
|
+
// the @blocklet/sdk wallet modules are NEVER constructed (no silent SDK fallback).
|
|
6
|
+
//
|
|
7
|
+
// The @blocklet/sdk wallet mocks throw on construct so any accidental SDK use
|
|
8
|
+
// fails this spec loudly (mirrors the CF fail-fast shims; this is the node-side
|
|
9
|
+
// guard the runtime-boundary requires).
|
|
10
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
11
|
+
import * as Mcrypto from '@ocap/mcrypto';
|
|
12
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
13
|
+
import { fromRandom } from '@ocap/wallet';
|
|
14
|
+
|
|
15
|
+
jest.mock('@blocklet/sdk/lib/wallet-handler', () => ({
|
|
16
|
+
WalletHandlers: class {
|
|
17
|
+
constructor() {
|
|
18
|
+
throw new Error('SDK wallet-handler must not be constructed under the did-connect-js runtime');
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
jest.mock('@blocklet/sdk/lib/wallet-authenticator', () => ({
|
|
23
|
+
WalletAuthenticator: class {
|
|
24
|
+
constructor() {
|
|
25
|
+
throw new Error('SDK wallet-authenticator must not be constructed under the did-connect-js runtime');
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const APP_WALLET_TYPE = {
|
|
31
|
+
role: Mcrypto.types.RoleType.ROLE_APPLICATION,
|
|
32
|
+
pk: Mcrypto.types.KeyType.ED25519,
|
|
33
|
+
hash: Mcrypto.types.HashType.SHA3,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ALL_ACTIONS = [
|
|
37
|
+
'collect',
|
|
38
|
+
'collect-batch',
|
|
39
|
+
'payment',
|
|
40
|
+
'setup',
|
|
41
|
+
'subscription',
|
|
42
|
+
'change-payment',
|
|
43
|
+
'change-plan',
|
|
44
|
+
'recharge',
|
|
45
|
+
'recharge-account',
|
|
46
|
+
'delegation',
|
|
47
|
+
'overdraft-protection',
|
|
48
|
+
're-stake',
|
|
49
|
+
'auto-recharge-auth',
|
|
50
|
+
'change-payer',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
describe('S3-CF DID convergence — @arcblock/did-connect-js runtime', () => {
|
|
54
|
+
it('registers the 14 payment DID actions and never constructs @blocklet/sdk wallets', () => {
|
|
55
|
+
// eslint-disable-next-line global-require
|
|
56
|
+
const { setDidConnectRuntime: setRuntime } = require('../../src/libs/auth');
|
|
57
|
+
// eslint-disable-next-line global-require
|
|
58
|
+
const { setIdentityDriver: setDriver } = require('../../src/libs/drivers');
|
|
59
|
+
// eslint-disable-next-line global-require
|
|
60
|
+
const { createDidConnectJsRuntime } = require('../../src/libs/did-connect/runtime-did-connect-js');
|
|
61
|
+
// eslint-disable-next-line global-require
|
|
62
|
+
const { buildConnectRoutesHono } = require('../../src/service');
|
|
63
|
+
|
|
64
|
+
const signer = fromRandom(APP_WALLET_TYPE);
|
|
65
|
+
setDriver({
|
|
66
|
+
resolveInstanceDidForHost: () => 'zMOCK_DIDJS',
|
|
67
|
+
getInstanceAppIdentity: async () => ({ appSk: signer.secretKey, appInfo: { name: 'T' } }),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const memStore: Record<string, any> = {};
|
|
71
|
+
const tokenStorage = {
|
|
72
|
+
create: async (token: string, status = 'created') => {
|
|
73
|
+
memStore[token] = { token, status };
|
|
74
|
+
return memStore[token];
|
|
75
|
+
},
|
|
76
|
+
read: async (token: string) => memStore[token] ?? null,
|
|
77
|
+
update: async (token: string, u: Record<string, any>) => {
|
|
78
|
+
memStore[token] = { ...memStore[token], ...u };
|
|
79
|
+
return memStore[token];
|
|
80
|
+
},
|
|
81
|
+
delete: async (token: string) => {
|
|
82
|
+
delete memStore[token];
|
|
83
|
+
},
|
|
84
|
+
on: () => undefined,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
setRuntime(createDidConnectJsRuntime({ tokenStorage }));
|
|
88
|
+
|
|
89
|
+
const connectApp = buildConnectRoutesHono();
|
|
90
|
+
const paths: Set<string> = new Set(((connectApp as any).routes || []).map((r: any) => r.path));
|
|
91
|
+
|
|
92
|
+
// every payment action exposes at least its /token entry under /api/did/<action>
|
|
93
|
+
for (const action of ALL_ACTIONS) {
|
|
94
|
+
expect([...paths].some((p) => p.startsWith(`/api/did/${action}/`))).toBe(true);
|
|
95
|
+
}
|
|
96
|
+
// no SDK wallet was constructed (the mocks above would have thrown)
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// S3-CF (DID convergence) — the shared per-tenant DID-Connect identity resolver.
|
|
2
|
+
//
|
|
3
|
+
// resolveTenantIdentity is the ONE path both the CF and arc-node AUTH_SERVICE
|
|
4
|
+
// runtimes use to derive their DID-Connect signing wallet from the host
|
|
5
|
+
// IdentityDriver.getInstanceAppIdentity — never a fixed isolate key. This spec
|
|
6
|
+
// locks the happy path + the two fail-closed errors.
|
|
7
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
8
|
+
import * as Mcrypto from '@ocap/mcrypto';
|
|
9
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
10
|
+
import { fromRandom } from '@ocap/wallet';
|
|
11
|
+
|
|
12
|
+
import { setIdentityDriver, createDefaultIdentityDriver, type IdentityDriver } from '../../src/libs/drivers';
|
|
13
|
+
import {
|
|
14
|
+
resolveTenantIdentity,
|
|
15
|
+
clearTenantIdentityCache,
|
|
16
|
+
getCachedTenantIdentity,
|
|
17
|
+
hasDynamicIdentity,
|
|
18
|
+
warmTenantIdentity,
|
|
19
|
+
} from '../../src/libs/did-connect/tenant-identity';
|
|
20
|
+
|
|
21
|
+
const INSTANCE = 'zMOCK_TENANT_IDENTITY';
|
|
22
|
+
|
|
23
|
+
// The DID-Connect signer is a ROLE_APPLICATION/ED25519/SHA3 wallet (the DID
|
|
24
|
+
// encoding depends on the type), so the test must mint source keys with the SAME
|
|
25
|
+
// type for the derived address to round-trip.
|
|
26
|
+
const APP_WALLET_TYPE = {
|
|
27
|
+
role: Mcrypto.types.RoleType.ROLE_APPLICATION,
|
|
28
|
+
pk: Mcrypto.types.KeyType.ED25519,
|
|
29
|
+
hash: Mcrypto.types.HashType.SHA3,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('S3-CF DID convergence — resolveTenantIdentity (shared identity resolver)', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
// resolveTenantIdentity now caches per instanceDid (LRU+TTL). These tests
|
|
35
|
+
// reuse one INSTANCE did with swapped drivers, so isolate by clearing.
|
|
36
|
+
clearTenantIdentityCache();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
setIdentityDriver(createDefaultIdentityDriver()); // reset module-level driver
|
|
40
|
+
clearTenantIdentityCache();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('derives the signing wallet from the driver getInstanceAppIdentity(appSk) + passes appInfo', async () => {
|
|
44
|
+
const signer = fromRandom(APP_WALLET_TYPE);
|
|
45
|
+
const driver: IdentityDriver = {
|
|
46
|
+
resolveInstanceDidForHost: () => INSTANCE,
|
|
47
|
+
getInstanceAppIdentity: async (did) => {
|
|
48
|
+
expect(did).toBe(INSTANCE);
|
|
49
|
+
return { appSk: signer.secretKey, appInfo: { name: 'Tenant X', icon: 'https://x/i.png' } };
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
setIdentityDriver(driver);
|
|
53
|
+
|
|
54
|
+
const resolved = await resolveTenantIdentity(INSTANCE);
|
|
55
|
+
expect(resolved.instanceDid).toBe(INSTANCE);
|
|
56
|
+
// the derived wallet is the same keypair as the source appSk
|
|
57
|
+
expect(resolved.wallet.address).toBe(signer.address);
|
|
58
|
+
// no rotated key → permanentWallet falls back to wallet
|
|
59
|
+
expect(resolved.permanentWallet.address).toBe(signer.address);
|
|
60
|
+
expect(resolved.appInfo).toEqual({ name: 'Tenant X', icon: 'https://x/i.png' });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('uses appPsk for the permanent wallet when keys have rotated', async () => {
|
|
64
|
+
const current = fromRandom(APP_WALLET_TYPE);
|
|
65
|
+
const permanent = fromRandom(APP_WALLET_TYPE);
|
|
66
|
+
setIdentityDriver({
|
|
67
|
+
resolveInstanceDidForHost: () => INSTANCE,
|
|
68
|
+
getInstanceAppIdentity: async () => ({ appSk: current.secretKey, appPsk: permanent.secretKey }),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const resolved = await resolveTenantIdentity(INSTANCE);
|
|
72
|
+
expect(resolved.wallet.address).toBe(current.address);
|
|
73
|
+
expect(resolved.permanentWallet.address).toBe(permanent.address);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('throws (no silent fallback) when the driver lacks getInstanceAppIdentity', async () => {
|
|
77
|
+
setIdentityDriver(createDefaultIdentityDriver()); // default driver has no getInstanceAppIdentity
|
|
78
|
+
await expect(resolveTenantIdentity(INSTANCE)).rejects.toThrow(/does not implement getInstanceAppIdentity/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('fails closed when the instance has no app signing key', async () => {
|
|
82
|
+
setIdentityDriver({
|
|
83
|
+
resolveInstanceDidForHost: () => INSTANCE,
|
|
84
|
+
// @ts-expect-error — intentionally returns no appSk to exercise the fail-closed path
|
|
85
|
+
getInstanceAppIdentity: async () => ({ appInfo: { name: 'no key' } }),
|
|
86
|
+
});
|
|
87
|
+
await expect(resolveTenantIdentity(INSTANCE)).rejects.toThrow(/no app signing key.*fail-closed/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Layer 4 (wallet-authenticator-dynamic.md) — business wallet support.
|
|
91
|
+
it('derives the ethereum business wallet from the same appSk (secp256k1)', async () => {
|
|
92
|
+
const signer = fromRandom(APP_WALLET_TYPE);
|
|
93
|
+
setIdentityDriver({
|
|
94
|
+
resolveInstanceDidForHost: () => INSTANCE,
|
|
95
|
+
getInstanceAppIdentity: async () => ({ appSk: signer.secretKey }),
|
|
96
|
+
});
|
|
97
|
+
const resolved = await resolveTenantIdentity(INSTANCE);
|
|
98
|
+
// distinct EVM address (0x-prefixed), not the arcblock DID
|
|
99
|
+
expect(resolved.ethWallet.address).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
100
|
+
expect(resolved.ethWallet.address).not.toBe(resolved.wallet.address);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('hasDynamicIdentity reflects whether the driver implements getInstanceAppIdentity', () => {
|
|
104
|
+
setIdentityDriver(createDefaultIdentityDriver());
|
|
105
|
+
expect(hasDynamicIdentity()).toBe(false);
|
|
106
|
+
setIdentityDriver({
|
|
107
|
+
resolveInstanceDidForHost: () => INSTANCE,
|
|
108
|
+
getInstanceAppIdentity: async () => ({ appSk: 'x' }),
|
|
109
|
+
});
|
|
110
|
+
expect(hasDynamicIdentity()).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('getCachedTenantIdentity returns the warmed value, and fails closed before warming', async () => {
|
|
114
|
+
const signer = fromRandom(APP_WALLET_TYPE);
|
|
115
|
+
setIdentityDriver({
|
|
116
|
+
resolveInstanceDidForHost: () => INSTANCE,
|
|
117
|
+
getInstanceAppIdentity: async () => ({ appSk: signer.secretKey }),
|
|
118
|
+
});
|
|
119
|
+
// not warmed yet → fail-closed (no silent default key)
|
|
120
|
+
expect(() => getCachedTenantIdentity(INSTANCE)).toThrow(/not resolved.*fail-closed/);
|
|
121
|
+
// warm (what the request/job middleware does), then the sync read resolves
|
|
122
|
+
await resolveTenantIdentity(INSTANCE);
|
|
123
|
+
expect(getCachedTenantIdentity(INSTANCE).wallet.address).toBe(signer.address);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('caches the derived identity — a second resolve does not re-call the driver', async () => {
|
|
127
|
+
let calls = 0;
|
|
128
|
+
const signer = fromRandom(APP_WALLET_TYPE);
|
|
129
|
+
setIdentityDriver({
|
|
130
|
+
resolveInstanceDidForHost: () => INSTANCE,
|
|
131
|
+
getInstanceAppIdentity: async () => {
|
|
132
|
+
calls += 1;
|
|
133
|
+
return { appSk: signer.secretKey };
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
await resolveTenantIdentity(INSTANCE);
|
|
137
|
+
await resolveTenantIdentity(INSTANCE);
|
|
138
|
+
expect(calls).toBe(1); // second call served from cache
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('warmTenantIdentity swallows resolve errors and leaves the cache cold (fail-closed on use)', async () => {
|
|
142
|
+
setIdentityDriver({
|
|
143
|
+
resolveInstanceDidForHost: () => INSTANCE,
|
|
144
|
+
getInstanceAppIdentity: async () => {
|
|
145
|
+
throw new Error('AUTH_SERVICE down');
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
// warm must NOT throw (a non-wallet request is not blocked)
|
|
149
|
+
await expect(warmTenantIdentity(INSTANCE)).resolves.toBeUndefined();
|
|
150
|
+
// but the cache stays cold → any wallet access fails closed
|
|
151
|
+
expect(() => getCachedTenantIdentity(INSTANCE)).toThrow(/not resolved.*fail-closed/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('warmTenantIdentity is a no-op on the blocklet-server runtime (no dynamic driver)', async () => {
|
|
155
|
+
setIdentityDriver(createDefaultIdentityDriver()); // no getInstanceAppIdentity
|
|
156
|
+
await expect(warmTenantIdentity(INSTANCE)).resolves.toBeUndefined();
|
|
157
|
+
expect(() => getCachedTenantIdentity(INSTANCE)).toThrow(/not resolved.*fail-closed/);
|
|
158
|
+
});
|
|
159
|
+
});
|