weifuwu 0.24.2 → 0.25.0

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
@@ -65,7 +65,8 @@ function isBundled() {
65
65
  return true ? true : false;
66
66
  }
67
67
  function isDev() {
68
- return process.env.NODE_ENV === "development";
68
+ const env2 = process.env.NODE_ENV;
69
+ return env2 !== "production" && env2 !== "test";
69
70
  }
70
71
  function isProd() {
71
72
  return process.env.NODE_ENV === "production";
@@ -293,20 +294,20 @@ function serve(handler, options) {
293
294
  process.off("SIGINT", shutdownHandler);
294
295
  shutdownHandler = null;
295
296
  }
296
- return new Promise((resolve14) => {
297
+ return new Promise((resolve16) => {
297
298
  if (!server.listening) {
298
- resolve14();
299
+ resolve16();
299
300
  return;
300
301
  }
301
302
  server.close();
302
303
  server.closeIdleConnections();
303
304
  const timer = setTimeout(() => {
304
305
  server.closeAllConnections();
305
- resolve14();
306
+ resolve16();
306
307
  }, timeoutMs);
307
308
  server.on("close", () => {
308
309
  clearTimeout(timer);
309
- resolve14();
310
+ resolve16();
310
311
  });
311
312
  });
312
313
  },
@@ -466,6 +467,8 @@ var Router = class _Router {
466
467
  _hasWildcard = false;
467
468
  _hub;
468
469
  _wss;
470
+ /** Track which ctx fields have been injected so far (for dependency checking). */
471
+ _ctxFields = /* @__PURE__ */ new Set();
469
472
  get wss() {
470
473
  if (!this._wss) this._wss = new WebSocketServer({ noServer: true });
471
474
  return this._wss;
@@ -489,17 +492,47 @@ var Router = class _Router {
489
492
  node = getOrCreateChild(node, segment, createTrieNode, false);
490
493
  }
491
494
  node.pathMws.push(arg2);
495
+ this._checkMiddlewareMeta(arg2, `${arg1}`);
492
496
  }
493
497
  } else if (typeof arg1 === "function") {
494
498
  this.globalMws.push(arg1);
499
+ this._checkMiddlewareMeta(arg1, "global");
495
500
  } else if (typeof arg1 === "object" && arg1 !== null && "middleware" in arg1 && // eslint-disable-next-line @typescript-eslint/no-explicit-any
496
501
  typeof arg1.middleware === "function" && arg1 instanceof _Router) {
497
502
  const mod = arg1;
498
- this.globalMws.push(mod.middleware());
503
+ const mw = mod.middleware();
504
+ this.globalMws.push(mw);
505
+ this._checkMiddlewareMeta(mw, "global (auto-registered)");
499
506
  this._mountRouter("/", mod);
500
507
  }
501
508
  return this;
502
509
  }
510
+ /**
511
+ * Check a middleware's dependency metadata and emit warnings if
512
+ * required fields haven't been injected yet.
513
+ * Attach __meta to a middleware function:
514
+ *
515
+ * ```ts
516
+ * mw.__meta = { injects: ['sql'], depends: ['session'] }
517
+ * ```
518
+ */
519
+ _checkMiddlewareMeta(mw, location) {
520
+ const meta = mw.__meta ?? mw.middleware?.().__meta;
521
+ if (!meta) return;
522
+ for (const dep of meta.depends) {
523
+ if (!this._ctxFields.has(dep)) {
524
+ console.warn(
525
+ `[weifuwu] Middleware at "${location}" depends on ctx.${dep} but it hasn't been registered yet.
526
+ Register the provider before this middleware:
527
+ app.use(${dep}()) // add before this middleware
528
+ Current ctx fields: [${[...this._ctxFields].join(", ")}]`
529
+ );
530
+ }
531
+ }
532
+ for (const field of meta.injects) {
533
+ this._ctxFields.add(field);
534
+ }
535
+ }
503
536
  // Route registration — returns Router<T> unchanged.
504
537
  // Route-level middleware and handlers get Context<T>.
505
538
  get(path2, ...args) {
@@ -1487,7 +1520,7 @@ function upload(options) {
1487
1520
  }
1488
1521
 
1489
1522
  // rate-limit.ts
1490
- function defaultKey(_req, _ctx) {
1523
+ function defaultKey(_req, _ctx2) {
1491
1524
  const forwarded = _req.headers.get("x-forwarded-for");
1492
1525
  if (forwarded) return forwarded.split(",")[0].trim();
1493
1526
  const realIp = _req.headers.get("x-real-ip");
@@ -1567,6 +1600,7 @@ function rateLimit(options) {
1567
1600
  const res = await next(req, ctx);
1568
1601
  return addRateLimitHeaders(res, max, remaining, reset);
1569
1602
  };
1603
+ mw.__meta = { injects: [], depends: [] };
1570
1604
  mw.close = () => {
1571
1605
  if (interval) clearInterval(interval);
1572
1606
  hits.clear();
@@ -1750,6 +1784,7 @@ function createSSEStream(iterable, opts) {
1750
1784
  }
1751
1785
 
1752
1786
  // test-utils.ts
1787
+ import { WebSocket as WSWebSocket } from "ws";
1753
1788
  var TestResponseImpl = class {
1754
1789
  response;
1755
1790
  constructor(response) {
@@ -1845,9 +1880,22 @@ var TestRequest = class {
1845
1880
  };
1846
1881
  var TestApp = class {
1847
1882
  router;
1883
+ wsServer = null;
1884
+ wsConnections = [];
1848
1885
  constructor() {
1849
1886
  this.router = new Router();
1850
1887
  }
1888
+ /**
1889
+ * Register a WebSocket handler.
1890
+ */
1891
+ ws(path2, handler) {
1892
+ this.router.ws(path2, handler);
1893
+ return this;
1894
+ }
1895
+ /** Get the raw Router (for advanced use). */
1896
+ get _router() {
1897
+ return this.router;
1898
+ }
1851
1899
  /** Add global middleware */
1852
1900
  use(mw) {
1853
1901
  this.router.use(mw);
@@ -1907,6 +1955,195 @@ var TestApp = class {
1907
1955
  handler() {
1908
1956
  return this.router.handler();
1909
1957
  }
1958
+ /** Start building a WebSocket connection to the given path. */
1959
+ wsReq(path2) {
1960
+ return new TestWSRequest(this, path2);
1961
+ }
1962
+ /**
1963
+ * Internal: ensure HTTP server is running for WebSocket connections.
1964
+ * Starts on a random port.
1965
+ */
1966
+ /* @internal */
1967
+ async _ensureServer() {
1968
+ if (this.wsServer) {
1969
+ return `http://localhost:${this.wsServer.port}`;
1970
+ }
1971
+ const wsHandler = this.router.websocketHandler();
1972
+ if (!wsHandler) {
1973
+ throw new Error(
1974
+ "No WebSocket routes registered. Use app.ws(path, handler) before calling wsReq()."
1975
+ );
1976
+ }
1977
+ this.wsServer = serve(this.router.handler(), {
1978
+ websocket: wsHandler
1979
+ });
1980
+ await this.wsServer.ready;
1981
+ return `http://localhost:${this.wsServer.port}`;
1982
+ }
1983
+ /**
1984
+ * Internal: register a WS connection for cleanup.
1985
+ */
1986
+ /* @internal */
1987
+ _trackConnection(conn) {
1988
+ this.wsConnections.push(conn);
1989
+ }
1990
+ /**
1991
+ * Cleanup all WebSocket connections and stop the server.
1992
+ */
1993
+ async close() {
1994
+ for (const conn of this.wsConnections) {
1995
+ try {
1996
+ conn.close();
1997
+ } catch {
1998
+ }
1999
+ }
2000
+ this.wsConnections = [];
2001
+ if (this.wsServer) {
2002
+ this.wsServer.stop();
2003
+ this.wsServer = null;
2004
+ }
2005
+ }
2006
+ };
2007
+ var TestWSRequest = class {
2008
+ app;
2009
+ path;
2010
+ _timeout = 5e3;
2011
+ constructor(app, path2) {
2012
+ this.app = app;
2013
+ this.path = path2;
2014
+ }
2015
+ /** Set the timeout for operations (default: 5000ms). */
2016
+ timeout(ms) {
2017
+ this._timeout = ms;
2018
+ return this;
2019
+ }
2020
+ /**
2021
+ * Connect to the WebSocket endpoint.
2022
+ * Starts a real HTTP server (random port) if not already running.
2023
+ */
2024
+ async connect() {
2025
+ const baseUrl = await this.app._ensureServer();
2026
+ const wsUrl = baseUrl.replace(/^http/, "ws") + this.path;
2027
+ const ws = new WSWebSocket(wsUrl, { handshakeTimeout: this._timeout });
2028
+ return new Promise((resolve16, reject) => {
2029
+ const timer = setTimeout(() => {
2030
+ reject(new Error(`WebSocket connection timed out after ${this._timeout}ms`));
2031
+ ws.close();
2032
+ }, this._timeout);
2033
+ ws.on("open", () => {
2034
+ clearTimeout(timer);
2035
+ const conn = new TestWSConnection(ws, this._timeout);
2036
+ this.app._trackConnection(conn);
2037
+ resolve16(conn);
2038
+ });
2039
+ ws.on("error", (err) => {
2040
+ clearTimeout(timer);
2041
+ reject(new Error(`WebSocket connection error: ${err.message}`));
2042
+ });
2043
+ ws.on("unexpected-response", (_req, res) => {
2044
+ clearTimeout(timer);
2045
+ let body = "";
2046
+ res.on("data", (chunk) => {
2047
+ body += chunk.toString();
2048
+ });
2049
+ res.on("end", () => {
2050
+ reject(new Error(`WebSocket upgrade rejected (${res.statusCode}): ${body.slice(0, 200)}`));
2051
+ });
2052
+ });
2053
+ });
2054
+ }
2055
+ };
2056
+ var TestWSConnection = class {
2057
+ ws;
2058
+ _timeout;
2059
+ messageQueue = [];
2060
+ resolveQueue = [];
2061
+ _closed = false;
2062
+ constructor(ws, timeout = 5e3) {
2063
+ this.ws = ws;
2064
+ this._timeout = timeout;
2065
+ ws.on("message", (data) => {
2066
+ const str = data.toString();
2067
+ if (this.resolveQueue.length > 0) {
2068
+ const resolve16 = this.resolveQueue.shift();
2069
+ resolve16(str);
2070
+ } else {
2071
+ this.messageQueue.push(str);
2072
+ }
2073
+ });
2074
+ ws.on("close", () => {
2075
+ this._closed = true;
2076
+ for (const _r of this.resolveQueue) {
2077
+ }
2078
+ });
2079
+ }
2080
+ /** Send a text message. */
2081
+ send(data) {
2082
+ this.ws.send(data);
2083
+ }
2084
+ /** Send a JSON message. */
2085
+ json(data) {
2086
+ this.ws.send(JSON.stringify(data));
2087
+ }
2088
+ /**
2089
+ * Wait for the next message. Returns the raw text.
2090
+ * Throws on timeout or if the connection is closed.
2091
+ */
2092
+ async receive(timeout) {
2093
+ if (this.messageQueue.length > 0) {
2094
+ return this.messageQueue.shift();
2095
+ }
2096
+ if (this._closed) {
2097
+ throw new Error("WebSocket connection closed");
2098
+ }
2099
+ return new Promise((resolve16, reject) => {
2100
+ const timer = setTimeout(() => {
2101
+ const idx = this.resolveQueue.indexOf(resolve16);
2102
+ if (idx !== -1) this.resolveQueue.splice(idx, 1);
2103
+ reject(new Error(`WebSocket receive timed out after ${timeout ?? this._timeout}ms`));
2104
+ }, timeout ?? this._timeout);
2105
+ this.resolveQueue.push((msg) => {
2106
+ clearTimeout(timer);
2107
+ resolve16(msg);
2108
+ });
2109
+ });
2110
+ }
2111
+ /** Wait for the next message and parse as JSON. */
2112
+ async receiveJson() {
2113
+ const msg = await this.receive();
2114
+ return JSON.parse(msg);
2115
+ }
2116
+ /**
2117
+ * Assert that no message is received within the given silence period.
2118
+ * Useful for verifying that something did NOT happen.
2119
+ */
2120
+ async expectSilent(ms) {
2121
+ return new Promise((resolve16, reject) => {
2122
+ if (this.messageQueue.length > 0) {
2123
+ reject(new Error(`Expected silence but got message: ${this.messageQueue[0].slice(0, 100)}`));
2124
+ return;
2125
+ }
2126
+ const timer = setTimeout(() => resolve16(), ms);
2127
+ const origPush = this.resolveQueue.push.bind(this.resolveQueue);
2128
+ this.resolveQueue.push = (_fn) => {
2129
+ clearTimeout(timer);
2130
+ reject(new Error("Expected silence but received a message"));
2131
+ return 0;
2132
+ };
2133
+ setTimeout(() => {
2134
+ this.resolveQueue.push = origPush;
2135
+ }, ms + 10).unref();
2136
+ });
2137
+ }
2138
+ /** Close the connection. */
2139
+ close() {
2140
+ this._closed = true;
2141
+ this.ws.close();
2142
+ }
2143
+ /** Whether the connection is closed. */
2144
+ get closed() {
2145
+ return this._closed;
2146
+ }
1910
2147
  };
1911
2148
  function testApp() {
1912
2149
  return new TestApp();
@@ -2506,6 +2743,7 @@ function aiProvider(options) {
2506
2743
  ctx.ai = provider;
2507
2744
  return next(req, ctx);
2508
2745
  };
2746
+ mw.__meta = { injects: ["ai"], depends: [] };
2509
2747
  return Object.assign(mw, provider);
2510
2748
  }
2511
2749
 
@@ -3115,6 +3353,7 @@ function postgres(opts) {
3115
3353
  ctx.sql = sql2;
3116
3354
  return next(req, ctx);
3117
3355
  });
3356
+ mw.__meta = { injects: ["sql"], depends: [] };
3118
3357
  mw.sql = sql2;
3119
3358
  mw.table = ((tableOrSchema, builders) => {
3120
3359
  if (typeof tableOrSchema === "string") {
@@ -3191,7 +3430,7 @@ var PgModule = class {
3191
3430
  };
3192
3431
 
3193
3432
  // user/client.ts
3194
- import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
3433
+ import { randomBytes, scryptSync, timingSafeEqual, createHash } from "node:crypto";
3195
3434
  import jwt2 from "jsonwebtoken";
3196
3435
  import { z as z2 } from "zod";
3197
3436
 
@@ -3317,7 +3556,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3317
3556
  { status: 400, headers: { "Content-Type": "text/html; charset=utf-8" } }
3318
3557
  );
3319
3558
  }
3320
- async function authorizeHandler(req, _ctx) {
3559
+ async function authorizeHandler(req, _ctx2) {
3321
3560
  const url = new URL(req.url);
3322
3561
  const clientId = url.searchParams.get("client_id") || "";
3323
3562
  const redirectUri = url.searchParams.get("redirect_uri") || "";
@@ -3738,6 +3977,10 @@ var LoginSchema = z2.object({
3738
3977
  email: z2.string().email(),
3739
3978
  password: z2.string().min(1)
3740
3979
  });
3980
+ var CreateApiKeySchema = z2.object({
3981
+ name: z2.string().min(1),
3982
+ scopes: z2.array(z2.string()).optional()
3983
+ });
3741
3984
  function escapeIdent2(s) {
3742
3985
  return `"${s.replace(/"/g, '""')}"`;
3743
3986
  }
@@ -3784,6 +4027,7 @@ function user(options) {
3784
4027
  const secret = options.jwtSecret;
3785
4028
  const expiresIn = options.expiresIn ?? "24h";
3786
4029
  const oauth2Enabled = options.oauth2?.server ?? false;
4030
+ const apiKeysEnabled = options.apiKeys ?? false;
3787
4031
  const base = hasDb ? new PgModule(pg) : null;
3788
4032
  const users = hasDb ? pg.table(table, {
3789
4033
  id: serial("id").primaryKey(),
@@ -3821,6 +4065,28 @@ function user(options) {
3821
4065
  ON "_auth_providers"(user_id)
3822
4066
  `);
3823
4067
  }
4068
+ if (apiKeysEnabled) {
4069
+ await _pg.sql.unsafe(`
4070
+ CREATE TABLE IF NOT EXISTS "_api_keys" (
4071
+ id SERIAL PRIMARY KEY,
4072
+ user_id INTEGER NOT NULL REFERENCES ${escapeIdent2(table)}(id) ON DELETE CASCADE,
4073
+ name TEXT NOT NULL,
4074
+ key_prefix TEXT NOT NULL,
4075
+ key_hash TEXT NOT NULL,
4076
+ scopes TEXT[] DEFAULT '{}',
4077
+ last_used_at TIMESTAMPTZ,
4078
+ expires_at TIMESTAMPTZ,
4079
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4080
+ revoked BOOLEAN DEFAULT false
4081
+ )
4082
+ `);
4083
+ await _pg.sql.unsafe(`
4084
+ CREATE INDEX IF NOT EXISTS "_api_keys_user_idx" ON "_api_keys"(user_id)
4085
+ `);
4086
+ await _pg.sql.unsafe(`
4087
+ CREATE UNIQUE INDEX IF NOT EXISTS "_api_keys_hash_idx" ON "_api_keys"(key_hash)
4088
+ `);
4089
+ }
3824
4090
  if (!oauth2Enabled) return;
3825
4091
  const clients3 = _pg.table("_oauth2_clients", {
3826
4092
  id: serial("id").primaryKey(),
@@ -3921,8 +4187,79 @@ function user(options) {
3921
4187
  return null;
3922
4188
  }
3923
4189
  }
4190
+ function hashApiKey(key) {
4191
+ return createHash("sha256").update(key).digest("hex");
4192
+ }
4193
+ function generateApiKey() {
4194
+ const random = randomBytes(32).toString("hex");
4195
+ return `sk_live_${random}`;
4196
+ }
4197
+ async function createApiKey(userId2, name, scopes) {
4198
+ if (!hasDb) throw new Error("user(): pg required for API key management");
4199
+ const key = generateApiKey();
4200
+ const keyHash = hashApiKey(key);
4201
+ const prefix = key.slice(0, 12) + "..." + key.slice(-4);
4202
+ const [row] = await _pg.sql.unsafe(
4203
+ `INSERT INTO "_api_keys" (user_id, name, key_prefix, key_hash, scopes)
4204
+ VALUES ($1, $2, $3, $4, $5) RETURNING id`,
4205
+ [userId2, name, prefix, keyHash, scopes ?? []]
4206
+ );
4207
+ return { id: row.id, key };
4208
+ }
4209
+ async function listApiKeys(userId2) {
4210
+ if (!hasDb) return [];
4211
+ const rows = await _pg.sql.unsafe(
4212
+ `SELECT id, name, key_prefix, scopes, last_used_at, created_at, revoked
4213
+ FROM "_api_keys" WHERE user_id = $1 ORDER BY created_at DESC`,
4214
+ [userId2]
4215
+ );
4216
+ return rows.map((r2) => ({
4217
+ id: r2.id,
4218
+ name: r2.name,
4219
+ prefix: r2.key_prefix,
4220
+ scopes: Array.isArray(r2.scopes) ? r2.scopes : [],
4221
+ last_used_at: r2.last_used_at ? new Date(r2.last_used_at).toISOString() : null,
4222
+ created_at: new Date(r2.created_at).toISOString(),
4223
+ revoked: !!r2.revoked
4224
+ }));
4225
+ }
4226
+ async function revokeApiKey(userId2, keyId) {
4227
+ if (!hasDb) throw new Error("user(): pg required for API key management");
4228
+ await _pg.sql.unsafe(`UPDATE "_api_keys" SET revoked = true WHERE id = $1 AND user_id = $2`, [
4229
+ keyId,
4230
+ userId2
4231
+ ]);
4232
+ }
4233
+ async function verifyApiKey(key) {
4234
+ if (!hasDb || !apiKeysEnabled) return null;
4235
+ const keyHash = hashApiKey(key);
4236
+ const [row] = await _pg.sql.unsafe(
4237
+ `SELECT id, user_id, scopes, revoked, expires_at
4238
+ FROM "_api_keys" WHERE key_hash = $1 LIMIT 1`,
4239
+ [keyHash]
4240
+ );
4241
+ if (!row) return null;
4242
+ if (row.revoked) return null;
4243
+ if (row.expires_at && new Date(row.expires_at) < /* @__PURE__ */ new Date()) return null;
4244
+ await _pg.sql.unsafe(
4245
+ `UPDATE "_api_keys" SET last_used_at = NOW() WHERE id = $1 AND last_used_at IS NULL OR last_used_at < NOW() - interval '1 minute'`,
4246
+ [row.id]
4247
+ ).catch(() => {
4248
+ });
4249
+ return {
4250
+ userId: row.user_id,
4251
+ scopes: Array.isArray(row.scopes) ? row.scopes : []
4252
+ };
4253
+ }
4254
+ async function tryApiKeyAuth(token) {
4255
+ if (!apiKeysEnabled || !hasDb) return null;
4256
+ if (!token.startsWith("sk_")) return null;
4257
+ return verifyApiKey(token);
4258
+ }
3924
4259
  const headerName = options.header ?? "Authorization";
3925
4260
  async function resolveUser(req, ctx) {
4261
+ const _ctx2 = ctx;
4262
+ if (_ctx2.user) return _ctx2.user;
3926
4263
  const s = ctx;
3927
4264
  const sessionUserId = s.session?.userId;
3928
4265
  if (sessionUserId !== void 0 && sessionUserId !== null) {
@@ -3998,6 +4335,17 @@ function user(options) {
3998
4335
  return null;
3999
4336
  }
4000
4337
  }
4338
+ if (token.startsWith("sk_")) {
4339
+ const result = await tryApiKeyAuth(token);
4340
+ if (result) {
4341
+ if (hasDb) {
4342
+ const row = await findById(result.userId);
4343
+ if (row) return { ...stripPassword(row), _apiKeyScopes: result.scopes };
4344
+ }
4345
+ return { id: result.userId, _apiKeyScopes: result.scopes };
4346
+ }
4347
+ return null;
4348
+ }
4001
4349
  if (secret && hasDb) {
4002
4350
  try {
4003
4351
  const payload = jwt2.verify(token, secret);
@@ -4083,6 +4431,33 @@ function user(options) {
4083
4431
  }
4084
4432
  });
4085
4433
  }
4434
+ if (apiKeysEnabled) {
4435
+ r.get("/api-keys", middleware(), async (_req, ctx) => {
4436
+ const keys = await listApiKeys(ctx.user.id);
4437
+ return Response.json(keys);
4438
+ });
4439
+ r.post("/api-keys", middleware(), async (req, ctx) => {
4440
+ try {
4441
+ const body = await parseBody2(req);
4442
+ const { name, scopes } = CreateApiKeySchema.parse(body);
4443
+ const result = await createApiKey(ctx.user.id, name, scopes);
4444
+ return Response.json(result, { status: 201 });
4445
+ } catch (err) {
4446
+ if (err instanceof z2.ZodError) {
4447
+ return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
4448
+ }
4449
+ return Response.json({ error: err.message }, { status: 500 });
4450
+ }
4451
+ });
4452
+ r.delete("/api-keys/:id", middleware(), async (req, ctx) => {
4453
+ const keyId = parseInt(ctx.params.id, 10);
4454
+ if (isNaN(keyId)) {
4455
+ return Response.json({ error: "Invalid key ID" }, { status: 400 });
4456
+ }
4457
+ await revokeApiKey(ctx.user.id, keyId);
4458
+ return Response.json({ ok: true });
4459
+ });
4460
+ }
4086
4461
  if (oauth2) {
4087
4462
  r.get("/oauth/authorize", (req, ctx) => oauth2.authorizeHandler(req, ctx));
4088
4463
  r.post("/oauth/consent", (req) => oauth2.consentHandler(req));
@@ -4127,6 +4502,21 @@ function user(options) {
4127
4502
  mod.revokeClient = oauth2 ? (clientId) => oauth2.revokeClient(clientId) : async () => {
4128
4503
  throw new Error("OAuth2 server is not enabled");
4129
4504
  };
4505
+ mod.createApiKey = hasDb && apiKeysEnabled ? createApiKey : async () => {
4506
+ throw new Error(
4507
+ "API key management is not enabled. Pass apiKeys: true in user() options."
4508
+ );
4509
+ };
4510
+ mod.listApiKeys = hasDb && apiKeysEnabled ? listApiKeys : async () => {
4511
+ throw new Error(
4512
+ "API key management is not enabled. Pass apiKeys: true in user() options."
4513
+ );
4514
+ };
4515
+ mod.revokeApiKey = hasDb && apiKeysEnabled ? revokeApiKey : async () => {
4516
+ throw new Error(
4517
+ "API key management is not enabled. Pass apiKeys: true in user() options."
4518
+ );
4519
+ };
4130
4520
  mod.close = hasDb ? () => base.close() : async () => {
4131
4521
  };
4132
4522
  return mod;
@@ -4143,6 +4533,7 @@ function redis(opts) {
4143
4533
  ctx.redis = client;
4144
4534
  return next(req, ctx);
4145
4535
  });
4536
+ mw.__meta = { injects: ["redis"], depends: [] };
4146
4537
  mw.redis = client;
4147
4538
  mw.close = () => client.quit();
4148
4539
  return mw;
@@ -5590,23 +5981,23 @@ function buildGraphQLHandler(sql2) {
5590
5981
  });
5591
5982
  return Response.json(result, { status: result.errors ? 400 : 200 });
5592
5983
  });
5593
- r.get("/", async (req, _ctx) => {
5984
+ r.get("/", async (req, _ctx2) => {
5594
5985
  const url = new URL(req.url);
5595
5986
  if (url.searchParams.has("query")) {
5596
- return handleGET(req, _ctx);
5987
+ return handleGET(req, _ctx2);
5597
5988
  }
5598
5989
  return new Response("GraphQL endpoint. Send POST /graphql with { query, variables }", {
5599
5990
  status: 200,
5600
5991
  headers: { "Content-Type": "text/plain" }
5601
5992
  });
5602
5993
  });
5603
- async function handleGET(req, _ctx) {
5994
+ async function handleGET(req, _ctx2) {
5604
5995
  const tables = await sql2`
5605
5996
  SELECT * FROM "_user_tables"
5606
- WHERE tenant_id = ${_ctx.tenant.id}
5997
+ WHERE tenant_id = ${_ctx2.tenant.id}
5607
5998
  ORDER BY created_at ASC
5608
5999
  `;
5609
- const buildCtx = { sql: sql2, tenantId: _ctx.tenant.id, tables, typeCache: /* @__PURE__ */ new Map() };
6000
+ const buildCtx = { sql: sql2, tenantId: _ctx2.tenant.id, tables, typeCache: /* @__PURE__ */ new Map() };
5610
6001
  const schema = new GraphQLSchema({
5611
6002
  query: new GraphQLObjectType({
5612
6003
  name: "Query",
@@ -5626,7 +6017,7 @@ function buildGraphQLHandler(sql2) {
5626
6017
  source: query,
5627
6018
  variableValues: variables,
5628
6019
  operationName: url.searchParams.get("operationName") || void 0,
5629
- contextValue: _ctx
6020
+ contextValue: _ctx2
5630
6021
  });
5631
6022
  return Response.json(result, { status: result.errors ? 400 : 200 });
5632
6023
  }
@@ -6653,14 +7044,14 @@ function forkApp(opts) {
6653
7044
  return { child, port: opts.port };
6654
7045
  }
6655
7046
  function stopProcess(mp, timeout = 1e4) {
6656
- return new Promise((resolve14) => {
7047
+ return new Promise((resolve16) => {
6657
7048
  const timer = setTimeout(() => {
6658
7049
  mp.child.kill("SIGKILL");
6659
- resolve14();
7050
+ resolve16();
6660
7051
  }, timeout);
6661
7052
  mp.child.on("exit", () => {
6662
7053
  clearTimeout(timer);
6663
- resolve14();
7054
+ resolve16();
6664
7055
  });
6665
7056
  mp.child.kill("SIGTERM");
6666
7057
  });
@@ -6992,22 +7383,168 @@ import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
6992
7383
 
6993
7384
  // ssr.ts
6994
7385
  import { createElement as createElement3 } from "react";
6995
- import { createHash as createHash3 } from "node:crypto";
6996
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
7386
+ import { createHash as createHash5 } from "node:crypto";
7387
+ import { existsSync as existsSync6, readdirSync } from "node:fs";
6997
7388
  import { readdir, stat } from "node:fs/promises";
6998
- import { dirname as dirname3, join as join5, resolve as resolve6, relative as relative2 } from "node:path";
7389
+ import { dirname as dirname4, join as join5, resolve as resolve8, relative as relative3 } from "node:path";
6999
7390
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
7000
7391
 
7001
7392
  // compile.ts
7002
- import * as esbuild from "esbuild";
7003
- import { existsSync, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
7004
- import { join as join2, resolve as resolve3, dirname } from "node:path";
7393
+ import * as esbuild2 from "esbuild";
7394
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3 } from "node:fs";
7395
+ import { join as join2, resolve as resolve4, dirname as dirname2 } from "node:path";
7005
7396
  import { pathToFileURL } from "node:url";
7006
- import { createHash } from "node:crypto";
7397
+ import { createHash as createHash2 } from "node:crypto";
7398
+ import { createRequire as createRequire2 } from "node:module";
7399
+
7400
+ // server-registry.ts
7401
+ import * as esbuild from "esbuild";
7402
+ import { existsSync, readFileSync as readFileSync2, statSync } from "node:fs";
7403
+ import { resolve as resolve3, dirname } from "node:path";
7007
7404
  import vm from "node:vm";
7008
7405
  import { createRequire } from "node:module";
7009
- var _cjsRequire = createRequire(import.meta.url);
7010
7406
  var _userRequire = null;
7407
+ function getUserRequire() {
7408
+ if (!_userRequire) {
7409
+ try {
7410
+ _userRequire = createRequire(resolve3(process.cwd(), "package.json"));
7411
+ } catch {
7412
+ _userRequire = createRequire(import.meta.url);
7413
+ }
7414
+ }
7415
+ return _userRequire;
7416
+ }
7417
+ var _alias = null;
7418
+ function resolveAliases() {
7419
+ if (_alias) return _alias;
7420
+ const configFiles = ["tsconfig.json", "jsconfig.json"];
7421
+ for (const file of configFiles) {
7422
+ const p = resolve3(file);
7423
+ if (existsSync(p)) {
7424
+ try {
7425
+ const config = JSON.parse(readFileSync2(p, "utf-8"));
7426
+ const paths = config.compilerOptions?.paths;
7427
+ if (paths) {
7428
+ const alias = {};
7429
+ for (const [key, values] of Object.entries(paths)) {
7430
+ const cleanKey = key.replace("/*", "");
7431
+ const val = values[0]?.replace("/*", "");
7432
+ if (val) alias[cleanKey] = resolve3(dirname(p), val);
7433
+ }
7434
+ _alias = alias;
7435
+ return alias;
7436
+ }
7437
+ } catch {
7438
+ }
7439
+ }
7440
+ }
7441
+ _alias = {};
7442
+ return {};
7443
+ }
7444
+ function applyAlias(id2, _moduleDir) {
7445
+ const aliases = resolveAliases();
7446
+ for (const [prefix, target] of Object.entries(aliases)) {
7447
+ if (id2.startsWith(prefix)) {
7448
+ const rest = id2.slice(prefix.length);
7449
+ return target + rest;
7450
+ }
7451
+ }
7452
+ return null;
7453
+ }
7454
+ var exts = [".tsx", ".ts", ".jsx", ".js"];
7455
+ function tryResolve(base) {
7456
+ if (existsSync(base)) {
7457
+ const stat3 = statSync(base);
7458
+ if (stat3.isFile()) return base;
7459
+ if (stat3.isDirectory()) {
7460
+ for (const ext of exts) {
7461
+ const p = resolve3(base, `index${ext}`);
7462
+ if (existsSync(p)) return p;
7463
+ }
7464
+ return null;
7465
+ }
7466
+ }
7467
+ for (const ext of exts) {
7468
+ const p = base + ext;
7469
+ if (existsSync(p)) return p;
7470
+ }
7471
+ return null;
7472
+ }
7473
+ var registry = /* @__PURE__ */ new Map();
7474
+ var _ctx = vm.createContext(Object.create(globalThis));
7475
+ function transformToCjs(absPath, source) {
7476
+ const isTsx = absPath.endsWith(".tsx");
7477
+ const result = esbuild.transformSync(source, {
7478
+ loader: isTsx ? "tsx" : "ts",
7479
+ format: "cjs",
7480
+ jsx: isTsx ? "automatic" : void 0,
7481
+ jsxImportSource: isTsx ? "react" : void 0,
7482
+ sourcemap: false
7483
+ });
7484
+ return result.code;
7485
+ }
7486
+ function makeRequire(modulePath) {
7487
+ const moduleDir = dirname(modulePath);
7488
+ return (id2) => {
7489
+ if (id2.startsWith(".")) {
7490
+ const base = resolve3(moduleDir, id2);
7491
+ const file = tryResolve(base);
7492
+ if (!file) {
7493
+ throw new Error(
7494
+ `[server-registry] Cannot resolve '${id2}' from '${modulePath}'. Tried: ${[base, ...exts.map((e) => base + e)].filter((p) => !p.endsWith(base)).join(", ")}`
7495
+ );
7496
+ }
7497
+ return getServerModule(file);
7498
+ }
7499
+ const aliased = applyAlias(id2, moduleDir);
7500
+ if (aliased) {
7501
+ const file = tryResolve(aliased);
7502
+ if (file) return getServerModule(file);
7503
+ }
7504
+ return getUserRequire()(id2);
7505
+ };
7506
+ }
7507
+ function evaluateModule(code, modulePath) {
7508
+ const mod = { exports: {} };
7509
+ const require2 = makeRequire(modulePath);
7510
+ const _dirname = dirname(modulePath);
7511
+ const _filename = modulePath;
7512
+ const wrapped = `(function(require,module,exports,__dirname,__filename){
7513
+ ${code}
7514
+ })`;
7515
+ try {
7516
+ new vm.Script(wrapped).runInContext(_ctx)(require2, mod, mod.exports, _dirname, _filename);
7517
+ } catch (err) {
7518
+ const msg = err instanceof Error ? err.message : String(err);
7519
+ const cause = err instanceof Error ? err : void 0;
7520
+ throw new Error(
7521
+ `[server-registry] Error evaluating '${modulePath}': ${msg}`,
7522
+ cause ? { cause } : void 0
7523
+ );
7524
+ }
7525
+ return mod.exports;
7526
+ }
7527
+ function getServerModule(absPath) {
7528
+ const normalized = resolve3(absPath);
7529
+ if (registry.has(normalized)) return registry.get(normalized).exports;
7530
+ const source = readFileSync2(normalized, "utf-8");
7531
+ const code = transformToCjs(normalized, source);
7532
+ const exports = evaluateModule(code, normalized);
7533
+ registry.set(normalized, { exports });
7534
+ return exports;
7535
+ }
7536
+ function clearServerModule(absPath) {
7537
+ if (absPath) {
7538
+ const normalized = resolve3(absPath);
7539
+ registry.delete(normalized);
7540
+ } else {
7541
+ registry.clear();
7542
+ _alias = null;
7543
+ }
7544
+ }
7545
+
7546
+ // compile.ts
7547
+ var _userRequire2 = null;
7011
7548
  var OUT_DIR = ".weifuwu/ssr";
7012
7549
  var cache = /* @__PURE__ */ new Map();
7013
7550
  var externals = [
@@ -7020,48 +7557,49 @@ var externals = [
7020
7557
  "@graphql-tools/schema",
7021
7558
  "ai"
7022
7559
  ];
7023
- var _alias = null;
7024
- function resolveAliases() {
7025
- if (_alias) return _alias;
7560
+ var _alias2 = null;
7561
+ function resolveAliases2() {
7562
+ if (_alias2) return _alias2;
7026
7563
  const configFiles = ["tsconfig.json", "jsconfig.json"];
7027
7564
  for (const file of configFiles) {
7028
- const p = resolve3(file);
7029
- if (existsSync(p)) {
7565
+ const p = resolve4(file);
7566
+ if (existsSync2(p)) {
7030
7567
  try {
7031
- const config = JSON.parse(readFileSync2(p, "utf-8"));
7568
+ const config = JSON.parse(readFileSync3(p, "utf-8"));
7032
7569
  const paths = config.compilerOptions?.paths;
7033
7570
  if (paths) {
7034
7571
  const alias = {};
7035
7572
  for (const [key, values] of Object.entries(paths)) {
7036
7573
  const cleanKey = key.replace("/*", "");
7037
7574
  const val = values[0]?.replace("/*", "");
7038
- if (val) alias[cleanKey] = resolve3(dirname(p), val);
7575
+ if (val) alias[cleanKey] = resolve4(dirname2(p), val);
7039
7576
  }
7040
- _alias = alias;
7577
+ _alias2 = alias;
7041
7578
  return alias;
7042
7579
  }
7043
7580
  } catch {
7044
7581
  }
7045
7582
  }
7046
7583
  }
7047
- _alias = {};
7584
+ _alias2 = {};
7048
7585
  return {};
7049
7586
  }
7050
7587
  function id(s) {
7051
- return createHash("md5").update(s).digest("hex").slice(0, 8);
7588
+ return createHash2("md5").update(s).digest("hex").slice(0, 8);
7052
7589
  }
7053
7590
  function clearCompileCache() {
7054
7591
  cache.clear();
7055
- _alias = null;
7592
+ clearServerModule();
7593
+ _alias2 = null;
7056
7594
  }
7057
7595
  async function compileTsx(path2) {
7058
- const absPath = resolve3(path2);
7596
+ const absPath = resolve4(path2);
7059
7597
  if (cache.has(absPath)) return cache.get(absPath);
7060
- const outDir = resolve3(OUT_DIR);
7598
+ const outDir = resolve4(OUT_DIR);
7061
7599
  mkdirSync(outDir, { recursive: true });
7062
7600
  const hash = id(absPath);
7063
7601
  const outPath = join2(outDir, hash + ".js");
7064
- await esbuild.build({
7602
+ await esbuild2.build({
7065
7603
  entryPoints: { [hash]: absPath },
7066
7604
  outdir: outDir,
7067
7605
  format: "esm",
@@ -7070,7 +7608,7 @@ async function compileTsx(path2) {
7070
7608
  jsxImportSource: "react",
7071
7609
  bundle: true,
7072
7610
  external: externals,
7073
- alias: resolveAliases(),
7611
+ alias: resolveAliases2(),
7074
7612
  write: true,
7075
7613
  allowOverwrite: true
7076
7614
  });
@@ -7078,42 +7616,20 @@ async function compileTsx(path2) {
7078
7616
  cache.set(absPath, mod);
7079
7617
  return mod;
7080
7618
  }
7081
- function loadSSRModule(code) {
7082
- const ctx = vm.createContext(Object.create(globalThis));
7083
- const mod = { exports: {} };
7084
- ctx.require = (name) => _cjsRequire(name);
7085
- ctx.module = mod;
7086
- ctx.exports = mod.exports;
7087
- new vm.Script(code).runInContext(ctx);
7088
- return mod.exports;
7089
- }
7090
- async function compileTsxDev(path2) {
7091
- const absPath = resolve3(path2);
7092
- if (cache.has(absPath)) return cache.get(absPath);
7093
- const result = await esbuild.build({
7094
- entryPoints: { [id(absPath)]: absPath },
7095
- format: "cjs",
7096
- platform: "node",
7097
- jsx: "automatic",
7098
- jsxImportSource: "react",
7099
- bundle: true,
7100
- external: externals,
7101
- alias: resolveAliases(),
7102
- write: false
7103
- });
7104
- const code = new TextDecoder().decode(result.outputFiles[0].contents);
7105
- const mod = loadSSRModule(code);
7619
+ function compileTsxDev(path2) {
7620
+ const absPath = resolve4(path2);
7621
+ const mod = getServerModule(absPath);
7106
7622
  cache.set(absPath, mod);
7107
7623
  return mod;
7108
7624
  }
7109
7625
  function compile(path2) {
7110
- return isDev() ? compileTsxDev(path2) : compileTsx(path2);
7626
+ return isDev() ? Promise.resolve(compileTsxDev(path2)) : compileTsx(path2);
7111
7627
  }
7112
7628
  var vendorBundle = null;
7113
7629
  var vendorHash = "";
7114
7630
  async function compileVendorBundle() {
7115
7631
  if (vendorBundle) return vendorBundle;
7116
- if (!_userRequire) _userRequire = createRequire(join2(process.cwd(), "package.json"));
7632
+ if (!_userRequire2) _userRequire2 = createRequire2(join2(process.cwd(), "package.json"));
7117
7633
  const modules = {
7118
7634
  react: [],
7119
7635
  "react-dom": ["react"],
@@ -7121,13 +7637,13 @@ async function compileVendorBundle() {
7121
7637
  "react/jsx-runtime": ["react"]
7122
7638
  };
7123
7639
  for (const request of Object.keys(modules)) {
7124
- const mod = _userRequire(request);
7640
+ const mod = _userRequire2(request);
7125
7641
  const keys = Object.keys(mod).filter((k) => !k.startsWith("_") && k !== "default");
7126
7642
  modules[request] = keys;
7127
7643
  }
7128
7644
  const baseDir = import.meta.dirname ?? __dirname;
7129
- const reactAbsPath = isBundled() ? resolve3(baseDir, "react.js") : resolve3(baseDir, "react.ts");
7130
- const reactSrc = readFileSync2(reactAbsPath, "utf-8");
7645
+ const reactAbsPath = isBundled() ? resolve4(baseDir, "react.js") : resolve4(baseDir, "react.ts");
7646
+ const reactSrc = readFileSync3(reactAbsPath, "utf-8");
7131
7647
  const wfwKeys = [];
7132
7648
  if (reactAbsPath.endsWith(".ts")) {
7133
7649
  for (const line of reactSrc.split("\n")) {
@@ -7158,7 +7674,7 @@ async function compileVendorBundle() {
7158
7674
  const uidWfw = wfwKeys.filter((k) => !used.has(k) && used.add(k));
7159
7675
  if (uidWfw.length > 0)
7160
7676
  stmts.push(`export { ${uidWfw.join(", ")} } from ${JSON.stringify(reactAbsPath)};`);
7161
- const result = await esbuild.build({
7677
+ const result = await esbuild2.build({
7162
7678
  stdin: { contents: stmts.join("\n"), resolveDir: process.cwd() },
7163
7679
  format: "esm",
7164
7680
  bundle: true,
@@ -7170,95 +7686,6 @@ async function compileVendorBundle() {
7170
7686
  vendorHash = Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
7171
7687
  return vendorBundle;
7172
7688
  }
7173
- async function compileBrowser(path2, outDir) {
7174
- const absPath = resolve3(path2);
7175
- const h = id(absPath);
7176
- outDir = outDir ?? resolve3(OUT_DIR);
7177
- const outPath = join2(outDir, h + ".js");
7178
- if (!isDev() && existsSync(outPath)) return h;
7179
- mkdirSync(outDir, { recursive: true });
7180
- const wfwDir = resolve3(import.meta.dirname ?? __dirname);
7181
- const plugin = {
7182
- name: "wfw-external",
7183
- setup(build2) {
7184
- build2.onResolve({ filter: /./ }, (args) => {
7185
- if (args.kind === "entry-point") return;
7186
- const abs = args.path.startsWith(".") ? join2(args.resolveDir, args.path) : args.path;
7187
- if (abs.startsWith(wfwDir) && !abs.includes("node_modules")) {
7188
- const rel = abs.slice(wfwDir.length + 1);
7189
- if (rel.includes("/")) return;
7190
- return { path: "weifuwu/react", external: true };
7191
- }
7192
- });
7193
- }
7194
- };
7195
- await esbuild.build({
7196
- entryPoints: { [h]: absPath },
7197
- outdir: outDir,
7198
- format: "esm",
7199
- platform: "browser",
7200
- jsx: "automatic",
7201
- jsxImportSource: "react",
7202
- bundle: true,
7203
- external: [
7204
- "react",
7205
- "react-dom",
7206
- "react-dom/client",
7207
- "react/jsx-runtime",
7208
- "weifuwu",
7209
- "weifuwu/react"
7210
- ],
7211
- plugins: [plugin],
7212
- write: true,
7213
- allowOverwrite: true
7214
- });
7215
- return h;
7216
- }
7217
- async function compileHotComponent(path2) {
7218
- const absPath = resolve3(path2);
7219
- const h = id(absPath);
7220
- const stdin = `import C from ${JSON.stringify(absPath)};
7221
- (window.__WFW_REFRESH||function(){})(C)`;
7222
- const wfwDir = resolve3(import.meta.dirname ?? __dirname);
7223
- const plugin = {
7224
- name: "wfw-external",
7225
- setup(build2) {
7226
- build2.onResolve({ filter: /./ }, (args) => {
7227
- if (args.kind === "entry-point") return;
7228
- const abs = args.path.startsWith(".") ? join2(args.resolveDir, args.path) : args.path;
7229
- if (abs.startsWith(wfwDir) && !abs.includes("node_modules")) {
7230
- const rel = abs.slice(wfwDir.length + 1);
7231
- if (rel.includes("/")) return;
7232
- return { path: "weifuwu/react", external: true };
7233
- }
7234
- });
7235
- }
7236
- };
7237
- const result = await esbuild.build({
7238
- stdin: { contents: stdin, loader: "tsx", resolveDir: dirname(absPath) },
7239
- format: "esm",
7240
- platform: "browser",
7241
- jsx: "automatic",
7242
- jsxImportSource: "react",
7243
- bundle: true,
7244
- external: [
7245
- "react",
7246
- "react-dom",
7247
- "react-dom/client",
7248
- "react/jsx-runtime",
7249
- "weifuwu",
7250
- "weifuwu/react"
7251
- ],
7252
- plugins: [plugin],
7253
- write: false
7254
- });
7255
- let code = new TextDecoder().decode(result.outputFiles[0].contents);
7256
- if (code.includes("__require") && (code.includes('"react"') || code.includes("'react'"))) {
7257
- code = `import * as __r from 'react';
7258
- ` + code.replace(/__require\(["']react["']\)/g, "__r");
7259
- }
7260
- return { hash: h, code };
7261
- }
7262
7689
 
7263
7690
  // stream.ts
7264
7691
  import { TextDecoder as TextDecoder2, TextEncoder as TextEncoder2 } from "node:util";
@@ -7276,6 +7703,8 @@ function getPublicEnv2() {
7276
7703
  function buildHeadPayload(opts) {
7277
7704
  const { ctx, base, tailwind } = opts;
7278
7705
  let result = "";
7706
+ result += `<script>window.__wfw={_cache:{},_k:function(u){return u.split('?')[0]},h:async function(u){var k=this._k(u);if(this._cache[k])return this._cache[k];var m=await import(u);this._cache[k]=m;return m},_update:function(u,mod){var k=this._k(u);this._cache[k]=mod}}</script>
7707
+ `;
7279
7708
  const vUrl = `${base}/__wfw/v/bundle?h=${vendorHash}`;
7280
7709
  result += `<script type="importmap">{
7281
7710
  "imports": {
@@ -7360,26 +7789,37 @@ function streamResponse(reactStream, opts, hydrationScript) {
7360
7789
  if (built) bodyScripts += built;
7361
7790
  if (opts.isDev) {
7362
7791
  const wsUrl = `${opts.base}/__weifuwu/livereload`;
7363
- const hbUrl = `${opts.base}/__wfw/h/`;
7364
7792
  bodyScripts += `
7365
7793
  <script>
7366
7794
  (function(){
7367
7795
  var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${wsUrl}');
7368
7796
  var t=0;
7797
+ var _w=window;
7369
7798
  ws.onmessage=function(e){
7370
7799
  try{
7371
7800
  var m=JSON.parse(e.data);
7372
- if(m.type==='component'){
7373
- import('${hbUrl}'+m.hash+'?t='+Date.now()).catch(function(){location.reload()});
7374
- if(m.css){
7375
- var s=document.querySelector('style[data-lr]')||function(){
7376
- var x=document.createElement('style');
7377
- x.setAttribute('data-lr','');
7378
- document.head.appendChild(x);
7379
- return x
7380
- }();
7381
- s.textContent=m.css
7382
- }
7801
+ if(m.type==='update'&&m.url&&m.code){
7802
+ var blob=new Blob([m.code],{type:'application/javascript'});
7803
+ var blobUrl=URL.createObjectURL(blob);
7804
+ import(blobUrl).then(function(mod){
7805
+ if(_w.__wfw) _w.__wfw._update(m.url,mod);
7806
+ // Re-import page module so it re-evaluates its __wfw.h() imports
7807
+ var pageUrl=_w.__WFW_PAGE_URL;
7808
+ if(pageUrl&&_w.__WFW_REFRESH){
7809
+ import(pageUrl.split('?')[0]+'?t='+Date.now()).then(function(pageMod){
7810
+ if(pageMod.default) _w.__WFW_REFRESH(pageMod.default);
7811
+ if(m.css){
7812
+ var s=document.querySelector('style[data-lr]')||function(){
7813
+ var x=document.createElement('style');
7814
+ x.setAttribute('data-lr','');
7815
+ document.head.appendChild(x);
7816
+ return x
7817
+ }();
7818
+ s.textContent=m.css
7819
+ }
7820
+ });
7821
+ }else{location.reload()}
7822
+ }).catch(function(){location.reload()});
7383
7823
  return
7384
7824
  }
7385
7825
  if(m.type==='css'){
@@ -7429,13 +7869,13 @@ ws.onclose=function(){
7429
7869
  var ssrEntries = /* @__PURE__ */ new Map();
7430
7870
 
7431
7871
  // tailwind.ts
7432
- import { createHash as createHash2 } from "node:crypto";
7433
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
7434
- import { join as join3, relative, resolve as resolve4 } from "node:path";
7872
+ import { createHash as createHash3 } from "node:crypto";
7873
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "node:fs";
7874
+ import { join as join3, relative, resolve as resolve5 } from "node:path";
7435
7875
  var extraSources = /* @__PURE__ */ new Set();
7436
7876
  var cssCache = /* @__PURE__ */ new Map();
7437
7877
  function tailwindContext(dir) {
7438
- const cssDir = resolve4(dir);
7878
+ const cssDir = resolve5(dir);
7439
7879
  const cssPath = join3(cssDir, "app", "globals.css");
7440
7880
  return async (req, ctx, next) => {
7441
7881
  if (!cssCache.has(cssPath)) {
@@ -7449,10 +7889,10 @@ function tailwindContext(dir) {
7449
7889
  };
7450
7890
  }
7451
7891
  function tailwindRouter(dir) {
7452
- const cssDir = resolve4(dir);
7892
+ const cssDir = resolve5(dir);
7453
7893
  const cssPath = join3(cssDir, "app", "globals.css");
7454
7894
  const r = new Router();
7455
- r.get("/__wfw/style/:hash.css", async (_req, _ctx) => {
7895
+ r.get("/__wfw/style/:hash.css", async (_req, _ctx2) => {
7456
7896
  if (!cssCache.has(cssPath)) {
7457
7897
  await compileTailwindCss(cssPath, cssDir);
7458
7898
  }
@@ -7466,13 +7906,13 @@ function tailwindRouter(dir) {
7466
7906
  }
7467
7907
  async function compileTailwindCss(cssPath, cssDir) {
7468
7908
  try {
7469
- if (!existsSync2(cssPath)) {
7909
+ if (!existsSync3(cssPath)) {
7470
7910
  mkdirSync2(cssDir, { recursive: true });
7471
7911
  writeFileSync(cssPath, '@import "tailwindcss"\n', "utf-8");
7472
7912
  }
7473
7913
  const { default: tailwindPlugin } = await import("@tailwindcss/postcss");
7474
7914
  const { default: postcss } = await import("postcss");
7475
- let src = readFileSync3(cssPath, "utf-8");
7915
+ let src = readFileSync4(cssPath, "utf-8");
7476
7916
  src = `@source "./";
7477
7917
  ${src}`;
7478
7918
  for (const srcDir of extraSources) {
@@ -7481,7 +7921,7 @@ ${src}`;
7481
7921
  ${src}`;
7482
7922
  }
7483
7923
  const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
7484
- const hash = createHash2("md5").update(result.css).digest("hex").slice(0, 8);
7924
+ const hash = createHash3("md5").update(result.css).digest("hex").slice(0, 8);
7485
7925
  cssCache.set(cssPath, { css: result.css, hash });
7486
7926
  return result.css;
7487
7927
  } catch (err) {
@@ -7492,22 +7932,151 @@ ${src}`;
7492
7932
 
7493
7933
  // live.ts
7494
7934
  import chokidar from "chokidar";
7495
- import { existsSync as existsSync3 } from "node:fs";
7496
- import { dirname as dirname2, join as join4, resolve as resolve5 } from "node:path";
7497
- var clients = /* @__PURE__ */ new Set();
7498
- var hotBundleCache = /* @__PURE__ */ new Map();
7499
- var hotKeys = [];
7500
- var MAX_HOT = 10;
7501
- function setHot(hash, code) {
7502
- if (!hotBundleCache.has(hash)) {
7503
- hotKeys.push(hash);
7504
- if (hotKeys.length > MAX_HOT) {
7505
- const old = hotKeys.shift();
7506
- hotBundleCache.delete(old);
7507
- }
7935
+ import { existsSync as existsSync5 } from "node:fs";
7936
+ import { join as join4, resolve as resolve7 } from "node:path";
7937
+
7938
+ // module-server.ts
7939
+ import * as esbuild3 from "esbuild";
7940
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
7941
+ import { resolve as resolve6, dirname as dirname3, relative as relative2 } from "node:path";
7942
+ import { createHash as createHash4 } from "node:crypto";
7943
+ var moduleCache = /* @__PURE__ */ new Map();
7944
+ var hashCache = /* @__PURE__ */ new Map();
7945
+ function clearModuleCache(filePath) {
7946
+ if (filePath) {
7947
+ const abs = resolve6(filePath);
7948
+ for (const key of moduleCache.keys()) {
7949
+ if (key.endsWith(abs)) moduleCache.delete(key);
7950
+ }
7951
+ hashCache.delete(abs);
7952
+ } else {
7953
+ moduleCache.clear();
7954
+ hashCache.clear();
7508
7955
  }
7509
- hotBundleCache.set(hash, code);
7510
7956
  }
7957
+ var _importRoots = [];
7958
+ function _setImportRoots(roots) {
7959
+ _importRoots = roots;
7960
+ }
7961
+ function fileHash(absPath) {
7962
+ const cached = hashCache.get(absPath);
7963
+ if (cached) return cached;
7964
+ try {
7965
+ const content = readFileSync5(absPath);
7966
+ const h = createHash4("md5").update(content).digest("hex").slice(0, 8);
7967
+ hashCache.set(absPath, h);
7968
+ return h;
7969
+ } catch {
7970
+ return "00000000";
7971
+ }
7972
+ }
7973
+ function rewriteImports(code, absPath, mountPath) {
7974
+ const prefix = mountPath ? `${mountPath}/__wfw/m` : "/__wfw/m";
7975
+ let varCounter = 0;
7976
+ return code.replace(
7977
+ /^(import|export)\s+(.+?)\s+from\s+['"]([^'"]+)['"];?\s*$/gm,
7978
+ (_match, keyword, clause, modPath) => {
7979
+ if (!modPath.startsWith(".")) return _match;
7980
+ const isReexport = keyword === "export";
7981
+ const imports = clause.replace(/^type\s+/, "");
7982
+ const resolved = resolve6(dirname3(absPath), modPath);
7983
+ for (const root of _importRoots) {
7984
+ const rel = relative2(root, resolved);
7985
+ if (!rel.startsWith("..") && !rel.startsWith("/")) {
7986
+ const v = fileHash(resolved);
7987
+ const url = `${prefix}/${rel}?v=${v}`;
7988
+ const defaultMatch = imports.match(/^\s*(\w[\w$]*)\s*$/);
7989
+ const namedMatch = imports.match(/^\s*\{\s*([\w$,\s]+)\s*\}\s*$/);
7990
+ const mixedMatch = imports.match(/^\s*(\w[\w$]*)\s*,\s*\{\s*([\w$,\s]+)\s*\}\s*$/);
7991
+ if (defaultMatch) {
7992
+ const name = defaultMatch[1];
7993
+ if (isReexport) {
7994
+ return `const { default: ${name} } = await __wfw.h("${url}");
7995
+ export { ${name} as default }`;
7996
+ }
7997
+ return `const { default: ${name} } = await __wfw.h("${url}");`;
7998
+ }
7999
+ if (namedMatch) {
8000
+ const names = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
8001
+ if (isReexport) {
8002
+ const tmp = `__wfw$${varCounter++}`;
8003
+ const lines = [`const ${tmp} = await __wfw.h("${url}");`];
8004
+ for (const n of names) lines.push(`export const ${n} = ${tmp}.${n};`);
8005
+ return lines.join("\n");
8006
+ }
8007
+ const decl = names.map((n) => `${n}`).join(", ");
8008
+ return `const { ${decl} } = await __wfw.h("${url}");`;
8009
+ }
8010
+ if (mixedMatch) {
8011
+ const defaultName = mixedMatch[1];
8012
+ const namedNames = mixedMatch[2].split(",").map((s) => s.trim()).filter(Boolean);
8013
+ const varName = `__wfw$${varCounter++}`;
8014
+ const lines = [
8015
+ `const ${varName} = await __wfw.h("${url}");`,
8016
+ `const ${defaultName} = ${varName}.default;`
8017
+ ];
8018
+ for (const n of namedNames) lines.push(`const { ${n} } = ${varName};`);
8019
+ return lines.join("\n");
8020
+ }
8021
+ return _match;
8022
+ }
8023
+ }
8024
+ return _match;
8025
+ }
8026
+ );
8027
+ }
8028
+ async function transformModule(absPath, root, mountPath) {
8029
+ const mp = mountPath || "";
8030
+ const cacheKey = mp + absPath;
8031
+ const cached = moduleCache.get(cacheKey);
8032
+ if (cached) return { url: `${mp}/__wfw/m/${relative2(root, absPath)}`, code: cached };
8033
+ const source = readFileSync5(absPath, "utf-8");
8034
+ const isTsx = absPath.endsWith(".tsx");
8035
+ const result = await esbuild3.transform(source, {
8036
+ loader: isTsx ? "tsx" : "ts",
8037
+ jsx: isTsx ? "automatic" : void 0,
8038
+ jsxImportSource: isTsx ? "react" : void 0,
8039
+ sourcemap: false
8040
+ });
8041
+ let code = result.code;
8042
+ code = rewriteImports(code, absPath, mp);
8043
+ moduleCache.set(cacheKey, code);
8044
+ const url = `${mp}/__wfw/m/${relative2(root, absPath)}`;
8045
+ return { url, code };
8046
+ }
8047
+ function moduleServer(opts) {
8048
+ const roots = Array.isArray(opts.root) ? opts.root : [opts.root];
8049
+ _setImportRoots(roots);
8050
+ const router = new Router();
8051
+ router.get("/__wfw/m/*", (async (req, ctx) => {
8052
+ const reqUrl = new URL(req.url);
8053
+ const filePath = (ctx.params["*"] || "").split("?")[0];
8054
+ const ext = filePath.split(".").pop();
8055
+ if (ext !== "tsx" && ext !== "ts") {
8056
+ return new Response("Not Found", { status: 404 });
8057
+ }
8058
+ const mountPath = ctx.mountPath || "";
8059
+ for (const root of roots) {
8060
+ const absPath = resolve6(root, filePath);
8061
+ if (existsSync4(absPath)) {
8062
+ try {
8063
+ const { code } = await transformModule(absPath, root, mountPath);
8064
+ return new Response(code, {
8065
+ headers: { "content-type": "application/javascript; charset=utf-8" }
8066
+ });
8067
+ } catch (err) {
8068
+ const msg = err instanceof Error ? err.message : String(err);
8069
+ return new Response(`/* Error: ${msg} */`, { status: 500 });
8070
+ }
8071
+ }
8072
+ }
8073
+ return new Response("Not Found", { status: 404 });
8074
+ }));
8075
+ return router;
8076
+ }
8077
+
8078
+ // live.ts
8079
+ var clients = /* @__PURE__ */ new Set();
7511
8080
  function broadcastReload() {
7512
8081
  for (const ws of clients) {
7513
8082
  try {
@@ -7529,7 +8098,7 @@ function broadcastCss(css) {
7529
8098
  }
7530
8099
  function liveWs() {
7531
8100
  return {
7532
- open(ws) {
8101
+ open(ws, _ctx2) {
7533
8102
  clients.add(ws);
7534
8103
  ws.on("close", () => clients.delete(ws));
7535
8104
  ws.on("error", () => clients.delete(ws));
@@ -7540,78 +8109,52 @@ function liveRouter(_dir) {
7540
8109
  const r = new Router();
7541
8110
  compileVendorBundle().catch(() => {
7542
8111
  });
7543
- r.get("/__wfw/h/:hash", async (req, ctx) => {
7544
- const hash = ctx.params.hash.replace(/\.js$/i, "");
7545
- const code = hotBundleCache.get(hash);
7546
- if (!code) return new Response("", { status: 404 });
7547
- return new Response(code, {
7548
- headers: { "content-type": "application/javascript; charset=utf-8" }
7549
- });
7550
- });
7551
8112
  return r;
7552
8113
  }
7553
8114
  function liveWatcher(dir) {
7554
- const resolved = resolve5(dir);
7555
- const entryPath = join4(resolved, "page.tsx");
8115
+ const resolved = resolve7(dir);
7556
8116
  const watcher = chokidar.watch(dir, {
7557
8117
  ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
7558
8118
  ignoreInitial: true
7559
8119
  });
7560
- function findEntries(changedPath) {
7561
- const matched = [];
7562
- for (const [, entry] of ssrEntries) {
7563
- if (!entry.path.startsWith(resolved)) continue;
7564
- if (entry.path === changedPath) {
7565
- matched.push(entry.path);
7566
- } else {
7567
- const ed = dirname2(entry.path);
7568
- if (changedPath.startsWith(ed)) matched.push(entry.path);
7569
- }
7570
- }
7571
- if (matched.length === 0) {
7572
- for (const [, entry] of ssrEntries) {
7573
- if (entry.path.startsWith(resolved)) matched.push(entry.path);
7574
- }
7575
- }
7576
- return matched;
7577
- }
7578
8120
  watcher.on("change", async (filePath) => {
7579
8121
  if (/\.tsx?$/i.test(filePath)) {
7580
8122
  if (filePath.endsWith("layout.tsx")) {
7581
8123
  return broadcastReload();
7582
8124
  }
7583
8125
  clearCompileCache();
7584
- const targets = existsSync3(entryPath) ? [entryPath] : findEntries(resolve5(filePath));
7585
- if (targets.length === 0) return broadcastReload();
8126
+ clearModuleCache();
7586
8127
  try {
7587
- let css;
7588
- const cssPath = join4(resolved, "app", "globals.css");
7589
- if (existsSync3(cssPath)) {
7590
- css = await compileTailwindCss(cssPath, resolved);
7591
- }
7592
- for (const target of targets) {
7593
- await compileTsxDev(target);
7594
- const { hash, code } = await compileHotComponent(target);
7595
- setHot(hash, code);
7596
- const entry = id(target);
7597
- const msg = { type: "component", hash, entry };
7598
- if (css) msg.css = css;
7599
- const str = JSON.stringify(msg);
7600
- for (const ws of clients) {
7601
- try {
7602
- ws.send(str);
7603
- } catch {
7604
- clients.delete(ws);
7605
- }
8128
+ await compileTsxDev(filePath);
8129
+ } catch (e) {
8130
+ console.error("server-side recompile failed:", e);
8131
+ return broadcastReload();
8132
+ }
8133
+ let css;
8134
+ const cssPath = join4(resolved, "app", "globals.css");
8135
+ if (existsSync5(cssPath)) {
8136
+ css = await compileTailwindCss(cssPath, resolved);
8137
+ }
8138
+ try {
8139
+ const absPath = resolve7(filePath);
8140
+ const { url, code } = await transformModule(absPath, resolved);
8141
+ const msg = { type: "update", url, code };
8142
+ if (css) msg.css = css;
8143
+ const str = JSON.stringify(msg);
8144
+ for (const ws of clients) {
8145
+ try {
8146
+ ws.send(str);
8147
+ } catch {
8148
+ clients.delete(ws);
7606
8149
  }
7607
8150
  }
7608
8151
  } catch (e) {
7609
- console.error("live reload failed, fallback to full reload:", e);
8152
+ console.error("module transform failed for HMR:", e);
7610
8153
  broadcastReload();
7611
8154
  }
7612
8155
  } else if (/\.css$/i.test(filePath)) {
7613
8156
  const cssPath = join4(resolved, "app", "globals.css");
7614
- if (existsSync3(cssPath)) {
8157
+ if (existsSync5(cssPath)) {
7615
8158
  const css = await compileTailwindCss(cssPath, resolved);
7616
8159
  if (css) broadcastCss(css);
7617
8160
  }
@@ -7699,7 +8242,7 @@ var isDev2 = isDev();
7699
8242
  var als2 = new AsyncLocalStorage2();
7700
8243
  __registerAls(() => als2.getStore());
7701
8244
  function hashId(s) {
7702
- return createHash3("md5").update(s).digest("hex").slice(0, 8);
8245
+ return createHash5("md5").update(s).digest("hex").slice(0, 8);
7703
8246
  }
7704
8247
  function serializeLoaderData(ctx) {
7705
8248
  const ld = ctx.loaderData;
@@ -7774,7 +8317,7 @@ async function resolveRoute(ssrDir, segments, routeCache) {
7774
8317
  return null;
7775
8318
  }
7776
8319
  const pageFile = join5(dir, "page.tsx");
7777
- if (!existsSync4(pageFile)) {
8320
+ if (!existsSync6(pageFile)) {
7778
8321
  routeCache.set(cacheKey, null);
7779
8322
  return null;
7780
8323
  }
@@ -7785,28 +8328,28 @@ async function resolveRoute(ssrDir, segments, routeCache) {
7785
8328
  let d = dir;
7786
8329
  while (d.startsWith(appDir)) {
7787
8330
  const lf = join5(d, "layout.tsx");
7788
- if (existsSync4(lf)) layoutFiles.unshift(lf);
8331
+ if (existsSync6(lf)) layoutFiles.unshift(lf);
7789
8332
  if (d === appDir) break;
7790
- d = dirname3(d);
8333
+ d = dirname4(d);
7791
8334
  }
7792
8335
  const errorFiles = [];
7793
8336
  d = dir;
7794
8337
  while (d.startsWith(appDir)) {
7795
8338
  const ef = join5(d, "error.tsx");
7796
- if (existsSync4(ef)) errorFiles.unshift(ef);
8339
+ if (existsSync6(ef)) errorFiles.unshift(ef);
7797
8340
  if (d === appDir) break;
7798
- d = dirname3(d);
8341
+ d = dirname4(d);
7799
8342
  }
7800
8343
  let notFoundFile = null;
7801
8344
  d = dir;
7802
8345
  while (d.startsWith(appDir)) {
7803
8346
  const nf = join5(d, "not-found.tsx");
7804
- if (existsSync4(nf)) {
8347
+ if (existsSync6(nf)) {
7805
8348
  notFoundFile = nf;
7806
8349
  break;
7807
8350
  }
7808
8351
  if (d === appDir) break;
7809
- d = dirname3(d);
8352
+ d = dirname4(d);
7810
8353
  }
7811
8354
  const result = {
7812
8355
  routePath: "/" + routeParams.join("/"),
@@ -7818,8 +8361,7 @@ async function resolveRoute(ssrDir, segments, routeCache) {
7818
8361
  routeCache.set(cacheKey, result);
7819
8362
  return result;
7820
8363
  }
7821
- function buildHydrationScript(entryId, ctxJson, base) {
7822
- const ssrPrefix = `${base}/__ssr`;
8364
+ function buildHydrationScript(pageUrl, ctxJson) {
7823
8365
  return `
7824
8366
  <script type="module">
7825
8367
  import { setCtx, TsxContext } from 'weifuwu/react';
@@ -7832,10 +8374,11 @@ setCtx(_ctx);
7832
8374
  const _root = document.getElementById('__weifuwu_root');
7833
8375
 
7834
8376
  async function init() {
7835
- const { default: Page } = await import('${ssrPrefix}/${entryId}.js');
7836
- const app = createElement(TsxContext.Provider, { value: _ctx },
7837
- createElement(Page));
8377
+ const { default: Page } = await import('${pageUrl}');
7838
8378
  ${isDev2 ? `
8379
+ // Store page URL for __wfw runtime
8380
+ window.__WFW_PAGE_URL = '${pageUrl}';
8381
+
7839
8382
  // Stable proxy \u2014 same function ref = React preserves fiber + useState state across HMR
7840
8383
  const _pageImpl = { current: Page };
7841
8384
  const _pageProxy = new Proxy(function __wfw_page(){}, {
@@ -7852,14 +8395,22 @@ async function init() {
7852
8395
  }
7853
8396
  renderPage();
7854
8397
 
8398
+ // HMR: re-render page (sub-modules resolved via __wfw.h() pick up changes)
8399
+ window.__WFW_RERENDER = () => {
8400
+ _tick++;
8401
+ reactRoot.render(createElement(TsxContext.Provider, { value: _ctx },
8402
+ createElement(_pageProxy, { __t: _tick })));
8403
+ };
8404
+
8405
+ // Fallback: swap entire page component
7855
8406
  window.__WFW_REFRESH = async (NewComponent) => {
7856
8407
  const store = globalThis.__WEIFUWU_CTX_STORE?._ctx || _ctx;
7857
8408
  _pageImpl.current = NewComponent;
7858
- _tick++;
7859
- reactRoot.render(createElement(TsxContext.Provider, { value: store },
7860
- createElement(_pageProxy, { __t: _tick })));
8409
+ __WFW_RERENDER();
7861
8410
  };
7862
8411
  ` : `
8412
+ const app = createElement(TsxContext.Provider, { value: _ctx },
8413
+ createElement(Page));
7863
8414
  hydrateRoot(_root, app);
7864
8415
  `}
7865
8416
  }
@@ -7867,8 +8418,8 @@ async function init() {
7867
8418
  init();
7868
8419
  </script>`;
7869
8420
  }
7870
- function renderPage(pageFile, outDir) {
7871
- const absPath = resolve6(pageFile);
8421
+ function renderPage(pageFile, projectDir) {
8422
+ const absPath = resolve8(pageFile);
7872
8423
  const entryId = hashId(absPath);
7873
8424
  ssrEntries.set(entryId, { path: absPath });
7874
8425
  return async (req, ctx) => {
@@ -7897,9 +8448,10 @@ function renderPage(pageFile, outDir) {
7897
8448
  loaderData,
7898
8449
  env: ctx.env ?? {}
7899
8450
  };
8451
+ const pageRelative = relative3(projectDir, absPath);
8452
+ const pageUrl = `${base}/__wfw/m/${pageRelative}`;
7900
8453
  return als2.run(ctxValue, async () => {
7901
8454
  setCtx(ctxValue);
7902
- await compileBrowser(absPath, outDir);
7903
8455
  let element = createElement3(
7904
8456
  "div",
7905
8457
  { id: "__weifuwu_root" },
@@ -7917,7 +8469,7 @@ function renderPage(pageFile, outDir) {
7917
8469
  loaderData,
7918
8470
  tailwind: ctx.tailwind
7919
8471
  },
7920
- buildHydrationScript(entryId, JSON.stringify(ctxValue), base)
8472
+ buildHydrationScript(pageUrl, JSON.stringify(ctxValue))
7921
8473
  );
7922
8474
  });
7923
8475
  };
@@ -7932,7 +8484,7 @@ function runChain(mws, handler, req, ctx) {
7932
8484
  }
7933
8485
  function discoverRoutes(dir) {
7934
8486
  const appDir = join5(dir, "app");
7935
- if (!existsSync4(appDir)) return [];
8487
+ if (!existsSync6(appDir)) return [];
7936
8488
  const result = [];
7937
8489
  function walk(currentDir, routePath) {
7938
8490
  let entries;
@@ -7953,7 +8505,7 @@ function discoverRoutes(dir) {
7953
8505
  } else if (entry.name === "page.tsx") {
7954
8506
  result.push({
7955
8507
  path: routePath || "/",
7956
- file: relative2(appDir, join5(currentDir, entry.name))
8508
+ file: relative3(appDir, join5(currentDir, entry.name))
7957
8509
  });
7958
8510
  }
7959
8511
  }
@@ -7963,28 +8515,19 @@ function discoverRoutes(dir) {
7963
8515
  }
7964
8516
  function ssr(opts) {
7965
8517
  const r = new Router();
7966
- const dir = resolve6(opts.dir);
7967
- const outDir = resolve6(OUT_DIR);
8518
+ const dir = resolve8(opts.dir);
7968
8519
  const routeCache = /* @__PURE__ */ new Map();
8520
+ const wfwRoot = resolve8(import.meta.dirname ?? __dirname);
8521
+ r.use("/", moduleServer({ root: [dir, wfwRoot] }));
7969
8522
  compileVendorBundle().catch(() => {
7970
8523
  });
7971
- r.get("/__ssr/:file", (req, ctx) => {
7972
- const filePath = join5(outDir, ctx.params.file);
7973
- if (!filePath.startsWith(outDir) || !existsSync4(filePath)) {
7974
- return new Response("Not Found", { status: 404 });
7975
- }
7976
- const content = readFileSync4(filePath, "utf-8");
7977
- return new Response(content, {
7978
- headers: { "content-type": "application/javascript; charset=utf-8" }
7979
- });
7980
- });
7981
8524
  r.get("/__wfw/v/bundle", async () => {
7982
8525
  const code = await compileVendorBundle();
7983
8526
  return new Response(code, {
7984
8527
  headers: { "content-type": "application/javascript; charset=utf-8" }
7985
8528
  });
7986
8529
  });
7987
- if (existsSync4(join5(dir, "app", "globals.css"))) {
8530
+ if (existsSync6(join5(dir, "app", "globals.css"))) {
7988
8531
  r.use("/", tailwindRouter(dir));
7989
8532
  }
7990
8533
  let devWatcher;
@@ -8020,7 +8563,7 @@ function ssr(opts) {
8020
8563
  ...resolved.layoutFiles.map((f) => layout(f)),
8021
8564
  tailwindContext(dir)
8022
8565
  ];
8023
- const handler = (req2, ctx2) => renderPage(resolved.pageFile, outDir)(req2, ctx2);
8566
+ const handler = (req2, ctx2) => renderPage(resolved.pageFile, dir)(req2, ctx2);
8024
8567
  return runChain(mws, handler, req, ctx);
8025
8568
  });
8026
8569
  const mod = r;
@@ -8272,13 +8815,13 @@ function createBashTool(ctx) {
8272
8815
  return { stdout: "", stderr: "Command denied: potentially dangerous command", exitCode: 1 };
8273
8816
  }
8274
8817
  const cwd = workdir ? `${ctx.workspace}/${workdir}` : ctx.workspace;
8275
- return new Promise((resolve14) => {
8818
+ return new Promise((resolve16) => {
8276
8819
  const child = exec(
8277
8820
  command,
8278
8821
  { cwd, timeout: timeout * 1e3, maxBuffer: 1024 * 1024 },
8279
8822
  (error, stdout, stderr) => {
8280
8823
  const truncated = stdout.length > 1e6 || stderr.length > 1e6;
8281
- resolve14({
8824
+ resolve16({
8282
8825
  stdout: stdout.slice(0, 1e6),
8283
8826
  stderr: stderr.slice(0, 1e6),
8284
8827
  exitCode: error?.code ?? 0,
@@ -8295,8 +8838,8 @@ function createBashTool(ctx) {
8295
8838
  // opencode/tools/read.ts
8296
8839
  import { tool as tool4 } from "ai";
8297
8840
  import { z as z6 } from "zod";
8298
- import { readFileSync as readFileSync5 } from "node:fs";
8299
- import { resolve as resolve7 } from "node:path";
8841
+ import { readFileSync as readFileSync7 } from "node:fs";
8842
+ import { resolve as resolve9 } from "node:path";
8300
8843
  function createReadTool(ctx) {
8301
8844
  return tool4({
8302
8845
  description: "Read file contents. Supports offset and limit for reading specific line ranges.",
@@ -8306,11 +8849,11 @@ function createReadTool(ctx) {
8306
8849
  limit: z6.number().optional().describe("Number of lines to read")
8307
8850
  }),
8308
8851
  execute: async ({ path: path2, offset, limit }) => {
8309
- const resolved = resolve7(ctx.workspace, path2);
8852
+ const resolved = resolve9(ctx.workspace, path2);
8310
8853
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
8311
8854
  return { error: "Path not allowed", content: null, totalLines: 0 };
8312
8855
  }
8313
- const content = readFileSync5(resolved, "utf-8");
8856
+ const content = readFileSync7(resolved, "utf-8");
8314
8857
  const lines = content.split("\n");
8315
8858
  const totalLines = lines.length;
8316
8859
  if (offset !== void 0) {
@@ -8338,7 +8881,7 @@ function createReadTool(ctx) {
8338
8881
  import { tool as tool5 } from "ai";
8339
8882
  import { z as z7 } from "zod";
8340
8883
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "node:fs";
8341
- import { resolve as resolve8, dirname as dirname4 } from "node:path";
8884
+ import { resolve as resolve10, dirname as dirname5 } from "node:path";
8342
8885
  function createWriteTool(ctx) {
8343
8886
  return tool5({
8344
8887
  description: "Create or overwrite a file. Parent directories are created automatically.",
@@ -8347,11 +8890,11 @@ function createWriteTool(ctx) {
8347
8890
  content: z7.string().describe("File content")
8348
8891
  }),
8349
8892
  execute: async ({ path: path2, content }) => {
8350
- const resolved = resolve8(ctx.workspace, path2);
8893
+ const resolved = resolve10(ctx.workspace, path2);
8351
8894
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
8352
8895
  return { error: "Path not allowed" };
8353
8896
  }
8354
- mkdirSync3(dirname4(resolved), { recursive: true });
8897
+ mkdirSync3(dirname5(resolved), { recursive: true });
8355
8898
  writeFileSync2(resolved, content, "utf-8");
8356
8899
  return { path: path2, size: content.length };
8357
8900
  }
@@ -8361,8 +8904,8 @@ function createWriteTool(ctx) {
8361
8904
  // opencode/tools/edit.ts
8362
8905
  import { tool as tool6 } from "ai";
8363
8906
  import { z as z8 } from "zod";
8364
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
8365
- import { resolve as resolve9 } from "node:path";
8907
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
8908
+ import { resolve as resolve11 } from "node:path";
8366
8909
  function createEditTool(ctx) {
8367
8910
  return tool6({
8368
8911
  description: "Perform exact string replacements in a file. If oldString appears multiple times, provide more surrounding context.",
@@ -8373,11 +8916,11 @@ function createEditTool(ctx) {
8373
8916
  replaceAll: z8.boolean().default(false).describe("Replace all occurrences")
8374
8917
  }),
8375
8918
  execute: async ({ path: path2, oldString, newString, replaceAll }) => {
8376
- const resolved = resolve9(ctx.workspace, path2);
8919
+ const resolved = resolve11(ctx.workspace, path2);
8377
8920
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
8378
8921
  return { error: "Path not allowed" };
8379
8922
  }
8380
- const content = readFileSync6(resolved, "utf-8");
8923
+ const content = readFileSync8(resolved, "utf-8");
8381
8924
  if (replaceAll) {
8382
8925
  if (!content.includes(oldString)) {
8383
8926
  return { error: "oldString not found in file", replaced: 0 };
@@ -8409,8 +8952,8 @@ function createEditTool(ctx) {
8409
8952
  import { tool as tool7 } from "ai";
8410
8953
  import { z as z9 } from "zod";
8411
8954
  import { execFileSync } from "node:child_process";
8412
- import { resolve as resolve10 } from "node:path";
8413
- import { existsSync as existsSync5 } from "node:fs";
8955
+ import { resolve as resolve12 } from "node:path";
8956
+ import { existsSync as existsSync7 } from "node:fs";
8414
8957
  function createGrepTool(ctx) {
8415
8958
  return tool7({
8416
8959
  description: "Search file contents using regex. Supports file type filtering and context lines.",
@@ -8421,10 +8964,10 @@ function createGrepTool(ctx) {
8421
8964
  context: z9.number().default(0).describe("Number of context lines before and after each match")
8422
8965
  }),
8423
8966
  execute: async ({ pattern, include, path: path2, context }) => {
8424
- const searchDir = path2 ? resolve10(ctx.workspace, path2) : ctx.workspace;
8967
+ const searchDir = path2 ? resolve12(ctx.workspace, path2) : ctx.workspace;
8425
8968
  try {
8426
8969
  let stdout;
8427
- if (existsSync5("/usr/bin/rg") || existsSync5("/usr/local/bin/rg")) {
8970
+ if (existsSync7("/usr/bin/rg") || existsSync7("/usr/local/bin/rg")) {
8428
8971
  const args = ["-n"];
8429
8972
  if (context > 0) args.push("-C", String(context));
8430
8973
  if (include) args.push("-g", include);
@@ -8457,7 +9000,7 @@ function createGrepTool(ctx) {
8457
9000
  import { tool as tool8 } from "ai";
8458
9001
  import { z as z10 } from "zod";
8459
9002
  import { execFileSync as execFileSync2 } from "node:child_process";
8460
- import { resolve as resolve11 } from "node:path";
9003
+ import { resolve as resolve13 } from "node:path";
8461
9004
  function createGlobTool(ctx) {
8462
9005
  return tool8({
8463
9006
  description: "Find files matching a glob pattern.",
@@ -8466,7 +9009,7 @@ function createGlobTool(ctx) {
8466
9009
  path: z10.string().optional().describe("Subdirectory relative to workspace")
8467
9010
  }),
8468
9011
  execute: async ({ pattern, path: path2 }) => {
8469
- const searchDir = path2 ? resolve11(ctx.workspace, path2) : ctx.workspace;
9012
+ const searchDir = path2 ? resolve13(ctx.workspace, path2) : ctx.workspace;
8470
9013
  try {
8471
9014
  const stdout = execFileSync2(
8472
9015
  "find",
@@ -8492,7 +9035,7 @@ function createGlobTool(ctx) {
8492
9035
  // opencode/tools/web.ts
8493
9036
  import { tool as tool9 } from "ai";
8494
9037
  import { z as z11 } from "zod";
8495
- function createWebTool(_ctx) {
9038
+ function createWebTool(_ctx2) {
8496
9039
  return tool9({
8497
9040
  description: "Fetch a URL and return the content as text.",
8498
9041
  inputSchema: z11.object({
@@ -8528,7 +9071,7 @@ function createQuestionTool(ctx) {
8528
9071
  options: z12.array(z12.string()).optional().describe("Optional multiple choice options")
8529
9072
  }),
8530
9073
  execute: async ({ question, options }, { toolCallId }) => {
8531
- return new Promise((resolve14, reject) => {
9074
+ return new Promise((resolve16, reject) => {
8532
9075
  const timeout = setTimeout(() => {
8533
9076
  ctx.pendingQuestions.delete(toolCallId);
8534
9077
  reject(new Error("Question timed out"));
@@ -8536,7 +9079,7 @@ function createQuestionTool(ctx) {
8536
9079
  ctx.pendingQuestions.set(toolCallId, {
8537
9080
  resolve: (answer) => {
8538
9081
  clearTimeout(timeout);
8539
- resolve14(answer);
9082
+ resolve16(answer);
8540
9083
  },
8541
9084
  reject: (err) => {
8542
9085
  clearTimeout(timeout);
@@ -8857,7 +9400,7 @@ function createWSHandler2(deps) {
8857
9400
  clients2.delete(ws);
8858
9401
  }
8859
9402
  },
8860
- error(ws, _ctx, _err) {
9403
+ error(ws, _ctx2, _err) {
8861
9404
  const client = clients2.get(ws);
8862
9405
  if (client) {
8863
9406
  client.abortController?.abort();
@@ -8870,7 +9413,7 @@ function createWSHandler2(deps) {
8870
9413
  // opencode/skills.ts
8871
9414
  import { readFile, glob } from "node:fs/promises";
8872
9415
  import { homedir } from "node:os";
8873
- import { resolve as resolve12 } from "node:path";
9416
+ import { resolve as resolve14 } from "node:path";
8874
9417
  import { parse as parseYaml } from "yaml";
8875
9418
  var SEARCH_DIRS = [
8876
9419
  (ws) => `${ws}/.opencode/skills`,
@@ -8908,7 +9451,7 @@ async function scanDir(dir) {
8908
9451
  try {
8909
9452
  const files = [];
8910
9453
  for await (const entry of glob("*/SKILL.md", { cwd: dir })) {
8911
- const skill = await parseSkillFile(resolve12(dir, entry));
9454
+ const skill = await parseSkillFile(resolve14(dir, entry));
8912
9455
  if (skill) files.push(skill);
8913
9456
  }
8914
9457
  return files;
@@ -9335,7 +9878,7 @@ function theme(options) {
9335
9878
 
9336
9879
  // i18n.ts
9337
9880
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
9338
- import { join as join7, resolve as resolve13 } from "node:path";
9881
+ import { join as join7, resolve as resolve15 } from "node:path";
9339
9882
  var DEFAULTS2 = {
9340
9883
  default: "en",
9341
9884
  cookie: "locale",
@@ -9353,7 +9896,7 @@ function translate(msgs, key, params, fallback) {
9353
9896
  }
9354
9897
  function i18n(options) {
9355
9898
  const opts = { ...DEFAULTS2, ...options };
9356
- const dir = opts.dir ? resolve13(opts.dir) : void 0;
9899
+ const dir = opts.dir ? resolve15(opts.dir) : void 0;
9357
9900
  const cache3 = /* @__PURE__ */ new Map();
9358
9901
  function validLocale(locale) {
9359
9902
  return /^[\w-]+$/.test(locale) && !locale.includes("..");
@@ -9863,381 +10406,6 @@ function logdb(options) {
9863
10406
  // iii/client.ts
9864
10407
  import crypto8 from "node:crypto";
9865
10408
 
9866
- // iii/stream.ts
9867
- function notify(channels, stream, group, item, event, data) {
9868
- const keys = [`${stream}`, `${stream}:${group}`, `${stream}:${group}:${item}`];
9869
- const msg = JSON.stringify({
9870
- type: "stream",
9871
- stream_name: stream,
9872
- group_id: group,
9873
- item_id: item,
9874
- event,
9875
- data
9876
- });
9877
- for (const key of keys) {
9878
- const subs = channels.get(key);
9879
- if (!subs) continue;
9880
- for (const ws of subs) {
9881
- try {
9882
- ws.send(msg);
9883
- } catch {
9884
- }
9885
- }
9886
- }
9887
- }
9888
- function deepClone(v) {
9889
- return JSON.parse(JSON.stringify(v));
9890
- }
9891
- function applyOps(value, ops) {
9892
- let current = deepClone(value ?? {});
9893
- for (const op2 of ops) {
9894
- switch (op2.op) {
9895
- case "set":
9896
- current = deepClone(op2.value);
9897
- break;
9898
- case "merge":
9899
- if (typeof current === "object" && current !== null && !Array.isArray(current)) {
9900
- current = { ...current, ...deepClone(op2.value) };
9901
- } else {
9902
- current = deepClone(op2.value);
9903
- }
9904
- break;
9905
- case "increment":
9906
- current = (typeof current === "number" ? current : 0) + op2.value;
9907
- break;
9908
- case "decrement":
9909
- current = (typeof current === "number" ? current : 0) - op2.value;
9910
- break;
9911
- case "append":
9912
- if (!Array.isArray(current)) current = [];
9913
- current.push(deepClone(op2.value));
9914
- break;
9915
- case "remove":
9916
- current = null;
9917
- break;
9918
- }
9919
- }
9920
- return current;
9921
- }
9922
- function createMemoryStore(channels) {
9923
- const store2 = /* @__PURE__ */ new Map();
9924
- function key(stream, group, item) {
9925
- return `${stream}:${group}:${item}`;
9926
- }
9927
- return {
9928
- async set(stream, group, item, data) {
9929
- const k = key(stream, group, item);
9930
- const old = store2.get(k) ?? null;
9931
- store2.set(k, deepClone(data));
9932
- notify(channels, stream, group, item, "set", data);
9933
- return { old_value: old, new_value: deepClone(data) };
9934
- },
9935
- async get(stream, group, item) {
9936
- const v = store2.get(key(stream, group, item)) ?? null;
9937
- return { value: deepClone(v) };
9938
- },
9939
- async delete(stream, group, item) {
9940
- const k = key(stream, group, item);
9941
- const old = store2.get(k) ?? null;
9942
- store2.delete(k);
9943
- notify(channels, stream, group, item, "delete", null);
9944
- return { old_value: old };
9945
- },
9946
- async list(stream, group) {
9947
- const items = [];
9948
- const prefix = `${stream}:${group}:`;
9949
- for (const [k, v] of store2) {
9950
- if (k.startsWith(prefix) && !k.slice(prefix.length).includes(":")) {
9951
- items.push({ item_id: k.slice(prefix.length), data: deepClone(v) });
9952
- }
9953
- }
9954
- return { items };
9955
- },
9956
- async list_groups(stream) {
9957
- const groups = /* @__PURE__ */ new Set();
9958
- const prefix = `${stream}:`;
9959
- for (const k of store2.keys()) {
9960
- if (k.startsWith(prefix)) {
9961
- const rest = k.slice(prefix.length);
9962
- const g = rest.split(":")[0];
9963
- if (g) groups.add(g);
9964
- }
9965
- }
9966
- return { groups: Array.from(groups) };
9967
- },
9968
- async list_all() {
9969
- const streamMap = /* @__PURE__ */ new Map();
9970
- for (const k of store2.keys()) {
9971
- const parts = k.split(":");
9972
- const s = parts[0];
9973
- const g = parts[1];
9974
- if (!streamMap.has(s)) streamMap.set(s, { groups: /* @__PURE__ */ new Set(), items: /* @__PURE__ */ new Set() });
9975
- const entry = streamMap.get(s);
9976
- if (g) entry.groups.add(g);
9977
- entry.items.add(k);
9978
- }
9979
- const streams = Array.from(streamMap.entries()).map(([name, info]) => ({
9980
- stream_name: name,
9981
- group_count: info.groups.size,
9982
- item_count: info.items.size
9983
- }));
9984
- return { streams, count: streams.length };
9985
- },
9986
- async send(stream, group, type, data, id2) {
9987
- notify(channels, stream, group, id2 ?? "", "send", { type, data });
9988
- },
9989
- async update(stream, group, item, ops) {
9990
- const k = key(stream, group, item);
9991
- const old = deepClone(store2.get(k) ?? null);
9992
- const newVal = applyOps(old, ops);
9993
- store2.set(k, deepClone(newVal));
9994
- notify(channels, stream, group, item, "update", newVal);
9995
- return { old_value: old, new_value: deepClone(newVal) };
9996
- }
9997
- };
9998
- }
9999
- function createPgStore(channels, pg) {
10000
- const sql2 = pg.sql;
10001
- return {
10002
- async set(stream, group, item, data) {
10003
- const rows = await sql2`
10004
- INSERT INTO "_iii_stream" (stream_name, group_id, item_id, data)
10005
- VALUES (${stream}, ${group}, ${item}, ${data})
10006
- ON CONFLICT (stream_name, group_id, item_id)
10007
- DO UPDATE SET data = ${data}, updated_at = NOW()
10008
- RETURNING data
10009
- `;
10010
- notify(channels, stream, group, item, "set", data);
10011
- return { old_value: null, new_value: data };
10012
- },
10013
- async get(stream, group, item) {
10014
- const rows = await sql2`
10015
- SELECT data FROM "_iii_stream"
10016
- WHERE stream_name = ${stream} AND group_id = ${group} AND item_id = ${item}
10017
- `;
10018
- const row = rows[0];
10019
- let value = row?.data ?? null;
10020
- if (typeof value === "string") value = JSON.parse(value);
10021
- return { value };
10022
- },
10023
- async delete(stream, group, item) {
10024
- const rows = await sql2`
10025
- DELETE FROM "_iii_stream"
10026
- WHERE stream_name = ${stream} AND group_id = ${group} AND item_id = ${item}
10027
- RETURNING data
10028
- `;
10029
- const old = rows[0]?.data ?? null;
10030
- notify(channels, stream, group, item, "delete", null);
10031
- return { old_value: old };
10032
- },
10033
- async list(stream, group) {
10034
- const rows = await sql2`
10035
- SELECT item_id, data FROM "_iii_stream"
10036
- WHERE stream_name = ${stream} AND group_id = ${group}
10037
- ORDER BY item_id
10038
- `;
10039
- const items = rows.map((r) => ({
10040
- item_id: r.item_id,
10041
- data: typeof r.data === "string" ? JSON.parse(r.data) : r.data
10042
- }));
10043
- return { items };
10044
- },
10045
- async list_groups(stream) {
10046
- const rows = await sql2`
10047
- SELECT DISTINCT group_id FROM "_iii_stream"
10048
- WHERE stream_name = ${stream}
10049
- ORDER BY group_id
10050
- `;
10051
- return { groups: rows.map((r) => r.group_id) };
10052
- },
10053
- async list_all() {
10054
- const rows = await sql2`
10055
- SELECT stream_name, COUNT(DISTINCT group_id) as group_count, COUNT(*) as item_count
10056
- FROM "_iii_stream"
10057
- GROUP BY stream_name
10058
- ORDER BY stream_name
10059
- `;
10060
- const streams = rows.map((r) => ({
10061
- stream_name: r.stream_name,
10062
- group_count: Number(r.group_count),
10063
- item_count: Number(r.item_count)
10064
- }));
10065
- return { streams, count: streams.length };
10066
- },
10067
- async send(stream, group, type, data, id2) {
10068
- notify(channels, stream, group, id2 ?? "", "send", { type, data });
10069
- },
10070
- async update(stream, group, item, ops) {
10071
- const { value: oldVal } = await this.get(stream, group, item);
10072
- const newVal = applyOps(oldVal, ops);
10073
- await sql2`
10074
- INSERT INTO "_iii_stream" (stream_name, group_id, item_id, data)
10075
- VALUES (${stream}, ${group}, ${item}, ${newVal})
10076
- ON CONFLICT (stream_name, group_id, item_id)
10077
- DO UPDATE SET data = ${newVal}, updated_at = NOW()
10078
- `;
10079
- notify(channels, stream, group, item, "update", newVal);
10080
- return { old_value: oldVal, new_value: deepClone(newVal) };
10081
- }
10082
- };
10083
- }
10084
- function createRedisStore(channels, redis2, ttl) {
10085
- function hashKey(stream, group) {
10086
- return `iii:stream:${stream}:${group}`;
10087
- }
10088
- function setTTL(hk) {
10089
- if (ttl) redis2.expire(hk, ttl);
10090
- }
10091
- return {
10092
- async set(stream, group, item, data) {
10093
- const hk = hashKey(stream, group);
10094
- const oldRaw = await redis2.hget(hk, item);
10095
- const old = oldRaw ? JSON.parse(oldRaw) : null;
10096
- await redis2.hset(hk, item, JSON.stringify(data));
10097
- setTTL(hk);
10098
- await redis2.publish(
10099
- `iii:stream:${stream}`,
10100
- JSON.stringify({ event: "set", group, item, data })
10101
- );
10102
- notify(channels, stream, group, item, "set", data);
10103
- return { old_value: old, new_value: deepClone(data) };
10104
- },
10105
- async get(stream, group, item) {
10106
- const raw = await redis2.hget(hashKey(stream, group), item);
10107
- return { value: raw ? JSON.parse(raw) : null };
10108
- },
10109
- async delete(stream, group, item) {
10110
- const hk = hashKey(stream, group);
10111
- const oldRaw = await redis2.hget(hk, item);
10112
- const old = oldRaw ? JSON.parse(oldRaw) : null;
10113
- await redis2.hdel(hk, item);
10114
- const remaining = await redis2.hlen(hk);
10115
- if (remaining === 0) await redis2.del(hk);
10116
- await redis2.publish(`iii:stream:${stream}`, JSON.stringify({ event: "delete", group, item }));
10117
- notify(channels, stream, group, item, "delete", null);
10118
- return { old_value: old };
10119
- },
10120
- async list(stream, group) {
10121
- const raw = await redis2.hgetall(hashKey(stream, group));
10122
- const items = Object.entries(raw).map(([item_id, data]) => ({
10123
- item_id,
10124
- data: JSON.parse(data)
10125
- }));
10126
- return { items };
10127
- },
10128
- async list_groups(stream) {
10129
- const pattern = `iii:stream:${stream}:*`;
10130
- let cursor = "0";
10131
- const groups = /* @__PURE__ */ new Set();
10132
- do {
10133
- const [next, keys] = await redis2.scan(cursor, "MATCH", pattern, "COUNT", "1000");
10134
- cursor = next;
10135
- for (const k of keys) {
10136
- const parts = k.split(":");
10137
- const g = parts.slice(3).join(":");
10138
- if (g) groups.add(g);
10139
- }
10140
- } while (cursor !== "0");
10141
- return { groups: Array.from(groups) };
10142
- },
10143
- async list_all() {
10144
- const pattern = "iii:stream:*";
10145
- let cursor = "0";
10146
- const streamMap = /* @__PURE__ */ new Map();
10147
- do {
10148
- const [next, keys] = await redis2.scan(cursor, "MATCH", pattern, "COUNT", "1000");
10149
- cursor = next;
10150
- for (const k of keys) {
10151
- const parts = k.split(":");
10152
- const s = parts[2];
10153
- const g = parts.slice(3).join(":");
10154
- if (!streamMap.has(s)) streamMap.set(s, { groups: /* @__PURE__ */ new Set(), items: 0 });
10155
- const entry = streamMap.get(s);
10156
- if (g) entry.groups.add(g);
10157
- entry.items++;
10158
- }
10159
- } while (cursor !== "0");
10160
- const streams = Array.from(streamMap.entries()).map(([name, info]) => ({
10161
- stream_name: name,
10162
- group_count: info.groups.size,
10163
- item_count: info.items
10164
- }));
10165
- return { streams, count: streams.length };
10166
- },
10167
- async send(stream, group, type, data, id2) {
10168
- notify(channels, stream, group, id2 ?? "", "send", { type, data });
10169
- },
10170
- async update(stream, group, item, ops) {
10171
- const hk = hashKey(stream, group);
10172
- const oldRaw = await redis2.hget(hk, item);
10173
- const old = oldRaw ? JSON.parse(oldRaw) : null;
10174
- const newVal = applyOps(old, ops);
10175
- await redis2.hset(hk, item, JSON.stringify(newVal));
10176
- setTTL(hk);
10177
- await redis2.publish(
10178
- `iii:stream:${stream}`,
10179
- JSON.stringify({ event: "update", group, item, data: newVal })
10180
- );
10181
- notify(channels, stream, group, item, "update", newVal);
10182
- return { old_value: old, new_value: deepClone(newVal) };
10183
- }
10184
- };
10185
- }
10186
- function createStream(opts) {
10187
- const channels = /* @__PURE__ */ new Map();
10188
- const store2 = opts?.pg ? createPgStore(channels, opts.pg) : opts?.redis ? createRedisStore(channels, opts.redis, opts.streamTTL ?? 3600) : createMemoryStore(channels);
10189
- let redisSub = null;
10190
- if (opts?.redis) {
10191
- redisSub = opts.redis.duplicate();
10192
- redisSub.on("message", (rawChannel, rawData) => {
10193
- if (!rawChannel.startsWith("iii:stream:")) return;
10194
- const stream = rawChannel.slice("iii:stream:".length);
10195
- try {
10196
- const msg = JSON.parse(rawData);
10197
- if (msg.event === "set" || msg.event === "update") {
10198
- notify(channels, stream, msg.group, msg.item, msg.event, msg.data);
10199
- } else if (msg.event === "delete") {
10200
- notify(channels, stream, msg.group, msg.item, "delete", null);
10201
- }
10202
- } catch {
10203
- }
10204
- });
10205
- }
10206
- return {
10207
- ...store2,
10208
- subscribe(ws, sub) {
10209
- 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;
10210
- if (!channels.has(key)) channels.set(key, /* @__PURE__ */ new Set());
10211
- channels.get(key).add(ws);
10212
- if (redisSub && sub.stream_name) {
10213
- redisSub.subscribe(`iii:stream:${sub.stream_name}`);
10214
- }
10215
- },
10216
- unsubscribe(ws) {
10217
- for (const [, subs] of channels) subs.delete(ws);
10218
- },
10219
- async migrate() {
10220
- if (opts?.pg) {
10221
- const sql2 = opts.pg.sql;
10222
- await sql2`
10223
- CREATE TABLE IF NOT EXISTS "_iii_stream" (
10224
- stream_name TEXT NOT NULL,
10225
- group_id TEXT NOT NULL,
10226
- item_id TEXT NOT NULL,
10227
- data JSONB,
10228
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10229
- PRIMARY KEY (stream_name, group_id, item_id)
10230
- )
10231
- `;
10232
- await sql2`CREATE INDEX IF NOT EXISTS idx_iii_stream_group ON "_iii_stream" (stream_name, group_id)`;
10233
- }
10234
- },
10235
- async close() {
10236
- if (redisSub) await redisSub.quit();
10237
- }
10238
- };
10239
- }
10240
-
10241
10409
  // iii/ws.ts
10242
10410
  function createWsHandler(deps) {
10243
10411
  const wsToWorkerId = /* @__PURE__ */ new Map();
@@ -10245,7 +10413,7 @@ function createWsHandler(deps) {
10245
10413
  return wsToWorkerId.get(ws) || "";
10246
10414
  }
10247
10415
  return {
10248
- open(_ws, _ctx) {
10416
+ open(_ws, _ctx2) {
10249
10417
  },
10250
10418
  async message(ws, ctx, data) {
10251
10419
  let msg;
@@ -10271,9 +10439,9 @@ function createWsHandler(deps) {
10271
10439
  const workerId = getWorkerId(ws);
10272
10440
  if (workerId) {
10273
10441
  deps.registerRemoteTrigger(workerId, {
10274
- type: msg.trigger_type || msg.type,
10275
- function_id: msg.function_id,
10276
- config: msg.config || {}
10442
+ type: msg.input?.type || "custom",
10443
+ function_id: msg.input?.function_id || msg.id,
10444
+ config: msg.input?.config || {}
10277
10445
  });
10278
10446
  }
10279
10447
  break;
@@ -10285,11 +10453,7 @@ function createWsHandler(deps) {
10285
10453
  }
10286
10454
  case "unregister_trigger": {
10287
10455
  const workerId = getWorkerId(ws);
10288
- if (workerId) deps.unregisterRemoteTrigger(workerId, msg.function_id);
10289
- break;
10290
- }
10291
- case "invoke": {
10292
- deps.handleInvoke(ws, msg.invocation_id, msg.function_id, msg.payload);
10456
+ if (workerId) deps.unregisterRemoteTrigger(workerId, msg.function_id || msg.id);
10293
10457
  break;
10294
10458
  }
10295
10459
  case "invoke_result": {
@@ -10300,31 +10464,20 @@ function createWsHandler(deps) {
10300
10464
  deps.handleInvokeError(msg.invocation_id, msg.error);
10301
10465
  break;
10302
10466
  }
10303
- case "subscribe": {
10304
- deps.addStreamSubscriber(ws, {
10305
- stream_name: msg.stream_name || msg.channel || "",
10306
- group_id: msg.group_id,
10307
- item_id: msg.item_id
10308
- });
10309
- break;
10310
- }
10311
- case "unsubscribe": {
10312
- deps.removeStreamSubscriber(ws);
10467
+ case "invoke": {
10468
+ deps.handleInvoke(ws, msg.invocation_id, msg.function_id, msg.payload);
10313
10469
  break;
10314
10470
  }
10471
+ default:
10472
+ ws.send(JSON.stringify({ type: "error", message: `Unknown message type: ${msg.type}` }));
10315
10473
  }
10316
10474
  },
10317
10475
  close(ws) {
10318
10476
  const workerId = getWorkerId(ws);
10319
- if (workerId) deps.unregisterRemoteWorker(workerId);
10320
- wsToWorkerId.delete(ws);
10321
- deps.removeStreamSubscriber(ws);
10322
- },
10323
- error(ws) {
10324
- const workerId = getWorkerId(ws);
10325
- if (workerId) deps.unregisterRemoteWorker(workerId);
10326
- wsToWorkerId.delete(ws);
10327
- deps.removeStreamSubscriber(ws);
10477
+ if (workerId) {
10478
+ deps.unregisterRemoteWorker(workerId);
10479
+ wsToWorkerId.delete(ws);
10480
+ }
10328
10481
  }
10329
10482
  };
10330
10483
  }
@@ -10369,38 +10522,11 @@ function buildRouter5(engine, wsHandler) {
10369
10522
  }
10370
10523
 
10371
10524
  // iii/client.ts
10372
- function iii(opts = {}) {
10373
- const stream = createStream({ pg: opts.pg, redis: opts.redis, streamTTL: opts.streamTTL });
10525
+ function iii(_opts = {}) {
10374
10526
  const workers = /* @__PURE__ */ new Map();
10375
10527
  const functions = /* @__PURE__ */ new Map();
10376
10528
  const triggers = /* @__PURE__ */ new Map();
10377
10529
  const pending = /* @__PURE__ */ new Map();
10378
- function registerBuiltin(id2, handler) {
10379
- functions.set(id2, {
10380
- id: id2,
10381
- handler,
10382
- workerId: "__iii__",
10383
- workerName: "__iii__",
10384
- triggers: []
10385
- });
10386
- }
10387
- registerBuiltin(
10388
- "stream::set",
10389
- (p) => stream.set(p.stream_name, p.group_id, p.item_id, p.data)
10390
- );
10391
- registerBuiltin("stream::get", (p) => stream.get(p.stream_name, p.group_id, p.item_id));
10392
- registerBuiltin("stream::delete", (p) => stream.delete(p.stream_name, p.group_id, p.item_id));
10393
- registerBuiltin("stream::list", (p) => stream.list(p.stream_name, p.group_id));
10394
- registerBuiltin("stream::list_groups", (p) => stream.list_groups(p.stream_name));
10395
- registerBuiltin("stream::list_all", () => stream.list_all());
10396
- registerBuiltin(
10397
- "stream::send",
10398
- (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id)
10399
- );
10400
- registerBuiltin(
10401
- "stream::update",
10402
- (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops)
10403
- );
10404
10530
  function addLocalWorker(worker) {
10405
10531
  const workerId = crypto8.randomUUID();
10406
10532
  const reg = {
@@ -10447,12 +10573,12 @@ function iii(opts = {}) {
10447
10573
  const handler = async (payload) => {
10448
10574
  if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
10449
10575
  const invocationId = crypto8.randomUUID();
10450
- return new Promise((resolve14, reject) => {
10576
+ return new Promise((resolve16, reject) => {
10451
10577
  const timer = setTimeout(() => {
10452
10578
  pending.delete(invocationId);
10453
10579
  reject(new Error(`Invocation timed out for "${id2}"`));
10454
10580
  }, 3e4);
10455
- pending.set(invocationId, { resolve: resolve14, reject, timer });
10581
+ pending.set(invocationId, { resolve: resolve16, reject, timer });
10456
10582
  worker.ws.send(
10457
10583
  JSON.stringify({
10458
10584
  type: "invoke",
@@ -10519,12 +10645,6 @@ function iii(opts = {}) {
10519
10645
  worker.triggers = worker.triggers.filter((t) => t.function_id !== functionId);
10520
10646
  }
10521
10647
  },
10522
- addStreamSubscriber(ws, sub) {
10523
- stream.subscribe(ws, sub);
10524
- },
10525
- removeStreamSubscriber(ws) {
10526
- stream.unsubscribe(ws);
10527
- },
10528
10648
  handleInvokeResult(invocationId, result) {
10529
10649
  const p = pending.get(invocationId);
10530
10650
  if (p) {
@@ -10577,7 +10697,7 @@ function iii(opts = {}) {
10577
10697
  }
10578
10698
  function trigger(request) {
10579
10699
  const fn = functions.get(request.function_id);
10580
- if (!fn) throw new Error(`Function "${request.function_id}" not found`);
10700
+ if (!fn) return Promise.reject(new Error(`Function "${request.function_id}" not found`));
10581
10701
  const ctx = { engine: engineRef, functionId: request.function_id, workerName: fn.workerName };
10582
10702
  if (request.action === "void") {
10583
10703
  queueMicrotask(() => fn.handler(request.payload, ctx));
@@ -10624,7 +10744,6 @@ function iii(opts = {}) {
10624
10744
  mod.listFunctions = listFunctions;
10625
10745
  mod.listTriggers = listTriggers;
10626
10746
  mod.migrate = async () => {
10627
- await stream.migrate();
10628
10747
  };
10629
10748
  mod.close = async () => {
10630
10749
  for (const [, p] of pending) {
@@ -10636,7 +10755,6 @@ function iii(opts = {}) {
10636
10755
  workers.clear();
10637
10756
  functions.clear();
10638
10757
  triggers.clear();
10639
- await stream.close();
10640
10758
  };
10641
10759
  return mod;
10642
10760
  }
@@ -10703,8 +10821,8 @@ function registerWorker(url) {
10703
10821
  function connect() {
10704
10822
  if (intentionalClose) return;
10705
10823
  ws = new WebSocket(url);
10706
- ready = new Promise((resolve14) => {
10707
- resolveReady = resolve14;
10824
+ ready = new Promise((resolve16) => {
10825
+ resolveReady = resolve16;
10708
10826
  });
10709
10827
  ws.onopen = () => {
10710
10828
  reconnectAttempt = 0;
@@ -10773,11 +10891,6 @@ function registerWorker(url) {
10773
10891
  }
10774
10892
  break;
10775
10893
  }
10776
- case "stream": {
10777
- const handler = handlers.get("__stream__");
10778
- if (handler) handler(msg, {});
10779
- break;
10780
- }
10781
10894
  }
10782
10895
  };
10783
10896
  ws.onclose = () => {
@@ -10835,13 +10948,13 @@ function registerWorker(url) {
10835
10948
  }
10836
10949
  return Promise.resolve(fn(request.payload, ctx));
10837
10950
  }
10838
- return new Promise((resolve14, reject) => {
10951
+ return new Promise((resolve16, reject) => {
10839
10952
  const invocationId = genId();
10840
10953
  const timer = setTimeout(() => {
10841
10954
  pendingInvocations.delete(invocationId);
10842
10955
  reject(new Error(`Invocation timed out for "${request.function_id}"`));
10843
10956
  }, request.timeout_ms || 3e4);
10844
- pendingInvocations.set(invocationId, { resolve: resolve14, reject, timer });
10957
+ pendingInvocations.set(invocationId, { resolve: resolve16, reject, timer });
10845
10958
  send({
10846
10959
  type: "invoke",
10847
10960
  invocation_id: invocationId,
@@ -10850,9 +10963,6 @@ function registerWorker(url) {
10850
10963
  });
10851
10964
  });
10852
10965
  },
10853
- onStream(handler) {
10854
- handlers.set("__stream__", handler);
10855
- },
10856
10966
  close() {
10857
10967
  intentionalClose = true;
10858
10968
  if (reconnectTimer) clearTimeout(reconnectTimer);
@@ -11104,6 +11214,7 @@ function session(options) {
11104
11214
  }
11105
11215
  return res;
11106
11216
  });
11217
+ mw.__meta = { injects: ["session"], depends: [] };
11107
11218
  mw.close = async () => {
11108
11219
  await closeStore?.();
11109
11220
  };
@@ -12033,6 +12144,475 @@ function permissions(options) {
12033
12144
  mw.migrate = migrate;
12034
12145
  return mw;
12035
12146
  }
12147
+
12148
+ // mcp.ts
12149
+ import { spawn } from "node:child_process";
12150
+ import { createInterface } from "node:readline";
12151
+ import { z as z14 } from "zod";
12152
+ var _requestId = 0;
12153
+ function nextId() {
12154
+ return ++_requestId;
12155
+ }
12156
+ function createRequest2(id2, method, params) {
12157
+ return JSON.stringify({
12158
+ jsonrpc: "2.0",
12159
+ id: id2,
12160
+ method,
12161
+ params
12162
+ });
12163
+ }
12164
+ function mcpClient(options) {
12165
+ const { command, args = [], env: env2 } = options;
12166
+ const timeout = options.timeout ?? 15e3;
12167
+ const maxResponseSize = options.maxResponseSize ?? 10 * 1024 * 1024;
12168
+ let proc = null;
12169
+ let rl = null;
12170
+ const pending = /* @__PURE__ */ new Map();
12171
+ let _tools = null;
12172
+ function ensureProcess() {
12173
+ if (proc && !proc.killed) return;
12174
+ proc = spawn(command, args, {
12175
+ stdio: ["pipe", "pipe", "pipe"],
12176
+ env: { ...process.env, ...env2 }
12177
+ });
12178
+ rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
12179
+ let buffer = "";
12180
+ rl.on("line", (line) => {
12181
+ buffer += line;
12182
+ try {
12183
+ const msg = JSON.parse(buffer);
12184
+ buffer = "";
12185
+ handleMessage(msg);
12186
+ } catch {
12187
+ }
12188
+ });
12189
+ proc.stderr?.on("data", (chunk) => {
12190
+ const text2 = chunk.toString().trim();
12191
+ if (text2) {
12192
+ console.debug(`[mcp:${command}] stderr:`, text2);
12193
+ }
12194
+ });
12195
+ proc.on("exit", (code, signal) => {
12196
+ console.debug(`[mcp:${command}] exited (code=${code} signal=${signal})`);
12197
+ for (const [id2, { reject, timer }] of pending) {
12198
+ clearTimeout(timer);
12199
+ reject(new Error(`MCP server exited (code=${code} signal=${signal})`));
12200
+ pending.delete(id2);
12201
+ }
12202
+ proc = null;
12203
+ rl = null;
12204
+ });
12205
+ proc.on("error", (err) => {
12206
+ console.error(`[mcp:${command}] error:`, err.message);
12207
+ for (const [, { reject, timer }] of pending) {
12208
+ clearTimeout(timer);
12209
+ reject(err);
12210
+ }
12211
+ pending.clear();
12212
+ });
12213
+ }
12214
+ function handleMessage(msg) {
12215
+ if (msg.id !== void 0 && pending.has(msg.id)) {
12216
+ const { resolve: resolve16, reject, timer } = pending.get(msg.id);
12217
+ clearTimeout(timer);
12218
+ pending.delete(msg.id);
12219
+ if (msg.error) {
12220
+ reject(new Error(`MCP error: ${JSON.stringify(msg.error)}`));
12221
+ } else {
12222
+ resolve16(msg.result);
12223
+ }
12224
+ }
12225
+ }
12226
+ function sendRequest(method, params) {
12227
+ ensureProcess();
12228
+ const id2 = nextId();
12229
+ const body = createRequest2(id2, method, params);
12230
+ return new Promise((resolve16, reject) => {
12231
+ const timer = setTimeout(() => {
12232
+ pending.delete(id2);
12233
+ reject(new Error(`MCP request "${method}" timed out after ${timeout}ms`));
12234
+ }, timeout);
12235
+ pending.set(id2, { resolve: resolve16, reject, timer });
12236
+ if (proc?.stdin?.writable) {
12237
+ proc.stdin.write(body + "\n");
12238
+ } else {
12239
+ clearTimeout(timer);
12240
+ pending.delete(id2);
12241
+ reject(new Error("MCP server stdin not available"));
12242
+ }
12243
+ });
12244
+ }
12245
+ async function initialize() {
12246
+ ensureProcess();
12247
+ await sendRequest("initialize", {
12248
+ protocolVersion: "0.1.0",
12249
+ capabilities: {},
12250
+ clientInfo: { name: "weifuwu", version: "0.25.0" }
12251
+ });
12252
+ try {
12253
+ if (proc?.stdin?.writable) {
12254
+ proc.stdin.write(
12255
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n"
12256
+ );
12257
+ }
12258
+ } catch {
12259
+ }
12260
+ }
12261
+ function mcpSchemaToZod(inputSchema) {
12262
+ if (!inputSchema || !inputSchema.properties) {
12263
+ return z14.object({});
12264
+ }
12265
+ const shape = {};
12266
+ const required = new Set(inputSchema.required ?? []);
12267
+ for (const [key, prop] of Object.entries(inputSchema.properties)) {
12268
+ const p = prop;
12269
+ let field;
12270
+ switch (p.type) {
12271
+ case "string":
12272
+ field = z14.string();
12273
+ break;
12274
+ case "number":
12275
+ field = z14.number();
12276
+ break;
12277
+ case "integer":
12278
+ field = z14.number().int();
12279
+ break;
12280
+ case "boolean":
12281
+ field = z14.boolean();
12282
+ break;
12283
+ case "array":
12284
+ field = z14.array(z14.any());
12285
+ break;
12286
+ case "object":
12287
+ field = z14.record(z14.string(), z14.any());
12288
+ break;
12289
+ default:
12290
+ field = z14.any();
12291
+ }
12292
+ if (p.description) {
12293
+ field = field.describe(p.description);
12294
+ }
12295
+ if (!required.has(key)) {
12296
+ field = field.optional();
12297
+ }
12298
+ shape[key] = field;
12299
+ }
12300
+ return z14.object(shape);
12301
+ }
12302
+ async function refresh() {
12303
+ _tools = null;
12304
+ await getTools();
12305
+ }
12306
+ async function getTools() {
12307
+ if (_tools) return _tools;
12308
+ await initialize();
12309
+ const result = await sendRequest("tools/list");
12310
+ const defs = result?.tools ?? [];
12311
+ const tools = {};
12312
+ for (const def of defs) {
12313
+ const paramsSchema = mcpSchemaToZod(def.inputSchema);
12314
+ tools[def.name] = {
12315
+ description: def.description ?? "MCP tool: " + def.name,
12316
+ parameters: paramsSchema,
12317
+ execute: async (args2) => {
12318
+ const raw = await callToolInternal(def.name, args2);
12319
+ return raw;
12320
+ }
12321
+ };
12322
+ }
12323
+ _tools = tools;
12324
+ return tools;
12325
+ }
12326
+ async function callToolInternal(name, args2) {
12327
+ const result = await sendRequest("tools/call", {
12328
+ name,
12329
+ arguments: args2
12330
+ });
12331
+ const content = result?.content;
12332
+ if (Array.isArray(content)) {
12333
+ const textParts = content.filter((c) => c.type === "text").map((c) => c.text ?? "");
12334
+ if (textParts.length > 0) {
12335
+ let combined = textParts.join("\n");
12336
+ if (combined.length > maxResponseSize) {
12337
+ combined = combined.slice(0, maxResponseSize) + "\n... [truncated]";
12338
+ }
12339
+ return combined;
12340
+ }
12341
+ const resourceParts = content.filter((c) => c.type === "resource");
12342
+ if (resourceParts.length > 0) {
12343
+ return resourceParts;
12344
+ }
12345
+ return content;
12346
+ }
12347
+ return result;
12348
+ }
12349
+ async function callTool(name, args2) {
12350
+ return callToolInternal(name, args2);
12351
+ }
12352
+ async function close() {
12353
+ try {
12354
+ await sendRequest("shutdown");
12355
+ } catch {
12356
+ }
12357
+ if (proc && !proc.killed) {
12358
+ proc.kill("SIGTERM");
12359
+ setTimeout(() => {
12360
+ if (proc && !proc.killed) {
12361
+ try {
12362
+ proc.kill("SIGKILL");
12363
+ } catch {
12364
+ }
12365
+ }
12366
+ }, 3e3);
12367
+ }
12368
+ for (const [, { reject, timer }] of pending) {
12369
+ clearTimeout(timer);
12370
+ reject(new Error("MCP client closed"));
12371
+ }
12372
+ pending.clear();
12373
+ proc = null;
12374
+ rl = null;
12375
+ }
12376
+ return {
12377
+ getTools,
12378
+ refresh,
12379
+ callTool,
12380
+ close
12381
+ };
12382
+ }
12383
+
12384
+ // notifier/client.ts
12385
+ var DEFAULT_CHANNELS = ["inbox"];
12386
+ function notifier(opts) {
12387
+ const { sql: sql2, mailer: mailer2, hub } = opts;
12388
+ const table = opts.table ?? "_notifications";
12389
+ const fromName = opts.fromName ?? "System";
12390
+ const pageSize = opts.pageSize ?? 50;
12391
+ function escapeIdent7(s) {
12392
+ return `"${s.replace(/"/g, '""')}"`;
12393
+ }
12394
+ const tbl = escapeIdent7(table);
12395
+ async function migrate() {
12396
+ await sql2.unsafe(`
12397
+ CREATE TABLE IF NOT EXISTS ${tbl} (
12398
+ id SERIAL PRIMARY KEY,
12399
+ user_id INTEGER NOT NULL,
12400
+ title TEXT NOT NULL,
12401
+ body TEXT NOT NULL DEFAULT '',
12402
+ action_url TEXT,
12403
+ action_text TEXT,
12404
+ type TEXT NOT NULL DEFAULT 'default',
12405
+ metadata JSONB NOT NULL DEFAULT '{}',
12406
+ read_at TIMESTAMPTZ,
12407
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
12408
+ )
12409
+ `);
12410
+ await sql2.unsafe(`
12411
+ CREATE INDEX IF NOT EXISTS "${table}_user_unread"
12412
+ ON ${tbl} (user_id, created_at DESC)
12413
+ WHERE read_at IS NULL
12414
+ `);
12415
+ await sql2.unsafe(`
12416
+ CREATE INDEX IF NOT EXISTS "${table}_user_all"
12417
+ ON ${tbl} (user_id, created_at DESC)
12418
+ `);
12419
+ await sql2.unsafe(`
12420
+ CREATE TABLE IF NOT EXISTS "_notify_prefs" (
12421
+ user_id INTEGER PRIMARY KEY,
12422
+ channels JSONB NOT NULL DEFAULT '["inbox"]'::jsonb
12423
+ )
12424
+ `);
12425
+ }
12426
+ async function insertNotification(userId2, message) {
12427
+ await sql2.unsafe(
12428
+ `INSERT INTO ${tbl} (user_id, title, body, action_url, action_text, type, metadata)
12429
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
12430
+ [
12431
+ userId2,
12432
+ message.title,
12433
+ message.body ?? "",
12434
+ message.actionUrl ?? null,
12435
+ message.actionText ?? null,
12436
+ message.type ?? "default",
12437
+ message.metadata ?? {}
12438
+ ]
12439
+ );
12440
+ }
12441
+ async function send(to, message) {
12442
+ const prefs = await getPreferences(to.userId);
12443
+ if (prefs.channels.includes("inbox")) {
12444
+ await insertNotification(to.userId, message);
12445
+ }
12446
+ if (prefs.channels.includes("email") && mailer2 && to.email) {
12447
+ const html = renderEmail(message);
12448
+ mailer2.send({
12449
+ to: to.email,
12450
+ subject: message.title,
12451
+ text: message.body ?? "",
12452
+ html
12453
+ }).catch((err) => console.error("[notifier] email send failed:", err.message));
12454
+ }
12455
+ if (prefs.channels.includes("ws") && hub) {
12456
+ hub.broadcast(`notify:${to.userId}`, {
12457
+ type: "notification",
12458
+ data: {
12459
+ title: message.title,
12460
+ body: message.body,
12461
+ actionUrl: message.actionUrl,
12462
+ actionText: message.actionText,
12463
+ type: message.type ?? "default",
12464
+ metadata: message.metadata,
12465
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
12466
+ }
12467
+ });
12468
+ }
12469
+ }
12470
+ async function broadcast(message) {
12471
+ const rows = await sql2`
12472
+ SELECT user_id FROM "_notify_prefs"
12473
+ WHERE channels @> ${sql2.json(["inbox"])}
12474
+ `;
12475
+ for (const row of rows) {
12476
+ await insertNotification(row.user_id, message);
12477
+ if (hub) {
12478
+ hub.broadcast(`notify:${row.user_id}`, {
12479
+ type: "notification",
12480
+ data: {
12481
+ title: message.title,
12482
+ body: message.body,
12483
+ actionUrl: message.actionUrl,
12484
+ actionText: message.actionText,
12485
+ type: message.type ?? "default",
12486
+ metadata: message.metadata,
12487
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
12488
+ }
12489
+ });
12490
+ }
12491
+ }
12492
+ }
12493
+ async function unreadCount(userId2) {
12494
+ const [row] = await sql2.unsafe(
12495
+ `SELECT COUNT(*)::int AS count FROM ${tbl} WHERE user_id = $1 AND read_at IS NULL`,
12496
+ [userId2]
12497
+ );
12498
+ return row?.count ?? 0;
12499
+ }
12500
+ async function count(userId2, unreadOnly = false) {
12501
+ if (unreadOnly) return unreadCount(userId2);
12502
+ const [row] = await sql2.unsafe(
12503
+ `SELECT COUNT(*)::int AS count FROM ${tbl} WHERE user_id = $1`,
12504
+ [userId2]
12505
+ );
12506
+ return row?.count ?? 0;
12507
+ }
12508
+ async function markRead(userId2, notificationIds) {
12509
+ if (notificationIds && notificationIds.length > 0) {
12510
+ const ids = notificationIds.map((_, i) => `$${i + 2}`).join(", ");
12511
+ await sql2.unsafe(
12512
+ `UPDATE ${tbl} SET read_at = NOW() WHERE user_id = $1 AND id IN (${ids}) AND read_at IS NULL`,
12513
+ [userId2, ...notificationIds]
12514
+ );
12515
+ } else {
12516
+ await sql2.unsafe(`UPDATE ${tbl} SET read_at = NOW() WHERE user_id = $1 AND read_at IS NULL`, [
12517
+ userId2
12518
+ ]);
12519
+ }
12520
+ }
12521
+ async function list(userId2, opts2) {
12522
+ const limit = opts2?.limit ?? pageSize;
12523
+ const offset = opts2?.offset ?? 0;
12524
+ let where = `user_id = $1`;
12525
+ const params = [userId2];
12526
+ const paramIdx = 2;
12527
+ if (opts2?.unreadOnly) {
12528
+ where += ` AND read_at IS NULL`;
12529
+ }
12530
+ const rows = await sql2.unsafe(
12531
+ `SELECT id, user_id, title, body, action_url, action_text, type, metadata, read_at, created_at
12532
+ FROM ${tbl}
12533
+ WHERE ${where}
12534
+ ORDER BY created_at DESC
12535
+ LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`,
12536
+ [...params, limit, offset]
12537
+ );
12538
+ return rows.map(normalizeNotification);
12539
+ }
12540
+ function normalizeNotification(row) {
12541
+ return {
12542
+ ...row,
12543
+ metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata ?? {}
12544
+ };
12545
+ }
12546
+ async function getPreferences(userId2) {
12547
+ const [row] = await sql2.unsafe(`SELECT channels FROM "_notify_prefs" WHERE user_id = $1`, [
12548
+ userId2
12549
+ ]);
12550
+ if (!row) {
12551
+ return { channels: [...DEFAULT_CHANNELS] };
12552
+ }
12553
+ let channels;
12554
+ if (typeof row.channels === "string") {
12555
+ channels = JSON.parse(row.channels);
12556
+ } else if (Array.isArray(row.channels)) {
12557
+ channels = row.channels;
12558
+ } else {
12559
+ channels = [...DEFAULT_CHANNELS];
12560
+ }
12561
+ return { channels };
12562
+ }
12563
+ async function setPreferences(userId2, prefs) {
12564
+ await sql2`
12565
+ INSERT INTO "_notify_prefs" (user_id, channels)
12566
+ VALUES (${userId2}, ${sql2.json(prefs.channels)})
12567
+ ON CONFLICT (user_id)
12568
+ DO UPDATE SET channels = ${sql2.json(prefs.channels)}
12569
+ `;
12570
+ }
12571
+ async function clean(days) {
12572
+ const result = await sql2.unsafe(`DELETE FROM ${tbl} WHERE created_at < NOW() - $1::interval`, [
12573
+ `${days} days`
12574
+ ]);
12575
+ return Array.isArray(result) ? result.length : 0;
12576
+ }
12577
+ function renderEmail(message) {
12578
+ const actionHtml = message.actionUrl ? `
12579
+ <p><a href="${escapeHtml3(message.actionUrl)}" style="display:inline-block;padding:10px 20px;background:#0066cc;color:#fff;text-decoration:none;border-radius:4px">${escapeHtml3(message.actionText ?? "View")}</a></p>` : "";
12580
+ return `<!DOCTYPE html>
12581
+ <html>
12582
+ <head><meta charset="utf-8"></head>
12583
+ <body style="font-family:sans-serif;padding:20px;max-width:600px">
12584
+ <h2>${escapeHtml3(message.title)}</h2>
12585
+ ${message.body ? `<p>${escapeHtml3(message.body)}</p>` : ""}
12586
+ ${actionHtml}
12587
+ <hr style="margin-top:30px;border:none;border-top:1px solid #eee">
12588
+ <p style="color:#999;font-size:12px">${escapeHtml3(fromName)}</p>
12589
+ </body>
12590
+ </html>`;
12591
+ }
12592
+ function escapeHtml3(s) {
12593
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12594
+ }
12595
+ const mw = async (req, ctx, next) => {
12596
+ ;
12597
+ ctx.notifier = api;
12598
+ return next(req, ctx);
12599
+ };
12600
+ const api = {
12601
+ send,
12602
+ broadcast,
12603
+ unreadCount,
12604
+ count,
12605
+ markRead,
12606
+ list,
12607
+ getPreferences,
12608
+ setPreferences,
12609
+ clean,
12610
+ migrate,
12611
+ close: async () => {
12612
+ }
12613
+ };
12614
+ return Object.assign(mw, api);
12615
+ }
12036
12616
  export {
12037
12617
  DEFAULT_MAX_BODY,
12038
12618
  MIGRATIONS_TABLE,
@@ -12087,7 +12667,9 @@ export {
12087
12667
  logdb,
12088
12668
  logger,
12089
12669
  mailer,
12670
+ mcpClient,
12090
12671
  messager,
12672
+ notifier,
12091
12673
  openai,
12092
12674
  opencode,
12093
12675
  permissions,