payment-kit 1.27.2 → 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,103 @@
|
|
|
1
|
+
// Lightweight ws shim for CF Workers — uses native WebSocket
|
|
2
|
+
// Avoids bundling the full 128KB ws package (pulled by ethers)
|
|
3
|
+
|
|
4
|
+
class WebSocketShim {
|
|
5
|
+
static CONNECTING = 0;
|
|
6
|
+
static OPEN = 1;
|
|
7
|
+
static CLOSING = 2;
|
|
8
|
+
static CLOSED = 3;
|
|
9
|
+
|
|
10
|
+
CONNECTING = 0;
|
|
11
|
+
OPEN = 1;
|
|
12
|
+
CLOSING = 2;
|
|
13
|
+
CLOSED = 3;
|
|
14
|
+
|
|
15
|
+
_ws: WebSocket | null = null;
|
|
16
|
+
readyState = 0;
|
|
17
|
+
|
|
18
|
+
onopen: ((ev: any) => void) | null = null;
|
|
19
|
+
onclose: ((ev: any) => void) | null = null;
|
|
20
|
+
onmessage: ((ev: any) => void) | null = null;
|
|
21
|
+
onerror: ((ev: any) => void) | null = null;
|
|
22
|
+
|
|
23
|
+
constructor(url: string, protocols?: string | string[]) {
|
|
24
|
+
try {
|
|
25
|
+
this._ws = new WebSocket(url, protocols);
|
|
26
|
+
this._ws.addEventListener('open', (ev) => {
|
|
27
|
+
this.readyState = 1;
|
|
28
|
+
this.onopen?.(ev);
|
|
29
|
+
});
|
|
30
|
+
this._ws.addEventListener('close', (ev) => {
|
|
31
|
+
this.readyState = 3;
|
|
32
|
+
this.onclose?.(ev);
|
|
33
|
+
});
|
|
34
|
+
this._ws.addEventListener('message', (ev) => {
|
|
35
|
+
this.onmessage?.(ev);
|
|
36
|
+
});
|
|
37
|
+
this._ws.addEventListener('error', (ev) => {
|
|
38
|
+
this.onerror?.(ev);
|
|
39
|
+
});
|
|
40
|
+
} catch (e) {
|
|
41
|
+
this.readyState = 3;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
send(data: any) {
|
|
46
|
+
this._ws?.send(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
close(code?: number, reason?: string) {
|
|
50
|
+
this.readyState = 2;
|
|
51
|
+
this._ws?.close(code, reason);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
addEventListener(type: string, listener: any) {
|
|
55
|
+
this._ws?.addEventListener(type, listener);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
removeEventListener(type: string, listener: any) {
|
|
59
|
+
this._ws?.removeEventListener(type, listener);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
on(event: string, listener: any) {
|
|
63
|
+
if (event === 'open') this.onopen = listener;
|
|
64
|
+
else if (event === 'close') this.onclose = listener;
|
|
65
|
+
else if (event === 'message') this.onmessage = listener;
|
|
66
|
+
else if (event === 'error') this.onerror = listener;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
off(_event: string, _listener: any) {
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
once(event: string, listener: any) {
|
|
75
|
+
const wrapped = (...args: any[]) => {
|
|
76
|
+
this.off(event, wrapped);
|
|
77
|
+
listener(...args);
|
|
78
|
+
};
|
|
79
|
+
return this.on(event, wrapped);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
terminate() {
|
|
83
|
+
this.close();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ping() {}
|
|
87
|
+
pong() {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// WebSocket.Server stub — not used in CF Workers
|
|
91
|
+
class ServerStub {
|
|
92
|
+
constructor(_opts?: any) {}
|
|
93
|
+
on() { return this; }
|
|
94
|
+
close() {}
|
|
95
|
+
address() { return { port: 0 }; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
(WebSocketShim as any).Server = ServerStub;
|
|
99
|
+
(WebSocketShim as any).WebSocket = WebSocketShim;
|
|
100
|
+
(WebSocketShim as any).WebSocketServer = ServerStub;
|
|
101
|
+
(WebSocketShim as any).createWebSocketStream = () => { throw new Error('Not supported in CF Workers'); };
|
|
102
|
+
|
|
103
|
+
export = WebSocketShim;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import cronModule, { cronInstance, __test__ } from '../../shims/cron';
|
|
2
|
+
|
|
3
|
+
const { matchesCron, shouldRunInWindow } = __test__;
|
|
4
|
+
|
|
5
|
+
// Anchor date: 2026-04-17T10:00:00Z (a Friday, minute=0, hour=10)
|
|
6
|
+
const at = (hh: number, mm: number, dayOffset = 0) =>
|
|
7
|
+
new Date(Date.UTC(2026, 3, 17 + dayOffset, hh, mm, 0));
|
|
8
|
+
|
|
9
|
+
describe('matchesCron (reference correctness, not modified by this fix)', () => {
|
|
10
|
+
it.each([
|
|
11
|
+
['0 * * * * *', 10, 0, true],
|
|
12
|
+
['0 5 * * * *', 10, 5, true],
|
|
13
|
+
['0 5 * * * *', 10, 4, false],
|
|
14
|
+
['0 */5 * * * *', 10, 0, true],
|
|
15
|
+
['0 */5 * * * *', 10, 5, true],
|
|
16
|
+
['0 */5 * * * *', 10, 4, false],
|
|
17
|
+
['0 */5 * * * *', 10, 7, false],
|
|
18
|
+
['0 */10 * * * *', 10, 0, true],
|
|
19
|
+
['0 */10 * * * *', 10, 5, false],
|
|
20
|
+
['0 */10 * * * *', 10, 10, true],
|
|
21
|
+
['0 */30 * * * *', 10, 0, true],
|
|
22
|
+
['0 */30 * * * *', 10, 15, false],
|
|
23
|
+
['0 */30 * * * *', 10, 30, true],
|
|
24
|
+
['0 5 */6 * * *', 6, 5, true],
|
|
25
|
+
['0 5 */6 * * *', 6, 6, false],
|
|
26
|
+
['0 5 */6 * * *', 7, 5, false],
|
|
27
|
+
['0 5 */6 * * *', 12, 5, true],
|
|
28
|
+
['0 0 0 * * *', 0, 0, true],
|
|
29
|
+
['0 0 0 * * *', 0, 1, false],
|
|
30
|
+
['0 0 10 * * *', 10, 0, true],
|
|
31
|
+
['0 0 10 * * *', 10, 1, false],
|
|
32
|
+
])('matchesCron(%s) at %d:%d -> %s', (expr, h, m, expected) => {
|
|
33
|
+
expect(matchesCron(expr, at(h, m))).toBe(expected);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// These tests define the POST-FIX behavior: 1-minute window, not 5-minute.
|
|
38
|
+
// Before fix: shouldRunInWindow("0 */5 * * * *", h:m) is true for any m in [0..59].
|
|
39
|
+
// After fix: shouldRunInWindow is true only at the exact matching minute.
|
|
40
|
+
describe('shouldRunInWindow — 1 minute window (post-fix behavior)', () => {
|
|
41
|
+
describe('every-5-minute cron "0 */5 * * * *"', () => {
|
|
42
|
+
it('matches exactly at minute 0, 5, 10, ..., 55 within an hour (12 times)', () => {
|
|
43
|
+
const matches: number[] = [];
|
|
44
|
+
for (let m = 0; m < 60; m += 1) {
|
|
45
|
+
if (shouldRunInWindow('0 */5 * * * *', at(10, m))) matches.push(m);
|
|
46
|
+
}
|
|
47
|
+
expect(matches).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
|
|
48
|
+
expect(matches).toHaveLength(12);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does NOT match at minutes 1-4, 6-9, 11-14, ... (the bug we are fixing)', () => {
|
|
52
|
+
for (const m of [1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14]) {
|
|
53
|
+
expect(shouldRunInWindow('0 */5 * * * *', at(10, m))).toBe(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('every-10-minute cron "0 */10 * * * *"', () => {
|
|
59
|
+
it('matches exactly 6 times per hour (minute 0, 10, 20, 30, 40, 50)', () => {
|
|
60
|
+
const matches: number[] = [];
|
|
61
|
+
for (let m = 0; m < 60; m += 1) {
|
|
62
|
+
if (shouldRunInWindow('0 */10 * * * *', at(10, m))) matches.push(m);
|
|
63
|
+
}
|
|
64
|
+
expect(matches).toEqual([0, 10, 20, 30, 40, 50]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('every-30-minute cron "0 */30 * * * *"', () => {
|
|
69
|
+
it('matches exactly 2 times per hour', () => {
|
|
70
|
+
const matches: number[] = [];
|
|
71
|
+
for (let m = 0; m < 60; m += 1) {
|
|
72
|
+
if (shouldRunInWindow('0 */30 * * * *', at(10, m))) matches.push(m);
|
|
73
|
+
}
|
|
74
|
+
expect(matches).toEqual([0, 30]);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('every-minute cron "* * * * *"', () => {
|
|
79
|
+
it('matches every minute', () => {
|
|
80
|
+
for (let m = 0; m < 60; m += 1) {
|
|
81
|
+
expect(shouldRunInWindow('* * * * *', at(10, m))).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('single-minute cron "0 5 */6 * * *" (every 6 hours at minute 5)', () => {
|
|
87
|
+
it('matches exactly 4 times in a 24-hour window (hours 0, 6, 12, 18)', () => {
|
|
88
|
+
const matches: string[] = [];
|
|
89
|
+
for (let h = 0; h < 24; h += 1) {
|
|
90
|
+
for (let m = 0; m < 60; m += 1) {
|
|
91
|
+
if (shouldRunInWindow('0 5 */6 * * *', at(h, m))) matches.push(`${h}:${m}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
expect(matches).toEqual(['0:5', '6:5', '12:5', '18:5']);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('daily cron "0 1 0 * * *" (00:01 every day)', () => {
|
|
99
|
+
it('matches exactly once per day', () => {
|
|
100
|
+
const matches: string[] = [];
|
|
101
|
+
for (let h = 0; h < 24; h += 1) {
|
|
102
|
+
for (let m = 0; m < 60; m += 1) {
|
|
103
|
+
if (shouldRunInWindow('0 1 0 * * *', at(h, m))) matches.push(`${h}:${m}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
expect(matches).toEqual(['0:1']);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('5-field cron "*/5 * * * *" (no seconds column)', () => {
|
|
111
|
+
it('also supported — matches 12 times per hour', () => {
|
|
112
|
+
const matches: number[] = [];
|
|
113
|
+
for (let m = 0; m < 60; m += 1) {
|
|
114
|
+
if (shouldRunInWindow('*/5 * * * *', at(10, m))) matches.push(m);
|
|
115
|
+
}
|
|
116
|
+
expect(matches).toHaveLength(12);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Invariant check: non-5-aligned cron expressions must still be reachable when
|
|
122
|
+
// shim uses 1-minute window (post-fix). This guards against the review finding
|
|
123
|
+
// that if CF trigger is "*/5" while shim uses 1-min matching, jobs at minute 1
|
|
124
|
+
// (e.g. expiredSessionCleanupCronTime="0 1 * * * *") would never fire.
|
|
125
|
+
describe('non-5-aligned cron expressions (review P1-1 regression guard)', () => {
|
|
126
|
+
it.each([
|
|
127
|
+
['0 1 * * * *', 'expiredSessionCleanupCronTime (minute 1 every hour)', [1]],
|
|
128
|
+
['0 1 0 * * *', 'paymentStatCronTime (00:01 daily)', [1]],
|
|
129
|
+
['0 7 * * * *', 'hypothetical minute 7', [7]],
|
|
130
|
+
['0 13 * * * *', 'hypothetical minute 13', [13]],
|
|
131
|
+
])('matches %s (%s) at the expected minute', (expr, _label, expectedMinutes) => {
|
|
132
|
+
const matches: number[] = [];
|
|
133
|
+
for (let m = 0; m < 60; m += 1) {
|
|
134
|
+
// Check at hour 0 so the hour field "0" in '0 1 0 * * *' also aligns.
|
|
135
|
+
if (shouldRunInWindow(expr, at(0, m))) matches.push(m);
|
|
136
|
+
}
|
|
137
|
+
expect(matches).toEqual(expectedMinutes);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('regression guard — bug condition from 2026-04-17 incident', () => {
|
|
142
|
+
it('each minute in an hour produces the EXPECTED number of matches for payment-kit cron expressions', () => {
|
|
143
|
+
const expressions: Record<string, number> = {
|
|
144
|
+
'0 */5 * * * *': 12, // depositVault, revokeStake
|
|
145
|
+
'0 */10 * * * *': 6, // creditConsume, vendorStatusCheck, vendorReturnScan
|
|
146
|
+
'0 */20 * * * *': 3, // stripePayment
|
|
147
|
+
'0 */30 * * * *': 2, // subscription, stripeInvoice
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
for (const [expr, expected] of Object.entries(expressions)) {
|
|
151
|
+
let count = 0;
|
|
152
|
+
for (let m = 0; m < 60; m += 1) {
|
|
153
|
+
if (shouldRunInWindow(expr, at(10, m))) count += 1;
|
|
154
|
+
}
|
|
155
|
+
expect({ expr, count }).toEqual({ expr, count: expected });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// runAll(date) integration test — verifies the shim uses the passed date
|
|
161
|
+
// (scheduledTime from CF) rather than wall-clock at execution time. This
|
|
162
|
+
// addresses PR #1341 review P2-1: a scheduled event intended for 00:01 but
|
|
163
|
+
// delivered late at 00:02 must still match "0 1 * * * *" jobs.
|
|
164
|
+
describe('cronInstance.runAll(date) — scheduledTime integration (review P2-1)', () => {
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
// cronModule.init({...}) mutates the singleton registry; reset between cases
|
|
167
|
+
cronModule.init({ jobs: [] });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('passes the provided date to shouldRunInWindow, not wall clock', async () => {
|
|
171
|
+
const calls: string[] = [];
|
|
172
|
+
cronModule.init({
|
|
173
|
+
jobs: [
|
|
174
|
+
{
|
|
175
|
+
name: 'test.minute-1',
|
|
176
|
+
time: '0 1 * * * *',
|
|
177
|
+
fn: async () => { calls.push('minute-1'); },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'test.minute-2',
|
|
181
|
+
time: '0 2 * * * *',
|
|
182
|
+
fn: async () => { calls.push('minute-2'); },
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Scheduled intended at 00:01 but delivered late so wall-clock is in minute 2.
|
|
188
|
+
// Caller (worker.ts) should pass the intended time (new Date(event.scheduledTime)).
|
|
189
|
+
const intendedMinute1 = new Date(Date.UTC(2026, 3, 17, 0, 1, 0));
|
|
190
|
+
await cronInstance.runAll(intendedMinute1);
|
|
191
|
+
|
|
192
|
+
expect(calls).toEqual(['minute-1']);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('defaults to new Date() when no arg is passed', async () => {
|
|
196
|
+
// Smoke check that the default-arg path still works (e.g. from manual dev endpoints).
|
|
197
|
+
let ran = false;
|
|
198
|
+
cronModule.init({
|
|
199
|
+
jobs: [
|
|
200
|
+
{
|
|
201
|
+
name: 'test.always',
|
|
202
|
+
time: '* * * * *',
|
|
203
|
+
fn: async () => { ran = true; },
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
await cronInstance.runAll();
|
|
208
|
+
expect(ran).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Tests for shims/queue.ts:runAllScheduledJobs aggregated D1 scan.
|
|
2
|
+
//
|
|
3
|
+
// Verifies the Plan 8 refactor: 16 × 2 per-queue queries collapse into a
|
|
4
|
+
// single Job.findAll across all registered queues. Behavior invariants
|
|
5
|
+
// preserved: due-delayed + stuck-immediate semantics, 2-minute created_at
|
|
6
|
+
// filter for immediate jobs, per-queue dispatch order by created_at.
|
|
7
|
+
|
|
8
|
+
// Mock the Job model before importing the shim — import order matters
|
|
9
|
+
// because shims/queue.ts imports Job at module top.
|
|
10
|
+
jest.mock('../../../api/src/store/models/job', () => ({
|
|
11
|
+
Job: {
|
|
12
|
+
findAll: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock createQueueStore so registering a handler doesn't hit D1.
|
|
17
|
+
jest.mock('../../../api/src/libs/queue/store', () => ({
|
|
18
|
+
__esModule: true,
|
|
19
|
+
default: jest.fn(() => ({
|
|
20
|
+
// Only the surface actually touched by dispatchJob / handler registration:
|
|
21
|
+
deleteJob: jest.fn().mockResolvedValue(true),
|
|
22
|
+
getJob: jest.fn(),
|
|
23
|
+
getJobs: jest.fn(),
|
|
24
|
+
getScheduledJobs: jest.fn(),
|
|
25
|
+
addJob: jest.fn(),
|
|
26
|
+
updateJob: jest.fn(),
|
|
27
|
+
isCancelled: jest.fn().mockResolvedValue(false),
|
|
28
|
+
findJobs: jest.fn(),
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import { Op } from 'sequelize';
|
|
33
|
+
import { Job } from '../../../api/src/store/models/job';
|
|
34
|
+
import {
|
|
35
|
+
runAllScheduledJobs,
|
|
36
|
+
setCFQueue,
|
|
37
|
+
getAllHandlerNames,
|
|
38
|
+
__test__,
|
|
39
|
+
} from '../../shims/queue';
|
|
40
|
+
|
|
41
|
+
const mockedFindAll = Job.findAll as unknown as jest.Mock;
|
|
42
|
+
|
|
43
|
+
describe('runAllScheduledJobs — aggregated D1 scan', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
__test__?.resetHandlers?.();
|
|
47
|
+
setCFQueue(null);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function registerQueue(name: string, executeJob = jest.fn().mockResolvedValue(undefined)) {
|
|
51
|
+
__test__!.registerForTest!(name, { executeJob });
|
|
52
|
+
return executeJob;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it('returns early with no query when no queues are registered', async () => {
|
|
56
|
+
const result = await runAllScheduledJobs();
|
|
57
|
+
expect(mockedFindAll).not.toHaveBeenCalled();
|
|
58
|
+
expect(result).toEqual({ dispatched: 0, failed: 0, queues: [] });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('issues exactly ONE findAll across all registered queues (no per-queue loop)', async () => {
|
|
62
|
+
for (let i = 0; i < 16; i += 1) registerQueue(`q${i}`);
|
|
63
|
+
mockedFindAll.mockResolvedValue([]);
|
|
64
|
+
|
|
65
|
+
await runAllScheduledJobs();
|
|
66
|
+
|
|
67
|
+
expect(mockedFindAll).toHaveBeenCalledTimes(1);
|
|
68
|
+
const arg = mockedFindAll.mock.calls[0][0];
|
|
69
|
+
expect(arg.where.queue).toEqual({ [Op.in]: expect.arrayContaining(['q0', 'q15']) });
|
|
70
|
+
expect(arg.where.cancelled).toBe(false);
|
|
71
|
+
expect(arg.where[Op.or]).toHaveLength(2);
|
|
72
|
+
expect(arg.order).toEqual([['created_at', 'ASC']]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('groups rows by queue and dispatches via the registered handler', async () => {
|
|
76
|
+
const execA = registerQueue('qa');
|
|
77
|
+
const execB = registerQueue('qb');
|
|
78
|
+
|
|
79
|
+
mockedFindAll.mockResolvedValue([
|
|
80
|
+
{ id: 'a1', queue: 'qa', job: { v: 1 }, created_at: new Date() },
|
|
81
|
+
{ id: 'b1', queue: 'qb', job: { v: 2 }, created_at: new Date() },
|
|
82
|
+
{ id: 'a2', queue: 'qa', job: { v: 3 }, created_at: new Date() },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const result = await runAllScheduledJobs();
|
|
86
|
+
|
|
87
|
+
expect(execA).toHaveBeenCalledTimes(2);
|
|
88
|
+
expect(execB).toHaveBeenCalledTimes(1);
|
|
89
|
+
expect(result.dispatched).toBe(3);
|
|
90
|
+
expect(result.failed).toBe(0);
|
|
91
|
+
expect(result.queues.sort()).toEqual(['qa', 'qb']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('filters out rows missing id or job payload', async () => {
|
|
95
|
+
const exec = registerQueue('qa');
|
|
96
|
+
|
|
97
|
+
mockedFindAll.mockResolvedValue([
|
|
98
|
+
{ id: 'a1', queue: 'qa', job: { v: 1 }, created_at: new Date() },
|
|
99
|
+
{ id: '', queue: 'qa', job: { v: 2 }, created_at: new Date() },
|
|
100
|
+
{ id: 'a3', queue: 'qa', job: null, created_at: new Date() },
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const result = await runAllScheduledJobs();
|
|
104
|
+
|
|
105
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(result.dispatched).toBe(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('continues dispatching siblings when one job throws', async () => {
|
|
110
|
+
const exec = jest
|
|
111
|
+
.fn()
|
|
112
|
+
.mockRejectedValueOnce(new Error('boom'))
|
|
113
|
+
.mockResolvedValue(undefined);
|
|
114
|
+
registerQueue('qa', exec);
|
|
115
|
+
|
|
116
|
+
mockedFindAll.mockResolvedValue([
|
|
117
|
+
{ id: 'a1', queue: 'qa', job: { v: 1 }, created_at: new Date() },
|
|
118
|
+
{ id: 'a2', queue: 'qa', job: { v: 2 }, created_at: new Date() },
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
const result = await runAllScheduledJobs();
|
|
122
|
+
|
|
123
|
+
expect(exec).toHaveBeenCalledTimes(2);
|
|
124
|
+
expect(result.dispatched).toBe(1);
|
|
125
|
+
expect(result.failed).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('returns empty result without throwing when D1 scan fails', async () => {
|
|
129
|
+
registerQueue('qa');
|
|
130
|
+
mockedFindAll.mockRejectedValue(new Error('D1 timeout'));
|
|
131
|
+
|
|
132
|
+
const result = await runAllScheduledJobs();
|
|
133
|
+
|
|
134
|
+
expect(result).toEqual({ dispatched: 0, failed: 0, queues: [] });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('ignores rows for queues that are no longer registered', async () => {
|
|
138
|
+
const exec = registerQueue('qa');
|
|
139
|
+
mockedFindAll.mockResolvedValue([
|
|
140
|
+
{ id: 'a1', queue: 'qa', job: { v: 1 }, created_at: new Date() },
|
|
141
|
+
{ id: 'z1', queue: 'qzombie', job: { v: 2 }, created_at: new Date() },
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const result = await runAllScheduledJobs();
|
|
145
|
+
|
|
146
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(result.dispatched).toBe(1);
|
|
148
|
+
expect(result.queues).toEqual(['qa']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('where clause covers due-delayed OR stuck-immediate jobs with 2-min cutoff', async () => {
|
|
152
|
+
registerQueue('qa');
|
|
153
|
+
mockedFindAll.mockResolvedValue([]);
|
|
154
|
+
|
|
155
|
+
const before = Date.now();
|
|
156
|
+
await runAllScheduledJobs();
|
|
157
|
+
const after = Date.now();
|
|
158
|
+
|
|
159
|
+
const arg = mockedFindAll.mock.calls[0][0];
|
|
160
|
+
const orClauses = arg.where[Op.or];
|
|
161
|
+
expect(orClauses).toHaveLength(2);
|
|
162
|
+
|
|
163
|
+
const delayedClause = orClauses.find((c: any) => c.delay && c.will_run_at);
|
|
164
|
+
expect(delayedClause).toBeDefined();
|
|
165
|
+
expect(delayedClause.delay).toEqual({ [Op.not]: -1 });
|
|
166
|
+
const dueCutoff = delayedClause.will_run_at[Op.lte];
|
|
167
|
+
expect(dueCutoff).toBeGreaterThanOrEqual(before);
|
|
168
|
+
expect(dueCutoff).toBeLessThanOrEqual(after);
|
|
169
|
+
|
|
170
|
+
const immediateClause = orClauses.find((c: any) => c.delay === -1);
|
|
171
|
+
expect(immediateClause).toBeDefined();
|
|
172
|
+
// D1 rejects Date objects at the bind layer, so the shim serializes to ISO string.
|
|
173
|
+
const cutoffValue: string = immediateClause.created_at[Op.lt];
|
|
174
|
+
expect(typeof cutoffValue).toBe('string');
|
|
175
|
+
const cutoffMs = new Date(cutoffValue).getTime();
|
|
176
|
+
// 2 minutes back from "now", allowing for small wall-clock drift.
|
|
177
|
+
expect(before - cutoffMs).toBeGreaterThanOrEqual(2 * 60 * 1000 - 50);
|
|
178
|
+
expect(before - cutoffMs).toBeLessThanOrEqual(2 * 60 * 1000 + 50);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('registers handlers that show up in getAllHandlerNames', () => {
|
|
182
|
+
registerQueue('q1');
|
|
183
|
+
registerQueue('q2');
|
|
184
|
+
expect(getAllHandlerNames().sort()).toEqual(['q1', 'q2']);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import svgr from 'vite-plugin-svgr';
|
|
4
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
const coreDir = path.resolve(__dirname, '..');
|
|
8
|
+
|
|
9
|
+
// Absolute path to the original session file we want to replace
|
|
10
|
+
const originalSessionPath = path.resolve(coreDir, 'src/contexts/session');
|
|
11
|
+
// Absolute path to our CF shim
|
|
12
|
+
const cfSessionShimPath = path.resolve(__dirname, 'frontend-shims/session.ts');
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
root: coreDir, // blocklets/core/ — where index.html and src/ live
|
|
16
|
+
plugins: [
|
|
17
|
+
// Redirect contexts/session imports to CF shim (must be first plugin)
|
|
18
|
+
{
|
|
19
|
+
name: 'cf-session-redirect',
|
|
20
|
+
enforce: 'pre' as const,
|
|
21
|
+
resolveId(source: string, importer: string | undefined) {
|
|
22
|
+
if (!importer) return null;
|
|
23
|
+
// Match relative imports that resolve to src/contexts/session
|
|
24
|
+
if (source.endsWith('/contexts/session') || source.endsWith('/contexts/session.ts')) {
|
|
25
|
+
const resolved = path.resolve(path.dirname(importer), source).replace(/\.ts$/, '');
|
|
26
|
+
if (resolved === originalSessionPath) {
|
|
27
|
+
return cfSessionShimPath;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
// Inject window.blocklet + global polyfills into HTML
|
|
34
|
+
{
|
|
35
|
+
name: 'cf-inject-blocklet',
|
|
36
|
+
transformIndexHtml(html: string) {
|
|
37
|
+
const injection = `<script>
|
|
38
|
+
window.global = globalThis;
|
|
39
|
+
// Minimal bootstrap — full config loaded from __blocklet__.js
|
|
40
|
+
if (!window.blocklet) {
|
|
41
|
+
window.blocklet = {
|
|
42
|
+
prefix: '/',
|
|
43
|
+
groupPrefix: '/',
|
|
44
|
+
appUrl: window.location.origin,
|
|
45
|
+
componentMountPoints: [{did:'z8ia1mAXo8ZE7ytGF36L5uBf9kD2kenhqFGp9',name:'Media Kit',mountPoint:'/media-kit',appId:'z8ia1mAXo8ZE7ytGF36L5uBf9kD2kenhqFGp9',status:'running',capabilities:{component:true}}],
|
|
46
|
+
navigation: [
|
|
47
|
+
{id:'payments',title:{en:'Payments',zh:'支付管理'},icon:'ion:card-outline',link:'/admin',section:['dashboard','sessionManager'],role:['admin','owner']},
|
|
48
|
+
{id:'integrations',title:{en:'Integrations',zh:'快速集成'},icon:'ion:flash-outline',link:'/integrations',section:['dashboard','sessionManager'],role:['admin','owner']},
|
|
49
|
+
{id:'billing',title:{en:'Billing',zh:'我的账单'},icon:'ion:receipt-outline',link:'/customer',private:true,section:['userCenter','sessionManager'],role:['owner','admin','member','guest']},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Load full config from /__blocklet__.js?type=json (served by worker, includes auth/branding/theme from AUTH_SERVICE).
|
|
54
|
+
// Using type=json lets us JSON.parse the response directly instead of slicing out a { } substring from a JS assignment.
|
|
55
|
+
(function() {
|
|
56
|
+
try {
|
|
57
|
+
var xhr = new XMLHttpRequest();
|
|
58
|
+
var pfx = (window.blocklet && window.blocklet.prefix) || '/';
|
|
59
|
+
xhr.open('GET', pfx + '__blocklet__.js?type=json&_t=' + Date.now(), false);
|
|
60
|
+
xhr.send();
|
|
61
|
+
if (xhr.status === 200) {
|
|
62
|
+
var remote = JSON.parse(xhr.responseText);
|
|
63
|
+
// navigation / componentMountPoints are owned by this blocklet — AUTH_SERVICE
|
|
64
|
+
// doesn't know about them, so we never want remote values to clobber the
|
|
65
|
+
// ones the bootstrap above set.
|
|
66
|
+
var localOnly = ['navigation', 'componentMountPoints'];
|
|
67
|
+
Object.keys(remote).forEach(function(k) {
|
|
68
|
+
if (localOnly.indexOf(k) === -1) {
|
|
69
|
+
window.blocklet[k] = remote[k];
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
if (!window.blocklet.env) window.blocklet.env = {};
|
|
73
|
+
window.blocklet.env.appName = window.blocklet.appName || '';
|
|
74
|
+
window.blocklet.env.appDescription = window.blocklet.appDescription || '';
|
|
75
|
+
window.blocklet.env.appLogo = window.blocklet.appLogo || '';
|
|
76
|
+
window.blocklet.env.appUrl = window.blocklet.appUrl || '';
|
|
77
|
+
}
|
|
78
|
+
} catch(e) { /* ignore */ }
|
|
79
|
+
})();
|
|
80
|
+
</script>`;
|
|
81
|
+
return html.replace('<head>', '<head>' + injection);
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
// Inject Buffer polyfill at the top of the entry file
|
|
85
|
+
{
|
|
86
|
+
name: 'cf-buffer-polyfill',
|
|
87
|
+
enforce: 'pre' as const,
|
|
88
|
+
transform(code: string, id: string) {
|
|
89
|
+
if (id.endsWith('/src/index.tsx')) {
|
|
90
|
+
return `import '${path.resolve(__dirname, 'frontend-shims/buffer-polyfill.ts').replace(/\\/g, '/')}';\n` + code;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
tsconfigPaths({ root: coreDir }),
|
|
96
|
+
react(),
|
|
97
|
+
svgr(),
|
|
98
|
+
],
|
|
99
|
+
resolve: {
|
|
100
|
+
alias: {
|
|
101
|
+
// Point to source code (workspace packages)
|
|
102
|
+
'@blocklet/payment-react': path.resolve(coreDir, '../../packages/react/src'),
|
|
103
|
+
'@blocklet/payment-react-headless': path.resolve(coreDir, '../../packages/payment-react-headless/src'),
|
|
104
|
+
'@blocklet/payment-js': path.resolve(coreDir, '../../packages/client/src'),
|
|
105
|
+
|
|
106
|
+
// Replace @blocklet/js-sdk with a simple axios wrapper
|
|
107
|
+
'@blocklet/js-sdk': path.resolve(__dirname, 'frontend-shims/js-sdk.ts'),
|
|
108
|
+
|
|
109
|
+
// Fix mime-types CJS interop for @blocklet/uploader
|
|
110
|
+
'mime-types': path.resolve(__dirname, 'frontend-shims/mime-types.ts'),
|
|
111
|
+
|
|
112
|
+
// lodash aliases
|
|
113
|
+
'lodash.assign': 'lodash/assign',
|
|
114
|
+
'lodash.clonedeep': 'lodash/cloneDeep',
|
|
115
|
+
'lodash.isequal': 'lodash/isEqual',
|
|
116
|
+
'lodash.merge': 'lodash/merge',
|
|
117
|
+
'lodash.find': 'lodash/find',
|
|
118
|
+
},
|
|
119
|
+
dedupe: [
|
|
120
|
+
'@arcblock/ux',
|
|
121
|
+
'@arcblock/did-connect-react',
|
|
122
|
+
'@blocklet/ui-react',
|
|
123
|
+
'@mui/material',
|
|
124
|
+
'@mui/icons-material',
|
|
125
|
+
'react',
|
|
126
|
+
'react-dom',
|
|
127
|
+
'lodash',
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
build: {
|
|
131
|
+
outDir: path.resolve(__dirname, 'public'),
|
|
132
|
+
emptyOutDir: true,
|
|
133
|
+
cssCodeSplit: false,
|
|
134
|
+
modulePreload: false,
|
|
135
|
+
commonjsOptions: {
|
|
136
|
+
include: [/node_modules/],
|
|
137
|
+
transformMixedEsModules: true,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
define: {
|
|
141
|
+
'global': 'globalThis',
|
|
142
|
+
},
|
|
143
|
+
optimizeDeps: {
|
|
144
|
+
include: ['react', 'react-dom', 'react/jsx-runtime', 'buffer'],
|
|
145
|
+
esbuildOptions: {
|
|
146
|
+
mainFields: ['module', 'main'],
|
|
147
|
+
resolveExtensions: ['.ts', '.tsx', '.js', '.jsx'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
server: {
|
|
151
|
+
proxy: {
|
|
152
|
+
'/api': {
|
|
153
|
+
target: 'http://localhost:8800',
|
|
154
|
+
changeOrigin: true,
|
|
155
|
+
},
|
|
156
|
+
'/.well-known': {
|
|
157
|
+
target: 'http://localhost:8800',
|
|
158
|
+
changeOrigin: true,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|