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/README.md +225 -0
- package/dist/iii/client.d.ts +1 -1
- package/dist/iii/register-worker.d.ts +0 -1
- package/dist/iii/types.d.ts +19 -33
- package/dist/iii/ws.d.ts +0 -7
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1007 -537
- package/dist/mcp.d.ts +34 -0
- package/dist/notifier/client.d.ts +2 -0
- package/dist/notifier/index.d.ts +2 -0
- package/dist/notifier/types.d.ts +105 -0
- package/dist/opencode/rest.d.ts +2 -1
- package/dist/opencode/ws.d.ts +2 -2
- package/dist/queue/types.d.ts +0 -1
- package/dist/rate-limit.d.ts +10 -5
- package/dist/redis/client.d.ts +2 -0
- package/dist/redis/index.d.ts +2 -3
- package/dist/router.d.ts +30 -0
- package/dist/serve.d.ts +1 -1
- package/dist/session.d.ts +20 -0
- package/dist/test-utils.d.ts +80 -2
- package/dist/types.d.ts +13 -0
- package/dist/user/client.d.ts +6 -1
- package/dist/user/types.d.ts +21 -0
- package/dist/validate.d.ts +16 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
3623
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4061
|
-
|
|
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
|
|
4083
|
-
|
|
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/
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
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(
|
|
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(
|
|
5836
|
-
|
|
5837
|
-
|
|
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(
|
|
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(
|
|
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) => ({
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
7351
|
-
safeUser[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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
8871
|
-
tokens_in:
|
|
8872
|
-
tokens_out:
|
|
8873
|
-
tokens_total:
|
|
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 (
|
|
8930
|
-
ws.send(
|
|
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 (
|
|
8985
|
-
if (
|
|
8986
|
-
ws.send(JSON.stringify({ type: "error", error:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 "
|
|
10457
|
-
deps.
|
|
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)
|
|
10473
|
-
|
|
10474
|
-
|
|
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(
|
|
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)
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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,
|