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.
- package/__blocklet__.js +37 -0
- package/api/ocap-1.30-subpath-shims.d.ts +35 -0
- package/api/src/crons/index.ts +10 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- package/api/src/integrations/arcblock/nft.ts +6 -2
- package/api/src/integrations/arcblock/stake.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +4 -4
- package/api/src/integrations/blocklet/notification.ts +1 -1
- package/api/src/integrations/ethereum/tx.ts +29 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
- package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
- package/api/src/integrations/stripe/resource.ts +8 -0
- package/api/src/libs/audit.ts +32 -16
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -0
- package/api/src/libs/error.ts +15 -0
- package/api/src/libs/event.ts +42 -1
- package/api/src/libs/invoice.ts +69 -34
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/pagination.ts +14 -9
- package/api/src/libs/payment.ts +25 -10
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +16 -15
- package/api/src/libs/wallet-migration.ts +72 -53
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/credit-consume.ts +94 -12
- package/api/src/queues/credit-grant.ts +4 -0
- package/api/src/queues/event.ts +14 -2
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +83 -15
- package/api/src/queues/refund.ts +84 -71
- package/api/src/queues/subscription.ts +1 -0
- package/api/src/routes/checkout-sessions.ts +82 -43
- package/api/src/routes/connect/change-payment.ts +2 -0
- package/api/src/routes/connect/change-plan.ts +2 -0
- package/api/src/routes/connect/pay.ts +12 -3
- package/api/src/routes/connect/setup.ts +3 -1
- package/api/src/routes/connect/shared.ts +52 -39
- package/api/src/routes/connect/subscribe.ts +4 -1
- package/api/src/routes/credit-grants.ts +25 -17
- package/api/src/routes/donations.ts +2 -2
- package/api/src/routes/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +1 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/models/customer.ts +23 -1
- package/api/src/store/models/payment-method.ts +4 -0
- package/api/src/store/models/price.ts +23 -14
- package/api/tests/libs/wallet-migration.spec.ts +4 -4
- package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
- package/api/tests/queues/credit-consume.spec.ts +8 -4
- package/api/tests/routes/credit-grants.spec.ts +1 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
- package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
- package/cloudflare/README.md +499 -0
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
- package/cloudflare/build.ts +151 -0
- package/cloudflare/did-connect-auth.ts +527 -0
- package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
- package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
- package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
- package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
- package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
- package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
- package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
- package/cloudflare/frontend-shims/js-sdk.ts +43 -0
- package/cloudflare/frontend-shims/mime-types.ts +46 -0
- package/cloudflare/frontend-shims/session.ts +24 -0
- package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
- package/cloudflare/index.html +40 -0
- package/cloudflare/migrate-to-d1.js +252 -0
- package/cloudflare/migrations/0001_initial_schema.sql +82 -0
- package/cloudflare/migrations/0002_indexes.sql +75 -0
- package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
- package/cloudflare/run-build.js +390 -0
- package/cloudflare/scripts/test-decrypt.js +102 -0
- package/cloudflare/shims/arcblock-ws.ts +20 -0
- package/cloudflare/shims/axios-http-adapter.ts +4 -0
- package/cloudflare/shims/axios-lite.ts +117 -0
- package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
- package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
- package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
- package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
- package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
- package/cloudflare/shims/cookie-parser.ts +3 -0
- package/cloudflare/shims/cors.ts +21 -0
- package/cloudflare/shims/cron.ts +189 -0
- package/cloudflare/shims/crypto-js-warn.ts +7 -0
- package/cloudflare/shims/did-space-js.ts +17 -0
- package/cloudflare/shims/did-space.ts +11 -0
- package/cloudflare/shims/error.ts +18 -0
- package/cloudflare/shims/express-compat/index.ts +80 -0
- package/cloudflare/shims/express-compat/types.ts +41 -0
- package/cloudflare/shims/fastq.ts +105 -0
- package/cloudflare/shims/lock.ts +115 -0
- package/cloudflare/shims/mime-types.ts +56 -0
- package/cloudflare/shims/nedb-storage.ts +9 -0
- package/cloudflare/shims/node-child-process.ts +9 -0
- package/cloudflare/shims/node-fs.ts +20 -0
- package/cloudflare/shims/node-http.ts +13 -0
- package/cloudflare/shims/node-https.ts +4 -0
- package/cloudflare/shims/node-misc.ts +15 -0
- package/cloudflare/shims/node-net.ts +8 -0
- package/cloudflare/shims/node-os.ts +14 -0
- package/cloudflare/shims/node-tty.ts +8 -0
- package/cloudflare/shims/node-zlib.ts +17 -0
- package/cloudflare/shims/noop.ts +26 -0
- package/cloudflare/shims/payment-vendor.ts +14 -0
- package/cloudflare/shims/querystring.ts +12 -0
- package/cloudflare/shims/queue.ts +585 -0
- package/cloudflare/shims/rolldown-runtime.ts +43 -0
- package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
- package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
- package/cloudflare/shims/sequelize-d1/index.ts +34 -0
- package/cloudflare/shims/sequelize-d1/model.ts +1157 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +293 -0
- package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
- package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
- package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
- package/cloudflare/shims/sequelize-d1/types.ts +35 -0
- package/cloudflare/shims/stripe-cf.ts +29 -0
- package/cloudflare/shims/ws-lite.ts +103 -0
- package/cloudflare/shims/xss.ts +3 -0
- package/cloudflare/tests/shims/cron.spec.ts +210 -0
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1553 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +69 -0
- package/cloudflare/wrangler.staging.json +66 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +26 -22
- package/src/app.tsx +62 -4
- package/src/components/customer/link.tsx +9 -13
- package/src/components/customer/notification-preference.tsx +3 -2
- package/src/components/filter-toolbar.tsx +4 -0
- package/src/components/invoice/list.tsx +9 -1
- package/src/components/invoice-pdf/utils.ts +2 -1
- package/src/components/layout/admin.tsx +39 -5
- package/src/components/layout/user-cf.tsx +77 -0
- package/src/components/payment-intent/actions.tsx +23 -3
- package/src/components/safe-did-address.tsx +75 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/customers/customers/detail.tsx +2 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/tsconfig.api.json +1 -6
- package/tsconfig.json +3 -4
- package/tsconfig.types.json +2 -1
- package/vite.config.ts +6 -1
|
@@ -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 };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// node:fs shim — CF Workers have no filesystem
|
|
2
|
+
export function readFileSync() { return ''; }
|
|
3
|
+
export function writeFileSync() {}
|
|
4
|
+
export function existsSync() { return false; }
|
|
5
|
+
export function mkdirSync() {}
|
|
6
|
+
export function readdirSync() { return []; }
|
|
7
|
+
export function statSync() { return { isFile: () => false, isDirectory: () => false }; }
|
|
8
|
+
export function unlinkSync() {}
|
|
9
|
+
export const promises = {
|
|
10
|
+
readFile: async () => '',
|
|
11
|
+
writeFile: async () => {},
|
|
12
|
+
mkdir: async () => {},
|
|
13
|
+
readdir: async () => [],
|
|
14
|
+
stat: async () => ({ isFile: () => false, isDirectory: () => false }),
|
|
15
|
+
unlink: async () => {},
|
|
16
|
+
access: async () => { throw new Error('ENOENT'); },
|
|
17
|
+
};
|
|
18
|
+
export function createReadStream() { throw new Error('fs not available in CF Workers'); }
|
|
19
|
+
export function createWriteStream() { throw new Error('fs not available in CF Workers'); }
|
|
20
|
+
export default { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, promises, createReadStream, createWriteStream };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// node:http shim — Stripe SDK and axios have conditional http/https usage
|
|
2
|
+
// In CF Workers they use fetch instead, so these are never actually called
|
|
3
|
+
export class IncomingMessage {}
|
|
4
|
+
export class ServerResponse {}
|
|
5
|
+
export class Server { listen() {} close() {} }
|
|
6
|
+
export function createServer() { return new Server(); }
|
|
7
|
+
export function request() { throw new Error('node:http not available in CF Workers'); }
|
|
8
|
+
export function get() { throw new Error('node:http not available in CF Workers'); }
|
|
9
|
+
export const METHODS = ['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'];
|
|
10
|
+
export const STATUS_CODES = { 200: 'OK', 404: 'Not Found', 500: 'Internal Server Error' };
|
|
11
|
+
export const Agent = class {};
|
|
12
|
+
export const globalAgent = new Agent();
|
|
13
|
+
export default { IncomingMessage, ServerResponse, Server, createServer, request, get, METHODS, STATUS_CODES, Agent, globalAgent };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Catch-all shim for node modules that CF Workers doesn't support
|
|
2
|
+
// tls, cluster, zlib, bufferutil
|
|
3
|
+
|
|
4
|
+
// tls
|
|
5
|
+
export const TLSSocket = class { on() { return this; } };
|
|
6
|
+
export const connect = () => new TLSSocket();
|
|
7
|
+
export const createServer = () => ({ listen() {}, close() {} });
|
|
8
|
+
|
|
9
|
+
// cluster
|
|
10
|
+
export const isMaster = true;
|
|
11
|
+
export const isPrimary = true;
|
|
12
|
+
export const isWorker = false;
|
|
13
|
+
export const workers = {};
|
|
14
|
+
|
|
15
|
+
export default {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// node:net shim
|
|
2
|
+
export class Socket { on() { return this; } write() {} end() {} destroy() {} connect() { return this; } }
|
|
3
|
+
export class Server { listen() {} close() {} address() { return null; } }
|
|
4
|
+
export function createServer() { return new Server(); }
|
|
5
|
+
export function createConnection() { return new Socket(); }
|
|
6
|
+
export function connect() { return new Socket(); }
|
|
7
|
+
export function isIP() { return 0; }
|
|
8
|
+
export default { Socket, Server, createServer, createConnection, connect, isIP };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// node:os shim for CF Workers
|
|
2
|
+
export function homedir() { return '/tmp'; }
|
|
3
|
+
export function hostname() { return 'cf-worker'; }
|
|
4
|
+
export function platform() { return 'linux'; }
|
|
5
|
+
export function arch() { return 'x64'; }
|
|
6
|
+
export function tmpdir() { return '/tmp'; }
|
|
7
|
+
export function type() { return 'Linux'; }
|
|
8
|
+
export function release() { return '0.0.0'; }
|
|
9
|
+
export function cpus() { return []; }
|
|
10
|
+
export function totalmem() { return 0; }
|
|
11
|
+
export function freemem() { return 0; }
|
|
12
|
+
export function networkInterfaces() { return {}; }
|
|
13
|
+
export const EOL = '\n';
|
|
14
|
+
export default { homedir, hostname, platform, arch, tmpdir, type, release, cpus, totalmem, freemem, networkInterfaces, EOL };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// node:zlib shim — constants needed by axios
|
|
2
|
+
export const Z_SYNC_FLUSH = 2;
|
|
3
|
+
export const Z_NO_FLUSH = 0;
|
|
4
|
+
export const Z_PARTIAL_FLUSH = 1;
|
|
5
|
+
export const Z_FULL_FLUSH = 3;
|
|
6
|
+
export const Z_FINISH = 4;
|
|
7
|
+
export const Z_DEFAULT_COMPRESSION = -1;
|
|
8
|
+
export const constants = { Z_SYNC_FLUSH, Z_NO_FLUSH, Z_PARTIAL_FLUSH, Z_FULL_FLUSH, Z_FINISH, Z_DEFAULT_COMPRESSION };
|
|
9
|
+
export function createGzip() { throw new Error('zlib not available in CF Workers'); }
|
|
10
|
+
export function createGunzip() { throw new Error('zlib not available in CF Workers'); }
|
|
11
|
+
export function createUnzip() { throw new Error('zlib not available in CF Workers'); }
|
|
12
|
+
export function createBrotliDecompress() { throw new Error('zlib not available in CF Workers'); }
|
|
13
|
+
export function gzipSync() { throw new Error('zlib not available in CF Workers'); }
|
|
14
|
+
export function gunzipSync() { throw new Error('zlib not available in CF Workers'); }
|
|
15
|
+
export function deflateSync() { throw new Error('zlib not available in CF Workers'); }
|
|
16
|
+
export function inflateSync() { throw new Error('zlib not available in CF Workers'); }
|
|
17
|
+
export default { Z_SYNC_FLUSH, Z_NO_FLUSH, Z_PARTIAL_FLUSH, Z_FULL_FLUSH, Z_FINISH, Z_DEFAULT_COMPRESSION, constants, createGzip, createGunzip, createUnzip, createBrotliDecompress, gzipSync, gunzipSync, deflateSync, inflateSync };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// noop shim — replaces modules not needed in CF Workers
|
|
2
|
+
export const createNamespace = (_name?: any) => ({
|
|
3
|
+
bind: (fn: any) => fn,
|
|
4
|
+
run: (fn: any) => fn(),
|
|
5
|
+
get: () => undefined,
|
|
6
|
+
set: () => {},
|
|
7
|
+
createContext: () => ({}),
|
|
8
|
+
enter: () => {},
|
|
9
|
+
exit: () => {},
|
|
10
|
+
active: null,
|
|
11
|
+
});
|
|
12
|
+
export const getNamespace = () => null;
|
|
13
|
+
export const destroyNamespace = () => {};
|
|
14
|
+
export class Client {
|
|
15
|
+
constructor(..._args: any[]) {}
|
|
16
|
+
connect() {
|
|
17
|
+
return Promise.resolve();
|
|
18
|
+
}
|
|
19
|
+
query() {
|
|
20
|
+
return Promise.resolve({ rows: [] });
|
|
21
|
+
}
|
|
22
|
+
end() {
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export default { createNamespace, getNamespace, destroyNamespace, Client };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @blocklet/payment-vendor shim
|
|
2
|
+
export class Auth {
|
|
3
|
+
constructor(_opts?: any) {}
|
|
4
|
+
verify(_req: any) { return true; }
|
|
5
|
+
}
|
|
6
|
+
export const VendorAuth = Auth;
|
|
7
|
+
|
|
8
|
+
const noopMiddleware = (_req: any, _res: any, next: any) => next();
|
|
9
|
+
|
|
10
|
+
export const middleware = {
|
|
11
|
+
ensureVendorAuth: (_verifyFn?: any) => noopMiddleware,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default { Auth, VendorAuth, middleware };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Minimal querystring shim for CF Workers
|
|
2
|
+
// Only implements stringify() which is what @arcblock/did-connect-js uses
|
|
3
|
+
|
|
4
|
+
export function stringify(obj: Record<string, any>): string {
|
|
5
|
+
return new URLSearchParams(obj).toString();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parse(str: string): Record<string, string> {
|
|
9
|
+
return Object.fromEntries(new URLSearchParams(str).entries());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default { stringify, parse };
|