weifuwu 0.24.3 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,6 +4,16 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // types.ts
8
+ var HttpError = class extends Error {
9
+ status;
10
+ constructor(message, status) {
11
+ super(message);
12
+ this.name = "HttpError";
13
+ this.status = status;
14
+ }
15
+ };
16
+
7
17
  // trace.ts
8
18
  import crypto2 from "node:crypto";
9
19
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -110,14 +120,6 @@ function env() {
110
120
 
111
121
  // serve.ts
112
122
  import http from "node:http";
113
- var HttpError = class extends Error {
114
- status;
115
- constructor(message, status) {
116
- super(message);
117
- this.status = status;
118
- this.name = "HttpError";
119
- }
120
- };
121
123
  var DEFAULT_MAX_BODY = 10 * 1024 * 1024;
122
124
  async function readBody(req, maxSize) {
123
125
  const limit = maxSize ?? DEFAULT_MAX_BODY;
@@ -467,6 +469,8 @@ var Router = class _Router {
467
469
  _hasWildcard = false;
468
470
  _hub;
469
471
  _wss;
472
+ /** Track which ctx fields have been injected so far (for dependency checking). */
473
+ _ctxFields = /* @__PURE__ */ new Set();
470
474
  get wss() {
471
475
  if (!this._wss) this._wss = new WebSocketServer({ noServer: true });
472
476
  return this._wss;
@@ -490,17 +494,47 @@ var Router = class _Router {
490
494
  node = getOrCreateChild(node, segment, createTrieNode, false);
491
495
  }
492
496
  node.pathMws.push(arg2);
497
+ this._checkMiddlewareMeta(arg2, `${arg1}`);
493
498
  }
494
499
  } else if (typeof arg1 === "function") {
495
500
  this.globalMws.push(arg1);
501
+ this._checkMiddlewareMeta(arg1, "global");
496
502
  } else if (typeof arg1 === "object" && arg1 !== null && "middleware" in arg1 && // eslint-disable-next-line @typescript-eslint/no-explicit-any
497
503
  typeof arg1.middleware === "function" && arg1 instanceof _Router) {
498
504
  const mod = arg1;
499
- this.globalMws.push(mod.middleware());
505
+ const mw = mod.middleware();
506
+ this.globalMws.push(mw);
507
+ this._checkMiddlewareMeta(mw, "global (auto-registered)");
500
508
  this._mountRouter("/", mod);
501
509
  }
502
510
  return this;
503
511
  }
512
+ /**
513
+ * Check a middleware's dependency metadata and emit warnings if
514
+ * required fields haven't been injected yet.
515
+ * Attach __meta to a middleware function:
516
+ *
517
+ * ```ts
518
+ * mw.__meta = { injects: ['sql'], depends: ['session'] }
519
+ * ```
520
+ */
521
+ _checkMiddlewareMeta(mw, location) {
522
+ const meta = mw.__meta ?? mw.middleware?.().__meta;
523
+ if (!meta) return;
524
+ for (const dep of meta.depends) {
525
+ if (!this._ctxFields.has(dep)) {
526
+ console.warn(
527
+ `[weifuwu] Middleware at "${location}" depends on ctx.${dep} but it hasn't been registered yet.
528
+ Register the provider before this middleware:
529
+ app.use(${dep}()) // add before this middleware
530
+ Current ctx fields: [${[...this._ctxFields].join(", ")}]`
531
+ );
532
+ }
533
+ }
534
+ for (const field of meta.injects) {
535
+ this._ctxFields.add(field);
536
+ }
537
+ }
504
538
  // Route registration — returns Router<T> unchanged.
505
539
  // Route-level middleware and handlers get Context<T>.
506
540
  get(path2, ...args) {
@@ -1239,7 +1273,7 @@ function parseBody(text2, ct) {
1239
1273
  return text2;
1240
1274
  }
1241
1275
  function validate(schemas) {
1242
- return async (req, ctx, next) => {
1276
+ const mw = async (req, ctx, next) => {
1243
1277
  const parsed = {};
1244
1278
  const issues = [];
1245
1279
  if (schemas?.params) {
@@ -1326,6 +1360,8 @@ function validate(schemas) {
1326
1360
  ctx.parsed = { ...ctx.parsed, ...parsed };
1327
1361
  return next(req, ctx);
1328
1362
  };
1363
+ mw.__meta = { injects: ["parsed"], depends: [] };
1364
+ return mw;
1329
1365
  }
1330
1366
 
1331
1367
  // cookie.ts
@@ -1429,7 +1465,7 @@ function detectMimeFromExtension(filename) {
1429
1465
  }
1430
1466
  function upload(options) {
1431
1467
  const saveDir = options?.dir;
1432
- return async (req, ctx, next) => {
1468
+ const mw = async (req, ctx, next) => {
1433
1469
  const ct = req.headers.get("content-type") ?? "";
1434
1470
  if (!ct.includes("multipart/form-data")) return next(req, ctx);
1435
1471
  try {
@@ -1485,6 +1521,8 @@ function upload(options) {
1485
1521
  ctx.parsed = { ...ctx.parsed, files, fields };
1486
1522
  return next(req, ctx);
1487
1523
  };
1524
+ mw.__meta = { injects: ["parsed"], depends: [] };
1525
+ return mw;
1488
1526
  }
1489
1527
 
1490
1528
  // rate-limit.ts
@@ -1568,7 +1606,8 @@ function rateLimit(options) {
1568
1606
  const res = await next(req, ctx);
1569
1607
  return addRateLimitHeaders(res, max, remaining, reset);
1570
1608
  };
1571
- mw.close = () => {
1609
+ mw.__meta = { injects: [], depends: [] };
1610
+ mw.close = async () => {
1572
1611
  if (interval) clearInterval(interval);
1573
1612
  hits.clear();
1574
1613
  };
@@ -1695,7 +1734,7 @@ import crypto3 from "node:crypto";
1695
1734
  function requestId(options) {
1696
1735
  const header = options?.header ?? "X-Request-ID";
1697
1736
  const gen = options?.generator ?? (() => crypto3.randomUUID());
1698
- return async (req, ctx, next) => {
1737
+ const mw = async (req, ctx, next) => {
1699
1738
  const existing = req.headers.get(header);
1700
1739
  const id2 = existing ?? gen();
1701
1740
  ctx.requestId = id2;
@@ -1705,6 +1744,8 @@ function requestId(options) {
1705
1744
  h.set(header, id2);
1706
1745
  return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h });
1707
1746
  };
1747
+ mw.__meta = { injects: ["requestId"], depends: [] };
1748
+ return mw;
1708
1749
  }
1709
1750
 
1710
1751
  // sse.ts
@@ -1751,6 +1792,7 @@ function createSSEStream(iterable, opts) {
1751
1792
  }
1752
1793
 
1753
1794
  // test-utils.ts
1795
+ import { WebSocket as WSWebSocket } from "ws";
1754
1796
  var TestResponseImpl = class {
1755
1797
  response;
1756
1798
  constructor(response) {
@@ -1799,6 +1841,7 @@ var TestRequest = class {
1799
1841
  }
1800
1842
  /** Shortcut: set ctx.user */
1801
1843
  withUser(user2) {
1844
+ ;
1802
1845
  this.ctxMixin.user = user2;
1803
1846
  return this;
1804
1847
  }
@@ -1846,9 +1889,22 @@ var TestRequest = class {
1846
1889
  };
1847
1890
  var TestApp = class {
1848
1891
  router;
1892
+ wsServer = null;
1893
+ wsConnections = [];
1849
1894
  constructor() {
1850
1895
  this.router = new Router();
1851
1896
  }
1897
+ /**
1898
+ * Register a WebSocket handler.
1899
+ */
1900
+ ws(path2, handler) {
1901
+ this.router.ws(path2, handler);
1902
+ return this;
1903
+ }
1904
+ /** Get the raw Router (for advanced use). */
1905
+ get _router() {
1906
+ return this.router;
1907
+ }
1852
1908
  /** Add global middleware */
1853
1909
  use(mw) {
1854
1910
  this.router.use(mw);
@@ -1908,6 +1964,195 @@ var TestApp = class {
1908
1964
  handler() {
1909
1965
  return this.router.handler();
1910
1966
  }
1967
+ /** Start building a WebSocket connection to the given path. */
1968
+ wsReq(path2) {
1969
+ return new TestWSRequest(this, path2);
1970
+ }
1971
+ /**
1972
+ * Internal: ensure HTTP server is running for WebSocket connections.
1973
+ * Starts on a random port.
1974
+ */
1975
+ /* @internal */
1976
+ async _ensureServer() {
1977
+ if (this.wsServer) {
1978
+ return `http://localhost:${this.wsServer.port}`;
1979
+ }
1980
+ const wsHandler = this.router.websocketHandler();
1981
+ if (!wsHandler) {
1982
+ throw new Error(
1983
+ "No WebSocket routes registered. Use app.ws(path, handler) before calling wsReq()."
1984
+ );
1985
+ }
1986
+ this.wsServer = serve(this.router.handler(), {
1987
+ websocket: wsHandler
1988
+ });
1989
+ await this.wsServer.ready;
1990
+ return `http://localhost:${this.wsServer.port}`;
1991
+ }
1992
+ /**
1993
+ * Internal: register a WS connection for cleanup.
1994
+ */
1995
+ /* @internal */
1996
+ _trackConnection(conn) {
1997
+ this.wsConnections.push(conn);
1998
+ }
1999
+ /**
2000
+ * Cleanup all WebSocket connections and stop the server.
2001
+ */
2002
+ async close() {
2003
+ for (const conn of this.wsConnections) {
2004
+ try {
2005
+ conn.close();
2006
+ } catch {
2007
+ }
2008
+ }
2009
+ this.wsConnections = [];
2010
+ if (this.wsServer) {
2011
+ this.wsServer.stop();
2012
+ this.wsServer = null;
2013
+ }
2014
+ }
2015
+ };
2016
+ var TestWSRequest = class {
2017
+ app;
2018
+ path;
2019
+ _timeout = 5e3;
2020
+ constructor(app, path2) {
2021
+ this.app = app;
2022
+ this.path = path2;
2023
+ }
2024
+ /** Set the timeout for operations (default: 5000ms). */
2025
+ timeout(ms) {
2026
+ this._timeout = ms;
2027
+ return this;
2028
+ }
2029
+ /**
2030
+ * Connect to the WebSocket endpoint.
2031
+ * Starts a real HTTP server (random port) if not already running.
2032
+ */
2033
+ async connect() {
2034
+ const baseUrl = await this.app._ensureServer();
2035
+ const wsUrl = baseUrl.replace(/^http/, "ws") + this.path;
2036
+ const ws = new WSWebSocket(wsUrl, { handshakeTimeout: this._timeout });
2037
+ return new Promise((resolve16, reject) => {
2038
+ const timer = setTimeout(() => {
2039
+ reject(new Error(`WebSocket connection timed out after ${this._timeout}ms`));
2040
+ ws.close();
2041
+ }, this._timeout);
2042
+ ws.on("open", () => {
2043
+ clearTimeout(timer);
2044
+ const conn = new TestWSConnection(ws, this._timeout);
2045
+ this.app._trackConnection(conn);
2046
+ resolve16(conn);
2047
+ });
2048
+ ws.on("error", (err) => {
2049
+ clearTimeout(timer);
2050
+ reject(new Error(`WebSocket connection error: ${err.message}`));
2051
+ });
2052
+ ws.on("unexpected-response", (_req, res) => {
2053
+ clearTimeout(timer);
2054
+ let body = "";
2055
+ res.on("data", (chunk) => {
2056
+ body += chunk.toString();
2057
+ });
2058
+ res.on("end", () => {
2059
+ reject(new Error(`WebSocket upgrade rejected (${res.statusCode}): ${body.slice(0, 200)}`));
2060
+ });
2061
+ });
2062
+ });
2063
+ }
2064
+ };
2065
+ var TestWSConnection = class {
2066
+ ws;
2067
+ _timeout;
2068
+ messageQueue = [];
2069
+ resolveQueue = [];
2070
+ _closed = false;
2071
+ constructor(ws, timeout = 5e3) {
2072
+ this.ws = ws;
2073
+ this._timeout = timeout;
2074
+ ws.on("message", (data) => {
2075
+ const str = data.toString();
2076
+ if (this.resolveQueue.length > 0) {
2077
+ const resolve16 = this.resolveQueue.shift();
2078
+ resolve16(str);
2079
+ } else {
2080
+ this.messageQueue.push(str);
2081
+ }
2082
+ });
2083
+ ws.on("close", () => {
2084
+ this._closed = true;
2085
+ for (const _r of this.resolveQueue) {
2086
+ }
2087
+ });
2088
+ }
2089
+ /** Send a text message. */
2090
+ send(data) {
2091
+ this.ws.send(data);
2092
+ }
2093
+ /** Send a JSON message. */
2094
+ json(data) {
2095
+ this.ws.send(JSON.stringify(data));
2096
+ }
2097
+ /**
2098
+ * Wait for the next message. Returns the raw text.
2099
+ * Throws on timeout or if the connection is closed.
2100
+ */
2101
+ async receive(timeout) {
2102
+ if (this.messageQueue.length > 0) {
2103
+ return this.messageQueue.shift();
2104
+ }
2105
+ if (this._closed) {
2106
+ throw new Error("WebSocket connection closed");
2107
+ }
2108
+ return new Promise((resolve16, reject) => {
2109
+ const timer = setTimeout(() => {
2110
+ const idx = this.resolveQueue.indexOf(resolve16);
2111
+ if (idx !== -1) this.resolveQueue.splice(idx, 1);
2112
+ reject(new Error(`WebSocket receive timed out after ${timeout ?? this._timeout}ms`));
2113
+ }, timeout ?? this._timeout);
2114
+ this.resolveQueue.push((msg) => {
2115
+ clearTimeout(timer);
2116
+ resolve16(msg);
2117
+ });
2118
+ });
2119
+ }
2120
+ /** Wait for the next message and parse as JSON. */
2121
+ async receiveJson() {
2122
+ const msg = await this.receive();
2123
+ return JSON.parse(msg);
2124
+ }
2125
+ /**
2126
+ * Assert that no message is received within the given silence period.
2127
+ * Useful for verifying that something did NOT happen.
2128
+ */
2129
+ async expectSilent(ms) {
2130
+ return new Promise((resolve16, reject) => {
2131
+ if (this.messageQueue.length > 0) {
2132
+ reject(new Error(`Expected silence but got message: ${this.messageQueue[0].slice(0, 100)}`));
2133
+ return;
2134
+ }
2135
+ const timer = setTimeout(() => resolve16(), ms);
2136
+ const origPush = this.resolveQueue.push.bind(this.resolveQueue);
2137
+ this.resolveQueue.push = (_fn) => {
2138
+ clearTimeout(timer);
2139
+ reject(new Error("Expected silence but received a message"));
2140
+ return 0;
2141
+ };
2142
+ setTimeout(() => {
2143
+ this.resolveQueue.push = origPush;
2144
+ }, ms + 10).unref();
2145
+ });
2146
+ }
2147
+ /** Close the connection. */
2148
+ close() {
2149
+ this._closed = true;
2150
+ this.ws.close();
2151
+ }
2152
+ /** Whether the connection is closed. */
2153
+ get closed() {
2154
+ return this._closed;
2155
+ }
1911
2156
  };
1912
2157
  function testApp() {
1913
2158
  return new TestApp();
@@ -2507,6 +2752,7 @@ function aiProvider(options) {
2507
2752
  ctx.ai = provider;
2508
2753
  return next(req, ctx);
2509
2754
  };
2755
+ mw.__meta = { injects: ["ai"], depends: [] };
2510
2756
  return Object.assign(mw, provider);
2511
2757
  }
2512
2758
 
@@ -3116,6 +3362,7 @@ function postgres(opts) {
3116
3362
  ctx.sql = sql2;
3117
3363
  return next(req, ctx);
3118
3364
  });
3365
+ mw.__meta = { injects: ["sql"], depends: [] };
3119
3366
  mw.sql = sql2;
3120
3367
  mw.table = ((tableOrSchema, builders) => {
3121
3368
  if (typeof tableOrSchema === "string") {
@@ -3192,7 +3439,7 @@ var PgModule = class {
3192
3439
  };
3193
3440
 
3194
3441
  // user/client.ts
3195
- import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
3442
+ import { randomBytes, scryptSync, timingSafeEqual, createHash } from "node:crypto";
3196
3443
  import jwt2 from "jsonwebtoken";
3197
3444
  import { z as z2 } from "zod";
3198
3445
 
@@ -3511,6 +3758,7 @@ var BUILTIN_PROVIDERS = {
3511
3758
  tokenUrl: "https://oauth2.googleapis.com/token",
3512
3759
  userUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
3513
3760
  scope: "openid email profile",
3761
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3514
3762
  parseUser: (data) => ({
3515
3763
  id: data.id,
3516
3764
  email: data.email,
@@ -3523,6 +3771,7 @@ var BUILTIN_PROVIDERS = {
3523
3771
  tokenUrl: "https://github.com/login/oauth/access_token",
3524
3772
  userUrl: "https://api.github.com/user",
3525
3773
  scope: "read:user user:email",
3774
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3526
3775
  parseUser: (data) => ({
3527
3776
  id: String(data.id),
3528
3777
  email: data.email ?? "",
@@ -3619,8 +3868,9 @@ function registerOAuthLoginRoutes(router, deps, providers) {
3619
3868
  const state = crypto5.randomUUID();
3620
3869
  const redirectUri = new URL(req.url);
3621
3870
  redirectUri.pathname = redirectUri.pathname.replace(/\/[^/]+$/, "/") + providerName + "/callback";
3622
- if (ctx.session) {
3623
- ctx.session.oauthState = { state, provider: providerName };
3871
+ const sess = ctx.session;
3872
+ if (sess) {
3873
+ sess.oauthState = { state, provider: providerName };
3624
3874
  }
3625
3875
  const scope = config.scope ?? meta.scope;
3626
3876
  const params = new URLSearchParams({
@@ -3647,11 +3897,12 @@ function registerOAuthLoginRoutes(router, deps, providers) {
3647
3897
  if (!code || !state) {
3648
3898
  return Response.json({ error: "Missing code or state parameter" }, { status: 400 });
3649
3899
  }
3650
- const savedState = ctx.session?.oauthState;
3900
+ const sess = ctx.session;
3901
+ const savedState = sess?.oauthState;
3651
3902
  if (!savedState || savedState.state !== state || savedState.provider !== providerName) {
3652
3903
  return Response.json({ error: "Invalid state \u2014 possible CSRF attack" }, { status: 403 });
3653
3904
  }
3654
- if (ctx.session) delete ctx.session.oauthState;
3905
+ if (sess) delete sess.oauthState;
3655
3906
  const redirectUri = url.origin + url.pathname.replace(/\/callback$/, "");
3656
3907
  let tokenRes;
3657
3908
  try {
@@ -3709,9 +3960,10 @@ function registerOAuthLoginRoutes(router, deps, providers) {
3709
3960
  return Response.json({ error: "Failed to create/link user" }, { status: 500 });
3710
3961
  }
3711
3962
  const token = signToken(user2);
3712
- if (ctx.session) {
3713
- ctx.session.userId = user2.id;
3714
- ctx.session.role = user2.role;
3963
+ const sess2 = ctx.session;
3964
+ if (sess2) {
3965
+ sess2.userId = user2.id;
3966
+ sess2.role = user2.role;
3715
3967
  }
3716
3968
  const accept = req.headers.get("accept") ?? "";
3717
3969
  if (accept.includes("application/json")) {
@@ -3739,6 +3991,10 @@ var LoginSchema = z2.object({
3739
3991
  email: z2.string().email(),
3740
3992
  password: z2.string().min(1)
3741
3993
  });
3994
+ var CreateApiKeySchema = z2.object({
3995
+ name: z2.string().min(1),
3996
+ scopes: z2.array(z2.string()).optional()
3997
+ });
3742
3998
  function escapeIdent2(s) {
3743
3999
  return `"${s.replace(/"/g, '""')}"`;
3744
4000
  }
@@ -3785,6 +4041,7 @@ function user(options) {
3785
4041
  const secret = options.jwtSecret;
3786
4042
  const expiresIn = options.expiresIn ?? "24h";
3787
4043
  const oauth2Enabled = options.oauth2?.server ?? false;
4044
+ const apiKeysEnabled = options.apiKeys ?? false;
3788
4045
  const base = hasDb ? new PgModule(pg) : null;
3789
4046
  const users = hasDb ? pg.table(table, {
3790
4047
  id: serial("id").primaryKey(),
@@ -3822,6 +4079,28 @@ function user(options) {
3822
4079
  ON "_auth_providers"(user_id)
3823
4080
  `);
3824
4081
  }
4082
+ if (apiKeysEnabled) {
4083
+ await _pg.sql.unsafe(`
4084
+ CREATE TABLE IF NOT EXISTS "_api_keys" (
4085
+ id SERIAL PRIMARY KEY,
4086
+ user_id INTEGER NOT NULL REFERENCES ${escapeIdent2(table)}(id) ON DELETE CASCADE,
4087
+ name TEXT NOT NULL,
4088
+ key_prefix TEXT NOT NULL,
4089
+ key_hash TEXT NOT NULL,
4090
+ scopes TEXT[] DEFAULT '{}',
4091
+ last_used_at TIMESTAMPTZ,
4092
+ expires_at TIMESTAMPTZ,
4093
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4094
+ revoked BOOLEAN DEFAULT false
4095
+ )
4096
+ `);
4097
+ await _pg.sql.unsafe(`
4098
+ CREATE INDEX IF NOT EXISTS "_api_keys_user_idx" ON "_api_keys"(user_id)
4099
+ `);
4100
+ await _pg.sql.unsafe(`
4101
+ CREATE UNIQUE INDEX IF NOT EXISTS "_api_keys_hash_idx" ON "_api_keys"(key_hash)
4102
+ `);
4103
+ }
3825
4104
  if (!oauth2Enabled) return;
3826
4105
  const clients3 = _pg.table("_oauth2_clients", {
3827
4106
  id: serial("id").primaryKey(),
@@ -3882,9 +4161,7 @@ function user(options) {
3882
4161
  const { email, password, name } = RegisterSchema.parse(data);
3883
4162
  const existing = await findByEmail(email);
3884
4163
  if (existing) {
3885
- const err = new Error("Email already registered");
3886
- err.status = 409;
3887
- throw err;
4164
+ throw new HttpError("Email already registered", 409);
3888
4165
  }
3889
4166
  const hashed = hashPassword(password);
3890
4167
  const row = await _users.insert({ email, password: hashed, name });
@@ -3897,14 +4174,10 @@ function user(options) {
3897
4174
  const { data: rows } = await _users.readMany({ email });
3898
4175
  const row = rows[0];
3899
4176
  if (!row) {
3900
- const err = new Error("Invalid email or password");
3901
- err.status = 401;
3902
- throw err;
4177
+ throw new HttpError("Invalid email or password", 401);
3903
4178
  }
3904
4179
  if (!verifyPassword(password, row.password)) {
3905
- const err = new Error("Invalid email or password");
3906
- err.status = 401;
3907
- throw err;
4180
+ throw new HttpError("Invalid email or password", 401);
3908
4181
  }
3909
4182
  const userData = row;
3910
4183
  const token = signToken(userData);
@@ -3922,8 +4195,79 @@ function user(options) {
3922
4195
  return null;
3923
4196
  }
3924
4197
  }
4198
+ function hashApiKey(key) {
4199
+ return createHash("sha256").update(key).digest("hex");
4200
+ }
4201
+ function generateApiKey() {
4202
+ const random = randomBytes(32).toString("hex");
4203
+ return `sk_live_${random}`;
4204
+ }
4205
+ async function createApiKey(userId2, name, scopes) {
4206
+ if (!hasDb) throw new Error("user(): pg required for API key management");
4207
+ const key = generateApiKey();
4208
+ const keyHash = hashApiKey(key);
4209
+ const prefix = key.slice(0, 12) + "..." + key.slice(-4);
4210
+ const [row] = await _pg.sql.unsafe(
4211
+ `INSERT INTO "_api_keys" (user_id, name, key_prefix, key_hash, scopes)
4212
+ VALUES ($1, $2, $3, $4, $5) RETURNING id`,
4213
+ [userId2, name, prefix, keyHash, scopes ?? []]
4214
+ );
4215
+ return { id: row.id, key };
4216
+ }
4217
+ async function listApiKeys(userId2) {
4218
+ if (!hasDb) return [];
4219
+ const rows = await _pg.sql.unsafe(
4220
+ `SELECT id, name, key_prefix, scopes, last_used_at, created_at, revoked
4221
+ FROM "_api_keys" WHERE user_id = $1 ORDER BY created_at DESC`,
4222
+ [userId2]
4223
+ );
4224
+ return rows.map((r2) => ({
4225
+ id: r2.id,
4226
+ name: r2.name,
4227
+ prefix: r2.key_prefix,
4228
+ scopes: Array.isArray(r2.scopes) ? r2.scopes : [],
4229
+ last_used_at: r2.last_used_at ? new Date(r2.last_used_at).toISOString() : null,
4230
+ created_at: new Date(r2.created_at).toISOString(),
4231
+ revoked: !!r2.revoked
4232
+ }));
4233
+ }
4234
+ async function revokeApiKey(userId2, keyId) {
4235
+ if (!hasDb) throw new Error("user(): pg required for API key management");
4236
+ await _pg.sql.unsafe(`UPDATE "_api_keys" SET revoked = true WHERE id = $1 AND user_id = $2`, [
4237
+ keyId,
4238
+ userId2
4239
+ ]);
4240
+ }
4241
+ async function verifyApiKey(key) {
4242
+ if (!hasDb || !apiKeysEnabled) return null;
4243
+ const keyHash = hashApiKey(key);
4244
+ const [row] = await _pg.sql.unsafe(
4245
+ `SELECT id, user_id, scopes, revoked, expires_at
4246
+ FROM "_api_keys" WHERE key_hash = $1 LIMIT 1`,
4247
+ [keyHash]
4248
+ );
4249
+ if (!row) return null;
4250
+ if (row.revoked) return null;
4251
+ if (row.expires_at && new Date(row.expires_at) < /* @__PURE__ */ new Date()) return null;
4252
+ await _pg.sql.unsafe(
4253
+ `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'`,
4254
+ [row.id]
4255
+ ).catch(() => {
4256
+ });
4257
+ return {
4258
+ userId: row.user_id,
4259
+ scopes: Array.isArray(row.scopes) ? row.scopes : []
4260
+ };
4261
+ }
4262
+ async function tryApiKeyAuth(token) {
4263
+ if (!apiKeysEnabled || !hasDb) return null;
4264
+ if (!token.startsWith("sk_")) return null;
4265
+ return verifyApiKey(token);
4266
+ }
3925
4267
  const headerName = options.header ?? "Authorization";
3926
4268
  async function resolveUser(req, ctx) {
4269
+ const _ctx2 = ctx;
4270
+ if (_ctx2.user) return _ctx2.user;
3927
4271
  const s = ctx;
3928
4272
  const sessionUserId = s.session?.userId;
3929
4273
  if (sessionUserId !== void 0 && sessionUserId !== null) {
@@ -3999,6 +4343,17 @@ function user(options) {
3999
4343
  return null;
4000
4344
  }
4001
4345
  }
4346
+ if (token.startsWith("sk_")) {
4347
+ const result = await tryApiKeyAuth(token);
4348
+ if (result) {
4349
+ if (hasDb) {
4350
+ const row = await findById(result.userId);
4351
+ if (row) return { ...stripPassword(row), _apiKeyScopes: result.scopes };
4352
+ }
4353
+ return { id: result.userId, _apiKeyScopes: result.scopes };
4354
+ }
4355
+ return null;
4356
+ }
4002
4357
  if (secret && hasDb) {
4003
4358
  try {
4004
4359
  const payload = jwt2.verify(token, secret);
@@ -4012,7 +4367,7 @@ function user(options) {
4012
4367
  return null;
4013
4368
  }
4014
4369
  function middleware() {
4015
- return async (req, ctx, next) => {
4370
+ const mw = async (req, ctx, next) => {
4016
4371
  const userData = await resolveUser(req, ctx);
4017
4372
  if (userData) {
4018
4373
  ctx.user = userData;
@@ -4023,9 +4378,11 @@ function user(options) {
4023
4378
  headers: headerName.toLowerCase() === "authorization" ? { "WWW-Authenticate": "Bearer" } : void 0
4024
4379
  });
4025
4380
  };
4381
+ mw.__meta = { injects: ["user"], depends: [] };
4382
+ return mw;
4026
4383
  }
4027
4384
  function middlewareOptional(_opts) {
4028
- return async (req, ctx, next) => {
4385
+ const mw = async (req, ctx, next) => {
4029
4386
  const userData = await resolveUser(req, ctx);
4030
4387
  if (userData) {
4031
4388
  ;
@@ -4033,6 +4390,8 @@ function user(options) {
4033
4390
  }
4034
4391
  return next(req, ctx);
4035
4392
  };
4393
+ mw.__meta = { injects: ["user"], depends: [] };
4394
+ return mw;
4036
4395
  }
4037
4396
  async function parseBody2(req) {
4038
4397
  const ct = req.headers.get("content-type") || "";
@@ -4057,8 +4416,9 @@ function user(options) {
4057
4416
  if (err instanceof z2.ZodError) {
4058
4417
  return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
4059
4418
  }
4060
- const status = err.status ?? 500;
4061
- return Response.json({ error: err.message }, { status });
4419
+ const status = err instanceof HttpError ? err.status : 500;
4420
+ const message = err instanceof Error ? err.message : String(err);
4421
+ return Response.json({ error: message }, { status });
4062
4422
  }
4063
4423
  });
4064
4424
  r.post("/login", async (req, ctx) => {
@@ -4079,10 +4439,39 @@ function user(options) {
4079
4439
  if (err instanceof z2.ZodError) {
4080
4440
  return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
4081
4441
  }
4082
- const status = err.status ?? 500;
4083
- return Response.json({ error: err.message }, { status });
4442
+ const status = err instanceof HttpError ? err.status : 500;
4443
+ const message = err instanceof Error ? err.message : String(err);
4444
+ return Response.json({ error: message }, { status });
4445
+ }
4446
+ });
4447
+ }
4448
+ if (apiKeysEnabled) {
4449
+ r.get("/api-keys", middleware(), async (_req, ctx) => {
4450
+ const keys = await listApiKeys(ctx.user.id);
4451
+ return Response.json(keys);
4452
+ });
4453
+ r.post("/api-keys", middleware(), async (req, ctx) => {
4454
+ try {
4455
+ const body = await parseBody2(req);
4456
+ const { name, scopes } = CreateApiKeySchema.parse(body);
4457
+ const result = await createApiKey(ctx.user.id, name, scopes);
4458
+ return Response.json(result, { status: 201 });
4459
+ } catch (err) {
4460
+ if (err instanceof z2.ZodError) {
4461
+ return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
4462
+ }
4463
+ const message = err instanceof Error ? err.message : String(err);
4464
+ return Response.json({ error: message }, { status: 500 });
4084
4465
  }
4085
4466
  });
4467
+ r.delete("/api-keys/:id", middleware(), async (req, ctx) => {
4468
+ const keyId = parseInt(ctx.params.id, 10);
4469
+ if (isNaN(keyId)) {
4470
+ return Response.json({ error: "Invalid key ID" }, { status: 400 });
4471
+ }
4472
+ await revokeApiKey(ctx.user.id, keyId);
4473
+ return Response.json({ ok: true });
4474
+ });
4086
4475
  }
4087
4476
  if (oauth2) {
4088
4477
  r.get("/oauth/authorize", (req, ctx) => oauth2.authorizeHandler(req, ctx));
@@ -4128,12 +4517,27 @@ function user(options) {
4128
4517
  mod.revokeClient = oauth2 ? (clientId) => oauth2.revokeClient(clientId) : async () => {
4129
4518
  throw new Error("OAuth2 server is not enabled");
4130
4519
  };
4520
+ mod.createApiKey = hasDb && apiKeysEnabled ? createApiKey : async () => {
4521
+ throw new Error(
4522
+ "API key management is not enabled. Pass apiKeys: true in user() options."
4523
+ );
4524
+ };
4525
+ mod.listApiKeys = hasDb && apiKeysEnabled ? listApiKeys : async () => {
4526
+ throw new Error(
4527
+ "API key management is not enabled. Pass apiKeys: true in user() options."
4528
+ );
4529
+ };
4530
+ mod.revokeApiKey = hasDb && apiKeysEnabled ? revokeApiKey : async () => {
4531
+ throw new Error(
4532
+ "API key management is not enabled. Pass apiKeys: true in user() options."
4533
+ );
4534
+ };
4131
4535
  mod.close = hasDb ? () => base.close() : async () => {
4132
4536
  };
4133
4537
  return mod;
4134
4538
  }
4135
4539
 
4136
- // redis/index.ts
4540
+ // redis/client.ts
4137
4541
  import { Redis as IORedis } from "ioredis";
4138
4542
  function redis(opts) {
4139
4543
  const options = typeof opts === "string" ? { url: opts } : opts ?? {};
@@ -4144,6 +4548,7 @@ function redis(opts) {
4144
4548
  ctx.redis = client;
4145
4549
  return next(req, ctx);
4146
4550
  });
4551
+ mw.__meta = { injects: ["redis"], depends: [] };
4147
4552
  mw.redis = client;
4148
4553
  mw.close = () => client.quit();
4149
4554
  return mw;
@@ -4328,15 +4733,12 @@ function createMemoryQueue(opts) {
4328
4733
  running = true;
4329
4734
  poll();
4330
4735
  };
4331
- mw.stop = function stop2() {
4736
+ mw.close = async function close() {
4332
4737
  running = false;
4333
4738
  if (pollTimer) {
4334
4739
  clearTimeout(pollTimer);
4335
4740
  pollTimer = null;
4336
4741
  }
4337
- };
4338
- mw.close = async function close() {
4339
- mw.stop();
4340
4742
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
4341
4743
  };
4342
4744
  mw.jobs = async function(limit) {
@@ -4501,15 +4903,12 @@ function createPgQueue(opts) {
4501
4903
  running = true;
4502
4904
  poll();
4503
4905
  };
4504
- mw.stop = function stop2() {
4906
+ mw.close = async function close() {
4505
4907
  running = false;
4506
4908
  if (pollTimer) {
4507
4909
  clearTimeout(pollTimer);
4508
4910
  pollTimer = null;
4509
4911
  }
4510
- };
4511
- mw.close = async function close() {
4512
- mw.stop();
4513
4912
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
4514
4913
  };
4515
4914
  mw.jobs = async function jobs(limit) {
@@ -4668,16 +5067,13 @@ function createRedisQueue(opts) {
4668
5067
  running = true;
4669
5068
  poll();
4670
5069
  };
4671
- mw.stop = function stop2() {
5070
+ mw.close = async function close() {
4672
5071
  running = false;
4673
5072
  epoch++;
4674
5073
  if (pollTimer) {
4675
5074
  clearTimeout(pollTimer);
4676
5075
  pollTimer = null;
4677
5076
  }
4678
- };
4679
- mw.close = async function close() {
4680
- mw.stop();
4681
5077
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
4682
5078
  redis2.disconnect();
4683
5079
  };
@@ -5675,7 +6071,7 @@ function tenant(options) {
5675
6071
  );
5676
6072
  }
5677
6073
  function middleware() {
5678
- return async (req, ctx, next) => {
6074
+ const mw = async (req, ctx, next) => {
5679
6075
  const user2 = ctx.user;
5680
6076
  if (!user2) {
5681
6077
  return new Response("Unauthorized", { status: 401 });
@@ -5711,6 +6107,8 @@ function tenant(options) {
5711
6107
  ctx.tenant = { id: member.id, name: member.name, role: member.role };
5712
6108
  return next(req, ctx);
5713
6109
  };
6110
+ mw.__meta = { injects: ["tenant"], depends: ["user"] };
6111
+ return mw;
5714
6112
  }
5715
6113
  const r = buildRouter(sql2, usersTable);
5716
6114
  const mod = r;
@@ -5789,17 +6187,13 @@ function buildRouter2(deps) {
5789
6187
  if (!body.input && !body.messages) {
5790
6188
  return Response.json({ error: "input or messages is required" }, { status: 400 });
5791
6189
  }
5792
- try {
5793
- const result = await runner.run(id2, body);
5794
- if ("stream" in result) {
5795
- return new Response(result.stream, {
5796
- headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }
5797
- });
5798
- }
5799
- return Response.json(result);
5800
- } catch (err) {
5801
- return Response.json({ error: err.message }, { status: 500 });
6190
+ const result = await runner.run(id2, body);
6191
+ if ("stream" in result) {
6192
+ return new Response(result.stream, {
6193
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }
6194
+ });
5802
6195
  }
6196
+ return Response.json(result);
5803
6197
  });
5804
6198
  r.get("/agents/:id/runs", async (_req, ctx) => {
5805
6199
  const agentId = parseInt(ctx.params.id, 10);
@@ -5830,13 +6224,26 @@ function buildRouter2(deps) {
5830
6224
  { orderBy: { created_at: "desc" } }
5831
6225
  );
5832
6226
  const total = rows.length;
5833
- const success = rows.filter((r2) => r2.status === "success" || r2.status === "stream").length;
6227
+ const success = rows.filter(
6228
+ (r2) => r2.status === "success" || r2.status === "stream"
6229
+ ).length;
5834
6230
  const error = rows.filter((r2) => r2.status === "error").length;
5835
- const totalTokensIn = rows.reduce((sum, r2) => sum + (r2.tokens_in || 0), 0);
5836
- const totalTokensOut = rows.reduce((sum, r2) => sum + (r2.tokens_out || 0), 0);
5837
- const totalElapsed = rows.reduce((sum, r2) => sum + (r2.elapsed_ms || 0), 0);
6231
+ const totalTokensIn = rows.reduce(
6232
+ (sum, r2) => sum + (r2.tokens_in || 0),
6233
+ 0
6234
+ );
6235
+ const totalTokensOut = rows.reduce(
6236
+ (sum, r2) => sum + (r2.tokens_out || 0),
6237
+ 0
6238
+ );
6239
+ const totalElapsed = rows.reduce(
6240
+ (sum, r2) => sum + (r2.elapsed_ms || 0),
6241
+ 0
6242
+ );
5838
6243
  const avgElapsed = total > 0 ? Math.round(totalElapsed / total) : 0;
5839
- const sorted = [...rows].sort((a, b) => (a.elapsed_ms || 0) - (b.elapsed_ms || 0));
6244
+ const sorted = [...rows].sort(
6245
+ (a, b) => (a.elapsed_ms || 0) - (b.elapsed_ms || 0)
6246
+ );
5840
6247
  const p95Idx = Math.ceil(sorted.length * 0.95) - 1;
5841
6248
  const p95Elapsed = sorted.length > 0 ? sorted[p95Idx]?.elapsed_ms || 0 : 0;
5842
6249
  return Response.json({
@@ -5862,7 +6269,10 @@ function buildRouter2(deps) {
5862
6269
  const doc = await runner.addKnowledge(agentId, body.title || "", body.content);
5863
6270
  return Response.json(doc, { status: 201 });
5864
6271
  } catch (err) {
5865
- return Response.json({ error: err.message }, { status: 500 });
6272
+ return Response.json(
6273
+ { error: err instanceof Error ? err.message : String(err) },
6274
+ { status: 500 }
6275
+ );
5866
6276
  }
5867
6277
  });
5868
6278
  r.get("/agents/:id/knowledge", async (_req, ctx) => {
@@ -5904,6 +6314,7 @@ function chunkContent(content, chunkSize, overlap) {
5904
6314
  // agent/run.ts
5905
6315
  function hasKnowledgeDocs(sql2, agentId) {
5906
6316
  return sql2`SELECT 1 FROM "_knowledge_documents" WHERE agent_id = ${agentId} LIMIT 1`.then(
6317
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5907
6318
  (r) => r.length > 0
5908
6319
  );
5909
6320
  }
@@ -5914,7 +6325,12 @@ async function searchKnowledge(sql2, provider, agentId, query, limit = 5) {
5914
6325
  `SELECT id, title, content, metadata, embedding <=> $1::vector AS _score FROM "_knowledge_documents" WHERE agent_id = $2 ORDER BY embedding <=> $1::vector LIMIT $3`,
5915
6326
  [vec, agentId, limit]
5916
6327
  );
5917
- return docs.map((d) => ({ id: d.id, title: d.title, content: d.content, score: d._score }));
6328
+ return docs.map((d) => ({
6329
+ id: d.id,
6330
+ title: d.title,
6331
+ content: d.content,
6332
+ score: d._score
6333
+ }));
5918
6334
  }
5919
6335
  async function loadAgent(agents, agentId) {
5920
6336
  const row = await agents.read(agentId);
@@ -6993,7 +7409,7 @@ import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
6993
7409
 
6994
7410
  // ssr.ts
6995
7411
  import { createElement as createElement3 } from "react";
6996
- import { createHash as createHash4 } from "node:crypto";
7412
+ import { createHash as createHash5 } from "node:crypto";
6997
7413
  import { existsSync as existsSync6, readdirSync } from "node:fs";
6998
7414
  import { readdir, stat } from "node:fs/promises";
6999
7415
  import { dirname as dirname4, join as join5, resolve as resolve8, relative as relative3 } from "node:path";
@@ -7004,7 +7420,7 @@ import * as esbuild2 from "esbuild";
7004
7420
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3 } from "node:fs";
7005
7421
  import { join as join2, resolve as resolve4, dirname as dirname2 } from "node:path";
7006
7422
  import { pathToFileURL } from "node:url";
7007
- import { createHash } from "node:crypto";
7423
+ import { createHash as createHash2 } from "node:crypto";
7008
7424
  import { createRequire as createRequire2 } from "node:module";
7009
7425
 
7010
7426
  // server-registry.ts
@@ -7195,7 +7611,7 @@ function resolveAliases2() {
7195
7611
  return {};
7196
7612
  }
7197
7613
  function id(s) {
7198
- return createHash("md5").update(s).digest("hex").slice(0, 8);
7614
+ return createHash2("md5").update(s).digest("hex").slice(0, 8);
7199
7615
  }
7200
7616
  function clearCompileCache() {
7201
7617
  cache.clear();
@@ -7344,11 +7760,12 @@ function buildHeadPayload(opts) {
7344
7760
  flash: ctx.flash,
7345
7761
  loaderData
7346
7762
  };
7347
- if (ctx.user && typeof ctx.user === "object") {
7763
+ const rawUser = ctx.user;
7764
+ if (rawUser && typeof rawUser === "object") {
7348
7765
  const safeUser = {};
7349
7766
  for (const k of ["id", "name", "email", "role", "avatar"]) {
7350
- if (k in ctx.user) {
7351
- safeUser[k] = ctx.user[k];
7767
+ if (k in rawUser) {
7768
+ safeUser[k] = rawUser[k];
7352
7769
  }
7353
7770
  }
7354
7771
  ctxData.user = safeUser;
@@ -7479,7 +7896,7 @@ ws.onclose=function(){
7479
7896
  var ssrEntries = /* @__PURE__ */ new Map();
7480
7897
 
7481
7898
  // tailwind.ts
7482
- import { createHash as createHash2 } from "node:crypto";
7899
+ import { createHash as createHash3 } from "node:crypto";
7483
7900
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "node:fs";
7484
7901
  import { join as join3, relative, resolve as resolve5 } from "node:path";
7485
7902
  var extraSources = /* @__PURE__ */ new Set();
@@ -7531,7 +7948,7 @@ ${src}`;
7531
7948
  ${src}`;
7532
7949
  }
7533
7950
  const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
7534
- const hash = createHash2("md5").update(result.css).digest("hex").slice(0, 8);
7951
+ const hash = createHash3("md5").update(result.css).digest("hex").slice(0, 8);
7535
7952
  cssCache.set(cssPath, { css: result.css, hash });
7536
7953
  return result.css;
7537
7954
  } catch (err) {
@@ -7549,7 +7966,7 @@ import { join as join4, resolve as resolve7 } from "node:path";
7549
7966
  import * as esbuild3 from "esbuild";
7550
7967
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
7551
7968
  import { resolve as resolve6, dirname as dirname3, relative as relative2 } from "node:path";
7552
- import { createHash as createHash3 } from "node:crypto";
7969
+ import { createHash as createHash4 } from "node:crypto";
7553
7970
  var moduleCache = /* @__PURE__ */ new Map();
7554
7971
  var hashCache = /* @__PURE__ */ new Map();
7555
7972
  function clearModuleCache(filePath) {
@@ -7573,7 +7990,7 @@ function fileHash(absPath) {
7573
7990
  if (cached) return cached;
7574
7991
  try {
7575
7992
  const content = readFileSync5(absPath);
7576
- const h = createHash3("md5").update(content).digest("hex").slice(0, 8);
7993
+ const h = createHash4("md5").update(content).digest("hex").slice(0, 8);
7577
7994
  hashCache.set(absPath, h);
7578
7995
  return h;
7579
7996
  } catch {
@@ -7852,7 +8269,7 @@ var isDev2 = isDev();
7852
8269
  var als2 = new AsyncLocalStorage2();
7853
8270
  __registerAls(() => als2.getStore());
7854
8271
  function hashId(s) {
7855
- return createHash4("md5").update(s).digest("hex").slice(0, 8);
8272
+ return createHash5("md5").update(s).digest("hex").slice(0, 8);
7856
8273
  }
7857
8274
  function serializeLoaderData(ctx) {
7858
8275
  const ld = ctx.loaderData;
@@ -8860,17 +9277,17 @@ async function buildRouter4(deps) {
8860
9277
  FROM "_opencode_messages"
8861
9278
  WHERE session_id = ${sessionId}
8862
9279
  `;
8863
- const stats = rows[0] || {
9280
+ const raw = rows[0] || {
8864
9281
  message_count: 0,
8865
9282
  total_tokens_in: 0,
8866
9283
  total_tokens_out: 0
8867
9284
  };
8868
9285
  return Response.json({
8869
9286
  session_id: sessionId,
8870
- message_count: stats.message_count,
8871
- tokens_in: stats.total_tokens_in,
8872
- tokens_out: stats.total_tokens_out,
8873
- tokens_total: stats.total_tokens_in + stats.total_tokens_out
9287
+ message_count: raw.message_count,
9288
+ tokens_in: raw.total_tokens_in,
9289
+ tokens_out: raw.total_tokens_out,
9290
+ tokens_total: Number(raw.total_tokens_in) + Number(raw.total_tokens_out)
8874
9291
  });
8875
9292
  });
8876
9293
  try {
@@ -8926,8 +9343,13 @@ function createWSHandler2(deps) {
8926
9343
  client.mountPath
8927
9344
  );
8928
9345
  ws.send(JSON.stringify({ type: "session_created", session: session2 }));
8929
- } catch (e) {
8930
- ws.send(JSON.stringify({ type: "error", error: e.message }));
9346
+ } catch (err) {
9347
+ ws.send(
9348
+ JSON.stringify({
9349
+ type: "error",
9350
+ error: err instanceof Error ? err.message : String(err)
9351
+ })
9352
+ );
8931
9353
  }
8932
9354
  break;
8933
9355
  }
@@ -8981,9 +9403,9 @@ function createWSHandler2(deps) {
8981
9403
  break;
8982
9404
  }
8983
9405
  }
8984
- } catch (e) {
8985
- if (e.name !== "AbortError") {
8986
- ws.send(JSON.stringify({ type: "error", error: e.message }));
9406
+ } catch (err) {
9407
+ if (err instanceof Error && err.name !== "AbortError") {
9408
+ ws.send(JSON.stringify({ type: "error", error: err.message }));
8987
9409
  }
8988
9410
  }
8989
9411
  break;
@@ -9466,6 +9888,7 @@ function theme(options) {
9466
9888
  };
9467
9889
  return next(req, ctx);
9468
9890
  };
9891
+ mw.__meta = { injects: ["theme"], depends: [] };
9469
9892
  class ThemeRouter extends Router {
9470
9893
  middleware() {
9471
9894
  return mw;
@@ -9567,6 +9990,7 @@ function i18n(options) {
9567
9990
  };
9568
9991
  return next(req, ctx);
9569
9992
  };
9993
+ mw.__meta = { injects: ["i18n"], depends: [] };
9570
9994
  class I18nRouter extends Router {
9571
9995
  middleware() {
9572
9996
  return mw;
@@ -9611,7 +10035,7 @@ function makeSetFlash(name, location) {
9611
10035
  }
9612
10036
  function flash(options) {
9613
10037
  const name = options?.name ?? "flash";
9614
- return async (req, ctx, next) => {
10038
+ const mw = async (req, ctx, next) => {
9615
10039
  const raw = getCookies(req)[name] ?? null;
9616
10040
  const referer = req.headers.get("referer") || "/";
9617
10041
  let value = void 0;
@@ -9634,6 +10058,8 @@ function flash(options) {
9634
10058
  }
9635
10059
  return res;
9636
10060
  };
10061
+ mw.__meta = { injects: ["flash"], depends: [] };
10062
+ return mw;
9637
10063
  }
9638
10064
 
9639
10065
  // seo.ts
@@ -9816,7 +10242,7 @@ function csrf(options) {
9816
10242
  const headerName = options?.header ?? "x-csrf-token";
9817
10243
  const bodyKey = options?.key ?? "_csrf";
9818
10244
  const excluded = new Set(options?.excludeMethods ?? ["GET", "HEAD", "OPTIONS"]);
9819
- return async (req, ctx, next) => {
10245
+ const mw = async (req, ctx, next) => {
9820
10246
  const method = req.method.toUpperCase();
9821
10247
  if (excluded.has(method)) {
9822
10248
  let token = getCookies(req)[cookieName];
@@ -9853,6 +10279,8 @@ function csrf(options) {
9853
10279
  }
9854
10280
  return next(req, ctx);
9855
10281
  };
10282
+ mw.__meta = { injects: ["csrf"], depends: [] };
10283
+ return mw;
9856
10284
  }
9857
10285
 
9858
10286
  // logdb/rest.ts
@@ -10016,381 +10444,6 @@ function logdb(options) {
10016
10444
  // iii/client.ts
10017
10445
  import crypto8 from "node:crypto";
10018
10446
 
10019
- // iii/stream.ts
10020
- function notify(channels, stream, group, item, event, data) {
10021
- const keys = [`${stream}`, `${stream}:${group}`, `${stream}:${group}:${item}`];
10022
- const msg = JSON.stringify({
10023
- type: "stream",
10024
- stream_name: stream,
10025
- group_id: group,
10026
- item_id: item,
10027
- event,
10028
- data
10029
- });
10030
- for (const key of keys) {
10031
- const subs = channels.get(key);
10032
- if (!subs) continue;
10033
- for (const ws of subs) {
10034
- try {
10035
- ws.send(msg);
10036
- } catch {
10037
- }
10038
- }
10039
- }
10040
- }
10041
- function deepClone(v) {
10042
- return JSON.parse(JSON.stringify(v));
10043
- }
10044
- function applyOps(value, ops) {
10045
- let current = deepClone(value ?? {});
10046
- for (const op2 of ops) {
10047
- switch (op2.op) {
10048
- case "set":
10049
- current = deepClone(op2.value);
10050
- break;
10051
- case "merge":
10052
- if (typeof current === "object" && current !== null && !Array.isArray(current)) {
10053
- current = { ...current, ...deepClone(op2.value) };
10054
- } else {
10055
- current = deepClone(op2.value);
10056
- }
10057
- break;
10058
- case "increment":
10059
- current = (typeof current === "number" ? current : 0) + op2.value;
10060
- break;
10061
- case "decrement":
10062
- current = (typeof current === "number" ? current : 0) - op2.value;
10063
- break;
10064
- case "append":
10065
- if (!Array.isArray(current)) current = [];
10066
- current.push(deepClone(op2.value));
10067
- break;
10068
- case "remove":
10069
- current = null;
10070
- break;
10071
- }
10072
- }
10073
- return current;
10074
- }
10075
- function createMemoryStore(channels) {
10076
- const store2 = /* @__PURE__ */ new Map();
10077
- function key(stream, group, item) {
10078
- return `${stream}:${group}:${item}`;
10079
- }
10080
- return {
10081
- async set(stream, group, item, data) {
10082
- const k = key(stream, group, item);
10083
- const old = store2.get(k) ?? null;
10084
- store2.set(k, deepClone(data));
10085
- notify(channels, stream, group, item, "set", data);
10086
- return { old_value: old, new_value: deepClone(data) };
10087
- },
10088
- async get(stream, group, item) {
10089
- const v = store2.get(key(stream, group, item)) ?? null;
10090
- return { value: deepClone(v) };
10091
- },
10092
- async delete(stream, group, item) {
10093
- const k = key(stream, group, item);
10094
- const old = store2.get(k) ?? null;
10095
- store2.delete(k);
10096
- notify(channels, stream, group, item, "delete", null);
10097
- return { old_value: old };
10098
- },
10099
- async list(stream, group) {
10100
- const items = [];
10101
- const prefix = `${stream}:${group}:`;
10102
- for (const [k, v] of store2) {
10103
- if (k.startsWith(prefix) && !k.slice(prefix.length).includes(":")) {
10104
- items.push({ item_id: k.slice(prefix.length), data: deepClone(v) });
10105
- }
10106
- }
10107
- return { items };
10108
- },
10109
- async list_groups(stream) {
10110
- const groups = /* @__PURE__ */ new Set();
10111
- const prefix = `${stream}:`;
10112
- for (const k of store2.keys()) {
10113
- if (k.startsWith(prefix)) {
10114
- const rest = k.slice(prefix.length);
10115
- const g = rest.split(":")[0];
10116
- if (g) groups.add(g);
10117
- }
10118
- }
10119
- return { groups: Array.from(groups) };
10120
- },
10121
- async list_all() {
10122
- const streamMap = /* @__PURE__ */ new Map();
10123
- for (const k of store2.keys()) {
10124
- const parts = k.split(":");
10125
- const s = parts[0];
10126
- const g = parts[1];
10127
- if (!streamMap.has(s)) streamMap.set(s, { groups: /* @__PURE__ */ new Set(), items: /* @__PURE__ */ new Set() });
10128
- const entry = streamMap.get(s);
10129
- if (g) entry.groups.add(g);
10130
- entry.items.add(k);
10131
- }
10132
- const streams = Array.from(streamMap.entries()).map(([name, info]) => ({
10133
- stream_name: name,
10134
- group_count: info.groups.size,
10135
- item_count: info.items.size
10136
- }));
10137
- return { streams, count: streams.length };
10138
- },
10139
- async send(stream, group, type, data, id2) {
10140
- notify(channels, stream, group, id2 ?? "", "send", { type, data });
10141
- },
10142
- async update(stream, group, item, ops) {
10143
- const k = key(stream, group, item);
10144
- const old = deepClone(store2.get(k) ?? null);
10145
- const newVal = applyOps(old, ops);
10146
- store2.set(k, deepClone(newVal));
10147
- notify(channels, stream, group, item, "update", newVal);
10148
- return { old_value: old, new_value: deepClone(newVal) };
10149
- }
10150
- };
10151
- }
10152
- function createPgStore(channels, pg) {
10153
- const sql2 = pg.sql;
10154
- return {
10155
- async set(stream, group, item, data) {
10156
- const rows = await sql2`
10157
- INSERT INTO "_iii_stream" (stream_name, group_id, item_id, data)
10158
- VALUES (${stream}, ${group}, ${item}, ${data})
10159
- ON CONFLICT (stream_name, group_id, item_id)
10160
- DO UPDATE SET data = ${data}, updated_at = NOW()
10161
- RETURNING data
10162
- `;
10163
- notify(channels, stream, group, item, "set", data);
10164
- return { old_value: null, new_value: data };
10165
- },
10166
- async get(stream, group, item) {
10167
- const rows = await sql2`
10168
- SELECT data FROM "_iii_stream"
10169
- WHERE stream_name = ${stream} AND group_id = ${group} AND item_id = ${item}
10170
- `;
10171
- const row = rows[0];
10172
- let value = row?.data ?? null;
10173
- if (typeof value === "string") value = JSON.parse(value);
10174
- return { value };
10175
- },
10176
- async delete(stream, group, item) {
10177
- const rows = await sql2`
10178
- DELETE FROM "_iii_stream"
10179
- WHERE stream_name = ${stream} AND group_id = ${group} AND item_id = ${item}
10180
- RETURNING data
10181
- `;
10182
- const old = rows[0]?.data ?? null;
10183
- notify(channels, stream, group, item, "delete", null);
10184
- return { old_value: old };
10185
- },
10186
- async list(stream, group) {
10187
- const rows = await sql2`
10188
- SELECT item_id, data FROM "_iii_stream"
10189
- WHERE stream_name = ${stream} AND group_id = ${group}
10190
- ORDER BY item_id
10191
- `;
10192
- const items = rows.map((r) => ({
10193
- item_id: r.item_id,
10194
- data: typeof r.data === "string" ? JSON.parse(r.data) : r.data
10195
- }));
10196
- return { items };
10197
- },
10198
- async list_groups(stream) {
10199
- const rows = await sql2`
10200
- SELECT DISTINCT group_id FROM "_iii_stream"
10201
- WHERE stream_name = ${stream}
10202
- ORDER BY group_id
10203
- `;
10204
- return { groups: rows.map((r) => r.group_id) };
10205
- },
10206
- async list_all() {
10207
- const rows = await sql2`
10208
- SELECT stream_name, COUNT(DISTINCT group_id) as group_count, COUNT(*) as item_count
10209
- FROM "_iii_stream"
10210
- GROUP BY stream_name
10211
- ORDER BY stream_name
10212
- `;
10213
- const streams = rows.map((r) => ({
10214
- stream_name: r.stream_name,
10215
- group_count: Number(r.group_count),
10216
- item_count: Number(r.item_count)
10217
- }));
10218
- return { streams, count: streams.length };
10219
- },
10220
- async send(stream, group, type, data, id2) {
10221
- notify(channels, stream, group, id2 ?? "", "send", { type, data });
10222
- },
10223
- async update(stream, group, item, ops) {
10224
- const { value: oldVal } = await this.get(stream, group, item);
10225
- const newVal = applyOps(oldVal, ops);
10226
- await sql2`
10227
- INSERT INTO "_iii_stream" (stream_name, group_id, item_id, data)
10228
- VALUES (${stream}, ${group}, ${item}, ${newVal})
10229
- ON CONFLICT (stream_name, group_id, item_id)
10230
- DO UPDATE SET data = ${newVal}, updated_at = NOW()
10231
- `;
10232
- notify(channels, stream, group, item, "update", newVal);
10233
- return { old_value: oldVal, new_value: deepClone(newVal) };
10234
- }
10235
- };
10236
- }
10237
- function createRedisStore(channels, redis2, ttl) {
10238
- function hashKey(stream, group) {
10239
- return `iii:stream:${stream}:${group}`;
10240
- }
10241
- function setTTL(hk) {
10242
- if (ttl) redis2.expire(hk, ttl);
10243
- }
10244
- return {
10245
- async set(stream, group, item, data) {
10246
- const hk = hashKey(stream, group);
10247
- const oldRaw = await redis2.hget(hk, item);
10248
- const old = oldRaw ? JSON.parse(oldRaw) : null;
10249
- await redis2.hset(hk, item, JSON.stringify(data));
10250
- setTTL(hk);
10251
- await redis2.publish(
10252
- `iii:stream:${stream}`,
10253
- JSON.stringify({ event: "set", group, item, data })
10254
- );
10255
- notify(channels, stream, group, item, "set", data);
10256
- return { old_value: old, new_value: deepClone(data) };
10257
- },
10258
- async get(stream, group, item) {
10259
- const raw = await redis2.hget(hashKey(stream, group), item);
10260
- return { value: raw ? JSON.parse(raw) : null };
10261
- },
10262
- async delete(stream, group, item) {
10263
- const hk = hashKey(stream, group);
10264
- const oldRaw = await redis2.hget(hk, item);
10265
- const old = oldRaw ? JSON.parse(oldRaw) : null;
10266
- await redis2.hdel(hk, item);
10267
- const remaining = await redis2.hlen(hk);
10268
- if (remaining === 0) await redis2.del(hk);
10269
- await redis2.publish(`iii:stream:${stream}`, JSON.stringify({ event: "delete", group, item }));
10270
- notify(channels, stream, group, item, "delete", null);
10271
- return { old_value: old };
10272
- },
10273
- async list(stream, group) {
10274
- const raw = await redis2.hgetall(hashKey(stream, group));
10275
- const items = Object.entries(raw).map(([item_id, data]) => ({
10276
- item_id,
10277
- data: JSON.parse(data)
10278
- }));
10279
- return { items };
10280
- },
10281
- async list_groups(stream) {
10282
- const pattern = `iii:stream:${stream}:*`;
10283
- let cursor = "0";
10284
- const groups = /* @__PURE__ */ new Set();
10285
- do {
10286
- const [next, keys] = await redis2.scan(cursor, "MATCH", pattern, "COUNT", "1000");
10287
- cursor = next;
10288
- for (const k of keys) {
10289
- const parts = k.split(":");
10290
- const g = parts.slice(3).join(":");
10291
- if (g) groups.add(g);
10292
- }
10293
- } while (cursor !== "0");
10294
- return { groups: Array.from(groups) };
10295
- },
10296
- async list_all() {
10297
- const pattern = "iii:stream:*";
10298
- let cursor = "0";
10299
- const streamMap = /* @__PURE__ */ new Map();
10300
- do {
10301
- const [next, keys] = await redis2.scan(cursor, "MATCH", pattern, "COUNT", "1000");
10302
- cursor = next;
10303
- for (const k of keys) {
10304
- const parts = k.split(":");
10305
- const s = parts[2];
10306
- const g = parts.slice(3).join(":");
10307
- if (!streamMap.has(s)) streamMap.set(s, { groups: /* @__PURE__ */ new Set(), items: 0 });
10308
- const entry = streamMap.get(s);
10309
- if (g) entry.groups.add(g);
10310
- entry.items++;
10311
- }
10312
- } while (cursor !== "0");
10313
- const streams = Array.from(streamMap.entries()).map(([name, info]) => ({
10314
- stream_name: name,
10315
- group_count: info.groups.size,
10316
- item_count: info.items
10317
- }));
10318
- return { streams, count: streams.length };
10319
- },
10320
- async send(stream, group, type, data, id2) {
10321
- notify(channels, stream, group, id2 ?? "", "send", { type, data });
10322
- },
10323
- async update(stream, group, item, ops) {
10324
- const hk = hashKey(stream, group);
10325
- const oldRaw = await redis2.hget(hk, item);
10326
- const old = oldRaw ? JSON.parse(oldRaw) : null;
10327
- const newVal = applyOps(old, ops);
10328
- await redis2.hset(hk, item, JSON.stringify(newVal));
10329
- setTTL(hk);
10330
- await redis2.publish(
10331
- `iii:stream:${stream}`,
10332
- JSON.stringify({ event: "update", group, item, data: newVal })
10333
- );
10334
- notify(channels, stream, group, item, "update", newVal);
10335
- return { old_value: old, new_value: deepClone(newVal) };
10336
- }
10337
- };
10338
- }
10339
- function createStream(opts) {
10340
- const channels = /* @__PURE__ */ new Map();
10341
- const store2 = opts?.pg ? createPgStore(channels, opts.pg) : opts?.redis ? createRedisStore(channels, opts.redis, opts.streamTTL ?? 3600) : createMemoryStore(channels);
10342
- let redisSub = null;
10343
- if (opts?.redis) {
10344
- redisSub = opts.redis.duplicate();
10345
- redisSub.on("message", (rawChannel, rawData) => {
10346
- if (!rawChannel.startsWith("iii:stream:")) return;
10347
- const stream = rawChannel.slice("iii:stream:".length);
10348
- try {
10349
- const msg = JSON.parse(rawData);
10350
- if (msg.event === "set" || msg.event === "update") {
10351
- notify(channels, stream, msg.group, msg.item, msg.event, msg.data);
10352
- } else if (msg.event === "delete") {
10353
- notify(channels, stream, msg.group, msg.item, "delete", null);
10354
- }
10355
- } catch {
10356
- }
10357
- });
10358
- }
10359
- return {
10360
- ...store2,
10361
- subscribe(ws, sub) {
10362
- 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;
10363
- if (!channels.has(key)) channels.set(key, /* @__PURE__ */ new Set());
10364
- channels.get(key).add(ws);
10365
- if (redisSub && sub.stream_name) {
10366
- redisSub.subscribe(`iii:stream:${sub.stream_name}`);
10367
- }
10368
- },
10369
- unsubscribe(ws) {
10370
- for (const [, subs] of channels) subs.delete(ws);
10371
- },
10372
- async migrate() {
10373
- if (opts?.pg) {
10374
- const sql2 = opts.pg.sql;
10375
- await sql2`
10376
- CREATE TABLE IF NOT EXISTS "_iii_stream" (
10377
- stream_name TEXT NOT NULL,
10378
- group_id TEXT NOT NULL,
10379
- item_id TEXT NOT NULL,
10380
- data JSONB,
10381
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10382
- PRIMARY KEY (stream_name, group_id, item_id)
10383
- )
10384
- `;
10385
- await sql2`CREATE INDEX IF NOT EXISTS idx_iii_stream_group ON "_iii_stream" (stream_name, group_id)`;
10386
- }
10387
- },
10388
- async close() {
10389
- if (redisSub) await redisSub.quit();
10390
- }
10391
- };
10392
- }
10393
-
10394
10447
  // iii/ws.ts
10395
10448
  function createWsHandler(deps) {
10396
10449
  const wsToWorkerId = /* @__PURE__ */ new Map();
@@ -10424,9 +10477,9 @@ function createWsHandler(deps) {
10424
10477
  const workerId = getWorkerId(ws);
10425
10478
  if (workerId) {
10426
10479
  deps.registerRemoteTrigger(workerId, {
10427
- type: msg.trigger_type || msg.type,
10428
- function_id: msg.function_id,
10429
- config: msg.config || {}
10480
+ type: msg.input?.type || "custom",
10481
+ function_id: msg.input?.function_id || msg.id,
10482
+ config: msg.input?.config || {}
10430
10483
  });
10431
10484
  }
10432
10485
  break;
@@ -10438,11 +10491,7 @@ function createWsHandler(deps) {
10438
10491
  }
10439
10492
  case "unregister_trigger": {
10440
10493
  const workerId = getWorkerId(ws);
10441
- if (workerId) deps.unregisterRemoteTrigger(workerId, msg.function_id);
10442
- break;
10443
- }
10444
- case "invoke": {
10445
- deps.handleInvoke(ws, msg.invocation_id, msg.function_id, msg.payload);
10494
+ if (workerId) deps.unregisterRemoteTrigger(workerId, msg.function_id || msg.id);
10446
10495
  break;
10447
10496
  }
10448
10497
  case "invoke_result": {
@@ -10453,31 +10502,20 @@ function createWsHandler(deps) {
10453
10502
  deps.handleInvokeError(msg.invocation_id, msg.error);
10454
10503
  break;
10455
10504
  }
10456
- case "subscribe": {
10457
- deps.addStreamSubscriber(ws, {
10458
- stream_name: msg.stream_name || msg.channel || "",
10459
- group_id: msg.group_id,
10460
- item_id: msg.item_id
10461
- });
10462
- break;
10463
- }
10464
- case "unsubscribe": {
10465
- deps.removeStreamSubscriber(ws);
10505
+ case "invoke": {
10506
+ deps.handleInvoke(ws, msg.invocation_id, msg.function_id, msg.payload);
10466
10507
  break;
10467
10508
  }
10509
+ default:
10510
+ ws.send(JSON.stringify({ type: "error", message: `Unknown message type: ${msg.type}` }));
10468
10511
  }
10469
10512
  },
10470
10513
  close(ws) {
10471
10514
  const workerId = getWorkerId(ws);
10472
- if (workerId) deps.unregisterRemoteWorker(workerId);
10473
- wsToWorkerId.delete(ws);
10474
- deps.removeStreamSubscriber(ws);
10475
- },
10476
- error(ws) {
10477
- const workerId = getWorkerId(ws);
10478
- if (workerId) deps.unregisterRemoteWorker(workerId);
10479
- wsToWorkerId.delete(ws);
10480
- deps.removeStreamSubscriber(ws);
10515
+ if (workerId) {
10516
+ deps.unregisterRemoteWorker(workerId);
10517
+ wsToWorkerId.delete(ws);
10518
+ }
10481
10519
  }
10482
10520
  };
10483
10521
  }
@@ -10522,38 +10560,11 @@ function buildRouter5(engine, wsHandler) {
10522
10560
  }
10523
10561
 
10524
10562
  // iii/client.ts
10525
- function iii(opts = {}) {
10526
- const stream = createStream({ pg: opts.pg, redis: opts.redis, streamTTL: opts.streamTTL });
10563
+ function iii(_opts = {}) {
10527
10564
  const workers = /* @__PURE__ */ new Map();
10528
10565
  const functions = /* @__PURE__ */ new Map();
10529
10566
  const triggers = /* @__PURE__ */ new Map();
10530
10567
  const pending = /* @__PURE__ */ new Map();
10531
- function registerBuiltin(id2, handler) {
10532
- functions.set(id2, {
10533
- id: id2,
10534
- handler,
10535
- workerId: "__iii__",
10536
- workerName: "__iii__",
10537
- triggers: []
10538
- });
10539
- }
10540
- registerBuiltin(
10541
- "stream::set",
10542
- (p) => stream.set(p.stream_name, p.group_id, p.item_id, p.data)
10543
- );
10544
- registerBuiltin("stream::get", (p) => stream.get(p.stream_name, p.group_id, p.item_id));
10545
- registerBuiltin("stream::delete", (p) => stream.delete(p.stream_name, p.group_id, p.item_id));
10546
- registerBuiltin("stream::list", (p) => stream.list(p.stream_name, p.group_id));
10547
- registerBuiltin("stream::list_groups", (p) => stream.list_groups(p.stream_name));
10548
- registerBuiltin("stream::list_all", () => stream.list_all());
10549
- registerBuiltin(
10550
- "stream::send",
10551
- (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id)
10552
- );
10553
- registerBuiltin(
10554
- "stream::update",
10555
- (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops)
10556
- );
10557
10568
  function addLocalWorker(worker) {
10558
10569
  const workerId = crypto8.randomUUID();
10559
10570
  const reg = {
@@ -10672,12 +10683,6 @@ function iii(opts = {}) {
10672
10683
  worker.triggers = worker.triggers.filter((t) => t.function_id !== functionId);
10673
10684
  }
10674
10685
  },
10675
- addStreamSubscriber(ws, sub) {
10676
- stream.subscribe(ws, sub);
10677
- },
10678
- removeStreamSubscriber(ws) {
10679
- stream.unsubscribe(ws);
10680
- },
10681
10686
  handleInvokeResult(invocationId, result) {
10682
10687
  const p = pending.get(invocationId);
10683
10688
  if (p) {
@@ -10730,7 +10735,7 @@ function iii(opts = {}) {
10730
10735
  }
10731
10736
  function trigger(request) {
10732
10737
  const fn = functions.get(request.function_id);
10733
- if (!fn) throw new Error(`Function "${request.function_id}" not found`);
10738
+ if (!fn) return Promise.reject(new Error(`Function "${request.function_id}" not found`));
10734
10739
  const ctx = { engine: engineRef, functionId: request.function_id, workerName: fn.workerName };
10735
10740
  if (request.action === "void") {
10736
10741
  queueMicrotask(() => fn.handler(request.payload, ctx));
@@ -10777,7 +10782,6 @@ function iii(opts = {}) {
10777
10782
  mod.listFunctions = listFunctions;
10778
10783
  mod.listTriggers = listTriggers;
10779
10784
  mod.migrate = async () => {
10780
- await stream.migrate();
10781
10785
  };
10782
10786
  mod.close = async () => {
10783
10787
  for (const [, p] of pending) {
@@ -10789,7 +10793,6 @@ function iii(opts = {}) {
10789
10793
  workers.clear();
10790
10794
  functions.clear();
10791
10795
  triggers.clear();
10792
- await stream.close();
10793
10796
  };
10794
10797
  return mod;
10795
10798
  }
@@ -10926,11 +10929,6 @@ function registerWorker(url) {
10926
10929
  }
10927
10930
  break;
10928
10931
  }
10929
- case "stream": {
10930
- const handler = handlers.get("__stream__");
10931
- if (handler) handler(msg, {});
10932
- break;
10933
- }
10934
10932
  }
10935
10933
  };
10936
10934
  ws.onclose = () => {
@@ -11003,9 +11001,6 @@ function registerWorker(url) {
11003
11001
  });
11004
11002
  });
11005
11003
  },
11006
- onStream(handler) {
11007
- handlers.set("__stream__", handler);
11008
- },
11009
11004
  close() {
11010
11005
  intentionalClose = true;
11011
11006
  if (reconnectTimer) clearTimeout(reconnectTimer);
@@ -11257,6 +11252,7 @@ function session(options) {
11257
11252
  }
11258
11253
  return res;
11259
11254
  });
11255
+ mw.__meta = { injects: ["session"], depends: [] };
11260
11256
  mw.close = async () => {
11261
11257
  await closeStore?.();
11262
11258
  };
@@ -11922,6 +11918,7 @@ function s3(options) {
11922
11918
  mw.url = url;
11923
11919
  mw.list = list;
11924
11920
  mw.client = client;
11921
+ mw.__meta = { injects: ["s3"], depends: [] };
11925
11922
  return mw;
11926
11923
  }
11927
11924
 
@@ -12184,10 +12181,481 @@ function permissions(options) {
12184
12181
  mw.requireRole = requireRole;
12185
12182
  mw.requirePermission = requirePermission;
12186
12183
  mw.migrate = migrate;
12184
+ mw.__meta = { injects: ["permissions"], depends: ["user"] };
12187
12185
  return mw;
12188
12186
  }
12187
+
12188
+ // mcp.ts
12189
+ import { spawn } from "node:child_process";
12190
+ import { createInterface } from "node:readline";
12191
+ import { z as z14 } from "zod";
12192
+ var _requestId = 0;
12193
+ function nextId() {
12194
+ return ++_requestId;
12195
+ }
12196
+ function createRequest2(id2, method, params) {
12197
+ return JSON.stringify({
12198
+ jsonrpc: "2.0",
12199
+ id: id2,
12200
+ method,
12201
+ params
12202
+ });
12203
+ }
12204
+ function mcpClient(options) {
12205
+ const { command, args = [], env: env2 } = options;
12206
+ const timeout = options.timeout ?? 15e3;
12207
+ const maxResponseSize = options.maxResponseSize ?? 10 * 1024 * 1024;
12208
+ let proc = null;
12209
+ let rl = null;
12210
+ const pending = /* @__PURE__ */ new Map();
12211
+ let _tools = null;
12212
+ function ensureProcess() {
12213
+ if (proc && !proc.killed) return;
12214
+ proc = spawn(command, args, {
12215
+ stdio: ["pipe", "pipe", "pipe"],
12216
+ env: { ...process.env, ...env2 }
12217
+ });
12218
+ rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
12219
+ let buffer = "";
12220
+ rl.on("line", (line) => {
12221
+ buffer += line;
12222
+ try {
12223
+ const msg = JSON.parse(buffer);
12224
+ buffer = "";
12225
+ handleMessage(msg);
12226
+ } catch {
12227
+ }
12228
+ });
12229
+ proc.stderr?.on("data", (chunk) => {
12230
+ const text2 = chunk.toString().trim();
12231
+ if (text2) {
12232
+ console.debug(`[mcp:${command}] stderr:`, text2);
12233
+ }
12234
+ });
12235
+ proc.on("exit", (code, signal) => {
12236
+ console.debug(`[mcp:${command}] exited (code=${code} signal=${signal})`);
12237
+ for (const [id2, { reject, timer }] of pending) {
12238
+ clearTimeout(timer);
12239
+ reject(new Error(`MCP server exited (code=${code} signal=${signal})`));
12240
+ pending.delete(id2);
12241
+ }
12242
+ proc = null;
12243
+ rl = null;
12244
+ });
12245
+ proc.on("error", (err) => {
12246
+ console.error(`[mcp:${command}] error:`, err.message);
12247
+ for (const [, { reject, timer }] of pending) {
12248
+ clearTimeout(timer);
12249
+ reject(err);
12250
+ }
12251
+ pending.clear();
12252
+ });
12253
+ }
12254
+ function handleMessage(msg) {
12255
+ if (msg.id !== void 0 && pending.has(msg.id)) {
12256
+ const { resolve: resolve16, reject, timer } = pending.get(msg.id);
12257
+ clearTimeout(timer);
12258
+ pending.delete(msg.id);
12259
+ if (msg.error) {
12260
+ reject(new Error(`MCP error: ${JSON.stringify(msg.error)}`));
12261
+ } else {
12262
+ resolve16(msg.result);
12263
+ }
12264
+ }
12265
+ }
12266
+ function sendRequest(method, params) {
12267
+ ensureProcess();
12268
+ const id2 = nextId();
12269
+ const body = createRequest2(id2, method, params);
12270
+ return new Promise((resolve16, reject) => {
12271
+ const timer = setTimeout(() => {
12272
+ pending.delete(id2);
12273
+ reject(new Error(`MCP request "${method}" timed out after ${timeout}ms`));
12274
+ }, timeout);
12275
+ pending.set(id2, { resolve: resolve16, reject, timer });
12276
+ if (proc?.stdin?.writable) {
12277
+ proc.stdin.write(body + "\n");
12278
+ } else {
12279
+ clearTimeout(timer);
12280
+ pending.delete(id2);
12281
+ reject(new Error("MCP server stdin not available"));
12282
+ }
12283
+ });
12284
+ }
12285
+ async function initialize() {
12286
+ ensureProcess();
12287
+ await sendRequest("initialize", {
12288
+ protocolVersion: "0.1.0",
12289
+ capabilities: {},
12290
+ clientInfo: { name: "weifuwu", version: "0.25.0" }
12291
+ });
12292
+ try {
12293
+ if (proc?.stdin?.writable) {
12294
+ proc.stdin.write(
12295
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n"
12296
+ );
12297
+ }
12298
+ } catch {
12299
+ }
12300
+ }
12301
+ function mcpSchemaToZod(inputSchema) {
12302
+ if (!inputSchema || !inputSchema.properties) {
12303
+ return z14.object({});
12304
+ }
12305
+ const shape = {};
12306
+ const required = new Set(inputSchema.required ?? []);
12307
+ for (const [key, prop] of Object.entries(inputSchema.properties)) {
12308
+ const p = prop;
12309
+ let field;
12310
+ switch (p.type) {
12311
+ case "string":
12312
+ field = z14.string();
12313
+ break;
12314
+ case "number":
12315
+ field = z14.number();
12316
+ break;
12317
+ case "integer":
12318
+ field = z14.number().int();
12319
+ break;
12320
+ case "boolean":
12321
+ field = z14.boolean();
12322
+ break;
12323
+ case "array":
12324
+ field = z14.array(z14.any());
12325
+ break;
12326
+ case "object":
12327
+ field = z14.record(z14.string(), z14.any());
12328
+ break;
12329
+ default:
12330
+ field = z14.any();
12331
+ }
12332
+ if (p.description) {
12333
+ field = field.describe(p.description);
12334
+ }
12335
+ if (!required.has(key)) {
12336
+ field = field.optional();
12337
+ }
12338
+ shape[key] = field;
12339
+ }
12340
+ return z14.object(shape);
12341
+ }
12342
+ async function refresh() {
12343
+ _tools = null;
12344
+ await getTools();
12345
+ }
12346
+ async function getTools() {
12347
+ if (_tools) return _tools;
12348
+ await initialize();
12349
+ const result = await sendRequest("tools/list");
12350
+ const defs = result?.tools ?? [];
12351
+ const tools = {};
12352
+ for (const def of defs) {
12353
+ const paramsSchema = mcpSchemaToZod(def.inputSchema);
12354
+ tools[def.name] = {
12355
+ description: def.description ?? "MCP tool: " + def.name,
12356
+ parameters: paramsSchema,
12357
+ execute: async (args2) => {
12358
+ const raw = await callToolInternal(def.name, args2);
12359
+ return raw;
12360
+ }
12361
+ };
12362
+ }
12363
+ _tools = tools;
12364
+ return tools;
12365
+ }
12366
+ async function callToolInternal(name, args2) {
12367
+ const result = await sendRequest("tools/call", {
12368
+ name,
12369
+ arguments: args2
12370
+ });
12371
+ const content = result?.content;
12372
+ if (Array.isArray(content)) {
12373
+ const textParts = content.filter((c) => c.type === "text").map((c) => c.text ?? "");
12374
+ if (textParts.length > 0) {
12375
+ let combined = textParts.join("\n");
12376
+ if (combined.length > maxResponseSize) {
12377
+ combined = combined.slice(0, maxResponseSize) + "\n... [truncated]";
12378
+ }
12379
+ return combined;
12380
+ }
12381
+ const resourceParts = content.filter((c) => c.type === "resource");
12382
+ if (resourceParts.length > 0) {
12383
+ return resourceParts;
12384
+ }
12385
+ return content;
12386
+ }
12387
+ return result;
12388
+ }
12389
+ async function callTool(name, args2) {
12390
+ return callToolInternal(name, args2);
12391
+ }
12392
+ async function close() {
12393
+ try {
12394
+ await sendRequest("shutdown");
12395
+ } catch {
12396
+ }
12397
+ if (proc && !proc.killed) {
12398
+ proc.kill("SIGTERM");
12399
+ setTimeout(() => {
12400
+ if (proc && !proc.killed) {
12401
+ try {
12402
+ proc.kill("SIGKILL");
12403
+ } catch {
12404
+ }
12405
+ }
12406
+ }, 3e3);
12407
+ }
12408
+ for (const [, { reject, timer }] of pending) {
12409
+ clearTimeout(timer);
12410
+ reject(new Error("MCP client closed"));
12411
+ }
12412
+ pending.clear();
12413
+ proc = null;
12414
+ rl = null;
12415
+ }
12416
+ return {
12417
+ getTools,
12418
+ refresh,
12419
+ callTool,
12420
+ close
12421
+ };
12422
+ }
12423
+
12424
+ // notifier/client.ts
12425
+ var DEFAULT_CHANNELS = ["inbox"];
12426
+ function notifier(opts) {
12427
+ const { sql: sql2, mailer: mailer2, hub } = opts;
12428
+ const table = opts.table ?? "_notifications";
12429
+ const fromName = opts.fromName ?? "System";
12430
+ const pageSize = opts.pageSize ?? 50;
12431
+ function escapeIdent7(s) {
12432
+ return `"${s.replace(/"/g, '""')}"`;
12433
+ }
12434
+ const tbl = escapeIdent7(table);
12435
+ async function migrate() {
12436
+ await sql2.unsafe(`
12437
+ CREATE TABLE IF NOT EXISTS ${tbl} (
12438
+ id SERIAL PRIMARY KEY,
12439
+ user_id INTEGER NOT NULL,
12440
+ title TEXT NOT NULL,
12441
+ body TEXT NOT NULL DEFAULT '',
12442
+ action_url TEXT,
12443
+ action_text TEXT,
12444
+ type TEXT NOT NULL DEFAULT 'default',
12445
+ metadata JSONB NOT NULL DEFAULT '{}',
12446
+ read_at TIMESTAMPTZ,
12447
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
12448
+ )
12449
+ `);
12450
+ await sql2.unsafe(`
12451
+ CREATE INDEX IF NOT EXISTS "${table}_user_unread"
12452
+ ON ${tbl} (user_id, created_at DESC)
12453
+ WHERE read_at IS NULL
12454
+ `);
12455
+ await sql2.unsafe(`
12456
+ CREATE INDEX IF NOT EXISTS "${table}_user_all"
12457
+ ON ${tbl} (user_id, created_at DESC)
12458
+ `);
12459
+ await sql2.unsafe(`
12460
+ CREATE TABLE IF NOT EXISTS "_notify_prefs" (
12461
+ user_id INTEGER PRIMARY KEY,
12462
+ channels JSONB NOT NULL DEFAULT '["inbox"]'::jsonb
12463
+ )
12464
+ `);
12465
+ }
12466
+ async function insertNotification(userId2, message) {
12467
+ await sql2.unsafe(
12468
+ `INSERT INTO ${tbl} (user_id, title, body, action_url, action_text, type, metadata)
12469
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
12470
+ [
12471
+ userId2,
12472
+ message.title,
12473
+ message.body ?? "",
12474
+ message.actionUrl ?? null,
12475
+ message.actionText ?? null,
12476
+ message.type ?? "default",
12477
+ message.metadata ?? {}
12478
+ ]
12479
+ );
12480
+ }
12481
+ async function send(to, message) {
12482
+ const prefs = await getPreferences(to.userId);
12483
+ if (prefs.channels.includes("inbox")) {
12484
+ await insertNotification(to.userId, message);
12485
+ }
12486
+ if (prefs.channels.includes("email") && mailer2 && to.email) {
12487
+ const html = renderEmail(message);
12488
+ mailer2.send({
12489
+ to: to.email,
12490
+ subject: message.title,
12491
+ text: message.body ?? "",
12492
+ html
12493
+ }).catch((err) => console.error("[notifier] email send failed:", err.message));
12494
+ }
12495
+ if (prefs.channels.includes("ws") && hub) {
12496
+ hub.broadcast(`notify:${to.userId}`, {
12497
+ type: "notification",
12498
+ data: {
12499
+ title: message.title,
12500
+ body: message.body,
12501
+ actionUrl: message.actionUrl,
12502
+ actionText: message.actionText,
12503
+ type: message.type ?? "default",
12504
+ metadata: message.metadata,
12505
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
12506
+ }
12507
+ });
12508
+ }
12509
+ }
12510
+ async function broadcast(message) {
12511
+ const rows = await sql2`
12512
+ SELECT user_id FROM "_notify_prefs"
12513
+ WHERE channels @> ${sql2.json(["inbox"])}
12514
+ `;
12515
+ for (const row of rows) {
12516
+ await insertNotification(row.user_id, message);
12517
+ if (hub) {
12518
+ hub.broadcast(`notify:${row.user_id}`, {
12519
+ type: "notification",
12520
+ data: {
12521
+ title: message.title,
12522
+ body: message.body,
12523
+ actionUrl: message.actionUrl,
12524
+ actionText: message.actionText,
12525
+ type: message.type ?? "default",
12526
+ metadata: message.metadata,
12527
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
12528
+ }
12529
+ });
12530
+ }
12531
+ }
12532
+ }
12533
+ async function unreadCount(userId2) {
12534
+ const [row] = await sql2.unsafe(
12535
+ `SELECT COUNT(*)::int AS count FROM ${tbl} WHERE user_id = $1 AND read_at IS NULL`,
12536
+ [userId2]
12537
+ );
12538
+ return row?.count ?? 0;
12539
+ }
12540
+ async function count(userId2, unreadOnly = false) {
12541
+ if (unreadOnly) return unreadCount(userId2);
12542
+ const [row] = await sql2.unsafe(
12543
+ `SELECT COUNT(*)::int AS count FROM ${tbl} WHERE user_id = $1`,
12544
+ [userId2]
12545
+ );
12546
+ return row?.count ?? 0;
12547
+ }
12548
+ async function markRead(userId2, notificationIds) {
12549
+ if (notificationIds && notificationIds.length > 0) {
12550
+ const ids = notificationIds.map((_, i) => `$${i + 2}`).join(", ");
12551
+ await sql2.unsafe(
12552
+ `UPDATE ${tbl} SET read_at = NOW() WHERE user_id = $1 AND id IN (${ids}) AND read_at IS NULL`,
12553
+ [userId2, ...notificationIds]
12554
+ );
12555
+ } else {
12556
+ await sql2.unsafe(`UPDATE ${tbl} SET read_at = NOW() WHERE user_id = $1 AND read_at IS NULL`, [
12557
+ userId2
12558
+ ]);
12559
+ }
12560
+ }
12561
+ async function list(userId2, opts2) {
12562
+ const limit = opts2?.limit ?? pageSize;
12563
+ const offset = opts2?.offset ?? 0;
12564
+ let where = `user_id = $1`;
12565
+ const params = [userId2];
12566
+ const paramIdx = 2;
12567
+ if (opts2?.unreadOnly) {
12568
+ where += ` AND read_at IS NULL`;
12569
+ }
12570
+ const rows = await sql2.unsafe(
12571
+ `SELECT id, user_id, title, body, action_url, action_text, type, metadata, read_at, created_at
12572
+ FROM ${tbl}
12573
+ WHERE ${where}
12574
+ ORDER BY created_at DESC
12575
+ LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`,
12576
+ [...params, limit, offset]
12577
+ );
12578
+ return rows.map(normalizeNotification);
12579
+ }
12580
+ function normalizeNotification(row) {
12581
+ return {
12582
+ ...row,
12583
+ metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata ?? {}
12584
+ };
12585
+ }
12586
+ async function getPreferences(userId2) {
12587
+ const [row] = await sql2.unsafe(`SELECT channels FROM "_notify_prefs" WHERE user_id = $1`, [
12588
+ userId2
12589
+ ]);
12590
+ if (!row) {
12591
+ return { channels: [...DEFAULT_CHANNELS] };
12592
+ }
12593
+ let channels;
12594
+ if (typeof row.channels === "string") {
12595
+ channels = JSON.parse(row.channels);
12596
+ } else if (Array.isArray(row.channels)) {
12597
+ channels = row.channels;
12598
+ } else {
12599
+ channels = [...DEFAULT_CHANNELS];
12600
+ }
12601
+ return { channels };
12602
+ }
12603
+ async function setPreferences(userId2, prefs) {
12604
+ await sql2`
12605
+ INSERT INTO "_notify_prefs" (user_id, channels)
12606
+ VALUES (${userId2}, ${sql2.json(prefs.channels)})
12607
+ ON CONFLICT (user_id)
12608
+ DO UPDATE SET channels = ${sql2.json(prefs.channels)}
12609
+ `;
12610
+ }
12611
+ async function clean(days) {
12612
+ const result = await sql2.unsafe(`DELETE FROM ${tbl} WHERE created_at < NOW() - $1::interval`, [
12613
+ `${days} days`
12614
+ ]);
12615
+ return Array.isArray(result) ? result.length : 0;
12616
+ }
12617
+ function renderEmail(message) {
12618
+ const actionHtml = message.actionUrl ? `
12619
+ <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>` : "";
12620
+ return `<!DOCTYPE html>
12621
+ <html>
12622
+ <head><meta charset="utf-8"></head>
12623
+ <body style="font-family:sans-serif;padding:20px;max-width:600px">
12624
+ <h2>${escapeHtml3(message.title)}</h2>
12625
+ ${message.body ? `<p>${escapeHtml3(message.body)}</p>` : ""}
12626
+ ${actionHtml}
12627
+ <hr style="margin-top:30px;border:none;border-top:1px solid #eee">
12628
+ <p style="color:#999;font-size:12px">${escapeHtml3(fromName)}</p>
12629
+ </body>
12630
+ </html>`;
12631
+ }
12632
+ function escapeHtml3(s) {
12633
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12634
+ }
12635
+ const mw = async (req, ctx, next) => {
12636
+ ;
12637
+ ctx.notifier = api;
12638
+ return next(req, ctx);
12639
+ };
12640
+ const api = {
12641
+ send,
12642
+ broadcast,
12643
+ unreadCount,
12644
+ count,
12645
+ markRead,
12646
+ list,
12647
+ getPreferences,
12648
+ setPreferences,
12649
+ clean,
12650
+ migrate,
12651
+ close: async () => {
12652
+ }
12653
+ };
12654
+ return Object.assign(mw, api);
12655
+ }
12189
12656
  export {
12190
12657
  DEFAULT_MAX_BODY,
12658
+ HttpError,
12191
12659
  MIGRATIONS_TABLE,
12192
12660
  MemoryCache,
12193
12661
  MemoryStore,
@@ -12240,7 +12708,9 @@ export {
12240
12708
  logdb,
12241
12709
  logger,
12242
12710
  mailer,
12711
+ mcpClient,
12243
12712
  messager,
12713
+ notifier,
12244
12714
  openai,
12245
12715
  opencode,
12246
12716
  permissions,