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.
Files changed (241) hide show
  1. package/__blocklet__.js +37 -0
  2. package/api/ocap-1.30-subpath-shims.d.ts +35 -0
  3. package/api/src/crons/index.ts +32 -0
  4. package/api/src/crons/metering-subscription-detection.ts +12 -14
  5. package/api/src/crons/overdue-detection.ts +51 -74
  6. package/api/src/crons/retry-pending-events.ts +58 -0
  7. package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
  8. package/api/src/integrations/app-store/client.ts +369 -0
  9. package/api/src/integrations/app-store/handlers/index.ts +46 -0
  10. package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
  11. package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
  12. package/api/src/integrations/app-store/notification-routing.ts +18 -0
  13. package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
  14. package/api/src/integrations/arcblock/nft.ts +6 -2
  15. package/api/src/integrations/arcblock/stake.ts +3 -2
  16. package/api/src/integrations/arcblock/token.ts +4 -4
  17. package/api/src/integrations/blocklet/notification.ts +1 -1
  18. package/api/src/integrations/ethereum/tx.ts +29 -0
  19. package/api/src/integrations/google-play/client.ts +276 -0
  20. package/api/src/integrations/google-play/handlers/index.ts +69 -0
  21. package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
  22. package/api/src/integrations/google-play/handlers/voided.ts +106 -0
  23. package/api/src/integrations/google-play/setup.ts +43 -0
  24. package/api/src/integrations/google-play/verify.ts +251 -0
  25. package/api/src/integrations/iap-reconcile.ts +415 -0
  26. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  27. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  28. package/api/src/integrations/stripe/resource.ts +8 -0
  29. package/api/src/libs/audit.ts +70 -24
  30. package/api/src/libs/auth.ts +49 -2
  31. package/api/src/libs/chain-error.ts +31 -0
  32. package/api/src/libs/entitlement.ts +399 -0
  33. package/api/src/libs/env.ts +2 -0
  34. package/api/src/libs/error.ts +15 -0
  35. package/api/src/libs/event.ts +42 -1
  36. package/api/src/libs/invoice.ts +69 -34
  37. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  38. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  39. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  40. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  41. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  42. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  43. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  44. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  45. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  46. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  47. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  48. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  49. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  50. package/api/src/libs/pagination.ts +14 -9
  51. package/api/src/libs/payment.ts +25 -10
  52. package/api/src/libs/security.ts +51 -0
  53. package/api/src/libs/session.ts +1 -1
  54. package/api/src/libs/subscription.ts +13 -1
  55. package/api/src/libs/timing.ts +35 -0
  56. package/api/src/libs/util.ts +29 -15
  57. package/api/src/libs/wallet-migration.ts +72 -53
  58. package/api/src/queues/auto-recharge.ts +1 -1
  59. package/api/src/queues/credit-consume.ts +94 -12
  60. package/api/src/queues/credit-grant.ts +4 -0
  61. package/api/src/queues/event.ts +39 -21
  62. package/api/src/queues/invoice.ts +1 -0
  63. package/api/src/queues/payment.ts +83 -15
  64. package/api/src/queues/refund.ts +84 -71
  65. package/api/src/queues/subscription.ts +1 -0
  66. package/api/src/queues/webhook.ts +12 -2
  67. package/api/src/routes/checkout-sessions.ts +82 -43
  68. package/api/src/routes/connect/change-payment.ts +2 -0
  69. package/api/src/routes/connect/change-plan.ts +2 -0
  70. package/api/src/routes/connect/pay.ts +12 -3
  71. package/api/src/routes/connect/setup.ts +3 -1
  72. package/api/src/routes/connect/shared.ts +52 -39
  73. package/api/src/routes/connect/subscribe.ts +4 -1
  74. package/api/src/routes/credit-grants.ts +25 -17
  75. package/api/src/routes/donations.ts +2 -2
  76. package/api/src/routes/entitlements.ts +105 -0
  77. package/api/src/routes/events.ts +2 -2
  78. package/api/src/routes/index.ts +12 -2
  79. package/api/src/routes/integrations/app-store.ts +267 -0
  80. package/api/src/routes/integrations/google-play.ts +324 -0
  81. package/api/src/routes/meter-events.ts +16 -6
  82. package/api/src/routes/payment-links.ts +1 -1
  83. package/api/src/routes/payment-methods.ts +131 -1
  84. package/api/src/routes/settings.ts +1 -1
  85. package/api/src/routes/tax-rates.ts +1 -1
  86. package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
  87. package/api/src/store/models/customer.ts +37 -1
  88. package/api/src/store/models/entitlement-grant.ts +118 -0
  89. package/api/src/store/models/entitlement-product.ts +48 -0
  90. package/api/src/store/models/entitlement.ts +86 -0
  91. package/api/src/store/models/index.ts +9 -0
  92. package/api/src/store/models/invoice.ts +20 -0
  93. package/api/src/store/models/payment-method.ts +66 -1
  94. package/api/src/store/models/price.ts +23 -14
  95. package/api/src/store/models/refund.ts +10 -0
  96. package/api/src/store/models/subscription.ts +14 -0
  97. package/api/src/store/models/types.ts +32 -0
  98. package/api/tests/integrations/app-store/client.spec.ts +335 -0
  99. package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
  100. package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
  101. package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
  102. package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
  103. package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
  104. package/api/tests/integrations/google-play/verify.spec.ts +215 -0
  105. package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
  106. package/api/tests/libs/entitlement.spec.ts +347 -0
  107. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  108. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  109. package/api/tests/queues/credit-consume.spec.ts +8 -4
  110. package/api/tests/routes/credit-grants.spec.ts +1 -0
  111. package/blocklet.yml +1 -1
  112. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  113. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  114. package/cloudflare/README.md +499 -0
  115. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  116. package/cloudflare/build.ts +151 -0
  117. package/cloudflare/did-connect-auth.ts +527 -0
  118. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  119. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  120. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  121. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  122. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  123. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  124. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  125. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  126. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  127. package/cloudflare/frontend-shims/session.ts +24 -0
  128. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  129. package/cloudflare/index.html +40 -0
  130. package/cloudflare/migrate-to-d1.js +252 -0
  131. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  132. package/cloudflare/migrations/0002_indexes.sql +75 -0
  133. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  134. package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
  135. package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
  136. package/cloudflare/run-build.js +391 -0
  137. package/cloudflare/scripts/test-decrypt.js +102 -0
  138. package/cloudflare/shims/arcblock-ws.ts +20 -0
  139. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  140. package/cloudflare/shims/axios-lite.ts +117 -0
  141. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  142. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  143. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  144. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  145. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  146. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  147. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  148. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  149. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  150. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  151. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  152. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  153. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  154. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  155. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  156. package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
  157. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  158. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  159. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  160. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  161. package/cloudflare/shims/cookie-parser.ts +3 -0
  162. package/cloudflare/shims/cors.ts +21 -0
  163. package/cloudflare/shims/cron.ts +189 -0
  164. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  165. package/cloudflare/shims/did-space-js.ts +17 -0
  166. package/cloudflare/shims/did-space.ts +11 -0
  167. package/cloudflare/shims/error.ts +18 -0
  168. package/cloudflare/shims/express-compat/index.ts +80 -0
  169. package/cloudflare/shims/express-compat/types.ts +41 -0
  170. package/cloudflare/shims/fastq.ts +105 -0
  171. package/cloudflare/shims/lock.ts +115 -0
  172. package/cloudflare/shims/mime-types.ts +56 -0
  173. package/cloudflare/shims/nedb-storage.ts +9 -0
  174. package/cloudflare/shims/node-child-process.ts +9 -0
  175. package/cloudflare/shims/node-fs.ts +20 -0
  176. package/cloudflare/shims/node-http.ts +13 -0
  177. package/cloudflare/shims/node-https.ts +4 -0
  178. package/cloudflare/shims/node-misc.ts +15 -0
  179. package/cloudflare/shims/node-net.ts +8 -0
  180. package/cloudflare/shims/node-os.ts +14 -0
  181. package/cloudflare/shims/node-tty.ts +8 -0
  182. package/cloudflare/shims/node-zlib.ts +17 -0
  183. package/cloudflare/shims/noop.ts +26 -0
  184. package/cloudflare/shims/payment-vendor.ts +14 -0
  185. package/cloudflare/shims/querystring.ts +12 -0
  186. package/cloudflare/shims/queue.ts +611 -0
  187. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  188. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  189. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  190. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  191. package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
  192. package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
  193. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  194. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  195. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  196. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  197. package/cloudflare/shims/stripe-cf.ts +29 -0
  198. package/cloudflare/shims/ws-lite.ts +103 -0
  199. package/cloudflare/shims/xss.ts +3 -0
  200. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  201. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
  202. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  203. package/cloudflare/vite.config.ts +162 -0
  204. package/cloudflare/worker.ts +1608 -0
  205. package/cloudflare/wrangler.json +63 -0
  206. package/cloudflare/wrangler.jsonc +75 -0
  207. package/cloudflare/wrangler.staging.json +67 -0
  208. package/cloudflare/wrangler.toml +28 -0
  209. package/jest.config.js +4 -12
  210. package/package.json +30 -22
  211. package/scripts/seed-google-play.ts +79 -0
  212. package/src/app.tsx +62 -4
  213. package/src/components/customer/link.tsx +9 -13
  214. package/src/components/customer/notification-preference.tsx +3 -2
  215. package/src/components/filter-toolbar.tsx +4 -0
  216. package/src/components/invoice/list.tsx +9 -1
  217. package/src/components/invoice-pdf/utils.ts +2 -1
  218. package/src/components/layout/admin.tsx +39 -5
  219. package/src/components/layout/user-cf.tsx +77 -0
  220. package/src/components/payment-intent/actions.tsx +23 -3
  221. package/src/components/payment-method/app-store.tsx +103 -0
  222. package/src/components/payment-method/form.tsx +7 -1
  223. package/src/components/payment-method/google-play.tsx +85 -0
  224. package/src/components/safe-did-address.tsx +75 -0
  225. package/src/components/subscription/list.tsx +20 -0
  226. package/src/libs/patch-user-card.ts +25 -0
  227. package/src/libs/util.ts +5 -7
  228. package/src/locales/en.tsx +63 -0
  229. package/src/locales/zh.tsx +63 -0
  230. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  231. package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
  232. package/src/pages/admin/customers/customers/detail.tsx +8 -2
  233. package/src/pages/admin/customers/customers/index.tsx +2 -2
  234. package/src/pages/admin/overview.tsx +3 -1
  235. package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
  236. package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
  237. package/src/pages/customer/subscription/detail.tsx +4 -4
  238. package/tsconfig.api.json +1 -6
  239. package/tsconfig.json +3 -4
  240. package/tsconfig.types.json +2 -1
  241. package/vite.config.ts +6 -1
@@ -6,6 +6,7 @@ import { encodeTransferItx } from '../../integrations/ethereum/token';
6
6
  import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
7
7
  import { CallbackArgs, ethWallet } from '../../libs/auth';
8
8
  import logger from '../../libs/logger';
9
+ import { parseChainError } from '../../libs/chain-error';
9
10
  import { getGasPayerExtra } from '../../libs/payment';
10
11
  import { createPaymentOutput } from '../../libs/session';
11
12
  import { getTxMetadata } from '../../libs/util';
@@ -130,6 +131,9 @@ export default {
130
131
  const client = paymentMethod.getOcapClient();
131
132
  const claim = claims.find((x) => x.type === 'prepareTx');
132
133
 
134
+ // Warm up chain context cache to avoid setTimeout(resolve,0) hang in workerd
135
+ await client.getContext();
136
+
133
137
  const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
134
138
  if (claim.delegator && claim.from) {
135
139
  tx.delegator = claim.delegator;
@@ -227,10 +231,15 @@ export default {
227
231
 
228
232
  return { hash: txHash };
229
233
  } catch (err) {
230
- console.error(err);
231
- logger.error('Failed to finalize paymentIntent on arcblock', { paymentIntent: paymentIntent.id, error: err });
234
+ const parsed = parseChainError(err);
235
+ logger.error('Failed to finalize paymentIntent on arcblock', {
236
+ paymentIntentId: paymentIntent.id,
237
+ code: parsed.code,
238
+ details: parsed.details,
239
+ raw: err,
240
+ });
232
241
  await paymentIntent.update({ status: 'requires_capture' });
233
- throw err;
242
+ throw parsed;
234
243
  }
235
244
  }
236
245
 
@@ -101,6 +101,8 @@ export default {
101
101
  billingThreshold: Math.max(minStakeAmount, billingThreshold),
102
102
  items,
103
103
  slippageConfig: subscription?.slippage_config || undefined,
104
+ // Reuse DelegateState fetched above — avoids a duplicate chain RPC on CF cold isolates
105
+ delegateState: delegation.state,
104
106
  }),
105
107
  });
106
108
  }
@@ -217,7 +219,7 @@ export default {
217
219
 
218
220
  await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
219
221
  if (invoice) {
220
- invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
222
+ await invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
221
223
  }
222
224
  await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
223
225
  logger.info('CheckoutSession updated on setup done', {
@@ -1,12 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
2
  /* eslint-disable prettier/prettier */
3
3
  import { toTypeInfo } from '@arcblock/did';
4
- import { toDelegateAddress } from '@arcblock/did-util';
5
- import type { Transaction } from '@ocap/client';
4
+ import { toDelegateAddress } from '@arcblock/did-util/cbor';
5
+ import type { DelegateState as DelegateStateType, Transaction } from '@ocap/client';
6
6
  import { BN, fromTokenToUnit, toBase58 } from '@ocap/util';
7
7
  import { fromPublicKey } from '@ocap/wallet';
8
8
  import type { Request } from 'express';
9
- import { isEmpty } from 'lodash';
9
+ import isEmpty from 'lodash/isEmpty';
10
10
  import dayjs from '../../libs/dayjs';
11
11
  import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/arcblock/stake';
12
12
  import { encodeApproveItx } from '../../integrations/ethereum/token';
@@ -31,6 +31,7 @@ import {
31
31
  } from '../../libs/subscription';
32
32
  import { getCustomerStakeAddress, OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
33
33
 
34
+ // eslint-disable-next-line import/no-cycle
34
35
  import { invoiceQueue } from '../../queues/invoice';
35
36
  import { AutoRechargeConfig, type TLineItemExpanded } from '../../store/models';
36
37
  import { CheckoutSession } from '../../store/models/checkout-session';
@@ -453,8 +454,6 @@ export async function ensureInvoiceForCheckout({
453
454
  }
454
455
  }
455
456
 
456
- const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
457
-
458
457
  const metadata = {
459
458
  ...(checkoutSession.invoice_creation?.invoice_data?.metadata || {}),
460
459
  };
@@ -467,7 +466,13 @@ export async function ensureInvoiceForCheckout({
467
466
  const trialInDays = Number(checkoutSession.subscription_data?.trial_period_days || 0);
468
467
  const trialEnd = Number(checkoutSession.subscription_data?.trial_end || 0);
469
468
  const now = dayjs().unix();
470
- let invoiceItems = lineItems || (await Price.expand(checkoutSession.line_items, { product: true, upsell: true }));
469
+
470
+ // Parallel: fetch currency + expand line items (independent queries)
471
+ const [currency, expandedItems] = await Promise.all([
472
+ PaymentCurrency.findByPk(checkoutSession.currency_id),
473
+ lineItems ? Promise.resolve(lineItems) : Price.expand(checkoutSession.line_items, { product: true, upsell: true }),
474
+ ]);
475
+ let invoiceItems = expandedItems;
471
476
 
472
477
  // For items with quote_id, fetch full quote info and attach to line item metadata
473
478
  // This ensures invoice line items have complete quote info for display
@@ -610,33 +615,21 @@ export async function ensureInvoiceForCheckout({
610
615
  discountCount: discountInfo.appliedDiscounts.length,
611
616
  });
612
617
 
613
- // only update invoice_id for single invoice
614
- if (!isGroupInvoice) {
615
- await checkoutSession.update({ invoice_id: invoice.id });
616
- }
617
- if (paymentIntent) {
618
- await paymentIntent.update({ invoice_id: invoice.id });
619
- }
620
- if (subscription) {
621
- await subscription.update({ latest_invoice_id: invoice.id });
622
- }
623
-
624
- // Update quotes with invoice_id for audit trail
625
- // Find quote_ids from invoice items and update the quotes
618
+ // Update invoice_id references in parallel — these are independent writes
626
619
  const quoteIdsFromItems = items
627
620
  .map((item) => item.metadata?.quote_id)
628
621
  .filter((id): id is string => typeof id === 'string' && id.length > 0);
629
622
 
630
- if (quoteIdsFromItems.length > 0) {
631
- await PriceQuote.update(
632
- { invoice_id: invoice.id },
633
- { where: { id: quoteIdsFromItems } }
634
- );
635
- logger.info('Updated quotes with invoice_id', {
636
- invoiceId: invoice.id,
637
- quoteIds: quoteIdsFromItems,
638
- });
639
- }
623
+ await Promise.all([
624
+ !isGroupInvoice ? checkoutSession.update({ invoice_id: invoice.id }) : Promise.resolve(),
625
+ paymentIntent ? paymentIntent.update({ invoice_id: invoice.id }) : Promise.resolve(),
626
+ subscription ? subscription.update({ latest_invoice_id: invoice.id }) : Promise.resolve(),
627
+ quoteIdsFromItems.length > 0
628
+ ? PriceQuote.update({ invoice_id: invoice.id }, { where: { id: quoteIdsFromItems } }).then(() => {
629
+ logger.info('Updated quotes with invoice_id', { invoiceId: invoice.id, quoteIds: quoteIdsFromItems });
630
+ })
631
+ : Promise.resolve(),
632
+ ]);
640
633
 
641
634
  return { invoice, items };
642
635
  }
@@ -861,6 +854,7 @@ export async function getDelegationTxClaim({
861
854
  billingThreshold = 0,
862
855
  requiredStake = true,
863
856
  slippageConfig,
857
+ delegateState,
864
858
  }: {
865
859
  userDid: string;
866
860
  userPk: string;
@@ -880,6 +874,10 @@ export async function getDelegationTxClaim({
880
874
  min_acceptable_rate?: string;
881
875
  base_currency?: string;
882
876
  };
877
+ // Optional DelegateState passthrough — avoids a duplicate chain RPC in getTokenLimitsForDelegation
878
+ // when the caller already fetched it (e.g. via isDelegationSufficientForPayment). Same-request
879
+ // usage only — do not cache across requests (see delegationCache note in libs/payment.ts).
880
+ delegateState?: DelegateStateType | null;
883
881
  }) {
884
882
  // Calculate authorization amount
885
883
  // When slippage_config with min_acceptable_rate is provided, use it for precise calculation
@@ -902,16 +900,23 @@ export async function getDelegationTxClaim({
902
900
  }
903
901
 
904
902
  const address = toDelegateAddress(userDid, wallet.address);
905
- const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
906
- let tokenRequirements = await getTokenRequirements({
907
- items,
908
- mode,
909
- trialing,
910
- billingThreshold,
911
- paymentMethod,
912
- paymentCurrency,
913
- requiredStake,
914
- });
903
+ // These two helpers each issue 1–2 chain RPCs and don't depend on each other,
904
+ // so fan them out — on CF Workers cold isolates the serial path easily pushed the
905
+ // request over the runtime CPU/real-time budget and tripped a "hung promise" 500
906
+ // (see #1351). Parallelising halves the wall-clock for the delegation tx claim.
907
+ const [tokenLimits, tokenRequirementsResult] = await Promise.all([
908
+ getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount, delegateState),
909
+ getTokenRequirements({
910
+ items,
911
+ mode,
912
+ trialing,
913
+ billingThreshold,
914
+ paymentMethod,
915
+ paymentCurrency,
916
+ requiredStake,
917
+ }),
918
+ ]);
919
+ let tokenRequirements = tokenRequirementsResult;
915
920
 
916
921
  if (mode === 'delegation') {
917
922
  tokenRequirements = [];
@@ -1049,6 +1054,9 @@ export async function getStakeTxClaim({
1049
1054
  if (paymentMethod.type === 'arcblock') {
1050
1055
  // create staking data
1051
1056
  const client = paymentMethod.getOcapClient();
1057
+ // CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
1058
+ // Same pattern as pay.ts; needed because did-connect-js tryWithTimeout bounds this callback at 10s.
1059
+ await client.getContext();
1052
1060
 
1053
1061
  const stakeId = hasGrouping ? `stake-group-${subscription.id}` : subscription.id;
1054
1062
 
@@ -1155,6 +1163,8 @@ export async function getOverdraftProtectionStakeTxClaim({
1155
1163
  if (paymentMethod.type === 'arcblock') {
1156
1164
  // create staking data
1157
1165
  const client = paymentMethod.getOcapClient();
1166
+ // CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
1167
+ await client.getContext();
1158
1168
  const nonce = `overdraft-protection-${subscription.id}`;
1159
1169
  const address = await getCustomerStakeAddress(userDid, nonce);
1160
1170
  const { state } = await client.getStakeState({ address });
@@ -1579,6 +1589,9 @@ export async function executeOcapTransactions(
1579
1589
  nonce?: string
1580
1590
  ) {
1581
1591
  const client = paymentMethod.getOcapClient();
1592
+ // CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
1593
+ // Each subsequent executeSingleTransaction (Delegate + Stake broadcast) needs ready context.
1594
+ await client.getContext();
1582
1595
  logger.info('start executeOcapTransactions', { userDid, claims });
1583
1596
 
1584
1597
  const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
@@ -133,6 +133,8 @@ export default {
133
133
  items,
134
134
  requiredStake,
135
135
  slippageConfig: primarySubscription?.slippage_config || undefined,
136
+ // Reuse DelegateState fetched above — avoids a duplicate chain RPC on CF cold isolates
137
+ delegateState: delegation.state,
136
138
  }),
137
139
  });
138
140
  }
@@ -243,7 +245,8 @@ export default {
243
245
 
244
246
  for (const invoice of invoices) {
245
247
  if (invoice) {
246
- invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
248
+ // eslint-disable-next-line no-await-in-loop
249
+ await invoiceQueue.pushAndWait({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
247
250
  }
248
251
  }
249
252
 
@@ -27,6 +27,7 @@ import { blocklet } from '../libs/auth';
27
27
  import { formatMetadata } from '../libs/util';
28
28
  import { getPriceUintAmountByCurrency } from '../libs/price';
29
29
  import { checkTokenBalance } from '../libs/payment';
30
+ import { measurePhase } from '../libs/timing';
30
31
  import { getExchangeRateService } from '../libs/exchange-rate/service';
31
32
  import { getExchangeRateSymbol, hasTokenAddress } from '../libs/exchange-rate/token-address-mapping';
32
33
  import { isRateBelowMinAcceptableRate } from '../libs/slippage';
@@ -290,16 +291,19 @@ router.get('/verify-availability', authMine, async (req, res) => {
290
291
  const { customer_id: customerId, currency_id: currencyId } = value;
291
292
  let pendingAmount = value.pending_amount;
292
293
 
293
- const customer = await Customer.findByPkOrDid(customerId);
294
+ // Parallelize Customer + PaymentCurrency lookups (independent, saves ~85ms RTT)
295
+ const [customer, currency] = await Promise.all([
296
+ Customer.findByPkOrDid(customerId),
297
+ PaymentCurrency.findByPk(currencyId),
298
+ ]);
294
299
  if (!customer) {
295
300
  return res.status(404).json({ error: `Customer ${customerId} not found` });
296
301
  }
297
-
298
- const currency = await PaymentCurrency.findByPk(currencyId);
299
302
  if (!currency) {
300
303
  return res.status(404).json({ error: `PaymentCurrency ${currencyId} not found` });
301
304
  }
302
305
 
306
+ // AutoRechargeConfig needs customer.id which we now have
303
307
  const config = (await AutoRechargeConfig.findOne({
304
308
  where: {
305
309
  customer_id: customer.id,
@@ -511,13 +515,15 @@ router.get('/verify-availability', authMine, async (req, res) => {
511
515
  // This ensures user can pay off pending amount and still have balance for continued usage
512
516
  // minimumRequiredBalance = requiredPaymentAmount (to pay off pending) + totalAmount (for one more recharge)
513
517
  const minimumRequiredBalance = requiredPaymentAmount.add(totalAmount);
514
- balanceResult = await checkTokenBalance({
515
- paymentMethod: config.paymentMethod,
516
- paymentCurrency: config.rechargeCurrency,
517
- userDid: payer,
518
- amount: minimumRequiredBalance.toString(),
519
- skipUserCheck: true,
520
- });
518
+ balanceResult = await measurePhase('chain', () =>
519
+ checkTokenBalance({
520
+ paymentMethod: config.paymentMethod!,
521
+ paymentCurrency: config.rechargeCurrency!,
522
+ userDid: payer,
523
+ amount: minimumRequiredBalance.toString(),
524
+ skipUserCheck: true,
525
+ })
526
+ );
521
527
 
522
528
  if (!balanceResult.sufficient) {
523
529
  return res.json({
@@ -531,13 +537,15 @@ router.get('/verify-availability', authMine, async (req, res) => {
531
537
  }
532
538
  } else {
533
539
  // No pending amount: check if balance can cover at least one recharge
534
- balanceResult = await checkTokenBalance({
535
- paymentMethod: config.paymentMethod,
536
- paymentCurrency: config.rechargeCurrency,
537
- userDid: payer,
538
- amount: totalAmount.toString(),
539
- skipUserCheck: true,
540
- });
540
+ balanceResult = await measurePhase('chain', () =>
541
+ checkTokenBalance({
542
+ paymentMethod: config.paymentMethod!,
543
+ paymentCurrency: config.rechargeCurrency!,
544
+ userDid: payer,
545
+ amount: totalAmount.toString(),
546
+ skipUserCheck: true,
547
+ })
548
+ );
541
549
 
542
550
  if (!balanceResult.sufficient) {
543
551
  return res.json({
@@ -1,4 +1,4 @@
1
- import { Joi } from '@arcblock/validator';
1
+ import Joi from 'joi';
2
2
  import { Router } from 'express';
3
3
 
4
4
  import { BN } from '@ocap/util';
@@ -24,7 +24,7 @@ const donationSchema = Joi.object<DonationSettings>({
24
24
  beneficiaries: Joi.array()
25
25
  .items(
26
26
  Joi.object({
27
- address: Joi.DID().required(),
27
+ address: Joi.string().required(),
28
28
  share: Joi.number().positive().required(),
29
29
  memo: Joi.string().max(64).optional(),
30
30
  })
@@ -0,0 +1,105 @@
1
+ // Cross-channel entitlement query API.
2
+ //
3
+ // Endpoints:
4
+ // GET /api/entitlements/check ?customer_did=&product_id=[&livemode=]
5
+ // GET /api/entitlements/list ?customer_did=[&livemode=]
6
+ //
7
+ // Auth model:
8
+ // - Component-to-component calls (other blocklets via @blocklet/payment-client):
9
+ // trusted via the component signature; `roles: ['owner','admin']` is the gate.
10
+ // - Logged-in end users (mobile demo, web SPA): `mine: true` lets them in iff
11
+ // their DID matches the query's customer_did — enforced in the handler.
12
+
13
+ import { Router } from 'express';
14
+ import Joi from 'joi';
15
+
16
+ import { checkEntitlement, listEntitlements } from '../libs/entitlement';
17
+ import logger from '../libs/logger';
18
+ import { authenticate } from '../libs/security';
19
+ import { PaymentMethod } from '../store/models';
20
+
21
+ const router = Router();
22
+ // component+owner/admin for cross-blocklet calls; ensureLogin for end users —
23
+ // handler then enforces that non-admin users can only query their own DID.
24
+ const auth = authenticate<PaymentMethod>({ component: true, roles: ['owner', 'admin'], ensureLogin: true });
25
+
26
+ function isAdminUser(role?: string): boolean {
27
+ return role === 'owner' || role === 'admin';
28
+ }
29
+
30
+ // Strip the `did:abt:` prefix if present so callers can compare DIDs across
31
+ // the two shapes that show up in the codebase (bare base58 vs canonical).
32
+ // The CF Workers AUTH_SERVICE returns bare addresses; clients send canonical.
33
+ function canonicalDid(did: string | undefined | null): string {
34
+ if (!did) return '';
35
+ return did.startsWith('did:abt:') ? did.slice('did:abt:'.length) : did;
36
+ }
37
+
38
+ function isSelf(req: any, customerDid: string): boolean {
39
+ const a = canonicalDid(req.user?.did);
40
+ const b = canonicalDid(customerDid);
41
+ return !!a && a === b;
42
+ }
43
+
44
+ const checkQuerySchema = Joi.object<{
45
+ customer_did: string;
46
+ product_id: string;
47
+ livemode?: string | boolean;
48
+ }>({
49
+ customer_did: Joi.string().required(),
50
+ product_id: Joi.string().required(),
51
+ livemode: Joi.alternatives(Joi.boolean(), Joi.string().valid('true', 'false')),
52
+ });
53
+
54
+ const listQuerySchema = Joi.object<{
55
+ customer_did: string;
56
+ livemode?: string | boolean;
57
+ }>({
58
+ customer_did: Joi.string().required(),
59
+ livemode: Joi.alternatives(Joi.boolean(), Joi.string().valid('true', 'false')),
60
+ });
61
+
62
+ function parseLivemode(value: string | boolean | undefined, fallback: boolean): boolean {
63
+ if (typeof value === 'boolean') return value;
64
+ if (value === 'true') return true;
65
+ if (value === 'false') return false;
66
+ return fallback;
67
+ }
68
+
69
+ router.get('/check', auth, async (req, res) => {
70
+ try {
71
+ const input = await checkQuerySchema.validateAsync(req.query, { stripUnknown: true });
72
+ if (!isAdminUser((req as any).user?.role) && !isSelf(req, input.customer_did)) {
73
+ res.status(403).json({ error: 'Cannot query entitlements for other customers' });
74
+ return;
75
+ }
76
+ const livemode = parseLivemode(input.livemode, !!req.livemode);
77
+ const result = await checkEntitlement({
78
+ customer_did: input.customer_did,
79
+ product_id: input.product_id,
80
+ livemode,
81
+ });
82
+ res.json(result);
83
+ } catch (err: any) {
84
+ logger.error('entitlements/check failed', { error: err?.message, stack: err?.stack });
85
+ res.status(400).json({ error: err?.message ?? 'check failed' });
86
+ }
87
+ });
88
+
89
+ router.get('/list', auth, async (req, res) => {
90
+ try {
91
+ const input = await listQuerySchema.validateAsync(req.query, { stripUnknown: true });
92
+ if (!isAdminUser((req as any).user?.role) && !isSelf(req, input.customer_did)) {
93
+ res.status(403).json({ error: 'Cannot list entitlements for other customers' });
94
+ return;
95
+ }
96
+ const livemode = parseLivemode(input.livemode, !!req.livemode);
97
+ const list = await listEntitlements({ customer_did: input.customer_did, livemode });
98
+ res.json({ list });
99
+ } catch (err: any) {
100
+ logger.error('entitlements/list failed', { error: err?.message, stack: err?.stack });
101
+ res.status(400).json({ error: err?.message ?? 'list failed' });
102
+ }
103
+ });
104
+
105
+ export default router;
@@ -186,7 +186,7 @@ router.get('/retry-webhooks', auth, async (req, res) => {
186
186
  // eslint-disable-next-line no-restricted-syntax
187
187
  for (const webhook of eventWebhooks) {
188
188
  // eslint-disable-next-line no-await-in-loop
189
- const added = await addWebhookJob(event.id, webhook.id, { persist: false });
189
+ const added = await addWebhookJob(event.id, webhook.id, { persist: true });
190
190
  if (added) {
191
191
  scheduled += 1;
192
192
  }
@@ -255,7 +255,7 @@ router.post('/:id/retry-webhooks', auth, async (req, res) => {
255
255
  // eslint-disable-next-line no-restricted-syntax
256
256
  for (const webhook of eventWebhooks) {
257
257
  // eslint-disable-next-line no-await-in-loop
258
- const added = await addWebhookJob(event.id, webhook.id, { persist: false });
258
+ const added = await addWebhookJob(event.id, webhook.id, { persist: true });
259
259
  if (added) {
260
260
  scheduled += 1;
261
261
  logger.info('Manually scheduled webhook retry', { eventId: event.id, webhookId: webhook.id });
@@ -10,6 +10,9 @@ import creditTransactions from './credit-transactions';
10
10
  import customers from './customers';
11
11
  import donations from './donations';
12
12
  import events from './events';
13
+ import entitlements from './entitlements';
14
+ import appStore from './integrations/app-store';
15
+ import googlePlay from './integrations/google-play';
13
16
  import stripe from './integrations/stripe';
14
17
  import invoices from './invoices';
15
18
  import meterEvents from './meter-events';
@@ -49,8 +52,12 @@ router.use((req, _, next) => {
49
52
  } catch {
50
53
  req.livemode = true;
51
54
  }
52
- } else {
53
- req.livemode = true;
55
+ } else if (typeof req.livemode !== 'boolean') {
56
+ // CF Workers' createExpressReq pre-populates req.livemode from the worker's
57
+ // PAYMENT_LIVEMODE env var; honor that when set. Express dev has no
58
+ // upstream, so we fall back to the env var ourselves — defaulting to
59
+ // livemode=true unless explicitly disabled via PAYMENT_LIVEMODE=false.
60
+ req.livemode = process.env.PAYMENT_LIVEMODE !== 'false';
54
61
  }
55
62
 
56
63
  next();
@@ -101,6 +108,9 @@ router.use('/donations', loadBaseCurrency, donations);
101
108
  router.use('/events', events);
102
109
  router.use('/invoices', invoices);
103
110
  router.use('/integrations/stripe', stripe);
111
+ router.use('/integrations/google-play', googlePlay);
112
+ router.use('/integrations/app-store', appStore);
113
+ router.use('/entitlements', entitlements);
104
114
  router.use('/meter-events', meterEvents);
105
115
  router.use('/meters', meters);
106
116
  router.use('/passports', passports);