weifuwu 0.8.0 → 0.8.1

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
@@ -10718,9 +10718,24 @@ var require_built3 = __commonJS({
10718
10718
 
10719
10719
  // serve.ts
10720
10720
  import http from "node:http";
10721
- async function readBody(req) {
10721
+ async function readBody(req, maxSize) {
10722
+ if (maxSize) {
10723
+ const cl = parseInt(req.headers["content-length"] ?? "0", 10);
10724
+ if (cl > maxSize) {
10725
+ const err = new Error("Request body too large");
10726
+ err.status = 413;
10727
+ throw err;
10728
+ }
10729
+ }
10722
10730
  const chunks = [];
10731
+ let total = 0;
10723
10732
  for await (const chunk of req) {
10733
+ total += chunk.byteLength;
10734
+ if (maxSize && total > maxSize) {
10735
+ const err = new Error("Request body too large");
10736
+ err.status = 413;
10737
+ throw err;
10738
+ }
10724
10739
  chunks.push(chunk);
10725
10740
  }
10726
10741
  return Buffer.concat(chunks);
@@ -10766,11 +10781,16 @@ function serve(handler, options) {
10766
10781
  const hostname = options?.hostname ?? "0.0.0.0";
10767
10782
  const server = http.createServer(async (req, res) => {
10768
10783
  try {
10769
- const body = await readBody(req);
10784
+ const body = await readBody(req, options?.maxBodySize);
10770
10785
  const [request, query] = createRequest(req, body);
10771
10786
  const response = await handler(request, { params: {}, query });
10772
10787
  await sendResponse(res, response);
10773
- } catch {
10788
+ } catch (err) {
10789
+ if (err?.status === 413) {
10790
+ res.writeHead(413, { "Content-Type": "text/plain" });
10791
+ res.end("Request Body Too Large");
10792
+ return;
10793
+ }
10774
10794
  res.writeHead(500, { "Content-Type": "text/plain" });
10775
10795
  res.end("Internal Server Error");
10776
10796
  }
@@ -11013,8 +11033,13 @@ var Router = class _Router {
11013
11033
  return mw(innerReq, ctx2, dispatch);
11014
11034
  }
11015
11035
  return await new Promise((resolve3) => {
11016
- upgradeSocket(wss, req, socket, head, match.handler, ctx2);
11017
- resolve3(new Response(null, { status: 101 }));
11036
+ try {
11037
+ upgradeSocket(wss, req, socket, head, match.handler, ctx2);
11038
+ resolve3(new Response(null, { status: 101 }));
11039
+ } catch {
11040
+ socket.destroy();
11041
+ resolve3(new Response("WebSocket upgrade failed", { status: 500 }));
11042
+ }
11018
11043
  });
11019
11044
  };
11020
11045
  Promise.resolve(dispatch(webReq, ctx)).then((result) => {
@@ -11815,7 +11840,7 @@ function validate(schemas) {
11815
11840
  if (issues.length > 0) {
11816
11841
  return Response.json({ error: "Validation failed", issues }, { status: 400 });
11817
11842
  }
11818
- ctx.parsed = parsed;
11843
+ ctx.parsed = { ...ctx.parsed, ...parsed };
11819
11844
  return next(req, ctx);
11820
11845
  };
11821
11846
  }
@@ -11831,7 +11856,11 @@ function getCookies(req) {
11831
11856
  const name15 = pair.slice(0, idx).trim();
11832
11857
  const value = pair.slice(idx + 1).trim();
11833
11858
  if (name15) {
11834
- cookies[name15] = decodeURIComponent(value);
11859
+ try {
11860
+ cookies[name15] = decodeURIComponent(value);
11861
+ } catch {
11862
+ cookies[name15] = value;
11863
+ }
11835
11864
  }
11836
11865
  }
11837
11866
  return cookies;
@@ -11874,67 +11903,45 @@ function upload(options) {
11874
11903
  const saveDir = options?.dir;
11875
11904
  return async (req, ctx, next) => {
11876
11905
  const ct = req.headers.get("content-type") ?? "";
11877
- if (!ct.includes("multipart/form-data")) {
11878
- return next(req, ctx);
11879
- }
11880
- const match = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
11881
- if (!match) {
11882
- return Response.json({ error: "Missing boundary" }, { status: 400 });
11906
+ if (!ct.includes("multipart/form-data")) return next(req, ctx);
11907
+ let formData;
11908
+ try {
11909
+ formData = await req.formData();
11910
+ } catch {
11911
+ return Response.json({ error: "Invalid multipart data" }, { status: 400 });
11883
11912
  }
11884
- const boundary = match[1] ?? match[2];
11885
- const body = await req.text();
11886
- const rawParts = body.split(`--${boundary}`).filter((p) => p && !p.startsWith("--") && !p.startsWith("\r\n--"));
11887
11913
  const files = {};
11888
11914
  const fields = {};
11889
- for (const raw of rawParts) {
11890
- const trimmed = raw.replace(/^\r?\n/, "");
11891
- const lines = trimmed.split(/\r?\n/);
11892
- let i = 0;
11893
- const headers = {};
11894
- while (i < lines.length && lines[i].length > 0) {
11895
- const sep3 = lines[i].indexOf(": ");
11896
- if (sep3 !== -1) headers[lines[i].slice(0, sep3).toLowerCase()] = lines[i].slice(sep3 + 2);
11897
- i++;
11898
- }
11899
- i++;
11900
- const bodyValue = lines.slice(i).join("\r\n");
11901
- const disposition = headers["content-disposition"] ?? "";
11902
- const nameMatch = disposition.match(/name="([^"]*)"/);
11903
- if (!nameMatch) continue;
11904
- const name15 = nameMatch[1];
11905
- const filenameMatch = disposition.match(/filename="([^"]*)"/);
11906
- const filename = filenameMatch?.[1];
11907
- if (filename) {
11908
- const buf = Buffer.from(bodyValue.replace(/\r?\n$/, ""), "binary");
11909
- if (options?.allowedTypes) {
11910
- const mime = headers["content-type"] ?? "application/octet-stream";
11911
- if (!options.allowedTypes.includes(mime)) {
11912
- return Response.json({ error: `File type not allowed: ${mime}` }, { status: 415 });
11913
- }
11915
+ for (const [key, value] of formData) {
11916
+ if (value instanceof File) {
11917
+ if (options?.allowedTypes && !options.allowedTypes.includes(value.type)) {
11918
+ return Response.json({ error: `File type not allowed: ${value.type}` }, { status: 415 });
11914
11919
  }
11915
- if (options?.maxFileSize && buf.byteLength > options.maxFileSize) {
11916
- return Response.json({ error: `File too large: ${filename}` }, { status: 413 });
11920
+ if (options?.maxFileSize && value.size > options.maxFileSize) {
11921
+ return Response.json({ error: `File too large: ${value.name}` }, { status: 413 });
11917
11922
  }
11923
+ const buf = Buffer.from(await value.arrayBuffer());
11918
11924
  const uf = {
11919
- name: filename,
11920
- type: headers["content-type"] ?? "application/octet-stream",
11925
+ name: value.name,
11926
+ type: value.type,
11921
11927
  size: buf.byteLength,
11922
11928
  buffer: saveDir ? void 0 : buf
11923
11929
  };
11924
11930
  if (saveDir) {
11925
- const filePath = join2(saveDir, `${randomUUID()}-${filename}`);
11931
+ const safeName = value.name.replace(/[/\\]/g, "");
11932
+ const filePath = join2(saveDir, `${randomUUID()}-${safeName}`);
11926
11933
  await mkdir(saveDir, { recursive: true });
11927
11934
  await writeFile(filePath, buf);
11928
11935
  uf.path = filePath;
11929
11936
  }
11930
- if (files[name15]) {
11931
- const existing = files[name15];
11932
- files[name15] = Array.isArray(existing) ? [...existing, uf] : [existing, uf];
11937
+ if (files[key]) {
11938
+ const existing = files[key];
11939
+ files[key] = Array.isArray(existing) ? [...existing, uf] : [existing, uf];
11933
11940
  } else {
11934
- files[name15] = uf;
11941
+ files[key] = uf;
11935
11942
  }
11936
11943
  } else {
11937
- fields[name15] = bodyValue.replace(/\r?\n$/, "");
11944
+ fields[key] = value;
11938
11945
  }
11939
11946
  }
11940
11947
  ctx.parsed = { ...ctx.parsed, files, fields };
@@ -13542,12 +13549,21 @@ function queue(opts) {
13542
13549
  if (!running) return;
13543
13550
  try {
13544
13551
  const now = Date.now();
13545
- const jobs = await redis2.zrangebyscore(jobKey, 0, now);
13546
- if (jobs.length > 0) {
13547
- await redis2.zrem(jobKey, ...jobs);
13548
- }
13549
- for (const raw of jobs) {
13550
- const job = JSON.parse(raw);
13552
+ while (true) {
13553
+ const result = await redis2.zpopmin(jobKey);
13554
+ if (result.length < 2) break;
13555
+ const raw = result[0];
13556
+ const score = parseInt(result[1], 10);
13557
+ if (score > now) {
13558
+ await redis2.zadd(jobKey, score, raw);
13559
+ break;
13560
+ }
13561
+ let job;
13562
+ try {
13563
+ job = JSON.parse(raw);
13564
+ } catch {
13565
+ continue;
13566
+ }
13551
13567
  const handler = handlers.get(job.type);
13552
13568
  if (handler) {
13553
13569
  handler(job).then(() => {
@@ -13555,11 +13571,13 @@ function queue(opts) {
13555
13571
  try {
13556
13572
  const nextRun = cronNext(job.schedule);
13557
13573
  const nextJob = { ...job, id: crypto3.randomUUID(), runAt: nextRun, createdAt: Date.now() };
13558
- redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob));
13574
+ redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob)).catch(() => {
13575
+ });
13559
13576
  } catch {
13560
13577
  }
13561
13578
  }
13562
- }).catch(() => {
13579
+ }).catch((e) => {
13580
+ console.error("[queue] handler error:", e);
13563
13581
  });
13564
13582
  }
13565
13583
  }
@@ -13844,6 +13862,10 @@ async function getUserTable(sql, tenantId, slug) {
13844
13862
  `;
13845
13863
  return row ?? null;
13846
13864
  }
13865
+ function requireAdmin(ctx) {
13866
+ if (ctx.tenant?.role !== "admin") return Response.json({ error: "Forbidden" }, { status: 403 });
13867
+ return null;
13868
+ }
13847
13869
  function buildRouter(sql, usersTable) {
13848
13870
  const r = new Router();
13849
13871
  r.post("/sys/tenants", async (req, ctx) => {
@@ -13869,6 +13891,8 @@ function buildRouter(sql, usersTable) {
13869
13891
  return Response.json(rows);
13870
13892
  });
13871
13893
  r.post("/sys/tenants/invite", async (req, ctx) => {
13894
+ const err = requireAdmin(ctx);
13895
+ if (err) return err;
13872
13896
  const { email, role = "member" } = await req.json();
13873
13897
  const [user2] = await sql`
13874
13898
  SELECT id FROM ${sql(usersTable)} WHERE "email" = ${email} LIMIT 1
@@ -13886,6 +13910,8 @@ function buildRouter(sql, usersTable) {
13886
13910
  return Response.json({ ok: true }, { status: 201 });
13887
13911
  });
13888
13912
  r.delete("/sys/tenants/members/:userId", async (req, ctx) => {
13913
+ const err = requireAdmin(ctx);
13914
+ if (err) return err;
13889
13915
  const userId = parseInt(ctx.params.userId, 10);
13890
13916
  await sql`
13891
13917
  DELETE FROM "_tenant_members"
@@ -13894,6 +13920,8 @@ function buildRouter(sql, usersTable) {
13894
13920
  return Response.json({ ok: true });
13895
13921
  });
13896
13922
  r.post("/sys/tables", async (req, ctx) => {
13923
+ const err = requireAdmin(ctx);
13924
+ if (err) return err;
13897
13925
  const body = await req.json();
13898
13926
  const slugErr = validateSlug(body.slug);
13899
13927
  if (slugErr) return Response.json({ error: slugErr }, { status: 400 });
@@ -13932,6 +13960,8 @@ function buildRouter(sql, usersTable) {
13932
13960
  return Response.json(table);
13933
13961
  });
13934
13962
  r.patch("/sys/tables/:slug", async (req, ctx) => {
13963
+ const err = requireAdmin(ctx);
13964
+ if (err) return err;
13935
13965
  const body = await req.json();
13936
13966
  if (!body.fields || !Array.isArray(body.fields)) {
13937
13967
  return Response.json({ error: "fields array required" }, { status: 400 });
@@ -13952,6 +13982,8 @@ function buildRouter(sql, usersTable) {
13952
13982
  return Response.json({ ...table, fields: merged });
13953
13983
  });
13954
13984
  r.delete("/sys/tables/:slug", async (_req, ctx) => {
13985
+ const err = requireAdmin(ctx);
13986
+ if (err) return err;
13955
13987
  await sql.unsafe(dropTableSQL(ctx.tenant.id, ctx.params.slug));
13956
13988
  await sql`
13957
13989
  DELETE FROM "_user_tables"
@@ -24407,15 +24439,19 @@ function createWSHandler(deps) {
24407
24439
  VALUES (${channel_id}, ${am.member_id}, 'agent', ${result.output})
24408
24440
  `.then(([r]) => {
24409
24441
  broadcastToChannel(channel_id, { type: "message", data: r });
24442
+ }).catch((e) => {
24443
+ console.error("[messager] agent reply insert failed:", e);
24410
24444
  });
24411
24445
  }
24412
- }).catch(() => {
24446
+ }).catch((e) => {
24447
+ console.error("[messager] agent run failed:", e);
24413
24448
  });
24414
24449
  }
24415
24450
  }
24416
24451
  break;
24417
24452
  }
24418
24453
  case "typing": {
24454
+ if (channel_id) subscribe(ws, userId, channel_id);
24419
24455
  broadcastToChannel(channel_id, {
24420
24456
  type: "typing",
24421
24457
  channel_id,
@@ -24426,6 +24462,7 @@ function createWSHandler(deps) {
24426
24462
  }
24427
24463
  case "read": {
24428
24464
  if (!channel_id || !last_message_id) return;
24465
+ subscribe(ws, userId, channel_id);
24429
24466
  await sql`
24430
24467
  UPDATE "_channel_members"
24431
24468
  SET last_read_id = ${last_message_id}, last_read_at = NOW()
@@ -24570,9 +24607,12 @@ function buildRouter3(deps) {
24570
24607
  VALUES (${channelId}, ${am.member_id}, 'agent', ${result.output})
24571
24608
  `.then(([r2]) => {
24572
24609
  broadcastToChannel(channelId, { type: "message", data: r2 });
24610
+ }).catch((e) => {
24611
+ console.error("[messager] agent reply insert failed:", e);
24573
24612
  });
24574
24613
  }
24575
- }).catch(() => {
24614
+ }).catch((e) => {
24615
+ console.error("[messager] agent run failed:", e);
24576
24616
  });
24577
24617
  }
24578
24618
  }
package/dist/serve.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface ServeOptions {
6
6
  hostname?: string;
7
7
  signal?: AbortSignal;
8
8
  websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
9
+ maxBodySize?: number;
9
10
  }
10
11
  export interface Server {
11
12
  stop: () => void;
@@ -13,7 +14,7 @@ export interface Server {
13
14
  readonly hostname: string;
14
15
  ready: Promise<void>;
15
16
  }
16
- export declare function readBody(req: IncomingMessage): Promise<Buffer>;
17
+ export declare function readBody(req: IncomingMessage, maxSize?: number): Promise<Buffer>;
17
18
  export declare function createRequest(req: IncomingMessage, body: Buffer): [Request, Record<string, string>];
18
19
  export declare function sendResponse(res: ServerResponse, response: Response): Promise<void>;
19
20
  export declare function serve(handler: Handler, options?: ServeOptions): Server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weifuwu",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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",