@unson/brainbase-mcp 0.12.0 → 0.13.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.
@@ -0,0 +1,40 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type JudgmentProblemReferenceProvider, type JudgmentProblemSnapshotAccessProvider } from './judgment-problem-snapshot.js';
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ /** Identity resolved by the host after authentication. Request bodies never supply it. */
5
+ export interface TrustedJudgmentProblemSnapshotRequestContext {
6
+ principal: string;
7
+ /** A non-empty value means that the host already verified the mutation origin. */
8
+ verifiedMutationOrigin?: string;
9
+ }
10
+ /**
11
+ * The host binds the reference resolution (for example the Foundation store
12
+ * and its own resolvers for authority, resource and deadline) to the trusted
13
+ * request context. The access provider defaults to the snapshot read policy.
14
+ */
15
+ export type JudgmentProblemSnapshotProviderFactory = (context: TrustedJudgmentProblemSnapshotRequestContext) => MaybePromise<{
16
+ referenceProvider: JudgmentProblemReferenceProvider;
17
+ accessProvider?: JudgmentProblemSnapshotAccessProvider;
18
+ }>;
19
+ export interface JudgmentProblemSnapshotHttpOptions {
20
+ /** Artifact root of the immutable snapshot store. */
21
+ root: string;
22
+ providerFactory: JudgmentProblemSnapshotProviderFactory;
23
+ basePath?: string;
24
+ bodyLimitBytes?: number;
25
+ /** Required for hosts that do not attach verifiedMutationOrigin to context. */
26
+ verifyMutationRequest?: (input: {
27
+ request: IncomingMessage;
28
+ context: TrustedJudgmentProblemSnapshotRequestContext;
29
+ }) => MaybePromise<boolean>;
30
+ }
31
+ /** A composable route handler. `false` means that the request is outside this route. */
32
+ export type JudgmentProblemSnapshotHttpHandler = (request: IncomingMessage, response: ServerResponse, context: TrustedJudgmentProblemSnapshotRequestContext | null) => Promise<boolean>;
33
+ /**
34
+ * Saves and reads immutable judgment problem snapshots over HTTP. The host
35
+ * authenticates, binds the principal, and supplies the reference resolution;
36
+ * this handler never takes identity from the request body.
37
+ */
38
+ export declare function createJudgmentProblemSnapshotHttpHandler(options: JudgmentProblemSnapshotHttpOptions): JudgmentProblemSnapshotHttpHandler;
39
+ export declare const createJudgmentProblemSnapshotRoute: typeof createJudgmentProblemSnapshotHttpHandler;
40
+ export {};
@@ -0,0 +1,255 @@
1
+ import { JudgmentProblemSnapshotError, loadJudgmentProblemSnapshot, saveJudgmentProblemSnapshot, } from './judgment-problem-snapshot.js';
2
+ const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
3
+ const DEFAULT_BASE_PATH = '/judgment-problem-snapshots';
4
+ const snapshotIdPattern = /^sha256:[0-9a-f]{64}$/u;
5
+ class HttpInputError extends Error {
6
+ }
7
+ class BodyLimitError extends Error {
8
+ constructor() {
9
+ super('Request body exceeds the configured limit');
10
+ }
11
+ }
12
+ function requestPath(request) {
13
+ try {
14
+ return new URL(request.url ?? '/', 'http://localhost').pathname;
15
+ }
16
+ catch {
17
+ return '/';
18
+ }
19
+ }
20
+ function headerValue(request, name) {
21
+ const value = request.headers[name.toLowerCase()];
22
+ return Array.isArray(value) ? value[0] : value;
23
+ }
24
+ function writeJson(response, statusCode, body, headers = {}) {
25
+ if (response.headersSent)
26
+ return;
27
+ response.statusCode = statusCode;
28
+ response.setHeader('Content-Type', 'application/json');
29
+ for (const [name, value] of Object.entries(headers))
30
+ response.setHeader(name, value);
31
+ response.end(JSON.stringify(body));
32
+ }
33
+ function writeError(response, statusCode, code, message, headers = {}) {
34
+ writeJson(response, statusCode, { error: { code, message } }, headers);
35
+ }
36
+ async function readJsonBody(request, limitBytes) {
37
+ const contentLength = Number(headerValue(request, 'content-length'));
38
+ if (Number.isFinite(contentLength) && contentLength > limitBytes) {
39
+ request.resume();
40
+ throw new BodyLimitError();
41
+ }
42
+ const chunks = [];
43
+ let totalBytes = 0;
44
+ await new Promise((resolve, reject) => {
45
+ let settled = false;
46
+ const finish = (error) => {
47
+ if (settled)
48
+ return;
49
+ settled = true;
50
+ request.removeListener('data', onData);
51
+ request.removeListener('end', onEnd);
52
+ request.removeListener('error', onError);
53
+ request.removeListener('aborted', onError);
54
+ if (error)
55
+ reject(error);
56
+ else
57
+ resolve();
58
+ };
59
+ const onData = (chunk) => {
60
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
61
+ totalBytes += buffer.byteLength;
62
+ if (totalBytes > limitBytes) {
63
+ request.resume();
64
+ finish(new BodyLimitError());
65
+ return;
66
+ }
67
+ chunks.push(buffer);
68
+ };
69
+ const onEnd = () => finish();
70
+ const onError = () => finish(new HttpInputError('Request body could not be read'));
71
+ request.on('data', onData);
72
+ request.once('end', onEnd);
73
+ request.once('error', onError);
74
+ request.once('aborted', onError);
75
+ });
76
+ if (chunks.length === 0)
77
+ throw new HttpInputError('Request body must be a JSON object');
78
+ try {
79
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
80
+ }
81
+ catch {
82
+ throw new HttpInputError('Request body must contain valid JSON');
83
+ }
84
+ }
85
+ function normalizeBasePath(basePath) {
86
+ const value = basePath ?? DEFAULT_BASE_PATH;
87
+ if (!value.startsWith('/'))
88
+ throw new TypeError('basePath must be an absolute URL path');
89
+ const normalized = value.replace(/\/+$/u, '');
90
+ if (normalized.length === 0)
91
+ throw new TypeError('basePath must not be root');
92
+ return normalized;
93
+ }
94
+ function matchRoute(pathname, basePath) {
95
+ const normalized = pathname.replace(/\/+$/u, '') || '/';
96
+ if (normalized === basePath)
97
+ return { operation: 'save' };
98
+ if (!normalized.startsWith(`${basePath}/`))
99
+ return null;
100
+ const rest = normalized.slice(basePath.length + 1);
101
+ if (rest.length === 0 || rest.includes('/'))
102
+ return { operation: 'unknown' };
103
+ try {
104
+ return { operation: 'read', snapshotId: decodeURIComponent(rest) };
105
+ }
106
+ catch {
107
+ return { operation: 'read', snapshotId: null };
108
+ }
109
+ }
110
+ function isPlainRecord(value) {
111
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
112
+ && Object.getPrototypeOf(value) === Object.prototype;
113
+ }
114
+ function parseSaveBody(body) {
115
+ if (!isPlainRecord(body))
116
+ throw new HttpInputError('Request body must be a JSON object');
117
+ const keys = Object.keys(body);
118
+ if (keys.length !== 1 || keys[0] !== 'snapshot')
119
+ throw new HttpInputError('Request body must contain only snapshot');
120
+ if (!isPlainRecord(body.snapshot))
121
+ throw new HttpInputError('snapshot must be a JSON object');
122
+ return body.snapshot;
123
+ }
124
+ const STATUS_BY_CODE = {
125
+ invalid_request: 400,
126
+ missing_reference: 422,
127
+ not_applicable: 422,
128
+ unresolved_constraint: 422,
129
+ unauthorized: 403,
130
+ not_found: 404,
131
+ conflict: 409,
132
+ integrity_mismatch: 409,
133
+ storage_io_error: 503,
134
+ };
135
+ function snapshotError(error) {
136
+ if (error instanceof JudgmentProblemSnapshotError) {
137
+ return { status: STATUS_BY_CODE[error.code] ?? 500, code: error.code, message: error.message };
138
+ }
139
+ return { status: 500, code: 'internal_error', message: 'Judgment problem snapshot request failed' };
140
+ }
141
+ function assertContext(context) {
142
+ if (typeof context.principal !== 'string' || context.principal.trim().length === 0 || context.principal.length > 512) {
143
+ throw new HttpInputError('Authenticated principal is required');
144
+ }
145
+ }
146
+ async function mutationVerified(options, request, context) {
147
+ if (typeof context.verifiedMutationOrigin === 'string' && context.verifiedMutationOrigin.trim().length > 0)
148
+ return true;
149
+ if (!options.verifyMutationRequest)
150
+ return false;
151
+ try {
152
+ return (await options.verifyMutationRequest({ request, context })) === true;
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
158
+ /**
159
+ * Saves and reads immutable judgment problem snapshots over HTTP. The host
160
+ * authenticates, binds the principal, and supplies the reference resolution;
161
+ * this handler never takes identity from the request body.
162
+ */
163
+ export function createJudgmentProblemSnapshotHttpHandler(options) {
164
+ if (!options || typeof options !== 'object')
165
+ throw new TypeError('options are required');
166
+ if (typeof options.root !== 'string' || options.root.trim().length === 0)
167
+ throw new TypeError('root must be a non-empty path');
168
+ if (typeof options.providerFactory !== 'function')
169
+ throw new TypeError('providerFactory is required');
170
+ const basePath = normalizeBasePath(options.basePath);
171
+ const bodyLimitBytes = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
172
+ if (!Number.isInteger(bodyLimitBytes) || bodyLimitBytes <= 0)
173
+ throw new TypeError('bodyLimitBytes must be a positive integer');
174
+ return async (request, response, context) => {
175
+ const route = matchRoute(requestPath(request), basePath);
176
+ if (!route)
177
+ return false;
178
+ if (!context) {
179
+ writeError(response, 401, 'unauthorized', 'Authenticated judgment problem snapshot context is required');
180
+ return true;
181
+ }
182
+ try {
183
+ assertContext(context);
184
+ }
185
+ catch (error) {
186
+ writeError(response, 401, 'unauthorized', error instanceof Error ? error.message : 'Authenticated principal is required');
187
+ return true;
188
+ }
189
+ if (route.operation === 'unknown') {
190
+ writeError(response, 404, 'not_found', 'Judgment problem snapshot route was not found');
191
+ return true;
192
+ }
193
+ const method = (request.method ?? 'GET').toUpperCase();
194
+ const allowed = route.operation === 'save' ? 'POST' : 'GET';
195
+ if (method !== allowed) {
196
+ writeError(response, 405, 'method_not_allowed', 'Method is not allowed for this judgment problem snapshot route', { Allow: allowed });
197
+ return true;
198
+ }
199
+ if (route.operation === 'read') {
200
+ if (!route.snapshotId || !snapshotIdPattern.test(route.snapshotId)) {
201
+ writeError(response, 400, 'invalid_request', 'snapshot_id must be sha256:<64 lowercase hex>');
202
+ return true;
203
+ }
204
+ try {
205
+ const providers = await options.providerFactory(context);
206
+ const snapshot = await loadJudgmentProblemSnapshot({
207
+ root: options.root,
208
+ snapshot_id: route.snapshotId,
209
+ access: { principal: context.principal },
210
+ reference_resolution: 'current',
211
+ referenceProvider: providers.referenceProvider,
212
+ ...(providers.accessProvider ? { accessProvider: providers.accessProvider } : {}),
213
+ });
214
+ writeJson(response, 200, { action: 'read', snapshotId: route.snapshotId, result: snapshot });
215
+ }
216
+ catch (error) {
217
+ const failure = snapshotError(error);
218
+ writeError(response, failure.status, failure.code, failure.message);
219
+ }
220
+ return true;
221
+ }
222
+ let snapshot;
223
+ try {
224
+ snapshot = parseSaveBody(await readJsonBody(request, bodyLimitBytes));
225
+ }
226
+ catch (error) {
227
+ if (error instanceof BodyLimitError)
228
+ writeError(response, 413, 'body_too_large', error.message);
229
+ else
230
+ writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
231
+ return true;
232
+ }
233
+ if (!await mutationVerified(options, request, context)) {
234
+ writeError(response, 403, 'mutation_origin_unverified', 'Mutation origin could not be verified');
235
+ return true;
236
+ }
237
+ try {
238
+ const providers = await options.providerFactory(context);
239
+ const receipt = await saveJudgmentProblemSnapshot({
240
+ root: options.root,
241
+ snapshot,
242
+ access: { principal: context.principal },
243
+ referenceProvider: providers.referenceProvider,
244
+ ...(providers.accessProvider ? { accessProvider: providers.accessProvider } : {}),
245
+ });
246
+ writeJson(response, receipt.status === 'created' ? 201 : 200, { action: 'save', result: receipt });
247
+ }
248
+ catch (error) {
249
+ const failure = snapshotError(error);
250
+ writeError(response, failure.status, failure.code, failure.message);
251
+ }
252
+ return true;
253
+ };
254
+ }
255
+ export const createJudgmentProblemSnapshotRoute = createJudgmentProblemSnapshotHttpHandler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -155,6 +155,10 @@
155
155
  "types": "./dist/judgment-problem-snapshot.d.ts",
156
156
  "import": "./dist/judgment-problem-snapshot.js"
157
157
  },
158
+ "./judgment-problem-snapshot-http": {
159
+ "types": "./dist/judgment-problem-snapshot-http.d.ts",
160
+ "import": "./dist/judgment-problem-snapshot-http.js"
161
+ },
158
162
  "./personal-knowledge": {
159
163
  "types": "./dist/personal-knowledge.d.ts",
160
164
  "import": "./dist/personal-knowledge.js"
@@ -348,5 +352,5 @@
348
352
  "engines": {
349
353
  "node": ">=20"
350
354
  },
351
- "gitHead": "084154257b751ba7bf6b303aa889c42ac551c9b9"
355
+ "gitHead": "c9b9adbff2dd409f77ed868d9c3939b3269f2d4f"
352
356
  }