weifuwu 0.7.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/README.md CHANGED
@@ -28,6 +28,7 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
28
28
  - **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
29
29
  - **Cookie** — `getCookies()`, `setCookie()`, `deleteCookie()` — immutable
30
30
  - **Error handling** — global `onError()`
31
+ - **Deploy** — `deploy()` — self-hosted PaaS: multi-app reverse proxy, subdomain routing, zero-downtime updates, auto SSL, Git-based deployment
31
32
  - **Zero build** — native TypeScript in Node.js v24+
32
33
  - **Zero deps** (core) — only `node:http` and `node:stream`
33
34
 
@@ -998,6 +999,42 @@ const app = new Router()
998
999
  .get('/crash', () => { throw new Error('boom') })
999
1000
  ```
1000
1001
 
1002
+ ## Deploy
1003
+
1004
+ See [deploy.md](./deploy.md) for complete documentation — VPS setup, subdomain routing, blue-green zero-downtime, WebSocket bridge, Git webhook, auto SSL, and management API.
1005
+
1006
+ Quick start on a fresh VPS:
1007
+
1008
+ ```bash
1009
+ # 1. Install Node.js
1010
+ curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
1011
+ apt-get install -y nodejs git
1012
+
1013
+ # 2. Create deploy project
1014
+ mkdir -p /opt/deploy && cd /opt/deploy
1015
+ npm init -y && npm install weifuwu
1016
+
1017
+ # 3. Write deploy.ts
1018
+ cat > deploy.ts << 'EOF'
1019
+ import { deploy, defineConfig } from 'weifuwu'
1020
+ await deploy(defineConfig({
1021
+ domain: 'example.com',
1022
+ deployToken: process.env.DEPLOY_TOKEN,
1023
+ apps: {
1024
+ blog: {
1025
+ repo: 'https://github.com/me/my-blog.git',
1026
+ subdomain: 'blog',
1027
+ entry: 'app.ts',
1028
+ port: 3001,
1029
+ },
1030
+ },
1031
+ }))
1032
+ EOF
1033
+
1034
+ # 4. Run
1035
+ DEPLOY_TOKEN='my-secret' node deploy.ts
1036
+ ```
1037
+
1001
1038
  ## API
1002
1039
 
1003
1040
  ### `serve(handler, options?)`
@@ -1099,6 +1136,13 @@ Returns `Promise<Router>`.
1099
1136
  | `ai(handler)` | AI streaming endpoint (POST) |
1100
1137
  | `workflow(handler)` | Workflow engine (POST + SSE) |
1101
1138
 
1139
+ ### Deploy
1140
+
1141
+ | Import | Description |
1142
+ |--------|-------------|
1143
+ | `deploy(config)` | Start the deployment platform — see [deploy.md](./deploy.md) |
1144
+ | `defineConfig(config)` | Type-safe config helper with validation — see [deploy.md](./deploy.md) |
1145
+
1102
1146
  ### Utilities
1103
1147
 
1104
1148
  | Function | Description |
@@ -0,0 +1,2 @@
1
+ import type { DeployConfig } from './types.ts';
2
+ export declare function defineConfig(config: DeployConfig): DeployConfig;
@@ -0,0 +1,2 @@
1
+ import type { DeployConfig, GatewayResult } from './types.ts';
2
+ export declare function createGateway(config: DeployConfig, getPort: (name: string) => number | undefined): GatewayResult;
@@ -0,0 +1,4 @@
1
+ import type { DeployConfig, DeployServer } from './types.ts';
2
+ export { defineConfig } from './config.ts';
3
+ export type { DeployConfig, AppConfig, DeployServer, AppStatus, GatewayResult } from './types.ts';
4
+ export declare function deploy(config: DeployConfig): Promise<DeployServer>;
@@ -0,0 +1,16 @@
1
+ import { Router } from '../router.ts';
2
+ import type { DeployConfig, AppStatus } from './types.ts';
3
+ export interface AppRuntime {
4
+ config: import('./types.ts').AppConfig;
5
+ status: AppStatus;
6
+ logs: string[];
7
+ process: import('node:child_process').ChildProcess | null;
8
+ currentPort: number;
9
+ startedAt: number | null;
10
+ restartCount: number;
11
+ restartTimer: ReturnType<typeof setTimeout> | undefined;
12
+ }
13
+ export declare function createManager(config: DeployConfig, apps: Map<string, AppRuntime>, manager: {
14
+ deployApp(name: string): Promise<void>;
15
+ reloadConfig(): Promise<void>;
16
+ }): Router;
@@ -0,0 +1,14 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ export interface ManagedProcess {
3
+ child: ChildProcess;
4
+ port: number;
5
+ }
6
+ export declare function forkApp(opts: {
7
+ cwd: string;
8
+ entry: string;
9
+ port: number;
10
+ env?: Record<string, string>;
11
+ onLog?: (line: string) => void;
12
+ }): ManagedProcess;
13
+ export declare function stopProcess(mp: ManagedProcess, timeout?: number): Promise<void>;
14
+ export declare function healthCheck(port: number, path?: string): Promise<boolean>;
@@ -0,0 +1,62 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ import type { Duplex } from 'node:stream';
3
+ import type { Handler } from '../types.ts';
4
+ export interface DeployConfig {
5
+ domain: string;
6
+ port?: number;
7
+ ssl?: {
8
+ email: string;
9
+ staging?: boolean;
10
+ };
11
+ deployToken?: string;
12
+ webhookSecret?: string;
13
+ appsDir?: string;
14
+ defaultApp?: string;
15
+ apps: Record<string, AppConfig>;
16
+ }
17
+ export interface AppConfig {
18
+ repo: string;
19
+ branch?: string;
20
+ subdomain?: string;
21
+ path?: string;
22
+ port: number;
23
+ ports?: [number, number];
24
+ entry: string;
25
+ env?: Record<string, string>;
26
+ healthEndpoint?: string;
27
+ buildCommand?: string;
28
+ }
29
+ export interface AppStatus {
30
+ name: string;
31
+ status: 'starting' | 'running' | 'stopped' | 'error';
32
+ port: number;
33
+ subdomain?: string;
34
+ path?: string;
35
+ pid?: number;
36
+ uptime?: number;
37
+ error?: string;
38
+ }
39
+ export interface DeployServer {
40
+ stop(): Promise<void>;
41
+ ready: Promise<void>;
42
+ url: string;
43
+ apps: {
44
+ list(): AppStatus[];
45
+ status(name: string): AppStatus | undefined;
46
+ deploy(name: string): Promise<void>;
47
+ restart(name: string): Promise<void>;
48
+ stop(name: string): Promise<void>;
49
+ start(name: string): Promise<void>;
50
+ };
51
+ }
52
+ export interface GatewayResult {
53
+ handler: Handler;
54
+ wsHandler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
55
+ }
56
+ declare module '../types.ts' {
57
+ interface Context {
58
+ deploy?: {
59
+ appName?: string;
60
+ };
61
+ }
62
+ }
package/dist/index.d.ts CHANGED
@@ -39,3 +39,5 @@ export { agent } from './agent/index.ts';
39
39
  export type { AgentOptions, AgentModule, AgentConfig, RunParams, RunResult, KnowledgeDoc } from './agent/types.ts';
40
40
  export { messager } from './messager/index.ts';
41
41
  export type { MessagerOptions, MessagerModule, Channel, ChannelMember, Message } from './messager/types.ts';
42
+ export { deploy, defineConfig } from './deploy/index.ts';
43
+ export type { DeployConfig, AppConfig, DeployServer, AppStatus } from './deploy/types.ts';
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
  }
@@ -10918,38 +10938,38 @@ var Router = class _Router {
10918
10938
  }
10919
10939
  return this;
10920
10940
  }
10921
- get(path, ...args) {
10922
- return this.route("GET", path, ...args);
10941
+ get(path2, ...args) {
10942
+ return this.route("GET", path2, ...args);
10923
10943
  }
10924
- post(path, ...args) {
10925
- return this.route("POST", path, ...args);
10944
+ post(path2, ...args) {
10945
+ return this.route("POST", path2, ...args);
10926
10946
  }
10927
- put(path, ...args) {
10928
- return this.route("PUT", path, ...args);
10947
+ put(path2, ...args) {
10948
+ return this.route("PUT", path2, ...args);
10929
10949
  }
10930
- delete(path, ...args) {
10931
- return this.route("DELETE", path, ...args);
10950
+ delete(path2, ...args) {
10951
+ return this.route("DELETE", path2, ...args);
10932
10952
  }
10933
- patch(path, ...args) {
10934
- return this.route("PATCH", path, ...args);
10953
+ patch(path2, ...args) {
10954
+ return this.route("PATCH", path2, ...args);
10935
10955
  }
10936
- head(path, ...args) {
10937
- return this.route("HEAD", path, ...args);
10956
+ head(path2, ...args) {
10957
+ return this.route("HEAD", path2, ...args);
10938
10958
  }
10939
- options(path, ...args) {
10940
- return this.route("OPTIONS", path, ...args);
10959
+ options(path2, ...args) {
10960
+ return this.route("OPTIONS", path2, ...args);
10941
10961
  }
10942
- all(path, ...args) {
10943
- return this.route("*", path, ...args);
10962
+ all(path2, ...args) {
10963
+ return this.route("*", path2, ...args);
10944
10964
  }
10945
10965
  onError(handler) {
10946
10966
  this.errorHandler = handler;
10947
10967
  return this;
10948
10968
  }
10949
- route(method, path, ...args) {
10969
+ route(method, path2, ...args) {
10950
10970
  const handler = args.pop();
10951
10971
  const middlewares = args;
10952
- const segments = this.splitPath(path);
10972
+ const segments = this.splitPath(path2);
10953
10973
  let node = this.root;
10954
10974
  for (const segment of segments) {
10955
10975
  if (segment === "*") {
@@ -10964,10 +10984,10 @@ var Router = class _Router {
10964
10984
  if (middlewares.length > 0) node.middlewares.set(method, middlewares);
10965
10985
  return this;
10966
10986
  }
10967
- ws(path, ...args) {
10987
+ ws(path2, ...args) {
10968
10988
  const handler = args.pop();
10969
10989
  const middlewares = args;
10970
- const segments = this.splitPath(path);
10990
+ const segments = this.splitPath(path2);
10971
10991
  let node = this.wsRoot;
10972
10992
  for (const segment of segments) {
10973
10993
  node = getWsNode(node, segment);
@@ -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) => {
@@ -11026,8 +11051,8 @@ var Router = class _Router {
11026
11051
  });
11027
11052
  };
11028
11053
  }
11029
- splitPath(path) {
11030
- return path.split("/").filter(Boolean);
11054
+ splitPath(path2) {
11055
+ return path2.split("/").filter(Boolean);
11031
11056
  }
11032
11057
  matchTrie(method, segments) {
11033
11058
  let node = this.root;
@@ -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 };
@@ -12192,9 +12199,9 @@ function tool(def) {
12192
12199
  }
12193
12200
 
12194
12201
  // workflow/reference.ts
12195
- function getByPath(obj, path) {
12202
+ function getByPath(obj, path2) {
12196
12203
  let current = obj;
12197
- for (const key of path) {
12204
+ for (const key of path2) {
12198
12205
  if (current === null || current === void 0) return void 0;
12199
12206
  if (typeof current === "object" && key in current) {
12200
12207
  current = current[key];
@@ -12204,9 +12211,9 @@ function getByPath(obj, path) {
12204
12211
  }
12205
12212
  return current;
12206
12213
  }
12207
- function resolveRef(path, ctx) {
12208
- if (path.startsWith("$nodes.")) {
12209
- const afterNodes = path.slice(7);
12214
+ function resolveRef(path2, ctx) {
12215
+ if (path2.startsWith("$nodes.")) {
12216
+ const afterNodes = path2.slice(7);
12210
12217
  const dotIdx = afterNodes.indexOf(".");
12211
12218
  if (dotIdx === -1) {
12212
12219
  return ctx.nodeOutputs.get(afterNodes);
@@ -12222,23 +12229,23 @@ function resolveRef(path, ctx) {
12222
12229
  }
12223
12230
  return getByPath(output, propPath.split("."));
12224
12231
  }
12225
- if (path.startsWith("$var.")) {
12226
- const name15 = path.slice(5);
12232
+ if (path2.startsWith("$var.")) {
12233
+ const name15 = path2.slice(5);
12227
12234
  if (!ctx.variables.has(name15)) {
12228
12235
  throw new Error(`Variable "${name15}" is not defined`);
12229
12236
  }
12230
12237
  return ctx.variables.get(name15);
12231
12238
  }
12232
- if (path.startsWith("$input.")) {
12233
- const key = path.slice(7);
12239
+ if (path2.startsWith("$input.")) {
12240
+ const key = path2.slice(7);
12234
12241
  return ctx.input[key];
12235
12242
  }
12236
- if (path === "true") return true;
12237
- if (path === "false") return false;
12238
- if (path === "null") return null;
12239
- const num = Number(path);
12240
- if (!isNaN(num) && path.trim() !== "") return num;
12241
- return path;
12243
+ if (path2 === "true") return true;
12244
+ if (path2 === "false") return false;
12245
+ if (path2 === "null") return null;
12246
+ const num = Number(path2);
12247
+ if (!isNaN(num) && path2.trim() !== "") return num;
12248
+ return path2;
12242
12249
  }
12243
12250
  function resolveValue(v, ctx) {
12244
12251
  if (typeof v === "string" && v.startsWith("$")) {
@@ -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"
@@ -23884,37 +23916,37 @@ function createOpenAI(options = {}) {
23884
23916
  );
23885
23917
  const createChatModel = (modelId) => new OpenAIChatLanguageModel(modelId, {
23886
23918
  provider: `${providerName}.chat`,
23887
- url: ({ path }) => `${baseURL}${path}`,
23919
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23888
23920
  headers: getHeaders,
23889
23921
  fetch: options.fetch
23890
23922
  });
23891
23923
  const createCompletionModel = (modelId) => new OpenAICompletionLanguageModel(modelId, {
23892
23924
  provider: `${providerName}.completion`,
23893
- url: ({ path }) => `${baseURL}${path}`,
23925
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23894
23926
  headers: getHeaders,
23895
23927
  fetch: options.fetch
23896
23928
  });
23897
23929
  const createEmbeddingModel = (modelId) => new OpenAIEmbeddingModel(modelId, {
23898
23930
  provider: `${providerName}.embedding`,
23899
- url: ({ path }) => `${baseURL}${path}`,
23931
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23900
23932
  headers: getHeaders,
23901
23933
  fetch: options.fetch
23902
23934
  });
23903
23935
  const createImageModel = (modelId) => new OpenAIImageModel(modelId, {
23904
23936
  provider: `${providerName}.image`,
23905
- url: ({ path }) => `${baseURL}${path}`,
23937
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23906
23938
  headers: getHeaders,
23907
23939
  fetch: options.fetch
23908
23940
  });
23909
23941
  const createTranscriptionModel = (modelId) => new OpenAITranscriptionModel(modelId, {
23910
23942
  provider: `${providerName}.transcription`,
23911
- url: ({ path }) => `${baseURL}${path}`,
23943
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23912
23944
  headers: getHeaders,
23913
23945
  fetch: options.fetch
23914
23946
  });
23915
23947
  const createSpeechModel = (modelId) => new OpenAISpeechModel(modelId, {
23916
23948
  provider: `${providerName}.speech`,
23917
- url: ({ path }) => `${baseURL}${path}`,
23949
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23918
23950
  headers: getHeaders,
23919
23951
  fetch: options.fetch
23920
23952
  });
@@ -23929,7 +23961,7 @@ function createOpenAI(options = {}) {
23929
23961
  const createResponsesModel = (modelId) => {
23930
23962
  return new OpenAIResponsesLanguageModel(modelId, {
23931
23963
  provider: `${providerName}.responses`,
23932
- url: ({ path }) => `${baseURL}${path}`,
23964
+ url: ({ path: path2 }) => `${baseURL}${path2}`,
23933
23965
  headers: getHeaders,
23934
23966
  fetch: options.fetch,
23935
23967
  fileIdPrefixes: ["file-"]
@@ -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
  }
@@ -24628,6 +24668,540 @@ function messager(options) {
24628
24668
  }
24629
24669
  };
24630
24670
  }
24671
+
24672
+ // deploy/index.ts
24673
+ import { execSync } from "node:child_process";
24674
+ import fs from "node:fs";
24675
+ import path from "node:path";
24676
+
24677
+ // deploy/gateway.ts
24678
+ import WebSocket, { WebSocketServer as WebSocketServer2 } from "ws";
24679
+ function isBareDomain(host, domain) {
24680
+ return host === domain || host === `www.${domain}`;
24681
+ }
24682
+ function matchApp(config, getPort, host, pathname) {
24683
+ for (const [name15, ac] of Object.entries(config.apps)) {
24684
+ if (ac.subdomain && host === `${ac.subdomain}.${config.domain}`) {
24685
+ const port = getPort(name15);
24686
+ if (port) return { name: name15, port };
24687
+ }
24688
+ }
24689
+ const pathApps = Object.entries(config.apps).filter(([, ac]) => ac.path).sort(([, a], [, b]) => (b.path?.length ?? 0) - (a.path?.length ?? 0));
24690
+ for (const [name15, ac] of pathApps) {
24691
+ if (ac.path && pathname.startsWith(ac.path)) {
24692
+ const port = getPort(name15);
24693
+ if (port) return { name: name15, port, stripPath: ac.path };
24694
+ }
24695
+ }
24696
+ if (config.defaultApp && isBareDomain(host, config.domain)) {
24697
+ const port = getPort(config.defaultApp);
24698
+ if (port) return { name: config.defaultApp, port };
24699
+ }
24700
+ return void 0;
24701
+ }
24702
+ function createGateway(config, getPort) {
24703
+ const handler = async (req) => {
24704
+ const url = new URL(req.url);
24705
+ const match = matchApp(config, getPort, url.hostname, url.pathname);
24706
+ if (!match) return new Response("Not Found", { status: 404 });
24707
+ let targetPath = url.pathname;
24708
+ if (match.stripPath && targetPath.startsWith(match.stripPath)) {
24709
+ targetPath = targetPath.slice(match.stripPath.length) || "/";
24710
+ }
24711
+ const target = `http://127.0.0.1:${match.port}${targetPath}${url.search}`;
24712
+ try {
24713
+ const proxyReq = new Request(target, {
24714
+ method: req.method,
24715
+ headers: req.headers,
24716
+ body: req.method !== "GET" && req.method !== "HEAD" ? req.body : null
24717
+ });
24718
+ return await fetch(proxyReq);
24719
+ } catch {
24720
+ return new Response("Bad Gateway", { status: 502 });
24721
+ }
24722
+ };
24723
+ const wss = new WebSocketServer2({ noServer: true });
24724
+ const wsHandler = (req, socket, head) => {
24725
+ const url = new URL(req.url ?? "/", "http://localhost");
24726
+ const host = req.headers.host?.split(":")[0] ?? "";
24727
+ const match = matchApp(config, getPort, host, url.pathname);
24728
+ if (!match) {
24729
+ socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
24730
+ socket.destroy();
24731
+ return;
24732
+ }
24733
+ let targetPath = url.pathname;
24734
+ if (match.stripPath && targetPath.startsWith(match.stripPath)) {
24735
+ targetPath = targetPath.slice(match.stripPath.length) || "/";
24736
+ }
24737
+ const wsUrl = `ws://127.0.0.1:${match.port}${targetPath}${url.search}`;
24738
+ const backendWS = new WebSocket(wsUrl);
24739
+ backendWS.on("open", () => {
24740
+ wss.handleUpgrade(req, socket, head, (clientWS) => {
24741
+ const clientSend = (data) => {
24742
+ clientWS.send(data);
24743
+ };
24744
+ const backendSend = (data) => {
24745
+ backendWS.send(data);
24746
+ };
24747
+ clientWS.on("message", backendSend);
24748
+ backendWS.on("message", clientSend);
24749
+ clientWS.on("close", () => backendWS.close());
24750
+ backendWS.on("close", () => clientWS.close());
24751
+ clientWS.on("error", () => backendWS.close());
24752
+ backendWS.on("error", () => clientWS.close());
24753
+ });
24754
+ });
24755
+ backendWS.on("error", () => {
24756
+ socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
24757
+ socket.destroy();
24758
+ });
24759
+ };
24760
+ return { handler, wsHandler };
24761
+ }
24762
+
24763
+ // deploy/manager.ts
24764
+ import crypto4 from "node:crypto";
24765
+ function createManager(config, apps, manager) {
24766
+ const router = new Router();
24767
+ const auth2 = (req, ctx, next) => {
24768
+ if (!config.deployToken) return next(req, ctx);
24769
+ const header = req.headers.get("authorization") ?? "";
24770
+ const token = header.replace("Bearer ", "");
24771
+ if (token !== config.deployToken) {
24772
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
24773
+ }
24774
+ return next(req, ctx);
24775
+ };
24776
+ router.get("/apps", auth2, () => {
24777
+ const list = Array.from(apps.values()).map((a) => a.status);
24778
+ return Response.json(list);
24779
+ });
24780
+ router.get("/apps/:name", auth2, (req, ctx) => {
24781
+ const app = apps.get(ctx.params.name);
24782
+ if (!app) return new Response("Not Found", { status: 404 });
24783
+ return Response.json(app.status);
24784
+ });
24785
+ router.post("/apps/:name/deploy", auth2, async (req, ctx) => {
24786
+ const app = apps.get(ctx.params.name);
24787
+ if (!app) return new Response("Not Found", { status: 404 });
24788
+ try {
24789
+ await manager.deployApp(ctx.params.name);
24790
+ return Response.json({ success: true });
24791
+ } catch (err) {
24792
+ const msg = err instanceof Error ? err.message : String(err);
24793
+ return Response.json({ error: msg }, { status: 500 });
24794
+ }
24795
+ });
24796
+ router.post("/apps/:name/restart", auth2, async (req, ctx) => {
24797
+ const app = apps.get(ctx.params.name);
24798
+ if (!app) return new Response("Not Found", { status: 404 });
24799
+ try {
24800
+ await manager.deployApp(ctx.params.name);
24801
+ return Response.json({ success: true });
24802
+ } catch (err) {
24803
+ const msg = err instanceof Error ? err.message : String(err);
24804
+ return Response.json({ error: msg }, { status: 500 });
24805
+ }
24806
+ });
24807
+ router.post("/apps/:name/stop", auth2, async (req, ctx) => {
24808
+ const app = apps.get(ctx.params.name);
24809
+ if (!app) return new Response("Not Found", { status: 404 });
24810
+ if (app.process) {
24811
+ app.process.kill("SIGTERM");
24812
+ app.process = null;
24813
+ }
24814
+ app.status = { ...app.status, status: "stopped", pid: void 0 };
24815
+ return Response.json({ success: true });
24816
+ });
24817
+ router.post("/apps/:name/start", auth2, async (req, ctx) => {
24818
+ const app = apps.get(ctx.params.name);
24819
+ if (!app) return new Response("Not Found", { status: 404 });
24820
+ try {
24821
+ await manager.deployApp(ctx.params.name);
24822
+ return Response.json({ success: true });
24823
+ } catch (err) {
24824
+ const msg = err instanceof Error ? err.message : String(err);
24825
+ return Response.json({ error: msg }, { status: 500 });
24826
+ }
24827
+ });
24828
+ router.get("/apps/:name/logs", auth2, (req, ctx) => {
24829
+ const app = apps.get(ctx.params.name);
24830
+ if (!app) return new Response("Not Found", { status: 404 });
24831
+ let index = app.logs.length;
24832
+ let interval;
24833
+ const stream = new ReadableStream({
24834
+ start(controller) {
24835
+ for (const line of app.logs) {
24836
+ controller.enqueue(`data: ${JSON.stringify({ line })}
24837
+
24838
+ `);
24839
+ }
24840
+ interval = setInterval(() => {
24841
+ while (index < app.logs.length) {
24842
+ controller.enqueue(`data: ${JSON.stringify({ line: app.logs[index] })}
24843
+
24844
+ `);
24845
+ index++;
24846
+ }
24847
+ }, 500);
24848
+ },
24849
+ cancel() {
24850
+ if (interval) clearInterval(interval);
24851
+ }
24852
+ });
24853
+ return new Response(stream, {
24854
+ headers: {
24855
+ "Content-Type": "text/event-stream",
24856
+ "Cache-Control": "no-cache"
24857
+ }
24858
+ });
24859
+ });
24860
+ router.post("/reload", auth2, async () => {
24861
+ try {
24862
+ await manager.reloadConfig();
24863
+ return Response.json({ success: true });
24864
+ } catch (err) {
24865
+ const msg = err instanceof Error ? err.message : String(err);
24866
+ return Response.json({ error: msg }, { status: 400 });
24867
+ }
24868
+ });
24869
+ router.post("/webhook", async (req) => {
24870
+ if (config.webhookSecret) {
24871
+ const sig = req.headers.get("x-hub-signature-256") ?? "";
24872
+ const body = await req.clone().text();
24873
+ const hmac = crypto4.createHmac("sha256", config.webhookSecret).update(body).digest("hex");
24874
+ if (sig !== `sha256=${hmac}`) {
24875
+ return Response.json({ error: "invalid signature" }, { status: 401 });
24876
+ }
24877
+ }
24878
+ const payload = await req.json();
24879
+ const repoUrl = payload?.repository?.clone_url ?? payload?.repository?.html_url ?? "";
24880
+ if (!repoUrl) return Response.json({ deployed: [] });
24881
+ const deployed = [];
24882
+ for (const [name15] of apps) {
24883
+ const ac = apps.get(name15)?.config;
24884
+ if (!ac) continue;
24885
+ const repoNorm = ac.repo.replace(/\.git$/, "").replace(/https:\/\/[^@]+@/, "https://");
24886
+ const payloadNorm = repoUrl.replace(/\.git$/, "").replace(/https:\/\/[^@]+@/, "https://");
24887
+ if (payloadNorm.includes(repoNorm)) {
24888
+ await manager.deployApp(name15);
24889
+ deployed.push(name15);
24890
+ }
24891
+ }
24892
+ return Response.json({ deployed });
24893
+ });
24894
+ return router;
24895
+ }
24896
+
24897
+ // deploy/process.ts
24898
+ import { fork } from "node:child_process";
24899
+ function forkApp(opts) {
24900
+ const child = fork(opts.entry, [], {
24901
+ cwd: opts.cwd,
24902
+ env: {
24903
+ ...process.env,
24904
+ ...opts.env,
24905
+ PORT: String(opts.port)
24906
+ },
24907
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
24908
+ });
24909
+ child.stdout?.on("data", (chunk) => {
24910
+ for (const line of chunk.toString().split("\n").filter(Boolean)) {
24911
+ opts.onLog?.(line);
24912
+ }
24913
+ });
24914
+ child.stderr?.on("data", (chunk) => {
24915
+ for (const line of chunk.toString().split("\n").filter(Boolean)) {
24916
+ opts.onLog?.(`[error] ${line}`);
24917
+ }
24918
+ });
24919
+ return { child, port: opts.port };
24920
+ }
24921
+ function stopProcess(mp, timeout = 1e4) {
24922
+ return new Promise((resolve3) => {
24923
+ const timer = setTimeout(() => {
24924
+ mp.child.kill("SIGKILL");
24925
+ resolve3();
24926
+ }, timeout);
24927
+ mp.child.on("exit", () => {
24928
+ clearTimeout(timer);
24929
+ resolve3();
24930
+ });
24931
+ mp.child.kill("SIGTERM");
24932
+ });
24933
+ }
24934
+ async function healthCheck(port, path2 = "/") {
24935
+ try {
24936
+ const res = await fetch(`http://127.0.0.1:${port}${path2}`, {
24937
+ signal: AbortSignal.timeout(5e3)
24938
+ });
24939
+ return res.ok;
24940
+ } catch {
24941
+ return false;
24942
+ }
24943
+ }
24944
+
24945
+ // deploy/config.ts
24946
+ function defineConfig(config) {
24947
+ if (!config.domain) throw new Error("deploy: domain is required");
24948
+ if (!config.apps || Object.keys(config.apps).length === 0) {
24949
+ throw new Error("deploy: at least one app is required");
24950
+ }
24951
+ for (const [name15, app] of Object.entries(config.apps)) {
24952
+ if (!app.repo) throw new Error(`deploy: app "${name15}" has no repo`);
24953
+ if (!app.entry) throw new Error(`deploy: app "${name15}" has no entry`);
24954
+ if (!app.port) throw new Error(`deploy: app "${name15}" has no port`);
24955
+ }
24956
+ return {
24957
+ port: config.port ?? 80,
24958
+ appsDir: config.appsDir ?? "/opt/weifuwu/apps",
24959
+ ...config
24960
+ };
24961
+ }
24962
+
24963
+ // deploy/index.ts
24964
+ async function deploy(config) {
24965
+ const appsDir = config.appsDir ?? "/opt/weifuwu/apps";
24966
+ const apps = /* @__PURE__ */ new Map();
24967
+ let httpServer;
24968
+ if (!fs.existsSync(appsDir)) {
24969
+ fs.mkdirSync(appsDir, { recursive: true });
24970
+ }
24971
+ async function forkAndCheck(cwd, entry, port, env, onLog, healthEndpoint) {
24972
+ try {
24973
+ const mp = forkApp({ cwd, entry, port, env, onLog });
24974
+ onLog(`[deploy] forked pid ${mp.child.pid} on port ${mp.port}`);
24975
+ const healthy = await healthCheck(port, healthEndpoint ?? "/");
24976
+ if (healthy) onLog("[deploy] health check passed");
24977
+ else onLog("[deploy] health check failed");
24978
+ return mp;
24979
+ } catch (err) {
24980
+ const msg = err instanceof Error ? err.message : String(err);
24981
+ onLog(`[deploy] fork error: ${msg}`);
24982
+ return null;
24983
+ }
24984
+ }
24985
+ function scheduleRestart(name15, runtime) {
24986
+ const delay = Math.min(1e3 * Math.pow(2, runtime.restartCount), 3e4);
24987
+ runtime.restartCount++;
24988
+ runtime.logs.push(`[deploy] auto-restart in ${delay}ms (attempt ${runtime.restartCount})`);
24989
+ runtime.restartTimer = setTimeout(() => initApp(name15), delay);
24990
+ }
24991
+ async function initApp(name15) {
24992
+ const ac = config.apps[name15];
24993
+ if (!ac) return;
24994
+ const old = apps.get(name15);
24995
+ if (old?.restartTimer) {
24996
+ clearTimeout(old.restartTimer);
24997
+ old.restartTimer = void 0;
24998
+ }
24999
+ const appDir = path.join(appsDir, name15);
25000
+ const logs = [];
25001
+ const log = (line) => {
25002
+ logs.push(line);
25003
+ if (logs.length > 1e3) logs.splice(0, logs.length - 1e3);
25004
+ };
25005
+ try {
25006
+ if (fs.existsSync(path.join(appDir, ".git"))) {
25007
+ execSync("git pull", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
25008
+ log("[deploy] git pull done");
25009
+ } else {
25010
+ if (fs.existsSync(appDir)) {
25011
+ fs.rmSync(appDir, { recursive: true });
25012
+ }
25013
+ execSync(`git clone ${ac.repo} ${appDir}`, { stdio: "pipe", timeout: 12e4 });
25014
+ log("[deploy] git clone done");
25015
+ if (ac.branch) {
25016
+ execSync(`git checkout ${ac.branch}`, { cwd: appDir, stdio: "pipe", timeout: 3e4 });
25017
+ log(`[deploy] switched to branch ${ac.branch}`);
25018
+ }
25019
+ }
25020
+ } catch (err) {
25021
+ const msg = err instanceof Error ? err.message : String(err);
25022
+ setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
25023
+ log(`[deploy] git error: ${msg}`);
25024
+ if (old?.process) {
25025
+ apps.set(name15, old);
25026
+ }
25027
+ return;
25028
+ }
25029
+ try {
25030
+ execSync("npm install", { cwd: appDir, stdio: "pipe", timeout: 12e4 });
25031
+ log("[deploy] npm install done");
25032
+ } catch (err) {
25033
+ const msg = err instanceof Error ? err.message : String(err);
25034
+ setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
25035
+ log(`[deploy] npm install error: ${msg}`);
25036
+ if (old?.process) apps.set(name15, old);
25037
+ return;
25038
+ }
25039
+ if (ac.buildCommand) {
25040
+ try {
25041
+ execSync(ac.buildCommand, { cwd: appDir, stdio: "pipe", timeout: 12e4 });
25042
+ log("[deploy] build done");
25043
+ } catch (err) {
25044
+ const msg = err instanceof Error ? err.message : String(err);
25045
+ setAppRuntime(name15, ac, logs, { status: "error", port: ac.port, error: msg });
25046
+ log(`[deploy] build error: ${msg}`);
25047
+ if (old?.process) apps.set(name15, old);
25048
+ return;
25049
+ }
25050
+ }
25051
+ let targetPort = ac.port;
25052
+ if (ac.ports && old?.process) {
25053
+ targetPort = old.currentPort === ac.ports[0] ? ac.ports[1] : ac.ports[0];
25054
+ }
25055
+ const mp = await forkAndCheck(appDir, ac.entry, targetPort, ac.env, log, ac.healthEndpoint);
25056
+ if (!mp) {
25057
+ log("[deploy] new process failed to start, keeping old running");
25058
+ if (old?.process) apps.set(name15, old);
25059
+ else {
25060
+ setAppRuntime(name15, ac, logs, { status: "error", port: targetPort, error: "failed to start" });
25061
+ }
25062
+ return;
25063
+ }
25064
+ const runtime = {
25065
+ config: ac,
25066
+ status: { name: name15, status: "running", port: targetPort, subdomain: ac.subdomain, path: ac.path, pid: mp.child.pid ?? void 0 },
25067
+ logs,
25068
+ process: mp.child,
25069
+ currentPort: targetPort,
25070
+ startedAt: Date.now(),
25071
+ restartCount: 0,
25072
+ restartTimer: void 0
25073
+ };
25074
+ apps.set(name15, runtime);
25075
+ mp.child.on("exit", (code, signal) => {
25076
+ runtime.process = null;
25077
+ runtime.status = {
25078
+ ...runtime.status,
25079
+ status: "error",
25080
+ error: `exited (code=${code} signal=${signal})`,
25081
+ pid: void 0
25082
+ };
25083
+ log(`[deploy] process exited code=${code} signal=${signal}`);
25084
+ if (code !== 0 && signal !== "SIGTERM") {
25085
+ scheduleRestart(name15, runtime);
25086
+ }
25087
+ });
25088
+ if (old?.process && old.currentPort !== targetPort) {
25089
+ if (old.restartTimer) clearTimeout(old.restartTimer);
25090
+ log(`[deploy] stopping old process on port ${old.currentPort}`);
25091
+ await stopProcess({ child: old.process, port: old.currentPort });
25092
+ }
25093
+ }
25094
+ function setAppRuntime(name15, ac, logs, overrides) {
25095
+ apps.set(name15, {
25096
+ config: ac,
25097
+ status: { name: name15, ...overrides },
25098
+ logs,
25099
+ process: null,
25100
+ currentPort: overrides.port ?? ac.port,
25101
+ startedAt: null,
25102
+ restartCount: 0,
25103
+ restartTimer: void 0
25104
+ });
25105
+ }
25106
+ for (const name15 of Object.keys(config.apps)) {
25107
+ await initApp(name15);
25108
+ }
25109
+ const getPort = (name15) => apps.get(name15)?.currentPort;
25110
+ const gw = createGateway(config, getPort);
25111
+ const managerRouter = createManager(config, apps, {
25112
+ deployApp: async (name15) => {
25113
+ await initApp(name15);
25114
+ },
25115
+ reloadConfig: async () => {
25116
+ throw new Error("reload not supported, restart the deploy process");
25117
+ }
25118
+ });
25119
+ const fullHandler = async (req, ctx) => {
25120
+ const url = new URL(req.url);
25121
+ if (url.pathname.startsWith("/_deploy")) {
25122
+ const stripped = url.pathname.replace("/_deploy", "") || "/";
25123
+ const rewritten = new URL(stripped + url.search, "http://deploy.local");
25124
+ const rewrittenReq = new Request(rewritten, req);
25125
+ return managerRouter.handler()(rewrittenReq, ctx);
25126
+ }
25127
+ return gw.handler(req, ctx);
25128
+ };
25129
+ if (config.ssl) {
25130
+ ensureCertificates(config);
25131
+ }
25132
+ httpServer = serve(fullHandler, {
25133
+ port: config.port,
25134
+ websocket: gw.wsHandler
25135
+ });
25136
+ const portSuffix = config.port !== 80 ? `:${config.port}` : "";
25137
+ return {
25138
+ stop: async () => {
25139
+ for (const [, app] of apps) {
25140
+ if (app.restartTimer) clearTimeout(app.restartTimer);
25141
+ if (app.process) {
25142
+ await stopProcess({ child: app.process, port: app.currentPort });
25143
+ }
25144
+ }
25145
+ httpServer?.stop();
25146
+ },
25147
+ ready: httpServer.ready,
25148
+ url: `http://${config.domain}${portSuffix}`,
25149
+ apps: {
25150
+ list: () => Array.from(apps.values()).map((a) => a.status),
25151
+ status: (name15) => apps.get(name15)?.status,
25152
+ deploy: async (name15) => {
25153
+ await initApp(name15);
25154
+ },
25155
+ restart: async (name15) => {
25156
+ await initApp(name15);
25157
+ },
25158
+ stop: async (name15) => {
25159
+ const app = apps.get(name15);
25160
+ if (app?.restartTimer) clearTimeout(app.restartTimer);
25161
+ if (app?.process) {
25162
+ await stopProcess({ child: app.process, port: app.currentPort });
25163
+ app.process = null;
25164
+ app.status = { ...app.status, status: "stopped", pid: void 0 };
25165
+ }
25166
+ },
25167
+ start: async (name15) => {
25168
+ await initApp(name15);
25169
+ }
25170
+ }
25171
+ };
25172
+ }
25173
+ function ensureCertificates(config) {
25174
+ const { domain, ssl } = config;
25175
+ if (!ssl) return;
25176
+ const certDir = "/etc/weifuwu/ssl";
25177
+ const certPath = path.join(certDir, `${domain}.pem`);
25178
+ const keyPath = path.join(certDir, `${domain}-key.pem`);
25179
+ if (fs.existsSync(certPath) && fs.existsSync(keyPath)) return;
25180
+ if (!fs.existsSync(certDir)) {
25181
+ fs.mkdirSync(certDir, { recursive: true });
25182
+ }
25183
+ const acmeHome = path.join(certDir, ".acme.sh");
25184
+ try {
25185
+ execSync("which acme.sh", { stdio: "pipe" });
25186
+ } catch {
25187
+ execSync(
25188
+ `curl -s https://get.acme.sh | sh -s email=${ssl.email}`,
25189
+ { stdio: "pipe", timeout: 6e4 }
25190
+ );
25191
+ }
25192
+ const subdomains = Object.values(config.apps).filter((a) => a.subdomain).map((a) => `${a.subdomain}.${domain}`).join(",");
25193
+ const allDomains = subdomains ? `${domain},${subdomains}` : domain;
25194
+ const acmeSh = path.join(acmeHome, "acme.sh");
25195
+ const staging = ssl.staging ? " --staging" : "";
25196
+ execSync(
25197
+ `${acmeSh} --issue -d ${allDomains} --standalone${staging} --cert-file ${certPath} --key-file ${keyPath}`,
25198
+ { stdio: "pipe", timeout: 12e4 }
25199
+ );
25200
+ execSync(
25201
+ `${acmeSh} --install-cronjob`,
25202
+ { stdio: "pipe", timeout: 3e4 }
25203
+ );
25204
+ }
24631
25205
  export {
24632
25206
  Router,
24633
25207
  TsxContext,
@@ -24638,7 +25212,9 @@ export {
24638
25212
  cors,
24639
25213
  createSSEManager,
24640
25214
  createWorkflowEngine,
25215
+ defineConfig,
24641
25216
  deleteCookie,
25217
+ deploy,
24642
25218
  generateWorkflow,
24643
25219
  getCookies,
24644
25220
  graphql,
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.7.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",