payment-kit 1.27.1 → 1.28.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 (184) 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 +10 -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/integrations/arcblock/nft.ts +6 -2
  7. package/api/src/integrations/arcblock/stake.ts +3 -2
  8. package/api/src/integrations/arcblock/token.ts +4 -4
  9. package/api/src/integrations/blocklet/notification.ts +1 -1
  10. package/api/src/integrations/ethereum/tx.ts +29 -0
  11. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  12. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  13. package/api/src/integrations/stripe/resource.ts +8 -0
  14. package/api/src/libs/audit.ts +32 -16
  15. package/api/src/libs/auth.ts +49 -2
  16. package/api/src/libs/chain-error.ts +31 -0
  17. package/api/src/libs/error.ts +15 -0
  18. package/api/src/libs/event.ts +42 -1
  19. package/api/src/libs/invoice.ts +69 -34
  20. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  21. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  22. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  23. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  24. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  25. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  26. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  27. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  28. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  29. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  30. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  31. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  32. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  33. package/api/src/libs/pagination.ts +14 -9
  34. package/api/src/libs/payment.ts +25 -10
  35. package/api/src/libs/session.ts +1 -1
  36. package/api/src/libs/timing.ts +35 -0
  37. package/api/src/libs/util.ts +16 -15
  38. package/api/src/libs/wallet-migration.ts +72 -53
  39. package/api/src/queues/auto-recharge.ts +1 -1
  40. package/api/src/queues/credit-consume.ts +94 -12
  41. package/api/src/queues/credit-grant.ts +4 -0
  42. package/api/src/queues/event.ts +14 -2
  43. package/api/src/queues/invoice.ts +1 -0
  44. package/api/src/queues/payment.ts +83 -15
  45. package/api/src/queues/refund.ts +84 -71
  46. package/api/src/queues/subscription.ts +1 -0
  47. package/api/src/routes/checkout-sessions.ts +82 -43
  48. package/api/src/routes/connect/change-payment.ts +2 -0
  49. package/api/src/routes/connect/change-plan.ts +2 -0
  50. package/api/src/routes/connect/pay.ts +12 -3
  51. package/api/src/routes/connect/setup.ts +3 -1
  52. package/api/src/routes/connect/shared.ts +52 -39
  53. package/api/src/routes/connect/subscribe.ts +4 -1
  54. package/api/src/routes/credit-grants.ts +25 -17
  55. package/api/src/routes/donations.ts +2 -2
  56. package/api/src/routes/meter-events.ts +16 -6
  57. package/api/src/routes/payment-links.ts +1 -1
  58. package/api/src/routes/payment-methods.ts +1 -1
  59. package/api/src/routes/settings.ts +1 -1
  60. package/api/src/routes/tax-rates.ts +1 -1
  61. package/api/src/store/models/customer.ts +23 -1
  62. package/api/src/store/models/payment-method.ts +4 -0
  63. package/api/src/store/models/price.ts +23 -14
  64. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  65. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  66. package/api/tests/queues/credit-consume.spec.ts +8 -4
  67. package/api/tests/routes/credit-grants.spec.ts +1 -0
  68. package/blocklet.yml +1 -1
  69. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  70. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  71. package/cloudflare/README.md +499 -0
  72. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  73. package/cloudflare/build.ts +151 -0
  74. package/cloudflare/did-connect-auth.ts +527 -0
  75. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  76. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  77. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  78. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  79. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  80. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  81. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  82. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  83. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  84. package/cloudflare/frontend-shims/session.ts +24 -0
  85. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  86. package/cloudflare/index.html +40 -0
  87. package/cloudflare/migrate-to-d1.js +252 -0
  88. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  89. package/cloudflare/migrations/0002_indexes.sql +75 -0
  90. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  91. package/cloudflare/run-build.js +390 -0
  92. package/cloudflare/scripts/test-decrypt.js +102 -0
  93. package/cloudflare/shims/arcblock-ws.ts +20 -0
  94. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  95. package/cloudflare/shims/axios-lite.ts +117 -0
  96. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  97. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  98. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  99. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  100. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  101. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  102. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  103. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  104. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  105. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  106. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  107. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  108. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  109. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  110. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  111. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  112. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  113. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  114. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  115. package/cloudflare/shims/cookie-parser.ts +3 -0
  116. package/cloudflare/shims/cors.ts +21 -0
  117. package/cloudflare/shims/cron.ts +189 -0
  118. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  119. package/cloudflare/shims/did-space-js.ts +17 -0
  120. package/cloudflare/shims/did-space.ts +11 -0
  121. package/cloudflare/shims/error.ts +18 -0
  122. package/cloudflare/shims/express-compat/index.ts +80 -0
  123. package/cloudflare/shims/express-compat/types.ts +41 -0
  124. package/cloudflare/shims/fastq.ts +105 -0
  125. package/cloudflare/shims/lock.ts +115 -0
  126. package/cloudflare/shims/mime-types.ts +56 -0
  127. package/cloudflare/shims/nedb-storage.ts +9 -0
  128. package/cloudflare/shims/node-child-process.ts +9 -0
  129. package/cloudflare/shims/node-fs.ts +20 -0
  130. package/cloudflare/shims/node-http.ts +13 -0
  131. package/cloudflare/shims/node-https.ts +4 -0
  132. package/cloudflare/shims/node-misc.ts +15 -0
  133. package/cloudflare/shims/node-net.ts +8 -0
  134. package/cloudflare/shims/node-os.ts +14 -0
  135. package/cloudflare/shims/node-tty.ts +8 -0
  136. package/cloudflare/shims/node-zlib.ts +17 -0
  137. package/cloudflare/shims/noop.ts +26 -0
  138. package/cloudflare/shims/payment-vendor.ts +14 -0
  139. package/cloudflare/shims/querystring.ts +12 -0
  140. package/cloudflare/shims/queue.ts +585 -0
  141. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  142. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  143. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  144. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  145. package/cloudflare/shims/sequelize-d1/model.ts +1157 -0
  146. package/cloudflare/shims/sequelize-d1/operators.ts +293 -0
  147. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  148. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  149. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  150. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  151. package/cloudflare/shims/stripe-cf.ts +29 -0
  152. package/cloudflare/shims/ws-lite.ts +103 -0
  153. package/cloudflare/shims/xss.ts +3 -0
  154. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  155. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  156. package/cloudflare/vite.config.ts +162 -0
  157. package/cloudflare/worker.ts +1553 -0
  158. package/cloudflare/wrangler.json +63 -0
  159. package/cloudflare/wrangler.jsonc +69 -0
  160. package/cloudflare/wrangler.staging.json +66 -0
  161. package/cloudflare/wrangler.toml +28 -0
  162. package/jest.config.js +4 -12
  163. package/package.json +26 -22
  164. package/src/app.tsx +62 -4
  165. package/src/components/customer/link.tsx +9 -13
  166. package/src/components/customer/notification-preference.tsx +3 -2
  167. package/src/components/filter-toolbar.tsx +4 -0
  168. package/src/components/invoice/list.tsx +9 -1
  169. package/src/components/invoice-pdf/utils.ts +2 -1
  170. package/src/components/layout/admin.tsx +39 -5
  171. package/src/components/layout/user-cf.tsx +77 -0
  172. package/src/components/payment-intent/actions.tsx +23 -3
  173. package/src/components/safe-did-address.tsx +75 -0
  174. package/src/libs/patch-user-card.ts +25 -0
  175. package/src/libs/util.ts +5 -7
  176. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  177. package/src/pages/admin/customers/customers/detail.tsx +2 -2
  178. package/src/pages/admin/customers/customers/index.tsx +2 -2
  179. package/src/pages/admin/overview.tsx +3 -1
  180. package/src/pages/customer/subscription/detail.tsx +4 -4
  181. package/tsconfig.api.json +1 -6
  182. package/tsconfig.json +3 -4
  183. package/tsconfig.types.json +2 -1
  184. package/vite.config.ts +6 -1
@@ -0,0 +1,33 @@
1
+ export class BlockletService {
2
+ async getVault() {
3
+ return { address: (globalThis as any).__CF_ENV__?.VAULT_ADDRESS || '' };
4
+ }
5
+ async getBlocklet() {
6
+ return { id: '', site: { id: '' } };
7
+ }
8
+ async updateRoutingRule() {}
9
+ async addRoutingRule() {}
10
+ async getPermissionsByRole(_role: string) {
11
+ return [];
12
+ }
13
+ async getRoles() {
14
+ return [];
15
+ }
16
+ async getUsers(_params?: any) {
17
+ return [];
18
+ }
19
+ async getUser(did: string, _opts?: any) {
20
+ // In CF Workers, the DID from DID Connect IS the wallet DID.
21
+ // Return it as a connectedAccount so getWalletDid(user) works correctly.
22
+ return {
23
+ user: {
24
+ did,
25
+ fullName: did,
26
+ email: '',
27
+ phone: '',
28
+ remark: '',
29
+ connectedAccounts: [{ provider: 'wallet', did }],
30
+ },
31
+ };
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ export function cdn() {
2
+ return (_req: any, _res: any, next: any) => next();
3
+ }
@@ -0,0 +1,35 @@
1
+ // componentApi shim — HTTP client with signature headers
2
+ // Mirrors the subset of axios API used by payment-kit (post, get, request)
3
+ export const componentApi = {
4
+ post: async (url: string, data: any, _opts?: any) => {
5
+ const res = await fetch(url, {
6
+ method: 'POST',
7
+ headers: { 'Content-Type': 'application/json' },
8
+ body: JSON.stringify(data),
9
+ });
10
+ return { data: await res.json() };
11
+ },
12
+ get: async (url: string, _opts?: any) => {
13
+ const res = await fetch(url);
14
+ return { data: await res.json() };
15
+ },
16
+ // Used by queues/webhook.ts for webhook delivery
17
+ request: async (config: { url: string; method?: string; data?: any; headers?: Record<string, string>; timeout?: number }) => {
18
+ const controller = new AbortController();
19
+ const timeoutId = config.timeout ? setTimeout(() => controller.abort(), config.timeout) : null;
20
+ try {
21
+ const res = await fetch(config.url, {
22
+ method: config.method || 'POST',
23
+ headers: { 'Content-Type': 'application/json', ...config.headers },
24
+ body: config.data ? JSON.stringify(config.data) : undefined,
25
+ signal: controller.signal,
26
+ });
27
+ const data = await res.json().catch(() => null);
28
+ return { data, status: res.status, headers: Object.fromEntries(res.headers.entries()) };
29
+ } finally {
30
+ if (timeoutId) clearTimeout(timeoutId);
31
+ }
32
+ },
33
+ };
34
+
35
+ export default componentApi;
@@ -0,0 +1,18 @@
1
+ // @blocklet/sdk/lib/component shim
2
+ function _getUrl(path?: string) {
3
+ const base = (globalThis as any).__CF_ENV__?.APP_URL || '';
4
+ if (!path) return base;
5
+ // In CF Workers there is no blocklet prefix — assets are served from root
6
+ return `${base}${path.startsWith('/') ? path : '/' + path}`;
7
+ }
8
+
9
+ const component = {
10
+ getUrl: _getUrl,
11
+ };
12
+
13
+ export default component;
14
+ export function getUrl(path?: string) {
15
+ return _getUrl(path);
16
+ }
17
+ export function getResources() { return []; }
18
+ export function getPackResources() { return []; }
@@ -0,0 +1,8 @@
1
+ import { env } from './env';
2
+
3
+ export { env };
4
+ export const Events = {};
5
+ export const events = { on: () => {}, emit: () => {} };
6
+
7
+ const config = { env, Events, events };
8
+ export default config;
@@ -0,0 +1,14 @@
1
+ // In Blocklet Server, getWalletDid(user) extracts wallet DID from user.connectedAccounts.
2
+ // In CF Workers, the user's DID from DID Connect IS the wallet DID (wallet signed directly).
3
+ export function getWalletDid(user?: any) {
4
+ if (user) {
5
+ // If user has connectedAccounts (from enriched getUser), use it
6
+ if (user.connectedAccounts) {
7
+ const walletAccount = user.connectedAccounts.find((a: any) => a.provider === 'wallet');
8
+ if (walletAccount) return walletAccount.did;
9
+ }
10
+ // In CF Workers, user.did IS the wallet DID (signed via DID Connect)
11
+ if (user.did) return user.did;
12
+ }
13
+ return (globalThis as any).__CF_ENV__?.WALLET_DID || '';
14
+ }
@@ -0,0 +1,12 @@
1
+ // @blocklet/sdk/lib/env shim
2
+ const env = {
3
+ get appPid() { return (globalThis as any).__CF_ENV__?.APP_PID || ''; },
4
+ get appName() { return (globalThis as any).__CF_ENV__?.APP_NAME || 'Payment Kit'; },
5
+ get appUrl() { return (globalThis as any).__CF_ENV__?.APP_URL || ''; },
6
+ get componentDid() { return (globalThis as any).__CF_ENV__?.COMPONENT_DID || ''; },
7
+ get dataDir() { return '/tmp'; },
8
+ get appId() { return (globalThis as any).__CF_ENV__?.APP_ID || ''; },
9
+ };
10
+
11
+ export { env };
12
+ export default env;
@@ -0,0 +1,3 @@
1
+ export async function publish(_event: string, _data: any) {
2
+ // TODO: CF Queues or HTTP POST
3
+ }
@@ -0,0 +1,3 @@
1
+ export function fallback(_file: string, _opts?: any) {
2
+ return (_req: any, _res: any, next: any) => next();
3
+ }
@@ -0,0 +1,11 @@
1
+ // @blocklet/sdk main entry shim
2
+ export { env } from './env';
3
+ export { Notification } from './notification';
4
+
5
+ export const component = {
6
+ getUrl: (..._args: any[]) => (globalThis as any).__CF_ENV__?.APP_URL || '',
7
+ };
8
+
9
+ export function getUrl(..._args: any[]) {
10
+ return (globalThis as any).__CF_ENV__?.APP_URL || '';
11
+ }
@@ -0,0 +1,11 @@
1
+ // @blocklet/logger shim
2
+ function createLogger(_name) {
3
+ return {
4
+ info: function() { console.log.apply(console, ['[INFO]'].concat(Array.from(arguments))); },
5
+ warn: function() { console.warn.apply(console, ['[WARN]'].concat(Array.from(arguments))); },
6
+ error: function() { console.error.apply(console, ['[ERROR]'].concat(Array.from(arguments))); },
7
+ debug: function() {},
8
+ };
9
+ }
10
+
11
+ module.exports = createLogger;
@@ -0,0 +1,15 @@
1
+ // @blocklet/sdk/lib/middlewares shim
2
+ export function csrf() {
3
+ return (_req: any, _res: any, next: any) => next();
4
+ }
5
+
6
+ export function auth(options?: { roles?: string[] }) {
7
+ return (req: any, res: any, next: any) => {
8
+ if (!options?.roles) return next();
9
+ if (!req.user?.did) return res.status(401).json({ error: 'Login required' });
10
+ if (!options.roles.includes(req.user.role)) {
11
+ return res.status(403).json({ error: 'Insufficient permissions' });
12
+ }
13
+ next();
14
+ };
15
+ }
@@ -0,0 +1,11 @@
1
+ export async function sendToRelay(_channel: string, _event: string, _data: any) {
2
+ // TODO: HTTP POST to notification service
3
+ }
4
+
5
+ export class Notification {
6
+ static async sendToUser(_did: string, _notification: any, _opts?: any) {
7
+ // TODO: HTTP POST to notification service
8
+ }
9
+ }
10
+
11
+ export default Notification;
@@ -0,0 +1,53 @@
1
+ // @blocklet/sdk/lib/security shim for CF Workers
2
+ //
3
+ // Uses the same crypto chain as original Blocklet Server:
4
+ // password = PBKDF2(BLOCKLET_APP_EK, BLOCKLET_DID, 256, 32, sha512).hex()
5
+ // plaintext = CryptoJS.AES.decrypt(ciphertext, password)
6
+
7
+ import crypto from 'crypto';
8
+ import CryptoJS from 'crypto-js';
9
+
10
+ let _password: string | null = null;
11
+
12
+ /** Initialize with EK + DID. After this, encrypt/decrypt work identically to Blocklet Server. */
13
+ export function initSecurity(appEk: string, blockletDid: string): void {
14
+ _password = crypto.pbkdf2Sync(appEk, blockletDid, 256, 32, 'sha512').toString('hex');
15
+ }
16
+
17
+ export function encrypt(data: string, password?: string, salt?: string): string {
18
+ const pw = password || _password;
19
+ if (!pw) return data;
20
+ return CryptoJS.AES.encrypt(data, pw).toString();
21
+ }
22
+
23
+ export function decrypt(data: string, password?: string, salt?: string): string {
24
+ const pw = password || _password;
25
+ if (!pw || !data) return data;
26
+ return CryptoJS.AES.decrypt(data, pw).toString(CryptoJS.enc.Utf8);
27
+ }
28
+
29
+ /**
30
+ * Fetch EK from AUTH_SERVICE and initialize. Call once at worker startup.
31
+ */
32
+ export async function initFromAuthService(env: {
33
+ AUTH_SERVICE?: any;
34
+ APP_PID?: string;
35
+ }): Promise<void> {
36
+ const { AUTH_SERVICE, APP_PID } = env;
37
+ if (!AUTH_SERVICE || !APP_PID) return;
38
+
39
+ try {
40
+ const appEk = await AUTH_SERVICE.getAppEk(APP_PID);
41
+ if (!appEk) {
42
+ console.warn('[Security] No APP_EK found for instance', APP_PID);
43
+ return;
44
+ }
45
+ // BLOCKLET_DID = APP_PID in Blocklet Server
46
+ initSecurity(appEk, APP_PID);
47
+ console.log('[Security] Initialized with EK from AUTH_SERVICE');
48
+ } catch (err: any) {
49
+ console.error('[Security] initFromAuthService failed:', err?.message);
50
+ }
51
+ }
52
+
53
+ export default { encrypt, decrypt, initSecurity, initFromAuthService };
@@ -0,0 +1,8 @@
1
+ // @blocklet/sdk/lib/middlewares/session shim
2
+ // Auth is resolved in Hono middleware layer via AUTH_SERVICE RPC.
3
+ // req.user is already populated by mountExpressRoutes().
4
+ export default function sessionMiddleware(_options?: any) {
5
+ return (_req: any, _res: any, next: any) => next();
6
+ }
7
+
8
+ export { sessionMiddleware };
@@ -0,0 +1,38 @@
1
+ // @blocklet/sdk/lib/util/verify-sign shim for CF Workers
2
+ //
3
+ // In Blocklet Server, component.call uses ED25519 signatures (x-component-sig)
4
+ // to authenticate inter-component calls.
5
+ //
6
+ // In CF Workers, all authentication is delegated to AUTH_SERVICE (blocklet-service)
7
+ // via the Hono caller middleware. By the time security.ts runs, the caller identity
8
+ // is already resolved and injected into req.headers['x-user-did'] / req.user.
9
+ //
10
+ // This shim delegates to AUTH_SERVICE.resolveIdentity() using the request's
11
+ // existing credentials (Cookie JWT or Authorization header), instead of
12
+ // doing local ED25519 signature verification.
13
+
14
+ export function getVerifyData(req: any, type = 'component') {
15
+ const sig = req.get?.(`x-${type}-sig`) || req.headers?.[`x-${type}-sig`] || '';
16
+ return { sig, data: {}, sigVersion: '', sigPk: '' };
17
+ }
18
+
19
+ export async function verify(_data: any, _sig: any): Promise<boolean> {
20
+ // AUTH_SERVICE handles all authentication in CF Workers.
21
+ // The Hono caller middleware (worker.ts) already called AUTH_SERVICE.resolveIdentity()
22
+ // and injected the result into x-user-did / x-user-role headers before Express runs.
23
+ //
24
+ // If AUTH_SERVICE verified the caller → x-user-did is set → security.ts x-user-did path handles it.
25
+ // If AUTH_SERVICE couldn't verify → caller is null → this path should reject.
26
+ //
27
+ // We return false here so the component.call path in security.ts falls through
28
+ // to the x-user-did path, which uses the AUTH_SERVICE-resolved identity.
29
+ const env = (globalThis as any).__CF_ENV__;
30
+ if (env?.AUTH_SERVICE) {
31
+ // AUTH_SERVICE is available — don't trust local signature, let the x-user-did path handle auth
32
+ return false;
33
+ }
34
+ // No AUTH_SERVICE — cannot verify
35
+ return false;
36
+ }
37
+
38
+ export default { verify, getVerifyData };
@@ -0,0 +1,3 @@
1
+ export class WalletAuthenticator {
2
+ constructor(_opts?: any) {}
3
+ }
@@ -0,0 +1,6 @@
1
+ export class WalletHandlers {
2
+ constructor(_opts?: any) {}
3
+ attach(_opts: any) {
4
+ // TODO: implement DID Connect for CF
5
+ }
6
+ }
@@ -0,0 +1,103 @@
1
+ // @blocklet/sdk/lib/wallet shim
2
+ // Uses @ocap/wallet to create real wallets from APP_SK for on-chain operations
3
+ import * as Mcrypto from '@ocap/mcrypto';
4
+ import { fromSecretKey } from '@ocap/wallet';
5
+
6
+ const walletTypeArcblock = {
7
+ role: Mcrypto.types.RoleType.ROLE_APPLICATION,
8
+ pk: Mcrypto.types.KeyType.ED25519,
9
+ hash: Mcrypto.types.HashType.SHA3,
10
+ };
11
+
12
+ const walletTypeEthereum = {
13
+ role: Mcrypto.types.RoleType.ROLE_APPLICATION,
14
+ pk: Mcrypto.types.KeyType.SECP256K1,
15
+ hash: Mcrypto.types.HashType.KECCAK,
16
+ };
17
+
18
+ // Cache wallets per type so they are not re-created on every call
19
+ let _arcblockWallet: any = null;
20
+ let _ethereumWallet: any = null;
21
+ let _lastSK: string = '';
22
+
23
+ function getAppSK(): string {
24
+ return (globalThis as any).__CF_ENV__?.APP_SK || process.env.APP_SK || '';
25
+ }
26
+
27
+ function ensureWallet(type?: string): any {
28
+ const sk = getAppSK();
29
+ if (!sk) {
30
+ return null;
31
+ }
32
+
33
+ // If SK changed, invalidate cached wallets
34
+ if (sk !== _lastSK) {
35
+ _arcblockWallet = null;
36
+ _ethereumWallet = null;
37
+ _lastSK = sk;
38
+ }
39
+
40
+ try {
41
+ if (type === 'ethereum') {
42
+ if (!_ethereumWallet) {
43
+ // Ethereum wallet uses first 66 chars of SK (0x + 32 bytes hex)
44
+ // matching @blocklet/sdk behavior: appSk.slice(0, 66)
45
+ const ethSk = sk.slice(0, 66);
46
+ _ethereumWallet = fromSecretKey(ethSk, walletTypeEthereum);
47
+ }
48
+ return _ethereumWallet;
49
+ }
50
+
51
+ if (!_arcblockWallet) {
52
+ _arcblockWallet = fromSecretKey(sk, walletTypeArcblock);
53
+ }
54
+ return _arcblockWallet;
55
+ } catch (e: any) {
56
+ console.error(`[CF Worker] ensureWallet(${type}) failed:`, e?.message);
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Returns a wallet proxy that lazily initializes the real wallet on first property access.
63
+ * This is needed because auth.ts calls getWallet() at module init time, before __CF_ENV__
64
+ * is available. The proxy defers the real wallet creation to request time.
65
+ */
66
+ export function getWallet(type?: string, _appSk?: string, _keyType?: string): any {
67
+ return new Proxy(
68
+ {},
69
+ {
70
+ get(_target, prop) {
71
+ const realWallet = ensureWallet(type);
72
+ if (!realWallet) {
73
+ if (prop === 'address') return '';
74
+ if (prop === 'publicKey') return '';
75
+ if (prop === 'toJSON') return () => ({});
76
+ if (prop === 'sign') return async () => '';
77
+ if (prop === 'verify') return async () => true;
78
+ if (prop === 'signJWT') return async () => '';
79
+ if (prop === 'then') return undefined;
80
+ return undefined;
81
+ }
82
+ const value = realWallet[prop];
83
+ if (typeof value === 'function') {
84
+ return value.bind(realWallet);
85
+ }
86
+ return value;
87
+ },
88
+ has(_target, prop) {
89
+ const realWallet = ensureWallet(type);
90
+ if (!realWallet) return false;
91
+ return prop in realWallet;
92
+ },
93
+ }
94
+ );
95
+ }
96
+
97
+ // CF shim re-exports: on CF the runtime/permanent/access wallets all derive
98
+ // from the single APP_SK, so these just alias getWallet. Needed because
99
+ // payment-kit's api/src/libs/auth.ts imports { getAccessWallet } and the
100
+ // patch in that file calls getWallet(undefined, '', 'sk').
101
+ export const getAccessWallet = () => getWallet('arcblock');
102
+ export const getPermanentWallet = () => getWallet('arcblock');
103
+ export const getEthereumWallet = () => getWallet('ethereum');
@@ -0,0 +1,3 @@
1
+ export default function cookieParser() {
2
+ return (_req: any, _res: any, next: any) => next();
3
+ }
@@ -0,0 +1,21 @@
1
+ // Minimal cors shim for CF Workers
2
+ // Uses Object.assign pattern so both ESM and CJS interop work:
3
+ // - ESM: import cors from 'cors' -> cors is the function
4
+ // - CJS: const cors = require('cors') -> cors is the function (via __toCommonJS default)
5
+ function cors(_opts?: any) {
6
+ return (_req: any, res: any, next: any) => {
7
+ res?.set?.('Access-Control-Allow-Origin', '*');
8
+ res?.set?.('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS');
9
+ res?.set?.('Access-Control-Allow-Headers', 'Content-Type,Authorization,x-user-did,x-csrf-token');
10
+ if (_req?.method === 'OPTIONS') {
11
+ res?.status?.(204)?.end?.();
12
+ return;
13
+ }
14
+ next();
15
+ };
16
+ }
17
+
18
+ // @ts-ignore - module.exports for CJS compat
19
+ module.exports = cors;
20
+ // @ts-ignore
21
+ module.exports.default = cors;
@@ -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
+ }