@unson/brainbase-mcp 0.6.0 → 0.8.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.
Files changed (148) hide show
  1. package/contracts/foundation/graph-history.sql +634 -0
  2. package/contracts/judgment-value-proof/schema.json +43 -1
  3. package/dist/canonical-task-contract.d.ts +9 -0
  4. package/dist/canonical-task-contract.js +39 -0
  5. package/dist/canonical-task-principal.d.ts +16 -0
  6. package/dist/canonical-task-principal.js +36 -0
  7. package/dist/canonical-task-service.d.ts +251 -0
  8. package/dist/canonical-task-service.js +820 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +107 -0
  11. package/dist/company-os-evaluation.d.ts +193 -0
  12. package/dist/company-os-evaluation.js +1150 -0
  13. package/dist/company-os-impact-review.d.ts +248 -0
  14. package/dist/company-os-impact-review.js +663 -0
  15. package/dist/company-os-learning-adoption.d.ts +288 -0
  16. package/dist/company-os-learning-adoption.js +944 -0
  17. package/dist/company-os-objectives.d.ts +63 -0
  18. package/dist/company-os-objectives.js +234 -0
  19. package/dist/company-os-receipt-adapter.d.ts +109 -0
  20. package/dist/company-os-receipt-adapter.js +636 -0
  21. package/dist/constraint-exception-store.d.ts +23 -0
  22. package/dist/constraint-exception-store.js +108 -0
  23. package/dist/constraint-resolution.d.ts +175 -0
  24. package/dist/constraint-resolution.js +922 -0
  25. package/dist/decision-adapter.d.ts +145 -0
  26. package/dist/decision-adapter.js +586 -0
  27. package/dist/durable-wait-http.d.ts +51 -0
  28. package/dist/durable-wait-http.js +497 -0
  29. package/dist/durable-waits.d.ts +234 -0
  30. package/dist/durable-waits.js +944 -0
  31. package/dist/execution-authority-http.d.ts +28 -0
  32. package/dist/execution-authority-http.js +359 -0
  33. package/dist/execution-authority.d.ts +201 -0
  34. package/dist/execution-authority.js +634 -0
  35. package/dist/foundation-authenticated-tools.d.ts +56 -0
  36. package/dist/foundation-authenticated-tools.js +347 -0
  37. package/dist/foundation-catalog.d.ts +26 -0
  38. package/dist/foundation-catalog.js +206 -0
  39. package/dist/foundation-constraint-store.d.ts +50 -0
  40. package/dist/foundation-constraint-store.js +215 -0
  41. package/dist/foundation-graph-http.d.ts +43 -0
  42. package/dist/foundation-graph-http.js +243 -0
  43. package/dist/foundation-graph-write.d.ts +90 -0
  44. package/dist/foundation-graph-write.js +447 -0
  45. package/dist/foundation-http.d.ts +90 -0
  46. package/dist/foundation-http.js +439 -0
  47. package/dist/foundation-public-provider.d.ts +140 -0
  48. package/dist/foundation-public-provider.js +192 -0
  49. package/dist/foundation-store.d.ts +132 -0
  50. package/dist/foundation-store.js +654 -0
  51. package/dist/graph-corrections.d.ts +157 -0
  52. package/dist/graph-corrections.js +620 -0
  53. package/dist/graph-foundation-reader.d.ts +60 -0
  54. package/dist/graph-foundation-reader.js +494 -0
  55. package/dist/graph-retrieval.d.ts +6 -1
  56. package/dist/graph-revision-reader.d.ts +80 -0
  57. package/dist/graph-revision-reader.js +246 -0
  58. package/dist/graph-web-http.d.ts +26 -0
  59. package/dist/graph-web-http.js +215 -0
  60. package/dist/graph-web.d.ts +364 -0
  61. package/dist/graph-web.js +639 -0
  62. package/dist/judgment-dag-composition.d.ts +224 -0
  63. package/dist/judgment-dag-composition.js +731 -0
  64. package/dist/judgment-dag.d.ts +2 -0
  65. package/dist/judgment-dag.js +1 -0
  66. package/dist/judgment-problem-snapshot.d.ts +213 -0
  67. package/dist/judgment-problem-snapshot.js +1073 -0
  68. package/dist/judgment-value-proof-review.d.ts +166 -0
  69. package/dist/judgment-value-proof-review.js +437 -0
  70. package/dist/judgment-value-proof.d.ts +28 -1
  71. package/dist/judgment-value-proof.js +53 -8
  72. package/dist/judgment-view-http.d.ts +22 -0
  73. package/dist/judgment-view-http.js +128 -0
  74. package/dist/judgment-view.d.ts +222 -0
  75. package/dist/judgment-view.js +627 -0
  76. package/dist/knowledge-adapter.d.ts +112 -0
  77. package/dist/knowledge-adapter.js +691 -0
  78. package/dist/knowledge-continuation.d.ts +127 -0
  79. package/dist/knowledge-continuation.js +446 -0
  80. package/dist/knowledge-lookup.d.ts +178 -0
  81. package/dist/knowledge-lookup.js +758 -0
  82. package/dist/local-web-fetch-bridge.d.ts +5 -0
  83. package/dist/local-web-fetch-bridge.js +37 -0
  84. package/dist/local-web-host.d.ts +109 -0
  85. package/dist/local-web-host.js +688 -0
  86. package/dist/local-web-security.d.ts +25 -0
  87. package/dist/local-web-security.js +95 -0
  88. package/dist/ontology-foundation.d.ts +209 -0
  89. package/dist/ontology-foundation.js +1000 -0
  90. package/dist/ontology.d.ts +13 -0
  91. package/dist/ontology.js +14 -0
  92. package/dist/organization-connection.d.ts +164 -0
  93. package/dist/organization-connection.js +428 -0
  94. package/dist/organization-graph.d.ts +15 -0
  95. package/dist/organization-graph.js +24 -4
  96. package/dist/personal-knowledge.d.ts +132 -0
  97. package/dist/personal-knowledge.js +779 -0
  98. package/dist/personal-memory-handover.d.ts +108 -0
  99. package/dist/personal-memory-handover.js +369 -0
  100. package/dist/philosophy-revision-reader.d.ts +85 -0
  101. package/dist/philosophy-revision-reader.js +241 -0
  102. package/dist/portable-graph.d.ts +24 -0
  103. package/dist/portable-graph.js +77 -0
  104. package/dist/problem-candidates.d.ts +149 -0
  105. package/dist/problem-candidates.js +898 -0
  106. package/dist/problem-selection.d.ts +238 -0
  107. package/dist/problem-selection.js +995 -0
  108. package/dist/resource-reservation-http.d.ts +31 -0
  109. package/dist/resource-reservation-http.js +371 -0
  110. package/dist/resource-reservations.d.ts +221 -0
  111. package/dist/resource-reservations.js +577 -0
  112. package/dist/server.d.ts +104 -2
  113. package/dist/server.js +186 -4
  114. package/dist/ssot.d.ts +6 -1
  115. package/dist/ssot.js +63 -5
  116. package/dist/streamable-http.d.ts +42 -0
  117. package/dist/streamable-http.js +288 -0
  118. package/dist/types.d.ts +20 -0
  119. package/dist/value-proof-review-http.d.ts +25 -0
  120. package/dist/value-proof-review-http.js +185 -0
  121. package/dist/world-model.d.ts +199 -0
  122. package/dist/world-model.js +955 -0
  123. package/package.json +230 -3
  124. package/ui/brainbase-tokens.css +56 -0
  125. package/ui/graph-own-share.js +367 -0
  126. package/ui/graph-projects-view.css +23 -0
  127. package/ui/graph-projects-view.js +730 -0
  128. package/ui/graph-registry-view.css +101 -0
  129. package/ui/graph-registry-view.js +804 -0
  130. package/ui/graph-view-shared.css +522 -0
  131. package/ui/graph-view-shared.js +1227 -0
  132. package/ui/judgment-view.css +312 -0
  133. package/ui/judgment-view.js +508 -0
  134. package/ui/local-web-shell.css +396 -0
  135. package/ui/local-web-shell.js +378 -0
  136. package/ui/objective-editor-http-port.js +154 -0
  137. package/ui/objective-editor.css +286 -0
  138. package/ui/objective-editor.js +1586 -0
  139. package/ui/outcome-knowledge.css +50 -14
  140. package/ui/outcome-knowledge.js +15 -1
  141. package/ui/outcome-mana.css +91 -14
  142. package/ui/outcome-mana.js +958 -61
  143. package/ui/value-proof-review.css +504 -0
  144. package/ui/value-proof-review.js +1104 -0
  145. package/ui/workspace-kit.css +485 -0
  146. package/ui/workspace-kit.js +229 -0
  147. package/ui/world-model-view.css +188 -0
  148. package/ui/world-model-view.js +486 -0
@@ -0,0 +1,288 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createServer as createHttpServer } from 'node:http';
3
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
+ const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
5
+ const DEFAULT_MCP_PATH = '/mcp';
6
+ const DEFAULT_HEALTH_PATH = '/health';
7
+ class BodyLimitError extends Error {
8
+ constructor() {
9
+ super('Request body exceeds the configured limit');
10
+ this.name = 'BodyLimitError';
11
+ }
12
+ }
13
+ class InvalidJsonError extends Error {
14
+ constructor() {
15
+ super('Parse error: Invalid JSON');
16
+ this.name = 'InvalidJsonError';
17
+ }
18
+ }
19
+ function jsonRpcError(code, message) {
20
+ return {
21
+ jsonrpc: '2.0',
22
+ error: { code, message },
23
+ id: null
24
+ };
25
+ }
26
+ function writeJson(response, statusCode, body, headers = {}) {
27
+ if (response.headersSent)
28
+ return;
29
+ response.statusCode = statusCode;
30
+ response.setHeader('Content-Type', 'application/json');
31
+ for (const [name, value] of Object.entries(headers))
32
+ response.setHeader(name, value);
33
+ response.end(JSON.stringify(body));
34
+ }
35
+ function requestPath(request) {
36
+ try {
37
+ return new URL(request.url ?? '/', 'http://localhost').pathname;
38
+ }
39
+ catch {
40
+ return '/';
41
+ }
42
+ }
43
+ function headerValue(request, name) {
44
+ const value = request.headers[name.toLowerCase()];
45
+ return Array.isArray(value) ? value[0] : value;
46
+ }
47
+ async function readJsonBody(request, limitBytes) {
48
+ const contentLengthHeader = headerValue(request, 'content-length');
49
+ if (contentLengthHeader !== undefined) {
50
+ const contentLength = Number(contentLengthHeader);
51
+ if (Number.isFinite(contentLength) && contentLength > limitBytes) {
52
+ request.resume();
53
+ throw new BodyLimitError();
54
+ }
55
+ }
56
+ const chunks = [];
57
+ let totalBytes = 0;
58
+ await new Promise((resolve, reject) => {
59
+ let settled = false;
60
+ const cleanup = () => {
61
+ request.removeListener('data', onData);
62
+ request.removeListener('end', onEnd);
63
+ request.removeListener('error', onError);
64
+ request.removeListener('aborted', onAborted);
65
+ };
66
+ const finish = (error) => {
67
+ if (settled)
68
+ return;
69
+ settled = true;
70
+ cleanup();
71
+ if (error)
72
+ reject(error);
73
+ else
74
+ resolve();
75
+ };
76
+ const onData = (chunk) => {
77
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
78
+ totalBytes += buffer.byteLength;
79
+ if (totalBytes > limitBytes) {
80
+ request.resume();
81
+ finish(new BodyLimitError());
82
+ return;
83
+ }
84
+ chunks.push(buffer);
85
+ };
86
+ const onEnd = () => finish();
87
+ const onError = () => finish(new Error('Request body could not be read'));
88
+ const onAborted = () => finish(new Error('Request was aborted'));
89
+ request.on('data', onData);
90
+ request.once('end', onEnd);
91
+ request.once('error', onError);
92
+ request.once('aborted', onAborted);
93
+ });
94
+ if (chunks.length === 0)
95
+ throw new InvalidJsonError();
96
+ try {
97
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
98
+ }
99
+ catch {
100
+ throw new InvalidJsonError();
101
+ }
102
+ }
103
+ function assignAuth(request, auth) {
104
+ const authenticatedRequest = request;
105
+ if (auth !== undefined)
106
+ authenticatedRequest.auth = auth;
107
+ return authenticatedRequest;
108
+ }
109
+ export function createStreamableHttpHandler(dependencies) {
110
+ const bodyLimitBytes = dependencies.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
111
+ const mcpPath = dependencies.mcpPath ?? DEFAULT_MCP_PATH;
112
+ const healthPath = dependencies.healthPath ?? DEFAULT_HEALTH_PATH;
113
+ if (!Number.isInteger(bodyLimitBytes) || bodyLimitBytes <= 0) {
114
+ throw new Error('bodyLimitBytes must be a positive integer');
115
+ }
116
+ const sessions = new Map();
117
+ const closeServer = async (session) => {
118
+ if (session.serverClosePromise)
119
+ return session.serverClosePromise;
120
+ session.serverCloseStarted = true;
121
+ session.serverClosePromise = session.server.close();
122
+ await session.serverClosePromise;
123
+ };
124
+ const disposeSession = async (session) => {
125
+ if (session.id)
126
+ sessions.delete(session.id);
127
+ if (session.closed && session.serverCloseStarted) {
128
+ await session.serverClosePromise;
129
+ return;
130
+ }
131
+ session.closed = true;
132
+ try {
133
+ await session.transport.close();
134
+ }
135
+ finally {
136
+ await closeServer(session);
137
+ }
138
+ };
139
+ const createSession = async (scope) => {
140
+ const server = await dependencies.createServer(scope);
141
+ let session;
142
+ const transport = new StreamableHTTPServerTransport({
143
+ sessionIdGenerator: randomUUID,
144
+ enableJsonResponse: true,
145
+ onsessioninitialized: (id) => {
146
+ session.id = id;
147
+ sessions.set(id, session);
148
+ },
149
+ onsessionclosed: (id) => {
150
+ sessions.delete(id);
151
+ session.closed = true;
152
+ }
153
+ });
154
+ session = {
155
+ server,
156
+ transport,
157
+ closed: false,
158
+ serverCloseStarted: false
159
+ };
160
+ transport.onclose = () => {
161
+ session.closed = true;
162
+ if (session.id)
163
+ sessions.delete(session.id);
164
+ void closeServer(session).catch(() => undefined);
165
+ };
166
+ await server.connect(transport);
167
+ return session;
168
+ };
169
+ const resolveScope = async (request, auth, sessionId, requestId) => {
170
+ const base = {
171
+ requestId,
172
+ method: request.method ?? 'GET',
173
+ path: requestPath(request),
174
+ ...(auth === undefined ? {} : { auth }),
175
+ ...(sessionId === undefined ? {} : { sessionId })
176
+ };
177
+ const custom = await dependencies.requestScope?.({ request, requestId, auth, sessionId });
178
+ return custom ? { ...base, ...custom } : base;
179
+ };
180
+ const handle = async (request, response) => {
181
+ const path = requestPath(request);
182
+ const requestId = randomUUID();
183
+ if (path === healthPath && request.method === 'GET') {
184
+ try {
185
+ const result = await dependencies.health?.({ request, requestId });
186
+ writeJson(response, 200, result ?? { status: 'ok' });
187
+ }
188
+ catch {
189
+ writeJson(response, 503, { status: 'unavailable' });
190
+ }
191
+ return;
192
+ }
193
+ if (path !== mcpPath) {
194
+ writeJson(response, 404, { error: 'Not Found' });
195
+ return;
196
+ }
197
+ if (request.method === 'GET') {
198
+ writeJson(response, 405, jsonRpcError(-32000, 'GET is not supported'), { Allow: 'POST, DELETE' });
199
+ return;
200
+ }
201
+ if (request.method !== 'POST' && request.method !== 'DELETE') {
202
+ writeJson(response, 405, jsonRpcError(-32000, 'Method not allowed'), { Allow: 'POST, DELETE' });
203
+ return;
204
+ }
205
+ let auth;
206
+ if (dependencies.authenticate) {
207
+ try {
208
+ const authenticated = await dependencies.authenticate(request);
209
+ if (authenticated === null) {
210
+ writeJson(response, 401, jsonRpcError(-32000, 'Unauthorized'), { 'WWW-Authenticate': 'Bearer' });
211
+ return;
212
+ }
213
+ auth = authenticated;
214
+ }
215
+ catch {
216
+ writeJson(response, 401, jsonRpcError(-32000, 'Unauthorized'), { 'WWW-Authenticate': 'Bearer' });
217
+ return;
218
+ }
219
+ }
220
+ const sessionId = headerValue(request, 'mcp-session-id');
221
+ let session = sessionId ? sessions.get(sessionId) : undefined;
222
+ if (sessionId && !session) {
223
+ writeJson(response, 404, jsonRpcError(-32001, 'Session not found'));
224
+ return;
225
+ }
226
+ try {
227
+ const body = request.method === 'POST' ? await readJsonBody(request, bodyLimitBytes) : undefined;
228
+ const scope = await resolveScope(request, auth, sessionId, requestId);
229
+ if (!session)
230
+ session = await createSession(scope);
231
+ let aborted = false;
232
+ const onClose = () => {
233
+ if (!response.writableFinished) {
234
+ aborted = true;
235
+ if (session)
236
+ void disposeSession(session).catch(() => undefined);
237
+ }
238
+ };
239
+ response.once('close', onClose);
240
+ try {
241
+ await session.transport.handleRequest(assignAuth(request, auth), response, body);
242
+ }
243
+ finally {
244
+ response.removeListener('close', onClose);
245
+ }
246
+ // An uninitialized, stateless request creates no session and must not leak
247
+ // its server. Initialized sessions remain available for list/call/delete.
248
+ if (!session.transport.sessionId && !sessionId && !aborted)
249
+ await disposeSession(session);
250
+ }
251
+ catch (error) {
252
+ if (response.headersSent) {
253
+ if (!response.writableEnded)
254
+ response.destroy();
255
+ return;
256
+ }
257
+ if (error instanceof BodyLimitError) {
258
+ writeJson(response, 413, jsonRpcError(-32013, error.message));
259
+ return;
260
+ }
261
+ if (error instanceof InvalidJsonError) {
262
+ writeJson(response, 400, jsonRpcError(-32700, error.message));
263
+ return;
264
+ }
265
+ writeJson(response, 500, jsonRpcError(-32603, 'Internal server error'));
266
+ }
267
+ };
268
+ return {
269
+ handle,
270
+ close: async () => {
271
+ await Promise.all([...sessions.values()].map((session) => disposeSession(session)));
272
+ sessions.clear();
273
+ },
274
+ get activeSessionCount() {
275
+ return sessions.size;
276
+ }
277
+ };
278
+ }
279
+ export function createStreamableHttpServer(dependencies) {
280
+ const handler = createStreamableHttpHandler(dependencies);
281
+ const server = createHttpServer((request, response) => {
282
+ void handler.handle(request, response);
283
+ });
284
+ server.once('close', () => {
285
+ void handler.close().catch(() => undefined);
286
+ });
287
+ return server;
288
+ }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { FoundationDefinition, FoundationRelationReference, FoundationRevision } from './ontology-foundation.js';
1
2
  export type EntityKind = 'person' | 'org' | 'project' | 'relationship' | 'decision';
2
3
  export type CanonicalEntityKind = 'person' | 'org' | 'project' | 'decision';
3
4
  export type CoreRelation = 'member_of' | 'participates_in' | 'accountable_for' | 'owned_by' | 'governs' | 'supersedes';
@@ -43,6 +44,24 @@ export interface CanonicalEdge {
43
44
  evidenceHash?: string;
44
45
  };
45
46
  }
47
+ /**
48
+ * Versioned definitions that extend the historical Graph v2 contract.
49
+ *
50
+ * This is deliberately an extension of the canonical aggregate rather than a
51
+ * second objectives/world-model file. The old Graph entities and edges keep
52
+ * their historical interpretation; foundation records are validated by their
53
+ * own shared ontology contract.
54
+ */
55
+ export interface FoundationCatalogRecord {
56
+ definition: FoundationDefinition;
57
+ digest: string;
58
+ }
59
+ export interface FoundationCatalog {
60
+ version: 1;
61
+ records: FoundationCatalogRecord[];
62
+ latest: Record<string, FoundationRevision>;
63
+ relations: FoundationRelationReference[];
64
+ }
46
65
  export interface GraphFileV2 {
47
66
  version: 2;
48
67
  ontology: {
@@ -57,6 +76,7 @@ export interface GraphFileV2 {
57
76
  };
58
77
  entities: CanonicalEntity[];
59
78
  edges: CanonicalEdge[];
79
+ foundation?: FoundationCatalog;
60
80
  }
61
81
  export type GraphFile = GraphFileV1 | GraphFileV2;
62
82
  export type CanonicalGraphFile = GraphFile;
@@ -0,0 +1,25 @@
1
+ import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
2
+ export declare const VALUE_PROOF_REVIEW_HTTP_VERSION: "value-proof-review-http.v1";
3
+ export declare const VALUE_PROOF_REVIEW_TOKEN_HEADER = "x-brainbase-review-token";
4
+ export interface ValueProofReviewHttpOptions {
5
+ /** Judgment journal root. Defaults to `~/.brainbase/personal-os/judgment-journal`. */
6
+ readonly journalRoot?: string;
7
+ /** Personal OS data directory that stores owner feedback. */
8
+ readonly dataDir?: string;
9
+ /** Required on mutating requests. */
10
+ readonly token: string;
11
+ readonly basePath?: string;
12
+ readonly now?: () => Date;
13
+ }
14
+ export type ValueProofReviewHttpHandler = (request: IncomingMessage, response: ServerResponse) => Promise<boolean>;
15
+ export declare function createValueProofReviewHttpHandler(options: ValueProofReviewHttpOptions): ValueProofReviewHttpHandler;
16
+ export interface ValueProofReviewHostOptions extends Omit<ValueProofReviewHttpOptions, 'token'> {
17
+ readonly token?: string;
18
+ /** Directory containing the packaged `ui/` files. */
19
+ readonly uiDir?: string;
20
+ }
21
+ /** Local single-owner host. The caller owns `listen()` and must bind to a loopback address. */
22
+ export declare function createValueProofReviewHost(options?: ValueProofReviewHostOptions): {
23
+ readonly server: Server;
24
+ readonly token: string;
25
+ };
@@ -0,0 +1,185 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { createServer } from 'node:http';
4
+ import { join, relative } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { buildJudgmentValueProofReviewHome, readJudgmentValueProofFeedback, readJudgmentValueProofJournal, recordJudgmentValueProofFeedback } from './judgment-value-proof-review.js';
7
+ import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError as HttpError, readJsonObjectBody, requestUrl, tokenMatches, writeHttpError as writeError, writeJson } from './local-web-security.js';
8
+ export const VALUE_PROOF_REVIEW_HTTP_VERSION = 'value-proof-review-http.v1';
9
+ export const VALUE_PROOF_REVIEW_TOKEN_HEADER = 'x-brainbase-review-token';
10
+ const DEFAULT_BASE_PATH = '/api/value-proofs';
11
+ const MAX_BODY_BYTES = 16 * 1024;
12
+ // The review part imports the shared screen pattern (workspace-kit), which reads the shared tokens.
13
+ const UI_FILES = Object.freeze({
14
+ '/ui/brainbase-tokens.css': { file: 'brainbase-tokens.css', type: 'text/css; charset=utf-8' },
15
+ '/ui/workspace-kit.js': { file: 'workspace-kit.js', type: 'text/javascript; charset=utf-8' },
16
+ '/ui/workspace-kit.css': { file: 'workspace-kit.css', type: 'text/css; charset=utf-8' },
17
+ '/ui/value-proof-review.js': { file: 'value-proof-review.js', type: 'text/javascript; charset=utf-8' },
18
+ '/ui/value-proof-review.css': { file: 'value-proof-review.css', type: 'text/css; charset=utf-8' }
19
+ });
20
+ function publicHome(home) {
21
+ if (home.status === 'unavailable')
22
+ return home;
23
+ const strip = (items) => Object.fromEntries(Object.entries(items).map(([section, list]) => [
24
+ section,
25
+ list.map(({ section: itemSection, proof, feedback_history }) => ({ section: itemSection, proof, feedback_history }))
26
+ ]));
27
+ return {
28
+ status: home.status,
29
+ root: home.root,
30
+ coverage: home.coverage,
31
+ sections: strip(home.sections),
32
+ // Holds only decision IDs, counts and states; no local paths.
33
+ delegation_map: home.delegation_map,
34
+ rejected: home.rejected.map((entry) => ({ file: relative(home.root, entry.file), reason: entry.reason }))
35
+ };
36
+ }
37
+ export function createValueProofReviewHttpHandler(options) {
38
+ if (typeof options.token !== 'string' || options.token.length < 16) {
39
+ throw new TypeError('token must be at least 16 characters');
40
+ }
41
+ const basePath = (options.basePath ?? DEFAULT_BASE_PATH).replace(/\/+$/u, '');
42
+ const now = options.now ?? (() => new Date());
43
+ async function readHome() {
44
+ const journal = await readJudgmentValueProofJournal({ root: options.journalRoot });
45
+ const feedback = journal.status === 'available'
46
+ ? await readJudgmentValueProofFeedback({ dataDir: options.dataDir })
47
+ : [];
48
+ return buildJudgmentValueProofReviewHome(journal, feedback, { now: now() });
49
+ }
50
+ async function recordFeedback(request) {
51
+ assertSameOrigin(request);
52
+ if (!tokenMatches(options.token, request.headers[VALUE_PROOF_REVIEW_TOKEN_HEADER])) {
53
+ throw new HttpError(403, 'review_token_required', 'A valid review token is required');
54
+ }
55
+ const body = await readJsonObjectBody(request, MAX_BODY_BYTES);
56
+ const intentId = typeof body.intent_id === 'string' ? body.intent_id : '';
57
+ const attemptId = typeof body.decision_attempt_id === 'string' ? body.decision_attempt_id : '';
58
+ const journal = await readJudgmentValueProofJournal({ root: options.journalRoot });
59
+ if (journal.status === 'unavailable') {
60
+ throw new HttpError(503, 'judgment_journal_unavailable', journal.reason);
61
+ }
62
+ const known = journal.entries.some((entry) => entry.proof.intent_id === intentId
63
+ && entry.proof.decision_attempt_id === attemptId);
64
+ if (!known)
65
+ throw new HttpError(404, 'value_proof_not_found', 'No saved value proof matches this decision');
66
+ try {
67
+ const result = await recordJudgmentValueProofFeedback({
68
+ dataDir: options.dataDir,
69
+ intent_id: intentId,
70
+ decision_attempt_id: attemptId,
71
+ status: body.status,
72
+ summary: typeof body.summary === 'string' ? body.summary : null,
73
+ target_layer: typeof body.target_layer === 'string' ? body.target_layer : null,
74
+ now: now()
75
+ });
76
+ return { statusCode: result.created ? 201 : 200, body: result };
77
+ }
78
+ catch (error) {
79
+ if (error instanceof TypeError)
80
+ throw new HttpError(400, 'invalid_feedback', error.message);
81
+ throw error;
82
+ }
83
+ }
84
+ return async (request, response) => {
85
+ const path = requestUrl(request).pathname;
86
+ if (path !== `${basePath}/home` && path !== `${basePath}/feedback`)
87
+ return false;
88
+ try {
89
+ if (path === `${basePath}/home`) {
90
+ if (request.method !== 'GET')
91
+ throw new HttpError(405, 'method_not_allowed', 'Use GET');
92
+ writeJson(response, 200, publicHome(await readHome()));
93
+ return true;
94
+ }
95
+ if (request.method !== 'POST')
96
+ throw new HttpError(405, 'method_not_allowed', 'Use POST');
97
+ const result = await recordFeedback(request);
98
+ writeJson(response, result.statusCode, result.body);
99
+ }
100
+ catch (error) {
101
+ if (error instanceof HttpError)
102
+ writeError(response, error);
103
+ else
104
+ writeJson(response, 500, { error: { code: 'internal_error', message: 'Unexpected error' } });
105
+ }
106
+ return true;
107
+ };
108
+ }
109
+ function escapeAttribute(value) {
110
+ return value.replace(/[&"<>]/gu, (character) => ({ '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' }[character] ?? character));
111
+ }
112
+ function shellHtml(token, basePath) {
113
+ return `<!doctype html>
114
+ <html lang="ja">
115
+ <head>
116
+ <meta charset="utf-8">
117
+ <meta name="viewport" content="width=device-width, initial-scale=1">
118
+ <meta name="brainbase-review-token" content="${escapeAttribute(token)}">
119
+ <meta name="brainbase-review-base-path" content="${escapeAttribute(basePath)}">
120
+ <title>Brainbase 判断の見返し</title>
121
+ <link rel="stylesheet" href="/ui/brainbase-tokens.css">
122
+ <link rel="stylesheet" href="/ui/workspace-kit.css">
123
+ <link rel="stylesheet" href="/ui/value-proof-review.css">
124
+ </head>
125
+ <body>
126
+ <main id="brainbase-value-proof-review"></main>
127
+ <script type="module" src="/app.js"></script>
128
+ </body>
129
+ </html>
130
+ `;
131
+ }
132
+ const BOOTSTRAP_JS = `import { createValueProofReviewUI } from '/ui/value-proof-review.js';
133
+ const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAttribute('content') ?? '';
134
+ createValueProofReviewUI({
135
+ root: document.getElementById('brainbase-value-proof-review'),
136
+ basePath: meta('brainbase-review-base-path'),
137
+ token: meta('brainbase-review-token')
138
+ });
139
+ `;
140
+ /** Local single-owner host. The caller owns `listen()` and must bind to a loopback address. */
141
+ export function createValueProofReviewHost(options = {}) {
142
+ const token = options.token ?? randomBytes(24).toString('base64url');
143
+ const basePath = (options.basePath ?? DEFAULT_BASE_PATH).replace(/\/+$/u, '');
144
+ const uiDir = options.uiDir ?? fileURLToPath(new URL('../ui/', import.meta.url));
145
+ const handler = createValueProofReviewHttpHandler({ ...options, token, basePath });
146
+ const server = createServer((request, response) => {
147
+ void (async () => {
148
+ if (!isLoopbackHost(request)) {
149
+ writeJson(response, 403, { error: { code: 'host_rejected', message: 'Host must be a loopback address for this server' } });
150
+ return;
151
+ }
152
+ if (await handler(request, response))
153
+ return;
154
+ const path = requestUrl(request).pathname;
155
+ if (request.method !== 'GET') {
156
+ writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'Use GET' } });
157
+ return;
158
+ }
159
+ response.setHeader('Cache-Control', 'no-store');
160
+ response.setHeader('X-Content-Type-Options', 'nosniff');
161
+ if (path === '/') {
162
+ response.setHeader('Content-Type', 'text/html; charset=utf-8');
163
+ response.setHeader('Content-Security-Policy', LOCAL_WEB_CONTENT_SECURITY_POLICY);
164
+ response.setHeader('Referrer-Policy', 'no-referrer');
165
+ response.end(shellHtml(token, basePath));
166
+ return;
167
+ }
168
+ if (path === '/app.js') {
169
+ response.setHeader('Content-Type', 'text/javascript; charset=utf-8');
170
+ response.end(BOOTSTRAP_JS);
171
+ return;
172
+ }
173
+ const asset = UI_FILES[path];
174
+ if (!asset) {
175
+ writeJson(response, 404, { error: { code: 'not_found', message: 'Not found' } });
176
+ return;
177
+ }
178
+ response.setHeader('Content-Type', asset.type);
179
+ response.end(await readFile(join(uiDir, asset.file)));
180
+ })().catch(() => {
181
+ writeJson(response, 500, { error: { code: 'internal_error', message: 'Unexpected error' } });
182
+ });
183
+ });
184
+ return { server, token };
185
+ }