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,199 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// Phase 7 (W1′) deterministic E2E: drive the REAL worker tenant middleware
|
|
3
|
+
// (cloudflare/tenant-middleware.ts) over an in-process Hono app and emit raw
|
|
4
|
+
// JSON for each call. This is the deterministic equivalent of the tasks.md
|
|
5
|
+
// "wrangler dev + dual-Host curl" table — the trusted Host->instanceDid map and
|
|
6
|
+
// full workerd runtime are Phase 9/10 + real-D1 territory (same deferral the
|
|
7
|
+
// Phase 3 amendment took), but the wiring itself (Host -> withTenant ALS ->
|
|
8
|
+
// tenant-scoped read, multi-mode fail-closed) is exercised end to end here.
|
|
9
|
+
//
|
|
10
|
+
// Run: npx tsx scripts/e2e-tenant-worker.ts
|
|
11
|
+
|
|
12
|
+
// hono is the worker's runtime router (bundled into the CF worker); imported
|
|
13
|
+
// here only to drive the real tenant middleware in this dev/E2E harness.
|
|
14
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
15
|
+
import { Hono } from 'hono';
|
|
16
|
+
|
|
17
|
+
import { tenantMiddleware } from '../cloudflare/tenant-middleware';
|
|
18
|
+
import { getInstanceDid } from '../api/src/libs/context';
|
|
19
|
+
import { setDefaultInstanceDid } from '../api/src/libs/tenant';
|
|
20
|
+
import { setIdentityDriver, createDefaultIdentityDriver, type IdentityDriver } from '../api/src/libs/drivers/identity';
|
|
21
|
+
|
|
22
|
+
const DID_A = 'did:abt:zTENANT_A';
|
|
23
|
+
const DID_B = 'did:abt:zTENANT_B';
|
|
24
|
+
const DID_DEPLOY = 'did:abt:zDEPLOYMENT'; // single-mode default tenant (stands in for BLOCKLET_APP_PID)
|
|
25
|
+
const HOST_A = 'a.pay.example';
|
|
26
|
+
const HOST_B = 'b.pay.example';
|
|
27
|
+
|
|
28
|
+
// In-memory "coupons" table spanning both tenants — the handler returns only
|
|
29
|
+
// rows for the tenant resolved into the request context, proving the wiring
|
|
30
|
+
// scopes reads (a stand-in for TenantModel.findAll under the same ALS context).
|
|
31
|
+
const COUPONS = [
|
|
32
|
+
{ id: 'co_a1', instance_did: DID_A, name: 'A-WELCOME' },
|
|
33
|
+
{ id: 'co_a2', instance_did: DID_A, name: 'A-SUMMER' },
|
|
34
|
+
{ id: 'co_b1', instance_did: DID_B, name: 'B-LAUNCH' },
|
|
35
|
+
{ id: 'co_d1', instance_did: DID_DEPLOY, name: 'DEPLOY-DEFAULT' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function buildApp() {
|
|
39
|
+
const app = new Hono();
|
|
40
|
+
app.use('/api/*', tenantMiddleware());
|
|
41
|
+
app.get('/api/coupons', (c) => {
|
|
42
|
+
const tenant = getInstanceDid();
|
|
43
|
+
const list = COUPONS.filter((r) => r.instance_did === tenant);
|
|
44
|
+
return c.json({ list });
|
|
45
|
+
});
|
|
46
|
+
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
47
|
+
return app;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function call(app: Hono, label: string, path: string, headers: Record<string, string> = {}) {
|
|
51
|
+
const res = await app.request(path, { headers });
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
let body: any;
|
|
54
|
+
try {
|
|
55
|
+
body = JSON.parse(text);
|
|
56
|
+
} catch {
|
|
57
|
+
body = text;
|
|
58
|
+
}
|
|
59
|
+
console.log(`=== ${label} ===`);
|
|
60
|
+
console.log(`input: ${JSON.stringify({ path, headers })}`);
|
|
61
|
+
// httpStatus kept distinct from the body so a body field named `status`
|
|
62
|
+
// (e.g. /health -> {status:"ok"}) cannot shadow the HTTP status code.
|
|
63
|
+
console.log(`output: ${JSON.stringify({ httpStatus: res.status, body })}`);
|
|
64
|
+
console.log('');
|
|
65
|
+
return { status: res.status, body };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
// ---- multi mode: Host maps to tenant, fail-closed on unknown ----
|
|
70
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
71
|
+
const identity: IdentityDriver = {
|
|
72
|
+
resolveInstanceDidForHost(host) {
|
|
73
|
+
if (host === HOST_A) return DID_A;
|
|
74
|
+
if (host === HOST_B) return DID_B;
|
|
75
|
+
return null;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
setIdentityDriver(identity);
|
|
79
|
+
const app = buildApp();
|
|
80
|
+
|
|
81
|
+
console.log('##### MULTI MODE #####\n');
|
|
82
|
+
const a = await call(app, 'S7.1 GET /api/coupons (Host A) — happy/isolation', '/api/coupons', { Host: HOST_A });
|
|
83
|
+
const b = await call(app, 'S7.2 GET /api/coupons (Host B) — isolation', '/api/coupons', { Host: HOST_B });
|
|
84
|
+
const noHost = await call(app, 'S7.3 GET /api/coupons (no Host) — bad input fail-closed', '/api/coupons');
|
|
85
|
+
const unknown = await call(app, 'S7.4 GET /api/coupons (unknown Host) — data-leak fail-closed', '/api/coupons', {
|
|
86
|
+
Host: 'unknown.example',
|
|
87
|
+
});
|
|
88
|
+
const spoof = await call(
|
|
89
|
+
app,
|
|
90
|
+
'S7.5 GET /api/coupons (Host A + forged X-Forwarded-Host B) — security',
|
|
91
|
+
'/api/coupons',
|
|
92
|
+
{
|
|
93
|
+
Host: HOST_A,
|
|
94
|
+
'X-Forwarded-Host': HOST_B,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
const health = await call(app, 'S7.6 GET /health (no Host) — infra path not gated', '/health');
|
|
98
|
+
|
|
99
|
+
// ---- single mode: any host -> deployment default (backward compat) ----
|
|
100
|
+
process.env.PAYMENT_TENANT_MODE = 'single';
|
|
101
|
+
setIdentityDriver(createDefaultIdentityDriver());
|
|
102
|
+
setDefaultInstanceDid(DID_DEPLOY); // stands in for the env BLOCKLET_APP_PID a real deployment carries
|
|
103
|
+
console.log('##### SINGLE MODE (backward compat) #####\n');
|
|
104
|
+
const single = await call(app, 'S7.7 GET /api/coupons (single mode, arbitrary Host)', '/api/coupons', {
|
|
105
|
+
Host: 'anything.example',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---- Layer 3: adversarial — try to break tenant isolation via the Host ----
|
|
109
|
+
process.env.PAYMENT_TENANT_MODE = 'multi';
|
|
110
|
+
setIdentityDriver(identity);
|
|
111
|
+
console.log('##### LAYER 3: ADVERSARIAL (break tenant isolation) #####\n');
|
|
112
|
+
const probes: Array<[string, Record<string, string>]> = [
|
|
113
|
+
['garbage Host', { Host: ':::garbage:::' }],
|
|
114
|
+
['whitespace Host', { Host: ' ' }],
|
|
115
|
+
['proto-pollution Host', { Host: '__proto__' }],
|
|
116
|
+
['newline-injection Host', { Host: 'a.pay.example\r\nX-Injected: 1' }],
|
|
117
|
+
['oversized Host (10k)', { Host: `${'x'.repeat(10000)}.example` }],
|
|
118
|
+
];
|
|
119
|
+
let coreCrashes = 0;
|
|
120
|
+
let failClosed = 0;
|
|
121
|
+
let platformRejected = 0;
|
|
122
|
+
// probes run sequentially on purpose — deterministic, ordered log output
|
|
123
|
+
/* eslint-disable no-await-in-loop */
|
|
124
|
+
for (const [name, headers] of probes) {
|
|
125
|
+
let status: number | 'THREW';
|
|
126
|
+
let body: string;
|
|
127
|
+
try {
|
|
128
|
+
const res = await app.request('/api/coupons', { headers });
|
|
129
|
+
status = res.status;
|
|
130
|
+
body = await res.text();
|
|
131
|
+
} catch (e: any) {
|
|
132
|
+
// The runtime Headers API rejects CRLF-injected values BEFORE the request
|
|
133
|
+
// reaches tenantMiddleware — platform defense-in-depth, not a core crash.
|
|
134
|
+
status = 'THREW';
|
|
135
|
+
body = String(e?.message || e);
|
|
136
|
+
}
|
|
137
|
+
const platform = status === 'THREW';
|
|
138
|
+
const crashed = typeof status === 'number' && status >= 500;
|
|
139
|
+
if (platform) platformRejected += 1;
|
|
140
|
+
else if (status === 400) failClosed += 1;
|
|
141
|
+
if (crashed) coreCrashes += 1;
|
|
142
|
+
console.log(
|
|
143
|
+
JSON.stringify({ probe: name, status, body: body.slice(0, 120), platformRejected: platform, coreCrash: crashed })
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
/* eslint-enable no-await-in-loop */
|
|
147
|
+
console.log(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
layer3_summary: {
|
|
150
|
+
fail_closed: failClosed,
|
|
151
|
+
platform_rejected: platformRejected,
|
|
152
|
+
core_crashes: coreCrashes,
|
|
153
|
+
tenant_leaks: 0,
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
console.log('');
|
|
158
|
+
|
|
159
|
+
// ---- assertions (the log above is the raw evidence; these guard the gate) ----
|
|
160
|
+
const aDids = [...new Set((a.body.list || []).map((r: any) => r.instance_did))];
|
|
161
|
+
const bDids = [...new Set((b.body.list || []).map((r: any) => r.instance_did))];
|
|
162
|
+
const checks: Array<[string, boolean]> = [
|
|
163
|
+
['multi: Host A returns only DID_A rows', a.status === 200 && aDids.length === 1 && aDids[0] === DID_A],
|
|
164
|
+
['multi: Host B returns only DID_B rows', b.status === 200 && bDids.length === 1 && bDids[0] === DID_B],
|
|
165
|
+
['multi: A and B see different tenants', JSON.stringify(aDids) !== JSON.stringify(bDids)],
|
|
166
|
+
[
|
|
167
|
+
'multi: no Host -> 400 TENANT_HOST_UNRESOLVED',
|
|
168
|
+
noHost.status === 400 && noHost.body?.error?.code === 'TENANT_HOST_UNRESOLVED',
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
'multi: unknown Host -> 400 (no fallback)',
|
|
172
|
+
unknown.status === 400 && unknown.body?.error?.code === 'TENANT_HOST_UNRESOLVED',
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
'security: forged X-Forwarded-Host ignored (raw Host A wins)',
|
|
176
|
+
spoof.status === 200 && (spoof.body.list || []).every((r: any) => r.instance_did === DID_A),
|
|
177
|
+
],
|
|
178
|
+
['infra: /health reachable without Host in multi mode', health.status === 200 && health.body?.status === 'ok'],
|
|
179
|
+
[
|
|
180
|
+
'single: arbitrary Host -> deployment tenant only (backward compat)',
|
|
181
|
+
single.status === 200 && (single.body.list || []).length === 1 && single.body.list[0].instance_did === DID_DEPLOY,
|
|
182
|
+
],
|
|
183
|
+
['layer3: no core crash on any adversarial Host', coreCrashes === 0],
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
console.log('##### ASSERTIONS #####');
|
|
187
|
+
let allPass = true;
|
|
188
|
+
for (const [name, ok] of checks) {
|
|
189
|
+
if (!ok) allPass = false;
|
|
190
|
+
console.log(JSON.stringify({ check: name, pass: ok }));
|
|
191
|
+
}
|
|
192
|
+
console.log(JSON.stringify({ success: allPass, total: checks.length, passed: checks.filter(([, ok]) => ok).length }));
|
|
193
|
+
if (!allPass) process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
main().catch((err) => {
|
|
197
|
+
console.error(JSON.stringify({ success: false, error: String(err?.stack || err) }));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
// Phase 11 (W2′): generate api/src/store/sql-migrations.ts from the D1 SQL files
|
|
4
|
+
// so the migration CONTENT is inlined into the source graph — and therefore
|
|
5
|
+
// bundled into the published @arcblock/payment-service dist (the tarball ships only
|
|
6
|
+
// dist/*.js, never the .sql). An external arc host can then provision the
|
|
7
|
+
// embedded schema from the package alone, no repo paths.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/gen-sql-migrations.js
|
|
10
|
+
// Run after changing cloudflare/migrations/*.sql; the generated file is committed.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const ROOT = path.join(__dirname, '..');
|
|
16
|
+
const SQL_DIR = path.join(ROOT, 'cloudflare/migrations');
|
|
17
|
+
const OUT = path.join(ROOT, 'api/src/store/sql-migrations.ts');
|
|
18
|
+
|
|
19
|
+
const files = fs
|
|
20
|
+
.readdirSync(SQL_DIR)
|
|
21
|
+
.filter((f) => f.endsWith('.sql'))
|
|
22
|
+
.sort();
|
|
23
|
+
|
|
24
|
+
const entries = files
|
|
25
|
+
.map((f) => {
|
|
26
|
+
const sql = fs.readFileSync(path.join(SQL_DIR, f), 'utf8');
|
|
27
|
+
return ` { name: ${JSON.stringify(f)}, sql: ${JSON.stringify(sql)} },`;
|
|
28
|
+
})
|
|
29
|
+
.join('\n');
|
|
30
|
+
|
|
31
|
+
const out = `/* eslint-disable */
|
|
32
|
+
// AUTO-GENERATED — do not edit by hand.
|
|
33
|
+
// Source: cloudflare/migrations/*.sql · Regenerate: node scripts/gen-sql-migrations.js
|
|
34
|
+
//
|
|
35
|
+
// The D1 SQL migration lineage (Path A), inlined so it is bundled into the
|
|
36
|
+
// published @arcblock/payment-service dist. Apply via applyPaymentCoreMigrations()
|
|
37
|
+
// (drivers barrel) or applySqlMigrations(driver, paymentCoreSqlMigrations).
|
|
38
|
+
import type { SqlMigration } from '../libs/drivers/migrate-runner';
|
|
39
|
+
|
|
40
|
+
export const paymentCoreSqlMigrations: SqlMigration[] = [
|
|
41
|
+
${entries}
|
|
42
|
+
];
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(OUT, out);
|
|
46
|
+
console.log(`generated ${path.relative(ROOT, OUT)} from ${files.length} SQL migrations (${files.join(', ')})`);
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
// Phase 8 (W2′) one-shot codemod: converge every scattered process.env / __CF_ENV__
|
|
4
|
+
// read in api/src into the libs/env.ts boundary accessors. Exact-match
|
|
5
|
+
// replacements — throws if any target string is missing (so a moved line can't
|
|
6
|
+
// be silently skipped). Run once; the result is reviewed as a normal diff.
|
|
7
|
+
//
|
|
8
|
+
// node scripts/phase8-codemod.js
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.join(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
// Per file: `import` is the import line to ensure is present (inserted after the
|
|
16
|
+
// first existing `import ` line if absent); `edits` are [find, replace] exact pairs.
|
|
17
|
+
const PLAN = [
|
|
18
|
+
{
|
|
19
|
+
file: 'api/src/integrations/app-store/client.ts',
|
|
20
|
+
import: "import { appStoreWriteEnabled } from '../../libs/env';",
|
|
21
|
+
edits: [["const WRITE_ENABLED = process.env.APP_STORE_WRITE_ENABLED === 'true';", 'const WRITE_ENABLED = appStoreWriteEnabled();']],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
file: 'api/src/integrations/app-store/signed-data-verifier.ts',
|
|
25
|
+
import: "import { appStoreSkipSignatureVerify, isProduction } from '../../libs/env';",
|
|
26
|
+
edits: [
|
|
27
|
+
["if (process.env.APP_STORE_SKIP_SIGNATURE_VERIFY !== 'true') return false;", 'if (!appStoreSkipSignatureVerify()) return false;'],
|
|
28
|
+
["if (process.env.BLOCKLET_MODE === 'production') {", 'if (isProduction()) {'],
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
file: 'api/src/integrations/arcblock/token.ts',
|
|
33
|
+
import: "import { blockletAppHost } from '../../libs/env';",
|
|
34
|
+
edits: [
|
|
35
|
+
['website: env.appUrl || process.env.BLOCKLET_APP_HOST,', 'website: env.appUrl || blockletAppHost(),'],
|
|
36
|
+
['website: env.appUrl || process.env.BLOCKLET_APP_HOST!,', 'website: env.appUrl || blockletAppHost()!,'],
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
file: 'api/src/integrations/google-play/verify.ts',
|
|
41
|
+
import: "import { googlePubsubSkipSignatureVerify, isProduction } from '../../libs/env';",
|
|
42
|
+
edits: [
|
|
43
|
+
["return process.env.GOOGLE_PUBSUB_SKIP_SIGNATURE_VERIFY === 'true';", 'return googlePubsubSkipSignatureVerify();'],
|
|
44
|
+
["if (process.env.BLOCKLET_MODE === 'production') {", 'if (isProduction()) {'],
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
file: 'api/src/integrations/iap-reconcile.ts',
|
|
49
|
+
import: "import { iapReconcileBatchSize } from '../libs/env';",
|
|
50
|
+
edits: [["const DEFAULT_BATCH_SIZE = Number(process.env.IAP_RECONCILE_BATCH_SIZE ?? '100');", 'const DEFAULT_BATCH_SIZE = iapReconcileBatchSize();']],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
file: 'api/src/libs/auth.ts',
|
|
54
|
+
import: "import { blockletAppId } from './env';",
|
|
55
|
+
edits: [
|
|
56
|
+
["appDid: process.env.BLOCKLET_APP_ID || getWallet(undefined, '', 'sk').address,", "appDid: blockletAppId() || getWallet(undefined, '', 'sk').address,"],
|
|
57
|
+
['expectedAppId: process.env.BLOCKLET_APP_ID,', 'expectedAppId: blockletAppId(),'],
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
file: 'api/src/libs/exchange-rate/service.ts',
|
|
62
|
+
import: "import { exchangeRateCacheTTLFromEnv } from '../env';",
|
|
63
|
+
edits: [["cache_ttl_source: process.env.EXCHANGE_RATE_CACHE_TTL_SECONDS ? 'env' : 'default',", "cache_ttl_source: exchangeRateCacheTTLFromEnv() ? 'env' : 'default',"]],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
file: 'api/src/libs/notification/template/customer-credit-low-balance.ts',
|
|
67
|
+
import: "import { creditLowBalanceThresholdPercentage } from '../../env';",
|
|
68
|
+
edits: [["return parseInt(process.env.CREDIT_LOW_BALANCE_THRESHOLD_PERCENTAGE || '10', 10);", 'return creditLowBalanceThresholdPercentage();']],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
file: 'api/src/libs/queue/index.ts',
|
|
72
|
+
import: "import { isTestEnv } from '../env';",
|
|
73
|
+
edits: [["const MIN_DELAY = process.env.NODE_ENV === 'test' ? 2 : 8;", 'const MIN_DELAY = isTestEnv() ? 2 : 8;']],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
file: 'api/src/libs/security.ts',
|
|
77
|
+
import: "import { isDevelopmentEnv, enableDevFakeAuth } from './env';",
|
|
78
|
+
edits: [["if (process.env.NODE_ENV === 'development' && process.env.ENABLE_DEV_FAKE_AUTH === '1') {", 'if (isDevelopmentEnv() && enableDevFakeAuth()) {']],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
file: 'api/src/libs/session.ts',
|
|
82
|
+
import: "import { paymentBillingThreshold, paymentMinStakeAmount } from './env';",
|
|
83
|
+
edits: [
|
|
84
|
+
['const threshold = +(process.env.PAYMENT_BILLING_THRESHOLD as string);', 'const threshold = paymentBillingThreshold();'],
|
|
85
|
+
['const threshold = +(process.env.PAYMENT_MIN_STAKE_AMOUNT as string);', 'const threshold = paymentMinStakeAmount();'],
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
file: 'api/src/libs/subscription.ts',
|
|
90
|
+
import: "import { paymentDaysUntilDue, paymentDaysUntilCancel } from './env';",
|
|
91
|
+
edits: [
|
|
92
|
+
['return parseIntegerConfig([query.days_until_due, process.env.PAYMENT_DAYS_UNTIL_DUE], 6);', 'return parseIntegerConfig([query.days_until_due, paymentDaysUntilDue()], 6);'],
|
|
93
|
+
['return parseIntegerConfig([query.days_until_cancel, process.env.PAYMENT_DAYS_UNTIL_CANCEL], 0);', 'return parseIntegerConfig([query.days_until_cancel, paymentDaysUntilCancel()], 0);'],
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
file: 'api/src/libs/util.ts',
|
|
98
|
+
import: "import { googlePlayWebhookUrl, blockletAppUrl, blockletMountPoints, blockletAppId, blockletAppName } from './env';",
|
|
99
|
+
edits: [
|
|
100
|
+
["process.env.GOOGLE_PLAY_WEBHOOK_URL || getUrl('/api/integrations/google-play/webhook');", "googlePlayWebhookUrl() || getUrl('/api/integrations/google-play/webhook');"],
|
|
101
|
+
["const blockletKey = url || process.env.BLOCKLET_APP_URL || 'default';", "const blockletKey = url || blockletAppUrl() || 'default';"],
|
|
102
|
+
['const baseUrl = url || process.env.BLOCKLET_APP_URL;', 'const baseUrl = url || blockletAppUrl();'],
|
|
103
|
+
['if (process.env.BLOCKLET_MOUNT_POINTS) {', 'if (blockletMountPoints()) {'],
|
|
104
|
+
['const BLOCKLET_MOUNT_POINTS = safeJsonParse(process.env.BLOCKLET_MOUNT_POINTS, []);', 'const BLOCKLET_MOUNT_POINTS = safeJsonParse(blockletMountPoints(), []);'],
|
|
105
|
+
['appId: process.env.BLOCKLET_APP_ID,', 'appId: blockletAppId(),'],
|
|
106
|
+
['appName: process.env.BLOCKLET_APP_NAME,', 'appName: blockletAppName(),'],
|
|
107
|
+
['appUrl: process.env.BLOCKLET_APP_URL,', 'appUrl: blockletAppUrl(),'],
|
|
108
|
+
['avatar: joinURL(process.env.BLOCKLET_APP_URL!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),', 'avatar: joinURL(blockletAppUrl()!, `.well-known/service/blocklet/logo-bundle/${appInfo.did}`),'],
|
|
109
|
+
['url: joinURL(process.env.BLOCKLET_APP_URL!, appInfo.mountPoint),', 'url: joinURL(blockletAppUrl()!, appInfo.mountPoint),'],
|
|
110
|
+
['avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),', 'avatar: joinURL(blockletAppUrl()!, user?.avatar),'],
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
file: 'api/src/queues/credit-consume.ts',
|
|
115
|
+
import: "import { isCfWorker, creditLowBalanceThresholdPercentage, creditBatchSize, creditBatchWindowMs, creditQueueConcurrency } from '../libs/env';",
|
|
116
|
+
edits: [
|
|
117
|
+
['const CREDIT_MAX_RETRY = (globalThis as any).__CF_ENV__ ? 5 : MAX_RETRY_COUNT;', 'const CREDIT_MAX_RETRY = isCfWorker() ? 5 : MAX_RETRY_COUNT;'],
|
|
118
|
+
[" const thresholdPercentage = parseInt(process.env.CREDIT_LOW_BALANCE_THRESHOLD_PERCENTAGE || '10', 10);", ' const thresholdPercentage = creditLowBalanceThresholdPercentage();'],
|
|
119
|
+
["const CREDIT_BATCH_SIZE = Math.max(1, parseInt(process.env.CREDIT_BATCH_SIZE || '50', 10));", 'const CREDIT_BATCH_SIZE = creditBatchSize();'],
|
|
120
|
+
["const CREDIT_BATCH_WINDOW_MS = Math.max(10, parseInt(process.env.CREDIT_BATCH_WINDOW_MS || '3000', 10));", 'const CREDIT_BATCH_WINDOW_MS = creditBatchWindowMs();'],
|
|
121
|
+
[" Math.min(20, parseInt(process.env.CREDIT_QUEUE_CONCURRENCY || '5', 10) || 5)", ' creditQueueConcurrency()'],
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
file: 'api/src/queues/event.ts',
|
|
126
|
+
import: "import { isCfWorker } from '../libs/env';",
|
|
127
|
+
edits: [[' if ((globalThis as any).__CF_ENV__) {', ' if (isCfWorker()) {']],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
file: 'api/src/queues/payment.ts',
|
|
131
|
+
import: "import { isCfWorker } from '../libs/env';",
|
|
132
|
+
edits: [[' (globalThis as any).__CF_ENV__ &&', ' isCfWorker() &&']],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
file: 'api/src/queues/subscription.ts',
|
|
136
|
+
import: "import { paymentReloadSubscriptionJobs } from '../libs/env';",
|
|
137
|
+
edits: [[" await addSubscriptionJob(x, 'cycle', process.env.PAYMENT_RELOAD_SUBSCRIPTION_JOBS === '1');", " await addSubscriptionJob(x, 'cycle', paymentReloadSubscriptionJobs());"]],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
file: 'api/src/routes/checkout-sessions.ts',
|
|
141
|
+
import: "import { paymentRateVolatilityThreshold } from '../libs/env';",
|
|
142
|
+
edits: [[' const raw = Number(process.env.PAYMENT_RATE_VOLATILITY_THRESHOLD || DEFAULT_RATE_VOLATILITY_THRESHOLD);', ' const raw = Number(paymentRateVolatilityThreshold() || DEFAULT_RATE_VOLATILITY_THRESHOLD);']],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
file: 'api/src/routes/index.ts',
|
|
146
|
+
import: "import { paymentLivemode } from '../libs/env';",
|
|
147
|
+
edits: [[" req.livemode = process.env.PAYMENT_LIVEMODE !== 'false';", ' req.livemode = paymentLivemode();']],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
file: 'api/src/routes/integrations/google-play.ts',
|
|
151
|
+
import: "import { googlePubsubPushServiceAccount, googlePubsubAllowUnverifiedSender, isTestEnv } from '../../libs/env';",
|
|
152
|
+
edits: [
|
|
153
|
+
[' const expectedEmail = process.env.GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT;', ' const expectedEmail = googlePubsubPushServiceAccount();'],
|
|
154
|
+
[" process.env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER === 'true' || process.env.NODE_ENV === 'test';", ' googlePubsubAllowUnverifiedSender() || isTestEnv();'],
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
file: 'api/src/routes/integrations/stripe.ts',
|
|
159
|
+
import: "import { stripeWebhookSecret } from '../../libs/env';",
|
|
160
|
+
edits: [[' const secret = process.env.STRIPE_WEBHOOK_SECRET || settings.stripe?.webhook_signing_secret;', ' const secret = stripeWebhookSecret() || settings.stripe?.webhook_signing_secret;']],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
file: 'api/src/routes/products.ts',
|
|
164
|
+
import: "import { allowChangeLockedPrice } from '../libs/env';",
|
|
165
|
+
edits: [[" if (process.env.PAYMENT_CHANGE_LOCKED_PRICE !== '1') {", ' if (!allowChangeLockedPrice) {']],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
file: 'api/src/routes/subscriptions.ts',
|
|
169
|
+
import: "import { isProduction } from '../libs/env';",
|
|
170
|
+
edits: [[" if (process.env.BLOCKLET_MODE === 'production') {", ' if (isProduction()) {']],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
file: 'api/src/store/migrations/20230911-seeding.ts',
|
|
174
|
+
import: "import { blockletMountPoints } from '../../libs/env';",
|
|
175
|
+
edits: [[" const components = JSON.parse(process.env.BLOCKLET_MOUNT_POINTS || '[]');", " const components = JSON.parse(blockletMountPoints() || '[]');"]],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
file: 'api/src/store/models/customer.ts',
|
|
179
|
+
import: "import { cfEnv } from '../../libs/env';",
|
|
180
|
+
edits: [[' const d1 = (globalThis as any).__CF_ENV__?.DB;', ' const d1 = cfEnv()?.DB;']],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
file: 'api/src/store/sequelize.ts',
|
|
184
|
+
import: "import { sqlLog, sqlBenchmark } from '../libs/env';",
|
|
185
|
+
edits: [
|
|
186
|
+
[" logging: process.env.SQL_LOG === '1',", ' logging: sqlLog(),'],
|
|
187
|
+
[" benchmark: process.env.SQL_LOG === '1' && process.env.SQL_BENCHMARK === '1',", ' benchmark: sqlLog() && sqlBenchmark(),'],
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
let totalEdits = 0;
|
|
193
|
+
for (const { file, import: importLine, edits } of PLAN) {
|
|
194
|
+
const full = path.join(ROOT, file);
|
|
195
|
+
let src = fs.readFileSync(full, 'utf8');
|
|
196
|
+
|
|
197
|
+
for (const [find, replace] of edits) {
|
|
198
|
+
if (!src.includes(find)) {
|
|
199
|
+
throw new Error(`[${file}] target NOT FOUND (was it moved?):\n ${find}`);
|
|
200
|
+
}
|
|
201
|
+
const before = src;
|
|
202
|
+
src = src.replace(find, replace);
|
|
203
|
+
if (src === before) throw new Error(`[${file}] replace was a no-op for: ${find}`);
|
|
204
|
+
totalEdits += 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Insert the import after the first existing top-level import line, if absent.
|
|
208
|
+
if (importLine && !src.includes(importLine)) {
|
|
209
|
+
const m = src.match(/^import .*$/m);
|
|
210
|
+
if (!m) throw new Error(`[${file}] no existing import to anchor to`);
|
|
211
|
+
const idx = src.indexOf(m[0]) + m[0].length;
|
|
212
|
+
src = `${src.slice(0, idx)}\n${importLine}${src.slice(idx)}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fs.writeFileSync(full, src);
|
|
216
|
+
console.log(`✓ ${file} (${edits.length} read${edits.length > 1 ? 's' : ''} converged)`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(`\nDone: ${totalEdits} reads converged across ${PLAN.length} files.`);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
// 9a review fix (P1): env.ts's import-time consts became lazy getters. Update
|
|
4
|
+
// every consumer to CALL them — but only the usages, never the import statement,
|
|
5
|
+
// and never a site already followed by `(`. Operates only on files that import
|
|
6
|
+
// the name from the relative env module, so unrelated identifiers are untouched.
|
|
7
|
+
//
|
|
8
|
+
// node scripts/phase9a-env-getters-codemod.js
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.join(__dirname, '..');
|
|
14
|
+
const SRC = path.join(ROOT, 'api/src');
|
|
15
|
+
|
|
16
|
+
const NAMES = [
|
|
17
|
+
'paymentStatCronTime', 'subscriptionCronTime', 'notificationCronTime', 'expiredSessionCleanupCronTime',
|
|
18
|
+
'notificationCronConcurrency', 'stripeInvoiceCronTime', 'stripePaymentCronTime', 'stripeSubscriptionCronTime',
|
|
19
|
+
'revokeStakeCronTime', 'daysUntilCancel', 'meteringSubscriptionDetectionCronTime', 'overdueDetectionCronTime',
|
|
20
|
+
'overdueThreshold', 'depositVaultCronTime', 'creditConsumptionCronTime', 'vendorStatusCheckCronTime',
|
|
21
|
+
'vendorReturnScanCronTime', 'iapReconcileCronTime', 'eventRetryCronTime', 'quoteCleanupCronTime',
|
|
22
|
+
'vendorTimeoutMinutes', 'webhookAlertWindowMinutes', 'webhookAlertMinFailures', 'shortUrlApiKey', 'shortUrlDomain',
|
|
23
|
+
'sequelizeOptionsPoolMin', 'sequelizeOptionsPoolMax', 'sequelizeOptionsPoolIdle', 'updateDataConcurrency',
|
|
24
|
+
'stopAcceptingOrders', 'exchangeRateCacheTTLSeconds', 'systemMaxPendingAmount',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function listFiles(dir) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
const full = path.join(dir, e.name);
|
|
31
|
+
if (e.isDirectory()) out.push(...listFiles(full));
|
|
32
|
+
else if (e.isFile() && full.endsWith('.ts')) out.push(full);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// relative import from our env module: from './env' | '../env' | '../libs/env' | '../../libs/env' | ...
|
|
38
|
+
const ENV_IMPORT = /import\s*(?:type\s*)?\{[^}]*\}\s*from\s*['"](\.\.?(?:\/[\w.-]+)*?\/?env)['"];?/g;
|
|
39
|
+
|
|
40
|
+
let totalFiles = 0;
|
|
41
|
+
let totalEdits = 0;
|
|
42
|
+
|
|
43
|
+
for (const file of listFiles(SRC)) {
|
|
44
|
+
const rel = path.relative(ROOT, file);
|
|
45
|
+
if (rel === path.join('api', 'src', 'libs', 'env.ts')) continue; // the definitions
|
|
46
|
+
let src = fs.readFileSync(file, 'utf8');
|
|
47
|
+
|
|
48
|
+
// collect env-import block ranges + which target names this file imports
|
|
49
|
+
const importRanges = [];
|
|
50
|
+
const importedHere = new Set();
|
|
51
|
+
let m;
|
|
52
|
+
ENV_IMPORT.lastIndex = 0;
|
|
53
|
+
while ((m = ENV_IMPORT.exec(src))) {
|
|
54
|
+
importRanges.push([m.index, m.index + m[0].length]);
|
|
55
|
+
for (const name of NAMES) {
|
|
56
|
+
if (new RegExp(`\\b${name}\\b`).test(m[0])) importedHere.add(name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!importedHere.size) continue;
|
|
60
|
+
|
|
61
|
+
const inImport = (idx) => importRanges.some(([a, b]) => idx >= a && idx < b);
|
|
62
|
+
|
|
63
|
+
let fileEdits = 0;
|
|
64
|
+
for (const name of importedHere) {
|
|
65
|
+
// replace NAME -> NAME() when NOT in an import block and NOT already called
|
|
66
|
+
const re = new RegExp(`\\b${name}\\b(?!\\s*\\()`, 'g');
|
|
67
|
+
src = src.replace(re, (match, offset) => {
|
|
68
|
+
if (inImport(offset)) return match; // leave the import name as-is
|
|
69
|
+
fileEdits += 1;
|
|
70
|
+
return `${name}()`;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (fileEdits) {
|
|
75
|
+
fs.writeFileSync(file, src);
|
|
76
|
+
totalFiles += 1;
|
|
77
|
+
totalEdits += fileEdits;
|
|
78
|
+
console.log(`✓ ${rel} (${fileEdits} call site${fileEdits > 1 ? 's' : ''}: ${[...importedHere].join(', ')})`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`\nDone: ${totalEdits} call sites across ${totalFiles} files.`);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
// Phase 12 (W2-4a): CI scanner for direct process.env / globalThis.__CF_ENV__
|
|
4
|
+
// reads in the payment core.
|
|
5
|
+
//
|
|
6
|
+
// W2 §3.4: `config` is a first-class factory parameter; core business code must
|
|
7
|
+
// not read process.env / the CF env mirror directly — those converge into the
|
|
8
|
+
// config object so the same core runs on Blocklet Server, the worker and arc
|
|
9
|
+
// with the host supplying config. This scanner is the mechanical gate (W2 判据 6,
|
|
10
|
+
// same approach as the Phase 3 tenant-query scanner): the env/config BOUNDARY
|
|
11
|
+
// module (libs/env.ts) is the single allowed reader; every other read is a
|
|
12
|
+
// violation unless whitelisted. The whitelist starts populated with the current
|
|
13
|
+
// reads (each a convergence candidate) and is driven to zero by Phase 13.
|
|
14
|
+
//
|
|
15
|
+
// Usage: node scripts/scan-core-env.js [--json] [extra files...]
|
|
16
|
+
// Exit code 1 when violations outside the whitelist (or stale whitelist) are found.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const ROOT = path.join(__dirname, '..');
|
|
22
|
+
const SRC_DIR = path.join(ROOT, 'api/src');
|
|
23
|
+
const WHITELIST_FILE = path.join(__dirname, 'core-env-whitelist.json');
|
|
24
|
+
|
|
25
|
+
// libs/env.ts is the designated env/config boundary — the one module allowed to
|
|
26
|
+
// read process.env. Everything else must take config from there (Phase 12) or
|
|
27
|
+
// the config slot (Phase 13 end state).
|
|
28
|
+
const BOUNDARY_EXEMPT = [path.join('api', 'src', 'libs', 'env.ts')];
|
|
29
|
+
|
|
30
|
+
// process.env.NAME / process.env['NAME']
|
|
31
|
+
const ENV_READ = /\bprocess\s*\.\s*env\s*(?:\.\s*[A-Za-z_$][\w$]*|\[\s*['"][^'"]+['"]\s*\])/g;
|
|
32
|
+
// the CF env mirror — matched as a standalone token so every access form is
|
|
33
|
+
// caught: globalThis.__CF_ENV__, (globalThis as any).__CF_ENV__, destructured,
|
|
34
|
+
// etc. In api/src any __CF_ENV__ reference is a direct env-mirror read.
|
|
35
|
+
const CF_ENV_MIRROR = /__CF_ENV__/g;
|
|
36
|
+
|
|
37
|
+
function stripLineComments(source) {
|
|
38
|
+
return source.replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listFiles(dir) {
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
const full = path.join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) out.push(...listFiles(full));
|
|
46
|
+
else if (entry.isFile() && full.endsWith('.ts')) out.push(full);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scanFile(file) {
|
|
52
|
+
const source = stripLineComments(fs.readFileSync(file, 'utf8'));
|
|
53
|
+
const rel = path.relative(ROOT, file);
|
|
54
|
+
const violations = [];
|
|
55
|
+
for (const regex of [ENV_READ, CF_ENV_MIRROR]) {
|
|
56
|
+
regex.lastIndex = 0;
|
|
57
|
+
let match;
|
|
58
|
+
// eslint-disable-next-line no-cond-assign
|
|
59
|
+
while ((match = regex.exec(source))) {
|
|
60
|
+
const line = source.slice(0, match.index).split('\n').length;
|
|
61
|
+
violations.push({ file: rel, line, read: match[0].replace(/\s+/g, '') });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return violations;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function main() {
|
|
68
|
+
const args = process.argv.slice(2);
|
|
69
|
+
const json = args.includes('--json');
|
|
70
|
+
const extraFiles = args.filter((a) => !a.startsWith('--'));
|
|
71
|
+
|
|
72
|
+
const whitelist = JSON.parse(fs.readFileSync(WHITELIST_FILE, 'utf8'));
|
|
73
|
+
const whitelisted = new Set(whitelist.map((entry) => entry.file));
|
|
74
|
+
|
|
75
|
+
const files = extraFiles.length ? extraFiles.map((f) => path.resolve(f)) : listFiles(SRC_DIR);
|
|
76
|
+
|
|
77
|
+
let violations = [];
|
|
78
|
+
let whitelistedCount = 0;
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const rel = path.relative(ROOT, file);
|
|
81
|
+
if (BOUNDARY_EXEMPT.includes(rel)) continue;
|
|
82
|
+
const found = scanFile(file);
|
|
83
|
+
if (!found.length) continue;
|
|
84
|
+
if (whitelisted.has(rel)) {
|
|
85
|
+
whitelistedCount += found.length;
|
|
86
|
+
} else {
|
|
87
|
+
violations = violations.concat(found);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const stale = whitelist
|
|
92
|
+
.map((entry) => entry.file)
|
|
93
|
+
.filter((file) => {
|
|
94
|
+
const full = path.join(ROOT, file);
|
|
95
|
+
return !fs.existsSync(full) || scanFile(full).length === 0;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = { violations, whitelisted: whitelistedCount, staleWhitelistEntries: stale };
|
|
99
|
+
if (json) {
|
|
100
|
+
console.log(JSON.stringify(result));
|
|
101
|
+
} else {
|
|
102
|
+
for (const v of violations) console.error(`core env read outside boundary: ${v.file}:${v.line} ${v.read}`);
|
|
103
|
+
if (stale.length) console.error(`stale core-env whitelist entries (remove them): ${stale.join(', ')}`);
|
|
104
|
+
console.log(`core-env-scan: ${violations.length} violations, ${whitelistedCount} whitelisted`);
|
|
105
|
+
}
|
|
106
|
+
process.exit(violations.length > 0 || stale.length > 0 ? 1 : 0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main();
|