weifuwu 0.24.3 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +5 -1
- package/dist/index.js +886 -457
- 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/router.d.ts +30 -0
- package/dist/test-utils.d.ts +80 -2
- package/dist/user/types.d.ts +21 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -467,6 +467,8 @@ var Router = class _Router {
|
|
|
467
467
|
_hasWildcard = false;
|
|
468
468
|
_hub;
|
|
469
469
|
_wss;
|
|
470
|
+
/** Track which ctx fields have been injected so far (for dependency checking). */
|
|
471
|
+
_ctxFields = /* @__PURE__ */ new Set();
|
|
470
472
|
get wss() {
|
|
471
473
|
if (!this._wss) this._wss = new WebSocketServer({ noServer: true });
|
|
472
474
|
return this._wss;
|
|
@@ -490,17 +492,47 @@ var Router = class _Router {
|
|
|
490
492
|
node = getOrCreateChild(node, segment, createTrieNode, false);
|
|
491
493
|
}
|
|
492
494
|
node.pathMws.push(arg2);
|
|
495
|
+
this._checkMiddlewareMeta(arg2, `${arg1}`);
|
|
493
496
|
}
|
|
494
497
|
} else if (typeof arg1 === "function") {
|
|
495
498
|
this.globalMws.push(arg1);
|
|
499
|
+
this._checkMiddlewareMeta(arg1, "global");
|
|
496
500
|
} else if (typeof arg1 === "object" && arg1 !== null && "middleware" in arg1 && // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
497
501
|
typeof arg1.middleware === "function" && arg1 instanceof _Router) {
|
|
498
502
|
const mod = arg1;
|
|
499
|
-
|
|
503
|
+
const mw = mod.middleware();
|
|
504
|
+
this.globalMws.push(mw);
|
|
505
|
+
this._checkMiddlewareMeta(mw, "global (auto-registered)");
|
|
500
506
|
this._mountRouter("/", mod);
|
|
501
507
|
}
|
|
502
508
|
return this;
|
|
503
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Check a middleware's dependency metadata and emit warnings if
|
|
512
|
+
* required fields haven't been injected yet.
|
|
513
|
+
* Attach __meta to a middleware function:
|
|
514
|
+
*
|
|
515
|
+
* ```ts
|
|
516
|
+
* mw.__meta = { injects: ['sql'], depends: ['session'] }
|
|
517
|
+
* ```
|
|
518
|
+
*/
|
|
519
|
+
_checkMiddlewareMeta(mw, location) {
|
|
520
|
+
const meta = mw.__meta ?? mw.middleware?.().__meta;
|
|
521
|
+
if (!meta) return;
|
|
522
|
+
for (const dep of meta.depends) {
|
|
523
|
+
if (!this._ctxFields.has(dep)) {
|
|
524
|
+
console.warn(
|
|
525
|
+
`[weifuwu] Middleware at "${location}" depends on ctx.${dep} but it hasn't been registered yet.
|
|
526
|
+
Register the provider before this middleware:
|
|
527
|
+
app.use(${dep}()) // add before this middleware
|
|
528
|
+
Current ctx fields: [${[...this._ctxFields].join(", ")}]`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const field of meta.injects) {
|
|
533
|
+
this._ctxFields.add(field);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
504
536
|
// Route registration — returns Router<T> unchanged.
|
|
505
537
|
// Route-level middleware and handlers get Context<T>.
|
|
506
538
|
get(path2, ...args) {
|
|
@@ -1568,6 +1600,7 @@ function rateLimit(options) {
|
|
|
1568
1600
|
const res = await next(req, ctx);
|
|
1569
1601
|
return addRateLimitHeaders(res, max, remaining, reset);
|
|
1570
1602
|
};
|
|
1603
|
+
mw.__meta = { injects: [], depends: [] };
|
|
1571
1604
|
mw.close = () => {
|
|
1572
1605
|
if (interval) clearInterval(interval);
|
|
1573
1606
|
hits.clear();
|
|
@@ -1751,6 +1784,7 @@ function createSSEStream(iterable, opts) {
|
|
|
1751
1784
|
}
|
|
1752
1785
|
|
|
1753
1786
|
// test-utils.ts
|
|
1787
|
+
import { WebSocket as WSWebSocket } from "ws";
|
|
1754
1788
|
var TestResponseImpl = class {
|
|
1755
1789
|
response;
|
|
1756
1790
|
constructor(response) {
|
|
@@ -1846,9 +1880,22 @@ var TestRequest = class {
|
|
|
1846
1880
|
};
|
|
1847
1881
|
var TestApp = class {
|
|
1848
1882
|
router;
|
|
1883
|
+
wsServer = null;
|
|
1884
|
+
wsConnections = [];
|
|
1849
1885
|
constructor() {
|
|
1850
1886
|
this.router = new Router();
|
|
1851
1887
|
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Register a WebSocket handler.
|
|
1890
|
+
*/
|
|
1891
|
+
ws(path2, handler) {
|
|
1892
|
+
this.router.ws(path2, handler);
|
|
1893
|
+
return this;
|
|
1894
|
+
}
|
|
1895
|
+
/** Get the raw Router (for advanced use). */
|
|
1896
|
+
get _router() {
|
|
1897
|
+
return this.router;
|
|
1898
|
+
}
|
|
1852
1899
|
/** Add global middleware */
|
|
1853
1900
|
use(mw) {
|
|
1854
1901
|
this.router.use(mw);
|
|
@@ -1908,6 +1955,195 @@ var TestApp = class {
|
|
|
1908
1955
|
handler() {
|
|
1909
1956
|
return this.router.handler();
|
|
1910
1957
|
}
|
|
1958
|
+
/** Start building a WebSocket connection to the given path. */
|
|
1959
|
+
wsReq(path2) {
|
|
1960
|
+
return new TestWSRequest(this, path2);
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Internal: ensure HTTP server is running for WebSocket connections.
|
|
1964
|
+
* Starts on a random port.
|
|
1965
|
+
*/
|
|
1966
|
+
/* @internal */
|
|
1967
|
+
async _ensureServer() {
|
|
1968
|
+
if (this.wsServer) {
|
|
1969
|
+
return `http://localhost:${this.wsServer.port}`;
|
|
1970
|
+
}
|
|
1971
|
+
const wsHandler = this.router.websocketHandler();
|
|
1972
|
+
if (!wsHandler) {
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
"No WebSocket routes registered. Use app.ws(path, handler) before calling wsReq()."
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
this.wsServer = serve(this.router.handler(), {
|
|
1978
|
+
websocket: wsHandler
|
|
1979
|
+
});
|
|
1980
|
+
await this.wsServer.ready;
|
|
1981
|
+
return `http://localhost:${this.wsServer.port}`;
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Internal: register a WS connection for cleanup.
|
|
1985
|
+
*/
|
|
1986
|
+
/* @internal */
|
|
1987
|
+
_trackConnection(conn) {
|
|
1988
|
+
this.wsConnections.push(conn);
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Cleanup all WebSocket connections and stop the server.
|
|
1992
|
+
*/
|
|
1993
|
+
async close() {
|
|
1994
|
+
for (const conn of this.wsConnections) {
|
|
1995
|
+
try {
|
|
1996
|
+
conn.close();
|
|
1997
|
+
} catch {
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
this.wsConnections = [];
|
|
2001
|
+
if (this.wsServer) {
|
|
2002
|
+
this.wsServer.stop();
|
|
2003
|
+
this.wsServer = null;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
var TestWSRequest = class {
|
|
2008
|
+
app;
|
|
2009
|
+
path;
|
|
2010
|
+
_timeout = 5e3;
|
|
2011
|
+
constructor(app, path2) {
|
|
2012
|
+
this.app = app;
|
|
2013
|
+
this.path = path2;
|
|
2014
|
+
}
|
|
2015
|
+
/** Set the timeout for operations (default: 5000ms). */
|
|
2016
|
+
timeout(ms) {
|
|
2017
|
+
this._timeout = ms;
|
|
2018
|
+
return this;
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Connect to the WebSocket endpoint.
|
|
2022
|
+
* Starts a real HTTP server (random port) if not already running.
|
|
2023
|
+
*/
|
|
2024
|
+
async connect() {
|
|
2025
|
+
const baseUrl = await this.app._ensureServer();
|
|
2026
|
+
const wsUrl = baseUrl.replace(/^http/, "ws") + this.path;
|
|
2027
|
+
const ws = new WSWebSocket(wsUrl, { handshakeTimeout: this._timeout });
|
|
2028
|
+
return new Promise((resolve16, reject) => {
|
|
2029
|
+
const timer = setTimeout(() => {
|
|
2030
|
+
reject(new Error(`WebSocket connection timed out after ${this._timeout}ms`));
|
|
2031
|
+
ws.close();
|
|
2032
|
+
}, this._timeout);
|
|
2033
|
+
ws.on("open", () => {
|
|
2034
|
+
clearTimeout(timer);
|
|
2035
|
+
const conn = new TestWSConnection(ws, this._timeout);
|
|
2036
|
+
this.app._trackConnection(conn);
|
|
2037
|
+
resolve16(conn);
|
|
2038
|
+
});
|
|
2039
|
+
ws.on("error", (err) => {
|
|
2040
|
+
clearTimeout(timer);
|
|
2041
|
+
reject(new Error(`WebSocket connection error: ${err.message}`));
|
|
2042
|
+
});
|
|
2043
|
+
ws.on("unexpected-response", (_req, res) => {
|
|
2044
|
+
clearTimeout(timer);
|
|
2045
|
+
let body = "";
|
|
2046
|
+
res.on("data", (chunk) => {
|
|
2047
|
+
body += chunk.toString();
|
|
2048
|
+
});
|
|
2049
|
+
res.on("end", () => {
|
|
2050
|
+
reject(new Error(`WebSocket upgrade rejected (${res.statusCode}): ${body.slice(0, 200)}`));
|
|
2051
|
+
});
|
|
2052
|
+
});
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
var TestWSConnection = class {
|
|
2057
|
+
ws;
|
|
2058
|
+
_timeout;
|
|
2059
|
+
messageQueue = [];
|
|
2060
|
+
resolveQueue = [];
|
|
2061
|
+
_closed = false;
|
|
2062
|
+
constructor(ws, timeout = 5e3) {
|
|
2063
|
+
this.ws = ws;
|
|
2064
|
+
this._timeout = timeout;
|
|
2065
|
+
ws.on("message", (data) => {
|
|
2066
|
+
const str = data.toString();
|
|
2067
|
+
if (this.resolveQueue.length > 0) {
|
|
2068
|
+
const resolve16 = this.resolveQueue.shift();
|
|
2069
|
+
resolve16(str);
|
|
2070
|
+
} else {
|
|
2071
|
+
this.messageQueue.push(str);
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
ws.on("close", () => {
|
|
2075
|
+
this._closed = true;
|
|
2076
|
+
for (const _r of this.resolveQueue) {
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
/** Send a text message. */
|
|
2081
|
+
send(data) {
|
|
2082
|
+
this.ws.send(data);
|
|
2083
|
+
}
|
|
2084
|
+
/** Send a JSON message. */
|
|
2085
|
+
json(data) {
|
|
2086
|
+
this.ws.send(JSON.stringify(data));
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Wait for the next message. Returns the raw text.
|
|
2090
|
+
* Throws on timeout or if the connection is closed.
|
|
2091
|
+
*/
|
|
2092
|
+
async receive(timeout) {
|
|
2093
|
+
if (this.messageQueue.length > 0) {
|
|
2094
|
+
return this.messageQueue.shift();
|
|
2095
|
+
}
|
|
2096
|
+
if (this._closed) {
|
|
2097
|
+
throw new Error("WebSocket connection closed");
|
|
2098
|
+
}
|
|
2099
|
+
return new Promise((resolve16, reject) => {
|
|
2100
|
+
const timer = setTimeout(() => {
|
|
2101
|
+
const idx = this.resolveQueue.indexOf(resolve16);
|
|
2102
|
+
if (idx !== -1) this.resolveQueue.splice(idx, 1);
|
|
2103
|
+
reject(new Error(`WebSocket receive timed out after ${timeout ?? this._timeout}ms`));
|
|
2104
|
+
}, timeout ?? this._timeout);
|
|
2105
|
+
this.resolveQueue.push((msg) => {
|
|
2106
|
+
clearTimeout(timer);
|
|
2107
|
+
resolve16(msg);
|
|
2108
|
+
});
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
/** Wait for the next message and parse as JSON. */
|
|
2112
|
+
async receiveJson() {
|
|
2113
|
+
const msg = await this.receive();
|
|
2114
|
+
return JSON.parse(msg);
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Assert that no message is received within the given silence period.
|
|
2118
|
+
* Useful for verifying that something did NOT happen.
|
|
2119
|
+
*/
|
|
2120
|
+
async expectSilent(ms) {
|
|
2121
|
+
return new Promise((resolve16, reject) => {
|
|
2122
|
+
if (this.messageQueue.length > 0) {
|
|
2123
|
+
reject(new Error(`Expected silence but got message: ${this.messageQueue[0].slice(0, 100)}`));
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const timer = setTimeout(() => resolve16(), ms);
|
|
2127
|
+
const origPush = this.resolveQueue.push.bind(this.resolveQueue);
|
|
2128
|
+
this.resolveQueue.push = (_fn) => {
|
|
2129
|
+
clearTimeout(timer);
|
|
2130
|
+
reject(new Error("Expected silence but received a message"));
|
|
2131
|
+
return 0;
|
|
2132
|
+
};
|
|
2133
|
+
setTimeout(() => {
|
|
2134
|
+
this.resolveQueue.push = origPush;
|
|
2135
|
+
}, ms + 10).unref();
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
/** Close the connection. */
|
|
2139
|
+
close() {
|
|
2140
|
+
this._closed = true;
|
|
2141
|
+
this.ws.close();
|
|
2142
|
+
}
|
|
2143
|
+
/** Whether the connection is closed. */
|
|
2144
|
+
get closed() {
|
|
2145
|
+
return this._closed;
|
|
2146
|
+
}
|
|
1911
2147
|
};
|
|
1912
2148
|
function testApp() {
|
|
1913
2149
|
return new TestApp();
|
|
@@ -2507,6 +2743,7 @@ function aiProvider(options) {
|
|
|
2507
2743
|
ctx.ai = provider;
|
|
2508
2744
|
return next(req, ctx);
|
|
2509
2745
|
};
|
|
2746
|
+
mw.__meta = { injects: ["ai"], depends: [] };
|
|
2510
2747
|
return Object.assign(mw, provider);
|
|
2511
2748
|
}
|
|
2512
2749
|
|
|
@@ -3116,6 +3353,7 @@ function postgres(opts) {
|
|
|
3116
3353
|
ctx.sql = sql2;
|
|
3117
3354
|
return next(req, ctx);
|
|
3118
3355
|
});
|
|
3356
|
+
mw.__meta = { injects: ["sql"], depends: [] };
|
|
3119
3357
|
mw.sql = sql2;
|
|
3120
3358
|
mw.table = ((tableOrSchema, builders) => {
|
|
3121
3359
|
if (typeof tableOrSchema === "string") {
|
|
@@ -3192,7 +3430,7 @@ var PgModule = class {
|
|
|
3192
3430
|
};
|
|
3193
3431
|
|
|
3194
3432
|
// user/client.ts
|
|
3195
|
-
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
3433
|
+
import { randomBytes, scryptSync, timingSafeEqual, createHash } from "node:crypto";
|
|
3196
3434
|
import jwt2 from "jsonwebtoken";
|
|
3197
3435
|
import { z as z2 } from "zod";
|
|
3198
3436
|
|
|
@@ -3739,6 +3977,10 @@ var LoginSchema = z2.object({
|
|
|
3739
3977
|
email: z2.string().email(),
|
|
3740
3978
|
password: z2.string().min(1)
|
|
3741
3979
|
});
|
|
3980
|
+
var CreateApiKeySchema = z2.object({
|
|
3981
|
+
name: z2.string().min(1),
|
|
3982
|
+
scopes: z2.array(z2.string()).optional()
|
|
3983
|
+
});
|
|
3742
3984
|
function escapeIdent2(s) {
|
|
3743
3985
|
return `"${s.replace(/"/g, '""')}"`;
|
|
3744
3986
|
}
|
|
@@ -3785,6 +4027,7 @@ function user(options) {
|
|
|
3785
4027
|
const secret = options.jwtSecret;
|
|
3786
4028
|
const expiresIn = options.expiresIn ?? "24h";
|
|
3787
4029
|
const oauth2Enabled = options.oauth2?.server ?? false;
|
|
4030
|
+
const apiKeysEnabled = options.apiKeys ?? false;
|
|
3788
4031
|
const base = hasDb ? new PgModule(pg) : null;
|
|
3789
4032
|
const users = hasDb ? pg.table(table, {
|
|
3790
4033
|
id: serial("id").primaryKey(),
|
|
@@ -3822,6 +4065,28 @@ function user(options) {
|
|
|
3822
4065
|
ON "_auth_providers"(user_id)
|
|
3823
4066
|
`);
|
|
3824
4067
|
}
|
|
4068
|
+
if (apiKeysEnabled) {
|
|
4069
|
+
await _pg.sql.unsafe(`
|
|
4070
|
+
CREATE TABLE IF NOT EXISTS "_api_keys" (
|
|
4071
|
+
id SERIAL PRIMARY KEY,
|
|
4072
|
+
user_id INTEGER NOT NULL REFERENCES ${escapeIdent2(table)}(id) ON DELETE CASCADE,
|
|
4073
|
+
name TEXT NOT NULL,
|
|
4074
|
+
key_prefix TEXT NOT NULL,
|
|
4075
|
+
key_hash TEXT NOT NULL,
|
|
4076
|
+
scopes TEXT[] DEFAULT '{}',
|
|
4077
|
+
last_used_at TIMESTAMPTZ,
|
|
4078
|
+
expires_at TIMESTAMPTZ,
|
|
4079
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
4080
|
+
revoked BOOLEAN DEFAULT false
|
|
4081
|
+
)
|
|
4082
|
+
`);
|
|
4083
|
+
await _pg.sql.unsafe(`
|
|
4084
|
+
CREATE INDEX IF NOT EXISTS "_api_keys_user_idx" ON "_api_keys"(user_id)
|
|
4085
|
+
`);
|
|
4086
|
+
await _pg.sql.unsafe(`
|
|
4087
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "_api_keys_hash_idx" ON "_api_keys"(key_hash)
|
|
4088
|
+
`);
|
|
4089
|
+
}
|
|
3825
4090
|
if (!oauth2Enabled) return;
|
|
3826
4091
|
const clients3 = _pg.table("_oauth2_clients", {
|
|
3827
4092
|
id: serial("id").primaryKey(),
|
|
@@ -3922,8 +4187,79 @@ function user(options) {
|
|
|
3922
4187
|
return null;
|
|
3923
4188
|
}
|
|
3924
4189
|
}
|
|
4190
|
+
function hashApiKey(key) {
|
|
4191
|
+
return createHash("sha256").update(key).digest("hex");
|
|
4192
|
+
}
|
|
4193
|
+
function generateApiKey() {
|
|
4194
|
+
const random = randomBytes(32).toString("hex");
|
|
4195
|
+
return `sk_live_${random}`;
|
|
4196
|
+
}
|
|
4197
|
+
async function createApiKey(userId2, name, scopes) {
|
|
4198
|
+
if (!hasDb) throw new Error("user(): pg required for API key management");
|
|
4199
|
+
const key = generateApiKey();
|
|
4200
|
+
const keyHash = hashApiKey(key);
|
|
4201
|
+
const prefix = key.slice(0, 12) + "..." + key.slice(-4);
|
|
4202
|
+
const [row] = await _pg.sql.unsafe(
|
|
4203
|
+
`INSERT INTO "_api_keys" (user_id, name, key_prefix, key_hash, scopes)
|
|
4204
|
+
VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
|
4205
|
+
[userId2, name, prefix, keyHash, scopes ?? []]
|
|
4206
|
+
);
|
|
4207
|
+
return { id: row.id, key };
|
|
4208
|
+
}
|
|
4209
|
+
async function listApiKeys(userId2) {
|
|
4210
|
+
if (!hasDb) return [];
|
|
4211
|
+
const rows = await _pg.sql.unsafe(
|
|
4212
|
+
`SELECT id, name, key_prefix, scopes, last_used_at, created_at, revoked
|
|
4213
|
+
FROM "_api_keys" WHERE user_id = $1 ORDER BY created_at DESC`,
|
|
4214
|
+
[userId2]
|
|
4215
|
+
);
|
|
4216
|
+
return rows.map((r2) => ({
|
|
4217
|
+
id: r2.id,
|
|
4218
|
+
name: r2.name,
|
|
4219
|
+
prefix: r2.key_prefix,
|
|
4220
|
+
scopes: Array.isArray(r2.scopes) ? r2.scopes : [],
|
|
4221
|
+
last_used_at: r2.last_used_at ? new Date(r2.last_used_at).toISOString() : null,
|
|
4222
|
+
created_at: new Date(r2.created_at).toISOString(),
|
|
4223
|
+
revoked: !!r2.revoked
|
|
4224
|
+
}));
|
|
4225
|
+
}
|
|
4226
|
+
async function revokeApiKey(userId2, keyId) {
|
|
4227
|
+
if (!hasDb) throw new Error("user(): pg required for API key management");
|
|
4228
|
+
await _pg.sql.unsafe(`UPDATE "_api_keys" SET revoked = true WHERE id = $1 AND user_id = $2`, [
|
|
4229
|
+
keyId,
|
|
4230
|
+
userId2
|
|
4231
|
+
]);
|
|
4232
|
+
}
|
|
4233
|
+
async function verifyApiKey(key) {
|
|
4234
|
+
if (!hasDb || !apiKeysEnabled) return null;
|
|
4235
|
+
const keyHash = hashApiKey(key);
|
|
4236
|
+
const [row] = await _pg.sql.unsafe(
|
|
4237
|
+
`SELECT id, user_id, scopes, revoked, expires_at
|
|
4238
|
+
FROM "_api_keys" WHERE key_hash = $1 LIMIT 1`,
|
|
4239
|
+
[keyHash]
|
|
4240
|
+
);
|
|
4241
|
+
if (!row) return null;
|
|
4242
|
+
if (row.revoked) return null;
|
|
4243
|
+
if (row.expires_at && new Date(row.expires_at) < /* @__PURE__ */ new Date()) return null;
|
|
4244
|
+
await _pg.sql.unsafe(
|
|
4245
|
+
`UPDATE "_api_keys" SET last_used_at = NOW() WHERE id = $1 AND last_used_at IS NULL OR last_used_at < NOW() - interval '1 minute'`,
|
|
4246
|
+
[row.id]
|
|
4247
|
+
).catch(() => {
|
|
4248
|
+
});
|
|
4249
|
+
return {
|
|
4250
|
+
userId: row.user_id,
|
|
4251
|
+
scopes: Array.isArray(row.scopes) ? row.scopes : []
|
|
4252
|
+
};
|
|
4253
|
+
}
|
|
4254
|
+
async function tryApiKeyAuth(token) {
|
|
4255
|
+
if (!apiKeysEnabled || !hasDb) return null;
|
|
4256
|
+
if (!token.startsWith("sk_")) return null;
|
|
4257
|
+
return verifyApiKey(token);
|
|
4258
|
+
}
|
|
3925
4259
|
const headerName = options.header ?? "Authorization";
|
|
3926
4260
|
async function resolveUser(req, ctx) {
|
|
4261
|
+
const _ctx2 = ctx;
|
|
4262
|
+
if (_ctx2.user) return _ctx2.user;
|
|
3927
4263
|
const s = ctx;
|
|
3928
4264
|
const sessionUserId = s.session?.userId;
|
|
3929
4265
|
if (sessionUserId !== void 0 && sessionUserId !== null) {
|
|
@@ -3999,6 +4335,17 @@ function user(options) {
|
|
|
3999
4335
|
return null;
|
|
4000
4336
|
}
|
|
4001
4337
|
}
|
|
4338
|
+
if (token.startsWith("sk_")) {
|
|
4339
|
+
const result = await tryApiKeyAuth(token);
|
|
4340
|
+
if (result) {
|
|
4341
|
+
if (hasDb) {
|
|
4342
|
+
const row = await findById(result.userId);
|
|
4343
|
+
if (row) return { ...stripPassword(row), _apiKeyScopes: result.scopes };
|
|
4344
|
+
}
|
|
4345
|
+
return { id: result.userId, _apiKeyScopes: result.scopes };
|
|
4346
|
+
}
|
|
4347
|
+
return null;
|
|
4348
|
+
}
|
|
4002
4349
|
if (secret && hasDb) {
|
|
4003
4350
|
try {
|
|
4004
4351
|
const payload = jwt2.verify(token, secret);
|
|
@@ -4084,6 +4431,33 @@ function user(options) {
|
|
|
4084
4431
|
}
|
|
4085
4432
|
});
|
|
4086
4433
|
}
|
|
4434
|
+
if (apiKeysEnabled) {
|
|
4435
|
+
r.get("/api-keys", middleware(), async (_req, ctx) => {
|
|
4436
|
+
const keys = await listApiKeys(ctx.user.id);
|
|
4437
|
+
return Response.json(keys);
|
|
4438
|
+
});
|
|
4439
|
+
r.post("/api-keys", middleware(), async (req, ctx) => {
|
|
4440
|
+
try {
|
|
4441
|
+
const body = await parseBody2(req);
|
|
4442
|
+
const { name, scopes } = CreateApiKeySchema.parse(body);
|
|
4443
|
+
const result = await createApiKey(ctx.user.id, name, scopes);
|
|
4444
|
+
return Response.json(result, { status: 201 });
|
|
4445
|
+
} catch (err) {
|
|
4446
|
+
if (err instanceof z2.ZodError) {
|
|
4447
|
+
return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
|
|
4448
|
+
}
|
|
4449
|
+
return Response.json({ error: err.message }, { status: 500 });
|
|
4450
|
+
}
|
|
4451
|
+
});
|
|
4452
|
+
r.delete("/api-keys/:id", middleware(), async (req, ctx) => {
|
|
4453
|
+
const keyId = parseInt(ctx.params.id, 10);
|
|
4454
|
+
if (isNaN(keyId)) {
|
|
4455
|
+
return Response.json({ error: "Invalid key ID" }, { status: 400 });
|
|
4456
|
+
}
|
|
4457
|
+
await revokeApiKey(ctx.user.id, keyId);
|
|
4458
|
+
return Response.json({ ok: true });
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4087
4461
|
if (oauth2) {
|
|
4088
4462
|
r.get("/oauth/authorize", (req, ctx) => oauth2.authorizeHandler(req, ctx));
|
|
4089
4463
|
r.post("/oauth/consent", (req) => oauth2.consentHandler(req));
|
|
@@ -4128,6 +4502,21 @@ function user(options) {
|
|
|
4128
4502
|
mod.revokeClient = oauth2 ? (clientId) => oauth2.revokeClient(clientId) : async () => {
|
|
4129
4503
|
throw new Error("OAuth2 server is not enabled");
|
|
4130
4504
|
};
|
|
4505
|
+
mod.createApiKey = hasDb && apiKeysEnabled ? createApiKey : async () => {
|
|
4506
|
+
throw new Error(
|
|
4507
|
+
"API key management is not enabled. Pass apiKeys: true in user() options."
|
|
4508
|
+
);
|
|
4509
|
+
};
|
|
4510
|
+
mod.listApiKeys = hasDb && apiKeysEnabled ? listApiKeys : async () => {
|
|
4511
|
+
throw new Error(
|
|
4512
|
+
"API key management is not enabled. Pass apiKeys: true in user() options."
|
|
4513
|
+
);
|
|
4514
|
+
};
|
|
4515
|
+
mod.revokeApiKey = hasDb && apiKeysEnabled ? revokeApiKey : async () => {
|
|
4516
|
+
throw new Error(
|
|
4517
|
+
"API key management is not enabled. Pass apiKeys: true in user() options."
|
|
4518
|
+
);
|
|
4519
|
+
};
|
|
4131
4520
|
mod.close = hasDb ? () => base.close() : async () => {
|
|
4132
4521
|
};
|
|
4133
4522
|
return mod;
|
|
@@ -4144,6 +4533,7 @@ function redis(opts) {
|
|
|
4144
4533
|
ctx.redis = client;
|
|
4145
4534
|
return next(req, ctx);
|
|
4146
4535
|
});
|
|
4536
|
+
mw.__meta = { injects: ["redis"], depends: [] };
|
|
4147
4537
|
mw.redis = client;
|
|
4148
4538
|
mw.close = () => client.quit();
|
|
4149
4539
|
return mw;
|
|
@@ -6993,7 +7383,7 @@ import { createOpenAI as createOpenAI3 } from "@ai-sdk/openai";
|
|
|
6993
7383
|
|
|
6994
7384
|
// ssr.ts
|
|
6995
7385
|
import { createElement as createElement3 } from "react";
|
|
6996
|
-
import { createHash as
|
|
7386
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
6997
7387
|
import { existsSync as existsSync6, readdirSync } from "node:fs";
|
|
6998
7388
|
import { readdir, stat } from "node:fs/promises";
|
|
6999
7389
|
import { dirname as dirname4, join as join5, resolve as resolve8, relative as relative3 } from "node:path";
|
|
@@ -7004,7 +7394,7 @@ import * as esbuild2 from "esbuild";
|
|
|
7004
7394
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
7005
7395
|
import { join as join2, resolve as resolve4, dirname as dirname2 } from "node:path";
|
|
7006
7396
|
import { pathToFileURL } from "node:url";
|
|
7007
|
-
import { createHash } from "node:crypto";
|
|
7397
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7008
7398
|
import { createRequire as createRequire2 } from "node:module";
|
|
7009
7399
|
|
|
7010
7400
|
// server-registry.ts
|
|
@@ -7195,7 +7585,7 @@ function resolveAliases2() {
|
|
|
7195
7585
|
return {};
|
|
7196
7586
|
}
|
|
7197
7587
|
function id(s) {
|
|
7198
|
-
return
|
|
7588
|
+
return createHash2("md5").update(s).digest("hex").slice(0, 8);
|
|
7199
7589
|
}
|
|
7200
7590
|
function clearCompileCache() {
|
|
7201
7591
|
cache.clear();
|
|
@@ -7479,7 +7869,7 @@ ws.onclose=function(){
|
|
|
7479
7869
|
var ssrEntries = /* @__PURE__ */ new Map();
|
|
7480
7870
|
|
|
7481
7871
|
// tailwind.ts
|
|
7482
|
-
import { createHash as
|
|
7872
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
7483
7873
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "node:fs";
|
|
7484
7874
|
import { join as join3, relative, resolve as resolve5 } from "node:path";
|
|
7485
7875
|
var extraSources = /* @__PURE__ */ new Set();
|
|
@@ -7531,7 +7921,7 @@ ${src}`;
|
|
|
7531
7921
|
${src}`;
|
|
7532
7922
|
}
|
|
7533
7923
|
const result = await postcss([tailwindPlugin()]).process(src, { from: cssPath });
|
|
7534
|
-
const hash =
|
|
7924
|
+
const hash = createHash3("md5").update(result.css).digest("hex").slice(0, 8);
|
|
7535
7925
|
cssCache.set(cssPath, { css: result.css, hash });
|
|
7536
7926
|
return result.css;
|
|
7537
7927
|
} catch (err) {
|
|
@@ -7549,7 +7939,7 @@ import { join as join4, resolve as resolve7 } from "node:path";
|
|
|
7549
7939
|
import * as esbuild3 from "esbuild";
|
|
7550
7940
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
7551
7941
|
import { resolve as resolve6, dirname as dirname3, relative as relative2 } from "node:path";
|
|
7552
|
-
import { createHash as
|
|
7942
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7553
7943
|
var moduleCache = /* @__PURE__ */ new Map();
|
|
7554
7944
|
var hashCache = /* @__PURE__ */ new Map();
|
|
7555
7945
|
function clearModuleCache(filePath) {
|
|
@@ -7573,7 +7963,7 @@ function fileHash(absPath) {
|
|
|
7573
7963
|
if (cached) return cached;
|
|
7574
7964
|
try {
|
|
7575
7965
|
const content = readFileSync5(absPath);
|
|
7576
|
-
const h =
|
|
7966
|
+
const h = createHash4("md5").update(content).digest("hex").slice(0, 8);
|
|
7577
7967
|
hashCache.set(absPath, h);
|
|
7578
7968
|
return h;
|
|
7579
7969
|
} catch {
|
|
@@ -7852,7 +8242,7 @@ var isDev2 = isDev();
|
|
|
7852
8242
|
var als2 = new AsyncLocalStorage2();
|
|
7853
8243
|
__registerAls(() => als2.getStore());
|
|
7854
8244
|
function hashId(s) {
|
|
7855
|
-
return
|
|
8245
|
+
return createHash5("md5").update(s).digest("hex").slice(0, 8);
|
|
7856
8246
|
}
|
|
7857
8247
|
function serializeLoaderData(ctx) {
|
|
7858
8248
|
const ld = ctx.loaderData;
|
|
@@ -10016,381 +10406,6 @@ function logdb(options) {
|
|
|
10016
10406
|
// iii/client.ts
|
|
10017
10407
|
import crypto8 from "node:crypto";
|
|
10018
10408
|
|
|
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
10409
|
// iii/ws.ts
|
|
10395
10410
|
function createWsHandler(deps) {
|
|
10396
10411
|
const wsToWorkerId = /* @__PURE__ */ new Map();
|
|
@@ -10424,9 +10439,9 @@ function createWsHandler(deps) {
|
|
|
10424
10439
|
const workerId = getWorkerId(ws);
|
|
10425
10440
|
if (workerId) {
|
|
10426
10441
|
deps.registerRemoteTrigger(workerId, {
|
|
10427
|
-
type: msg.
|
|
10428
|
-
function_id: msg.function_id,
|
|
10429
|
-
config: msg.config || {}
|
|
10442
|
+
type: msg.input?.type || "custom",
|
|
10443
|
+
function_id: msg.input?.function_id || msg.id,
|
|
10444
|
+
config: msg.input?.config || {}
|
|
10430
10445
|
});
|
|
10431
10446
|
}
|
|
10432
10447
|
break;
|
|
@@ -10438,11 +10453,7 @@ function createWsHandler(deps) {
|
|
|
10438
10453
|
}
|
|
10439
10454
|
case "unregister_trigger": {
|
|
10440
10455
|
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);
|
|
10456
|
+
if (workerId) deps.unregisterRemoteTrigger(workerId, msg.function_id || msg.id);
|
|
10446
10457
|
break;
|
|
10447
10458
|
}
|
|
10448
10459
|
case "invoke_result": {
|
|
@@ -10453,31 +10464,20 @@ function createWsHandler(deps) {
|
|
|
10453
10464
|
deps.handleInvokeError(msg.invocation_id, msg.error);
|
|
10454
10465
|
break;
|
|
10455
10466
|
}
|
|
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);
|
|
10467
|
+
case "invoke": {
|
|
10468
|
+
deps.handleInvoke(ws, msg.invocation_id, msg.function_id, msg.payload);
|
|
10466
10469
|
break;
|
|
10467
10470
|
}
|
|
10471
|
+
default:
|
|
10472
|
+
ws.send(JSON.stringify({ type: "error", message: `Unknown message type: ${msg.type}` }));
|
|
10468
10473
|
}
|
|
10469
10474
|
},
|
|
10470
10475
|
close(ws) {
|
|
10471
10476
|
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);
|
|
10477
|
+
if (workerId) {
|
|
10478
|
+
deps.unregisterRemoteWorker(workerId);
|
|
10479
|
+
wsToWorkerId.delete(ws);
|
|
10480
|
+
}
|
|
10481
10481
|
}
|
|
10482
10482
|
};
|
|
10483
10483
|
}
|
|
@@ -10522,38 +10522,11 @@ function buildRouter5(engine, wsHandler) {
|
|
|
10522
10522
|
}
|
|
10523
10523
|
|
|
10524
10524
|
// iii/client.ts
|
|
10525
|
-
function iii(
|
|
10526
|
-
const stream = createStream({ pg: opts.pg, redis: opts.redis, streamTTL: opts.streamTTL });
|
|
10525
|
+
function iii(_opts = {}) {
|
|
10527
10526
|
const workers = /* @__PURE__ */ new Map();
|
|
10528
10527
|
const functions = /* @__PURE__ */ new Map();
|
|
10529
10528
|
const triggers = /* @__PURE__ */ new Map();
|
|
10530
10529
|
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
10530
|
function addLocalWorker(worker) {
|
|
10558
10531
|
const workerId = crypto8.randomUUID();
|
|
10559
10532
|
const reg = {
|
|
@@ -10672,12 +10645,6 @@ function iii(opts = {}) {
|
|
|
10672
10645
|
worker.triggers = worker.triggers.filter((t) => t.function_id !== functionId);
|
|
10673
10646
|
}
|
|
10674
10647
|
},
|
|
10675
|
-
addStreamSubscriber(ws, sub) {
|
|
10676
|
-
stream.subscribe(ws, sub);
|
|
10677
|
-
},
|
|
10678
|
-
removeStreamSubscriber(ws) {
|
|
10679
|
-
stream.unsubscribe(ws);
|
|
10680
|
-
},
|
|
10681
10648
|
handleInvokeResult(invocationId, result) {
|
|
10682
10649
|
const p = pending.get(invocationId);
|
|
10683
10650
|
if (p) {
|
|
@@ -10730,7 +10697,7 @@ function iii(opts = {}) {
|
|
|
10730
10697
|
}
|
|
10731
10698
|
function trigger(request) {
|
|
10732
10699
|
const fn = functions.get(request.function_id);
|
|
10733
|
-
if (!fn)
|
|
10700
|
+
if (!fn) return Promise.reject(new Error(`Function "${request.function_id}" not found`));
|
|
10734
10701
|
const ctx = { engine: engineRef, functionId: request.function_id, workerName: fn.workerName };
|
|
10735
10702
|
if (request.action === "void") {
|
|
10736
10703
|
queueMicrotask(() => fn.handler(request.payload, ctx));
|
|
@@ -10777,7 +10744,6 @@ function iii(opts = {}) {
|
|
|
10777
10744
|
mod.listFunctions = listFunctions;
|
|
10778
10745
|
mod.listTriggers = listTriggers;
|
|
10779
10746
|
mod.migrate = async () => {
|
|
10780
|
-
await stream.migrate();
|
|
10781
10747
|
};
|
|
10782
10748
|
mod.close = async () => {
|
|
10783
10749
|
for (const [, p] of pending) {
|
|
@@ -10789,7 +10755,6 @@ function iii(opts = {}) {
|
|
|
10789
10755
|
workers.clear();
|
|
10790
10756
|
functions.clear();
|
|
10791
10757
|
triggers.clear();
|
|
10792
|
-
await stream.close();
|
|
10793
10758
|
};
|
|
10794
10759
|
return mod;
|
|
10795
10760
|
}
|
|
@@ -10926,11 +10891,6 @@ function registerWorker(url) {
|
|
|
10926
10891
|
}
|
|
10927
10892
|
break;
|
|
10928
10893
|
}
|
|
10929
|
-
case "stream": {
|
|
10930
|
-
const handler = handlers.get("__stream__");
|
|
10931
|
-
if (handler) handler(msg, {});
|
|
10932
|
-
break;
|
|
10933
|
-
}
|
|
10934
10894
|
}
|
|
10935
10895
|
};
|
|
10936
10896
|
ws.onclose = () => {
|
|
@@ -11003,9 +10963,6 @@ function registerWorker(url) {
|
|
|
11003
10963
|
});
|
|
11004
10964
|
});
|
|
11005
10965
|
},
|
|
11006
|
-
onStream(handler) {
|
|
11007
|
-
handlers.set("__stream__", handler);
|
|
11008
|
-
},
|
|
11009
10966
|
close() {
|
|
11010
10967
|
intentionalClose = true;
|
|
11011
10968
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
@@ -11257,6 +11214,7 @@ function session(options) {
|
|
|
11257
11214
|
}
|
|
11258
11215
|
return res;
|
|
11259
11216
|
});
|
|
11217
|
+
mw.__meta = { injects: ["session"], depends: [] };
|
|
11260
11218
|
mw.close = async () => {
|
|
11261
11219
|
await closeStore?.();
|
|
11262
11220
|
};
|
|
@@ -12186,6 +12144,475 @@ function permissions(options) {
|
|
|
12186
12144
|
mw.migrate = migrate;
|
|
12187
12145
|
return mw;
|
|
12188
12146
|
}
|
|
12147
|
+
|
|
12148
|
+
// mcp.ts
|
|
12149
|
+
import { spawn } from "node:child_process";
|
|
12150
|
+
import { createInterface } from "node:readline";
|
|
12151
|
+
import { z as z14 } from "zod";
|
|
12152
|
+
var _requestId = 0;
|
|
12153
|
+
function nextId() {
|
|
12154
|
+
return ++_requestId;
|
|
12155
|
+
}
|
|
12156
|
+
function createRequest2(id2, method, params) {
|
|
12157
|
+
return JSON.stringify({
|
|
12158
|
+
jsonrpc: "2.0",
|
|
12159
|
+
id: id2,
|
|
12160
|
+
method,
|
|
12161
|
+
params
|
|
12162
|
+
});
|
|
12163
|
+
}
|
|
12164
|
+
function mcpClient(options) {
|
|
12165
|
+
const { command, args = [], env: env2 } = options;
|
|
12166
|
+
const timeout = options.timeout ?? 15e3;
|
|
12167
|
+
const maxResponseSize = options.maxResponseSize ?? 10 * 1024 * 1024;
|
|
12168
|
+
let proc = null;
|
|
12169
|
+
let rl = null;
|
|
12170
|
+
const pending = /* @__PURE__ */ new Map();
|
|
12171
|
+
let _tools = null;
|
|
12172
|
+
function ensureProcess() {
|
|
12173
|
+
if (proc && !proc.killed) return;
|
|
12174
|
+
proc = spawn(command, args, {
|
|
12175
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
12176
|
+
env: { ...process.env, ...env2 }
|
|
12177
|
+
});
|
|
12178
|
+
rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
12179
|
+
let buffer = "";
|
|
12180
|
+
rl.on("line", (line) => {
|
|
12181
|
+
buffer += line;
|
|
12182
|
+
try {
|
|
12183
|
+
const msg = JSON.parse(buffer);
|
|
12184
|
+
buffer = "";
|
|
12185
|
+
handleMessage(msg);
|
|
12186
|
+
} catch {
|
|
12187
|
+
}
|
|
12188
|
+
});
|
|
12189
|
+
proc.stderr?.on("data", (chunk) => {
|
|
12190
|
+
const text2 = chunk.toString().trim();
|
|
12191
|
+
if (text2) {
|
|
12192
|
+
console.debug(`[mcp:${command}] stderr:`, text2);
|
|
12193
|
+
}
|
|
12194
|
+
});
|
|
12195
|
+
proc.on("exit", (code, signal) => {
|
|
12196
|
+
console.debug(`[mcp:${command}] exited (code=${code} signal=${signal})`);
|
|
12197
|
+
for (const [id2, { reject, timer }] of pending) {
|
|
12198
|
+
clearTimeout(timer);
|
|
12199
|
+
reject(new Error(`MCP server exited (code=${code} signal=${signal})`));
|
|
12200
|
+
pending.delete(id2);
|
|
12201
|
+
}
|
|
12202
|
+
proc = null;
|
|
12203
|
+
rl = null;
|
|
12204
|
+
});
|
|
12205
|
+
proc.on("error", (err) => {
|
|
12206
|
+
console.error(`[mcp:${command}] error:`, err.message);
|
|
12207
|
+
for (const [, { reject, timer }] of pending) {
|
|
12208
|
+
clearTimeout(timer);
|
|
12209
|
+
reject(err);
|
|
12210
|
+
}
|
|
12211
|
+
pending.clear();
|
|
12212
|
+
});
|
|
12213
|
+
}
|
|
12214
|
+
function handleMessage(msg) {
|
|
12215
|
+
if (msg.id !== void 0 && pending.has(msg.id)) {
|
|
12216
|
+
const { resolve: resolve16, reject, timer } = pending.get(msg.id);
|
|
12217
|
+
clearTimeout(timer);
|
|
12218
|
+
pending.delete(msg.id);
|
|
12219
|
+
if (msg.error) {
|
|
12220
|
+
reject(new Error(`MCP error: ${JSON.stringify(msg.error)}`));
|
|
12221
|
+
} else {
|
|
12222
|
+
resolve16(msg.result);
|
|
12223
|
+
}
|
|
12224
|
+
}
|
|
12225
|
+
}
|
|
12226
|
+
function sendRequest(method, params) {
|
|
12227
|
+
ensureProcess();
|
|
12228
|
+
const id2 = nextId();
|
|
12229
|
+
const body = createRequest2(id2, method, params);
|
|
12230
|
+
return new Promise((resolve16, reject) => {
|
|
12231
|
+
const timer = setTimeout(() => {
|
|
12232
|
+
pending.delete(id2);
|
|
12233
|
+
reject(new Error(`MCP request "${method}" timed out after ${timeout}ms`));
|
|
12234
|
+
}, timeout);
|
|
12235
|
+
pending.set(id2, { resolve: resolve16, reject, timer });
|
|
12236
|
+
if (proc?.stdin?.writable) {
|
|
12237
|
+
proc.stdin.write(body + "\n");
|
|
12238
|
+
} else {
|
|
12239
|
+
clearTimeout(timer);
|
|
12240
|
+
pending.delete(id2);
|
|
12241
|
+
reject(new Error("MCP server stdin not available"));
|
|
12242
|
+
}
|
|
12243
|
+
});
|
|
12244
|
+
}
|
|
12245
|
+
async function initialize() {
|
|
12246
|
+
ensureProcess();
|
|
12247
|
+
await sendRequest("initialize", {
|
|
12248
|
+
protocolVersion: "0.1.0",
|
|
12249
|
+
capabilities: {},
|
|
12250
|
+
clientInfo: { name: "weifuwu", version: "0.25.0" }
|
|
12251
|
+
});
|
|
12252
|
+
try {
|
|
12253
|
+
if (proc?.stdin?.writable) {
|
|
12254
|
+
proc.stdin.write(
|
|
12255
|
+
JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n"
|
|
12256
|
+
);
|
|
12257
|
+
}
|
|
12258
|
+
} catch {
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
function mcpSchemaToZod(inputSchema) {
|
|
12262
|
+
if (!inputSchema || !inputSchema.properties) {
|
|
12263
|
+
return z14.object({});
|
|
12264
|
+
}
|
|
12265
|
+
const shape = {};
|
|
12266
|
+
const required = new Set(inputSchema.required ?? []);
|
|
12267
|
+
for (const [key, prop] of Object.entries(inputSchema.properties)) {
|
|
12268
|
+
const p = prop;
|
|
12269
|
+
let field;
|
|
12270
|
+
switch (p.type) {
|
|
12271
|
+
case "string":
|
|
12272
|
+
field = z14.string();
|
|
12273
|
+
break;
|
|
12274
|
+
case "number":
|
|
12275
|
+
field = z14.number();
|
|
12276
|
+
break;
|
|
12277
|
+
case "integer":
|
|
12278
|
+
field = z14.number().int();
|
|
12279
|
+
break;
|
|
12280
|
+
case "boolean":
|
|
12281
|
+
field = z14.boolean();
|
|
12282
|
+
break;
|
|
12283
|
+
case "array":
|
|
12284
|
+
field = z14.array(z14.any());
|
|
12285
|
+
break;
|
|
12286
|
+
case "object":
|
|
12287
|
+
field = z14.record(z14.string(), z14.any());
|
|
12288
|
+
break;
|
|
12289
|
+
default:
|
|
12290
|
+
field = z14.any();
|
|
12291
|
+
}
|
|
12292
|
+
if (p.description) {
|
|
12293
|
+
field = field.describe(p.description);
|
|
12294
|
+
}
|
|
12295
|
+
if (!required.has(key)) {
|
|
12296
|
+
field = field.optional();
|
|
12297
|
+
}
|
|
12298
|
+
shape[key] = field;
|
|
12299
|
+
}
|
|
12300
|
+
return z14.object(shape);
|
|
12301
|
+
}
|
|
12302
|
+
async function refresh() {
|
|
12303
|
+
_tools = null;
|
|
12304
|
+
await getTools();
|
|
12305
|
+
}
|
|
12306
|
+
async function getTools() {
|
|
12307
|
+
if (_tools) return _tools;
|
|
12308
|
+
await initialize();
|
|
12309
|
+
const result = await sendRequest("tools/list");
|
|
12310
|
+
const defs = result?.tools ?? [];
|
|
12311
|
+
const tools = {};
|
|
12312
|
+
for (const def of defs) {
|
|
12313
|
+
const paramsSchema = mcpSchemaToZod(def.inputSchema);
|
|
12314
|
+
tools[def.name] = {
|
|
12315
|
+
description: def.description ?? "MCP tool: " + def.name,
|
|
12316
|
+
parameters: paramsSchema,
|
|
12317
|
+
execute: async (args2) => {
|
|
12318
|
+
const raw = await callToolInternal(def.name, args2);
|
|
12319
|
+
return raw;
|
|
12320
|
+
}
|
|
12321
|
+
};
|
|
12322
|
+
}
|
|
12323
|
+
_tools = tools;
|
|
12324
|
+
return tools;
|
|
12325
|
+
}
|
|
12326
|
+
async function callToolInternal(name, args2) {
|
|
12327
|
+
const result = await sendRequest("tools/call", {
|
|
12328
|
+
name,
|
|
12329
|
+
arguments: args2
|
|
12330
|
+
});
|
|
12331
|
+
const content = result?.content;
|
|
12332
|
+
if (Array.isArray(content)) {
|
|
12333
|
+
const textParts = content.filter((c) => c.type === "text").map((c) => c.text ?? "");
|
|
12334
|
+
if (textParts.length > 0) {
|
|
12335
|
+
let combined = textParts.join("\n");
|
|
12336
|
+
if (combined.length > maxResponseSize) {
|
|
12337
|
+
combined = combined.slice(0, maxResponseSize) + "\n... [truncated]";
|
|
12338
|
+
}
|
|
12339
|
+
return combined;
|
|
12340
|
+
}
|
|
12341
|
+
const resourceParts = content.filter((c) => c.type === "resource");
|
|
12342
|
+
if (resourceParts.length > 0) {
|
|
12343
|
+
return resourceParts;
|
|
12344
|
+
}
|
|
12345
|
+
return content;
|
|
12346
|
+
}
|
|
12347
|
+
return result;
|
|
12348
|
+
}
|
|
12349
|
+
async function callTool(name, args2) {
|
|
12350
|
+
return callToolInternal(name, args2);
|
|
12351
|
+
}
|
|
12352
|
+
async function close() {
|
|
12353
|
+
try {
|
|
12354
|
+
await sendRequest("shutdown");
|
|
12355
|
+
} catch {
|
|
12356
|
+
}
|
|
12357
|
+
if (proc && !proc.killed) {
|
|
12358
|
+
proc.kill("SIGTERM");
|
|
12359
|
+
setTimeout(() => {
|
|
12360
|
+
if (proc && !proc.killed) {
|
|
12361
|
+
try {
|
|
12362
|
+
proc.kill("SIGKILL");
|
|
12363
|
+
} catch {
|
|
12364
|
+
}
|
|
12365
|
+
}
|
|
12366
|
+
}, 3e3);
|
|
12367
|
+
}
|
|
12368
|
+
for (const [, { reject, timer }] of pending) {
|
|
12369
|
+
clearTimeout(timer);
|
|
12370
|
+
reject(new Error("MCP client closed"));
|
|
12371
|
+
}
|
|
12372
|
+
pending.clear();
|
|
12373
|
+
proc = null;
|
|
12374
|
+
rl = null;
|
|
12375
|
+
}
|
|
12376
|
+
return {
|
|
12377
|
+
getTools,
|
|
12378
|
+
refresh,
|
|
12379
|
+
callTool,
|
|
12380
|
+
close
|
|
12381
|
+
};
|
|
12382
|
+
}
|
|
12383
|
+
|
|
12384
|
+
// notifier/client.ts
|
|
12385
|
+
var DEFAULT_CHANNELS = ["inbox"];
|
|
12386
|
+
function notifier(opts) {
|
|
12387
|
+
const { sql: sql2, mailer: mailer2, hub } = opts;
|
|
12388
|
+
const table = opts.table ?? "_notifications";
|
|
12389
|
+
const fromName = opts.fromName ?? "System";
|
|
12390
|
+
const pageSize = opts.pageSize ?? 50;
|
|
12391
|
+
function escapeIdent7(s) {
|
|
12392
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
12393
|
+
}
|
|
12394
|
+
const tbl = escapeIdent7(table);
|
|
12395
|
+
async function migrate() {
|
|
12396
|
+
await sql2.unsafe(`
|
|
12397
|
+
CREATE TABLE IF NOT EXISTS ${tbl} (
|
|
12398
|
+
id SERIAL PRIMARY KEY,
|
|
12399
|
+
user_id INTEGER NOT NULL,
|
|
12400
|
+
title TEXT NOT NULL,
|
|
12401
|
+
body TEXT NOT NULL DEFAULT '',
|
|
12402
|
+
action_url TEXT,
|
|
12403
|
+
action_text TEXT,
|
|
12404
|
+
type TEXT NOT NULL DEFAULT 'default',
|
|
12405
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
12406
|
+
read_at TIMESTAMPTZ,
|
|
12407
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
12408
|
+
)
|
|
12409
|
+
`);
|
|
12410
|
+
await sql2.unsafe(`
|
|
12411
|
+
CREATE INDEX IF NOT EXISTS "${table}_user_unread"
|
|
12412
|
+
ON ${tbl} (user_id, created_at DESC)
|
|
12413
|
+
WHERE read_at IS NULL
|
|
12414
|
+
`);
|
|
12415
|
+
await sql2.unsafe(`
|
|
12416
|
+
CREATE INDEX IF NOT EXISTS "${table}_user_all"
|
|
12417
|
+
ON ${tbl} (user_id, created_at DESC)
|
|
12418
|
+
`);
|
|
12419
|
+
await sql2.unsafe(`
|
|
12420
|
+
CREATE TABLE IF NOT EXISTS "_notify_prefs" (
|
|
12421
|
+
user_id INTEGER PRIMARY KEY,
|
|
12422
|
+
channels JSONB NOT NULL DEFAULT '["inbox"]'::jsonb
|
|
12423
|
+
)
|
|
12424
|
+
`);
|
|
12425
|
+
}
|
|
12426
|
+
async function insertNotification(userId2, message) {
|
|
12427
|
+
await sql2.unsafe(
|
|
12428
|
+
`INSERT INTO ${tbl} (user_id, title, body, action_url, action_text, type, metadata)
|
|
12429
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
12430
|
+
[
|
|
12431
|
+
userId2,
|
|
12432
|
+
message.title,
|
|
12433
|
+
message.body ?? "",
|
|
12434
|
+
message.actionUrl ?? null,
|
|
12435
|
+
message.actionText ?? null,
|
|
12436
|
+
message.type ?? "default",
|
|
12437
|
+
message.metadata ?? {}
|
|
12438
|
+
]
|
|
12439
|
+
);
|
|
12440
|
+
}
|
|
12441
|
+
async function send(to, message) {
|
|
12442
|
+
const prefs = await getPreferences(to.userId);
|
|
12443
|
+
if (prefs.channels.includes("inbox")) {
|
|
12444
|
+
await insertNotification(to.userId, message);
|
|
12445
|
+
}
|
|
12446
|
+
if (prefs.channels.includes("email") && mailer2 && to.email) {
|
|
12447
|
+
const html = renderEmail(message);
|
|
12448
|
+
mailer2.send({
|
|
12449
|
+
to: to.email,
|
|
12450
|
+
subject: message.title,
|
|
12451
|
+
text: message.body ?? "",
|
|
12452
|
+
html
|
|
12453
|
+
}).catch((err) => console.error("[notifier] email send failed:", err.message));
|
|
12454
|
+
}
|
|
12455
|
+
if (prefs.channels.includes("ws") && hub) {
|
|
12456
|
+
hub.broadcast(`notify:${to.userId}`, {
|
|
12457
|
+
type: "notification",
|
|
12458
|
+
data: {
|
|
12459
|
+
title: message.title,
|
|
12460
|
+
body: message.body,
|
|
12461
|
+
actionUrl: message.actionUrl,
|
|
12462
|
+
actionText: message.actionText,
|
|
12463
|
+
type: message.type ?? "default",
|
|
12464
|
+
metadata: message.metadata,
|
|
12465
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12466
|
+
}
|
|
12467
|
+
});
|
|
12468
|
+
}
|
|
12469
|
+
}
|
|
12470
|
+
async function broadcast(message) {
|
|
12471
|
+
const rows = await sql2`
|
|
12472
|
+
SELECT user_id FROM "_notify_prefs"
|
|
12473
|
+
WHERE channels @> ${sql2.json(["inbox"])}
|
|
12474
|
+
`;
|
|
12475
|
+
for (const row of rows) {
|
|
12476
|
+
await insertNotification(row.user_id, message);
|
|
12477
|
+
if (hub) {
|
|
12478
|
+
hub.broadcast(`notify:${row.user_id}`, {
|
|
12479
|
+
type: "notification",
|
|
12480
|
+
data: {
|
|
12481
|
+
title: message.title,
|
|
12482
|
+
body: message.body,
|
|
12483
|
+
actionUrl: message.actionUrl,
|
|
12484
|
+
actionText: message.actionText,
|
|
12485
|
+
type: message.type ?? "default",
|
|
12486
|
+
metadata: message.metadata,
|
|
12487
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
12488
|
+
}
|
|
12489
|
+
});
|
|
12490
|
+
}
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
async function unreadCount(userId2) {
|
|
12494
|
+
const [row] = await sql2.unsafe(
|
|
12495
|
+
`SELECT COUNT(*)::int AS count FROM ${tbl} WHERE user_id = $1 AND read_at IS NULL`,
|
|
12496
|
+
[userId2]
|
|
12497
|
+
);
|
|
12498
|
+
return row?.count ?? 0;
|
|
12499
|
+
}
|
|
12500
|
+
async function count(userId2, unreadOnly = false) {
|
|
12501
|
+
if (unreadOnly) return unreadCount(userId2);
|
|
12502
|
+
const [row] = await sql2.unsafe(
|
|
12503
|
+
`SELECT COUNT(*)::int AS count FROM ${tbl} WHERE user_id = $1`,
|
|
12504
|
+
[userId2]
|
|
12505
|
+
);
|
|
12506
|
+
return row?.count ?? 0;
|
|
12507
|
+
}
|
|
12508
|
+
async function markRead(userId2, notificationIds) {
|
|
12509
|
+
if (notificationIds && notificationIds.length > 0) {
|
|
12510
|
+
const ids = notificationIds.map((_, i) => `$${i + 2}`).join(", ");
|
|
12511
|
+
await sql2.unsafe(
|
|
12512
|
+
`UPDATE ${tbl} SET read_at = NOW() WHERE user_id = $1 AND id IN (${ids}) AND read_at IS NULL`,
|
|
12513
|
+
[userId2, ...notificationIds]
|
|
12514
|
+
);
|
|
12515
|
+
} else {
|
|
12516
|
+
await sql2.unsafe(`UPDATE ${tbl} SET read_at = NOW() WHERE user_id = $1 AND read_at IS NULL`, [
|
|
12517
|
+
userId2
|
|
12518
|
+
]);
|
|
12519
|
+
}
|
|
12520
|
+
}
|
|
12521
|
+
async function list(userId2, opts2) {
|
|
12522
|
+
const limit = opts2?.limit ?? pageSize;
|
|
12523
|
+
const offset = opts2?.offset ?? 0;
|
|
12524
|
+
let where = `user_id = $1`;
|
|
12525
|
+
const params = [userId2];
|
|
12526
|
+
const paramIdx = 2;
|
|
12527
|
+
if (opts2?.unreadOnly) {
|
|
12528
|
+
where += ` AND read_at IS NULL`;
|
|
12529
|
+
}
|
|
12530
|
+
const rows = await sql2.unsafe(
|
|
12531
|
+
`SELECT id, user_id, title, body, action_url, action_text, type, metadata, read_at, created_at
|
|
12532
|
+
FROM ${tbl}
|
|
12533
|
+
WHERE ${where}
|
|
12534
|
+
ORDER BY created_at DESC
|
|
12535
|
+
LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`,
|
|
12536
|
+
[...params, limit, offset]
|
|
12537
|
+
);
|
|
12538
|
+
return rows.map(normalizeNotification);
|
|
12539
|
+
}
|
|
12540
|
+
function normalizeNotification(row) {
|
|
12541
|
+
return {
|
|
12542
|
+
...row,
|
|
12543
|
+
metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata ?? {}
|
|
12544
|
+
};
|
|
12545
|
+
}
|
|
12546
|
+
async function getPreferences(userId2) {
|
|
12547
|
+
const [row] = await sql2.unsafe(`SELECT channels FROM "_notify_prefs" WHERE user_id = $1`, [
|
|
12548
|
+
userId2
|
|
12549
|
+
]);
|
|
12550
|
+
if (!row) {
|
|
12551
|
+
return { channels: [...DEFAULT_CHANNELS] };
|
|
12552
|
+
}
|
|
12553
|
+
let channels;
|
|
12554
|
+
if (typeof row.channels === "string") {
|
|
12555
|
+
channels = JSON.parse(row.channels);
|
|
12556
|
+
} else if (Array.isArray(row.channels)) {
|
|
12557
|
+
channels = row.channels;
|
|
12558
|
+
} else {
|
|
12559
|
+
channels = [...DEFAULT_CHANNELS];
|
|
12560
|
+
}
|
|
12561
|
+
return { channels };
|
|
12562
|
+
}
|
|
12563
|
+
async function setPreferences(userId2, prefs) {
|
|
12564
|
+
await sql2`
|
|
12565
|
+
INSERT INTO "_notify_prefs" (user_id, channels)
|
|
12566
|
+
VALUES (${userId2}, ${sql2.json(prefs.channels)})
|
|
12567
|
+
ON CONFLICT (user_id)
|
|
12568
|
+
DO UPDATE SET channels = ${sql2.json(prefs.channels)}
|
|
12569
|
+
`;
|
|
12570
|
+
}
|
|
12571
|
+
async function clean(days) {
|
|
12572
|
+
const result = await sql2.unsafe(`DELETE FROM ${tbl} WHERE created_at < NOW() - $1::interval`, [
|
|
12573
|
+
`${days} days`
|
|
12574
|
+
]);
|
|
12575
|
+
return Array.isArray(result) ? result.length : 0;
|
|
12576
|
+
}
|
|
12577
|
+
function renderEmail(message) {
|
|
12578
|
+
const actionHtml = message.actionUrl ? `
|
|
12579
|
+
<p><a href="${escapeHtml3(message.actionUrl)}" style="display:inline-block;padding:10px 20px;background:#0066cc;color:#fff;text-decoration:none;border-radius:4px">${escapeHtml3(message.actionText ?? "View")}</a></p>` : "";
|
|
12580
|
+
return `<!DOCTYPE html>
|
|
12581
|
+
<html>
|
|
12582
|
+
<head><meta charset="utf-8"></head>
|
|
12583
|
+
<body style="font-family:sans-serif;padding:20px;max-width:600px">
|
|
12584
|
+
<h2>${escapeHtml3(message.title)}</h2>
|
|
12585
|
+
${message.body ? `<p>${escapeHtml3(message.body)}</p>` : ""}
|
|
12586
|
+
${actionHtml}
|
|
12587
|
+
<hr style="margin-top:30px;border:none;border-top:1px solid #eee">
|
|
12588
|
+
<p style="color:#999;font-size:12px">${escapeHtml3(fromName)}</p>
|
|
12589
|
+
</body>
|
|
12590
|
+
</html>`;
|
|
12591
|
+
}
|
|
12592
|
+
function escapeHtml3(s) {
|
|
12593
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
12594
|
+
}
|
|
12595
|
+
const mw = async (req, ctx, next) => {
|
|
12596
|
+
;
|
|
12597
|
+
ctx.notifier = api;
|
|
12598
|
+
return next(req, ctx);
|
|
12599
|
+
};
|
|
12600
|
+
const api = {
|
|
12601
|
+
send,
|
|
12602
|
+
broadcast,
|
|
12603
|
+
unreadCount,
|
|
12604
|
+
count,
|
|
12605
|
+
markRead,
|
|
12606
|
+
list,
|
|
12607
|
+
getPreferences,
|
|
12608
|
+
setPreferences,
|
|
12609
|
+
clean,
|
|
12610
|
+
migrate,
|
|
12611
|
+
close: async () => {
|
|
12612
|
+
}
|
|
12613
|
+
};
|
|
12614
|
+
return Object.assign(mw, api);
|
|
12615
|
+
}
|
|
12189
12616
|
export {
|
|
12190
12617
|
DEFAULT_MAX_BODY,
|
|
12191
12618
|
MIGRATIONS_TABLE,
|
|
@@ -12240,7 +12667,9 @@ export {
|
|
|
12240
12667
|
logdb,
|
|
12241
12668
|
logger,
|
|
12242
12669
|
mailer,
|
|
12670
|
+
mcpClient,
|
|
12243
12671
|
messager,
|
|
12672
|
+
notifier,
|
|
12244
12673
|
openai,
|
|
12245
12674
|
opencode,
|
|
12246
12675
|
permissions,
|