payment-kit 1.29.1 → 1.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +36 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +27 -24
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +99 -38
- package/api/src/libs/context.ts +78 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +81 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +50 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +259 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +73 -0
- package/api/src/middlewares/hono/csrf.ts +72 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +214 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +17 -12
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +12 -4
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +41 -28
- package/api/src/queues/payout.ts +9 -5
- package/api/src/queues/refund.ts +18 -12
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +667 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +66 -22
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +236 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/README.md +26 -6
- package/cloudflare/build.ts +28 -13
- package/cloudflare/did-connect-auth.ts +0 -217
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +10 -56
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
- package/cloudflare/worker.ts +204 -433
- package/cloudflare/wrangler.local-e2e.jsonc +26 -0
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/env.d.ts +13 -1
- package/tsconfig.json +1 -1
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -0,0 +1,153 @@
|
|
|
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 { TENANT_A, TENANT_B } from '../fixtures/tenants';
|
|
10
|
+
|
|
11
|
+
jest.mock('../../src/libs/logger', () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const STORE_DIR = path.join(__dirname, '../../src/store');
|
|
17
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-tenant-'));
|
|
18
|
+
const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
|
|
19
|
+
const umzug = new Umzug({
|
|
20
|
+
migrations: {
|
|
21
|
+
glob: ['migrations/*.ts', { cwd: STORE_DIR }],
|
|
22
|
+
resolve: ({ name, path: p, context }) => {
|
|
23
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
24
|
+
const migration = require(p!);
|
|
25
|
+
return {
|
|
26
|
+
name: name.replace(/\.ts$/, '.js'),
|
|
27
|
+
up: () => migration.up({ context }),
|
|
28
|
+
down: () => migration.down({ context }),
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
context: sequelize.getQueryInterface(),
|
|
33
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
34
|
+
logger: undefined,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let audit: typeof import('../../src/libs/audit');
|
|
38
|
+
let logger: any;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
await umzug.up();
|
|
42
|
+
// eslint-disable-next-line global-require
|
|
43
|
+
const models = require('../../src/store/models');
|
|
44
|
+
models.initialize(sequelize);
|
|
45
|
+
// eslint-disable-next-line global-require
|
|
46
|
+
audit = require('../../src/libs/audit');
|
|
47
|
+
// eslint-disable-next-line global-require
|
|
48
|
+
logger = require('../../src/libs/logger').default;
|
|
49
|
+
}, 120000);
|
|
50
|
+
|
|
51
|
+
afterAll(async () => {
|
|
52
|
+
await sequelize.close();
|
|
53
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
await sequelize.query('DELETE FROM events');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const fakeModel = (tenant: string | null, extra: Record<string, any> = {}) => ({
|
|
61
|
+
id: 'obj_1',
|
|
62
|
+
livemode: false,
|
|
63
|
+
instance_did: tenant,
|
|
64
|
+
dataValues: { id: 'obj_1', instance_did: tenant, ...extra },
|
|
65
|
+
_previousDataValues: {},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('audit tenant resolution (phase 4)', () => {
|
|
69
|
+
describe('happy path', () => {
|
|
70
|
+
it('takes the tenant from the model row', async () => {
|
|
71
|
+
await audit.createEvent('Customer', 'customer.created', fakeModel(TENANT_A));
|
|
72
|
+
const [rows] = await sequelize.query('SELECT type, instance_did FROM events');
|
|
73
|
+
expect(rows).toEqual([{ type: 'customer.created', instance_did: TENANT_A }]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('falls back to the tenant context when the model has none', async () => {
|
|
77
|
+
await withTenant(TENANT_B, () => audit.createEvent('Customer', 'customer.created', fakeModel(null)));
|
|
78
|
+
const [rows] = await sequelize.query('SELECT instance_did FROM events');
|
|
79
|
+
expect(rows).toEqual([{ instance_did: TENANT_B }]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('createFlexibleEvent (system source) uses the tenant context', async () => {
|
|
83
|
+
const event: any = await withTenant(TENANT_A, () =>
|
|
84
|
+
audit.createFlexibleEvent('payout.created', 'payout', 'po_1', { ok: true })
|
|
85
|
+
);
|
|
86
|
+
expect(event.instance_did).toBe(TENANT_A);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('bad input: both sources missing', () => {
|
|
91
|
+
it('rejects in multi mode and writes no event row', async () => {
|
|
92
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
93
|
+
try {
|
|
94
|
+
await expect(audit.createEvent('Customer', 'customer.created', fakeModel(null))).rejects.toMatchObject({
|
|
95
|
+
code: TENANT_CONTEXT_MISSING,
|
|
96
|
+
});
|
|
97
|
+
} finally {
|
|
98
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
99
|
+
}
|
|
100
|
+
const [rows] = await sequelize.query('SELECT COUNT(*) AS n FROM events');
|
|
101
|
+
expect((rows as any[])[0].n).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('security: contradictory sources', () => {
|
|
106
|
+
it('rejects when model tenant and context tenant disagree', async () => {
|
|
107
|
+
await withTenant(TENANT_A, async () => {
|
|
108
|
+
await expect(audit.createEvent('Customer', 'customer.created', fakeModel(TENANT_B))).rejects.toMatchObject({
|
|
109
|
+
code: TENANT_MISMATCH,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
const [rows] = await sequelize.query('SELECT COUNT(*) AS n FROM events');
|
|
113
|
+
expect((rows as any[])[0].n).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('data loss: rejection never blocks the business write', () => {
|
|
118
|
+
it('reportAuditFailure is fire-and-forget with a dedicated code', () => {
|
|
119
|
+
const err = Object.assign(new Error('conflict'), { code: TENANT_MISMATCH });
|
|
120
|
+
expect(() => audit.reportAuditFailure(err)).not.toThrow();
|
|
121
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
122
|
+
'[audit] event creation failed',
|
|
123
|
+
expect.objectContaining({ code: TENANT_MISMATCH })
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
audit.reportAuditFailure(new Error('boom'));
|
|
127
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
128
|
+
'[audit] event creation failed',
|
|
129
|
+
expect.objectContaining({ code: 'EVENT_CREATE_FAILED' })
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('data leak: status/custom event paths carry the tenant too', () => {
|
|
135
|
+
it('createStatusEvent stamps the model tenant', async () => {
|
|
136
|
+
const model = fakeModel(TENANT_A, { status: 'active' });
|
|
137
|
+
await audit.createStatusEvent('Subscription', 'customer.subscription', { active: 'activated' }, model, {
|
|
138
|
+
fields: ['status'],
|
|
139
|
+
});
|
|
140
|
+
const [rows] = await sequelize.query('SELECT instance_did FROM events');
|
|
141
|
+
expect(rows).toEqual([{ instance_did: TENANT_A }]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('createCustomEvent stamps the model tenant', async () => {
|
|
145
|
+
const model = fakeModel(TENANT_B, { status: 'canceled' });
|
|
146
|
+
await audit.createCustomEvent('Subscription', 'customer.subscription', () => 'deleted', model, {
|
|
147
|
+
fields: ['status'],
|
|
148
|
+
});
|
|
149
|
+
const [rows] = await sequelize.query('SELECT type, instance_did FROM events');
|
|
150
|
+
expect(rows).toEqual([{ type: 'customer.subscription.deleted', instance_did: TENANT_B }]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TENANT_CONTEXT_MISSING,
|
|
3
|
+
TenantError,
|
|
4
|
+
context,
|
|
5
|
+
getDefaultInstanceDid,
|
|
6
|
+
getInstanceDid,
|
|
7
|
+
getTenantMode,
|
|
8
|
+
withTenant,
|
|
9
|
+
} from '../../src/libs/context';
|
|
10
|
+
import { TENANT_A, TENANT_B } from '../fixtures/tenants';
|
|
11
|
+
|
|
12
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
|
|
14
|
+
describe('libs/context tenant infrastructure', () => {
|
|
15
|
+
const originalMode = process.env.PAYMENT_TENANT_MODE;
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (originalMode === undefined) {
|
|
19
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
20
|
+
} else {
|
|
21
|
+
process.env.PAYMENT_TENANT_MODE = originalMode;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const asMulti = () => {
|
|
26
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('tenant mode', () => {
|
|
30
|
+
it('defaults to single mode', () => {
|
|
31
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
32
|
+
expect(getTenantMode()).toBe('single');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reads multi mode from env', () => {
|
|
36
|
+
asMulti();
|
|
37
|
+
expect(getTenantMode()).toBe('multi');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('happy path', () => {
|
|
42
|
+
it('returns the tenant inside withTenant and across await boundaries', async () => {
|
|
43
|
+
await withTenant(TENANT_A, async () => {
|
|
44
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
45
|
+
await sleep(5);
|
|
46
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
47
|
+
await Promise.resolve().then(() => {
|
|
48
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('exposes instanceDid via context.getContext()', async () => {
|
|
54
|
+
await withTenant(TENANT_A, async () => {
|
|
55
|
+
expect(context.getContext().instanceDid).toBe(TENANT_A);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns the value produced by fn', async () => {
|
|
60
|
+
const result = await withTenant(TENANT_A, async () => 42);
|
|
61
|
+
expect(result).toBe(42);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('keeps tenant when context.run is nested inside withTenant', async () => {
|
|
65
|
+
await withTenant(TENANT_A, () =>
|
|
66
|
+
context.run({ requestedBy: 'user-x' }, async () => {
|
|
67
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
68
|
+
expect(context.getRequestedBy()).toBe('user-x');
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('keeps tenant when withTenant is nested inside context.run', async () => {
|
|
74
|
+
await context.run({ requestedBy: 'user-x' }, () =>
|
|
75
|
+
withTenant(TENANT_A, async () => {
|
|
76
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
77
|
+
expect(context.getRequestedBy()).toBe('user-x');
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('bad input', () => {
|
|
84
|
+
it.each([['' as any], [' ' as any], [null as any], [undefined as any], [123 as any], ['a b' as any]])(
|
|
85
|
+
'rejects invalid instanceDid %p',
|
|
86
|
+
async (bad) => {
|
|
87
|
+
await expect(withTenant(bad, async () => 'never')).rejects.toMatchObject({
|
|
88
|
+
code: TENANT_CONTEXT_MISSING,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
it('throws TENANT_CONTEXT_MISSING in multi mode without context', () => {
|
|
94
|
+
asMulti();
|
|
95
|
+
try {
|
|
96
|
+
getInstanceDid();
|
|
97
|
+
throw new Error('expected getInstanceDid to throw');
|
|
98
|
+
} catch (err: any) {
|
|
99
|
+
expect(err).toBeInstanceOf(TenantError);
|
|
100
|
+
expect(err.code).toBe(TENANT_CONTEXT_MISSING);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('security: concurrent isolation', () => {
|
|
106
|
+
it('does not leak tenant between interleaved concurrent withTenant scopes', async () => {
|
|
107
|
+
const observed: Record<string, string[]> = { a: [], b: [] };
|
|
108
|
+
await Promise.all([
|
|
109
|
+
withTenant(TENANT_A, async () => {
|
|
110
|
+
observed.a!.push(getInstanceDid());
|
|
111
|
+
await sleep(10);
|
|
112
|
+
observed.a!.push(getInstanceDid());
|
|
113
|
+
await sleep(20);
|
|
114
|
+
observed.a!.push(getInstanceDid());
|
|
115
|
+
}),
|
|
116
|
+
withTenant(TENANT_B, async () => {
|
|
117
|
+
observed.b!.push(getInstanceDid());
|
|
118
|
+
await sleep(15);
|
|
119
|
+
observed.b!.push(getInstanceDid());
|
|
120
|
+
await sleep(5);
|
|
121
|
+
observed.b!.push(getInstanceDid());
|
|
122
|
+
}),
|
|
123
|
+
]);
|
|
124
|
+
expect(observed.a).toEqual([TENANT_A, TENANT_A, TENANT_A]);
|
|
125
|
+
expect(observed.b).toEqual([TENANT_B, TENANT_B, TENANT_B]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('data loss: context survives async scheduling primitives', () => {
|
|
130
|
+
it('keeps tenant across setTimeout', async () => {
|
|
131
|
+
await withTenant(TENANT_A, async () => {
|
|
132
|
+
const seen = await new Promise((resolve) => {
|
|
133
|
+
setTimeout(() => resolve(getInstanceDid()), 5);
|
|
134
|
+
});
|
|
135
|
+
expect(seen).toBe(TENANT_A);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('keeps tenant across queueMicrotask', async () => {
|
|
140
|
+
await withTenant(TENANT_A, async () => {
|
|
141
|
+
const seen = await new Promise((resolve) => {
|
|
142
|
+
queueMicrotask(() => resolve(getInstanceDid()));
|
|
143
|
+
});
|
|
144
|
+
expect(seen).toBe(TENANT_A);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('keeps tenant inside event emitter callbacks registered within the scope', async () => {
|
|
149
|
+
// eslint-disable-next-line global-require
|
|
150
|
+
const { EventEmitter } = require('events');
|
|
151
|
+
const emitter = new EventEmitter();
|
|
152
|
+
await withTenant(TENANT_A, async () => {
|
|
153
|
+
const seen = await new Promise((resolve) => {
|
|
154
|
+
emitter.on('ping', () => resolve(getInstanceDid()));
|
|
155
|
+
emitter.emit('ping');
|
|
156
|
+
});
|
|
157
|
+
expect(seen).toBe(TENANT_A);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('data damage: nesting and immutability', () => {
|
|
163
|
+
it('restores outer tenant after a nested withTenant exits', async () => {
|
|
164
|
+
await withTenant(TENANT_A, async () => {
|
|
165
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
166
|
+
await withTenant(TENANT_B, async () => {
|
|
167
|
+
expect(getInstanceDid()).toBe(TENANT_B);
|
|
168
|
+
});
|
|
169
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('prevents fn from mutating the stored context to affect sibling calls', async () => {
|
|
174
|
+
await withTenant(TENANT_A, async () => {
|
|
175
|
+
const ctx = context.getContext() as any;
|
|
176
|
+
try {
|
|
177
|
+
ctx.instanceDid = TENANT_B;
|
|
178
|
+
} catch {
|
|
179
|
+
// frozen object throws in strict mode — either way the mutation must not stick
|
|
180
|
+
}
|
|
181
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('data leak: single mode default fill', () => {
|
|
187
|
+
it('falls back to the configured app DID in single mode', () => {
|
|
188
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
189
|
+
// jest globalSetup configures BLOCKLET_APP_PID with the test wallet address
|
|
190
|
+
expect(getInstanceDid()).toBe(getDefaultInstanceDid());
|
|
191
|
+
expect(getDefaultInstanceDid()).toBe(process.env.BLOCKLET_APP_PID);
|
|
192
|
+
expect(getDefaultInstanceDid()).not.toBe(TENANT_A);
|
|
193
|
+
expect(getDefaultInstanceDid()).not.toBe(TENANT_B);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('never leaks a tenant injected by another scope into the default', async () => {
|
|
197
|
+
await withTenant(TENANT_A, async () => {
|
|
198
|
+
expect(getInstanceDid()).toBe(TENANT_A);
|
|
199
|
+
});
|
|
200
|
+
// outside any scope, single mode returns the app DID again
|
|
201
|
+
expect(getInstanceDid()).toBe(getDefaultInstanceDid());
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -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
|
+
});
|