weifuwu 0.9.4 → 0.9.6

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/ai.d.ts CHANGED
@@ -1,7 +1,19 @@
1
- import { streamText } from 'ai';
2
1
  import type { Context } from './types.ts';
3
2
  import { Router } from './router.ts';
4
- type StreamTextParams = Parameters<typeof streamText>[0];
3
+ type StreamTextParams = {
4
+ model: unknown;
5
+ prompt?: string;
6
+ system?: string;
7
+ messages?: unknown[];
8
+ maxTokens?: number;
9
+ temperature?: number;
10
+ [key: string]: unknown;
11
+ };
5
12
  export type AIHandler = (req: Request, ctx: Context) => StreamTextParams | Promise<StreamTextParams>;
6
- export declare function ai(handler: AIHandler): Router;
13
+ export declare const _ai: {
14
+ streamText: (params: StreamTextParams) => {
15
+ toTextStreamResponse: () => Response;
16
+ };
17
+ };
18
+ export declare function ai(handler: AIHandler): Promise<Router>;
7
19
  export {};
package/dist/index.js CHANGED
@@ -41,7 +41,12 @@ function createRequest(req, body) {
41
41
  async function sendResponse(res, response) {
42
42
  const headers = {};
43
43
  response.headers.forEach((value, key) => {
44
- headers[key] = value;
44
+ if (key.toLowerCase() === "set-cookie") {
45
+ const existing = headers[key];
46
+ headers[key] = existing ? Array.isArray(existing) ? [...existing, value] : [existing, value] : value;
47
+ } else {
48
+ headers[key] = value;
49
+ }
45
50
  });
46
51
  res.writeHead(response.status, response.statusText, headers);
47
52
  if (response.body) {
@@ -104,6 +109,10 @@ function serve(handler, options) {
104
109
  server.close();
105
110
  }, { once: true });
106
111
  }
112
+ server.on("error", (err) => {
113
+ console.error("Failed to start server:", err.message);
114
+ process.exit(1);
115
+ });
107
116
  server.listen(port, hostname, () => {
108
117
  resolveReady();
109
118
  });
@@ -167,6 +176,10 @@ var matchTrieNode = (node, segment, params) => {
167
176
  return null;
168
177
  };
169
178
  var getWsNode = (node, segment) => {
179
+ if (segment === "*") {
180
+ node.wildcard = true;
181
+ return node;
182
+ }
170
183
  if (segment.startsWith(":")) {
171
184
  if (!node.children.has(":")) {
172
185
  const child2 = createWsNode();
@@ -193,6 +206,7 @@ var matchWsNode = (node, segment, params) => {
193
206
  if (child.param) params[child.param] = segment;
194
207
  return child;
195
208
  }
209
+ if (node.wildcard) return node;
196
210
  return null;
197
211
  };
198
212
  var Router = class _Router {
@@ -200,6 +214,7 @@ var Router = class _Router {
200
214
  wsRoot = createWsNode();
201
215
  globalMws = [];
202
216
  errorHandler;
217
+ wss = new WebSocketServer({ noServer: true });
203
218
  use(arg1, arg2) {
204
219
  if (typeof arg1 === "string") {
205
220
  if (arg2 instanceof _Router) {
@@ -255,6 +270,10 @@ var Router = class _Router {
255
270
  let node = this.root;
256
271
  for (const segment of segments) {
257
272
  if (segment === "*") {
273
+ const remaining = segments.indexOf("*") < segments.length - 1;
274
+ if (remaining) {
275
+ console.warn(`Route "${path2}": segments after "*" are ignored`);
276
+ }
258
277
  node.wildcard = true;
259
278
  node.handlers.set(method, handler);
260
279
  if (middlewares.length > 0) node.middlewares.set(method, middlewares);
@@ -285,7 +304,6 @@ var Router = class _Router {
285
304
  };
286
305
  }
287
306
  websocketHandler() {
288
- const wss = new WebSocketServer({ noServer: true });
289
307
  const wsRoot = this.wsRoot;
290
308
  const router = this;
291
309
  return (req, socket, head) => {
@@ -302,7 +320,7 @@ var Router = class _Router {
302
320
  });
303
321
  const ctx = { params: match.params, query };
304
322
  if (match.middlewares.length === 0) {
305
- upgradeSocket(wss, req, socket, head, match.handler, ctx);
323
+ upgradeSocket(router.wss, req, socket, head, match.handler, ctx);
306
324
  return;
307
325
  }
308
326
  let index = 0;
@@ -313,7 +331,7 @@ var Router = class _Router {
313
331
  }
314
332
  return await new Promise((resolve3) => {
315
333
  try {
316
- upgradeSocket(wss, req, socket, head, match.handler, ctx2);
334
+ upgradeSocket(router.wss, req, socket, head, match.handler, ctx2);
317
335
  resolve3(new Response(null, { status: 101 }));
318
336
  } catch {
319
337
  socket.destroy();
@@ -346,7 +364,7 @@ var Router = class _Router {
346
364
  matchTrie(method, segments) {
347
365
  let node = this.root;
348
366
  const params = {};
349
- const pathMws = [...this.root.pathMws];
367
+ const pathMws = [];
350
368
  let wildcardHandler = null;
351
369
  let wildcardMws = [];
352
370
  let wildcardIdx = -1;
@@ -380,6 +398,7 @@ var Router = class _Router {
380
398
  }
381
399
  node = next;
382
400
  }
401
+ pathMws.push(...node.pathMws);
383
402
  if (node.subRouter) {
384
403
  return {
385
404
  pathMws,
@@ -388,7 +407,6 @@ var Router = class _Router {
388
407
  subRouter: { router: node.subRouter, remainingIdx: segments.length }
389
408
  };
390
409
  }
391
- pathMws.push(...node.pathMws);
392
410
  const handler = node.handlers.get(method) || node.handlers.get("*");
393
411
  if (handler) {
394
412
  if (node.wildcard) params["*"] = segments.slice(segments.length).join("/");
@@ -422,10 +440,16 @@ var Router = class _Router {
422
440
  const remainingSegments = segments.slice(remainingIdx);
423
441
  const delegate = (req2, ctx2) => sub.handle(req2, ctx2, remainingSegments, query);
424
442
  const allMws = this.globalMws.length + match.pathMws.length === 0 ? [] : [...this.globalMws, ...match.pathMws];
443
+ const levelMount = "/" + segments.slice(0, remainingIdx).join("/");
425
444
  try {
426
- return await this.runChain(allMws, delegate, req, { ...ctx, params: { ...ctx.params, ...match.params } });
445
+ return await this.runChain(allMws, delegate, req, {
446
+ ...ctx,
447
+ params: { ...ctx.params, ...match.params },
448
+ mountPath: (ctx.mountPath || "") + levelMount
449
+ });
427
450
  } catch (e) {
428
- return this.errorHandler ? this.errorHandler(e, req, ctx) : new Response("Internal Server Error", { status: 500 });
451
+ const err = e instanceof Error ? e : new Error(String(e));
452
+ return this.errorHandler ? this.errorHandler(err, req, ctx) : new Response("Internal Server Error", { status: 500 });
429
453
  }
430
454
  }
431
455
  if (match?.handler) {
@@ -435,7 +459,8 @@ var Router = class _Router {
435
459
  try {
436
460
  return await this.runChain(allMws, handler, req, ctxWithMatch);
437
461
  } catch (e) {
438
- return this.errorHandler ? this.errorHandler(e, req, ctxWithMatch) : new Response("Internal Server Error", { status: 500 });
462
+ const err = e instanceof Error ? e : new Error(String(e));
463
+ return this.errorHandler ? this.errorHandler(err, req, ctxWithMatch) : new Response("Internal Server Error", { status: 500 });
439
464
  }
440
465
  }
441
466
  if (this.globalMws.length > 0) {
@@ -443,7 +468,8 @@ var Router = class _Router {
443
468
  const delegate = () => new Response("Not Found", { status: 404 });
444
469
  return await this.runChain(this.globalMws, delegate, req, ctx);
445
470
  } catch (e) {
446
- return this.errorHandler ? this.errorHandler(e, req, ctx) : new Response("Internal Server Error", { status: 500 });
471
+ const err = e instanceof Error ? e : new Error(String(e));
472
+ return this.errorHandler ? this.errorHandler(err, req, ctx) : new Response("Internal Server Error", { status: 500 });
447
473
  }
448
474
  }
449
475
  return new Response("Not Found", { status: 404 });
@@ -528,8 +554,13 @@ var isDev = process.env.NODE_ENV !== "production";
528
554
  var _uiDir = "";
529
555
  var _allFiles = [];
530
556
  var _outDir = "";
557
+ var _router = null;
558
+ var _pagesDir = "";
559
+ var _nodeEntries = {};
531
560
  var tailwindCssUrl = null;
532
561
  var tailwindCssCode = "";
562
+ var _tailwindPlugin = null;
563
+ var _postcss = null;
533
564
  var _cjsRequire = createRequire(import.meta.url);
534
565
  var _vmCtx = vm.createContext(Object.create(globalThis));
535
566
  function loadSSRModule(code) {
@@ -708,7 +739,7 @@ function startFileWatcher() {
708
739
  let timeout = null;
709
740
  const pending = /* @__PURE__ */ new Set();
710
741
  chokidar.watch(_uiDir, {
711
- ignored: /(^|[/\\])\.(?!\.)|node_modules|\.weifuwu|dist/,
742
+ ignored: /(^|[/\\])\.(?!\.)|node_modules|[/\\]\.weifuwu[/\\]|[/\\]dist[/\\]/,
712
743
  persistent: false,
713
744
  ignoreInitial: true
714
745
  }).on("all", async (event, filePath) => {
@@ -726,6 +757,8 @@ function startFileWatcher() {
726
757
  );
727
758
  if (allKnown) {
728
759
  for (const f of exists) await recompileAndSwap(f, _outDir);
760
+ await reprocessTailwind();
761
+ broadcastReload();
729
762
  } else {
730
763
  await recompileAll();
731
764
  }
@@ -751,6 +784,7 @@ async function recompileAndSwap(filePath, outDir) {
751
784
  const name15 = basename(filePath);
752
785
  if (name15 === "layout.tsx") {
753
786
  layoutModules.set(filePath, mod);
787
+ clientBundleCache.clear();
754
788
  } else if (name15 === "route.ts") {
755
789
  const handlers = /* @__PURE__ */ new Map();
756
790
  for (const m of ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) {
@@ -763,14 +797,30 @@ async function recompileAndSwap(filePath, outDir) {
763
797
  pageModules.set(filePath, mod);
764
798
  clientBundleCache.delete(id(filePath));
765
799
  }
766
- await reprocessTailwind();
767
- broadcastReload();
768
800
  } catch (err) {
769
801
  console.error("recompile failed:", err.message);
770
802
  }
771
803
  }
772
804
  async function recompileAll() {
773
805
  try {
806
+ const freshPages = scanPages(_pagesDir);
807
+ const freshFiles = /* @__PURE__ */ new Set();
808
+ const nodeEntries = {};
809
+ for (const p of freshPages) {
810
+ const nodeKey = p.entryPath || p.routePath || "";
811
+ nodeEntries[nodeKey] = { route: p.route, entryPath: p.entryPath, layouts: p.layouts, loadPath: p.loadPath, routePath: p.routePath };
812
+ if (p.entryPath) freshFiles.add(p.entryPath);
813
+ if (p.loadPath) freshFiles.add(p.loadPath);
814
+ for (const lp of p.layouts) freshFiles.add(lp);
815
+ if (p.routePath) freshFiles.add(p.routePath);
816
+ }
817
+ const nfPath = join(_pagesDir, "not-found.tsx");
818
+ if (existsSync(nfPath)) {
819
+ freshFiles.add(nfPath);
820
+ const rootLayouts = resolveLayouts(_pagesDir, _pagesDir);
821
+ for (const lp of rootLayouts) freshFiles.add(lp);
822
+ }
823
+ _allFiles = [...freshFiles];
774
824
  const result = await esbuild.build({
775
825
  entryPoints: Object.fromEntries(_allFiles.map((f) => [id(f), f])),
776
826
  outdir: _outDir,
@@ -789,9 +839,54 @@ async function recompileAll() {
789
839
  const srcPath = _allFiles.find((f) => file.path.endsWith(id(f) + ".js"));
790
840
  if (!srcPath) continue;
791
841
  const name15 = basename(srcPath);
792
- if (name15 === "layout.tsx") layoutModules.set(srcPath, mod);
793
- else if (name15 === "load.ts") loadModules.set(srcPath, mod);
794
- else pageModules.set(srcPath, mod);
842
+ if (name15 === "layout.tsx") {
843
+ layoutModules.set(srcPath, mod);
844
+ } else if (name15 === "route.ts") {
845
+ const handlers = /* @__PURE__ */ new Map();
846
+ for (const m of ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]) {
847
+ if (mod[m]) handlers.set(m, mod[m]);
848
+ }
849
+ routeModules.set(srcPath, handlers);
850
+ } else if (name15 === "load.ts") {
851
+ loadModules.set(srcPath, mod);
852
+ } else if (name15 !== "not-found.tsx") {
853
+ pageModules.set(srcPath, mod);
854
+ }
855
+ }
856
+ if (_router) {
857
+ const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
858
+ for (const [key, entry] of Object.entries(nodeEntries)) {
859
+ if (_nodeEntries[key]) continue;
860
+ if (entry.routePath && !entry.entryPath) {
861
+ _router.route(
862
+ "GET",
863
+ entry.route,
864
+ (req, ctx) => routeModules.get(entry.routePath)?.get("GET")?.(req, ctx) ?? new Response("", { status: 501 })
865
+ );
866
+ for (const m of methods) {
867
+ _router.route(
868
+ m,
869
+ entry.route,
870
+ (req, ctx) => routeModules.get(entry.routePath)?.get(m)?.(req, ctx) ?? new Response("", { status: 501 })
871
+ );
872
+ }
873
+ }
874
+ if (entry.entryPath) {
875
+ const handler = makeSsrHandler(entry.entryPath, entry.layouts, entry.loadPath, _pagesDir, _router);
876
+ _router.get(entry.route, handler);
877
+ if (entry.routePath) {
878
+ for (const m of methods) {
879
+ _router.route(
880
+ m,
881
+ entry.route,
882
+ (req, ctx) => routeModules.get(entry.routePath)?.get(m)?.(req, ctx) ?? new Response("", { status: 501 })
883
+ );
884
+ }
885
+ }
886
+ }
887
+ console.log("\u2139 weifuwu/tsx: registered new route " + entry.route);
888
+ }
889
+ _nodeEntries = nodeEntries;
795
890
  }
796
891
  clientBundleCache.clear();
797
892
  await reprocessTailwind();
@@ -801,10 +896,9 @@ async function recompileAll() {
801
896
  }
802
897
  }
803
898
  async function setupTailwind(uiDir, router) {
804
- let tailwindPlugin, postcss;
805
899
  try {
806
- tailwindPlugin = (await import("@tailwindcss/postcss")).default;
807
- postcss = (await import("postcss")).default;
900
+ _tailwindPlugin = (await import("@tailwindcss/postcss")).default;
901
+ _postcss = (await import("postcss")).default;
808
902
  } catch {
809
903
  return;
810
904
  }
@@ -816,7 +910,7 @@ async function setupTailwind(uiDir, router) {
816
910
  }
817
911
  try {
818
912
  const src = readFileSync(inputFile, "utf-8");
819
- const result = await postcss([tailwindPlugin()]).process(src, { from: inputFile });
913
+ const result = await _postcss([_tailwindPlugin()]).process(src, { from: inputFile });
820
914
  tailwindCssCode = result.css;
821
915
  } catch (err) {
822
916
  console.warn("Tailwind CSS processing failed:", err.message);
@@ -830,7 +924,7 @@ async function setupTailwind(uiDir, router) {
830
924
  chokidar.watch(inputFile, { persistent: false }).on("change", async () => {
831
925
  try {
832
926
  const newSrc = readFileSync(inputFile, "utf-8");
833
- const newResult = await postcss([tailwindPlugin()]).process(newSrc, { from: inputFile });
927
+ const newResult = await _postcss([_tailwindPlugin()]).process(newSrc, { from: inputFile });
834
928
  tailwindCssCode = newResult.css;
835
929
  broadcastReload();
836
930
  } catch (err) {
@@ -840,62 +934,77 @@ async function setupTailwind(uiDir, router) {
840
934
  }
841
935
  }
842
936
  async function reprocessTailwind() {
843
- if (!tailwindCssUrl) return;
937
+ if (!tailwindCssUrl || !_postcss || !_tailwindPlugin) return;
844
938
  try {
845
939
  const inputFile = resolve(_uiDir, "app.css");
846
940
  if (!existsSync(inputFile)) return;
847
- const tailwindPlugin = (await import("@tailwindcss/postcss")).default;
848
- const postcss = (await import("postcss")).default;
849
941
  const src = readFileSync(inputFile, "utf-8");
850
- const result = await postcss([tailwindPlugin()]).process(src, { from: inputFile });
942
+ const result = await _postcss([_tailwindPlugin()]).process(src, { from: inputFile });
851
943
  tailwindCssCode = result.css;
852
944
  } catch {
853
945
  }
854
946
  }
855
947
  var clientBundleCache = /* @__PURE__ */ new Map();
856
948
  var clientRouteLog = /* @__PURE__ */ new WeakMap();
949
+ var clientBuildParams = /* @__PURE__ */ new Map();
950
+ async function buildClientBundle(entryPath, layoutPaths, pagesDir) {
951
+ try {
952
+ const nested = layoutPaths.slice(1);
953
+ const layoutsImport = nested.map(
954
+ (p, i) => `import L${i} from${JSON.stringify(p)};`
955
+ ).join("");
956
+ const layoutsWrap = nested.map((_, i) => {
957
+ const idx = nested.length - 1 - i;
958
+ return `el=createElement(L${idx},null,el);`;
959
+ }).join("");
960
+ const code = [
961
+ `import{hydrateRoot}from'react-dom/client';`,
962
+ `import{createElement}from'react';`,
963
+ `import P from${JSON.stringify(entryPath)};`,
964
+ layoutsImport,
965
+ `const p=window.__WEIFUWU_PROPS;`,
966
+ `let el=createElement(P,p);`,
967
+ layoutsWrap,
968
+ `hydrateRoot(document.getElementById('__weifuwu_root'),el);`
969
+ ].join("");
970
+ const result = await esbuild.build({
971
+ stdin: { contents: code, loader: "tsx", resolveDir: pagesDir },
972
+ bundle: true,
973
+ format: "esm",
974
+ jsx: "automatic",
975
+ jsxImportSource: "react",
976
+ alias: resolveAliases(),
977
+ write: false,
978
+ minify: true
979
+ });
980
+ return result.outputFiles[0].contents;
981
+ } catch (err) {
982
+ console.error("hydration bundle failed:", err);
983
+ return null;
984
+ }
985
+ }
857
986
  async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router) {
858
987
  const key = id(entryPath);
859
988
  const url = `/__wfw/client/${key}.js`;
989
+ clientBuildParams.set(key, { entryPath, layoutPaths, pagesDir });
860
990
  if (!clientRouteLog.get(router)?.has(url)) {
861
991
  if (!clientBundleCache.has(key)) {
862
- try {
863
- const nested = layoutPaths.slice(1);
864
- const layoutsImport = nested.map(
865
- (p, i) => `import L${i} from${JSON.stringify(p)};`
866
- ).join("");
867
- const layoutsWrap = nested.map((_, i) => {
868
- const idx = nested.length - 1 - i;
869
- return `el=createElement(L${idx},null,el);`;
870
- }).join("");
871
- const code = [
872
- `import{hydrateRoot}from'react-dom/client';`,
873
- `import{createElement}from'react';`,
874
- `import P from${JSON.stringify(entryPath)};`,
875
- layoutsImport,
876
- `const p=window.__WEIFUWU_PROPS;`,
877
- `let el=createElement(P,p);`,
878
- layoutsWrap,
879
- `hydrateRoot(document.getElementById('__weifuwu_root'),el);`
880
- ].join("");
881
- const result = await esbuild.build({
882
- stdin: { contents: code, loader: "tsx", resolveDir: pagesDir },
883
- bundle: true,
884
- format: "esm",
885
- jsx: "automatic",
886
- jsxImportSource: "react",
887
- alias: resolveAliases(),
888
- write: false,
889
- minify: true
890
- });
891
- clientBundleCache.set(key, result.outputFiles[0].contents);
892
- } catch (err) {
893
- console.error("hydration bundle failed:", err);
894
- return null;
992
+ const buf = await buildClientBundle(entryPath, layoutPaths, pagesDir);
993
+ if (!buf) return null;
994
+ clientBundleCache.set(key, buf);
995
+ }
996
+ router.get(url, async () => {
997
+ let buf = clientBundleCache.get(key);
998
+ if (!buf) {
999
+ const params = clientBuildParams.get(key);
1000
+ if (params) {
1001
+ const rebuilt = await buildClientBundle(params.entryPath, params.layoutPaths, params.pagesDir);
1002
+ if (rebuilt) {
1003
+ clientBundleCache.set(key, rebuilt);
1004
+ buf = rebuilt;
1005
+ }
1006
+ }
895
1007
  }
896
- }
897
- router.get(url, () => {
898
- const buf = clientBundleCache.get(key);
899
1008
  return buf ? new Response(buf, {
900
1009
  headers: { "content-type": "application/javascript; charset=utf-8" }
901
1010
  }) : new Response("", { status: 500 });
@@ -908,6 +1017,7 @@ async function getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router)
908
1017
  }
909
1018
  function makeSsrHandler(entryPath, layoutPaths, loadPath, pagesDir, router) {
910
1019
  return async (req, ctx) => {
1020
+ const base = (ctx.mountPath || "").replace(/\/$/, "");
911
1021
  const pageMod = pageModules.get(entryPath);
912
1022
  if (!pageMod) return new Response("", { status: 500 });
913
1023
  const Component = pageMod.default;
@@ -936,21 +1046,21 @@ function makeSsrHandler(entryPath, layoutPaths, loadPath, pagesDir, router) {
936
1046
  scripts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(allProps)}</script>`);
937
1047
  const bundle = await getOrBuildClientBundle(entryPath, layoutPaths, pagesDir, router);
938
1048
  if (bundle) {
939
- scripts.push(`<script type="module" src="${bundle.url}"></script>`);
1049
+ scripts.push(`<script type="module" src="${base}${bundle.url}"></script>`);
940
1050
  }
941
- let html = `<!DOCTYPE html>
942
- ${body}
943
- ${scripts.join("\n")}`;
1051
+ let html = body.startsWith("<!DOCTYPE html>") ? body : `<!DOCTYPE html>
1052
+ ${body}`;
1053
+ html += "\n" + scripts.join("\n");
944
1054
  if (tailwindCssUrl && html.includes("</head>")) {
945
1055
  html = html.replace(
946
1056
  "</head>",
947
- `<link rel="stylesheet" href="${tailwindCssUrl}" />
1057
+ `<link rel="stylesheet" href="${base}${tailwindCssUrl}" />
948
1058
  </head>`
949
1059
  );
950
1060
  }
951
1061
  if (isDev) {
952
1062
  html += `
953
- <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
1063
+ <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${base}/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
954
1064
  }
955
1065
  return new Response(html, {
956
1066
  headers: { "content-type": "text/html; charset=utf-8" }
@@ -961,6 +1071,7 @@ async function tsx(options) {
961
1071
  const uiDir = resolve(options.dir);
962
1072
  const pagesDir = existsSync(join(uiDir, "pages")) ? join(uiDir, "pages") : uiDir;
963
1073
  _uiDir = uiDir;
1074
+ _pagesDir = pagesDir;
964
1075
  const outDir = join(uiDir, ".weifuwu", "ssr");
965
1076
  _outDir = outDir;
966
1077
  const pages = scanPages(pagesDir);
@@ -983,8 +1094,11 @@ async function tsx(options) {
983
1094
  _allFiles = [...allFiles];
984
1095
  await compileAll(_allFiles, outDir, "node", resolveAliases());
985
1096
  const router = new Router();
1097
+ _router = router;
986
1098
  const methods = ["POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
987
1099
  for (const p of pages) {
1100
+ const nodeKey = p.entryPath || p.routePath || "";
1101
+ _nodeEntries[nodeKey] = { route: p.route, entryPath: p.entryPath || "", layouts: p.layouts, loadPath: p.loadPath, routePath: p.routePath };
988
1102
  if (p.routeOnly && p.routePath) {
989
1103
  const rUrl = compiledUrl(p.routePath, outDir);
990
1104
  const modR = await import(rUrl);
@@ -1049,6 +1163,7 @@ async function tsx(options) {
1049
1163
  }
1050
1164
  }
1051
1165
  const handler = async (req, ctx) => {
1166
+ const base = (ctx.mountPath || "").replace(/\/$/, "");
1052
1167
  const nfMod = pageModules.get(nfPath);
1053
1168
  if (!nfMod) return new Response("Not Found", { status: 404 });
1054
1169
  const NfComponent = nfMod.default;
@@ -1063,18 +1178,18 @@ async function tsx(options) {
1063
1178
  }, element);
1064
1179
  const stream = await renderToReadableStream(element);
1065
1180
  const body = await readStream(stream);
1066
- let html = `<!DOCTYPE html>
1181
+ let html = body.startsWith("<!DOCTYPE html>") ? body : `<!DOCTYPE html>
1067
1182
  ${body}`;
1068
1183
  if (tailwindCssUrl && html.includes("</head>")) {
1069
1184
  html = html.replace(
1070
1185
  "</head>",
1071
- `<link rel="stylesheet" href="${tailwindCssUrl}" />
1186
+ `<link rel="stylesheet" href="${base}${tailwindCssUrl}" />
1072
1187
  </head>`
1073
1188
  );
1074
1189
  }
1075
1190
  if (isDev) {
1076
1191
  html += `
1077
- <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
1192
+ <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${base}/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`;
1078
1193
  }
1079
1194
  return new Response(html, {
1080
1195
  status: 404,
@@ -1120,7 +1235,12 @@ function cors(options) {
1120
1235
  ...options
1121
1236
  };
1122
1237
  function resolveOrigin(requestOrigin) {
1123
- if (typeof opts.origin === "string") return opts.origin === "*" ? "*" : opts.origin;
1238
+ if (typeof opts.origin === "string") {
1239
+ if (opts.origin === "*") {
1240
+ return opts.credentials ? requestOrigin : "*";
1241
+ }
1242
+ return opts.origin;
1243
+ }
1124
1244
  if (Array.isArray(opts.origin)) {
1125
1245
  return opts.origin.includes(requestOrigin) ? requestOrigin : "";
1126
1246
  }
@@ -1135,7 +1255,7 @@ function cors(options) {
1135
1255
  headers.set("Access-Control-Allow-Origin", acao);
1136
1256
  if (opts.credentials) headers.set("Access-Control-Allow-Credentials", "true");
1137
1257
  if (opts.exposedHeaders?.length) headers.set("Access-Control-Expose-Headers", opts.exposedHeaders.join(", "));
1138
- headers.set("Vary", "Origin");
1258
+ if (acao !== "*") headers.set("Vary", "Origin");
1139
1259
  return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
1140
1260
  }
1141
1261
  return (req, ctx, next) => {
@@ -1148,7 +1268,7 @@ function cors(options) {
1148
1268
  headers.set("Access-Control-Allow-Headers", opts.allowedHeaders.join(", "));
1149
1269
  if (opts.credentials) headers.set("Access-Control-Allow-Credentials", "true");
1150
1270
  if (opts.maxAge != null) headers.set("Access-Control-Max-Age", String(opts.maxAge));
1151
- headers.set("Vary", "Origin");
1271
+ if (acao !== "*") headers.set("Vary", "Origin");
1152
1272
  return new Response(null, { status: 204, headers });
1153
1273
  }
1154
1274
  if (!acao) return next(req, ctx);
@@ -1156,20 +1276,23 @@ function cors(options) {
1156
1276
  };
1157
1277
  }
1158
1278
  function auth(options) {
1279
+ if (!options.token && !options.verify && !options.proxy) {
1280
+ throw new Error("auth() requires at least one of: token, verify, or proxy");
1281
+ }
1159
1282
  return async (req, ctx, next) => {
1160
1283
  const headerName = options.header ?? "Authorization";
1161
1284
  let from = "header";
1162
1285
  let header = req.headers.get(headerName);
1163
1286
  let token = "";
1164
1287
  if (header) {
1165
- token = header;
1288
+ token = header.trim();
1166
1289
  if (headerName.toLowerCase() === "authorization") {
1167
1290
  const parts = header.split(" ");
1168
1291
  if (parts[0]?.toLowerCase() === "bearer") {
1169
- token = parts.slice(1).join(" ");
1292
+ token = parts.slice(1).join(" ").trim();
1170
1293
  }
1171
1294
  }
1172
- } else {
1295
+ } else if (!options.header) {
1173
1296
  const url = new URL(req.url);
1174
1297
  const qsToken = url.searchParams.get("access_token");
1175
1298
  if (qsToken) {
@@ -1184,7 +1307,12 @@ function auth(options) {
1184
1307
  });
1185
1308
  }
1186
1309
  if (options.proxy) {
1187
- const proxyUrl = typeof options.proxy === "string" ? new URL(options.proxy) : options.proxy;
1310
+ let proxyUrl;
1311
+ try {
1312
+ proxyUrl = typeof options.proxy === "string" ? new URL(options.proxy) : options.proxy;
1313
+ } catch {
1314
+ return new Response("Invalid proxy URL", { status: 500 });
1315
+ }
1188
1316
  const proxyHeaders = {};
1189
1317
  if (from === "header" && header) {
1190
1318
  proxyHeaders[headerName] = header;
@@ -1234,7 +1362,7 @@ function auth(options) {
1234
1362
 
1235
1363
  // static.ts
1236
1364
  import { createHash as createHash2 } from "node:crypto";
1237
- import { open } from "node:fs/promises";
1365
+ import { open, realpath } from "node:fs/promises";
1238
1366
  import { extname, resolve as resolve2, normalize, sep as sep2 } from "node:path";
1239
1367
  function serveStatic(root, options) {
1240
1368
  const rootDir = resolve2(root);
@@ -1252,7 +1380,12 @@ function serveStatic(root, options) {
1252
1380
  let fileHandle;
1253
1381
  try {
1254
1382
  fileHandle = await open(filePath, "r");
1255
- const stat = await fileHandle.stat();
1383
+ let stat = await fileHandle.stat();
1384
+ const realPath = await realpath(filePath);
1385
+ if (!realPath.startsWith(rootDir + sep2) && realPath !== rootDir) {
1386
+ await fileHandle.close();
1387
+ return new Response("Forbidden", { status: 403 });
1388
+ }
1256
1389
  if (stat.isDirectory()) {
1257
1390
  await fileHandle.close();
1258
1391
  const indexFile = opts.index ?? "index.html";
@@ -1261,8 +1394,8 @@ function serveStatic(root, options) {
1261
1394
  return new Response("Forbidden", { status: 403 });
1262
1395
  }
1263
1396
  fileHandle = await open(filePath, "r");
1264
- const dirStat = await fileHandle.stat();
1265
- if (!dirStat.isFile()) {
1397
+ stat = await fileHandle.stat();
1398
+ if (!stat.isFile()) {
1266
1399
  await fileHandle.close();
1267
1400
  return new Response("Not Found", { status: 404 });
1268
1401
  }
@@ -1302,8 +1435,8 @@ var MIME_TYPES = {
1302
1435
  ".html": "text/html; charset=utf-8",
1303
1436
  ".htm": "text/html; charset=utf-8",
1304
1437
  ".css": "text/css; charset=utf-8",
1305
- ".js": "application/javascript; charset=utf-8",
1306
- ".mjs": "application/javascript; charset=utf-8",
1438
+ ".js": "text/javascript; charset=utf-8",
1439
+ ".mjs": "text/javascript; charset=utf-8",
1307
1440
  ".json": "application/json",
1308
1441
  ".png": "image/png",
1309
1442
  ".jpg": "image/jpeg",
@@ -1323,7 +1456,16 @@ var MIME_TYPES = {
1323
1456
  ".pdf": "application/pdf",
1324
1457
  ".zip": "application/zip",
1325
1458
  ".wasm": "application/wasm",
1326
- ".map": "application/json"
1459
+ ".map": "application/json",
1460
+ ".ts": "application/x-typescript",
1461
+ ".tsx": "application/x-typescript",
1462
+ ".md": "text/markdown; charset=utf-8",
1463
+ ".yaml": "application/x-yaml",
1464
+ ".yml": "application/x-yaml",
1465
+ ".csv": "text/csv; charset=utf-8",
1466
+ ".mp4": "video/mp4",
1467
+ ".mp3": "audio/mpeg",
1468
+ ".wav": "audio/wav"
1327
1469
  };
1328
1470
 
1329
1471
  // validate.ts
@@ -1369,18 +1511,28 @@ function validate(schemas) {
1369
1511
  }
1370
1512
  }
1371
1513
  if (schemas.body) {
1372
- if (req.body === null) {
1514
+ if (req.method === "GET" || req.method === "HEAD") {
1515
+ } else if (req.body === null) {
1373
1516
  issues.push({ path: ["body"], message: "Request body is required" });
1374
1517
  } else {
1375
1518
  const bodyText = await req.text();
1376
- if (!bodyText && req.method !== "GET" && req.method !== "HEAD") {
1519
+ if (!bodyText) {
1377
1520
  issues.push({ path: ["body"], message: "Request body is required" });
1378
1521
  } else {
1379
- let bodyValue;
1380
- try {
1381
- bodyValue = JSON.parse(bodyText);
1382
- } catch {
1522
+ const ct = req.headers.get("content-type") ?? "";
1523
+ let bodyValue = bodyText;
1524
+ if (ct.includes("application/json") || ct.includes("text/") || ct.includes("*/json")) {
1525
+ try {
1526
+ bodyValue = JSON.parse(bodyText);
1527
+ } catch {
1528
+ }
1529
+ } else if (ct.includes("application/x-www-form-urlencoded") || ct.includes("multipart/form-data")) {
1383
1530
  bodyValue = bodyText;
1531
+ } else {
1532
+ try {
1533
+ bodyValue = JSON.parse(bodyText);
1534
+ } catch {
1535
+ }
1384
1536
  }
1385
1537
  const result = schemas.body.safeParse(bodyValue);
1386
1538
  if (result.success) {
@@ -1410,14 +1562,20 @@ function getCookies(req) {
1410
1562
  for (const pair of header.split(";")) {
1411
1563
  const idx = pair.indexOf("=");
1412
1564
  if (idx === -1) continue;
1413
- const name15 = pair.slice(0, idx).trim();
1414
- const value = pair.slice(idx + 1).trim();
1415
- if (name15) {
1416
- try {
1417
- cookies[name15] = decodeURIComponent(value);
1418
- } catch {
1419
- cookies[name15] = value;
1420
- }
1565
+ let name15 = pair.slice(0, idx).trim();
1566
+ let value = pair.slice(idx + 1).trim();
1567
+ if (!name15) continue;
1568
+ try {
1569
+ name15 = decodeURIComponent(name15);
1570
+ } catch {
1571
+ }
1572
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
1573
+ value = value.slice(1, -1);
1574
+ }
1575
+ try {
1576
+ cookies[name15] = decodeURIComponent(value);
1577
+ } catch {
1578
+ cookies[name15] = value;
1421
1579
  }
1422
1580
  }
1423
1581
  return cookies;
@@ -1444,7 +1602,11 @@ function setCookie(res, name15, value, options) {
1444
1602
  }
1445
1603
  function deleteCookie(res, name15, options) {
1446
1604
  const headers = new Headers(res.headers);
1447
- headers.append("Set-Cookie", serializeCookie(name15, "", { ...options, maxAge: 0 }));
1605
+ headers.append("Set-Cookie", serializeCookie(name15, "", {
1606
+ ...options,
1607
+ maxAge: 0,
1608
+ expires: /* @__PURE__ */ new Date(0)
1609
+ }));
1448
1610
  return new Response(res.body, {
1449
1611
  status: res.status,
1450
1612
  statusText: res.statusText,
@@ -1455,12 +1617,43 @@ function deleteCookie(res, name15, options) {
1455
1617
  // upload.ts
1456
1618
  import { writeFile, mkdir } from "node:fs/promises";
1457
1619
  import { randomUUID } from "node:crypto";
1458
- import { join as join2 } from "node:path";
1620
+ import { join as join2, extname as extname2 } from "node:path";
1621
+ var extensionMimeMap = {
1622
+ ".jpg": "image/jpeg",
1623
+ ".jpeg": "image/jpeg",
1624
+ ".png": "image/png",
1625
+ ".gif": "image/gif",
1626
+ ".webp": "image/webp",
1627
+ ".svg": "image/svg+xml",
1628
+ ".pdf": "application/pdf",
1629
+ ".doc": "application/msword",
1630
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1631
+ ".xls": "application/vnd.ms-excel",
1632
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1633
+ ".zip": "application/zip",
1634
+ ".gz": "application/gzip",
1635
+ ".mp4": "video/mp4",
1636
+ ".mp3": "audio/mpeg",
1637
+ ".wav": "audio/wav",
1638
+ ".json": "application/json",
1639
+ ".csv": "text/csv",
1640
+ ".txt": "text/plain",
1641
+ ".html": "text/html",
1642
+ ".css": "text/css",
1643
+ ".js": "text/javascript",
1644
+ ".ts": "application/x-typescript",
1645
+ ".tsx": "application/x-typescript"
1646
+ };
1647
+ function detectMimeFromExtension(filename) {
1648
+ return extensionMimeMap[extname2(filename).toLowerCase()];
1649
+ }
1459
1650
  function upload(options) {
1460
1651
  const saveDir = options?.dir;
1461
1652
  return async (req, ctx, next) => {
1462
1653
  const ct = req.headers.get("content-type") ?? "";
1463
1654
  if (!ct.includes("multipart/form-data")) return next(req, ctx);
1655
+ if (saveDir) await mkdir(saveDir, { recursive: true }).catch(() => {
1656
+ });
1464
1657
  let formData;
1465
1658
  try {
1466
1659
  formData = await req.formData();
@@ -1471,8 +1664,13 @@ function upload(options) {
1471
1664
  const fields = {};
1472
1665
  for (const [key, value] of formData) {
1473
1666
  if (value instanceof File) {
1474
- if (options?.allowedTypes && !options.allowedTypes.includes(value.type)) {
1475
- return Response.json({ error: `File type not allowed: ${value.type}` }, { status: 415 });
1667
+ if (options?.allowedTypes) {
1668
+ const clientOk = options.allowedTypes.includes(value.type);
1669
+ const extType = detectMimeFromExtension(value.name);
1670
+ const extOk = extType ? options.allowedTypes.includes(extType) : false;
1671
+ if (!clientOk && !extOk) {
1672
+ return Response.json({ error: `File type not allowed: ${value.type}` }, { status: 415 });
1673
+ }
1476
1674
  }
1477
1675
  if (options?.maxFileSize && value.size > options.maxFileSize) {
1478
1676
  return Response.json({ error: `File too large: ${value.name}` }, { status: 413 });
@@ -1485,9 +1683,8 @@ function upload(options) {
1485
1683
  buffer: saveDir ? void 0 : buf
1486
1684
  };
1487
1685
  if (saveDir) {
1488
- const safeName = value.name.replace(/[/\\]/g, "");
1686
+ const safeName = value.name.replace(/[/\\\0]/g, "_");
1489
1687
  const filePath = join2(saveDir, `${randomUUID()}-${safeName}`);
1490
- await mkdir(saveDir, { recursive: true });
1491
1688
  await writeFile(filePath, buf);
1492
1689
  uf.path = filePath;
1493
1690
  }
@@ -1513,7 +1710,11 @@ function rateLimit(options) {
1513
1710
  const getKey = options?.key ?? ((req) => {
1514
1711
  const forwarded = req.headers.get("x-forwarded-for");
1515
1712
  if (forwarded) return forwarded.split(",")[0].trim();
1516
- return new URL(req.url).hostname;
1713
+ const realIp = req.headers.get("x-real-ip");
1714
+ if (realIp) return realIp;
1715
+ const cfIp = req.headers.get("cf-connecting-ip");
1716
+ if (cfIp) return cfIp;
1717
+ return req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "global";
1517
1718
  });
1518
1719
  const message = options?.message ?? "Too Many Requests";
1519
1720
  const hits = /* @__PURE__ */ new Map();
@@ -1524,7 +1725,7 @@ function rateLimit(options) {
1524
1725
  }
1525
1726
  }, window);
1526
1727
  if (interval.unref) interval.unref();
1527
- return async (req, ctx, next) => {
1728
+ const mw = async (req, ctx, next) => {
1528
1729
  const key = getKey(req);
1529
1730
  const now = Date.now();
1530
1731
  const entry = hits.get(key);
@@ -1557,10 +1758,15 @@ function rateLimit(options) {
1557
1758
  headers.set("X-RateLimit-Reset", String(Math.ceil(entry.reset / 1e3)));
1558
1759
  return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
1559
1760
  };
1761
+ mw.stop = () => {
1762
+ clearInterval(interval);
1763
+ hits.clear();
1764
+ };
1765
+ return mw;
1560
1766
  }
1561
1767
 
1562
1768
  // compress.ts
1563
- import { gzipSync, brotliCompressSync, constants } from "node:zlib";
1769
+ import { gzipSync, brotliCompressSync, deflateSync, constants } from "node:zlib";
1564
1770
  function compress(options) {
1565
1771
  const level = options?.level ?? 6;
1566
1772
  const threshold = options?.threshold ?? 1024;
@@ -1573,7 +1779,7 @@ function compress(options) {
1573
1779
  return next(req, ctx);
1574
1780
  }
1575
1781
  const res = await next(req, ctx);
1576
- if (res.status === 304 || res.status === 204 || res.status < 200 || res.status >= 300) {
1782
+ if (res.status === 304 || res.status === 204 || res.status === 206 || res.status < 200 || res.status >= 300) {
1577
1783
  return res;
1578
1784
  }
1579
1785
  const ce = res.headers.get("content-encoding");
@@ -1595,14 +1801,15 @@ function compress(options) {
1595
1801
  compressed = gzipSync(body, { level: Math.min(level, 9) });
1596
1802
  encoding = "gzip";
1597
1803
  } else {
1598
- compressed = gzipSync(body, { level: Math.min(level, 9) });
1804
+ compressed = deflateSync(body, { level: Math.min(level, 9) });
1599
1805
  encoding = "deflate";
1600
1806
  }
1601
1807
  const headers = new Headers(res.headers);
1602
1808
  headers.set("Content-Encoding", encoding);
1603
1809
  headers.set("Content-Length", String(compressed.byteLength));
1604
1810
  headers.delete("Content-Range");
1605
- headers.set("Vary", "Accept-Encoding");
1811
+ const existingVary = headers.get("Vary");
1812
+ headers.set("Vary", existingVary ? `${existingVary}, Accept-Encoding` : "Accept-Encoding");
1606
1813
  return new Response(compressed, {
1607
1814
  status: res.status,
1608
1815
  statusText: res.statusText,
@@ -1653,9 +1860,10 @@ async function executeQuery(schema, params, options, req, ctx) {
1653
1860
  variableValues: params.variables,
1654
1861
  operationName: params.operationName
1655
1862
  });
1656
- return Response.json(result, { status: result.errors ? 400 : 200 });
1863
+ return Response.json(result, { status: result.errors ? 200 : 200 });
1657
1864
  }
1658
1865
  function graphiqlHTML(endpoint) {
1866
+ const safeEndpoint = endpoint.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\x3C");
1659
1867
  return `<!doctype html>
1660
1868
  <html lang="en">
1661
1869
  <head>
@@ -1687,7 +1895,7 @@ function graphiqlHTML(endpoint) {
1687
1895
  import { createGraphiQLFetcher } from '@graphiql/toolkit';
1688
1896
  import 'graphiql/setup-workers/esm.sh';
1689
1897
 
1690
- const fetcher = createGraphiQLFetcher({ url: "${endpoint}" });
1898
+ const fetcher = createGraphiQLFetcher({ url: "${safeEndpoint}" });
1691
1899
 
1692
1900
  function App() {
1693
1901
  return React.createElement(GraphiQL, { fetcher });
@@ -1734,12 +1942,17 @@ function graphql(handler) {
1734
1942
  }
1735
1943
 
1736
1944
  // ai.ts
1737
- import { streamText } from "ai";
1738
- function ai(handler) {
1945
+ var _ai = {
1946
+ streamText: null
1947
+ };
1948
+ async function ai(handler) {
1949
+ if (!_ai.streamText) {
1950
+ _ai.streamText = (await import("ai")).streamText;
1951
+ }
1739
1952
  const r = new Router();
1740
1953
  r.post("/", async (req, ctx) => {
1741
1954
  const options = await handler(req, ctx);
1742
- const result = streamText(options);
1955
+ const result = _ai.streamText(options);
1743
1956
  return result.toTextStreamResponse();
1744
1957
  });
1745
1958
  return r;
@@ -1756,9 +1969,11 @@ function tool(def) {
1756
1969
  }
1757
1970
 
1758
1971
  // workflow/reference.ts
1972
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1759
1973
  function getByPath(obj, path2) {
1760
1974
  let current = obj;
1761
1975
  for (const key of path2) {
1976
+ if (DANGEROUS_KEYS.has(key)) return void 0;
1762
1977
  if (current === null || current === void 0) return void 0;
1763
1978
  if (typeof current === "object" && key in current) {
1764
1979
  current = current[key];
@@ -1828,27 +2043,42 @@ function evaluateExpression(expr, ctx) {
1828
2043
  { op: "!==", fn: (a, b) => a !== b },
1829
2044
  { op: ">=", fn: (a, b) => Number(a) >= Number(b) },
1830
2045
  { op: "<=", fn: (a, b) => Number(a) <= Number(b) },
1831
- { op: ">", fn: (a, b) => Number(a) > Number(b) },
1832
- { op: "<", fn: (a, b) => Number(a) < Number(b) },
1833
2046
  { op: "==", fn: (a, b) => a == b },
1834
2047
  { op: "!=", fn: (a, b) => a != b },
2048
+ { op: "&&", fn: (a, b) => Boolean(a) && Boolean(b) },
2049
+ { op: "||", fn: (a, b) => Boolean(a) || Boolean(b) },
2050
+ { op: ">", fn: (a, b) => Number(a) > Number(b) },
2051
+ { op: "<", fn: (a, b) => Number(a) < Number(b) },
1835
2052
  { op: "+", fn: (a, b) => Number(a) + Number(b) },
1836
2053
  { op: "-", fn: (a, b) => Number(a) - Number(b) },
1837
2054
  { op: "*", fn: (a, b) => Number(a) * Number(b) },
1838
2055
  { op: "/", fn: (a, b) => Number(a) / Number(b) },
1839
- { op: "%", fn: (a, b) => Number(a) % Number(b) },
1840
- { op: "&&", fn: (a, b) => Boolean(a) && Boolean(b) },
1841
- { op: "||", fn: (a, b) => Boolean(a) || Boolean(b) }
2056
+ { op: "%", fn: (a, b) => Number(a) % Number(b) }
1842
2057
  ];
2058
+ let bestIdx = -1;
2059
+ let bestOp = null;
2060
+ let bestFn = null;
1843
2061
  for (const { op, fn } of operators) {
1844
2062
  const idx = expr.indexOf(op);
1845
- if (idx > 0) {
1846
- const leftRaw = expr.slice(0, idx).trim();
1847
- const rightRaw = expr.slice(idx + op.length).trim();
1848
- const left = resolveValue(leftRaw, ctx);
1849
- const right = resolveValue(rightRaw, ctx);
1850
- return fn(left, right);
1851
- }
2063
+ if (idx > 0 && (bestIdx === -1 || idx < bestIdx)) {
2064
+ bestIdx = idx;
2065
+ bestOp = op;
2066
+ bestFn = fn;
2067
+ }
2068
+ }
2069
+ function resolveOperand(raw) {
2070
+ const trimmed2 = raw.trim();
2071
+ if (/^[\d.]+$/.test(trimmed2)) return Number(trimmed2);
2072
+ const subIdx = operators.findIndex(({ op }) => trimmed2.includes(op));
2073
+ if (subIdx !== -1) return evaluateExpression(trimmed2, ctx);
2074
+ return resolveValue(trimmed2, ctx);
2075
+ }
2076
+ if (bestIdx > 0 && bestOp && bestFn) {
2077
+ const leftRaw = expr.slice(0, bestIdx).trim();
2078
+ const rightRaw = expr.slice(bestIdx + bestOp.length).trim();
2079
+ const left = resolveOperand(leftRaw);
2080
+ const right = resolveOperand(rightRaw);
2081
+ return bestFn(left, right);
1852
2082
  }
1853
2083
  const trimmed = expr.trim();
1854
2084
  if (trimmed === "true") return true;
@@ -2228,6 +2458,7 @@ data: ${JSON.stringify(event.data)}
2228
2458
  }
2229
2459
 
2230
2460
  // workflow/route.ts
2461
+ import crypto from "node:crypto";
2231
2462
  function workflow(handler) {
2232
2463
  const r = new Router();
2233
2464
  const sseManager = createSSEManager();
@@ -2790,7 +3021,12 @@ h2{color:#dc2626}.desc{color:#555}</style>
2790
3021
  if (!codeVerifier) {
2791
3022
  return Response.json({ error: "invalid_grant", error_description: "code_verifier required" }, { status: 400 });
2792
3023
  }
2793
- const expected = crypto2.createHash("sha256").update(codeVerifier).digest().toString("base64url");
3024
+ let expected;
3025
+ if (stored.code_challenge_method === "plain") {
3026
+ expected = codeVerifier;
3027
+ } else {
3028
+ expected = crypto2.createHash("sha256").update(codeVerifier).digest().toString("base64url");
3029
+ }
2794
3030
  if (expected !== stored.code_challenge) {
2795
3031
  return Response.json({ error: "invalid_grant", error_description: "code_verifier mismatch" }, { status: 400 });
2796
3032
  }
@@ -3259,7 +3495,7 @@ function sqlTypeForField(field) {
3259
3495
  }
3260
3496
  var RESERVED_SLUGS = /* @__PURE__ */ new Set(["sys", "graphql", "auth"]);
3261
3497
  function validateSlug(slug) {
3262
- if (!/^[a-z][a-z0-9_]+$/.test(slug)) {
3498
+ if (!/^[a-z][a-z0-9_]*$/.test(slug)) {
3263
3499
  return "Slug must start with a letter and contain only lowercase letters, numbers, and underscores";
3264
3500
  }
3265
3501
  if (RESERVED_SLUGS.has(slug)) {
@@ -13681,15 +13917,16 @@ function buildRouter2(deps) {
13681
13917
  return Response.json(rows);
13682
13918
  });
13683
13919
  r.delete("/agents/:id/knowledge/:docId", async (_req, ctx) => {
13920
+ const agentId = parseInt(ctx.params.id, 10);
13684
13921
  const docId = parseInt(ctx.params.docId, 10);
13685
- await sql`DELETE FROM "_knowledge_documents" WHERE id = ${docId}`;
13922
+ await sql`DELETE FROM "_knowledge_documents" WHERE id = ${docId} AND agent_id = ${agentId}`;
13686
13923
  return Response.json({ ok: true });
13687
13924
  });
13688
13925
  return r;
13689
13926
  }
13690
13927
 
13691
13928
  // agent/run.ts
13692
- import { streamText as streamText2, generateText as generateText2, embed } from "ai";
13929
+ import { streamText, generateText as generateText2, embed } from "ai";
13693
13930
  import { z as z27 } from "zod";
13694
13931
  function chunkContent(content, chunkSize = 512, overlap = 64) {
13695
13932
  const paragraphs = content.split(/\n\n+/);
@@ -13754,7 +13991,7 @@ function createRunner(deps) {
13754
13991
  }
13755
13992
  const system = agent2.system_prompt || void 0;
13756
13993
  if (params.stream) {
13757
- const result = streamText2({
13994
+ const result = streamText({
13758
13995
  model,
13759
13996
  system,
13760
13997
  messages,
@@ -14325,7 +14562,9 @@ function createManager(config, apps, manager) {
14325
14562
  if (!config.deployToken) return next(req, ctx);
14326
14563
  const header = req.headers.get("authorization") ?? "";
14327
14564
  const token = header.replace("Bearer ", "");
14328
- if (token !== config.deployToken) {
14565
+ const tokenBuf = Buffer.from(token);
14566
+ const secretBuf = Buffer.from(config.deployToken);
14567
+ if (tokenBuf.length !== secretBuf.length || !crypto4.timingSafeEqual(tokenBuf, secretBuf)) {
14329
14568
  return Response.json({ error: "Unauthorized" }, { status: 401 });
14330
14569
  }
14331
14570
  return next(req, ctx);
@@ -14424,15 +14663,22 @@ function createManager(config, apps, manager) {
14424
14663
  }
14425
14664
  });
14426
14665
  router.post("/webhook", async (req) => {
14666
+ const rawBody = await req.text();
14427
14667
  if (config.webhookSecret) {
14428
14668
  const sig = req.headers.get("x-hub-signature-256") ?? "";
14429
- const body = await req.clone().text();
14430
- const hmac = crypto4.createHmac("sha256", config.webhookSecret).update(body).digest("hex");
14431
- if (sig !== `sha256=${hmac}`) {
14669
+ const expected = `sha256=${crypto4.createHmac("sha256", config.webhookSecret).update(rawBody).digest("hex")}`;
14670
+ const sigBuf = Buffer.from(sig);
14671
+ const expectedBuf = Buffer.from(expected);
14672
+ if (sigBuf.length !== expectedBuf.length || !crypto4.timingSafeEqual(sigBuf, expectedBuf)) {
14432
14673
  return Response.json({ error: "invalid signature" }, { status: 401 });
14433
14674
  }
14434
14675
  }
14435
- const payload = await req.json();
14676
+ let payload;
14677
+ try {
14678
+ payload = JSON.parse(rawBody);
14679
+ } catch {
14680
+ payload = {};
14681
+ }
14436
14682
  const repoUrl = payload?.repository?.clone_url ?? payload?.repository?.html_url ?? "";
14437
14683
  if (!repoUrl) return Response.json({ deployed: [] });
14438
14684
  const deployed = [];
@@ -14560,6 +14806,12 @@ async function deploy(config) {
14560
14806
  if (logs.length > 1e3) logs.splice(0, logs.length - 1e3);
14561
14807
  };
14562
14808
  try {
14809
+ if (typeof ac.repo !== "string" || !/^https?:\/\/[^\s"']+\/[^\s"']+/.test(ac.repo) && !/^git@[^\s"']+:[^\s"']+/.test(ac.repo)) {
14810
+ throw new Error(`Invalid repo URL: ${ac.repo}`);
14811
+ }
14812
+ if (ac.branch && typeof ac.branch === "string" && !/^[\w.\-/]+$/.test(ac.branch)) {
14813
+ throw new Error(`Invalid branch name: ${ac.branch}`);
14814
+ }
14563
14815
  if (fs.existsSync(path.join(appDir, ".git"))) {
14564
14816
  execSync("git pull", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
14565
14817
  log("[deploy] git pull done");
@@ -14567,7 +14819,7 @@ async function deploy(config) {
14567
14819
  if (fs.existsSync(appDir)) {
14568
14820
  fs.rmSync(appDir, { recursive: true });
14569
14821
  }
14570
- execSync(`git clone ${ac.repo} ${appDir}`, { stdio: "pipe", timeout: 12e4 });
14822
+ execSync(`git clone --depth=1 ${ac.repo} ${appDir}`, { stdio: "pipe", timeout: 12e4 });
14571
14823
  log("[deploy] git clone done");
14572
14824
  if (ac.branch) {
14573
14825
  execSync(`git checkout ${ac.branch}`, { cwd: appDir, stdio: "pipe", timeout: 3e4 });
@@ -14594,6 +14846,10 @@ async function deploy(config) {
14594
14846
  return;
14595
14847
  }
14596
14848
  if (ac.buildCommand) {
14849
+ if (typeof ac.buildCommand !== "string" || /[;&|`$()]/.test(ac.buildCommand)) {
14850
+ log(`[deploy] invalid build command (rejected): ${ac.buildCommand}`);
14851
+ return;
14852
+ }
14597
14853
  try {
14598
14854
  execSync(ac.buildCommand, { cwd: appDir, stdio: "pipe", timeout: 12e4 });
14599
14855
  log("[deploy] build done");
@@ -5,4 +5,6 @@ export interface RateLimitOptions {
5
5
  key?: (req: Request) => string;
6
6
  message?: string;
7
7
  }
8
- export declare function rateLimit(options?: RateLimitOptions): Middleware;
8
+ export declare function rateLimit(options?: RateLimitOptions): Middleware & {
9
+ stop: () => void;
10
+ };
package/dist/router.d.ts CHANGED
@@ -14,6 +14,7 @@ export declare class Router {
14
14
  private wsRoot;
15
15
  private globalMws;
16
16
  private errorHandler?;
17
+ private wss;
17
18
  use(mw: Middleware): this;
18
19
  use(path: string, router: Router): this;
19
20
  use(path: string, mw: Middleware): this;
package/dist/types.d.ts CHANGED
@@ -3,7 +3,8 @@ export interface Context {
3
3
  query: Record<string, string>;
4
4
  user?: unknown;
5
5
  parsed?: Record<string, unknown>;
6
+ mountPath?: string;
6
7
  }
7
8
  export type Handler = (req: Request, ctx: Context) => Response | Promise<Response>;
8
9
  export type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>;
9
- export type ErrorHandler = (error: Error, req: Request, ctx: Context) => Response;
10
+ export type ErrorHandler = (error: Error, req: Request, ctx: Context) => Response | Promise<Response>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
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",