payment-kit 1.29.0 → 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/docs/2026-06-10-bundle-size-analysis.md +288 -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 +31 -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/node-fetch.ts +35 -0
- 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,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,66 @@
|
|
|
1
|
+
// Phase 8 (W2-1a): the libs/lock facade — tenant prefix + fail-closed.
|
|
2
|
+
//
|
|
3
|
+
// The driver-level semantics live in packages/payment-core/tests/drivers; this
|
|
4
|
+
// spec covers the facade behavior that the call sites depend on: tenant
|
|
5
|
+
// prefixing, the global-scope exemption, and multi-mode fail-closed.
|
|
6
|
+
//
|
|
7
|
+
// Dynamic require() is required: each case re-imports lock.ts after setting the
|
|
8
|
+
// tenant-mode env + jest.resetModules so the module-level driver is fresh.
|
|
9
|
+
/* eslint-disable global-require, import/no-dynamic-require */
|
|
10
|
+
|
|
11
|
+
describe('getLock facade — tenant prefix + fail-closed', () => {
|
|
12
|
+
const ORIGINAL_MODE = process.env.PAYMENT_TENANT_MODE;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (ORIGINAL_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
|
|
16
|
+
else process.env.PAYMENT_TENANT_MODE = ORIGINAL_MODE;
|
|
17
|
+
jest.resetModules();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('rejects an empty lock name', () => {
|
|
21
|
+
const { getLock } = require('../../src/libs/lock');
|
|
22
|
+
expect(() => getLock('')).toThrow(/non-empty lock name/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('multi mode: tenant-scoped getLock without context fails closed', () => {
|
|
26
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
27
|
+
jest.resetModules();
|
|
28
|
+
const { getLock } = require('../../src/libs/lock');
|
|
29
|
+
let code: string | undefined;
|
|
30
|
+
try {
|
|
31
|
+
getLock('payment-intent-1');
|
|
32
|
+
} catch (e: any) {
|
|
33
|
+
code = e.code;
|
|
34
|
+
}
|
|
35
|
+
expect(code).toBe('TENANT_CONTEXT_MISSING');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('multi mode: global-scoped getLock needs no tenant', () => {
|
|
39
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
40
|
+
jest.resetModules();
|
|
41
|
+
const { getLock } = require('../../src/libs/lock');
|
|
42
|
+
const lock = getLock('startInvoiceQueue', { scope: 'global' });
|
|
43
|
+
expect(lock.name).toBe('startInvoiceQueue');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('multi mode: tenant-scoped getLock inside withTenant carries the prefix', async () => {
|
|
47
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
48
|
+
jest.resetModules();
|
|
49
|
+
const { getLock } = require('../../src/libs/lock');
|
|
50
|
+
const { withTenant } = require('../../src/libs/context');
|
|
51
|
+
await withTenant('did:abt:TENANT_A', () => {
|
|
52
|
+
const lock = getLock('payment-intent-1');
|
|
53
|
+
expect(lock.name).toBe('tenant:did:abt:TENANT_A::payment-intent-1');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('single mode: tenant-scoped getLock falls back to the deployment app DID prefix', () => {
|
|
58
|
+
process.env.PAYMENT_TENANT_MODE = 'single';
|
|
59
|
+
jest.resetModules();
|
|
60
|
+
const { getLock } = require('../../src/libs/lock');
|
|
61
|
+
const lock = getLock('payment-intent-1');
|
|
62
|
+
// single mode never fails closed — name is prefixed with the constant default tenant
|
|
63
|
+
expect(lock.name.startsWith('tenant:')).toBe(true);
|
|
64
|
+
expect(lock.name.endsWith('::payment-intent-1')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { Sequelize } from 'sequelize';
|
|
6
|
+
import { SequelizeStorage, Umzug } from 'umzug';
|
|
7
|
+
|
|
8
|
+
import { withTenant } from '../../src/libs/context';
|
|
9
|
+
import { TENANT_CONTEXT_MISSING, TENANT_MISMATCH } from '../../src/libs/tenant';
|
|
10
|
+
import {
|
|
11
|
+
scopedAggregate,
|
|
12
|
+
scopedCount,
|
|
13
|
+
scopedCreate,
|
|
14
|
+
scopedDestroy,
|
|
15
|
+
scopedFindAll,
|
|
16
|
+
scopedFindByPk,
|
|
17
|
+
scopedFindOne,
|
|
18
|
+
scopedQuery,
|
|
19
|
+
scopedSum,
|
|
20
|
+
scopedUpdate,
|
|
21
|
+
} from '../../src/store/scoped';
|
|
22
|
+
import { TENANT_A, TENANT_B } from '../fixtures/tenants';
|
|
23
|
+
|
|
24
|
+
const STORE_DIR = path.join(__dirname, '../../src/store');
|
|
25
|
+
|
|
26
|
+
// real DB: full migration chain on a temp file, then bind the model
|
|
27
|
+
// singletons to it (order matters — initialize() after DDL, see
|
|
28
|
+
// tenant-columns-model.spec.ts for why)
|
|
29
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'scoped-spec-'));
|
|
30
|
+
const sequelize = new Sequelize({ dialect: 'sqlite', storage: path.join(dir, 'test.db'), logging: false });
|
|
31
|
+
const umzug = new Umzug({
|
|
32
|
+
migrations: {
|
|
33
|
+
glob: ['migrations/*.ts', { cwd: STORE_DIR }],
|
|
34
|
+
resolve: ({ name, path: p, context }) => {
|
|
35
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
36
|
+
const migration = require(p!);
|
|
37
|
+
return {
|
|
38
|
+
name: name.replace(/\.ts$/, '.js'),
|
|
39
|
+
up: () => migration.up({ context }),
|
|
40
|
+
down: () => migration.down({ context }),
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
context: sequelize.getQueryInterface(),
|
|
45
|
+
storage: new SequelizeStorage({ sequelize }),
|
|
46
|
+
logger: undefined,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let Customer: any;
|
|
50
|
+
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
await umzug.up();
|
|
53
|
+
// eslint-disable-next-line global-require
|
|
54
|
+
const models = require('../../src/store/models');
|
|
55
|
+
models.initialize(sequelize);
|
|
56
|
+
Customer = models.Customer;
|
|
57
|
+
}, 120000);
|
|
58
|
+
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
await sequelize.close();
|
|
61
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const seedCustomer = (tenant: string, did: string) =>
|
|
65
|
+
withTenant(tenant, () => scopedCreate(Customer, { livemode: false, did, delinquent: false }));
|
|
66
|
+
|
|
67
|
+
describe('scoped helpers (phase 3)', () => {
|
|
68
|
+
beforeEach(async () => {
|
|
69
|
+
await sequelize.query('DELETE FROM customers');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('happy path', () => {
|
|
73
|
+
it('scoped find returns only the active tenant rows; create stamps the tenant', async () => {
|
|
74
|
+
await seedCustomer(TENANT_A, 'z-user-a');
|
|
75
|
+
await seedCustomer(TENANT_B, 'z-user-b');
|
|
76
|
+
|
|
77
|
+
const aRows = await withTenant(TENANT_A, () => scopedFindAll(Customer));
|
|
78
|
+
expect(aRows.map((r: any) => r.did)).toEqual(['z-user-a']);
|
|
79
|
+
expect((aRows[0] as any).instance_did).toBe(TENANT_A);
|
|
80
|
+
|
|
81
|
+
const bRows = await withTenant(TENANT_B, () => scopedFindAll(Customer));
|
|
82
|
+
expect(bRows.map((r: any) => r.did)).toEqual(['z-user-b']);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('bad input', () => {
|
|
87
|
+
it('multi mode without context fails closed', async () => {
|
|
88
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
89
|
+
try {
|
|
90
|
+
await expect(scopedFindAll(Customer)).rejects.toMatchObject({ code: TENANT_CONTEXT_MISSING });
|
|
91
|
+
} finally {
|
|
92
|
+
delete process.env.PAYMENT_TENANT_MODE;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('raw SQL without an instance_did binding is refused', async () => {
|
|
97
|
+
await withTenant(TENANT_A, async () => {
|
|
98
|
+
expect(() => scopedQuery(sequelize, 'SELECT * FROM customers')).toThrow(
|
|
99
|
+
expect.objectContaining({ code: TENANT_MISMATCH })
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('raw SQL with the binding is auto-supplied the active tenant', async () => {
|
|
105
|
+
await seedCustomer(TENANT_A, 'z-user-a');
|
|
106
|
+
await seedCustomer(TENANT_B, 'z-user-b');
|
|
107
|
+
const [rows] = await withTenant(TENANT_A, () =>
|
|
108
|
+
scopedQuery(sequelize, 'SELECT did FROM customers WHERE instance_did = $instance_did')
|
|
109
|
+
);
|
|
110
|
+
expect((rows as any[]).map((r) => r.did)).toEqual(['z-user-a']);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('security', () => {
|
|
115
|
+
it('explicit foreign where.instance_did is rejected, not silently overridden', async () => {
|
|
116
|
+
await withTenant(TENANT_A, async () => {
|
|
117
|
+
await expect(scopedFindAll(Customer, { where: { instance_did: TENANT_B } as any })).rejects.toMatchObject({
|
|
118
|
+
code: TENANT_MISMATCH,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('explicit foreign instance_did on create is rejected', async () => {
|
|
124
|
+
await withTenant(TENANT_A, async () => {
|
|
125
|
+
await expect(
|
|
126
|
+
scopedCreate(Customer, { livemode: false, did: 'z-x', delinquent: false, instance_did: TENANT_B })
|
|
127
|
+
).rejects.toMatchObject({ code: TENANT_MISMATCH });
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('scanner flags the deliberately-violating fixture file (raw query, no $instance_did)', () => {
|
|
132
|
+
const fixture = path.join(__dirname, '../fixtures/bare-query-violation.ts');
|
|
133
|
+
let failed = false;
|
|
134
|
+
let output = '';
|
|
135
|
+
try {
|
|
136
|
+
execFileSync('node', [path.join(__dirname, '../../../scripts/scan-tenant-queries.js'), '--json', fixture], {
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
});
|
|
139
|
+
} catch (err: any) {
|
|
140
|
+
failed = true;
|
|
141
|
+
output = err.stdout || '';
|
|
142
|
+
}
|
|
143
|
+
expect(failed).toBe(true);
|
|
144
|
+
const result = JSON.parse(output);
|
|
145
|
+
expect(result.violations.length).toBeGreaterThan(0);
|
|
146
|
+
expect(result.violations[0].file).toContain('bare-query-violation.ts');
|
|
147
|
+
expect(result.violations[0].line).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('data loss', () => {
|
|
152
|
+
it('scoped destroy/update only touch the active tenant rows', async () => {
|
|
153
|
+
await seedCustomer(TENANT_A, 'z-user-a');
|
|
154
|
+
await seedCustomer(TENANT_B, 'z-user-b');
|
|
155
|
+
|
|
156
|
+
await withTenant(TENANT_A, () => scopedDestroy(Customer, { where: {} }));
|
|
157
|
+
const [allRows] = await sequelize.query('SELECT did, instance_did FROM customers');
|
|
158
|
+
expect(allRows).toHaveLength(1);
|
|
159
|
+
expect((allRows as any[])[0].instance_did).toBe(TENANT_B);
|
|
160
|
+
|
|
161
|
+
await withTenant(TENANT_B, () => scopedUpdate(Customer, { name: 'renamed' }, { where: {} }));
|
|
162
|
+
const [updated] = await sequelize.query('SELECT name FROM customers');
|
|
163
|
+
expect((updated as any[])[0].name).toBe('renamed');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('data damage', () => {
|
|
168
|
+
it('findByPk on another tenant row returns null (cross-tenant = not-found, §13.1)', async () => {
|
|
169
|
+
// Customer now extends TenantModel: scopedFindByPk delegates to the
|
|
170
|
+
// scoped override, so a cross-tenant pk resolves to null (not-found)
|
|
171
|
+
// rather than throwing — the unified §13.1 semantics (404, not 403).
|
|
172
|
+
const created: any = await seedCustomer(TENANT_B, 'z-user-b');
|
|
173
|
+
await withTenant(TENANT_A, async () => {
|
|
174
|
+
await expect(scopedFindByPk(Customer, created.id)).resolves.toBeNull();
|
|
175
|
+
});
|
|
176
|
+
// same pk under the right tenant works
|
|
177
|
+
const found: any = await withTenant(TENANT_B, () => scopedFindByPk(Customer, created.id));
|
|
178
|
+
expect(found.did).toBe('z-user-b');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('scopedUpdate refuses to change instance_did (rows never change tenant)', async () => {
|
|
182
|
+
await seedCustomer(TENANT_A, 'z-user-a');
|
|
183
|
+
await withTenant(TENANT_A, async () => {
|
|
184
|
+
await expect(scopedUpdate(Customer, { instance_did: TENANT_B } as any, { where: {} })).rejects.toMatchObject({
|
|
185
|
+
code: TENANT_MISMATCH,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('data leak', () => {
|
|
192
|
+
it('findOne/count never see the other tenant even with identical data', async () => {
|
|
193
|
+
await seedCustomer(TENANT_A, 'z-same-shape');
|
|
194
|
+
await withTenant(TENANT_B, () =>
|
|
195
|
+
scopedCreate(Customer, { livemode: false, did: 'z-same-shape-b', delinquent: false, name: 'same' })
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
await withTenant(TENANT_A, async () => {
|
|
199
|
+
expect(await scopedCount(Customer)).toBe(1);
|
|
200
|
+
const one: any = await scopedFindOne(Customer, { where: { livemode: false } });
|
|
201
|
+
expect(one.did).toBe('z-same-shape');
|
|
202
|
+
expect(await scopedFindOne(Customer, { where: { did: 'z-same-shape-b' } })).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('sum/aggregate are tenant-isolated too', async () => {
|
|
207
|
+
await withTenant(TENANT_A, () =>
|
|
208
|
+
scopedCreate(Customer, { livemode: false, did: 'z-sum-a', delinquent: false, next_invoice_sequence: 5 })
|
|
209
|
+
);
|
|
210
|
+
await withTenant(TENANT_B, () =>
|
|
211
|
+
scopedCreate(Customer, { livemode: false, did: 'z-sum-b', delinquent: false, next_invoice_sequence: 11 })
|
|
212
|
+
);
|
|
213
|
+
await withTenant(TENANT_A, async () => {
|
|
214
|
+
expect(await scopedSum(Customer, 'next_invoice_sequence' as any)).toBe(5);
|
|
215
|
+
expect(await scopedAggregate(Customer, 'next_invoice_sequence' as any, 'max')).toBe(5);
|
|
216
|
+
});
|
|
217
|
+
await withTenant(TENANT_B, async () => {
|
|
218
|
+
expect(await scopedSum(Customer, 'next_invoice_sequence' as any)).toBe(11);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Phase 11 (W2-3): the tenant-aware secrets facade resolves the tenant from
|
|
2
|
+
// TenantContext and fails closed in multi mode without context.
|
|
3
|
+
/* eslint-disable global-require */
|
|
4
|
+
|
|
5
|
+
describe('secrets facade — tenant resolution + fail-closed', () => {
|
|
6
|
+
const ORIGINAL_MODE = process.env.PAYMENT_TENANT_MODE;
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (ORIGINAL_MODE === undefined) delete process.env.PAYMENT_TENANT_MODE;
|
|
10
|
+
else process.env.PAYMENT_TENANT_MODE = ORIGINAL_MODE;
|
|
11
|
+
jest.resetModules();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('multi mode: encryptSecret without tenant context fails closed', () => {
|
|
15
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
16
|
+
jest.resetModules();
|
|
17
|
+
const { encryptSecret } = require('../../src/libs/secrets');
|
|
18
|
+
let code: string | undefined;
|
|
19
|
+
try {
|
|
20
|
+
encryptSecret('value');
|
|
21
|
+
} catch (e: any) {
|
|
22
|
+
code = e.code;
|
|
23
|
+
}
|
|
24
|
+
expect(code).toBe('TENANT_CONTEXT_MISSING');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('single mode: encryptSecret/decryptSecret roundtrip via the default (process) key', () => {
|
|
28
|
+
process.env.PAYMENT_TENANT_MODE = 'single';
|
|
29
|
+
jest.resetModules();
|
|
30
|
+
const { encryptSecret, decryptSecret } = require('../../src/libs/secrets');
|
|
31
|
+
const ct = encryptSecret('stripe-secret');
|
|
32
|
+
expect(decryptSecret(ct)).toBe('stripe-secret');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('multi mode: inside withTenant, the facade resolves and uses that tenant', async () => {
|
|
36
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
37
|
+
jest.resetModules();
|
|
38
|
+
const { withTenant } = require('../../src/libs/context');
|
|
39
|
+
const { setSecretsDriver, createKeyringSecretsDriver } = require('../../src/libs/drivers');
|
|
40
|
+
const { encryptSecret, decryptSecret, warmupSecrets } = require('../../src/libs/secrets');
|
|
41
|
+
const identity = {
|
|
42
|
+
resolveInstanceDidForHost: () => null,
|
|
43
|
+
getAppEk: (did: string) => `ek-${did}`,
|
|
44
|
+
};
|
|
45
|
+
setSecretsDriver(createKeyringSecretsDriver(identity));
|
|
46
|
+
await withTenant('did:abt:zFACADE_A', async () => {
|
|
47
|
+
await warmupSecrets(); // warm current tenant for the sync hot path
|
|
48
|
+
const ct = encryptSecret('per-tenant');
|
|
49
|
+
expect(decryptSecret(ct)).toBe('per-tenant');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|