weifuwu 0.16.2 → 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")
@@ -3465,9 +3491,65 @@ function redis(opts) {
3465
3491
  return mw;
3466
3492
  }
3467
3493
 
3494
+ // hub.ts
3495
+ function createHub(opts) {
3496
+ const prefix = opts?.prefix ?? "hub:";
3497
+ const channels2 = /* @__PURE__ */ new Map();
3498
+ let redisPub;
3499
+ let redisSub = null;
3500
+ if (opts?.redis) {
3501
+ redisPub = opts.redis;
3502
+ redisSub = opts.redis.duplicate();
3503
+ redisSub.on("message", (rawChannel, rawData) => {
3504
+ if (!rawChannel.startsWith(prefix)) return;
3505
+ const key = rawChannel.slice(prefix.length);
3506
+ const members = channels2.get(key);
3507
+ if (!members) return;
3508
+ for (const ws of members) {
3509
+ try {
3510
+ ws.send(rawData);
3511
+ } catch {
3512
+ }
3513
+ }
3514
+ });
3515
+ }
3516
+ function join5(key, ws) {
3517
+ if (!channels2.has(key)) {
3518
+ channels2.set(key, /* @__PURE__ */ new Set());
3519
+ redisSub?.subscribe(`${prefix}${key}`);
3520
+ }
3521
+ channels2.get(key).add(ws);
3522
+ }
3523
+ function leave(ws) {
3524
+ for (const [, members] of channels2) {
3525
+ members.delete(ws);
3526
+ }
3527
+ }
3528
+ function broadcast(key, data) {
3529
+ const msg = JSON.stringify(data);
3530
+ const members = channels2.get(key);
3531
+ if (members) {
3532
+ for (const ws of members) {
3533
+ try {
3534
+ ws.send(msg);
3535
+ } catch {
3536
+ }
3537
+ }
3538
+ }
3539
+ redisPub?.publish(`${prefix}${key}`, msg);
3540
+ }
3541
+ async function close() {
3542
+ channels2.clear();
3543
+ if (redisSub) {
3544
+ await redisSub.quit();
3545
+ }
3546
+ }
3547
+ return { join: join5, leave, broadcast, close };
3548
+ }
3549
+
3468
3550
  // queue/index.ts
3469
3551
  import { Redis as IORedis2 } from "ioredis";
3470
- import crypto3 from "node:crypto";
3552
+ import crypto4 from "node:crypto";
3471
3553
  function cronNext(expr, from = /* @__PURE__ */ new Date()) {
3472
3554
  const parts = expr.trim().split(/\s+/);
3473
3555
  if (parts.length !== 5) throw new Error(`Invalid cron expression "${expr}": expected 5 fields`);
@@ -3558,7 +3640,7 @@ function queue(opts) {
3558
3640
  if (job.schedule) {
3559
3641
  try {
3560
3642
  const nextRun = cronNext(job.schedule);
3561
- const nextJob = { ...job, id: crypto3.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3643
+ const nextJob = { ...job, id: crypto4.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3562
3644
  redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob)).catch(() => {
3563
3645
  });
3564
3646
  } catch {
@@ -3576,7 +3658,7 @@ function queue(opts) {
3576
3658
  }
3577
3659
  }
3578
3660
  mw.add = function add(type, payload, opts2) {
3579
- const id2 = crypto3.randomUUID();
3661
+ const id2 = crypto4.randomUUID();
3580
3662
  let runAt;
3581
3663
  if (opts2?.schedule) {
3582
3664
  runAt = cronNext(opts2.schedule);
@@ -4809,40 +4891,17 @@ function agent(options) {
4809
4891
  }
4810
4892
 
4811
4893
  // messager/ws.ts
4812
- var channels = /* @__PURE__ */ new Map();
4813
4894
  var userConnections = /* @__PURE__ */ new Map();
4895
+ var hub;
4814
4896
  function broadcastToChannel(channelId, data) {
4815
- const members = channels.get(channelId);
4816
- if (!members) return;
4817
- const msg = JSON.stringify(data);
4818
- for (const { ws } of members) {
4819
- try {
4820
- ws.send(msg);
4821
- } catch {
4822
- }
4823
- }
4824
- }
4825
- function subscribe(ws, userId, channelId) {
4826
- if (!channels.has(channelId)) channels.set(channelId, /* @__PURE__ */ new Set());
4827
- channels.get(channelId).add({ ws, userId });
4828
- if (!userConnections.has(userId)) userConnections.set(userId, /* @__PURE__ */ new Set());
4829
- userConnections.get(userId).add(ws);
4830
- }
4831
- function unsubscribe(ws) {
4832
- for (const [, members] of channels) {
4833
- for (const m of members) {
4834
- if (m.ws === ws) {
4835
- members.delete(m);
4836
- break;
4837
- }
4838
- }
4839
- }
4840
- for (const [, conns] of userConnections) {
4841
- conns.delete(ws);
4842
- }
4897
+ hub?.broadcast(`messager:${channelId}`, data);
4843
4898
  }
4844
4899
  function createWSHandler(deps) {
4845
4900
  const { sql: sql2, agents } = deps;
4901
+ hub = createHub({
4902
+ redis: deps.redis,
4903
+ prefix: "messager:"
4904
+ });
4846
4905
  return {
4847
4906
  open(ws, ctx) {
4848
4907
  const userId = ctx.user?.id;
@@ -4871,8 +4930,10 @@ function createWSHandler(deps) {
4871
4930
  RETURNING *
4872
4931
  `;
4873
4932
  const message = row;
4933
+ hub.join(`messager:${channel_id}`, ws);
4934
+ if (!userConnections.has(userId)) userConnections.set(userId, /* @__PURE__ */ new Set());
4935
+ userConnections.get(userId).add(ws);
4874
4936
  broadcastToChannel(channel_id, { type: "message", data: message });
4875
- subscribe(ws, userId, channel_id);
4876
4937
  if (agents) {
4877
4938
  const agentMembers = await sql2`
4878
4939
  SELECT member_id FROM "_channel_members"
@@ -4898,7 +4959,9 @@ function createWSHandler(deps) {
4898
4959
  break;
4899
4960
  }
4900
4961
  case "typing": {
4901
- if (channel_id) subscribe(ws, userId, channel_id);
4962
+ if (channel_id) {
4963
+ hub.join(`messager:${channel_id}`, ws);
4964
+ }
4902
4965
  broadcastToChannel(channel_id, {
4903
4966
  type: "typing",
4904
4967
  channel_id,
@@ -4909,7 +4972,7 @@ function createWSHandler(deps) {
4909
4972
  }
4910
4973
  case "read": {
4911
4974
  if (!channel_id || !last_message_id) return;
4912
- subscribe(ws, userId, channel_id);
4975
+ hub.join(`messager:${channel_id}`, ws);
4913
4976
  await sql2`
4914
4977
  UPDATE "_channel_members"
4915
4978
  SET last_read_id = ${last_message_id}, last_read_at = NOW()
@@ -4926,22 +4989,28 @@ function createWSHandler(deps) {
4926
4989
  }
4927
4990
  },
4928
4991
  close(ws) {
4929
- unsubscribe(ws);
4992
+ hub?.leave(ws);
4993
+ for (const [, conns] of userConnections) {
4994
+ conns.delete(ws);
4995
+ }
4930
4996
  },
4931
4997
  error(ws) {
4932
- unsubscribe(ws);
4998
+ hub?.leave(ws);
4999
+ for (const [, conns] of userConnections) {
5000
+ conns.delete(ws);
5001
+ }
4933
5002
  }
4934
5003
  };
4935
5004
  }
4936
5005
 
4937
5006
  // messager/rest.ts
4938
5007
  function buildRouter3(deps) {
4939
- const { sql: sql2, channels: channels3, members, messages: messages2, agents } = deps;
5008
+ const { sql: sql2, channels: channels2, members, messages: messages2, agents } = deps;
4940
5009
  const r = new Router();
4941
5010
  r.post("/channels", async (req) => {
4942
5011
  const body = await req.json();
4943
5012
  if (!body.name) return Response.json({ error: "name is required" }, { status: 400 });
4944
- const channel = await channels3.insert({
5013
+ const channel = await channels2.insert({
4945
5014
  name: body.name,
4946
5015
  type: body.type || "channel",
4947
5016
  created_by: body.created_by || 1
@@ -4984,14 +5053,14 @@ function buildRouter3(deps) {
4984
5053
  });
4985
5054
  r.get("/channels/:id", async (_req, ctx) => {
4986
5055
  const id2 = parseInt(ctx.params.id, 10);
4987
- const ch = await channels3.read(id2);
5056
+ const ch = await channels2.read(id2);
4988
5057
  if (!ch) return Response.json({ error: "Channel not found" }, { status: 404 });
4989
5058
  const { data: memberRows } = await members.readMany({ channel_id: id2 });
4990
5059
  return Response.json({ channel: ch, members: memberRows });
4991
5060
  });
4992
5061
  r.delete("/channels/:id", async (_req, ctx) => {
4993
5062
  const id2 = parseInt(ctx.params.id, 10);
4994
- await channels3.delete(id2);
5063
+ await channels2.delete(id2);
4995
5064
  return Response.json({ ok: true });
4996
5065
  });
4997
5066
  r.post("/channels/:id/members", async (req, ctx) => {
@@ -5092,8 +5161,9 @@ function messager(options) {
5092
5161
  const pg = options.pg;
5093
5162
  const sql2 = pg.sql;
5094
5163
  const agents = options.agents;
5164
+ const redis2 = options.redis;
5095
5165
  const base = new PgModule(pg);
5096
- const channels3 = pg.table("_channels", {
5166
+ const channels2 = pg.table("_channels", {
5097
5167
  id: serial("id").primaryKey(),
5098
5168
  tenant_id: text("tenant_id"),
5099
5169
  name: text("name").notNull().default(""),
@@ -5125,16 +5195,16 @@ function messager(options) {
5125
5195
  });
5126
5196
  return {
5127
5197
  migrate: async () => {
5128
- await channels3.create();
5129
- await channels3.createIndex("tenant_id");
5198
+ await channels2.create();
5199
+ await channels2.createIndex("tenant_id");
5130
5200
  await members.create();
5131
5201
  await members.createIndex("member_id");
5132
5202
  await members.createIndex(["channel_id", "member_id", "member_type"], { unique: true });
5133
5203
  await messages2.create();
5134
5204
  await messages2.createIndex(["channel_id", "created_at"], { desc: true });
5135
5205
  },
5136
- router: () => buildRouter3({ sql: sql2, channels: channels3, members, messages: messages2, agents }),
5137
- wsHandler: () => createWSHandler({ sql: sql2, agents }),
5206
+ router: () => buildRouter3({ sql: sql2, channels: channels2, members, messages: messages2, agents }),
5207
+ wsHandler: () => createWSHandler({ sql: sql2, agents, redis: redis2 }),
5138
5208
  async send(channelId, content, opts) {
5139
5209
  const msg = await messages2.insert({
5140
5210
  channel_id: channelId,
@@ -5242,7 +5312,7 @@ function createGateway(config, getPort) {
5242
5312
  }
5243
5313
 
5244
5314
  // deploy/manager.ts
5245
- import crypto4 from "node:crypto";
5315
+ import crypto5 from "node:crypto";
5246
5316
  function createManager(config, apps, manager) {
5247
5317
  const router = new Router();
5248
5318
  const auth2 = (req, ctx, next) => {
@@ -5251,7 +5321,7 @@ function createManager(config, apps, manager) {
5251
5321
  const token = header.replace("Bearer ", "");
5252
5322
  const tokenBuf = Buffer.from(token);
5253
5323
  const secretBuf = Buffer.from(config.deployToken);
5254
- if (tokenBuf.length !== secretBuf.length || !crypto4.timingSafeEqual(tokenBuf, secretBuf)) {
5324
+ if (tokenBuf.length !== secretBuf.length || !crypto5.timingSafeEqual(tokenBuf, secretBuf)) {
5255
5325
  return Response.json({ error: "Unauthorized" }, { status: 401 });
5256
5326
  }
5257
5327
  return next(req, ctx);
@@ -5350,10 +5420,10 @@ function createManager(config, apps, manager) {
5350
5420
  const rawBody = await req.text();
5351
5421
  if (config.webhookSecret) {
5352
5422
  const sig = req.headers.get("x-hub-signature-256") ?? "";
5353
- 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")}`;
5354
5424
  const sigBuf = Buffer.from(sig);
5355
5425
  const expectedBuf = Buffer.from(expected);
5356
- if (sigBuf.length !== expectedBuf.length || !crypto4.timingSafeEqual(sigBuf, expectedBuf)) {
5426
+ if (sigBuf.length !== expectedBuf.length || !crypto5.timingSafeEqual(sigBuf, expectedBuf)) {
5357
5427
  return Response.json({ error: "invalid signature" }, { status: 401 });
5358
5428
  }
5359
5429
  }
@@ -6310,7 +6380,7 @@ async function buildRouter4(deps) {
6310
6380
  skills: allSkills,
6311
6381
  systemPrompt: session.system_prompt || systemPrompt
6312
6382
  });
6313
- const history = await getHistory(sql2, sessionId);
6383
+ const history2 = await getHistory(sql2, sessionId);
6314
6384
  await addTextMessage(sql2, sessionId, "user", content);
6315
6385
  const stream = executeGenerator({
6316
6386
  sessionId,
@@ -6318,7 +6388,7 @@ async function buildRouter4(deps) {
6318
6388
  model,
6319
6389
  tools,
6320
6390
  systemPrompt: sysPrompt,
6321
- messages: history,
6391
+ messages: history2,
6322
6392
  sql: sql2
6323
6393
  });
6324
6394
  return createSSEStream(stream);
@@ -6398,7 +6468,7 @@ function createWSHandler2(deps) {
6398
6468
  skills: allSkills,
6399
6469
  systemPrompt: session.system_prompt || systemPrompt
6400
6470
  });
6401
- const history = await getHistory(sql2, session_id);
6471
+ const history2 = await getHistory(sql2, session_id);
6402
6472
  await addTextMessage(sql2, session_id, "user", content);
6403
6473
  const stream = executeGenerator({
6404
6474
  sessionId: session_id,
@@ -6406,7 +6476,7 @@ function createWSHandler2(deps) {
6406
6476
  model,
6407
6477
  tools,
6408
6478
  systemPrompt: sysPrompt,
6409
- messages: history,
6479
+ messages: history2,
6410
6480
  sql: sql2,
6411
6481
  abortSignal: controller.signal
6412
6482
  });
@@ -6843,6 +6913,251 @@ function mailer(options) {
6843
6913
  return { send, close };
6844
6914
  }
6845
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
+
6846
7161
  // logdb/rest.ts
6847
7162
  function createHandler(entries) {
6848
7163
  return async (req, ctx) => {
@@ -7010,10 +7325,10 @@ function logdb(options) {
7010
7325
  }
7011
7326
 
7012
7327
  // iii/client.ts
7013
- import crypto5 from "node:crypto";
7328
+ import crypto6 from "node:crypto";
7014
7329
 
7015
7330
  // iii/stream.ts
7016
- var channels2 = /* @__PURE__ */ new Map();
7331
+ var channels = /* @__PURE__ */ new Map();
7017
7332
  function notify(stream, group, item, event, data) {
7018
7333
  const keys = [
7019
7334
  `${stream}`,
@@ -7022,7 +7337,7 @@ function notify(stream, group, item, event, data) {
7022
7337
  ];
7023
7338
  const msg = JSON.stringify({ type: "stream", stream_name: stream, group_id: group, item_id: item, event, data });
7024
7339
  for (const key of keys) {
7025
- const subs = channels2.get(key);
7340
+ const subs = channels.get(key);
7026
7341
  if (!subs) continue;
7027
7342
  for (const ws of subs) {
7028
7343
  try {
@@ -7347,14 +7662,14 @@ function createStream(opts) {
7347
7662
  ...store,
7348
7663
  subscribe(ws, sub) {
7349
7664
  const key = sub.item_id ? `${sub.stream_name}:${sub.group_id}:${sub.item_id}` : sub.group_id ? `${sub.stream_name}:${sub.group_id}` : sub.stream_name;
7350
- if (!channels2.has(key)) channels2.set(key, /* @__PURE__ */ new Set());
7351
- channels2.get(key).add(ws);
7665
+ if (!channels.has(key)) channels.set(key, /* @__PURE__ */ new Set());
7666
+ channels.get(key).add(ws);
7352
7667
  if (redisSub && sub.stream_name) {
7353
7668
  redisSub.subscribe(`iii:stream:${sub.stream_name}`);
7354
7669
  }
7355
7670
  },
7356
7671
  unsubscribe(ws) {
7357
- for (const [, subs] of channels2) subs.delete(ws);
7672
+ for (const [, subs] of channels) subs.delete(ws);
7358
7673
  },
7359
7674
  async migrate() {
7360
7675
  if (opts?.pg) {
@@ -7536,7 +7851,7 @@ function iii(opts = {}) {
7536
7851
  registerBuiltin("stream::send", (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id));
7537
7852
  registerBuiltin("stream::update", (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops));
7538
7853
  function addLocalWorker(worker) {
7539
- const workerId = crypto5.randomUUID();
7854
+ const workerId = crypto6.randomUUID();
7540
7855
  const reg = {
7541
7856
  id: workerId,
7542
7857
  name: worker.name,
@@ -7551,7 +7866,7 @@ function iii(opts = {}) {
7551
7866
  const triggerIds = [];
7552
7867
  for (const t of worker.getTriggers()) {
7553
7868
  if (t.input.function_id === fn.id) {
7554
- const tid = crypto5.randomUUID();
7869
+ const tid = crypto6.randomUUID();
7555
7870
  triggers.set(tid, {
7556
7871
  id: tid,
7557
7872
  type: t.input.type,
@@ -7580,7 +7895,7 @@ function iii(opts = {}) {
7580
7895
  if (!worker) return;
7581
7896
  const handler = async (payload) => {
7582
7897
  if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
7583
- const invocationId = crypto5.randomUUID();
7898
+ const invocationId = crypto6.randomUUID();
7584
7899
  return new Promise((resolve11, reject) => {
7585
7900
  const timer = setTimeout(() => {
7586
7901
  pending.delete(invocationId);
@@ -7614,7 +7929,7 @@ function iii(opts = {}) {
7614
7929
  }
7615
7930
  const wsHandler = createWsHandler({
7616
7931
  registerRemoteWorker(ws, name) {
7617
- const id2 = crypto5.randomUUID();
7932
+ const id2 = crypto6.randomUUID();
7618
7933
  workers.set(id2, { id: id2, name, ws, functions: [], triggers: [] });
7619
7934
  return id2;
7620
7935
  },
@@ -7625,7 +7940,7 @@ function iii(opts = {}) {
7625
7940
  addRemoteFunction(workerId, id2);
7626
7941
  },
7627
7942
  registerRemoteTrigger(workerId, input) {
7628
- const tid = crypto5.randomUUID();
7943
+ const tid = crypto6.randomUUID();
7629
7944
  const reg = { id: tid, ...input, workerId };
7630
7945
  triggers.set(tid, reg);
7631
7946
  const worker = workers.get(workerId);
@@ -7990,6 +8305,7 @@ function registerWorker(url) {
7990
8305
  };
7991
8306
  }
7992
8307
  export {
8308
+ Link,
7993
8309
  Router,
7994
8310
  TsxContext,
7995
8311
  agent,
@@ -7997,10 +8313,12 @@ export {
7997
8313
  auth,
7998
8314
  compress,
7999
8315
  cors,
8316
+ createHub,
8000
8317
  createOpenAI,
8001
8318
  createSSEStream,
8002
8319
  createTestServer,
8003
8320
  createWorker,
8321
+ csrf,
8004
8322
  defineConfig,
8005
8323
  deleteCookie,
8006
8324
  deploy,
@@ -8021,6 +8339,7 @@ export {
8021
8339
  logger,
8022
8340
  mailer,
8023
8341
  messager,
8342
+ navigate,
8024
8343
  openai,
8025
8344
  opencode,
8026
8345
  postgres,
@@ -8043,7 +8362,10 @@ export {
8043
8362
  tool2 as tool,
8044
8363
  tsx,
8045
8364
  upload,
8365
+ useAction,
8366
+ useNavigate,
8046
8367
  useTsx,
8368
+ useWebsocket,
8047
8369
  user,
8048
8370
  validate
8049
8371
  };