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,194 @@
|
|
|
1
|
+
// Phase 11 (W2-3): secrets slot driver contract — per-tenant keyring.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the process-level single key (cloudflare/shims/blocklet-sdk/security
|
|
4
|
+
// `_password`, initialized once from APP_PID's EK). Each tenant gets its own
|
|
5
|
+
// encryption key derived from its own EK, so one tenant's key can never decrypt
|
|
6
|
+
// another's ciphertext.
|
|
7
|
+
//
|
|
8
|
+
// Two surfaces, because the payment hot path (PaymentMethod.encrypt/decrypt
|
|
9
|
+
// Settings, getStripeClient, ~50 sync call sites) cannot become async without a
|
|
10
|
+
// large, risky ripple:
|
|
11
|
+
// - encrypt/decrypt (async): the full contract — lazy EK fetch + cache + TTL,
|
|
12
|
+
// and decrypt-failure forces one EK re-fetch (tolerates identity-side EK
|
|
13
|
+
// rotation). Hosts use this.
|
|
14
|
+
// - encryptSync/decryptSync: the hot path — uses an already-resolved key
|
|
15
|
+
// (default driver: the process key; keyring driver: the cached password,
|
|
16
|
+
// warmed via warmup()). Throws if the keyring key is cold (fail-closed).
|
|
17
|
+
// - warmup(instanceDid): async pre-resolve so the sync path is a cache hit
|
|
18
|
+
// (no-op for the single-tenant default driver).
|
|
19
|
+
//
|
|
20
|
+
// single mode (Blocklet Server) uses the default driver = the existing process
|
|
21
|
+
// `@blocklet/sdk/lib/security`, so existing ciphertext stays decryptable and
|
|
22
|
+
// behavior is unchanged.
|
|
23
|
+
|
|
24
|
+
/* eslint-disable max-classes-per-file */
|
|
25
|
+
import crypto from 'crypto';
|
|
26
|
+
|
|
27
|
+
import type { IdentityDriver } from './identity';
|
|
28
|
+
|
|
29
|
+
// crypto-js ships no type declarations; required untyped (same AES chain as the
|
|
30
|
+
// prior process security so ciphertext is interchangeable)
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-extraneous-dependencies
|
|
32
|
+
const CryptoJS: any = require('crypto-js');
|
|
33
|
+
|
|
34
|
+
export interface SecretsDriver {
|
|
35
|
+
/** async per-tenant encrypt — lazy-resolves the tenant key on first use */
|
|
36
|
+
encrypt(instanceDid: string, value: string): Promise<string>;
|
|
37
|
+
/** async per-tenant decrypt — re-fetches the EK once on failure (EK rotation tolerance) */
|
|
38
|
+
decrypt(instanceDid: string, value: string): Promise<string>;
|
|
39
|
+
/** sync hot path — requires the tenant key already resolved (warmup first for the keyring) */
|
|
40
|
+
encryptSync(instanceDid: string, value: string): string;
|
|
41
|
+
decryptSync(instanceDid: string, value: string): string;
|
|
42
|
+
/** pre-resolve the tenant key so the sync path is a cache hit (no-op for the default driver) */
|
|
43
|
+
warmup(instanceDid: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// default driver — single-tenant, delegates to the process @blocklet/sdk security
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
class DefaultSecretsDriver implements SecretsDriver {
|
|
51
|
+
// lazily required so importing this module stays side-effect-free
|
|
52
|
+
private security() {
|
|
53
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
54
|
+
return require('@blocklet/sdk/lib/security').default ?? require('@blocklet/sdk/lib/security');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
encryptSync(_instanceDid: string, value: string): string {
|
|
58
|
+
return this.security().encrypt(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
decryptSync(_instanceDid: string, value: string): string {
|
|
62
|
+
return this.security().decrypt(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// eslint-disable-next-line require-await -- async contract; the single key is sync
|
|
66
|
+
async encrypt(instanceDid: string, value: string): Promise<string> {
|
|
67
|
+
return this.encryptSync(instanceDid, value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// eslint-disable-next-line require-await -- async contract; the single key is sync
|
|
71
|
+
async decrypt(instanceDid: string, value: string): Promise<string> {
|
|
72
|
+
return this.decryptSync(instanceDid, value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async warmup(): Promise<void> {
|
|
76
|
+
/* single key is always available — nothing to warm */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createDefaultSecretsDriver(): SecretsDriver {
|
|
81
|
+
return new DefaultSecretsDriver();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// keyring driver — per-tenant key derived from each tenant's EK
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 min
|
|
89
|
+
|
|
90
|
+
type CacheEntry = { password: string; expiresAt: number };
|
|
91
|
+
|
|
92
|
+
// AES decrypt with a wrong key can throw "Malformed UTF-8" or return empty,
|
|
93
|
+
// depending on the ciphertext bytes. Normalize both to '' so a wrong key is a
|
|
94
|
+
// deterministic failure (never the plaintext, never a crash) — this is what
|
|
95
|
+
// drives the decrypt-retry and what keeps cross-tenant decryption safe.
|
|
96
|
+
function safeUtf8(decrypted: any): string {
|
|
97
|
+
try {
|
|
98
|
+
return decrypted.toString(CryptoJS.enc.Utf8);
|
|
99
|
+
} catch {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function derivePassword(appEk: string, instanceDid: string): string {
|
|
105
|
+
// identical chain to the prior process security: PBKDF2(EK, salt) -> AES key.
|
|
106
|
+
// salt = the tenant DID (== blockletDid in single mode), so single-mode
|
|
107
|
+
// ciphertext written by the old path stays decryptable when the same EK is
|
|
108
|
+
// returned by identity.getAppEk(instanceDid).
|
|
109
|
+
return crypto.pbkdf2Sync(appEk, instanceDid, 256, 32, 'sha512').toString('hex');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class KeyringSecretsDriver implements SecretsDriver {
|
|
113
|
+
private identity: IdentityDriver;
|
|
114
|
+
private ttlMs: number;
|
|
115
|
+
private cache = new Map<string, CacheEntry>();
|
|
116
|
+
|
|
117
|
+
constructor(identity: IdentityDriver, opts?: { ttlMs?: number }) {
|
|
118
|
+
if (typeof identity.getAppEk !== 'function') {
|
|
119
|
+
throw new Error('createKeyringSecretsDriver: identity driver must provide getAppEk(instanceDid)');
|
|
120
|
+
}
|
|
121
|
+
this.identity = identity;
|
|
122
|
+
this.ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private fresh(instanceDid: string): string | null {
|
|
126
|
+
const entry = this.cache.get(instanceDid);
|
|
127
|
+
if (entry && entry.expiresAt > Date.now()) return entry.password;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async resolvePassword(instanceDid: string, force = false): Promise<string> {
|
|
132
|
+
if (!force) {
|
|
133
|
+
const cached = this.fresh(instanceDid);
|
|
134
|
+
if (cached) return cached;
|
|
135
|
+
}
|
|
136
|
+
const appEk = await this.identity.getAppEk!(instanceDid);
|
|
137
|
+
if (!appEk) {
|
|
138
|
+
throw new Error(`secrets: no EK for tenant ${instanceDid}`);
|
|
139
|
+
}
|
|
140
|
+
const password = derivePassword(appEk, instanceDid);
|
|
141
|
+
this.cache.set(instanceDid, { password, expiresAt: Date.now() + this.ttlMs });
|
|
142
|
+
return password;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async warmup(instanceDid: string): Promise<void> {
|
|
146
|
+
await this.resolvePassword(instanceDid);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
encryptSync(instanceDid: string, value: string): string {
|
|
150
|
+
const password = this.fresh(instanceDid);
|
|
151
|
+
if (!password) throw new Error(`secrets: key for tenant ${instanceDid} is not warmed (call warmup first)`);
|
|
152
|
+
return CryptoJS.AES.encrypt(value, password).toString();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
decryptSync(instanceDid: string, value: string): string {
|
|
156
|
+
const password = this.fresh(instanceDid);
|
|
157
|
+
if (!password) throw new Error(`secrets: key for tenant ${instanceDid} is not warmed (call warmup first)`);
|
|
158
|
+
return safeUtf8(CryptoJS.AES.decrypt(value, password));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async encrypt(instanceDid: string, value: string): Promise<string> {
|
|
162
|
+
const password = await this.resolvePassword(instanceDid);
|
|
163
|
+
return CryptoJS.AES.encrypt(value, password).toString();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async decrypt(instanceDid: string, value: string): Promise<string> {
|
|
167
|
+
let password = await this.resolvePassword(instanceDid);
|
|
168
|
+
let plain = safeUtf8(CryptoJS.AES.decrypt(value, password));
|
|
169
|
+
if (!plain) {
|
|
170
|
+
// empty result == wrong key (likely rotated EK) -> force one re-fetch + retry
|
|
171
|
+
password = await this.resolvePassword(instanceDid, true);
|
|
172
|
+
plain = safeUtf8(CryptoJS.AES.decrypt(value, password));
|
|
173
|
+
}
|
|
174
|
+
return plain;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createKeyringSecretsDriver(identity: IdentityDriver, opts?: { ttlMs?: number }): SecretsDriver {
|
|
179
|
+
return new KeyringSecretsDriver(identity, opts);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// active driver + tenant-aware facade (resolves tenant from TenantContext)
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
let activeSecretsDriver: SecretsDriver = createDefaultSecretsDriver();
|
|
187
|
+
|
|
188
|
+
export function setSecretsDriver(driver: SecretsDriver): void {
|
|
189
|
+
activeSecretsDriver = driver;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getSecretsDriver(): SecretsDriver {
|
|
193
|
+
return activeSecretsDriver;
|
|
194
|
+
}
|
package/api/src/libs/env.ts
CHANGED
|
@@ -1,68 +1,184 @@
|
|
|
1
1
|
import { env } from '@blocklet/sdk/lib/env';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
export
|
|
3
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Phase 8 (W2′): the injected-config slot, made authoritative here.
|
|
5
|
+
//
|
|
6
|
+
// libs/env.ts is the ONE module the core-env scanner exempts — the single
|
|
7
|
+
// allowed reader of process.env / the CF env mirror. Phase 8 converges every
|
|
8
|
+
// scattered `process.env.X` read in api/src INTO the accessors below so the
|
|
9
|
+
// whitelist goes to zero.
|
|
10
|
+
//
|
|
11
|
+
// The factory (createEmbeddedPaymentService) calls setCoreConfig(config) so the
|
|
12
|
+
// injected config object becomes the source of truth. Reads fall back to
|
|
13
|
+
// process.env when a key is absent from the injected config — which keeps every
|
|
14
|
+
// host working unchanged: the blocklet server populates process.env natively,
|
|
15
|
+
// and the CF worker mirrors CF env into process.env per request (that mirror is
|
|
16
|
+
// deleted in Phase 12, once the worker routes through the factory config slot).
|
|
17
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
18
|
+
let activeConfig: Record<string, any> | undefined;
|
|
19
|
+
|
|
20
|
+
/** Wire the injected config object (factory only). Pass undefined to clear (tests). */
|
|
21
|
+
export function setCoreConfig(config: Record<string, any> | undefined): void {
|
|
22
|
+
activeConfig = config;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The injected config object, or undefined before the factory has run. */
|
|
26
|
+
export function getCoreConfig(): Record<string, any> | undefined {
|
|
27
|
+
return activeConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a config key: the injected config wins; otherwise the process.env
|
|
32
|
+
* fallback (blocklet server native / worker mirror). Returns a string or
|
|
33
|
+
* undefined — callers parse/typecheck. This is the single boundary read.
|
|
34
|
+
*/
|
|
35
|
+
export function readConfig(key: string): string | undefined {
|
|
36
|
+
const injected = activeConfig?.[key];
|
|
37
|
+
if (injected !== undefined && injected !== null) return String(injected);
|
|
38
|
+
const fromEnv = process.env[key];
|
|
39
|
+
return fromEnv === undefined ? undefined : fromEnv;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True iff the key is present (non-empty) in injected config or process.env. */
|
|
43
|
+
export function hasConfig(key: string): boolean {
|
|
44
|
+
const v = readConfig(key);
|
|
45
|
+
return v !== undefined && v !== '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// P1 (9a review fix): these were import-time `process.env` consts — frozen
|
|
50
|
+
// before the factory could setCoreConfig, so injected config could never
|
|
51
|
+
// override them (now that 9a bundles the canonical core into the published
|
|
52
|
+
// package, that mattered for arc). They are LAZY getters now, reading via the
|
|
53
|
+
// readConfig boundary, honored at call time. Consumers call them at use time.
|
|
54
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
55
|
+
const numConfig = (key: string, fallback: number): number => {
|
|
56
|
+
const v = readConfig(key);
|
|
57
|
+
return v ? +v : fallback;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const paymentStatCronTime = (): string => '0 1 0 * * *'; // 默认每天一次,计算前一天的
|
|
61
|
+
export const subscriptionCronTime = (): string => readConfig('SUBSCRIPTION_CRON_TIME') || '0 */30 * * * *';
|
|
62
|
+
export const notificationCronTime = (): string => readConfig('NOTIFICATION_CRON_TIME') || '0 5 */6 * * *';
|
|
63
|
+
export const expiredSessionCleanupCronTime = (): string =>
|
|
64
|
+
readConfig('EXPIRED_SESSION_CLEANUP_CRON_TIME') || '0 1 * * * *';
|
|
65
|
+
export const notificationCronConcurrency = (): number => Number(readConfig('NOTIFICATION_CRON_CONCURRENCY')) || 8;
|
|
66
|
+
export const stripeInvoiceCronTime = (): string => readConfig('STRIPE_INVOICE_CRON_TIME') || '0 */30 * * * *';
|
|
67
|
+
export const stripePaymentCronTime = (): string => readConfig('STRIPE_PAYMENT_CRON_TIME') || '0 */20 * * * *';
|
|
68
|
+
export const stripeSubscriptionCronTime = (): string => readConfig('STRIPE_SUBSCRIPTION_CRON_TIME') || '0 10 */8 * * *';
|
|
69
|
+
export const revokeStakeCronTime = (): string => readConfig('REVOKE_STAKE_CRON_TIME') || '0 */5 * * * *';
|
|
70
|
+
export const daysUntilCancel = (): string | undefined => readConfig('DAYS_UNTIL_CANCEL');
|
|
71
|
+
export const meteringSubscriptionDetectionCronTime = (): string =>
|
|
72
|
+
readConfig('METERING_SUBSCRIPTION_DETECTION_CRON_TIME') || '0 0 10 * * *';
|
|
73
|
+
export const overdueDetectionCronTime = (): string => readConfig('OVERDUE_DETECTION_CRON_TIME') || '0 0 10 * * *';
|
|
74
|
+
export const overdueThreshold = (): number => numConfig('OVERDUE_THRESHOLD', 5);
|
|
75
|
+
export const depositVaultCronTime = (): string => readConfig('DEPOSIT_VAULT_CRON_TIME') || '0 */5 * * * *';
|
|
76
|
+
export const creditConsumptionCronTime = (): string => readConfig('CREDIT_CONSUMPTION_CRON_TIME') || '0 */10 * * * *';
|
|
77
|
+
export const vendorStatusCheckCronTime = (): string => readConfig('VENDOR_STATUS_CHECK_CRON_TIME') || '0 */10 * * * *';
|
|
78
|
+
export const vendorReturnScanCronTime = (): string => readConfig('VENDOR_RETURN_SCAN_CRON_TIME') || '0 */10 * * * *';
|
|
79
|
+
export const iapReconcileCronTime = (): string => readConfig('IAP_RECONCILE_CRON_TIME') || '0 */5 * * * *';
|
|
80
|
+
export const eventRetryCronTime = (): string => readConfig('EVENT_RETRY_CRON_TIME') || '30 */5 * * * *';
|
|
81
|
+
export const quoteCleanupCronTime = (): string => readConfig('QUOTE_CLEANUP_CRON_TIME') || '0 0 2 * * *';
|
|
82
|
+
export const vendorTimeoutMinutes = (): number => numConfig('VENDOR_TIMEOUT_MINUTES', 10);
|
|
83
|
+
export const webhookAlertWindowMinutes = (): number => numConfig('WEBHOOK_ALERT_WINDOW_MINUTES', 10);
|
|
84
|
+
export const webhookAlertMinFailures = (): number => numConfig('WEBHOOK_ALERT_MIN_FAILURES', 3);
|
|
85
|
+
|
|
86
|
+
export const shortUrlApiKey = (): string => readConfig('SHORT_URL_API_KEY') || '';
|
|
87
|
+
export const shortUrlDomain = (): string => readConfig('SHORT_URL_DOMAIN') || 's.abtnet.io';
|
|
36
88
|
|
|
37
89
|
// sequelize 配置相关
|
|
38
|
-
export const sequelizeOptionsPoolMin: number
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
: 5;
|
|
44
|
-
export const sequelizeOptionsPoolIdle: number = process.env.SEQUELIZE_OPTIONS_POOL_IDLE
|
|
45
|
-
? +process.env.SEQUELIZE_OPTIONS_POOL_IDLE
|
|
46
|
-
: 10 * 1000;
|
|
47
|
-
|
|
48
|
-
export const updateDataConcurrency: number = process.env.UPDATE_DATA_CONCURRENCY
|
|
49
|
-
? +process.env.UPDATE_DATA_CONCURRENCY
|
|
50
|
-
: 5; // 默认并发数为 5
|
|
90
|
+
export const sequelizeOptionsPoolMin = (): number => numConfig('SEQUELIZE_OPTIONS_POOL_MIN', 0);
|
|
91
|
+
export const sequelizeOptionsPoolMax = (): number => numConfig('SEQUELIZE_OPTIONS_POOL_MAX', 5);
|
|
92
|
+
export const sequelizeOptionsPoolIdle = (): number => numConfig('SEQUELIZE_OPTIONS_POOL_IDLE', 10 * 1000);
|
|
93
|
+
|
|
94
|
+
export const updateDataConcurrency = (): number => numConfig('UPDATE_DATA_CONCURRENCY', 5);
|
|
51
95
|
|
|
52
96
|
// When set to 'true' or '1', the system stops accepting new orders.
|
|
53
97
|
// Existing checkout sessions can still be viewed but new submissions will be rejected.
|
|
54
|
-
export const stopAcceptingOrders: boolean
|
|
55
|
-
|
|
98
|
+
export const stopAcceptingOrders = (): boolean =>
|
|
99
|
+
readConfig('PAYMENT_KIT_STOP_ACCEPTING_ORDERS') === 'true' || readConfig('PAYMENT_KIT_STOP_ACCEPTING_ORDERS') === '1';
|
|
56
100
|
|
|
57
|
-
export const exchangeRateCacheTTLSeconds: number
|
|
58
|
-
? +process.env.EXCHANGE_RATE_CACHE_TTL_SECONDS
|
|
59
|
-
: 10 * 60;
|
|
101
|
+
export const exchangeRateCacheTTLSeconds = (): number => numConfig('EXCHANGE_RATE_CACHE_TTL_SECONDS', 10 * 60);
|
|
60
102
|
|
|
61
103
|
// System-level maximum pending amount limit (in token format, e.g., "10")
|
|
62
104
|
// Default is 0 (disabled). Set PAYMENT_KIT_MAX_PENDING_AMOUNT to enable this limit.
|
|
63
|
-
export const systemMaxPendingAmount: number
|
|
64
|
-
|
|
65
|
-
|
|
105
|
+
export const systemMaxPendingAmount = (): number => numConfig('PAYMENT_KIT_MAX_PENDING_AMOUNT', 5);
|
|
106
|
+
|
|
107
|
+
// Whether a locked price may still be edited. Lazy (reads injected config via
|
|
108
|
+
// the boundary at call time) like every other Phase 8 accessor.
|
|
109
|
+
export const allowChangeLockedPrice = (): boolean => readConfig('PAYMENT_CHANGE_LOCKED_PRICE') === '1';
|
|
110
|
+
|
|
111
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
112
|
+
// Phase 8 (W2′): converged accessors. Every read below used to live inline in
|
|
113
|
+
// api/src as a direct process.env / __CF_ENV__ access (the 57-entry whitelist).
|
|
114
|
+
// They are LAZY (functions, not import-time consts) so the injected config —
|
|
115
|
+
// wired by the factory AFTER module import — is honored at call time, and so
|
|
116
|
+
// tests that flip process.env at runtime keep working.
|
|
117
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
// -- runtime mode / environment --
|
|
120
|
+
export const blockletMode = (): string | undefined => readConfig('BLOCKLET_MODE');
|
|
121
|
+
export const isProduction = (): boolean => blockletMode() === 'production';
|
|
122
|
+
export const nodeEnv = (): string | undefined => readConfig('NODE_ENV');
|
|
123
|
+
export const isTestEnv = (): boolean => nodeEnv() === 'test';
|
|
124
|
+
export const isDevelopmentEnv = (): boolean => nodeEnv() === 'development';
|
|
125
|
+
export const enableDevFakeAuth = (): boolean => readConfig('ENABLE_DEV_FAKE_AUTH') === '1';
|
|
126
|
+
|
|
127
|
+
// -- tenant mode-source (getTenantMode / getDefaultInstanceDid read these) --
|
|
128
|
+
export const tenantModeRaw = (): string | undefined => readConfig('PAYMENT_TENANT_MODE');
|
|
129
|
+
export const blockletAppPid = (): string | undefined => readConfig('BLOCKLET_APP_PID');
|
|
130
|
+
|
|
131
|
+
// -- app identity / urls --
|
|
132
|
+
export const blockletAppId = (): string | undefined => readConfig('BLOCKLET_APP_ID');
|
|
133
|
+
export const blockletAppName = (): string | undefined => readConfig('BLOCKLET_APP_NAME');
|
|
134
|
+
export const blockletAppUrl = (): string | undefined => readConfig('BLOCKLET_APP_URL');
|
|
135
|
+
export const blockletAppHost = (): string | undefined => readConfig('BLOCKLET_APP_HOST');
|
|
136
|
+
export const blockletAppDir = (): string | undefined => readConfig('BLOCKLET_APP_DIR');
|
|
137
|
+
export const blockletPort = (): string | undefined => readConfig('BLOCKLET_PORT');
|
|
138
|
+
export const blockletMountPoints = (): string | undefined => readConfig('BLOCKLET_MOUNT_POINTS');
|
|
139
|
+
|
|
140
|
+
// -- integrations --
|
|
141
|
+
export const appStoreWriteEnabled = (): boolean => readConfig('APP_STORE_WRITE_ENABLED') === 'true';
|
|
142
|
+
export const appStoreSkipSignatureVerify = (): boolean => readConfig('APP_STORE_SKIP_SIGNATURE_VERIFY') === 'true';
|
|
143
|
+
export const googlePubsubSkipSignatureVerify = (): boolean =>
|
|
144
|
+
readConfig('GOOGLE_PUBSUB_SKIP_SIGNATURE_VERIFY') === 'true';
|
|
145
|
+
export const googlePubsubPushServiceAccount = (): string | undefined =>
|
|
146
|
+
readConfig('GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT');
|
|
147
|
+
export const googlePubsubAllowUnverifiedSender = (): boolean =>
|
|
148
|
+
readConfig('GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER') === 'true';
|
|
149
|
+
export const googlePlayWebhookUrl = (): string | undefined => readConfig('GOOGLE_PLAY_WEBHOOK_URL');
|
|
150
|
+
export const stripeWebhookSecret = (): string | undefined => readConfig('STRIPE_WEBHOOK_SECRET');
|
|
151
|
+
export const iapReconcileBatchSize = (): number => Number(readConfig('IAP_RECONCILE_BATCH_SIZE') ?? '100');
|
|
152
|
+
|
|
153
|
+
// -- payment params --
|
|
154
|
+
export const paymentBillingThreshold = (): number => +(readConfig('PAYMENT_BILLING_THRESHOLD') as string);
|
|
155
|
+
export const paymentMinStakeAmount = (): number => +(readConfig('PAYMENT_MIN_STAKE_AMOUNT') as string);
|
|
156
|
+
export const paymentDaysUntilDue = (): string | undefined => readConfig('PAYMENT_DAYS_UNTIL_DUE');
|
|
157
|
+
export const paymentDaysUntilCancel = (): string | undefined => readConfig('PAYMENT_DAYS_UNTIL_CANCEL');
|
|
158
|
+
export const paymentReloadSubscriptionJobs = (): boolean => readConfig('PAYMENT_RELOAD_SUBSCRIPTION_JOBS') === '1';
|
|
159
|
+
export const paymentRateVolatilityThreshold = (): string | undefined => readConfig('PAYMENT_RATE_VOLATILITY_THRESHOLD');
|
|
160
|
+
export const paymentLivemode = (): boolean => readConfig('PAYMENT_LIVEMODE') !== 'false';
|
|
161
|
+
|
|
162
|
+
// -- credit queue --
|
|
163
|
+
export const creditLowBalanceThresholdPercentage = (): number =>
|
|
164
|
+
parseInt(readConfig('CREDIT_LOW_BALANCE_THRESHOLD_PERCENTAGE') || '10', 10);
|
|
165
|
+
export const creditBatchSize = (): number => Math.max(1, parseInt(readConfig('CREDIT_BATCH_SIZE') || '50', 10));
|
|
166
|
+
export const creditBatchWindowMs = (): number =>
|
|
167
|
+
Math.max(10, parseInt(readConfig('CREDIT_BATCH_WINDOW_MS') || '3000', 10));
|
|
168
|
+
export const creditQueueConcurrency = (): number =>
|
|
169
|
+
Math.max(1, Math.min(20, parseInt(readConfig('CREDIT_QUEUE_CONCURRENCY') || '5', 10) || 5));
|
|
170
|
+
|
|
171
|
+
// -- exchange rate cache TTL source (the value itself is exchangeRateCacheTTLSeconds above) --
|
|
172
|
+
export const exchangeRateCacheTTLFromEnv = (): boolean => hasConfig('EXCHANGE_RATE_CACHE_TTL_SECONDS');
|
|
173
|
+
|
|
174
|
+
// -- store / sequelize logging --
|
|
175
|
+
export const sqlLog = (): boolean => readConfig('SQL_LOG') === '1';
|
|
176
|
+
export const sqlBenchmark = (): boolean => readConfig('SQL_BENCHMARK') === '1';
|
|
177
|
+
|
|
178
|
+
// -- CF worker runtime detection + env mirror (set by cloudflare/worker.ts on
|
|
179
|
+
// globalThis; the boundary so core never reads the global directly) --
|
|
180
|
+
export const cfEnv = (): any => (globalThis as any).__CF_ENV__;
|
|
181
|
+
export const isCfWorker = (): boolean => !!cfEnv();
|
|
66
182
|
|
|
67
183
|
export default {
|
|
68
184
|
...env,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-await-in-loop */
|
|
2
2
|
import BigNumber from 'bignumber.js';
|
|
3
|
+
import { exchangeRateCacheTTLFromEnv, exchangeRateCacheTTLSeconds } from '../env';
|
|
3
4
|
import { ExchangeRateProvider } from '../../store/models/exchange-rate-provider';
|
|
4
5
|
import logger from '../logger';
|
|
5
6
|
import { events } from '../event';
|
|
@@ -8,15 +9,15 @@ import { SymbolNotSupportedError } from './types';
|
|
|
8
9
|
import { TokenDataProvider } from './token-data-provider';
|
|
9
10
|
import { CoinGeckoProvider } from './coingecko-provider';
|
|
10
11
|
import { CoinMarketCapProvider } from './coinmarketcap-provider';
|
|
11
|
-
import { exchangeRateCacheTTLSeconds } from '../env';
|
|
12
12
|
|
|
13
13
|
interface CacheEntry {
|
|
14
14
|
data: ExchangeRateResult;
|
|
15
15
|
timestamp: number;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Cache TTL from
|
|
19
|
-
|
|
18
|
+
// Cache TTL from config, default 10 minutes. Lazy so injected config (set after
|
|
19
|
+
// module import) is honored — not frozen in a module-level const.
|
|
20
|
+
const cacheTtlMs = (): number => (exchangeRateCacheTTLSeconds() || 10 * 60) * 1000;
|
|
20
21
|
const MAX_RATE_AGE_MS = 5 * 60 * 1000; // 5 minutes
|
|
21
22
|
const MAX_DEVIATION_PERCENT = 5; // 5%
|
|
22
23
|
const HISTORY_WINDOW_SIZE = 10;
|
|
@@ -185,7 +186,7 @@ export class ExchangeRateService {
|
|
|
185
186
|
|
|
186
187
|
// Check cache first
|
|
187
188
|
const cached = this.cache.get(cacheKey);
|
|
188
|
-
if (cached && Date.now() - cached.timestamp <
|
|
189
|
+
if (cached && Date.now() - cached.timestamp < cacheTtlMs()) {
|
|
189
190
|
logger.debug('Exchange rate cache hit', { symbol, age: Date.now() - cached.timestamp });
|
|
190
191
|
return cached.data;
|
|
191
192
|
}
|
|
@@ -575,8 +576,8 @@ export function getExchangeRateService(): ExchangeRateService {
|
|
|
575
576
|
if (!serviceInstance) {
|
|
576
577
|
serviceInstance = new ExchangeRateService();
|
|
577
578
|
logger.info('Exchange rate service initialized', {
|
|
578
|
-
cache_ttl_seconds:
|
|
579
|
-
cache_ttl_source:
|
|
579
|
+
cache_ttl_seconds: cacheTtlMs() / 1000,
|
|
580
|
+
cache_ttl_source: exchangeRateCacheTTLFromEnv() ? 'env' : 'default',
|
|
580
581
|
});
|
|
581
582
|
}
|
|
582
583
|
return serviceInstance;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// D5 — the Web-Fetch entry for hosts that own their own app shell (arc-node's
|
|
2
|
+
// registerPrefixHandler): `svc.http.fetch(req, {basePath})` with no express bridge.
|
|
3
|
+
//
|
|
4
|
+
// Phase 4 (express→hono): the loopback http.Server + express app are gone, so this
|
|
5
|
+
// is just a base-strip wrapper over `honoApp.fetch`. The outward signature +
|
|
6
|
+
// strip semantics are unchanged (arc consumes this contract); raw-body fidelity
|
|
7
|
+
// (Stripe webhook signature) holds because the stripped path reuses the exact
|
|
8
|
+
// request bytes/headers, and status/Set-Cookie/redirect come from the hono Response.
|
|
9
|
+
|
|
10
|
+
import type { Hono } from 'hono';
|
|
11
|
+
|
|
12
|
+
/** Options for svc.http.fetch — basePath is stripped to reach the internal /api/*. */
|
|
13
|
+
export interface PaymentFetchOptions {
|
|
14
|
+
/** the host's mount prefix (e.g. "/.well-known/payment"); stripped, no alias special-case */
|
|
15
|
+
basePath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FetchHandler {
|
|
19
|
+
(request: Request, opts?: PaymentFetchOptions): Promise<Response>;
|
|
20
|
+
/** teardown hook (host lifecycle). No-op now that there is no loopback server. */
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createFetchHandler(app: Hono): FetchHandler {
|
|
25
|
+
const handler = (async (request: Request, opts?: PaymentFetchOptions): Promise<Response> => {
|
|
26
|
+
const url = new URL(request.url);
|
|
27
|
+
|
|
28
|
+
// ① base-strip: remove basePath → internal /api/* path (no alias special-case).
|
|
29
|
+
// Precise segment-boundary check (=== basePath || startsWith(basePath + '/'))
|
|
30
|
+
// so "/foo" never matches "/foobar" — byte-identical to the old loopback strip.
|
|
31
|
+
const basePath = opts?.basePath ?? '';
|
|
32
|
+
if (basePath && (url.pathname === basePath || url.pathname.startsWith(`${basePath}/`))) {
|
|
33
|
+
url.pathname = url.pathname.slice(basePath.length) || '/';
|
|
34
|
+
// ② rebuild the request at the stripped URL. The body is read ONCE into a
|
|
35
|
+
// fixed buffer (raw bytes preserved for Stripe webhook signature) and the
|
|
36
|
+
// original headers/method are carried verbatim (Host preserved).
|
|
37
|
+
const method = request.method.toUpperCase();
|
|
38
|
+
const hasBody = method !== 'GET' && method !== 'HEAD';
|
|
39
|
+
const body = hasBody ? await request.arrayBuffer() : undefined;
|
|
40
|
+
return app.fetch(new Request(url.toString(), { method, headers: request.headers, body }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// no strip needed — hand the request straight to hono (it owns body parsing).
|
|
44
|
+
return app.fetch(request);
|
|
45
|
+
}) as FetchHandler;
|
|
46
|
+
|
|
47
|
+
handler.close = (): Promise<void> => Promise.resolve();
|
|
48
|
+
|
|
49
|
+
return handler;
|
|
50
|
+
}
|
package/api/src/libs/invoice.ts
CHANGED