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/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
- `let el=createElement(P,p);`,
972
- `hydrateRoot(document.getElementById('__weifuwu_root'),el);`
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(TsxContext.Provider, {
1032
- value: { params: ctx.params, query: ctx.query, user: ctx.user, parsed: ctx.parsed }
1033
- }, createElement(Component, allProps));
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, createElement("div", { id: "__weifuwu_root" }, element))
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 window = options?.window ?? 6e4;
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
- }, window);
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 + window });
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 + window) / 1e3)));
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 crypto from "node:crypto";
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 ?? (() => crypto.randomUUID());
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 crypto2 from "node:crypto";
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 = crypto2.randomUUID();
2972
- const clientSecret = crypto2.randomBytes(32).toString("hex");
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 = crypto2.randomUUID();
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 = crypto2.createHash("sha256").update(codeVerifier).digest().toString("base64url");
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 = crypto2.randomUUID();
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 crypto3 from "node:crypto";
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: crypto3.randomUUID(), runAt: nextRun, createdAt: Date.now() };
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 = crypto3.randomUUID();
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 crypto4 from "node:crypto";
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 || !crypto4.timingSafeEqual(tokenBuf, secretBuf)) {
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=${crypto4.createHmac("sha256", config.webhookSecret).update(rawBody).digest("hex")}`;
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 || !crypto4.timingSafeEqual(sigBuf, expectedBuf)) {
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 history = await getHistory(sql2, sessionId);
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: history,
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 history = await getHistory(sql2, session_id);
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: history,
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 crypto5 from "node:crypto";
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 = crypto5.randomUUID();
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 = crypto5.randomUUID();
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 = crypto5.randomUUID();
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 = crypto5.randomUUID();
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 = crypto5.randomUUID();
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
@@ -7,6 +7,7 @@ export interface ServeOptions {
7
7
  signal?: AbortSignal;
8
8
  websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
9
9
  maxBodySize?: number;
10
+ shutdown?: boolean;
10
11
  }
11
12
  export interface Server {
12
13
  stop: () => void;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
4
4
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",