weifuwu 0.16.3 → 0.16.4
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/README.md +654 -1327
- package/cli.ts +103 -5
- package/dist/cli.js +98 -5
- package/dist/client-router.d.ts +296 -0
- package/dist/csrf.d.ts +8 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +312 -35
- package/dist/serve.d.ts +1 -0
- package/dist/use-action.d.ts +14 -0
- package/dist/use-websocket.d.ts +17 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -122,6 +122,14 @@ function serve(handler, options) {
|
|
|
122
122
|
const ready = new Promise((r) => {
|
|
123
123
|
resolveReady = r;
|
|
124
124
|
});
|
|
125
|
+
if (options?.shutdown !== false) {
|
|
126
|
+
process.on("SIGTERM", () => {
|
|
127
|
+
server.close();
|
|
128
|
+
});
|
|
129
|
+
process.on("SIGINT", () => {
|
|
130
|
+
server.close();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
125
133
|
if (options?.signal) {
|
|
126
134
|
if (options.signal.aborted) {
|
|
127
135
|
server.close();
|
|
@@ -968,9 +976,16 @@ ${src}`;
|
|
|
968
976
|
`import{createElement}from'react';`,
|
|
969
977
|
`import P from${JSON.stringify(entryPath)};`,
|
|
970
978
|
`const p=window.__WEIFUWU_PROPS;`,
|
|
971
|
-
`
|
|
972
|
-
`hydrateRoot(
|
|
979
|
+
`const c=document.getElementById('__weifuwu_root');`,
|
|
980
|
+
`const r=hydrateRoot(c,createElement(P,p));`,
|
|
981
|
+
`window.__WEIFUWU_ROOT=r;`
|
|
973
982
|
].join("");
|
|
983
|
+
const publicEnv = {};
|
|
984
|
+
for (const key of Object.keys(process.env)) {
|
|
985
|
+
if (key.startsWith("WEIFUWU_PUBLIC_")) {
|
|
986
|
+
publicEnv[`process.env.${key}`] = JSON.stringify(process.env[key]);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
974
989
|
const result = await esbuild.build({
|
|
975
990
|
stdin: { contents: code, loader: "tsx", resolveDir: pagesDir },
|
|
976
991
|
bundle: true,
|
|
@@ -978,6 +993,8 @@ ${src}`;
|
|
|
978
993
|
jsx: "automatic",
|
|
979
994
|
jsxImportSource: "react",
|
|
980
995
|
alias: resolveAliases(),
|
|
996
|
+
banner: { js: "self.process={env:{}};" },
|
|
997
|
+
define: Object.keys(publicEnv).length > 0 ? publicEnv : void 0,
|
|
981
998
|
write: false,
|
|
982
999
|
minify: true
|
|
983
1000
|
});
|
|
@@ -1028,9 +1045,13 @@ ${src}`;
|
|
|
1028
1045
|
const loadFn = loadMod?.default;
|
|
1029
1046
|
const loadProps = loadFn ? await loadFn({ params: ctx.params, query: ctx.query }) : {};
|
|
1030
1047
|
const allProps = { ...loadProps, params: ctx.params, query: ctx.query };
|
|
1031
|
-
let element = createElement(
|
|
1032
|
-
|
|
1033
|
-
|
|
1048
|
+
let element = createElement(
|
|
1049
|
+
"div",
|
|
1050
|
+
{ id: "__weifuwu_root" },
|
|
1051
|
+
createElement(TsxContext.Provider, {
|
|
1052
|
+
value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
|
|
1053
|
+
}, createElement(Component, allProps))
|
|
1054
|
+
);
|
|
1034
1055
|
if (layoutPaths.length === 0) {
|
|
1035
1056
|
element = createElement(
|
|
1036
1057
|
"html",
|
|
@@ -1042,7 +1063,7 @@ ${src}`;
|
|
|
1042
1063
|
createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
|
1043
1064
|
createElement("title", null, "weifuwu")
|
|
1044
1065
|
),
|
|
1045
|
-
createElement("body", null,
|
|
1066
|
+
createElement("body", null, element)
|
|
1046
1067
|
);
|
|
1047
1068
|
} else {
|
|
1048
1069
|
for (let i = layoutPaths.length - 1; i >= 0; i--) {
|
|
@@ -1059,6 +1080,11 @@ ${src}`;
|
|
|
1059
1080
|
}
|
|
1060
1081
|
const stream = await renderToReadableStream(element);
|
|
1061
1082
|
const body = await readStream(stream);
|
|
1083
|
+
if (layoutPaths.length > 0 && (body.match(/__weifuwu_root/g) || []).length > 1) {
|
|
1084
|
+
console.warn(
|
|
1085
|
+
'[weifuwu/tsx] <div id="__weifuwu_root"> is auto-injected by the framework. Remove the duplicate from your root layout to avoid hydration conflicts.'
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1062
1088
|
const scripts = [];
|
|
1063
1089
|
scripts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(allProps)}</script>`);
|
|
1064
1090
|
const bundle = await this.getOrBuildClientBundle(entryPath, layoutPaths, this.pagesDir);
|
|
@@ -1747,7 +1773,7 @@ function upload(options) {
|
|
|
1747
1773
|
// rate-limit.ts
|
|
1748
1774
|
function rateLimit(options) {
|
|
1749
1775
|
const max = options?.max ?? 100;
|
|
1750
|
-
const
|
|
1776
|
+
const window2 = options?.window ?? 6e4;
|
|
1751
1777
|
const getKey = options?.key ?? ((req) => {
|
|
1752
1778
|
const forwarded = req.headers.get("x-forwarded-for");
|
|
1753
1779
|
if (forwarded) return forwarded.split(",")[0].trim();
|
|
@@ -1764,19 +1790,19 @@ function rateLimit(options) {
|
|
|
1764
1790
|
for (const [key, entry] of hits) {
|
|
1765
1791
|
if (entry.reset < now) hits.delete(key);
|
|
1766
1792
|
}
|
|
1767
|
-
},
|
|
1793
|
+
}, window2);
|
|
1768
1794
|
if (interval.unref) interval.unref();
|
|
1769
1795
|
const mw = async (req, ctx, next) => {
|
|
1770
1796
|
const key = getKey(req);
|
|
1771
1797
|
const now = Date.now();
|
|
1772
1798
|
const entry = hits.get(key);
|
|
1773
1799
|
if (!entry || entry.reset < now) {
|
|
1774
|
-
hits.set(key, { count: 1, reset: now +
|
|
1800
|
+
hits.set(key, { count: 1, reset: now + window2 });
|
|
1775
1801
|
const res2 = await next(req, ctx);
|
|
1776
1802
|
const headers2 = new Headers(res2.headers);
|
|
1777
1803
|
headers2.set("X-RateLimit-Limit", String(max));
|
|
1778
1804
|
headers2.set("X-RateLimit-Remaining", String(max - 1));
|
|
1779
|
-
headers2.set("X-RateLimit-Reset", String(Math.ceil((now +
|
|
1805
|
+
headers2.set("X-RateLimit-Reset", String(Math.ceil((now + window2) / 1e3)));
|
|
1780
1806
|
return new Response(res2.body, { status: res2.status, statusText: res2.statusText, headers: headers2 });
|
|
1781
1807
|
}
|
|
1782
1808
|
entry.count++;
|
|
@@ -1910,10 +1936,10 @@ function helmet(options) {
|
|
|
1910
1936
|
}
|
|
1911
1937
|
|
|
1912
1938
|
// request-id.ts
|
|
1913
|
-
import
|
|
1939
|
+
import crypto2 from "node:crypto";
|
|
1914
1940
|
function requestId(options) {
|
|
1915
1941
|
const header = options?.header ?? "X-Request-ID";
|
|
1916
|
-
const gen = options?.generator ?? (() =>
|
|
1942
|
+
const gen = options?.generator ?? (() => crypto2.randomUUID());
|
|
1917
1943
|
return async (req, ctx, next) => {
|
|
1918
1944
|
const existing = req.headers.get(header);
|
|
1919
1945
|
const id2 = existing ?? gen();
|
|
@@ -2949,7 +2975,7 @@ import jwt2 from "jsonwebtoken";
|
|
|
2949
2975
|
import { z as z2 } from "zod";
|
|
2950
2976
|
|
|
2951
2977
|
// user/oauth2.ts
|
|
2952
|
-
import
|
|
2978
|
+
import crypto3 from "node:crypto";
|
|
2953
2979
|
import jwt from "jsonwebtoken";
|
|
2954
2980
|
function createOAuth2Server(deps) {
|
|
2955
2981
|
const { pg, users, jwtSecret, expiresIn } = deps;
|
|
@@ -2968,8 +2994,8 @@ function createOAuth2Server(deps) {
|
|
|
2968
2994
|
};
|
|
2969
2995
|
}
|
|
2970
2996
|
async function registerClient(data) {
|
|
2971
|
-
const clientId =
|
|
2972
|
-
const clientSecret =
|
|
2997
|
+
const clientId = crypto3.randomUUID();
|
|
2998
|
+
const clientSecret = crypto3.randomBytes(32).toString("hex");
|
|
2973
2999
|
const [row] = await pg.sql`
|
|
2974
3000
|
INSERT INTO "_oauth2_clients" ("name", "client_id", "client_secret", "redirect_uris")
|
|
2975
3001
|
VALUES (${data.name}, ${clientId}, ${clientSecret}, ${pg.sql.array(data.redirectUris)})
|
|
@@ -3117,7 +3143,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3117
3143
|
const loc2 = `${redirectUri}?error=access_denied${state ? `&state=${state}` : ""}`;
|
|
3118
3144
|
return Response.redirect(loc2, 302);
|
|
3119
3145
|
}
|
|
3120
|
-
const code =
|
|
3146
|
+
const code = crypto3.randomUUID();
|
|
3121
3147
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
3122
3148
|
await pg.sql`
|
|
3123
3149
|
INSERT INTO "_oauth2_codes" ("code", "client_id", "user_id", "redirect_uri", "code_challenge", "code_challenge_method", "scope", "expires_at")
|
|
@@ -3182,7 +3208,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3182
3208
|
if (stored.code_challenge_method === "plain") {
|
|
3183
3209
|
expected = codeVerifier;
|
|
3184
3210
|
} else {
|
|
3185
|
-
expected =
|
|
3211
|
+
expected = crypto3.createHash("sha256").update(codeVerifier).digest().toString("base64url");
|
|
3186
3212
|
}
|
|
3187
3213
|
if (expected !== stored.code_challenge) {
|
|
3188
3214
|
return Response.json({ error: "invalid_grant", error_description: "code_verifier mismatch" }, { status: 400 });
|
|
@@ -3199,7 +3225,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3199
3225
|
jwtSecret,
|
|
3200
3226
|
{ expiresIn }
|
|
3201
3227
|
);
|
|
3202
|
-
const refreshToken =
|
|
3228
|
+
const refreshToken = crypto3.randomUUID();
|
|
3203
3229
|
const refreshExpires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3);
|
|
3204
3230
|
await pg.sql`
|
|
3205
3231
|
INSERT INTO "_oauth2_tokens" ("token", "client_id", "user_id", "scope", "expires_at")
|
|
@@ -3523,7 +3549,7 @@ function createHub(opts) {
|
|
|
3523
3549
|
|
|
3524
3550
|
// queue/index.ts
|
|
3525
3551
|
import { Redis as IORedis2 } from "ioredis";
|
|
3526
|
-
import
|
|
3552
|
+
import crypto4 from "node:crypto";
|
|
3527
3553
|
function cronNext(expr, from = /* @__PURE__ */ new Date()) {
|
|
3528
3554
|
const parts = expr.trim().split(/\s+/);
|
|
3529
3555
|
if (parts.length !== 5) throw new Error(`Invalid cron expression "${expr}": expected 5 fields`);
|
|
@@ -3614,7 +3640,7 @@ function queue(opts) {
|
|
|
3614
3640
|
if (job.schedule) {
|
|
3615
3641
|
try {
|
|
3616
3642
|
const nextRun = cronNext(job.schedule);
|
|
3617
|
-
const nextJob = { ...job, id:
|
|
3643
|
+
const nextJob = { ...job, id: crypto4.randomUUID(), runAt: nextRun, createdAt: Date.now() };
|
|
3618
3644
|
redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob)).catch(() => {
|
|
3619
3645
|
});
|
|
3620
3646
|
} catch {
|
|
@@ -3632,7 +3658,7 @@ function queue(opts) {
|
|
|
3632
3658
|
}
|
|
3633
3659
|
}
|
|
3634
3660
|
mw.add = function add(type, payload, opts2) {
|
|
3635
|
-
const id2 =
|
|
3661
|
+
const id2 = crypto4.randomUUID();
|
|
3636
3662
|
let runAt;
|
|
3637
3663
|
if (opts2?.schedule) {
|
|
3638
3664
|
runAt = cronNext(opts2.schedule);
|
|
@@ -5286,7 +5312,7 @@ function createGateway(config, getPort) {
|
|
|
5286
5312
|
}
|
|
5287
5313
|
|
|
5288
5314
|
// deploy/manager.ts
|
|
5289
|
-
import
|
|
5315
|
+
import crypto5 from "node:crypto";
|
|
5290
5316
|
function createManager(config, apps, manager) {
|
|
5291
5317
|
const router = new Router();
|
|
5292
5318
|
const auth2 = (req, ctx, next) => {
|
|
@@ -5295,7 +5321,7 @@ function createManager(config, apps, manager) {
|
|
|
5295
5321
|
const token = header.replace("Bearer ", "");
|
|
5296
5322
|
const tokenBuf = Buffer.from(token);
|
|
5297
5323
|
const secretBuf = Buffer.from(config.deployToken);
|
|
5298
|
-
if (tokenBuf.length !== secretBuf.length || !
|
|
5324
|
+
if (tokenBuf.length !== secretBuf.length || !crypto5.timingSafeEqual(tokenBuf, secretBuf)) {
|
|
5299
5325
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
5300
5326
|
}
|
|
5301
5327
|
return next(req, ctx);
|
|
@@ -5394,10 +5420,10 @@ function createManager(config, apps, manager) {
|
|
|
5394
5420
|
const rawBody = await req.text();
|
|
5395
5421
|
if (config.webhookSecret) {
|
|
5396
5422
|
const sig = req.headers.get("x-hub-signature-256") ?? "";
|
|
5397
|
-
const expected = `sha256=${
|
|
5423
|
+
const expected = `sha256=${crypto5.createHmac("sha256", config.webhookSecret).update(rawBody).digest("hex")}`;
|
|
5398
5424
|
const sigBuf = Buffer.from(sig);
|
|
5399
5425
|
const expectedBuf = Buffer.from(expected);
|
|
5400
|
-
if (sigBuf.length !== expectedBuf.length || !
|
|
5426
|
+
if (sigBuf.length !== expectedBuf.length || !crypto5.timingSafeEqual(sigBuf, expectedBuf)) {
|
|
5401
5427
|
return Response.json({ error: "invalid signature" }, { status: 401 });
|
|
5402
5428
|
}
|
|
5403
5429
|
}
|
|
@@ -6354,7 +6380,7 @@ async function buildRouter4(deps) {
|
|
|
6354
6380
|
skills: allSkills,
|
|
6355
6381
|
systemPrompt: session.system_prompt || systemPrompt
|
|
6356
6382
|
});
|
|
6357
|
-
const
|
|
6383
|
+
const history2 = await getHistory(sql2, sessionId);
|
|
6358
6384
|
await addTextMessage(sql2, sessionId, "user", content);
|
|
6359
6385
|
const stream = executeGenerator({
|
|
6360
6386
|
sessionId,
|
|
@@ -6362,7 +6388,7 @@ async function buildRouter4(deps) {
|
|
|
6362
6388
|
model,
|
|
6363
6389
|
tools,
|
|
6364
6390
|
systemPrompt: sysPrompt,
|
|
6365
|
-
messages:
|
|
6391
|
+
messages: history2,
|
|
6366
6392
|
sql: sql2
|
|
6367
6393
|
});
|
|
6368
6394
|
return createSSEStream(stream);
|
|
@@ -6442,7 +6468,7 @@ function createWSHandler2(deps) {
|
|
|
6442
6468
|
skills: allSkills,
|
|
6443
6469
|
systemPrompt: session.system_prompt || systemPrompt
|
|
6444
6470
|
});
|
|
6445
|
-
const
|
|
6471
|
+
const history2 = await getHistory(sql2, session_id);
|
|
6446
6472
|
await addTextMessage(sql2, session_id, "user", content);
|
|
6447
6473
|
const stream = executeGenerator({
|
|
6448
6474
|
sessionId: session_id,
|
|
@@ -6450,7 +6476,7 @@ function createWSHandler2(deps) {
|
|
|
6450
6476
|
model,
|
|
6451
6477
|
tools,
|
|
6452
6478
|
systemPrompt: sysPrompt,
|
|
6453
|
-
messages:
|
|
6479
|
+
messages: history2,
|
|
6454
6480
|
sql: sql2,
|
|
6455
6481
|
abortSignal: controller.signal
|
|
6456
6482
|
});
|
|
@@ -6887,6 +6913,251 @@ function mailer(options) {
|
|
|
6887
6913
|
return { send, close };
|
|
6888
6914
|
}
|
|
6889
6915
|
|
|
6916
|
+
// use-websocket.ts
|
|
6917
|
+
import { useEffect, useRef, useCallback, useState } from "react";
|
|
6918
|
+
var RECONNECT_DELAY = 3e3;
|
|
6919
|
+
var MAX_RETRIES = 10;
|
|
6920
|
+
function resolveUrl(url) {
|
|
6921
|
+
return typeof url === "function" ? url() : url;
|
|
6922
|
+
}
|
|
6923
|
+
function useWebsocket(url, options) {
|
|
6924
|
+
const { onMessage, reconnect: reconnectOpt = true, protocols, enabled = true } = options ?? {};
|
|
6925
|
+
const [lastMessage, setLastMessage] = useState(null);
|
|
6926
|
+
const [readyState, setReadyState] = useState(WebSocket.CLOSED);
|
|
6927
|
+
const wsRef = useRef(null);
|
|
6928
|
+
const retryRef = useRef(0);
|
|
6929
|
+
const timerRef = useRef(void 0);
|
|
6930
|
+
const mountedRef = useRef(true);
|
|
6931
|
+
const shouldReconnectRef = useRef(true);
|
|
6932
|
+
const urlRef = useRef(url);
|
|
6933
|
+
const optsRef = useRef({ onMessage, reconnectOpt, protocols });
|
|
6934
|
+
urlRef.current = url;
|
|
6935
|
+
optsRef.current = { onMessage, reconnectOpt, protocols };
|
|
6936
|
+
const cleanup = useCallback(() => {
|
|
6937
|
+
clearTimeout(timerRef.current);
|
|
6938
|
+
wsRef.current?.close();
|
|
6939
|
+
wsRef.current = null;
|
|
6940
|
+
}, []);
|
|
6941
|
+
const connect = useCallback(() => {
|
|
6942
|
+
if (!mountedRef.current || !enabled) return;
|
|
6943
|
+
const resolved = resolveUrl(urlRef.current);
|
|
6944
|
+
if (!resolved) return;
|
|
6945
|
+
wsRef.current?.close();
|
|
6946
|
+
const ws = new WebSocket(resolved, optsRef.current.protocols);
|
|
6947
|
+
wsRef.current = ws;
|
|
6948
|
+
setReadyState(WebSocket.CONNECTING);
|
|
6949
|
+
ws.addEventListener("open", () => {
|
|
6950
|
+
if (!mountedRef.current) return;
|
|
6951
|
+
retryRef.current = 0;
|
|
6952
|
+
setReadyState(WebSocket.OPEN);
|
|
6953
|
+
});
|
|
6954
|
+
ws.addEventListener("message", (e) => {
|
|
6955
|
+
if (!mountedRef.current) return;
|
|
6956
|
+
const data = typeof e.data === "string" ? e.data : String(e.data);
|
|
6957
|
+
setLastMessage(data);
|
|
6958
|
+
optsRef.current.onMessage?.(data);
|
|
6959
|
+
});
|
|
6960
|
+
ws.addEventListener("close", () => {
|
|
6961
|
+
if (!mountedRef.current) return;
|
|
6962
|
+
setReadyState(WebSocket.CLOSED);
|
|
6963
|
+
const ro = optsRef.current.reconnectOpt;
|
|
6964
|
+
if (ro && shouldReconnectRef.current && mountedRef.current) {
|
|
6965
|
+
const maxRetries = typeof ro === "object" ? ro.maxRetries ?? MAX_RETRIES : MAX_RETRIES;
|
|
6966
|
+
const delay = typeof ro === "object" ? ro.delay ?? RECONNECT_DELAY : RECONNECT_DELAY;
|
|
6967
|
+
if (retryRef.current < maxRetries) {
|
|
6968
|
+
retryRef.current++;
|
|
6969
|
+
timerRef.current = setTimeout(() => connect(), delay);
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
});
|
|
6973
|
+
}, [enabled]);
|
|
6974
|
+
useEffect(() => {
|
|
6975
|
+
mountedRef.current = true;
|
|
6976
|
+
shouldReconnectRef.current = true;
|
|
6977
|
+
if (enabled) connect();
|
|
6978
|
+
return () => {
|
|
6979
|
+
mountedRef.current = false;
|
|
6980
|
+
cleanup();
|
|
6981
|
+
};
|
|
6982
|
+
}, [enabled, connect, cleanup]);
|
|
6983
|
+
const send = useCallback((data) => {
|
|
6984
|
+
wsRef.current?.send(data);
|
|
6985
|
+
}, []);
|
|
6986
|
+
const close = useCallback(() => {
|
|
6987
|
+
shouldReconnectRef.current = false;
|
|
6988
|
+
cleanup();
|
|
6989
|
+
setReadyState(WebSocket.CLOSED);
|
|
6990
|
+
}, [cleanup]);
|
|
6991
|
+
const reconnectFn = useCallback(() => {
|
|
6992
|
+
retryRef.current = 0;
|
|
6993
|
+
shouldReconnectRef.current = true;
|
|
6994
|
+
cleanup();
|
|
6995
|
+
connect();
|
|
6996
|
+
}, [cleanup, connect]);
|
|
6997
|
+
return { send, close, readyState, lastMessage, reconnect: reconnectFn };
|
|
6998
|
+
}
|
|
6999
|
+
|
|
7000
|
+
// use-action.ts
|
|
7001
|
+
import { useState as useState2, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
7002
|
+
function getCsrfToken() {
|
|
7003
|
+
if (typeof document === "undefined") return void 0;
|
|
7004
|
+
const match = document.cookie.match(/(?:^|;\s*)_csrf=([^;]+)/);
|
|
7005
|
+
return match ? decodeURIComponent(match[1]) : void 0;
|
|
7006
|
+
}
|
|
7007
|
+
function useAction(url, options) {
|
|
7008
|
+
const { method = "POST", headers, onSuccess, onError } = options ?? {};
|
|
7009
|
+
const [data, setData] = useState2(null);
|
|
7010
|
+
const [error, setError] = useState2(null);
|
|
7011
|
+
const [pending, setPending] = useState2(false);
|
|
7012
|
+
const mountedRef = useRef2(true);
|
|
7013
|
+
const submit = useCallback2(async (body) => {
|
|
7014
|
+
setPending(true);
|
|
7015
|
+
setError(null);
|
|
7016
|
+
try {
|
|
7017
|
+
const csrfToken = getCsrfToken();
|
|
7018
|
+
const hdrs = { ...headers };
|
|
7019
|
+
if (csrfToken) hdrs["x-csrf-token"] = csrfToken;
|
|
7020
|
+
if (body && typeof body === "object" && !(body instanceof FormData)) {
|
|
7021
|
+
hdrs["content-type"] = "application/json";
|
|
7022
|
+
}
|
|
7023
|
+
const res = await fetch(url, {
|
|
7024
|
+
method,
|
|
7025
|
+
headers: hdrs,
|
|
7026
|
+
body: body instanceof FormData ? body : body !== void 0 ? JSON.stringify(body) : void 0
|
|
7027
|
+
});
|
|
7028
|
+
if (!res.ok) {
|
|
7029
|
+
const text2 = await res.text();
|
|
7030
|
+
throw new Error(text2 || `HTTP ${res.status}`);
|
|
7031
|
+
}
|
|
7032
|
+
const result = res.status === 204 ? void 0 : await res.json();
|
|
7033
|
+
if (mountedRef.current) {
|
|
7034
|
+
setData(result);
|
|
7035
|
+
onSuccess?.(result);
|
|
7036
|
+
}
|
|
7037
|
+
return result;
|
|
7038
|
+
} catch (err) {
|
|
7039
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
7040
|
+
if (mountedRef.current) {
|
|
7041
|
+
setError(e);
|
|
7042
|
+
onError?.(e);
|
|
7043
|
+
}
|
|
7044
|
+
return void 0;
|
|
7045
|
+
} finally {
|
|
7046
|
+
if (mountedRef.current) setPending(false);
|
|
7047
|
+
}
|
|
7048
|
+
}, [url, method, headers, onSuccess, onError]);
|
|
7049
|
+
const reset = useCallback2(() => {
|
|
7050
|
+
setData(null);
|
|
7051
|
+
setError(null);
|
|
7052
|
+
}, []);
|
|
7053
|
+
return { submit, data, error, pending, reset };
|
|
7054
|
+
}
|
|
7055
|
+
|
|
7056
|
+
// client-router.ts
|
|
7057
|
+
import { createElement as createElement2, useCallback as useCallback3 } from "react";
|
|
7058
|
+
async function navigate(href) {
|
|
7059
|
+
if (typeof document === "undefined") return;
|
|
7060
|
+
const url = new URL(href, location.origin);
|
|
7061
|
+
if (url.origin !== location.origin) {
|
|
7062
|
+
location.href = href;
|
|
7063
|
+
return;
|
|
7064
|
+
}
|
|
7065
|
+
const html = await fetch(url.pathname + url.search, {
|
|
7066
|
+
headers: { accept: "text/html" }
|
|
7067
|
+
}).then((r) => r.text());
|
|
7068
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
7069
|
+
const rootEl = doc.getElementById("__weifuwu_root");
|
|
7070
|
+
if (!rootEl) {
|
|
7071
|
+
location.href = href;
|
|
7072
|
+
return;
|
|
7073
|
+
}
|
|
7074
|
+
const newHtml = rootEl.innerHTML;
|
|
7075
|
+
const propsMatch = html.match(/window\.__WEIFUWU_PROPS=(.+?)<\/script>/);
|
|
7076
|
+
if (!propsMatch) {
|
|
7077
|
+
location.href = href;
|
|
7078
|
+
return;
|
|
7079
|
+
}
|
|
7080
|
+
const bundleMatch = html.match(/src="(\/__wfw\/client\/[^"]+\.js)"/);
|
|
7081
|
+
const bundleUrl = bundleMatch ? bundleMatch[1] : null;
|
|
7082
|
+
const currentRoot = document.getElementById("__weifuwu_root");
|
|
7083
|
+
if (!currentRoot) {
|
|
7084
|
+
location.href = href;
|
|
7085
|
+
return;
|
|
7086
|
+
}
|
|
7087
|
+
;
|
|
7088
|
+
window.__WEIFUWU_ROOT?.unmount();
|
|
7089
|
+
currentRoot.innerHTML = newHtml;
|
|
7090
|
+
window.__WEIFUWU_PROPS = JSON.parse(propsMatch[1]);
|
|
7091
|
+
history.pushState(null, "", url.pathname + url.search);
|
|
7092
|
+
if (bundleUrl) {
|
|
7093
|
+
const cacheBust = bundleUrl.includes("?") ? "&_t=" : "?_t=";
|
|
7094
|
+
try {
|
|
7095
|
+
await import(
|
|
7096
|
+
/* @vite-ignore */
|
|
7097
|
+
`${bundleUrl}${cacheBust}${Date.now()}`
|
|
7098
|
+
);
|
|
7099
|
+
} catch (e) {
|
|
7100
|
+
console.error("[weifuwu/router] hydration failed:", e);
|
|
7101
|
+
}
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
function useNavigate() {
|
|
7105
|
+
return useCallback3((href) => navigate(href), []);
|
|
7106
|
+
}
|
|
7107
|
+
function Link({ href, children, onClick, ...props }) {
|
|
7108
|
+
const handleClick = useCallback3((e) => {
|
|
7109
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
7110
|
+
e.preventDefault();
|
|
7111
|
+
navigate(href);
|
|
7112
|
+
onClick?.(e);
|
|
7113
|
+
}, [href, onClick]);
|
|
7114
|
+
return createElement2("a", { href, onClick: handleClick, ...props }, children);
|
|
7115
|
+
}
|
|
7116
|
+
|
|
7117
|
+
// csrf.ts
|
|
7118
|
+
function csrf(options) {
|
|
7119
|
+
const cookieName = options?.cookie ?? "_csrf";
|
|
7120
|
+
const headerName = options?.header ?? "x-csrf-token";
|
|
7121
|
+
const bodyKey = options?.key ?? "_csrf";
|
|
7122
|
+
const excluded = new Set(options?.excludeMethods ?? ["GET", "HEAD", "OPTIONS"]);
|
|
7123
|
+
return async (req, ctx, next) => {
|
|
7124
|
+
const method = req.method.toUpperCase();
|
|
7125
|
+
if (excluded.has(method)) {
|
|
7126
|
+
let token = getCookies(req)[cookieName];
|
|
7127
|
+
if (!token) {
|
|
7128
|
+
token = crypto.randomUUID();
|
|
7129
|
+
ctx.csrfToken = token;
|
|
7130
|
+
} else {
|
|
7131
|
+
;
|
|
7132
|
+
ctx.csrfToken = token;
|
|
7133
|
+
}
|
|
7134
|
+
const res = await next(req, ctx);
|
|
7135
|
+
const tokenToSet = ctx.csrfToken;
|
|
7136
|
+
if (tokenToSet && !getCookies(req)[cookieName]) {
|
|
7137
|
+
return setCookie(res, cookieName, tokenToSet, {
|
|
7138
|
+
httpOnly: true,
|
|
7139
|
+
sameSite: "strict",
|
|
7140
|
+
path: "/"
|
|
7141
|
+
});
|
|
7142
|
+
}
|
|
7143
|
+
return res;
|
|
7144
|
+
}
|
|
7145
|
+
const cookieToken = getCookies(req)[cookieName];
|
|
7146
|
+
let headerToken = req.headers.get(headerName);
|
|
7147
|
+
if (!headerToken) {
|
|
7148
|
+
try {
|
|
7149
|
+
const body = await req.clone().json();
|
|
7150
|
+
headerToken = body[bodyKey];
|
|
7151
|
+
} catch {
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
|
7155
|
+
return new Response("CSRF token mismatch", { status: 403 });
|
|
7156
|
+
}
|
|
7157
|
+
return next(req, ctx);
|
|
7158
|
+
};
|
|
7159
|
+
}
|
|
7160
|
+
|
|
6890
7161
|
// logdb/rest.ts
|
|
6891
7162
|
function createHandler(entries) {
|
|
6892
7163
|
return async (req, ctx) => {
|
|
@@ -7054,7 +7325,7 @@ function logdb(options) {
|
|
|
7054
7325
|
}
|
|
7055
7326
|
|
|
7056
7327
|
// iii/client.ts
|
|
7057
|
-
import
|
|
7328
|
+
import crypto6 from "node:crypto";
|
|
7058
7329
|
|
|
7059
7330
|
// iii/stream.ts
|
|
7060
7331
|
var channels = /* @__PURE__ */ new Map();
|
|
@@ -7580,7 +7851,7 @@ function iii(opts = {}) {
|
|
|
7580
7851
|
registerBuiltin("stream::send", (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id));
|
|
7581
7852
|
registerBuiltin("stream::update", (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops));
|
|
7582
7853
|
function addLocalWorker(worker) {
|
|
7583
|
-
const workerId =
|
|
7854
|
+
const workerId = crypto6.randomUUID();
|
|
7584
7855
|
const reg = {
|
|
7585
7856
|
id: workerId,
|
|
7586
7857
|
name: worker.name,
|
|
@@ -7595,7 +7866,7 @@ function iii(opts = {}) {
|
|
|
7595
7866
|
const triggerIds = [];
|
|
7596
7867
|
for (const t of worker.getTriggers()) {
|
|
7597
7868
|
if (t.input.function_id === fn.id) {
|
|
7598
|
-
const tid =
|
|
7869
|
+
const tid = crypto6.randomUUID();
|
|
7599
7870
|
triggers.set(tid, {
|
|
7600
7871
|
id: tid,
|
|
7601
7872
|
type: t.input.type,
|
|
@@ -7624,7 +7895,7 @@ function iii(opts = {}) {
|
|
|
7624
7895
|
if (!worker) return;
|
|
7625
7896
|
const handler = async (payload) => {
|
|
7626
7897
|
if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
|
|
7627
|
-
const invocationId =
|
|
7898
|
+
const invocationId = crypto6.randomUUID();
|
|
7628
7899
|
return new Promise((resolve11, reject) => {
|
|
7629
7900
|
const timer = setTimeout(() => {
|
|
7630
7901
|
pending.delete(invocationId);
|
|
@@ -7658,7 +7929,7 @@ function iii(opts = {}) {
|
|
|
7658
7929
|
}
|
|
7659
7930
|
const wsHandler = createWsHandler({
|
|
7660
7931
|
registerRemoteWorker(ws, name) {
|
|
7661
|
-
const id2 =
|
|
7932
|
+
const id2 = crypto6.randomUUID();
|
|
7662
7933
|
workers.set(id2, { id: id2, name, ws, functions: [], triggers: [] });
|
|
7663
7934
|
return id2;
|
|
7664
7935
|
},
|
|
@@ -7669,7 +7940,7 @@ function iii(opts = {}) {
|
|
|
7669
7940
|
addRemoteFunction(workerId, id2);
|
|
7670
7941
|
},
|
|
7671
7942
|
registerRemoteTrigger(workerId, input) {
|
|
7672
|
-
const tid =
|
|
7943
|
+
const tid = crypto6.randomUUID();
|
|
7673
7944
|
const reg = { id: tid, ...input, workerId };
|
|
7674
7945
|
triggers.set(tid, reg);
|
|
7675
7946
|
const worker = workers.get(workerId);
|
|
@@ -8034,6 +8305,7 @@ function registerWorker(url) {
|
|
|
8034
8305
|
};
|
|
8035
8306
|
}
|
|
8036
8307
|
export {
|
|
8308
|
+
Link,
|
|
8037
8309
|
Router,
|
|
8038
8310
|
TsxContext,
|
|
8039
8311
|
agent,
|
|
@@ -8046,6 +8318,7 @@ export {
|
|
|
8046
8318
|
createSSEStream,
|
|
8047
8319
|
createTestServer,
|
|
8048
8320
|
createWorker,
|
|
8321
|
+
csrf,
|
|
8049
8322
|
defineConfig,
|
|
8050
8323
|
deleteCookie,
|
|
8051
8324
|
deploy,
|
|
@@ -8066,6 +8339,7 @@ export {
|
|
|
8066
8339
|
logger,
|
|
8067
8340
|
mailer,
|
|
8068
8341
|
messager,
|
|
8342
|
+
navigate,
|
|
8069
8343
|
openai,
|
|
8070
8344
|
opencode,
|
|
8071
8345
|
postgres,
|
|
@@ -8088,7 +8362,10 @@ export {
|
|
|
8088
8362
|
tool2 as tool,
|
|
8089
8363
|
tsx,
|
|
8090
8364
|
upload,
|
|
8365
|
+
useAction,
|
|
8366
|
+
useNavigate,
|
|
8091
8367
|
useTsx,
|
|
8368
|
+
useWebsocket,
|
|
8092
8369
|
user,
|
|
8093
8370
|
validate
|
|
8094
8371
|
};
|
package/dist/serve.d.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface UseActionOptions<T = any> {
|
|
2
|
+
method?: string;
|
|
3
|
+
headers?: Record<string, string>;
|
|
4
|
+
onSuccess?: (data: T) => void;
|
|
5
|
+
onError?: (err: Error) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface UseActionReturn<T = any> {
|
|
8
|
+
submit: (body?: any) => Promise<T | undefined>;
|
|
9
|
+
data: T | null;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
pending: boolean;
|
|
12
|
+
reset: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function useAction<T = any>(url: string | URL, options?: UseActionOptions<T>): UseActionReturn<T>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type UseWebsocketOptions = {
|
|
2
|
+
onMessage?: (data: string) => void;
|
|
3
|
+
reconnect?: boolean | {
|
|
4
|
+
maxRetries?: number;
|
|
5
|
+
delay?: number;
|
|
6
|
+
};
|
|
7
|
+
protocols?: string | string[];
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type UseWebsocketReturn = {
|
|
11
|
+
send: (data: string | ArrayBuffer | Blob) => void;
|
|
12
|
+
close: () => void;
|
|
13
|
+
readyState: number;
|
|
14
|
+
lastMessage: string | null;
|
|
15
|
+
reconnect: () => void;
|
|
16
|
+
};
|
|
17
|
+
export declare function useWebsocket(url: string | URL | (() => string | URL | null), options?: UseWebsocketOptions): UseWebsocketReturn;
|