crbro-memory 2.10.0 → 2.11.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/LICENSE +21 -21
- package/README.md +256 -465
- package/SECURITY.md +114 -12
- package/bin/crbro.mjs +539 -300
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +139 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +667 -232
- package/dist/engine/doctor.js +312 -14
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +18 -7
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +721 -162
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +29 -6
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +25 -3
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +797 -176
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +583 -373
- package/dist/sync/git.js +203 -72
- package/dist/sync/space.js +100 -25
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +94 -7
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +9 -5
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -682
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/doctor.d.ts +0 -93
- package/dist/engine/doctor.d.ts.map +0 -1
- package/dist/engine/doctor.js.map +0 -1
- package/dist/engine/evidence.d.ts +0 -26
- package/dist/engine/evidence.d.ts.map +0 -1
- package/dist/engine/evidence.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -64
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/projects.d.ts +0 -20
- package/dist/engine/projects.d.ts.map +0 -1
- package/dist/engine/projects.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -155
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -328
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/quality.d.ts +0 -18
- package/dist/search/quality.d.ts.map +0 -1
- package/dist/search/quality.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -36
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -147
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -118
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -369
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -13
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── crbro-memory serve --http ───────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// The MCP server over Streamable HTTP (the SDK's own transport), for clients
|
|
5
|
+
// that cannot spawn a process. The engine runs in this process exactly like
|
|
6
|
+
// a classic stdio server; each MCP session gets its own McpServer over the
|
|
7
|
+
// shared engines, the way the daemon serves its clients.
|
|
8
|
+
//
|
|
9
|
+
// What protects it:
|
|
10
|
+
// · a bearer token on every request, compared in constant time, never
|
|
11
|
+
// logged, read from CRBRO_HTTP_TOKEN or the OS keychain;
|
|
12
|
+
// · loopback by default; another interface needs --allow-remote, and the
|
|
13
|
+
// command then says plainly there is no TLS and no OAuth;
|
|
14
|
+
// · the Host header must be one this server was started for (DNS
|
|
15
|
+
// rebinding), and a browser Origin is refused unless allowed;
|
|
16
|
+
// · one path (/mcp), bounded bodies, bounded sessions, idle sessions
|
|
17
|
+
// closed, internal errors never echoed.
|
|
18
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.REMOTE_WARNING = exports.SERVE_HELP = exports.TOKEN_NAME = void 0;
|
|
23
|
+
exports.allowedHostHeaders = allowedHostHeaders;
|
|
24
|
+
exports.startHttpServer = startHttpServer;
|
|
25
|
+
exports.resolveToken = resolveToken;
|
|
26
|
+
exports.createToken = createToken;
|
|
27
|
+
exports.main = main;
|
|
28
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
29
|
+
const node_crypto_1 = require("node:crypto");
|
|
30
|
+
const webStandardStreamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js");
|
|
31
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
32
|
+
const common_js_1 = require("./common.js");
|
|
33
|
+
exports.TOKEN_NAME = 'CRBRO_HTTP_TOKEN';
|
|
34
|
+
const MIN_TOKEN_CHARS = 32;
|
|
35
|
+
const PATH_RE = /^\/[A-Za-z0-9._~-]+(\/[A-Za-z0-9._~-]+){0,4}$/;
|
|
36
|
+
exports.SERVE_HELP = `
|
|
37
|
+
crbro-memory serve --http [--port 8787] [--host 127.0.0.1] [--allow-remote] [--new-token] [--allow-secrets]
|
|
38
|
+
|
|
39
|
+
Serve the memory over MCP Streamable HTTP at http://<host>:<port>/mcp, for
|
|
40
|
+
clients that connect to a URL instead of starting a process.
|
|
41
|
+
|
|
42
|
+
--http Required: the transport (stdio is the default command).
|
|
43
|
+
--port <n> Default 8787.
|
|
44
|
+
--host <addr> Default 127.0.0.1 (this machine only).
|
|
45
|
+
--allow-remote Needed for any address that is not loopback. There is
|
|
46
|
+
NO TLS (put a reverse proxy or a tunnel in front) and
|
|
47
|
+
NO OAuth (claude.ai web and mobile custom connectors
|
|
48
|
+
require OAuth, which this server does not offer).
|
|
49
|
+
--allowed-host <h> Extra Host header to accept, e.g. the public name a
|
|
50
|
+
tunnel or proxy forwards (repeatable).
|
|
51
|
+
--path <p> The one path served (default /mcp).
|
|
52
|
+
--new-token Create a new random token in the OS keychain as
|
|
53
|
+
${exports.TOKEN_NAME} (never printed), then serve with it.
|
|
54
|
+
--allow-secrets Also serve crbro_secret. Left out by default: with it,
|
|
55
|
+
anyone holding the token reads every keychain secret.
|
|
56
|
+
|
|
57
|
+
Every request needs "Authorization: Bearer <token>". The token comes from
|
|
58
|
+
the ${exports.TOKEN_NAME} environment variable, else from the keychain:
|
|
59
|
+
crbro-memory secret get ${exports.TOKEN_NAME}
|
|
60
|
+
Ctrl+C stops it and flushes the search index.
|
|
61
|
+
`;
|
|
62
|
+
/** Printed whenever the server listens beyond loopback. */
|
|
63
|
+
exports.REMOTE_WARNING = [
|
|
64
|
+
' ⚠️ Reachable beyond this machine. There is NO TLS: the token and the memory travel in clear text',
|
|
65
|
+
' unless a reverse proxy or a tunnel with HTTPS sits in front. There is NO OAuth: claude.ai web and',
|
|
66
|
+
' mobile custom connectors require OAuth, which this server does not offer.',
|
|
67
|
+
].join('\n');
|
|
68
|
+
class HttpError extends Error {
|
|
69
|
+
status;
|
|
70
|
+
headers;
|
|
71
|
+
constructor(status, message, headers = {}) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.status = status;
|
|
74
|
+
this.headers = headers;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function sendJson(res, status, payload, headers = {}) {
|
|
78
|
+
const body = JSON.stringify(payload);
|
|
79
|
+
res.writeHead(status, {
|
|
80
|
+
...common_js_1.BASE_SECURITY_HEADERS,
|
|
81
|
+
'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none'",
|
|
82
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
83
|
+
'Content-Length': String(Buffer.byteLength(body)),
|
|
84
|
+
...headers,
|
|
85
|
+
});
|
|
86
|
+
res.end(body);
|
|
87
|
+
}
|
|
88
|
+
/** A JSON-RPC error body, the shape MCP clients expect from the transport. */
|
|
89
|
+
const rpcError = (code, message) => ({ jsonrpc: '2.0', error: { code, message }, id: null });
|
|
90
|
+
async function readJsonBody(req, max) {
|
|
91
|
+
const declared = Number(req.headers['content-length'] || 0);
|
|
92
|
+
if (declared > max)
|
|
93
|
+
throw new HttpError(413, 'request body too large');
|
|
94
|
+
const chunks = [];
|
|
95
|
+
let size = 0;
|
|
96
|
+
for await (const chunk of req) {
|
|
97
|
+
size += chunk.length;
|
|
98
|
+
if (size > max)
|
|
99
|
+
throw new HttpError(413, 'request body too large');
|
|
100
|
+
chunks.push(chunk);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new HttpError(400, 'invalid JSON');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Hand one request to the SDK transport and write its answer. The body was
|
|
111
|
+
* already read and bounded by us, so it travels as parsedBody. A JSON answer
|
|
112
|
+
* is buffered and sent with a Content-Length; only an event stream (the GET
|
|
113
|
+
* notification channel) is streamed.
|
|
114
|
+
*/
|
|
115
|
+
async function relay(transport, req, res, body) {
|
|
116
|
+
const headers = new Headers();
|
|
117
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
118
|
+
if (v === undefined || k.startsWith(':'))
|
|
119
|
+
continue;
|
|
120
|
+
headers.set(k, Array.isArray(v) ? v.join(', ') : v);
|
|
121
|
+
}
|
|
122
|
+
const abort = new AbortController();
|
|
123
|
+
res.on('close', () => abort.abort());
|
|
124
|
+
const request = new Request(`http://localhost${req.url || '/'}`, { method: req.method, headers, signal: abort.signal });
|
|
125
|
+
const response = await transport.handleRequest(request, body === undefined ? undefined : { parsedBody: body });
|
|
126
|
+
const out = {};
|
|
127
|
+
response.headers.forEach((v, k) => { out[k] = v; });
|
|
128
|
+
if (!response.body) {
|
|
129
|
+
res.writeHead(response.status, out);
|
|
130
|
+
res.end();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if ((response.headers.get('content-type') || '').includes('text/event-stream')) {
|
|
134
|
+
res.writeHead(response.status, out);
|
|
135
|
+
res.flushHeaders();
|
|
136
|
+
const reader = response.body.getReader();
|
|
137
|
+
res.on('close', () => { reader.cancel().catch(() => undefined); });
|
|
138
|
+
try {
|
|
139
|
+
for (;;) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done)
|
|
142
|
+
break;
|
|
143
|
+
res.write(value);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch { /* the client went away */ }
|
|
147
|
+
res.end();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
151
|
+
out['content-length'] = String(buf.length);
|
|
152
|
+
res.writeHead(response.status, out);
|
|
153
|
+
res.end(buf);
|
|
154
|
+
}
|
|
155
|
+
/** Host header values for a listener on host:port. */
|
|
156
|
+
function allowedHostHeaders(host, port, extra = []) {
|
|
157
|
+
const h = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
158
|
+
const list = new Set([`${h}:${port}`.toLowerCase()]);
|
|
159
|
+
if ((0, common_js_1.isLoopbackHost)(host))
|
|
160
|
+
for (const l of ['127.0.0.1', 'localhost', '[::1]'])
|
|
161
|
+
list.add(`${l}:${port}`);
|
|
162
|
+
for (const e of extra)
|
|
163
|
+
list.add(e.toLowerCase());
|
|
164
|
+
return [...list];
|
|
165
|
+
}
|
|
166
|
+
async function startHttpServer(opts) {
|
|
167
|
+
if (!opts.token || opts.token.length < MIN_TOKEN_CHARS)
|
|
168
|
+
throw new Error(`the token must be at least ${MIN_TOKEN_CHARS} characters`);
|
|
169
|
+
const host = opts.host ?? '127.0.0.1';
|
|
170
|
+
const mcpPath = opts.path ?? '/mcp';
|
|
171
|
+
if (!PATH_RE.test(mcpPath))
|
|
172
|
+
throw new Error('the path must look like /mcp');
|
|
173
|
+
const maxBody = opts.maxBodyBytes ?? 1024 * 1024;
|
|
174
|
+
const maxSessions = opts.maxSessions ?? 32;
|
|
175
|
+
const idleMs = opts.sessionIdleMs ?? 30 * 60_000;
|
|
176
|
+
const log = opts.log ?? (() => undefined);
|
|
177
|
+
const makeServer = opts.makeServer ?? (await defaultServerFactory()).make;
|
|
178
|
+
const allowedOrigins = new Set((opts.allowedOrigins ?? []).map(o => o.toLowerCase()));
|
|
179
|
+
const sessions = new Map();
|
|
180
|
+
let allowed = [];
|
|
181
|
+
let failures = 0;
|
|
182
|
+
const closeSession = async (id) => {
|
|
183
|
+
const s = sessions.get(id);
|
|
184
|
+
if (!s)
|
|
185
|
+
return;
|
|
186
|
+
sessions.delete(id);
|
|
187
|
+
try {
|
|
188
|
+
await s.transport.close();
|
|
189
|
+
}
|
|
190
|
+
catch { /* already closed */ }
|
|
191
|
+
try {
|
|
192
|
+
await s.server.close();
|
|
193
|
+
}
|
|
194
|
+
catch { /* already closed */ }
|
|
195
|
+
};
|
|
196
|
+
const sweep = setInterval(() => {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
for (const [id, s] of sessions)
|
|
199
|
+
if (now - s.lastSeen > idleMs)
|
|
200
|
+
void closeSession(id);
|
|
201
|
+
}, Math.min(idleMs, 60_000));
|
|
202
|
+
sweep.unref();
|
|
203
|
+
const handle = async (req, res) => {
|
|
204
|
+
// 1. Host: before anything else, so a rebound name learns nothing.
|
|
205
|
+
if (!(0, common_js_1.hostAllowed)(req.headers.host, allowed))
|
|
206
|
+
throw new HttpError(421, 'Host not allowed');
|
|
207
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
208
|
+
if (url.pathname !== mcpPath)
|
|
209
|
+
throw new HttpError(404, 'not found');
|
|
210
|
+
const method = req.method || 'GET';
|
|
211
|
+
if (method === 'OPTIONS')
|
|
212
|
+
throw new HttpError(405, 'method not allowed', { Allow: 'GET, POST, DELETE' });
|
|
213
|
+
if (!['GET', 'POST', 'DELETE'].includes(method))
|
|
214
|
+
throw new HttpError(405, 'method not allowed', { Allow: 'GET, POST, DELETE' });
|
|
215
|
+
// 2. Browsers: no CORS, and a foreign Origin is refused outright.
|
|
216
|
+
const origin = req.headers.origin;
|
|
217
|
+
if (origin !== undefined && !allowedOrigins.has(String(origin).toLowerCase()))
|
|
218
|
+
throw new HttpError(403, 'Origin not allowed');
|
|
219
|
+
// 3. The token.
|
|
220
|
+
if (!(0, common_js_1.tokenMatches)((0, common_js_1.bearerOf)(req.headers.authorization), opts.token)) {
|
|
221
|
+
failures++;
|
|
222
|
+
throw new HttpError(401, 'missing or invalid bearer token', { 'WWW-Authenticate': 'Bearer realm="crbro"' });
|
|
223
|
+
}
|
|
224
|
+
// 4. The session.
|
|
225
|
+
const sid = req.headers['mcp-session-id'];
|
|
226
|
+
const sessionId = Array.isArray(sid) ? sid[0] : sid;
|
|
227
|
+
const body = method === 'POST' ? await readJsonBody(req, maxBody) : undefined;
|
|
228
|
+
if (sessionId) {
|
|
229
|
+
const s = sessions.get(sessionId);
|
|
230
|
+
if (!s)
|
|
231
|
+
throw new HttpError(404, 'unknown or expired session');
|
|
232
|
+
s.lastSeen = Date.now();
|
|
233
|
+
await relay(s.transport, req, res, body);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (method !== 'POST' || !(0, types_js_1.isInitializeRequest)(body)) {
|
|
237
|
+
throw new HttpError(400, 'no session: start with an initialize request');
|
|
238
|
+
}
|
|
239
|
+
if (sessions.size >= maxSessions)
|
|
240
|
+
throw new HttpError(503, 'too many open sessions', { 'Retry-After': '30' });
|
|
241
|
+
const server = await makeServer();
|
|
242
|
+
let created = null;
|
|
243
|
+
const transport = new webStandardStreamableHttp_js_1.WebStandardStreamableHTTPServerTransport({
|
|
244
|
+
sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
|
|
245
|
+
// One JSON answer per request, sent whole with a Content-Length: nothing
|
|
246
|
+
// CRBRO does streams, and a plain response survives reverse proxies and
|
|
247
|
+
// the HTTP-inspecting antivirus that mangled chunked ones on the
|
|
248
|
+
// reference machine. The GET stream for notifications stays available.
|
|
249
|
+
enableJsonResponse: true,
|
|
250
|
+
maxRequestBodySize: maxBody,
|
|
251
|
+
onsessioninitialized: (id) => {
|
|
252
|
+
created = { transport, server, lastSeen: Date.now() };
|
|
253
|
+
sessions.set(id, created);
|
|
254
|
+
},
|
|
255
|
+
onsessionclosed: (id) => { void closeSession(id); },
|
|
256
|
+
});
|
|
257
|
+
transport.onclose = () => {
|
|
258
|
+
const id = transport.sessionId;
|
|
259
|
+
if (id && sessions.get(id)?.transport === transport)
|
|
260
|
+
sessions.delete(id);
|
|
261
|
+
};
|
|
262
|
+
await server.connect(transport);
|
|
263
|
+
await relay(transport, req, res, body);
|
|
264
|
+
if (!created) {
|
|
265
|
+
try {
|
|
266
|
+
await transport.close();
|
|
267
|
+
await server.close();
|
|
268
|
+
}
|
|
269
|
+
catch { /* never initialized */ }
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const httpServer = node_http_1.default.createServer((req, res) => {
|
|
273
|
+
for (const [k, v] of Object.entries(common_js_1.BASE_SECURITY_HEADERS))
|
|
274
|
+
res.setHeader(k, v);
|
|
275
|
+
handle(req, res).catch(err => {
|
|
276
|
+
if (res.headersSent) {
|
|
277
|
+
res.destroy();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (err instanceof HttpError) {
|
|
281
|
+
sendJson(res, err.status, rpcError(-32000, err.message), err.headers);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// Never echo an internal error: it can quote paths or brain text.
|
|
285
|
+
log(`internal error on ${req.method} ${(req.url || '').split('?')[0]}: ${err instanceof Error ? err.name : 'error'}`);
|
|
286
|
+
sendJson(res, 500, rpcError(-32603, 'internal error'));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
httpServer.headersTimeout = 10_000;
|
|
291
|
+
httpServer.requestTimeout = 60_000;
|
|
292
|
+
httpServer.maxHeadersCount = 64;
|
|
293
|
+
httpServer.keepAliveTimeout = 5_000;
|
|
294
|
+
await new Promise((resolve, reject) => {
|
|
295
|
+
httpServer.once('error', reject);
|
|
296
|
+
const ready = () => { httpServer.off('error', reject); resolve(); };
|
|
297
|
+
if (opts.socketPath)
|
|
298
|
+
httpServer.listen(opts.socketPath, ready);
|
|
299
|
+
else
|
|
300
|
+
httpServer.listen(opts.port ?? 8787, host, ready);
|
|
301
|
+
});
|
|
302
|
+
const port = opts.socketPath ? 0 : httpServer.address().port;
|
|
303
|
+
allowed = opts.socketPath ? (opts.allowedHosts ?? []).map(h => h.toLowerCase()) : allowedHostHeaders(host, port, opts.allowedHosts);
|
|
304
|
+
const shown = host.includes(':') ? `[${host}]` : host;
|
|
305
|
+
let closing = null;
|
|
306
|
+
return {
|
|
307
|
+
url: opts.socketPath ? `http://localhost${mcpPath}` : `http://${shown}:${port}${mcpPath}`,
|
|
308
|
+
port, host,
|
|
309
|
+
sessions: () => sessions.size,
|
|
310
|
+
close: () => closing ??= (async () => {
|
|
311
|
+
clearInterval(sweep);
|
|
312
|
+
await Promise.all([...sessions.keys()].map(closeSession));
|
|
313
|
+
await new Promise(resolve => { httpServer.close(() => resolve()); httpServer.closeAllConnections?.(); });
|
|
314
|
+
if (failures)
|
|
315
|
+
log(`${failures} request(s) were refused for a missing or wrong token`);
|
|
316
|
+
if (opts.onStop)
|
|
317
|
+
await opts.onStop();
|
|
318
|
+
})(),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* One set of engines for the process, one McpServer per session — the
|
|
323
|
+
* daemon's arrangement. crbro_secret is left out unless asked for: a token
|
|
324
|
+
* holder would otherwise read every keychain secret over the network.
|
|
325
|
+
*/
|
|
326
|
+
async function defaultServerFactory(allowSecrets = false) {
|
|
327
|
+
const { createEngines, createServer } = await import('../server.js');
|
|
328
|
+
const engines = createEngines();
|
|
329
|
+
void engines.searchEngine.init().catch(() => undefined);
|
|
330
|
+
const options = allowSecrets ? {} : { excludeTools: ['crbro_secret'] };
|
|
331
|
+
return { make: () => createServer(engines, options), flush: () => engines.searchEngine.flush() };
|
|
332
|
+
}
|
|
333
|
+
async function resolveToken(env = process.env) {
|
|
334
|
+
const fromEnv = (env[exports.TOKEN_NAME] || '').trim();
|
|
335
|
+
if (fromEnv)
|
|
336
|
+
return { token: fromEnv, from: 'env' };
|
|
337
|
+
try {
|
|
338
|
+
const kc = await import('../engine/keychain.js');
|
|
339
|
+
const v = kc.getSecret(exports.TOKEN_NAME);
|
|
340
|
+
return v ? { token: v.trim(), from: 'keychain' } : { token: null, from: null };
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
return { token: null, from: null, problem: `the keychain could not be read (${err instanceof Error ? err.message : 'unavailable'})` };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function createToken() {
|
|
347
|
+
const kc = await import('../engine/keychain.js');
|
|
348
|
+
kc.setSecret(exports.TOKEN_NAME, (0, common_js_1.randomToken)(), 'crbro-memory serve --http bearer token');
|
|
349
|
+
}
|
|
350
|
+
// ─── CLI ─────────────────────────────────────────────────────────
|
|
351
|
+
/** CLI entry: resolves with the exit code once the server is stopped. */
|
|
352
|
+
async function main(argv, deps = {}) {
|
|
353
|
+
const env = deps.env ?? process.env;
|
|
354
|
+
const a = (0, common_js_1.parseArgs)(argv, { http: 'boolean', port: 'number', host: 'string', 'allow-remote': 'boolean', 'new-token': 'boolean', 'allowed-host': 'list', path: 'string', 'allow-secrets': 'boolean' });
|
|
355
|
+
if (a.flags.help) {
|
|
356
|
+
console.log(exports.SERVE_HELP);
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
if (!a.flags.http)
|
|
360
|
+
a.errors.push('--http is required (the stdio server is plain `crbro-memory`)');
|
|
361
|
+
const port = a.flags.port ?? 8787;
|
|
362
|
+
if (port > 65535)
|
|
363
|
+
a.errors.push('--port must be 0-65535 (0 = any free port)');
|
|
364
|
+
const host = (a.flags.host ?? '127.0.0.1').trim();
|
|
365
|
+
if (!host || /[\s/]/.test(host))
|
|
366
|
+
a.errors.push('--host must be an address');
|
|
367
|
+
if (a.positionals.length)
|
|
368
|
+
a.errors.push(`unexpected argument: ${a.positionals[0]}`);
|
|
369
|
+
const mcpPath = a.flags.path ?? '/mcp';
|
|
370
|
+
if (!PATH_RE.test(mcpPath))
|
|
371
|
+
a.errors.push('--path must look like /mcp');
|
|
372
|
+
const extraHosts = a.flags['allowed-host'] ?? [];
|
|
373
|
+
for (const h of extraHosts)
|
|
374
|
+
if (!/^[A-Za-z0-9.\-[\]:]{1,255}$/.test(h))
|
|
375
|
+
a.errors.push(`--allowed-host ${h} is not a host[:port]`);
|
|
376
|
+
if (a.errors.length) {
|
|
377
|
+
console.error(` ❌ ${a.errors.join('; ')}`);
|
|
378
|
+
console.error(' Usage: crbro-memory serve --http [--port 8787] [--host 127.0.0.1] [--allow-remote] [--new-token] (--help for more)');
|
|
379
|
+
return 2;
|
|
380
|
+
}
|
|
381
|
+
const remote = !(0, common_js_1.isLoopbackHost)(host);
|
|
382
|
+
if (remote && !a.flags['allow-remote']) {
|
|
383
|
+
console.error(` ❌ ${host} is not a loopback address. Serving the memory beyond this machine needs --allow-remote,`);
|
|
384
|
+
console.error(' and you should read what that means first: crbro-memory serve --help');
|
|
385
|
+
return 2;
|
|
386
|
+
}
|
|
387
|
+
if (a.flags['new-token']) {
|
|
388
|
+
try {
|
|
389
|
+
await createToken();
|
|
390
|
+
console.log(` 🔐 New token stored in the OS keychain as ${exports.TOKEN_NAME} (not printed).`);
|
|
391
|
+
console.log(` Read it to configure a client: crbro-memory secret get ${exports.TOKEN_NAME}`);
|
|
392
|
+
if ((env[exports.TOKEN_NAME] || '').trim())
|
|
393
|
+
console.log(` ⚠️ ${exports.TOKEN_NAME} is also set in the environment, and the environment wins: unset it to use the new token.`);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
console.error(` ❌ could not store a token: ${err instanceof Error ? err.message : String(err)}`);
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const src = await resolveToken(env);
|
|
401
|
+
if (!src.token) {
|
|
402
|
+
console.error(` ❌ No token. ${src.problem ? `${src.problem[0].toUpperCase()}${src.problem.slice(1)}. ` : ''}Create one with: crbro-memory serve --http --new-token`);
|
|
403
|
+
console.error(` or set ${exports.TOKEN_NAME} in the environment (at least ${MIN_TOKEN_CHARS} characters).`);
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
if (src.token.length < MIN_TOKEN_CHARS) {
|
|
407
|
+
console.error(` ❌ The ${exports.TOKEN_NAME} token is shorter than ${MIN_TOKEN_CHARS} characters. Make a new one: crbro-memory serve --http --new-token`);
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
let factory;
|
|
411
|
+
let running;
|
|
412
|
+
try {
|
|
413
|
+
factory = await defaultServerFactory(a.flags['allow-secrets'] === true);
|
|
414
|
+
running = await startHttpServer({
|
|
415
|
+
token: src.token, port, host, path: mcpPath, allowedHosts: extraHosts, makeServer: factory.make,
|
|
416
|
+
onStop: async () => { try {
|
|
417
|
+
await factory.flush();
|
|
418
|
+
}
|
|
419
|
+
catch { /* index is derived data */ } },
|
|
420
|
+
log: line => console.error(` [http] ${line}`),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
425
|
+
console.error(` ❌ serve: ${/EADDRINUSE/.test(msg) ? `port ${port} is in use; pick another with --port` : msg}`);
|
|
426
|
+
return 1;
|
|
427
|
+
}
|
|
428
|
+
console.log('');
|
|
429
|
+
console.log(` 🧠 CRBRO MCP over Streamable HTTP: ${running.url}`);
|
|
430
|
+
console.log(` Auth: Authorization: Bearer <token> (token from ${src.from === 'env' ? `the ${exports.TOKEN_NAME} environment variable` : `the keychain, ${exports.TOKEN_NAME}`}).`);
|
|
431
|
+
console.log(a.flags['allow-secrets'] === true
|
|
432
|
+
? ' ⚠️ crbro_secret is served (--allow-secrets): anyone with the token can read every keychain secret.'
|
|
433
|
+
: ' crbro_secret is not served (keychain secrets stay off HTTP); --allow-secrets adds it.');
|
|
434
|
+
if (remote)
|
|
435
|
+
console.log(exports.REMOTE_WARNING);
|
|
436
|
+
try {
|
|
437
|
+
const { daemonEnabled } = await import('../daemon/endpoint.js');
|
|
438
|
+
const { resolveBrainDir } = await import('../engine/brain.js');
|
|
439
|
+
if (daemonEnabled(resolveBrainDir())) {
|
|
440
|
+
console.log(' ℹ️ The daemon is on for this brain. This server runs its own engine in this process, like a client');
|
|
441
|
+
console.log(' with the daemon off: writes are safe (files are locked), but the two indexes refresh separately.');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch { /* informational only */ }
|
|
445
|
+
console.log(' Ctrl+C to stop.');
|
|
446
|
+
console.log('');
|
|
447
|
+
deps.onReady?.(running);
|
|
448
|
+
await (0, common_js_1.waitForStop)(deps.signal);
|
|
449
|
+
await running.close();
|
|
450
|
+
console.log(' HTTP server stopped.');
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
//# sourceMappingURL=serve.js.map
|