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
@@ -0,0 +1,189 @@
1
+ // @abtnode/cron shim for CF Cron Triggers
2
+ //
3
+ // The real @abtnode/cron exports { init } where init({ context, jobs, onError }) registers jobs.
4
+ // In CF Workers, we store the jobs and execute them via the scheduled() handler.
5
+ //
6
+ // The shim parses 6-field cron expressions (sec min hour day month weekday)
7
+ // and only runs jobs whose schedule matches the current 5-minute window.
8
+
9
+ type CronJob = {
10
+ name: string;
11
+ time: string;
12
+ fn: () => Promise<any> | any;
13
+ options?: { runOnInit?: boolean };
14
+ };
15
+
16
+ type InitOptions = {
17
+ context?: any;
18
+ jobs: CronJob[];
19
+ onError?: (error: Error, name: string) => void;
20
+ };
21
+
22
+ // Singleton storage for registered cron jobs
23
+ const registeredJobs: CronJob[] = [];
24
+ let onErrorHandler: ((error: Error, name: string) => void) | undefined;
25
+
26
+ // --- Cron expression matcher ---
27
+ // Supports 6-field format: second minute hour dayOfMonth month dayOfWeek
28
+ // Supports: numbers, *, */N, ranges (1-5), lists (1,3,5)
29
+
30
+ function parseField(field: string, min: number, max: number): number[] | null {
31
+ // null means "match all"
32
+ if (field === '*') return null;
33
+
34
+ const values = new Set<number>();
35
+
36
+ for (const part of field.split(',')) {
37
+ // */N — every N
38
+ const stepMatch = part.match(/^\*\/(\d+)$/);
39
+ if (stepMatch) {
40
+ const step = parseInt(stepMatch[1], 10);
41
+ for (let i = min; i <= max; i += step) {
42
+ values.add(i);
43
+ }
44
+ continue;
45
+ }
46
+
47
+ // N-M — range
48
+ const rangeMatch = part.match(/^(\d+)-(\d+)$/);
49
+ if (rangeMatch) {
50
+ const from = parseInt(rangeMatch[1], 10);
51
+ const to = parseInt(rangeMatch[2], 10);
52
+ for (let i = from; i <= to; i++) {
53
+ values.add(i);
54
+ }
55
+ continue;
56
+ }
57
+
58
+ // N — single value
59
+ const num = parseInt(part, 10);
60
+ if (!isNaN(num)) {
61
+ values.add(num);
62
+ }
63
+ }
64
+
65
+ return values.size > 0 ? Array.from(values) : null;
66
+ }
67
+
68
+ /**
69
+ * Check if a date matches a 6-field cron expression.
70
+ * Returns true if the date's minute/hour/day/month/weekday match.
71
+ * Seconds field is ignored (CF triggers are minute-level).
72
+ */
73
+ function matchesCron(cronExpr: string, date: Date): boolean {
74
+ const fields = cronExpr.trim().split(/\s+/);
75
+ if (fields.length < 5) return true; // Can't parse — run it
76
+
77
+ // 6-field: sec min hour dom month dow
78
+ // 5-field: min hour dom month dow
79
+ const offset = fields.length >= 6 ? 1 : 0;
80
+
81
+ const minuteField = parseField(fields[offset], 0, 59);
82
+ const hourField = parseField(fields[offset + 1], 0, 23);
83
+ const domField = parseField(fields[offset + 2], 1, 31);
84
+ const monthField = parseField(fields[offset + 3], 0, 11); // cron months are 1-12, JS is 0-11
85
+ const dowField = parseField(fields[offset + 4], 0, 6);
86
+
87
+ const m = date.getUTCMinutes();
88
+ const h = date.getUTCHours();
89
+ const dom = date.getUTCDate();
90
+ const month = date.getUTCMonth() + 1; // JS 0-based → cron 1-based
91
+ const dow = date.getUTCDay(); // 0=Sunday
92
+
93
+ if (minuteField && !minuteField.includes(m)) return false;
94
+ if (hourField && !hourField.includes(h)) return false;
95
+ if (domField && !domField.includes(dom)) return false;
96
+ if (monthField && !monthField.includes(month)) return false;
97
+ if (dowField && !dowField.includes(dow)) return false;
98
+
99
+ return true;
100
+ }
101
+
102
+ // Check if a cron expression should fire at the given date (minute-level match).
103
+ //
104
+ // History: this helper previously used a 5-minute look-ahead window, under the
105
+ // assumption that CF Cron Triggers fire every 5 minutes. Once the deploy config
106
+ // switched to every-minute cron, that window made every stepped expression fire
107
+ // 5x more often than intended, driving CF Queues past the free-tier daily cap
108
+ // (2026-04-17 incident). With CF Scheduled firing every minute, we only check
109
+ // the current minute — each cron expression triggers at its designed frequency.
110
+ // See docs/cf-queues-ops-alert-analysis.md § 改动 B.
111
+ function shouldRunInWindow(cronExpr: string, date: Date): boolean {
112
+ return matchesCron(cronExpr, date);
113
+ }
114
+
115
+ // --- Cron shim API ---
116
+
117
+ function init(options: InitOptions) {
118
+ registeredJobs.length = 0;
119
+ onErrorHandler = options.onError;
120
+
121
+ for (const job of options.jobs || []) {
122
+ if (job.name && job.time && typeof job.fn === 'function') {
123
+ registeredJobs.push(job);
124
+ }
125
+ }
126
+
127
+ // Skip runOnInit jobs in CF Workers — they'll run on the next matching trigger
128
+ // (running them at module init time would block the request and may exceed CPU limits)
129
+
130
+ return {
131
+ addJob(name: string, time: string, fn: Function, opts?: any) {
132
+ registeredJobs.push({ name, time, fn: fn as any, options: opts });
133
+ },
134
+ start() { /* no-op */ },
135
+ };
136
+ }
137
+
138
+ // Called by worker.ts scheduled handler — only runs jobs matching the trigger time.
139
+ // Accepts an optional `now` so the caller can pass `event.scheduledTime` (the
140
+ // intended trigger minute) instead of relying on wall-clock at execution time.
141
+ // CF may deliver scheduled events with a small delay that crosses a minute
142
+ // boundary; matching on `scheduledTime` keeps exact-minute cron reliable.
143
+ async function runAll(now: Date = new Date()) {
144
+ const matched: string[] = [];
145
+ const skipped: string[] = [];
146
+
147
+ for (const job of registeredJobs) {
148
+ if (shouldRunInWindow(job.time, now)) {
149
+ matched.push(job.name);
150
+ try {
151
+ await job.fn();
152
+ } catch (err: any) {
153
+ console.error(`[Cron] ${job.name} failed:`, err?.message || err);
154
+ onErrorHandler?.(err, job.name);
155
+ }
156
+ } else {
157
+ skipped.push(job.name);
158
+ }
159
+ }
160
+
161
+ console.log(`[Cron] Ran ${matched.length} jobs: [${matched.join(', ')}]. Skipped ${skipped.length}.`);
162
+ }
163
+
164
+ async function runJob(name: string) {
165
+ const job = registeredJobs.find((j) => j.name === name);
166
+ if (job) {
167
+ try {
168
+ await job.fn();
169
+ } catch (err: any) {
170
+ console.error(`[Cron] ${name} failed:`, err?.message || err);
171
+ onErrorHandler?.(err, name);
172
+ }
173
+ } else {
174
+ console.warn(`[Cron] Job ${name} not found`);
175
+ }
176
+ }
177
+
178
+ function getJobNames(): string[] {
179
+ return registeredJobs.map((j) => `${j.name} (${j.time})`);
180
+ }
181
+
182
+ // Export matching @abtnode/cron API: default export is { init }
183
+ export default { init };
184
+
185
+ // Export helper functions for the worker.ts scheduled handler
186
+ export const cronInstance = { runAll, runJob, getJobNames };
187
+
188
+ // Exported for unit tests; not part of the public cron API.
189
+ export const __test__ = { matchesCron, shouldRunInWindow };
@@ -0,0 +1,7 @@
1
+ // crypto-js shim — warns when AES legacy encryption is attempted in CF Workers
2
+ const warn = (method: string) =>
3
+ console.warn(`[cf-shim] crypto-js/${method} called — legacy AES crypto is not available in CF Workers`);
4
+
5
+ export const encrypt = (..._args: any[]) => { warn('aes.encrypt'); return ''; };
6
+ export const decrypt = (..._args: any[]) => { warn('aes.decrypt'); return ''; };
7
+ export default { encrypt, decrypt };
@@ -0,0 +1,17 @@
1
+ // noop shim for @blocklet/did-space-js in CF Workers
2
+ const noop = (..._args: any[]) => {};
3
+
4
+ export class SpaceClient {
5
+ constructor(..._args: any[]) {}
6
+ send = noop;
7
+ }
8
+
9
+ export class GetObjectCommand {
10
+ constructor(..._args: any[]) {}
11
+ }
12
+
13
+ export class PutObjectCommand {
14
+ constructor(..._args: any[]) {}
15
+ }
16
+
17
+ export default { SpaceClient, GetObjectCommand, PutObjectCommand };
@@ -0,0 +1,11 @@
1
+ // @blocklet/did-space-js shim
2
+ export class SpaceClient {
3
+ constructor(_opts?: any) {}
4
+ async send(_cmd: any) { return {}; }
5
+ }
6
+ export class GetObjectCommand {
7
+ constructor(_opts?: any) {}
8
+ }
9
+ export class PutObjectCommand {
10
+ constructor(_opts?: any) {}
11
+ }
@@ -0,0 +1,18 @@
1
+ export class CustomError extends Error {
2
+ statusCode: number;
3
+ code: string;
4
+
5
+ constructor(statusCode: number, code: string, message?: string) {
6
+ super(message || code);
7
+ this.statusCode = statusCode;
8
+ this.code = code;
9
+ }
10
+ }
11
+
12
+ export function formatError(err: any) {
13
+ return { code: err.code || 'UNKNOWN', message: err.message };
14
+ }
15
+
16
+ export function getStatusFromError(err: any) {
17
+ return err.statusCode || 500;
18
+ }
@@ -0,0 +1,80 @@
1
+ // Express compatibility shim for CF Workers
2
+ // Allows existing Express route handlers to work with Hono
3
+
4
+ export type RouteEntry = { method: string; path: string; handlers: Function[] };
5
+
6
+ export function Router() {
7
+ const routes: RouteEntry[] = [];
8
+ const middlewares: Function[] = [];
9
+
10
+ const router: any = function () {};
11
+
12
+ for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
13
+ router[method] = (path: string, ...handlers: Function[]) => {
14
+ routes.push({ method: method.toUpperCase(), path, handlers: [...middlewares, ...handlers] });
15
+ return router;
16
+ };
17
+ }
18
+
19
+ router.use = (...args: any[]) => {
20
+ if (typeof args[0] === 'string') {
21
+ // router.use('/path', ...middlewaresOrSubRouter)
22
+ const path = args[0];
23
+ const rest = args.slice(1);
24
+
25
+ // Find the sub-router (has _routes) — it's typically the last arg
26
+ const subRouterIdx = rest.findIndex((a: any) => a?._routes);
27
+ if (subRouterIdx >= 0) {
28
+ const subRouter = rest[subRouterIdx];
29
+ // Middlewares are everything before the sub-router
30
+ const pathMiddlewares = rest.slice(0, subRouterIdx).filter((a: any) => typeof a === 'function');
31
+
32
+ for (const route of subRouter._routes) {
33
+ routes.push({
34
+ method: route.method,
35
+ path: path + route.path,
36
+ // Chain: parent middlewares -> path-level middlewares -> sub-router middlewares from route
37
+ handlers: [...middlewares, ...pathMiddlewares, ...route.handlers],
38
+ });
39
+ }
40
+ } else {
41
+ // All args are middlewares for this path — treat as path-scoped middleware
42
+ for (const fn of rest) {
43
+ if (typeof fn === 'function') {
44
+ // We store path-scoped middlewares as a special route entry
45
+ // They'll match any method and sub-path
46
+ middlewares.push(fn);
47
+ }
48
+ }
49
+ }
50
+ } else {
51
+ // router.use(middleware1, middleware2, ...)
52
+ for (const fn of args) {
53
+ if (typeof fn === 'function') {
54
+ middlewares.push(fn);
55
+ }
56
+ }
57
+ }
58
+ return router;
59
+ };
60
+
61
+ router._routes = routes;
62
+ router._middlewares = middlewares;
63
+
64
+ return router;
65
+ }
66
+
67
+ // Express app stub
68
+ export function express() {
69
+ return Router();
70
+ }
71
+
72
+ // Static middleware stubs
73
+ express.json = (_opts?: any) => (_req: any, _res: any, next: any) => next();
74
+ express.urlencoded = (_opts?: any) => (_req: any, _res: any, next: any) => next();
75
+ express.static = (_path: string, _opts?: any) => (_req: any, _res: any, next: any) => next();
76
+ express.Router = Router;
77
+ express.raw = (_opts?: any) => (_req: any, _res: any, next: any) => next();
78
+
79
+ export default express;
80
+ export type { Request, Response, NextFunction, ErrorRequestHandler } from './types';
@@ -0,0 +1,41 @@
1
+ // Express type stubs
2
+ export interface Request {
3
+ method: string;
4
+ path: string;
5
+ url: string;
6
+ originalUrl: string;
7
+ query: Record<string, any>;
8
+ params: Record<string, any>;
9
+ body: any;
10
+ headers: Record<string, string>;
11
+ user?: any;
12
+ customer?: any;
13
+ livemode?: boolean;
14
+ baseCurrency?: any;
15
+ doc?: any;
16
+ stripeEvent?: any;
17
+ stripeClient?: any;
18
+ get(name: string): string | undefined;
19
+ header(name: string): string | undefined;
20
+ [key: string]: any;
21
+ }
22
+
23
+ export interface Response {
24
+ json(data: any): any;
25
+ status(code: number): Response;
26
+ send(data: any): any;
27
+ redirect(url: string): any;
28
+ set(key: string, value: string): Response;
29
+ setHeader(key: string, value: string): Response;
30
+ cookie(name: string, value: string, options?: any): Response;
31
+ end(): void;
32
+ headersSent: boolean;
33
+ statusCode: number;
34
+ _statusCode?: number;
35
+ _headers?: Record<string, string>;
36
+ _body?: any;
37
+ _sent?: boolean;
38
+ }
39
+
40
+ export type NextFunction = (err?: any) => void;
41
+ export type ErrorRequestHandler = (err: any, req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,105 @@
1
+ // fastq shim for CF Workers
2
+ // Executes jobs serially with proper callback handling.
3
+ //
4
+ // Key design decisions:
5
+ // 1. The original queue's worker is `async (data, cb) => { ... }` — it's both
6
+ // async (returns a promise) AND manually calls cb(). We must handle BOTH signals.
7
+ // 2. If cb is called, that's the primary completion signal (with err/result).
8
+ // 3. If the async worker's promise resolves WITHOUT cb being called (e.g.,
9
+ // ensureUniqueExecution early-return when job is already running), we must
10
+ // still resolve to avoid hanging the processQueue loop.
11
+ // 4. If the promise rejects without cb being called, forward the error to cb.
12
+
13
+ type Task = { data: any; cb?: Function };
14
+
15
+ export default function fastq(_context: any, worker: Function, _concurrency: number) {
16
+ const pending: Task[] = [];
17
+ let running = false;
18
+ let drainFn: (() => void) | null = null;
19
+
20
+ async function processQueue() {
21
+ if (running) return;
22
+ running = true;
23
+
24
+ while (pending.length > 0) {
25
+ const task = pending.shift()!;
26
+ try {
27
+ await new Promise<void>((resolve) => {
28
+ let cbCalled = false;
29
+
30
+ // Wrap the callback so we track whether it was called
31
+ const wrappedCb = (err: any, res: any) => {
32
+ if (cbCalled) return; // guard against double-call
33
+ cbCalled = true;
34
+ task.cb?.(err, res);
35
+ resolve();
36
+ };
37
+
38
+ const result = worker(task.data, wrappedCb);
39
+
40
+ // Handle async workers that may resolve/reject without calling cb
41
+ if (result && typeof result.then === 'function') {
42
+ result.then(
43
+ () => {
44
+ // Worker's promise resolved. If cb wasn't called, the worker
45
+ // completed without signaling (e.g., duplicate job early-return).
46
+ // Resolve the processQueue loop to avoid hanging.
47
+ if (!cbCalled) {
48
+ console.warn('[fastq-shim] worker resolved without calling cb — resolving with no-op');
49
+ cbCalled = true;
50
+ resolve();
51
+ }
52
+ },
53
+ (err: any) => {
54
+ // Worker's promise rejected without calling cb
55
+ if (!cbCalled) {
56
+ console.error('[fastq-shim] worker rejected without calling cb:', err);
57
+ cbCalled = true;
58
+ task.cb?.(err);
59
+ resolve();
60
+ }
61
+ }
62
+ );
63
+ }
64
+ });
65
+ } catch (err) {
66
+ console.error('[fastq-shim] unexpected error in processQueue:', err);
67
+ task.cb?.(err);
68
+ }
69
+ }
70
+
71
+ running = false;
72
+ if (pending.length === 0 && drainFn) {
73
+ drainFn();
74
+ }
75
+ }
76
+
77
+ const q: any = {
78
+ push(data: any, cb?: Function) {
79
+ pending.push({ data, cb });
80
+ // Use queueMicrotask to allow the push caller to set up event listeners
81
+ // before the job starts executing
82
+ queueMicrotask(() => processQueue());
83
+ },
84
+ unshift(data: any, cb?: Function) {
85
+ pending.unshift({ data, cb });
86
+ queueMicrotask(() => processQueue());
87
+ },
88
+ pause() {},
89
+ resume() {},
90
+ idle() { return pending.length === 0 && !running; },
91
+ length() { return pending.length; },
92
+ kill() { pending.length = 0; },
93
+ killAndDrain() {
94
+ pending.length = 0;
95
+ if (drainFn) drainFn();
96
+ },
97
+ get drain() { return drainFn; },
98
+ set drain(fn: any) { drainFn = fn; },
99
+ empty: null as any,
100
+ saturated: null as any,
101
+ error: null as any,
102
+ };
103
+
104
+ return q;
105
+ }
@@ -0,0 +1,115 @@
1
+ // CF Workers lock shim — replaces libs/lock.ts
2
+ //
3
+ // In Blocklet Server (single-process Node.js), the original in-memory lock works fine.
4
+ // In CF Workers, each HTTP request runs in a separate isolate, so in-memory locks
5
+ // are useless for cross-request mutual exclusion.
6
+ //
7
+ // This shim uses D1 (SQLite) as the coordination layer:
8
+ // - INSERT OR IGNORE with PRIMARY KEY constraint provides atomic lock acquisition
9
+ // - owner token prevents accidental release by other isolates
10
+ // - TTL-based expiry prevents deadlocks from crashed isolates
11
+ // - Exponential backoff with jitter avoids D1 write hotspots
12
+
13
+ import { getDB } from './sequelize-d1/model';
14
+
15
+ let _nanoidCounter = 0;
16
+ function simpleId(): string {
17
+ // Lightweight unique ID — no need for crypto-strength randomness for lock owners
18
+ return `${Date.now().toString(36)}-${(++_nanoidCounter).toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
19
+ }
20
+
21
+ export class Lock {
22
+ name: string;
23
+ owner: string;
24
+ locked: boolean;
25
+ ttl: number;
26
+
27
+ // EventEmitter stub for API compatibility — original Lock extends EventEmitter
28
+ events: { on: () => void; removeListener: () => void; emit: () => void };
29
+
30
+ constructor(name: string, options?: { ttl?: number }) {
31
+ this.name = name;
32
+ this.owner = simpleId();
33
+ this.locked = false;
34
+ this.ttl = options?.ttl || 5000; // default 5s TTL for short critical sections
35
+ this.events = { on: () => {}, removeListener: () => {}, emit: () => {} };
36
+ }
37
+
38
+ async acquire(maxWaitMs = 10000): Promise<true> {
39
+ const db = getDB();
40
+ const deadline = Date.now() + maxWaitMs;
41
+ let delay = 30; // start at 30ms
42
+
43
+ while (Date.now() < deadline) {
44
+ const now = Date.now();
45
+
46
+ try {
47
+ // Atomically: clean expired locks + try to insert ours + verify ownership
48
+ // All 3 statements in a single D1 batch (1 round-trip instead of 2)
49
+ const batchResult = await db.batch([
50
+ db.prepare('DELETE FROM _locks WHERE name = ? AND expires_at < ?').bind(this.name, now),
51
+ db.prepare('INSERT OR IGNORE INTO _locks (name, owner, expires_at) VALUES (?, ?, ?)')
52
+ .bind(this.name, this.owner, now + this.ttl),
53
+ db.prepare('SELECT owner FROM _locks WHERE name = ?').bind(this.name),
54
+ ]);
55
+
56
+ const row = batchResult[2]?.results?.[0] as { owner: string } | undefined;
57
+ if (row?.owner === this.owner) {
58
+ this.locked = true;
59
+ return true;
60
+ }
61
+ } catch (err: any) {
62
+ // D1 errors (e.g. table not found on first deploy) — log and retry
63
+ console.error(`[D1Lock] acquire error for "${this.name}":`, err?.message || err);
64
+ }
65
+
66
+ // Exponential backoff with jitter to avoid D1 write hotspots
67
+ const jitter = Math.random() * delay * 0.3;
68
+ await new Promise(r => setTimeout(r, delay + jitter));
69
+ delay = Math.min(delay * 2, 500); // cap at 500ms
70
+ }
71
+
72
+ // Timeout — throw to match original behavior where acquire() always resolves
73
+ // All callers use try/catch or don't check the return value
74
+ console.warn(`[D1Lock] acquire timeout: "${this.name}" after ${maxWaitMs}ms`);
75
+ throw new Error(`[D1Lock] Failed to acquire lock "${this.name}" within ${maxWaitMs}ms`);
76
+ }
77
+
78
+ release(): void {
79
+ if (!this.locked) return;
80
+ this.locked = false;
81
+
82
+ // Fire-and-forget: 8 out of 10 call sites do NOT await release().
83
+ // We register the promise with __cfPendingJobs__ so flushPendingJobs()
84
+ // in worker.ts ensures it completes before the response is sent.
85
+ try {
86
+ const db = getDB();
87
+ const promise = db.prepare('DELETE FROM _locks WHERE name = ? AND owner = ?')
88
+ .bind(this.name, this.owner)
89
+ .run()
90
+ .catch((err: any) => console.error(`[D1Lock] release error for "${this.name}":`, err?.message || err));
91
+
92
+ // For HTTP context, use waitUntil (non-blocking) instead of pendingJobs (blocking)
93
+ const isHttp = (globalThis as any).__cfHttpContext__;
94
+ if (isHttp) {
95
+ const waitUntil = (globalThis as any).__cfWaitUntil__;
96
+ if (typeof waitUntil === 'function') {
97
+ waitUntil(promise);
98
+ }
99
+ } else {
100
+ const pending = (globalThis as any).__cfPendingJobs__;
101
+ if (Array.isArray(pending)) {
102
+ pending.push(promise);
103
+ }
104
+ }
105
+ } catch (err: any) {
106
+ console.error(`[D1Lock] release setup error for "${this.name}":`, err?.message || err);
107
+ }
108
+ }
109
+ }
110
+
111
+ // In CF Workers, don't cache Lock instances across requests — each request is isolated.
112
+ // The original code caches by name, but that's only useful within a single process.
113
+ export function getLock(name: string, options?: { ttl?: number }): Lock {
114
+ return new Lock(name, options);
115
+ }
@@ -0,0 +1,56 @@
1
+ // Lightweight mime-types shim — avoids bundling the full 295KB mime-db
2
+ // Only includes types actually used in payment-kit API routes
3
+
4
+ const types: Record<string, string> = {
5
+ '.html': 'text/html',
6
+ '.htm': 'text/html',
7
+ '.json': 'application/json',
8
+ '.js': 'application/javascript',
9
+ '.mjs': 'application/javascript',
10
+ '.css': 'text/css',
11
+ '.txt': 'text/plain',
12
+ '.xml': 'application/xml',
13
+ '.png': 'image/png',
14
+ '.jpg': 'image/jpeg',
15
+ '.jpeg': 'image/jpeg',
16
+ '.gif': 'image/gif',
17
+ '.svg': 'image/svg+xml',
18
+ '.webp': 'image/webp',
19
+ '.ico': 'image/x-icon',
20
+ '.woff': 'font/woff',
21
+ '.woff2': 'font/woff2',
22
+ '.pdf': 'application/pdf',
23
+ '.zip': 'application/zip',
24
+ '.csv': 'text/csv',
25
+ '.mp4': 'video/mp4',
26
+ '.webm': 'video/webm',
27
+ '.mp3': 'audio/mpeg',
28
+ '.wav': 'audio/wav',
29
+ '.bin': 'application/octet-stream',
30
+ };
31
+
32
+ const extensions: Record<string, string> = {};
33
+ for (const [ext, mime] of Object.entries(types)) {
34
+ extensions[mime] = ext;
35
+ }
36
+
37
+ export function lookup(path: string): string | false {
38
+ const ext = '.' + path.split('.').pop()?.toLowerCase();
39
+ return types[ext] || false;
40
+ }
41
+
42
+ export function contentType(typeOrPath: string): string | false {
43
+ const mime = typeOrPath.includes('/') ? typeOrPath : lookup(typeOrPath);
44
+ if (!mime) return false;
45
+ if (mime.startsWith('text/')) return `${mime}; charset=utf-8`;
46
+ return mime;
47
+ }
48
+
49
+ export function extension(mime: string): string | false {
50
+ return extensions[mime]?.slice(1) || false;
51
+ }
52
+
53
+ export const charset = (_mime: string) => 'UTF-8';
54
+ export const charsets = { lookup: charset };
55
+
56
+ export default { lookup, contentType, extension, charset, charsets, types };
@@ -0,0 +1,9 @@
1
+ // NeDB → D1 storage shim for DID Connect sessions
2
+ export default class AuthStorage {
3
+ constructor(_opts?: any) {}
4
+ create(_id: string, _data: any) { return Promise.resolve(); }
5
+ read(_id: string) { return Promise.resolve(null); }
6
+ update(_id: string, _data: any) { return Promise.resolve(); }
7
+ delete(_id: string) { return Promise.resolve(); }
8
+ on(_event: string, _fn: Function) {}
9
+ }
@@ -0,0 +1,9 @@
1
+ // child_process shim
2
+ export function exec(_cmd: any, _opts: any, cb?: any) {
3
+ if (typeof _opts === 'function') { cb = _opts; }
4
+ if (cb) cb(new Error('child_process not available in CF Workers'));
5
+ }
6
+ export function execSync() { throw new Error('child_process not available in CF Workers'); }
7
+ export function spawn() { throw new Error('child_process not available in CF Workers'); }
8
+ export function fork() { throw new Error('child_process not available in CF Workers'); }
9
+ export default { exec, execSync, spawn, fork };