payment-kit 1.27.2 → 1.29.0
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/__blocklet__.js +37 -0
- package/api/ocap-1.30-subpath-shims.d.ts +35 -0
- package/api/src/crons/index.ts +32 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- package/api/src/crons/retry-pending-events.ts +58 -0
- package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
- package/api/src/integrations/app-store/client.ts +369 -0
- package/api/src/integrations/app-store/handlers/index.ts +46 -0
- package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
- package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
- package/api/src/integrations/app-store/notification-routing.ts +18 -0
- package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
- package/api/src/integrations/arcblock/nft.ts +6 -2
- package/api/src/integrations/arcblock/stake.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +4 -4
- package/api/src/integrations/blocklet/notification.ts +1 -1
- package/api/src/integrations/ethereum/tx.ts +29 -0
- package/api/src/integrations/google-play/client.ts +276 -0
- package/api/src/integrations/google-play/handlers/index.ts +69 -0
- package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
- package/api/src/integrations/google-play/handlers/voided.ts +106 -0
- package/api/src/integrations/google-play/setup.ts +43 -0
- package/api/src/integrations/google-play/verify.ts +251 -0
- package/api/src/integrations/iap-reconcile.ts +415 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
- package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
- package/api/src/integrations/stripe/resource.ts +8 -0
- package/api/src/libs/audit.ts +70 -24
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -0
- package/api/src/libs/entitlement.ts +399 -0
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/error.ts +15 -0
- package/api/src/libs/event.ts +42 -1
- package/api/src/libs/invoice.ts +69 -34
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/pagination.ts +14 -9
- package/api/src/libs/payment.ts +25 -10
- package/api/src/libs/security.ts +51 -0
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/subscription.ts +13 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +29 -15
- package/api/src/libs/wallet-migration.ts +72 -53
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/credit-consume.ts +94 -12
- package/api/src/queues/credit-grant.ts +4 -0
- package/api/src/queues/event.ts +39 -21
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +83 -15
- package/api/src/queues/refund.ts +84 -71
- package/api/src/queues/subscription.ts +1 -0
- package/api/src/queues/webhook.ts +12 -2
- package/api/src/routes/checkout-sessions.ts +82 -43
- package/api/src/routes/connect/change-payment.ts +2 -0
- package/api/src/routes/connect/change-plan.ts +2 -0
- package/api/src/routes/connect/pay.ts +12 -3
- package/api/src/routes/connect/setup.ts +3 -1
- package/api/src/routes/connect/shared.ts +52 -39
- package/api/src/routes/connect/subscribe.ts +4 -1
- package/api/src/routes/credit-grants.ts +25 -17
- package/api/src/routes/donations.ts +2 -2
- package/api/src/routes/entitlements.ts +105 -0
- package/api/src/routes/events.ts +2 -2
- package/api/src/routes/index.ts +12 -2
- package/api/src/routes/integrations/app-store.ts +267 -0
- package/api/src/routes/integrations/google-play.ts +324 -0
- package/api/src/routes/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +131 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
- package/api/src/store/models/customer.ts +37 -1
- package/api/src/store/models/entitlement-grant.ts +118 -0
- package/api/src/store/models/entitlement-product.ts +48 -0
- package/api/src/store/models/entitlement.ts +86 -0
- package/api/src/store/models/index.ts +9 -0
- package/api/src/store/models/invoice.ts +20 -0
- package/api/src/store/models/payment-method.ts +66 -1
- package/api/src/store/models/price.ts +23 -14
- package/api/src/store/models/refund.ts +10 -0
- package/api/src/store/models/subscription.ts +14 -0
- package/api/src/store/models/types.ts +32 -0
- package/api/tests/integrations/app-store/client.spec.ts +335 -0
- package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
- package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
- package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
- package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
- package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
- package/api/tests/integrations/google-play/verify.spec.ts +215 -0
- package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
- package/api/tests/libs/entitlement.spec.ts +347 -0
- package/api/tests/libs/wallet-migration.spec.ts +4 -4
- package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
- package/api/tests/queues/credit-consume.spec.ts +8 -4
- package/api/tests/routes/credit-grants.spec.ts +1 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
- package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
- package/cloudflare/README.md +499 -0
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
- package/cloudflare/build.ts +151 -0
- package/cloudflare/did-connect-auth.ts +527 -0
- package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
- package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
- package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
- package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
- package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
- package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
- package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
- package/cloudflare/frontend-shims/js-sdk.ts +43 -0
- package/cloudflare/frontend-shims/mime-types.ts +46 -0
- package/cloudflare/frontend-shims/session.ts +24 -0
- package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
- package/cloudflare/index.html +40 -0
- package/cloudflare/migrate-to-d1.js +252 -0
- package/cloudflare/migrations/0001_initial_schema.sql +82 -0
- package/cloudflare/migrations/0002_indexes.sql +75 -0
- package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
- package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
- package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
- package/cloudflare/run-build.js +391 -0
- package/cloudflare/scripts/test-decrypt.js +102 -0
- package/cloudflare/shims/arcblock-ws.ts +20 -0
- package/cloudflare/shims/axios-http-adapter.ts +4 -0
- package/cloudflare/shims/axios-lite.ts +117 -0
- package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
- package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
- package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
- package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
- package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
- package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
- package/cloudflare/shims/cookie-parser.ts +3 -0
- package/cloudflare/shims/cors.ts +21 -0
- package/cloudflare/shims/cron.ts +189 -0
- package/cloudflare/shims/crypto-js-warn.ts +7 -0
- package/cloudflare/shims/did-space-js.ts +17 -0
- package/cloudflare/shims/did-space.ts +11 -0
- package/cloudflare/shims/error.ts +18 -0
- package/cloudflare/shims/express-compat/index.ts +80 -0
- package/cloudflare/shims/express-compat/types.ts +41 -0
- package/cloudflare/shims/fastq.ts +105 -0
- package/cloudflare/shims/lock.ts +115 -0
- package/cloudflare/shims/mime-types.ts +56 -0
- package/cloudflare/shims/nedb-storage.ts +9 -0
- package/cloudflare/shims/node-child-process.ts +9 -0
- package/cloudflare/shims/node-fs.ts +20 -0
- package/cloudflare/shims/node-http.ts +13 -0
- package/cloudflare/shims/node-https.ts +4 -0
- package/cloudflare/shims/node-misc.ts +15 -0
- package/cloudflare/shims/node-net.ts +8 -0
- package/cloudflare/shims/node-os.ts +14 -0
- package/cloudflare/shims/node-tty.ts +8 -0
- package/cloudflare/shims/node-zlib.ts +17 -0
- package/cloudflare/shims/noop.ts +26 -0
- package/cloudflare/shims/payment-vendor.ts +14 -0
- package/cloudflare/shims/querystring.ts +12 -0
- package/cloudflare/shims/queue.ts +611 -0
- package/cloudflare/shims/rolldown-runtime.ts +43 -0
- package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
- package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
- package/cloudflare/shims/sequelize-d1/index.ts +34 -0
- package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
- package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
- package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
- package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
- package/cloudflare/shims/sequelize-d1/types.ts +35 -0
- package/cloudflare/shims/stripe-cf.ts +29 -0
- package/cloudflare/shims/ws-lite.ts +103 -0
- package/cloudflare/shims/xss.ts +3 -0
- package/cloudflare/tests/shims/cron.spec.ts +210 -0
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1608 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +75 -0
- package/cloudflare/wrangler.staging.json +67 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +30 -22
- package/scripts/seed-google-play.ts +79 -0
- package/src/app.tsx +62 -4
- package/src/components/customer/link.tsx +9 -13
- package/src/components/customer/notification-preference.tsx +3 -2
- package/src/components/filter-toolbar.tsx +4 -0
- package/src/components/invoice/list.tsx +9 -1
- package/src/components/invoice-pdf/utils.ts +2 -1
- package/src/components/layout/admin.tsx +39 -5
- package/src/components/layout/user-cf.tsx +77 -0
- package/src/components/payment-intent/actions.tsx +23 -3
- package/src/components/payment-method/app-store.tsx +103 -0
- package/src/components/payment-method/form.tsx +7 -1
- package/src/components/payment-method/google-play.tsx +85 -0
- package/src/components/safe-did-address.tsx +75 -0
- package/src/components/subscription/list.tsx +20 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/locales/en.tsx +63 -0
- package/src/locales/zh.tsx +63 -0
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
- package/src/pages/admin/customers/customers/detail.tsx +8 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -1
- package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/tsconfig.api.json +1 -6
- package/tsconfig.json +3 -4
- package/tsconfig.types.json +2 -1
- package/vite.config.ts +6 -1
package/api/src/libs/error.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export default class CustomError extends Error {
|
|
3
3
|
code: string;
|
|
4
4
|
|
|
5
|
+
details?: Record<string, unknown>;
|
|
6
|
+
|
|
5
7
|
constructor(code = 'GENERIC', ...params: any[]) {
|
|
6
8
|
super(...params);
|
|
7
9
|
|
|
@@ -11,6 +13,19 @@ export default class CustomError extends Error {
|
|
|
11
13
|
|
|
12
14
|
this.code = code;
|
|
13
15
|
}
|
|
16
|
+
|
|
17
|
+
withDetails(details: Record<string, unknown>) {
|
|
18
|
+
this.details = details;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toJSON() {
|
|
23
|
+
return {
|
|
24
|
+
code: this.code,
|
|
25
|
+
message: this.message,
|
|
26
|
+
details: this.details,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
/**
|
package/api/src/libs/event.ts
CHANGED
|
@@ -9,7 +9,48 @@ interface MyEventType extends EventEmitter {
|
|
|
9
9
|
emit(eventName: LiteralUnion<EventType, string | symbol>, ...args: any[]): boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
13
|
+
const _events = new EventEmitter();
|
|
14
|
+
|
|
15
|
+
// Wrap on/once so async listeners auto-register their Promises in CF Workers.
|
|
16
|
+
// EventEmitter.emit() is synchronous and discards async listener return values.
|
|
17
|
+
// In CF Workers, untracked Promises are abandoned when the request/consumer ends.
|
|
18
|
+
// This wrapper ensures async listener work completes via flushPendingJobs().
|
|
19
|
+
// In Blocklet Server (__cfPendingJobs__ doesn't exist), this is a transparent pass-through.
|
|
20
|
+
function wrapAsyncListener(listener: (...args: any[]) => any): (...args: any[]) => any {
|
|
21
|
+
return function (...args: any[]) {
|
|
22
|
+
const result = listener(...args);
|
|
23
|
+
if (result && typeof result.then === 'function') {
|
|
24
|
+
// Context-aware: HTTP uses waitUntil (non-blocking), queue/cron uses pendingJobs (blocking)
|
|
25
|
+
const isHttp = (globalThis as any).__cfHttpContext__;
|
|
26
|
+
if (isHttp) {
|
|
27
|
+
const waitUntil = (globalThis as any).__cfWaitUntil__;
|
|
28
|
+
if (typeof waitUntil === 'function') {
|
|
29
|
+
waitUntil(result.catch((err: any) => console.error('[event] async listener error:', err?.message || err)));
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
const pending = (globalThis as any).__cfPendingJobs__;
|
|
33
|
+
if (Array.isArray(pending)) {
|
|
34
|
+
pending.push(result.catch((err: any) => console.error('[event] async listener error:', err?.message || err)));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
43
|
+
const _origOn = _events.on.bind(_events);
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
45
|
+
const _origOnce = _events.once.bind(_events);
|
|
46
|
+
_events.on = function (eventName: string | symbol, listener: (...args: any[]) => void) {
|
|
47
|
+
return _origOn(eventName, wrapAsyncListener(listener));
|
|
48
|
+
};
|
|
49
|
+
_events.once = function (eventName: string | symbol, listener: (...args: any[]) => void) {
|
|
50
|
+
return _origOnce(eventName, wrapAsyncListener(listener));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const events = _events as MyEventType;
|
|
13
54
|
|
|
14
55
|
export const emitAsync = (event: string, ...args: any[]) => {
|
|
15
56
|
return new Promise((resolve, reject) => {
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { withQuery } from 'ufo';
|
|
|
4
4
|
|
|
5
5
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
6
6
|
import { Op, type WhereOptions } from 'sequelize';
|
|
7
|
-
import
|
|
7
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
8
|
+
import pick from 'lodash/pick';
|
|
8
9
|
import {
|
|
9
10
|
Customer,
|
|
10
11
|
Invoice,
|
|
@@ -78,8 +79,10 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
|
|
|
78
79
|
throw new Error(`Invoice not found: ${invoiceId}`);
|
|
79
80
|
}
|
|
80
81
|
const json = doc.toJSON();
|
|
81
|
-
const products =
|
|
82
|
-
|
|
82
|
+
const [products, prices] = await Promise.all([
|
|
83
|
+
Product.findAll().then((xs) => xs.map((x) => x.toJSON())),
|
|
84
|
+
Price.findAll().then((xs) => xs.map((x) => x.toJSON())),
|
|
85
|
+
]);
|
|
83
86
|
// @ts-ignore
|
|
84
87
|
expandLineItems(json.lines, products, prices);
|
|
85
88
|
const oneTimePaymentInfo: Array<{
|
|
@@ -108,12 +111,18 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
|
|
|
108
111
|
|
|
109
112
|
export async function getInvoiceShouldPayTotal(invoice: Invoice) {
|
|
110
113
|
try {
|
|
111
|
-
|
|
114
|
+
// Parallel fetch: subscription + paymentCurrency are independent;
|
|
115
|
+
// subscriptionItems + invoiceItems are also independent of each other.
|
|
116
|
+
const [subscription, paymentCurrency, subscriptionItems, invoiceItems] = await Promise.all([
|
|
117
|
+
Subscription.findByPk(invoice.subscription_id),
|
|
118
|
+
PaymentCurrency.findByPk(invoice.currency_id),
|
|
119
|
+
SubscriptionItem.findAll({ where: { subscription_id: invoice.subscription_id } }),
|
|
120
|
+
InvoiceItem.findAll({ where: { invoice_id: invoice.id } }),
|
|
121
|
+
]);
|
|
122
|
+
|
|
112
123
|
if (!subscription) {
|
|
113
124
|
throw new Error(`Subscription not found: ${invoice.subscription_id}`);
|
|
114
125
|
}
|
|
115
|
-
|
|
116
|
-
const paymentCurrency = await PaymentCurrency.findByPk(invoice.currency_id);
|
|
117
126
|
if (!paymentCurrency) {
|
|
118
127
|
throw new Error(`Payment currency not found: ${invoice.currency_id}`);
|
|
119
128
|
}
|
|
@@ -123,12 +132,6 @@ export async function getInvoiceShouldPayTotal(invoice: Invoice) {
|
|
|
123
132
|
throw new Error(`Payment method not found: ${paymentCurrency.payment_method_id}`);
|
|
124
133
|
}
|
|
125
134
|
|
|
126
|
-
const subscriptionItems = await SubscriptionItem.findAll({
|
|
127
|
-
where: { subscription_id: subscription.id },
|
|
128
|
-
});
|
|
129
|
-
const invoiceItems = await InvoiceItem.findAll({
|
|
130
|
-
where: { invoice_id: invoice.id },
|
|
131
|
-
});
|
|
132
135
|
const invoiceQuoteIds = invoiceItems
|
|
133
136
|
.map((item) => item.metadata?.quote_id)
|
|
134
137
|
.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
|
@@ -577,25 +580,50 @@ async function createInvoiceWithItems(props: BaseInvoiceProps): Promise<{
|
|
|
577
580
|
})
|
|
578
581
|
: itemsData;
|
|
579
582
|
|
|
583
|
+
// Pre-fetch all prices and products in bulk to avoid N+1 queries per item
|
|
584
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
585
|
+
const _itemPriceIds = enrichedItemsData.map((item) => item.price_id).filter(Boolean);
|
|
586
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
587
|
+
const [_allPrices, _taxRateForCustomer] = await Promise.all([
|
|
588
|
+
Price.findAll({ where: { id: _itemPriceIds }, include: [{ model: Product, as: 'product' }] }),
|
|
589
|
+
// Tax rate matching only depends on customer address — same for all items
|
|
590
|
+
customer.address?.country
|
|
591
|
+
? TaxRate.findMatchingRate({
|
|
592
|
+
country: customer.address.country,
|
|
593
|
+
state: customer.address.state,
|
|
594
|
+
postalCode: customer.address.postal_code,
|
|
595
|
+
livemode,
|
|
596
|
+
}).catch(() => null)
|
|
597
|
+
: Promise.resolve(null),
|
|
598
|
+
]);
|
|
599
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
600
|
+
const _priceMap = new Map(_allPrices.map((p: any) => [p.id, p]));
|
|
601
|
+
|
|
580
602
|
// create invoice items
|
|
581
603
|
const items = await Promise.all(
|
|
582
604
|
enrichedItemsData.map(async (item) => {
|
|
583
|
-
//
|
|
605
|
+
// Use pre-fetched price/product for tax rate matching
|
|
584
606
|
let taxRateId: string | undefined;
|
|
585
607
|
if (customer.address?.country && item.price_id) {
|
|
586
608
|
try {
|
|
587
|
-
const price =
|
|
609
|
+
const price = _priceMap.get(item.price_id);
|
|
588
610
|
if (price?.product_id) {
|
|
589
|
-
const product =
|
|
611
|
+
const { product } = price as any;
|
|
590
612
|
if (product) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
613
|
+
// Use pre-fetched tax rate if no product-specific tax_code,
|
|
614
|
+
// otherwise do a specific lookup
|
|
615
|
+
if (!product.tax_code) {
|
|
616
|
+
taxRateId = (_taxRateForCustomer as any)?.id;
|
|
617
|
+
} else {
|
|
618
|
+
const taxRate = await TaxRate.findMatchingRate({
|
|
619
|
+
country: customer.address.country,
|
|
620
|
+
state: customer.address.state,
|
|
621
|
+
postalCode: customer.address.postal_code,
|
|
622
|
+
taxCode: product.tax_code,
|
|
623
|
+
livemode,
|
|
624
|
+
});
|
|
625
|
+
taxRateId = taxRate?.id;
|
|
626
|
+
}
|
|
599
627
|
}
|
|
600
628
|
}
|
|
601
629
|
} catch (error) {
|
|
@@ -785,8 +813,10 @@ export async function cleanupInvoiceAndItems(invoiceId: string) {
|
|
|
785
813
|
return;
|
|
786
814
|
}
|
|
787
815
|
|
|
788
|
-
const removedItem = await
|
|
789
|
-
|
|
816
|
+
const [removedItem, removedInvoice] = await Promise.all([
|
|
817
|
+
InvoiceItem.destroy({ where: { invoice_id: invoiceId } }),
|
|
818
|
+
Invoice.destroy({ where: { id: invoiceId } }),
|
|
819
|
+
]);
|
|
790
820
|
logger.info('cleanup invoice and items', { invoiceId, removedItem, removedInvoice });
|
|
791
821
|
}
|
|
792
822
|
|
|
@@ -1345,21 +1375,26 @@ export const migrateSubscriptionPaymentMethodInvoice = async (
|
|
|
1345
1375
|
}
|
|
1346
1376
|
}
|
|
1347
1377
|
|
|
1348
|
-
// 3. Get old and new payment
|
|
1349
|
-
const oldPaymentCurrency = await
|
|
1378
|
+
// 3. Get old and new payment currency in parallel
|
|
1379
|
+
const [oldPaymentCurrency, newPaymentCurrency] = await Promise.all([
|
|
1380
|
+
PaymentCurrency.findByPk(oldCurrencyId),
|
|
1381
|
+
PaymentCurrency.findByPk(newCurrencyId),
|
|
1382
|
+
]);
|
|
1350
1383
|
if (!oldPaymentCurrency) {
|
|
1351
1384
|
throw new Error(`Payment currency ${oldCurrencyId} not found`);
|
|
1352
1385
|
}
|
|
1353
|
-
const oldPaymentMethod = await PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id);
|
|
1354
|
-
if (!oldPaymentMethod) {
|
|
1355
|
-
throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
const newPaymentCurrency = await PaymentCurrency.findByPk(newCurrencyId);
|
|
1359
1386
|
if (!newPaymentCurrency) {
|
|
1360
1387
|
throw new Error(`Payment currency ${newCurrencyId} not found`);
|
|
1361
1388
|
}
|
|
1362
|
-
|
|
1389
|
+
|
|
1390
|
+
// Get old and new payment method in parallel
|
|
1391
|
+
const [oldPaymentMethod, newPaymentMethod] = await Promise.all([
|
|
1392
|
+
PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id),
|
|
1393
|
+
PaymentMethod.findByPk(newPaymentCurrency.payment_method_id),
|
|
1394
|
+
]);
|
|
1395
|
+
if (!oldPaymentMethod) {
|
|
1396
|
+
throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
|
|
1397
|
+
}
|
|
1363
1398
|
if (!newPaymentMethod) {
|
|
1364
1399
|
throw new Error(`Payment method for currency ${newCurrencyId} not found`);
|
|
1365
1400
|
}
|
|
@@ -29,9 +29,7 @@ interface CustomerAutoRechargeDailyLimitExceededEmailTemplateContext {
|
|
|
29
29
|
customerIndexUrl: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export class CustomerAutoRechargeDailyLimitExceededEmailTemplate
|
|
33
|
-
implements BaseEmailTemplate<CustomerAutoRechargeDailyLimitExceededEmailTemplateContext>
|
|
34
|
-
{
|
|
32
|
+
export class CustomerAutoRechargeDailyLimitExceededEmailTemplate implements BaseEmailTemplate<CustomerAutoRechargeDailyLimitExceededEmailTemplateContext> {
|
|
35
33
|
options: CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions;
|
|
36
34
|
|
|
37
35
|
constructor(options: CustomerAutoRechargeDailyLimitExceededEmailTemplateOptions) {
|
|
@@ -46,9 +46,7 @@ interface CustomerAutoRechargeFailedEmailTemplateContext {
|
|
|
46
46
|
isSkipped: boolean;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export class CustomerAutoRechargeFailedEmailTemplate
|
|
50
|
-
implements BaseEmailTemplate<CustomerAutoRechargeFailedEmailTemplateContext>
|
|
51
|
-
{
|
|
49
|
+
export class CustomerAutoRechargeFailedEmailTemplate implements BaseEmailTemplate<CustomerAutoRechargeFailedEmailTemplateContext> {
|
|
52
50
|
options: CustomerAutoRechargeFailedEmailTemplateOptions;
|
|
53
51
|
|
|
54
52
|
constructor(options: CustomerAutoRechargeFailedEmailTemplateOptions) {
|
|
@@ -21,9 +21,7 @@ interface CustomerCreditGrantGrantedEmailTemplateContext {
|
|
|
21
21
|
at: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export class CustomerCreditGrantGrantedEmailTemplate
|
|
25
|
-
implements BaseEmailTemplate<CustomerCreditGrantGrantedEmailTemplateContext>
|
|
26
|
-
{
|
|
24
|
+
export class CustomerCreditGrantGrantedEmailTemplate implements BaseEmailTemplate<CustomerCreditGrantGrantedEmailTemplateContext> {
|
|
27
25
|
options: CustomerCreditGrantGrantedEmailTemplateOptions;
|
|
28
26
|
|
|
29
27
|
constructor(options: CustomerCreditGrantGrantedEmailTemplateOptions) {
|
|
@@ -37,9 +37,7 @@ interface CustomerCreditInsufficientEmailTemplateContext {
|
|
|
37
37
|
at: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export class CustomerCreditInsufficientEmailTemplate
|
|
41
|
-
implements BaseEmailTemplate<CustomerCreditInsufficientEmailTemplateContext>
|
|
42
|
-
{
|
|
40
|
+
export class CustomerCreditInsufficientEmailTemplate implements BaseEmailTemplate<CustomerCreditInsufficientEmailTemplateContext> {
|
|
43
41
|
options: CustomerCreditInsufficientEmailTemplateOptions;
|
|
44
42
|
|
|
45
43
|
constructor(options: CustomerCreditInsufficientEmailTemplateOptions) {
|
|
@@ -26,9 +26,7 @@ interface CustomerCreditLowBalanceEmailTemplateContext {
|
|
|
26
26
|
isCritical: boolean; // true if percentage < 1%
|
|
27
27
|
rechargeUrl: string | null;
|
|
28
28
|
}
|
|
29
|
-
export class CustomerCreditLowBalanceEmailTemplate
|
|
30
|
-
implements BaseEmailTemplate<CustomerCreditLowBalanceEmailTemplateContext>
|
|
31
|
-
{
|
|
29
|
+
export class CustomerCreditLowBalanceEmailTemplate implements BaseEmailTemplate<CustomerCreditLowBalanceEmailTemplateContext> {
|
|
32
30
|
// Notification configuration: 10 minute grace period before sending
|
|
33
31
|
static readonly delay = 10 * 60; // seconds
|
|
34
32
|
|
|
@@ -43,9 +43,7 @@ interface CustomerRevenueSucceededEmailTemplateContext {
|
|
|
43
43
|
* @class CustomerRevenueSucceededEmailTemplate
|
|
44
44
|
* @implements {BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext>}
|
|
45
45
|
*/
|
|
46
|
-
export class CustomerRevenueSucceededEmailTemplate
|
|
47
|
-
implements BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext>
|
|
48
|
-
{
|
|
46
|
+
export class CustomerRevenueSucceededEmailTemplate implements BaseEmailTemplate<CustomerRevenueSucceededEmailTemplateContext> {
|
|
49
47
|
options: CustomerRevenueSucceededEmailTemplateOptions;
|
|
50
48
|
|
|
51
49
|
constructor(options: CustomerRevenueSucceededEmailTemplateOptions) {
|
|
@@ -64,9 +64,7 @@ interface CustomerRewardSucceededEmailTemplateContext {
|
|
|
64
64
|
* @class CustomerRewardSucceededEmailTemplate
|
|
65
65
|
* @implements {BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext>}
|
|
66
66
|
*/
|
|
67
|
-
export class CustomerRewardSucceededEmailTemplate
|
|
68
|
-
implements BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext>
|
|
69
|
-
{
|
|
67
|
+
export class CustomerRewardSucceededEmailTemplate implements BaseEmailTemplate<CustomerRewardSucceededEmailTemplateContext> {
|
|
70
68
|
options: CustomerRewardSucceededEmailTemplateOptions;
|
|
71
69
|
|
|
72
70
|
constructor(options: CustomerRewardSucceededEmailTemplateOptions) {
|
|
@@ -29,9 +29,7 @@ interface OneTimePaymentRefundSucceededEmailTemplateContext {
|
|
|
29
29
|
invoiceNumber?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export class OneTimePaymentRefundSucceededEmailTemplate
|
|
33
|
-
implements BaseEmailTemplate<OneTimePaymentRefundSucceededEmailTemplateContext>
|
|
34
|
-
{
|
|
32
|
+
export class OneTimePaymentRefundSucceededEmailTemplate implements BaseEmailTemplate<OneTimePaymentRefundSucceededEmailTemplateContext> {
|
|
35
33
|
options: OneTimePaymentRefundSucceededEmailTemplateOptions;
|
|
36
34
|
|
|
37
35
|
constructor(options: OneTimePaymentRefundSucceededEmailTemplateOptions) {
|
|
@@ -37,9 +37,7 @@ interface OneTimePaymentSucceededEmailTemplateContext {
|
|
|
37
37
|
* @class OneTimePaymentSucceededEmailTemplate
|
|
38
38
|
* @implements {BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext>}
|
|
39
39
|
*/
|
|
40
|
-
export class OneTimePaymentSucceededEmailTemplate
|
|
41
|
-
implements BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext>
|
|
42
|
-
{
|
|
40
|
+
export class OneTimePaymentSucceededEmailTemplate implements BaseEmailTemplate<OneTimePaymentSucceededEmailTemplateContext> {
|
|
43
41
|
options: OneTimePaymentSucceededEmailTemplateOptions;
|
|
44
42
|
|
|
45
43
|
constructor(options: OneTimePaymentSucceededEmailTemplateOptions) {
|
|
@@ -50,9 +50,7 @@ interface SubscriptionRenewFailedEmailTemplateContext {
|
|
|
50
50
|
payer: string;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export class SubscriptionRenewFailedEmailTemplate
|
|
54
|
-
implements BaseEmailTemplate<SubscriptionRenewFailedEmailTemplateContext>
|
|
55
|
-
{
|
|
53
|
+
export class SubscriptionRenewFailedEmailTemplate implements BaseEmailTemplate<SubscriptionRenewFailedEmailTemplateContext> {
|
|
56
54
|
options: SubscriptionRenewFailedEmailTemplateOptions;
|
|
57
55
|
|
|
58
56
|
constructor(options: SubscriptionRenewFailedEmailTemplateOptions) {
|
|
@@ -27,9 +27,7 @@ interface SubscriptionSlippageExceededEmailTemplateContext {
|
|
|
27
27
|
customActions: any[];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export class SubscriptionSlippageExceededEmailTemplate
|
|
31
|
-
implements BaseEmailTemplate<SubscriptionSlippageExceededEmailTemplateContext>
|
|
32
|
-
{
|
|
30
|
+
export class SubscriptionSlippageExceededEmailTemplate implements BaseEmailTemplate<SubscriptionSlippageExceededEmailTemplateContext> {
|
|
33
31
|
options: SubscriptionSlippageExceededEmailTemplateOptions;
|
|
34
32
|
|
|
35
33
|
constructor(options: SubscriptionSlippageExceededEmailTemplateOptions) {
|
|
@@ -29,9 +29,7 @@ interface SubscriptionSlippageWarningEmailTemplateContext {
|
|
|
29
29
|
customActions: any[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export class SubscriptionSlippageWarningEmailTemplate
|
|
33
|
-
implements BaseEmailTemplate<SubscriptionSlippageWarningEmailTemplateContext>
|
|
34
|
-
{
|
|
32
|
+
export class SubscriptionSlippageWarningEmailTemplate implements BaseEmailTemplate<SubscriptionSlippageWarningEmailTemplateContext> {
|
|
35
33
|
options: SubscriptionSlippageWarningEmailTemplateOptions;
|
|
36
34
|
|
|
37
35
|
constructor(options: SubscriptionSlippageWarningEmailTemplateOptions) {
|
|
@@ -68,7 +68,7 @@ export class SubscriptionSucceededEmailTemplate extends BaseSubscriptionEmailTem
|
|
|
68
68
|
|
|
69
69
|
return Boolean(
|
|
70
70
|
['disabled', 'minted', 'sent', 'error'].includes(checkoutSession?.nft_mint_status as string) &&
|
|
71
|
-
|
|
71
|
+
(invoice?.payment_intent_id || (invoice && +invoice.amount_remaining === 0))
|
|
72
72
|
);
|
|
73
73
|
},
|
|
74
74
|
{ timeout: 1000 * 10, interval: 1000 }
|
|
@@ -140,17 +140,22 @@ function calculateFetchStrategy<T>(
|
|
|
140
140
|
return { fetchLimit: Math.max(sourceCount, 1000), fetchOffset: 0 };
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// For database sources with multiple sources, use
|
|
143
|
+
// For database sources with multiple sources, use demand-driven strategy
|
|
144
144
|
if (sourceMeta?.type === 'database') {
|
|
145
145
|
if (sources.length > 1) {
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
// Multi-source merge fetches from offset=0 and merge-sorts in memory.
|
|
147
|
+
// We need (offset + pageSize) items after merging. Since we don't know
|
|
148
|
+
// how items interleave between sources, fetch (offset + pageSize) from
|
|
149
|
+
// each source with a buffer for merge uncertainty.
|
|
150
|
+
//
|
|
151
|
+
// Previous approach used sourceCount * 0.6~0.8 which over-fetched
|
|
152
|
+
// massively for early pages (e.g. page 1 fetched 2000+ rows) and still
|
|
153
|
+
// under-fetched for late pages. This demand-driven approach fetches
|
|
154
|
+
// proportionally to the page position — fast for early pages, more data
|
|
155
|
+
// only when needed for later pages.
|
|
156
|
+
const needed = offset + pageSize;
|
|
157
|
+
const buffer = Math.max(pageSize * 3, 50); // generous buffer for interleave uncertainty
|
|
158
|
+
const fetchLimit = Math.min(sourceCount, needed + buffer);
|
|
154
159
|
return { fetchLimit, fetchOffset: 0 };
|
|
155
160
|
}
|
|
156
161
|
// Single database source can use precise offset
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -387,20 +387,20 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
387
387
|
delegator,
|
|
388
388
|
source,
|
|
389
389
|
});
|
|
390
|
-
return { sufficient: false, reason: 'NO_DELEGATION' };
|
|
390
|
+
return { sufficient: false, reason: 'NO_DELEGATION', state };
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
// have transfer permissions?
|
|
394
394
|
const grant = (state as DelegateState).ops.find((x: any) => x.key === OCAP_PAYMENT_TX_TYPE)?.value;
|
|
395
395
|
if (!grant) {
|
|
396
|
-
return { sufficient: false, reason: 'NO_TRANSFER_PERMISSION' };
|
|
396
|
+
return { sufficient: false, reason: 'NO_TRANSFER_PERMISSION', state };
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
// check token limits
|
|
400
400
|
if (grant.limit && Array.isArray(grant.limit.tokens) && grant.limit.tokens.length > 0) {
|
|
401
401
|
const tokenLimit = grant.limit.tokens.find((x: any) => x.address === tokenAddress);
|
|
402
402
|
if (!tokenLimit) {
|
|
403
|
-
return { sufficient: false, reason: 'NO_TOKEN_PERMISSION' };
|
|
403
|
+
return { sufficient: false, reason: 'NO_TOKEN_PERMISSION', state };
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
// FIXME: @wangshijun check other conditions in the token limit: txCount, totalAllowance, validUntil, rateLimit
|
|
@@ -411,7 +411,7 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
411
411
|
const allWalletAddresses = [wallet.address, ...migratedFrom];
|
|
412
412
|
const hasValidTo = allWalletAddresses.some((addr) => tokenLimit.to.includes(addr));
|
|
413
413
|
if (!hasValidTo) {
|
|
414
|
-
return { sufficient: false, reason: 'NO_TRANSFER_TO' };
|
|
414
|
+
return { sufficient: false, reason: 'NO_TRANSFER_TO', state };
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
|
|
@@ -426,12 +426,14 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
426
426
|
return {
|
|
427
427
|
sufficient: false,
|
|
428
428
|
reason: 'NO_ENOUGH_ALLOWANCE',
|
|
429
|
+
state,
|
|
429
430
|
};
|
|
430
431
|
}
|
|
431
432
|
} else if (totalAmount.gt(allowance)) {
|
|
432
433
|
return {
|
|
433
434
|
sufficient: false,
|
|
434
435
|
reason: 'NO_ENOUGH_ALLOWANCE',
|
|
436
|
+
state,
|
|
435
437
|
};
|
|
436
438
|
}
|
|
437
439
|
}
|
|
@@ -441,7 +443,7 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
441
443
|
const { tokens } = await client.getAccountTokens({ address: delegator, token: tokenAddress });
|
|
442
444
|
const [token] = tokens;
|
|
443
445
|
if (!token) {
|
|
444
|
-
return { sufficient: false, reason: 'NO_TOKEN' };
|
|
446
|
+
return { sufficient: false, reason: 'NO_TOKEN', state };
|
|
445
447
|
}
|
|
446
448
|
|
|
447
449
|
if (new BN(token.balance).lt(totalAmount)) {
|
|
@@ -449,6 +451,7 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
449
451
|
sufficient: false,
|
|
450
452
|
reason: 'NO_ENOUGH_TOKEN',
|
|
451
453
|
token,
|
|
454
|
+
state,
|
|
452
455
|
requestedAmount: totalAmount.toString(),
|
|
453
456
|
};
|
|
454
457
|
}
|
|
@@ -602,7 +605,11 @@ export async function getTokenLimitsForDelegation(
|
|
|
602
605
|
paymentMethod: PaymentMethod,
|
|
603
606
|
paymentCurrency: PaymentCurrency,
|
|
604
607
|
address: string,
|
|
605
|
-
amount: string
|
|
608
|
+
amount: string,
|
|
609
|
+
// Optional: if caller already fetched DelegateState (e.g. isDelegationSufficientForPayment),
|
|
610
|
+
// pass it here to skip a redundant chain RPC. Caller MUST use state from the same request
|
|
611
|
+
// to avoid stale-balance issues (see note on delegationCache above).
|
|
612
|
+
delegateState?: DelegateState | null
|
|
606
613
|
): Promise<TokenLimit[]> {
|
|
607
614
|
const hasMetered = items.some((x) => x.price.recurring?.usage_type === 'metered');
|
|
608
615
|
const allowance = hasMetered ? '0' : amount;
|
|
@@ -618,8 +625,16 @@ export async function getTokenLimitsForDelegation(
|
|
|
618
625
|
};
|
|
619
626
|
|
|
620
627
|
if (paymentMethod.type === 'arcblock') {
|
|
621
|
-
|
|
622
|
-
|
|
628
|
+
let state: DelegateState | null | undefined = delegateState;
|
|
629
|
+
if (state === undefined) {
|
|
630
|
+
const client = paymentMethod.getOcapClient();
|
|
631
|
+
// CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
|
|
632
|
+
await client.getContext();
|
|
633
|
+
({ state } = await client.getDelegateState({ address }));
|
|
634
|
+
logger.info('getTokenLimitsForDelegation: fetched DelegateState from chain', { address });
|
|
635
|
+
} else {
|
|
636
|
+
logger.info('getTokenLimitsForDelegation: reused DelegateState from caller', { address });
|
|
637
|
+
}
|
|
623
638
|
|
|
624
639
|
// If we never delegated before
|
|
625
640
|
if (!state) {
|
|
@@ -630,10 +645,10 @@ export async function getTokenLimitsForDelegation(
|
|
|
630
645
|
return [entry];
|
|
631
646
|
}
|
|
632
647
|
|
|
633
|
-
const op = state.ops.find((x) => x.key === OCAP_PAYMENT_TX_TYPE);
|
|
648
|
+
const op = state.ops.find((x: any) => x.key === OCAP_PAYMENT_TX_TYPE);
|
|
634
649
|
if (op && Array.isArray(op.value.limit?.tokens) && op.value.limit.tokens.length > 0) {
|
|
635
650
|
const tokenLimits = cloneDeep(op.value.limit.tokens);
|
|
636
|
-
const index = op.value.limit.tokens.findIndex((x) => x.address === paymentCurrency.contract);
|
|
651
|
+
const index = op.value.limit.tokens.findIndex((x: any) => x.address === paymentCurrency.contract);
|
|
637
652
|
// we are updating an existing token limit
|
|
638
653
|
if (index > -1) {
|
|
639
654
|
const limit = op.value.limit.tokens[index] as TokenLimit;
|
package/api/src/libs/security.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { auth } from '@blocklet/sdk/lib/middlewares';
|
|
2
2
|
import { getVerifyData, verify } from '@blocklet/sdk/lib/util/verify-sign';
|
|
3
|
+
import { verifyLoginToken } from '@blocklet/sdk/lib/util/verify-session';
|
|
3
4
|
import { getWallet } from '@blocklet/sdk/lib/wallet';
|
|
4
5
|
import type { NextFunction, Request, Response } from 'express';
|
|
5
6
|
import type { Model } from 'sequelize';
|
|
@@ -39,6 +40,56 @@ export function authenticate<T extends Model>({
|
|
|
39
40
|
ensureLogin,
|
|
40
41
|
}: PermissionSpec<T>) {
|
|
41
42
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
43
|
+
// Dev-only bypass: requires both NODE_ENV=development AND the explicit
|
|
44
|
+
// opt-in env ENABLE_DEV_FAKE_AUTH=1, plus the x-dev-fake-did header on
|
|
45
|
+
// the request. Lets mobile demo clients that don't go through DID Connect
|
|
46
|
+
// (e.g. the local-tunnel backend which bypasses Blocklet Server) still
|
|
47
|
+
// exercise real handlers. Production never sets ENABLE_DEV_FAKE_AUTH, so
|
|
48
|
+
// this branch can't trigger there; dev defaults off, so we don't
|
|
49
|
+
// accidentally regress to fake auth after wiring the real flow.
|
|
50
|
+
if (process.env.NODE_ENV === 'development' && process.env.ENABLE_DEV_FAKE_AUTH === '1') {
|
|
51
|
+
const devDid = req.get('x-dev-fake-did');
|
|
52
|
+
if (devDid) {
|
|
53
|
+
req.user = {
|
|
54
|
+
did: devDid,
|
|
55
|
+
role: 'owner', // satisfies routes that require owner/admin
|
|
56
|
+
provider: 'dev',
|
|
57
|
+
fullName: 'dev-fake-user',
|
|
58
|
+
walletOS: '',
|
|
59
|
+
via: 'dev',
|
|
60
|
+
};
|
|
61
|
+
return next();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Authenticate by Authorization: Bearer <login-token>. The token is a JWT
|
|
66
|
+
// signed with this blocklet's session secret (see @blocklet/sdk session
|
|
67
|
+
// middleware). When clients hit us through a tunnel that bypasses Blocklet
|
|
68
|
+
// Server (so x-user-did is NOT injected), we need to verify the token
|
|
69
|
+
// ourselves. verifyLoginToken does local JWT signature verification, no
|
|
70
|
+
// HTTP callback. On success we forward into the existing x-user-did branch
|
|
71
|
+
// by populating req.headers so the role/mine/record cascade below applies
|
|
72
|
+
// unchanged.
|
|
73
|
+
const authHeader = req.get('authorization');
|
|
74
|
+
if (authHeader && /^Bearer\s+/i.test(authHeader) && !req.headers['x-user-did']) {
|
|
75
|
+
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
|
|
76
|
+
if (token) {
|
|
77
|
+
const session = await verifyLoginToken({ token, strictMode: false }).catch(() => null);
|
|
78
|
+
if (session?.did) {
|
|
79
|
+
// Some BS versions put a bare base58 address in the JWT, others
|
|
80
|
+
// the canonical `did:abt:…` form. Normalize so downstream code
|
|
81
|
+
// (entitlement self-check, mine: lookups by req.user.did) sees the
|
|
82
|
+
// same shape clients send in `customer_did` query params.
|
|
83
|
+
const canonicalDid = session.did.startsWith('did:abt:') ? session.did : `did:abt:${session.did}`;
|
|
84
|
+
req.headers['x-user-did'] = canonicalDid;
|
|
85
|
+
req.headers['x-user-role'] = `blocklet-${session.role || 'user'}`;
|
|
86
|
+
req.headers['x-user-provider'] = session.provider || 'wallet';
|
|
87
|
+
req.headers['x-user-fullname'] = encodeURIComponent(session.fullName || '');
|
|
88
|
+
req.headers['x-user-wallet-os'] = session.walletOS || '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
42
93
|
// authenticate by component call
|
|
43
94
|
const sig = req.get('x-component-sig');
|
|
44
95
|
if (component && sig) {
|
package/api/src/libs/session.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
|
5
5
|
import cloneDeep from 'lodash/cloneDeep';
|
|
6
6
|
import isEqual from 'lodash/isEqual';
|
|
7
7
|
import pAll from 'p-all';
|
|
8
|
-
import
|
|
8
|
+
import omit from 'lodash/omit';
|
|
9
9
|
import dayjs from './dayjs';
|
|
10
10
|
import { validCoupon } from './discount/coupon';
|
|
11
11
|
import { getPriceUintAmountByCurrency, getPriceCurrencyOptions } from './price';
|
|
@@ -1357,8 +1357,20 @@ export async function isSubscriptionOverdraftProtectionEnabled(subscription: Sub
|
|
|
1357
1357
|
throw new Error(`PaymentMethod not found in ${subscription.id}`);
|
|
1358
1358
|
}
|
|
1359
1359
|
|
|
1360
|
+
// Overdraft protection is only meaningful for on-chain (arcblock) payment
|
|
1361
|
+
// methods where users stake tokens. IAP / Stripe channels have no notion of
|
|
1362
|
+
// staking; return disabled-default silently rather than throwing into the
|
|
1363
|
+
// outer catch (which would spam the error log on every google_play /
|
|
1364
|
+
// app_store / stripe subscription event).
|
|
1360
1365
|
if (paymentMethod.type !== 'arcblock') {
|
|
1361
|
-
|
|
1366
|
+
return {
|
|
1367
|
+
enabled: false,
|
|
1368
|
+
remaining: '0',
|
|
1369
|
+
used: '0',
|
|
1370
|
+
shouldPay: '0',
|
|
1371
|
+
unused: '0',
|
|
1372
|
+
revokedStake: '0',
|
|
1373
|
+
};
|
|
1362
1374
|
}
|
|
1363
1375
|
if (!subscription.overdraft_protection) {
|
|
1364
1376
|
return {
|