payment-kit 1.29.1 → 1.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +36 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +27 -24
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +99 -38
- package/api/src/libs/context.ts +78 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +81 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +50 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +259 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +73 -0
- package/api/src/middlewares/hono/csrf.ts +72 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +214 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +17 -12
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +12 -4
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +41 -28
- package/api/src/queues/payout.ts +9 -5
- package/api/src/queues/refund.ts +18 -12
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +667 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +66 -22
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +236 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/README.md +26 -6
- package/cloudflare/build.ts +28 -13
- package/cloudflare/did-connect-auth.ts +0 -217
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +10 -56
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
- package/cloudflare/worker.ts +204 -433
- package/cloudflare/wrangler.local-e2e.jsonc +26 -0
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/env.d.ts +13 -1
- package/tsconfig.json +1 -1
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Phase 3 (express→hono) — hono fork of @blocklet/sdk/lib/middlewares/session.js
|
|
2
|
+
// (sessionMiddleware). NOT in the Phase 1 fork set, but used by many resource
|
|
3
|
+
// routes (customers, checkout-sessions, payment-currencies, auto-recharge-configs,
|
|
4
|
+
// …) via `sessionMiddleware({ accessKey: true })`. Unlike authenticate(), this is
|
|
5
|
+
// NOT a gate: it POPULATES c.set('user', …) when a login token OR access key is
|
|
6
|
+
// present and otherwise just calls next() (the downstream handler decides). The
|
|
7
|
+
// token verification (verifyLoginToken / verifyAccessKey / verifyComponentCall /
|
|
8
|
+
// verifySignedToken) + the optional blacklist check are reused verbatim; only the
|
|
9
|
+
// req plumbing becomes a thin hono shim for getTokenFromReq.
|
|
10
|
+
import type { MiddlewareHandler } from 'hono';
|
|
11
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
12
|
+
import { getTokenFromReq } from '@abtnode/util/lib/get-token-from-req';
|
|
13
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
14
|
+
import {
|
|
15
|
+
verifyLoginToken,
|
|
16
|
+
verifyAccessKey,
|
|
17
|
+
verifyComponentCall,
|
|
18
|
+
verifySignedToken,
|
|
19
|
+
} from '@blocklet/sdk/lib/util/verify-session';
|
|
20
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
21
|
+
import { isLoginToken, isAccessKey } from '@blocklet/sdk/lib/util/login';
|
|
22
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
23
|
+
import config from '@blocklet/sdk/lib/config';
|
|
24
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
25
|
+
import serviceApi from '@blocklet/sdk/lib/util/service-api';
|
|
26
|
+
import { isTestEnv } from '../../libs/env';
|
|
27
|
+
|
|
28
|
+
interface SessionOptions {
|
|
29
|
+
loginToken?: boolean;
|
|
30
|
+
componentCall?: boolean;
|
|
31
|
+
signedToken?: string;
|
|
32
|
+
strictMode?: boolean;
|
|
33
|
+
accessKey?: boolean;
|
|
34
|
+
signedTokenKey?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function sessionMiddleware(options: SessionOptions = {}): MiddlewareHandler {
|
|
38
|
+
const {
|
|
39
|
+
loginToken = true,
|
|
40
|
+
componentCall = false,
|
|
41
|
+
signedToken = '',
|
|
42
|
+
strictMode = false,
|
|
43
|
+
accessKey = false,
|
|
44
|
+
signedTokenKey = '__jwt',
|
|
45
|
+
} = options;
|
|
46
|
+
|
|
47
|
+
return async (c, next) => {
|
|
48
|
+
let result: any = null;
|
|
49
|
+
// a thin express-req shim covering exactly what getTokenFromReq /
|
|
50
|
+
// verifyComponentCall read (query / cookie+authorization headers / method / url / body).
|
|
51
|
+
const url = new URL(c.req.url);
|
|
52
|
+
const shimReq: any = {
|
|
53
|
+
query: c.req.query(),
|
|
54
|
+
headers: { cookie: c.req.header('cookie'), authorization: c.req.header('authorization') },
|
|
55
|
+
get: (h: string) => c.req.header(h),
|
|
56
|
+
method: c.req.method,
|
|
57
|
+
originalUrl: url.pathname + url.search,
|
|
58
|
+
body: c.get('sanitizedBody') ?? {},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (loginToken || accessKey) {
|
|
63
|
+
const { _duplicate: hasDuplicate, token: loginTokenValue } = await getTokenFromReq(shimReq, {
|
|
64
|
+
cookie: { key: 'login_token' },
|
|
65
|
+
});
|
|
66
|
+
if (hasDuplicate) {
|
|
67
|
+
return c.text('Access token found in multiple locations', 400);
|
|
68
|
+
}
|
|
69
|
+
if (loginTokenValue && typeof loginTokenValue === 'string') {
|
|
70
|
+
if (!isTestEnv()) {
|
|
71
|
+
const blockletSettings = config.getBlockletSettings();
|
|
72
|
+
if (blockletSettings.enableBlacklist) {
|
|
73
|
+
const { data: checkResult } = await serviceApi.post('/api/user/checkToken', { token: loginTokenValue });
|
|
74
|
+
if (!checkResult.valid) {
|
|
75
|
+
if (strictMode) {
|
|
76
|
+
return c.text('Access token is blocked', 401);
|
|
77
|
+
}
|
|
78
|
+
// not strict → treat as unauthenticated and proceed. NOT awaited
|
|
79
|
+
// on purpose: awaiting would pull downstream route errors into this
|
|
80
|
+
// try/catch and wrongly report them as 401 (parity with the express
|
|
81
|
+
// `next(); return;`, which does not await downstream).
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/return-await
|
|
83
|
+
return next();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (isLoginToken(loginTokenValue)) {
|
|
88
|
+
result = await verifyLoginToken({ token: loginTokenValue, strictMode });
|
|
89
|
+
} else if (isAccessKey(loginTokenValue) && accessKey) {
|
|
90
|
+
result = await verifyAccessKey({ token: loginTokenValue, strictMode });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!result && componentCall) {
|
|
96
|
+
result = await verifyComponentCall({ req: shimReq, strictMode });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!result && signedToken) {
|
|
100
|
+
const token = c.req.query(signedTokenKey) || '';
|
|
101
|
+
result = await verifySignedToken({ token, strictMode });
|
|
102
|
+
}
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
return c.json({ error: err.message }, 401);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (result) {
|
|
108
|
+
c.set('user', result);
|
|
109
|
+
}
|
|
110
|
+
return next();
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default sessionMiddleware;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Phase 1 (express→hono) — hono fork of @blocklet/xss (cjs/index.js:26).
|
|
2
|
+
//
|
|
3
|
+
// The express version sanitizes body/params/headers/query IN PLACE. hono's
|
|
4
|
+
// Request is immutable and the query/params/headers are never reflected as HTML
|
|
5
|
+
// (pure JSON API → Joi + parameterized Sequelize), so this fork DELIBERATELY
|
|
6
|
+
// NARROWS to body-only sanitization (design §7, decided 2026-06-12). The
|
|
7
|
+
// query/params/headers narrowing is locked by an explicit spec assertion.
|
|
8
|
+
//
|
|
9
|
+
// This middleware is the SINGLE body read-point: it consumes the request body
|
|
10
|
+
// once, sanitizes it, and stores it on c.set('sanitizedBody'). Downstream routes
|
|
11
|
+
// MUST read c.get('sanitizedBody') and NEVER call c.req.json() again — hono's
|
|
12
|
+
// bodyCache holds the UN-sanitized original, so a re-read is a security hole
|
|
13
|
+
// (design §7). The Stripe webhook (raw-body) is mounted so it never passes
|
|
14
|
+
// through this middleware (Phase 3e).
|
|
15
|
+
import type { MiddlewareHandler } from 'hono';
|
|
16
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
17
|
+
import { initSanitize } from '@blocklet/xss';
|
|
18
|
+
|
|
19
|
+
// allowedKeys: [] matches the express call site `xss({ allowedKeys: [] })`.
|
|
20
|
+
const sanitize = initSanitize({ allowedKeys: [] });
|
|
21
|
+
|
|
22
|
+
// Raw-body routes whose bytes must reach the handler unconsumed — the Stripe
|
|
23
|
+
// webhook verifies its HMAC signature over the EXACT received bytes. Parity with
|
|
24
|
+
// the express app shell, which routes this path around the json/body middleware
|
|
25
|
+
// (service.ts buildNodeHandler). The route reads c.req.arrayBuffer() itself.
|
|
26
|
+
const RAW_BODY_PREFIXES = ['/api/integrations/stripe/webhook'];
|
|
27
|
+
|
|
28
|
+
export function xss(): MiddlewareHandler {
|
|
29
|
+
return async (c, next) => {
|
|
30
|
+
if (RAW_BODY_PREFIXES.some((p) => c.req.path.startsWith(p))) {
|
|
31
|
+
c.set('sanitizedBody', undefined);
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
// Parse the body by CONTENT-TYPE, not method — express's body-parser parses a
|
|
35
|
+
// json/form body on ANY method (including a GET with a body, which a few
|
|
36
|
+
// routes use, e.g. customers GET /:id reading body.create). A request with no
|
|
37
|
+
// parseable content-type yields no sanitizedBody (routes read `?? {}`, matching
|
|
38
|
+
// express.json()'s empty {} ).
|
|
39
|
+
const contentType = c.req.header('content-type') || '';
|
|
40
|
+
let body: unknown;
|
|
41
|
+
if (contentType.includes('application/json')) {
|
|
42
|
+
// single read; empty body → {} to match express.json() (which yields {}),
|
|
43
|
+
// malformed JSON throws (→ app.onError, parity with the express 400 path).
|
|
44
|
+
const raw = await c.req.text();
|
|
45
|
+
body = raw === '' ? {} : JSON.parse(raw);
|
|
46
|
+
} else if (
|
|
47
|
+
contentType.includes('application/x-www-form-urlencoded') ||
|
|
48
|
+
contentType.includes('multipart/form-data')
|
|
49
|
+
) {
|
|
50
|
+
body = await c.req.parseBody();
|
|
51
|
+
} else {
|
|
52
|
+
// no parseable body content-type (or no body) — nothing to sanitize
|
|
53
|
+
body = undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
c.set('sanitizedBody', body === undefined ? undefined : sanitize(body));
|
|
57
|
+
return next();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default xss;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { BN } from '@ocap/util';
|
|
2
|
-
|
|
3
2
|
import { Op } from 'sequelize';
|
|
4
|
-
import
|
|
3
|
+
import { systemFindByPk, systemFindOne } from '../store/scoped';
|
|
4
|
+
|
|
5
|
+
import createQueue, { assertJobObjectTenant } from '../libs/queue';
|
|
5
6
|
import {
|
|
6
7
|
AutoRechargeConfig,
|
|
7
8
|
ChainType,
|
|
@@ -48,13 +49,14 @@ export async function processAutoRecharge(job: AutoRechargeJobData) {
|
|
|
48
49
|
logger.info('Processing auto recharge job', { job });
|
|
49
50
|
const { customer_id: customerId, currency_id: currencyId } = job;
|
|
50
51
|
|
|
51
|
-
const customer = await Customer
|
|
52
|
+
const customer = await systemFindByPk(Customer, customerId);
|
|
52
53
|
if (!customer) {
|
|
53
54
|
logger.error('Customer not found', { customerId });
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
57
|
+
assertJobObjectTenant(customer);
|
|
56
58
|
|
|
57
|
-
const currency = await PaymentCurrency
|
|
59
|
+
const currency = await systemFindByPk(PaymentCurrency, currencyId);
|
|
58
60
|
if (!currency) {
|
|
59
61
|
logger.error('Currency not found', { currencyId });
|
|
60
62
|
return;
|
|
@@ -63,7 +65,7 @@ export async function processAutoRecharge(job: AutoRechargeJobData) {
|
|
|
63
65
|
// Check if the associated meter is inactive
|
|
64
66
|
if (currency.type === 'credit') {
|
|
65
67
|
// Find meter by currency_id (meter.currency_id -> PaymentCurrency) or by metadata.meter_id
|
|
66
|
-
const meter = await Meter
|
|
68
|
+
const meter = await systemFindOne(Meter, {
|
|
67
69
|
where: { currency_id: currencyId },
|
|
68
70
|
});
|
|
69
71
|
if (meter && meter.status === 'inactive') {
|
|
@@ -77,7 +79,7 @@ export async function processAutoRecharge(job: AutoRechargeJobData) {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
// 1. find auto recharge config
|
|
80
|
-
const config = (await AutoRechargeConfig
|
|
82
|
+
const config = (await systemFindOne(AutoRechargeConfig, {
|
|
81
83
|
where: {
|
|
82
84
|
customer_id: customerId,
|
|
83
85
|
currency_id: currencyId,
|
|
@@ -378,7 +380,7 @@ async function createInvoiceForAutoRecharge({
|
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
// Check for existing invoice
|
|
381
|
-
const existInvoice = await Invoice
|
|
383
|
+
const existInvoice = await systemFindOne(Invoice, {
|
|
382
384
|
where: {
|
|
383
385
|
customer_id: customer.id,
|
|
384
386
|
currency_id: rechargeCurrency.id,
|
|
@@ -633,21 +635,21 @@ export async function checkAndTriggerAutoRecharge(
|
|
|
633
635
|
}
|
|
634
636
|
|
|
635
637
|
// T2b: Reuse caller-provided currency to avoid redundant DB query
|
|
636
|
-
const currency = options?.currency ?? (await PaymentCurrency
|
|
638
|
+
const currency = options?.currency ?? (await systemFindByPk(PaymentCurrency, currencyId));
|
|
637
639
|
|
|
638
640
|
// Reuse caller-provided meter when available to avoid redundant DB query
|
|
639
641
|
let meterPromise: Promise<any>;
|
|
640
642
|
if (options?.meter != null) {
|
|
641
643
|
meterPromise = Promise.resolve(options.meter);
|
|
642
644
|
} else if (currency?.type === 'credit') {
|
|
643
|
-
meterPromise = Meter
|
|
645
|
+
meterPromise = systemFindOne(Meter, { where: { currency_id: currencyId } });
|
|
644
646
|
} else {
|
|
645
647
|
meterPromise = Promise.resolve(null);
|
|
646
648
|
}
|
|
647
649
|
|
|
648
650
|
const [meter, config] = await Promise.all([
|
|
649
651
|
meterPromise,
|
|
650
|
-
AutoRechargeConfig
|
|
652
|
+
systemFindOne(AutoRechargeConfig, {
|
|
651
653
|
where: {
|
|
652
654
|
customer_id: customer.id,
|
|
653
655
|
currency_id: currencyId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-await-in-loop */
|
|
2
2
|
import { Op } from 'sequelize';
|
|
3
|
+
import { systemFindAll, systemFindByPk, systemFindOne } from '../store/scoped';
|
|
3
4
|
|
|
4
5
|
import { mintNftForCheckoutSession } from '../integrations/arcblock/nft';
|
|
5
6
|
import { ensurePassportIssued } from '../integrations/blocklet/passport';
|
|
@@ -7,7 +8,7 @@ import { ensureInvoiceForCheckout } from '../routes/connect/shared';
|
|
|
7
8
|
import dayjs from '../libs/dayjs';
|
|
8
9
|
import { events } from '../libs/event';
|
|
9
10
|
import logger from '../libs/logger';
|
|
10
|
-
import createQueue from '../libs/queue';
|
|
11
|
+
import createQueue, { assertJobObjectTenant } from '../libs/queue';
|
|
11
12
|
import {
|
|
12
13
|
CheckoutSession,
|
|
13
14
|
Customer,
|
|
@@ -40,11 +41,12 @@ export const checkoutSessionQueue = createQueue<CheckoutSessionJob>({
|
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
export async function handleCheckoutSessionJob(job: CheckoutSessionJob): Promise<void> {
|
|
43
|
-
const checkoutSession = await CheckoutSession
|
|
44
|
+
const checkoutSession = await systemFindByPk(CheckoutSession, job.id);
|
|
44
45
|
if (!checkoutSession) {
|
|
45
46
|
logger.warn('CheckoutSession not found', { id: job.id });
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
49
|
+
assertJobObjectTenant(checkoutSession);
|
|
48
50
|
if (job.action === 'expire') {
|
|
49
51
|
if (checkoutSession.status !== 'open') {
|
|
50
52
|
logger.info('Skip expire CheckoutSession since status is not open', {
|
|
@@ -80,7 +82,7 @@ export async function handleCheckoutSessionJob(job: CheckoutSessionJob): Promise
|
|
|
80
82
|
export async function startCheckoutSessionQueue() {
|
|
81
83
|
// Auto populate subscription queue
|
|
82
84
|
const now = dayjs().unix();
|
|
83
|
-
const checkoutSessions = await CheckoutSession
|
|
85
|
+
const checkoutSessions = await systemFindAll(CheckoutSession, {
|
|
84
86
|
where: {
|
|
85
87
|
status: 'open',
|
|
86
88
|
expires_at: { [Op.lte]: now },
|
|
@@ -146,7 +148,7 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
|
|
|
146
148
|
});
|
|
147
149
|
} else {
|
|
148
150
|
// Do some reverse lookup if invoice is not related to checkout session
|
|
149
|
-
const invoice = await Invoice
|
|
151
|
+
const invoice = await systemFindOne(Invoice, { where: { checkout_session_id: checkoutSession.id } });
|
|
150
152
|
if (invoice) {
|
|
151
153
|
await destroyExistingInvoice(invoice);
|
|
152
154
|
logger.info('Invoice and InvoiceItem for checkout session deleted on expire', {
|
|
@@ -165,10 +167,10 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
|
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
if (checkoutSession.payment_intent_id && checkoutSession.payment_status !== 'paid') {
|
|
168
|
-
const paymentIntent = await PaymentIntent
|
|
170
|
+
const paymentIntent = await systemFindByPk(PaymentIntent, checkoutSession.payment_intent_id);
|
|
169
171
|
const stripePaymentId = paymentIntent?.payment_details?.stripe?.payment_intent_id;
|
|
170
172
|
if (paymentIntent && stripePaymentId) {
|
|
171
|
-
const method = await PaymentMethod
|
|
173
|
+
const method = await systemFindByPk(PaymentMethod, paymentIntent.payment_method_id);
|
|
172
174
|
if (method?.type === 'stripe') {
|
|
173
175
|
const client = method.getStripeClient();
|
|
174
176
|
try {
|
|
@@ -198,12 +200,12 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
|
|
|
198
200
|
|
|
199
201
|
const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
|
|
200
202
|
if (subscriptionIds.length > 0) {
|
|
201
|
-
const subscriptions = await Subscription
|
|
203
|
+
const subscriptions = await systemFindAll(Subscription, { where: { id: { [Op.in]: subscriptionIds } } });
|
|
202
204
|
await Promise.all(
|
|
203
205
|
subscriptions.map(async (subscription) => {
|
|
204
206
|
const stripeSubscriptionId = subscription?.payment_details?.stripe?.subscription_id;
|
|
205
207
|
if (subscription && stripeSubscriptionId) {
|
|
206
|
-
const method = await PaymentMethod
|
|
208
|
+
const method = await systemFindByPk(PaymentMethod, subscription.default_payment_method_id);
|
|
207
209
|
if (method?.type === 'stripe') {
|
|
208
210
|
const client = method.getStripeClient();
|
|
209
211
|
try {
|
|
@@ -245,7 +247,7 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
|
|
|
245
247
|
|
|
246
248
|
// update price lock status
|
|
247
249
|
for (const item of checkoutSession.line_items) {
|
|
248
|
-
const price = await Price
|
|
250
|
+
const price = await systemFindByPk(Price, item.price_id);
|
|
249
251
|
if (price?.locked) {
|
|
250
252
|
const used = await price.isUsed(false);
|
|
251
253
|
logger.info('Price used status recheck on expire', {
|
|
@@ -269,11 +271,12 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
|
|
|
269
271
|
events.on(
|
|
270
272
|
'checkout.session.pending_invoice',
|
|
271
273
|
async ({ checkoutSessionId, paymentIntentId }: { checkoutSessionId: string; paymentIntentId: string }) => {
|
|
272
|
-
const checkoutSession = await CheckoutSession
|
|
274
|
+
const checkoutSession = await systemFindByPk(CheckoutSession, checkoutSessionId);
|
|
273
275
|
if (!checkoutSession) {
|
|
274
276
|
logger.warn('CheckoutSession not found for pending invoice', { checkoutSessionId });
|
|
275
277
|
return;
|
|
276
278
|
}
|
|
279
|
+
assertJobObjectTenant(checkoutSession);
|
|
277
280
|
if (checkoutSession.invoice_id) {
|
|
278
281
|
logger.info('Invoice already exists for checkout session', {
|
|
279
282
|
checkoutSessionId,
|
|
@@ -293,15 +296,17 @@ events.on(
|
|
|
293
296
|
return;
|
|
294
297
|
}
|
|
295
298
|
|
|
296
|
-
const paymentIntent = await PaymentIntent
|
|
299
|
+
const paymentIntent = await systemFindByPk(PaymentIntent, paymentIntentId);
|
|
297
300
|
if (!paymentIntent) {
|
|
298
301
|
return;
|
|
299
302
|
}
|
|
303
|
+
assertJobObjectTenant(paymentIntent);
|
|
300
304
|
|
|
301
|
-
const customer = await Customer
|
|
305
|
+
const customer = await systemFindByPk(Customer, checkoutSession.customer_id);
|
|
302
306
|
if (!customer) {
|
|
303
307
|
return;
|
|
304
308
|
}
|
|
309
|
+
assertJobObjectTenant(customer);
|
|
305
310
|
|
|
306
311
|
await ensureInvoiceForCheckout({
|
|
307
312
|
checkoutSession,
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
2
2
|
import { Op } from 'sequelize';
|
|
3
3
|
import pAll from 'p-all';
|
|
4
|
+
import {
|
|
5
|
+
isCfWorker,
|
|
6
|
+
creditLowBalanceThresholdPercentage,
|
|
7
|
+
creditBatchSize,
|
|
8
|
+
creditBatchWindowMs,
|
|
9
|
+
creditQueueConcurrency,
|
|
10
|
+
} from '../libs/env';
|
|
11
|
+
import { systemFindAll, systemFindByPk, systemFindOne } from '../store/scoped';
|
|
4
12
|
|
|
5
13
|
import { getLock } from '../libs/lock';
|
|
6
14
|
import logger from '../libs/logger';
|
|
7
|
-
import createQueue from '../libs/queue';
|
|
8
|
-
import { createEvent } from '../libs/audit';
|
|
15
|
+
import createQueue, { assertJobObjectTenant } from '../libs/queue';
|
|
16
|
+
import { createEvent, reportAuditFailure } from '../libs/audit';
|
|
9
17
|
import { MeterEvent, CreditGrant, CreditTransaction, Customer, Subscription, TMeterExpanded } from '../store/models';
|
|
10
18
|
import { getCachedMeterExpanded } from '../libs/reference-cache';
|
|
11
19
|
|
|
@@ -20,7 +28,7 @@ import { addTokenTransferJob } from './token-transfer';
|
|
|
20
28
|
// In Blocklet Server (no Queue ops limit), use the full MAX_RETRY_COUNT.
|
|
21
29
|
// After max retries, mark as requires_action — retryFailedEventsForCustomer()
|
|
22
30
|
// picks them up when credit is granted.
|
|
23
|
-
const CREDIT_MAX_RETRY = (
|
|
31
|
+
const CREDIT_MAX_RETRY = isCfWorker() ? 5 : MAX_RETRY_COUNT;
|
|
24
32
|
|
|
25
33
|
type CreditConsumptionJob = {
|
|
26
34
|
meterEventId: string;
|
|
@@ -82,7 +90,7 @@ async function checkLowBalance(
|
|
|
82
90
|
if (totalCreditAmountBn.lte(new BN(0))) return;
|
|
83
91
|
const remainingAmountBn = new BN(remainingBalance);
|
|
84
92
|
// Get threshold percentage from env var, default to 10%
|
|
85
|
-
const thresholdPercentage =
|
|
93
|
+
const thresholdPercentage = creditLowBalanceThresholdPercentage();
|
|
86
94
|
const threshold = totalCreditAmountBn.mul(new BN(thresholdPercentage)).div(new BN(100));
|
|
87
95
|
if (remainingAmountBn.gt(new BN(0)) && remainingAmountBn.lte(threshold)) {
|
|
88
96
|
const percentage = remainingAmountBn.mul(new BN(100)).div(totalCreditAmountBn).toString();
|
|
@@ -94,7 +102,7 @@ async function checkLowBalance(
|
|
|
94
102
|
percentage,
|
|
95
103
|
subscription_id: context.subscription?.id,
|
|
96
104
|
},
|
|
97
|
-
}).catch(
|
|
105
|
+
}).catch(reportAuditFailure);
|
|
98
106
|
}
|
|
99
107
|
} catch (error: any) {
|
|
100
108
|
logger.error('Failed to check low balance', {
|
|
@@ -118,7 +126,7 @@ async function loadReferenceData(
|
|
|
118
126
|
|
|
119
127
|
const [meter, customer] = await Promise.all([
|
|
120
128
|
getCachedMeterExpanded(meterEvent.event_name) as Promise<TMeterExpanded | null>,
|
|
121
|
-
Customer
|
|
129
|
+
systemFindByPk(Customer, customerId),
|
|
122
130
|
]);
|
|
123
131
|
|
|
124
132
|
if (!meter) {
|
|
@@ -163,7 +171,7 @@ async function consumeAvailableCredits(
|
|
|
163
171
|
const currencyId = context.meter.currency_id!;
|
|
164
172
|
const meterEventId = context.meterEvent.id;
|
|
165
173
|
|
|
166
|
-
const existingTransactions = await CreditTransaction
|
|
174
|
+
const existingTransactions = await systemFindAll(CreditTransaction, {
|
|
167
175
|
where: {
|
|
168
176
|
source: meterEventId,
|
|
169
177
|
},
|
|
@@ -300,7 +308,7 @@ async function handlePostConsumptionEvents(
|
|
|
300
308
|
currency_id: currencyId,
|
|
301
309
|
subscription_id: context.subscription?.id,
|
|
302
310
|
},
|
|
303
|
-
}).catch(
|
|
311
|
+
}).catch(reportAuditFailure);
|
|
304
312
|
}
|
|
305
313
|
|
|
306
314
|
// 如果有关联订阅且订阅活跃,将其标记为逾期
|
|
@@ -338,7 +346,7 @@ async function handlePostConsumptionEvents(
|
|
|
338
346
|
currency_id: currencyId,
|
|
339
347
|
subscription_id: context.subscription?.id,
|
|
340
348
|
},
|
|
341
|
-
}).catch(
|
|
349
|
+
}).catch(reportAuditFailure);
|
|
342
350
|
}
|
|
343
351
|
|
|
344
352
|
if (!insufficientTriggered) {
|
|
@@ -479,7 +487,7 @@ async function createCreditTransaction(
|
|
|
479
487
|
});
|
|
480
488
|
|
|
481
489
|
// 重新查询已存在的 transaction
|
|
482
|
-
const duplicateTransaction = await CreditTransaction
|
|
490
|
+
const duplicateTransaction = await systemFindOne(CreditTransaction, {
|
|
483
491
|
where: {
|
|
484
492
|
source: meterEventId,
|
|
485
493
|
credit_grant_id: creditGrantId,
|
|
@@ -508,11 +516,12 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
508
516
|
logger.info('Starting credit consumption job', { meterEventId });
|
|
509
517
|
|
|
510
518
|
// Pre-check before acquiring lock
|
|
511
|
-
const preCheckEvent = await MeterEvent
|
|
519
|
+
const preCheckEvent = await systemFindByPk(MeterEvent, meterEventId);
|
|
512
520
|
if (!preCheckEvent) {
|
|
513
521
|
logger.warn('Skipping credit consumption job: MeterEvent not found', { meterEventId });
|
|
514
522
|
return;
|
|
515
523
|
}
|
|
524
|
+
assertJobObjectTenant(preCheckEvent);
|
|
516
525
|
if (preCheckEvent.status === 'completed' || preCheckEvent.status === 'canceled') {
|
|
517
526
|
logger.info('Skipping credit consumption job: MeterEvent already processed', {
|
|
518
527
|
meterEventId,
|
|
@@ -554,8 +563,8 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
554
563
|
|
|
555
564
|
// Fresh loads inside lock for consistency
|
|
556
565
|
const [freshEvent, freshSubscription] = await Promise.all([
|
|
557
|
-
MeterEvent
|
|
558
|
-
context._subscriptionId ? Subscription
|
|
566
|
+
systemFindByPk(MeterEvent, meterEventId),
|
|
567
|
+
context._subscriptionId ? systemFindByPk(Subscription, context._subscriptionId) : Promise.resolve(null),
|
|
559
568
|
]);
|
|
560
569
|
if (!freshEvent) {
|
|
561
570
|
logger.warn('MeterEvent disappeared after lock acquired', { meterEventId });
|
|
@@ -691,7 +700,7 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
691
700
|
|
|
692
701
|
// Handle retry logic with more robust error handling
|
|
693
702
|
try {
|
|
694
|
-
const meterEvent = await MeterEvent
|
|
703
|
+
const meterEvent = await systemFindByPk(MeterEvent, meterEventId);
|
|
695
704
|
if (meterEvent && !['completed', 'canceled', 'requires_action'].includes(meterEvent.status)) {
|
|
696
705
|
const attemptCount = meterEvent.attempt_count + 1;
|
|
697
706
|
const nonRetryable = isNonRetryableCreditError(error);
|
|
@@ -762,9 +771,6 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
762
771
|
// ============================================================================
|
|
763
772
|
// Batch credit consumption
|
|
764
773
|
// ============================================================================
|
|
765
|
-
const CREDIT_BATCH_SIZE = Math.max(1, parseInt(process.env.CREDIT_BATCH_SIZE || '50', 10));
|
|
766
|
-
const CREDIT_BATCH_WINDOW_MS = Math.max(10, parseInt(process.env.CREDIT_BATCH_WINDOW_MS || '3000', 10));
|
|
767
|
-
|
|
768
774
|
type PendingBatch = { eventIds: string[]; timer: NodeJS.Timeout | null };
|
|
769
775
|
const pendingBatches = new Map<string, PendingBatch>();
|
|
770
776
|
// Track in-flight batch jobs per key to avoid head-of-line blocking.
|
|
@@ -785,13 +791,13 @@ function addToBatch(customerId: string, eventName: string, meterEventId: string,
|
|
|
785
791
|
|
|
786
792
|
batch.eventIds.push(meterEventId);
|
|
787
793
|
|
|
788
|
-
if (batch.eventIds.length >=
|
|
794
|
+
if (batch.eventIds.length >= creditBatchSize()) {
|
|
789
795
|
flushBatch(key);
|
|
790
796
|
return;
|
|
791
797
|
}
|
|
792
798
|
|
|
793
799
|
if (!batch.timer) {
|
|
794
|
-
batch.timer = setTimeout(() => flushBatch(key),
|
|
800
|
+
batch.timer = setTimeout(() => flushBatch(key), creditBatchWindowMs());
|
|
795
801
|
}
|
|
796
802
|
}
|
|
797
803
|
|
|
@@ -864,7 +870,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
864
870
|
// ==========================================
|
|
865
871
|
// Pre-check: filter already-processed events
|
|
866
872
|
// ==========================================
|
|
867
|
-
const preCheckEvents = await MeterEvent
|
|
873
|
+
const preCheckEvents = await systemFindAll(MeterEvent, {
|
|
868
874
|
where: { id: { [Op.in]: meterEventIds } },
|
|
869
875
|
});
|
|
870
876
|
|
|
@@ -894,7 +900,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
894
900
|
// ==========================================
|
|
895
901
|
const [meter, customer] = await Promise.all([
|
|
896
902
|
getCachedMeterExpanded(eventName) as Promise<TMeterExpanded | null>,
|
|
897
|
-
Customer
|
|
903
|
+
systemFindByPk(Customer, customerId),
|
|
898
904
|
]);
|
|
899
905
|
|
|
900
906
|
if (!meter || !meter.currency_id || !customer) {
|
|
@@ -925,7 +931,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
925
931
|
// Fresh read inside lock
|
|
926
932
|
// ==========================================
|
|
927
933
|
const processableIds = processableEvents.map((e) => e.id);
|
|
928
|
-
const freshEvents = await MeterEvent
|
|
934
|
+
const freshEvents = await systemFindAll(MeterEvent, {
|
|
929
935
|
where: { id: { [Op.in]: processableIds } },
|
|
930
936
|
});
|
|
931
937
|
|
|
@@ -940,7 +946,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
940
946
|
let priceIds: string[] | undefined;
|
|
941
947
|
|
|
942
948
|
if (subscriptionId) {
|
|
943
|
-
subscription = await Subscription
|
|
949
|
+
subscription = await systemFindByPk(Subscription, subscriptionId);
|
|
944
950
|
if (!subscription) {
|
|
945
951
|
logger.warn('Batch: Subscription not found inside lock, skipping', {
|
|
946
952
|
customerId,
|
|
@@ -965,7 +971,7 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
965
971
|
// Batch idempotency check
|
|
966
972
|
// ==========================================
|
|
967
973
|
const allEventIds = freshProcessable.map((e) => e.id);
|
|
968
|
-
const existingTransactions = await CreditTransaction
|
|
974
|
+
const existingTransactions = await systemFindAll(CreditTransaction, {
|
|
969
975
|
where: { source: { [Op.in]: allEventIds } },
|
|
970
976
|
});
|
|
971
977
|
const txBySource = new Map<string, CreditTransaction[]>();
|
|
@@ -1249,11 +1255,6 @@ async function handleBatchCreditConsumptionInner(meterEventIds: string[], batchS
|
|
|
1249
1255
|
// Queue setup
|
|
1250
1256
|
// ============================================================================
|
|
1251
1257
|
|
|
1252
|
-
const creditQueueConcurrency = Math.max(
|
|
1253
|
-
1,
|
|
1254
|
-
Math.min(20, parseInt(process.env.CREDIT_QUEUE_CONCURRENCY || '5', 10) || 5)
|
|
1255
|
-
);
|
|
1256
|
-
|
|
1257
1258
|
export const creditQueue = createQueue<CreditConsumptionJob | BatchCreditConsumptionJob>({
|
|
1258
1259
|
name: 'credit-consumption',
|
|
1259
1260
|
onJob: (job) => {
|
|
@@ -1263,7 +1264,7 @@ export const creditQueue = createQueue<CreditConsumptionJob | BatchCreditConsump
|
|
|
1263
1264
|
return handleCreditConsumption(job as CreditConsumptionJob);
|
|
1264
1265
|
},
|
|
1265
1266
|
options: {
|
|
1266
|
-
concurrency: creditQueueConcurrency,
|
|
1267
|
+
concurrency: creditQueueConcurrency(),
|
|
1267
1268
|
maxRetries: 0,
|
|
1268
1269
|
enableScheduledJob: true,
|
|
1269
1270
|
},
|
|
@@ -1308,11 +1309,12 @@ const addCreditConsumptionJob = async (
|
|
|
1308
1309
|
}
|
|
1309
1310
|
|
|
1310
1311
|
if (!options.skipStatusCheck) {
|
|
1311
|
-
const meterEvent = await MeterEvent
|
|
1312
|
+
const meterEvent = await systemFindByPk(MeterEvent, meterEventId);
|
|
1312
1313
|
if (!meterEvent) {
|
|
1313
1314
|
logger.warn('Cannot add credit consumption job: MeterEvent not found', { meterEventId });
|
|
1314
1315
|
return;
|
|
1315
1316
|
}
|
|
1317
|
+
assertJobObjectTenant(meterEvent);
|
|
1316
1318
|
|
|
1317
1319
|
if (meterEvent.status === 'completed' || meterEvent.status === 'canceled') {
|
|
1318
1320
|
logger.debug('Skipping credit consumption job: MeterEvent already processed', {
|
|
@@ -1342,7 +1344,7 @@ creditQueue.on('retry', ({ id, job }) => {
|
|
|
1342
1344
|
});
|
|
1343
1345
|
|
|
1344
1346
|
export async function startCreditConsumeQueue(): Promise<void> {
|
|
1345
|
-
const lock = getLock('startCreditConsumeQueue');
|
|
1347
|
+
const lock = getLock('startCreditConsumeQueue', { scope: 'global' });
|
|
1346
1348
|
if (lock.locked) {
|
|
1347
1349
|
return;
|
|
1348
1350
|
}
|
|
@@ -1359,7 +1361,7 @@ export async function startCreditConsumeQueue(): Promise<void> {
|
|
|
1359
1361
|
|
|
1360
1362
|
do {
|
|
1361
1363
|
// eslint-disable-next-line no-await-in-loop
|
|
1362
|
-
batchEvents = await MeterEvent
|
|
1364
|
+
batchEvents = await systemFindAll(MeterEvent, {
|
|
1363
1365
|
where: {
|
|
1364
1366
|
status: ['pending', 'requires_capture', 'processing'],
|
|
1365
1367
|
},
|
|
@@ -1491,13 +1493,14 @@ events.on('customer.credit_grant.granted', async (creditGrant: CreditGrant) => {
|
|
|
1491
1493
|
});
|
|
1492
1494
|
|
|
1493
1495
|
async function retryFailedEventsForCustomer(creditGrant: CreditGrant): Promise<void> {
|
|
1494
|
-
const grant = await CreditGrant
|
|
1496
|
+
const grant = await systemFindByPk(CreditGrant, creditGrant.id);
|
|
1495
1497
|
if (!grant) {
|
|
1496
1498
|
logger.error('Credit grant not found', {
|
|
1497
1499
|
creditGrantId: creditGrant.id,
|
|
1498
1500
|
});
|
|
1499
1501
|
return;
|
|
1500
1502
|
}
|
|
1503
|
+
assertJobObjectTenant(grant);
|
|
1501
1504
|
|
|
1502
1505
|
const customerId = grant.customer_id;
|
|
1503
1506
|
const currencyId = grant.currency_id;
|
|
@@ -1583,8 +1586,9 @@ async function retryFailedEventsForCustomer(creditGrant: CreditGrant): Promise<v
|
|
|
1583
1586
|
|
|
1584
1587
|
let chunkIndex = 0;
|
|
1585
1588
|
for (const [, subEventIds] of bySubscription) {
|
|
1586
|
-
|
|
1587
|
-
|
|
1589
|
+
const batchSize = creditBatchSize();
|
|
1590
|
+
for (let i = 0; i < subEventIds.length; i += batchSize) {
|
|
1591
|
+
const chunk = subEventIds.slice(i, i + batchSize);
|
|
1588
1592
|
creditQueue.push({
|
|
1589
1593
|
id: `retry-batch-${customerId}-${Date.now()}-${chunkIndex++}`,
|
|
1590
1594
|
job: { meterEventIds: chunk } as any,
|