@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.
- package/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-contract.d.ts +9 -0
- package/dist/canonical-task-contract.js +39 -0
- package/dist/canonical-task-principal.d.ts +16 -0
- package/dist/canonical-task-principal.js +36 -0
- package/dist/canonical-task-service.d.ts +251 -0
- package/dist/canonical-task-service.js +820 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-evaluation.d.ts +193 -0
- package/dist/company-os-evaluation.js +1150 -0
- package/dist/company-os-impact-review.d.ts +248 -0
- package/dist/company-os-impact-review.js +663 -0
- package/dist/company-os-learning-adoption.d.ts +288 -0
- package/dist/company-os-learning-adoption.js +944 -0
- package/dist/company-os-objectives.d.ts +63 -0
- package/dist/company-os-objectives.js +234 -0
- package/dist/company-os-receipt-adapter.d.ts +109 -0
- package/dist/company-os-receipt-adapter.js +636 -0
- package/dist/constraint-exception-store.d.ts +23 -0
- package/dist/constraint-exception-store.js +108 -0
- package/dist/constraint-resolution.d.ts +175 -0
- package/dist/constraint-resolution.js +922 -0
- package/dist/decision-adapter.d.ts +145 -0
- package/dist/decision-adapter.js +586 -0
- package/dist/durable-wait-http.d.ts +51 -0
- package/dist/durable-wait-http.js +497 -0
- package/dist/durable-waits.d.ts +234 -0
- package/dist/durable-waits.js +944 -0
- package/dist/execution-authority-http.d.ts +28 -0
- package/dist/execution-authority-http.js +359 -0
- package/dist/execution-authority.d.ts +201 -0
- package/dist/execution-authority.js +634 -0
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -0
- package/dist/foundation-catalog.d.ts +26 -0
- package/dist/foundation-catalog.js +206 -0
- package/dist/foundation-constraint-store.d.ts +50 -0
- package/dist/foundation-constraint-store.js +215 -0
- package/dist/foundation-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -0
- package/dist/judgment-dag-composition.d.ts +224 -0
- package/dist/judgment-dag-composition.js +731 -0
- package/dist/judgment-dag.d.ts +2 -0
- package/dist/judgment-dag.js +1 -0
- package/dist/judgment-problem-snapshot.d.ts +213 -0
- package/dist/judgment-problem-snapshot.js +1073 -0
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- package/dist/judgment-view-http.d.ts +22 -0
- package/dist/judgment-view-http.js +128 -0
- package/dist/judgment-view.d.ts +222 -0
- package/dist/judgment-view.js +627 -0
- package/dist/knowledge-adapter.d.ts +112 -0
- package/dist/knowledge-adapter.js +691 -0
- package/dist/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -0
- package/dist/ontology-foundation.d.ts +209 -0
- package/dist/ontology-foundation.js +1000 -0
- package/dist/ontology.d.ts +13 -0
- package/dist/ontology.js +14 -0
- package/dist/organization-connection.d.ts +164 -0
- package/dist/organization-connection.js +428 -0
- package/dist/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +132 -0
- package/dist/personal-knowledge.js +779 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -0
- package/dist/problem-candidates.d.ts +149 -0
- package/dist/problem-candidates.js +898 -0
- package/dist/problem-selection.d.ts +238 -0
- package/dist/problem-selection.js +995 -0
- package/dist/resource-reservation-http.d.ts +31 -0
- package/dist/resource-reservation-http.js +371 -0
- package/dist/resource-reservations.d.ts +221 -0
- package/dist/resource-reservations.js +577 -0
- package/dist/server.d.ts +104 -2
- package/dist/server.js +186 -4
- package/dist/ssot.d.ts +6 -1
- package/dist/ssot.js +63 -5
- package/dist/streamable-http.d.ts +42 -0
- package/dist/streamable-http.js +288 -0
- package/dist/types.d.ts +20 -0
- package/dist/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +199 -0
- package/dist/world-model.js +955 -0
- package/package.json +230 -3
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +312 -0
- package/ui/judgment-view.js +508 -0
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +286 -0
- package/ui/objective-editor.js +1586 -0
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { ResourceReservationService } from './resource-reservations.js';
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
/** Context resolved by the host after authentication and tenant selection. */
|
|
5
|
+
export interface TrustedResourceReservationRequestContext {
|
|
6
|
+
tenantId: string;
|
|
7
|
+
principal: string;
|
|
8
|
+
scopeId: string;
|
|
9
|
+
/** A non-empty value means that the host already verified the mutation origin. */
|
|
10
|
+
verifiedMutationOrigin?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The factory is the boundary where a host binds a service to its trusted context. */
|
|
13
|
+
export type ResourceReservationServiceFactory = (context: TrustedResourceReservationRequestContext) => MaybePromise<ResourceReservationService>;
|
|
14
|
+
export interface ResourceReservationMutationVerificationInput {
|
|
15
|
+
request: IncomingMessage;
|
|
16
|
+
context: TrustedResourceReservationRequestContext;
|
|
17
|
+
}
|
|
18
|
+
export interface ResourceReservationHttpOptions {
|
|
19
|
+
serviceFactory: ResourceReservationServiceFactory;
|
|
20
|
+
basePath?: string;
|
|
21
|
+
bodyLimitBytes?: number;
|
|
22
|
+
/** Required for hosts that do not attach verifiedMutationOrigin to context. */
|
|
23
|
+
verifyMutationRequest?: (input: ResourceReservationMutationVerificationInput) => MaybePromise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A composable route handler. `false` means that the request is outside this
|
|
27
|
+
* route and may be passed to the host's next handler.
|
|
28
|
+
*/
|
|
29
|
+
export type ResourceReservationHttpHandler = (request: IncomingMessage, response: ServerResponse, context: TrustedResourceReservationRequestContext | null) => Promise<boolean>;
|
|
30
|
+
export declare function createResourceReservationHttpHandler(options: ResourceReservationHttpOptions): ResourceReservationHttpHandler;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ResourceReservationError, } from './resource-reservations.js';
|
|
3
|
+
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
4
|
+
const DEFAULT_BASE_PATH = '/resource-reservations';
|
|
5
|
+
const snapshotIdPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
6
|
+
const boundedText = z.string().min(1).max(512).refine((value) => !/[\u0000-\u001f\u007f]/u.test(value), 'must be a bounded text value');
|
|
7
|
+
const periodSchema = z.object({
|
|
8
|
+
startsAt: boundedText,
|
|
9
|
+
endsAt: boundedText,
|
|
10
|
+
}).strict();
|
|
11
|
+
const problemSchema = z.object({
|
|
12
|
+
problem_snapshot_id: z.string().min(1).max(512).regex(snapshotIdPattern),
|
|
13
|
+
problem_id: boundedText,
|
|
14
|
+
revision: boundedText,
|
|
15
|
+
}).strict();
|
|
16
|
+
const reservationRequestSchema = z.object({
|
|
17
|
+
operationId: boundedText,
|
|
18
|
+
tenantId: boundedText,
|
|
19
|
+
principal: boundedText,
|
|
20
|
+
scopeId: boundedText,
|
|
21
|
+
resourceId: boundedText,
|
|
22
|
+
period: periodSchema,
|
|
23
|
+
amount: z.number().finite().positive(),
|
|
24
|
+
unit: boundedText,
|
|
25
|
+
runId: boundedText,
|
|
26
|
+
problem: problemSchema,
|
|
27
|
+
}).strict();
|
|
28
|
+
const reserveRequestSchema = reservationRequestSchema.extend({
|
|
29
|
+
approvalId: boundedText.optional(),
|
|
30
|
+
}).strict();
|
|
31
|
+
const mutationSchema = z.object({
|
|
32
|
+
operationId: boundedText,
|
|
33
|
+
tenantId: boundedText,
|
|
34
|
+
principal: boundedText,
|
|
35
|
+
scopeId: boundedText,
|
|
36
|
+
reservationId: boundedText,
|
|
37
|
+
}).strict();
|
|
38
|
+
class HttpInputError extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'HttpInputError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
class BodyLimitError extends Error {
|
|
45
|
+
constructor() {
|
|
46
|
+
super('Request body exceeds the configured limit');
|
|
47
|
+
this.name = 'BodyLimitError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class BodyReadError extends Error {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = 'BodyReadError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function requestPath(request) {
|
|
57
|
+
try {
|
|
58
|
+
return new URL(request.url ?? '/', 'http://localhost').pathname;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return '/';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function headerValue(request, name) {
|
|
65
|
+
const value = request.headers[name.toLowerCase()];
|
|
66
|
+
return Array.isArray(value) ? value[0] : value;
|
|
67
|
+
}
|
|
68
|
+
function writeJson(response, statusCode, body, headers = {}) {
|
|
69
|
+
if (response.headersSent)
|
|
70
|
+
return;
|
|
71
|
+
response.statusCode = statusCode;
|
|
72
|
+
response.setHeader('Content-Type', 'application/json');
|
|
73
|
+
for (const [name, value] of Object.entries(headers))
|
|
74
|
+
response.setHeader(name, value);
|
|
75
|
+
response.end(JSON.stringify(body));
|
|
76
|
+
}
|
|
77
|
+
async function readJsonBody(request, limitBytes) {
|
|
78
|
+
const contentLengthHeader = headerValue(request, 'content-length');
|
|
79
|
+
if (contentLengthHeader !== undefined) {
|
|
80
|
+
const contentLength = Number(contentLengthHeader);
|
|
81
|
+
if (Number.isFinite(contentLength) && contentLength > limitBytes) {
|
|
82
|
+
request.resume();
|
|
83
|
+
throw new BodyLimitError();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let totalBytes = 0;
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
let settled = false;
|
|
90
|
+
const cleanup = () => {
|
|
91
|
+
request.removeListener('data', onData);
|
|
92
|
+
request.removeListener('end', onEnd);
|
|
93
|
+
request.removeListener('error', onError);
|
|
94
|
+
request.removeListener('aborted', onAborted);
|
|
95
|
+
};
|
|
96
|
+
const finish = (error) => {
|
|
97
|
+
if (settled)
|
|
98
|
+
return;
|
|
99
|
+
settled = true;
|
|
100
|
+
cleanup();
|
|
101
|
+
if (error)
|
|
102
|
+
reject(error);
|
|
103
|
+
else
|
|
104
|
+
resolve();
|
|
105
|
+
};
|
|
106
|
+
const onData = (chunk) => {
|
|
107
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
108
|
+
totalBytes += buffer.byteLength;
|
|
109
|
+
if (totalBytes > limitBytes) {
|
|
110
|
+
request.resume();
|
|
111
|
+
finish(new BodyLimitError());
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
chunks.push(buffer);
|
|
115
|
+
};
|
|
116
|
+
const onEnd = () => finish();
|
|
117
|
+
const onError = () => finish(new BodyReadError('Request body could not be read'));
|
|
118
|
+
const onAborted = () => finish(new BodyReadError('Request was aborted'));
|
|
119
|
+
request.on('data', onData);
|
|
120
|
+
request.once('end', onEnd);
|
|
121
|
+
request.once('error', onError);
|
|
122
|
+
request.once('aborted', onAborted);
|
|
123
|
+
});
|
|
124
|
+
if (chunks.length === 0)
|
|
125
|
+
throw new HttpInputError('Request body must be a JSON object');
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new HttpInputError('Request body must contain valid JSON');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function normalizeBasePath(basePath) {
|
|
134
|
+
const value = basePath ?? DEFAULT_BASE_PATH;
|
|
135
|
+
if (typeof value !== 'string' || !value.startsWith('/') || value === '/' || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
136
|
+
throw new TypeError('basePath must be an absolute URL path');
|
|
137
|
+
}
|
|
138
|
+
const normalized = value.replace(/\/+$/u, '');
|
|
139
|
+
if (normalized.length === 0)
|
|
140
|
+
throw new TypeError('basePath must not be root');
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
function normalizePath(path) {
|
|
144
|
+
if (path.length > 1)
|
|
145
|
+
return path.replace(/\/+$/u, '');
|
|
146
|
+
return path;
|
|
147
|
+
}
|
|
148
|
+
function matchRoute(path, basePath) {
|
|
149
|
+
const normalized = normalizePath(path);
|
|
150
|
+
if (normalized === basePath)
|
|
151
|
+
return { operation: 'read' };
|
|
152
|
+
const prefix = `${basePath}/`;
|
|
153
|
+
if (!normalized.startsWith(prefix))
|
|
154
|
+
return null;
|
|
155
|
+
const operation = normalized.slice(prefix.length);
|
|
156
|
+
const operations = ['estimate', 'approve', 'reserve', 'release', 'cancel', 'consume'];
|
|
157
|
+
return { operation: operations.includes(operation) ? operation : null };
|
|
158
|
+
}
|
|
159
|
+
function isRecord(value) {
|
|
160
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
function hasOwn(value, key) {
|
|
163
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
164
|
+
}
|
|
165
|
+
function assertContext(context) {
|
|
166
|
+
if (!context
|
|
167
|
+
|| typeof context.tenantId !== 'string' || context.tenantId.length === 0
|
|
168
|
+
|| typeof context.principal !== 'string' || context.principal.length === 0
|
|
169
|
+
|| typeof context.scopeId !== 'string' || context.scopeId.length === 0) {
|
|
170
|
+
throw new HttpInputError('Authenticated reservation context is required');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function bindTrustedIdentity(body, context) {
|
|
174
|
+
if (!isRecord(body))
|
|
175
|
+
throw new HttpInputError('Request body must be a JSON object');
|
|
176
|
+
const input = { ...body };
|
|
177
|
+
if (hasOwn(input, 'authority') || hasOwn(input, 'authorities')) {
|
|
178
|
+
throw new HttpInputError('authority is not accepted in the request body');
|
|
179
|
+
}
|
|
180
|
+
const identityFields = [
|
|
181
|
+
['tenantId', ['tenantId', 'tenant_id', 'tenant'], context.tenantId],
|
|
182
|
+
['principal', ['principal', 'actorId', 'actor_id', 'actor'], context.principal],
|
|
183
|
+
['scopeId', ['scopeId', 'scope_id'], context.scopeId],
|
|
184
|
+
];
|
|
185
|
+
for (const [canonical, aliases, expected] of identityFields) {
|
|
186
|
+
for (const alias of aliases) {
|
|
187
|
+
if (!hasOwn(input, alias))
|
|
188
|
+
continue;
|
|
189
|
+
if (typeof input[alias] !== 'string' || input[alias] !== expected) {
|
|
190
|
+
throw new HttpInputError(`${canonical} does not match the trusted request context`);
|
|
191
|
+
}
|
|
192
|
+
delete input[alias];
|
|
193
|
+
}
|
|
194
|
+
input[canonical] = expected;
|
|
195
|
+
}
|
|
196
|
+
return input;
|
|
197
|
+
}
|
|
198
|
+
function parseOperationBody(operation, body, context) {
|
|
199
|
+
const bound = bindTrustedIdentity(body, context);
|
|
200
|
+
const parsed = operation === 'reserve'
|
|
201
|
+
? reserveRequestSchema.safeParse(bound)
|
|
202
|
+
: operation === 'estimate' || operation === 'approve'
|
|
203
|
+
? reservationRequestSchema.safeParse(bound)
|
|
204
|
+
: mutationSchema.safeParse(bound);
|
|
205
|
+
if (!parsed.success) {
|
|
206
|
+
throw new HttpInputError(parsed.error.issues[0]?.message ?? 'Request body is invalid');
|
|
207
|
+
}
|
|
208
|
+
return parsed.data;
|
|
209
|
+
}
|
|
210
|
+
function serviceError(error) {
|
|
211
|
+
if (!(error instanceof ResourceReservationError)) {
|
|
212
|
+
return { status: 500, code: 'internal_error', message: 'Resource reservation operation failed' };
|
|
213
|
+
}
|
|
214
|
+
const status = (() => {
|
|
215
|
+
switch (error.code) {
|
|
216
|
+
case 'authorization_denied':
|
|
217
|
+
case 'scope_mismatch':
|
|
218
|
+
return 403;
|
|
219
|
+
case 'reservation_not_found':
|
|
220
|
+
return 404;
|
|
221
|
+
case 'capacity_unknown':
|
|
222
|
+
return 503;
|
|
223
|
+
case 'operation_conflict':
|
|
224
|
+
case 'approval_expired':
|
|
225
|
+
case 'approval_revoked':
|
|
226
|
+
case 'approval_required':
|
|
227
|
+
case 'approval_mismatch':
|
|
228
|
+
case 'capacity_exceeded':
|
|
229
|
+
case 'reservation_state_conflict':
|
|
230
|
+
case 'external_action_reconciliation_required':
|
|
231
|
+
return 409;
|
|
232
|
+
case 'ledger_invalid':
|
|
233
|
+
return 500;
|
|
234
|
+
case 'validation_error':
|
|
235
|
+
case 'problem_snapshot_invalid':
|
|
236
|
+
return 400;
|
|
237
|
+
default:
|
|
238
|
+
return 500;
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
return { status, code: error.code, message: error.message };
|
|
242
|
+
}
|
|
243
|
+
function writeError(response, status, code, message, headers = {}) {
|
|
244
|
+
writeJson(response, status, { error: { code, message } }, headers);
|
|
245
|
+
}
|
|
246
|
+
function methodAllowed(operation, method) {
|
|
247
|
+
return operation === 'read' ? method === 'GET' : method === 'POST';
|
|
248
|
+
}
|
|
249
|
+
async function verifyMutationRequest(options, request, context) {
|
|
250
|
+
if (typeof context.verifiedMutationOrigin === 'string' && context.verifiedMutationOrigin.trim().length > 0)
|
|
251
|
+
return true;
|
|
252
|
+
if (!options.verifyMutationRequest)
|
|
253
|
+
return false;
|
|
254
|
+
try {
|
|
255
|
+
return Boolean(await options.verifyMutationRequest({ request, context }));
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export function createResourceReservationHttpHandler(options) {
|
|
262
|
+
if (!options || typeof options !== 'object' || typeof options.serviceFactory !== 'function') {
|
|
263
|
+
throw new TypeError('serviceFactory is required');
|
|
264
|
+
}
|
|
265
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
266
|
+
const bodyLimitBytes = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
267
|
+
if (!Number.isInteger(bodyLimitBytes) || bodyLimitBytes <= 0) {
|
|
268
|
+
throw new TypeError('bodyLimitBytes must be a positive integer');
|
|
269
|
+
}
|
|
270
|
+
return async (request, response, context) => {
|
|
271
|
+
const route = matchRoute(requestPath(request), basePath);
|
|
272
|
+
if (!route)
|
|
273
|
+
return false;
|
|
274
|
+
if (!context) {
|
|
275
|
+
writeError(response, 401, 'unauthorized', 'Authenticated reservation context is required');
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
assertContext(context);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
writeError(response, 401, 'unauthorized', error instanceof Error ? error.message : 'Authenticated reservation context is required');
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
if (!route.operation) {
|
|
286
|
+
writeError(response, 404, 'not_found', 'Resource reservation route was not found');
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
290
|
+
if (!methodAllowed(route.operation, method)) {
|
|
291
|
+
writeError(response, 405, 'method_not_allowed', 'Method is not allowed for this reservation route', { Allow: route.operation === 'read' ? 'GET' : 'POST' });
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (route.operation === 'read') {
|
|
295
|
+
try {
|
|
296
|
+
const service = await options.serviceFactory(context);
|
|
297
|
+
const result = await service.readLedger({
|
|
298
|
+
tenantId: context.tenantId,
|
|
299
|
+
principal: context.principal,
|
|
300
|
+
scopeId: context.scopeId,
|
|
301
|
+
});
|
|
302
|
+
writeJson(response, 200, { action: 'read', result });
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const failure = serviceError(error);
|
|
306
|
+
writeError(response, failure.status, failure.code, failure.message);
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
let body;
|
|
311
|
+
try {
|
|
312
|
+
body = await readJsonBody(request, bodyLimitBytes);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error instanceof BodyLimitError) {
|
|
316
|
+
writeError(response, 413, 'body_too_large', error.message);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
let parsed;
|
|
324
|
+
try {
|
|
325
|
+
parsed = parseOperationBody(route.operation, body, context);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (!await verifyMutationRequest(options, request, context)) {
|
|
332
|
+
writeError(response, 403, 'mutation_origin_unverified', 'Mutation origin could not be verified');
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const service = await options.serviceFactory(context);
|
|
337
|
+
let result;
|
|
338
|
+
switch (route.operation) {
|
|
339
|
+
case 'estimate':
|
|
340
|
+
result = await service.estimate(parsed);
|
|
341
|
+
break;
|
|
342
|
+
case 'approve':
|
|
343
|
+
result = await service.approve(parsed);
|
|
344
|
+
break;
|
|
345
|
+
case 'reserve':
|
|
346
|
+
result = await service.reserve(parsed);
|
|
347
|
+
break;
|
|
348
|
+
case 'release':
|
|
349
|
+
result = await service.release(parsed);
|
|
350
|
+
break;
|
|
351
|
+
case 'cancel':
|
|
352
|
+
result = await service.cancel(parsed);
|
|
353
|
+
break;
|
|
354
|
+
case 'consume':
|
|
355
|
+
result = await service.consume(parsed);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
const operationId = 'operationId' in parsed ? parsed.operationId : undefined;
|
|
359
|
+
writeJson(response, 200, {
|
|
360
|
+
action: route.operation,
|
|
361
|
+
...(operationId === undefined ? {} : { operationId }),
|
|
362
|
+
result,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
const failure = serviceError(error);
|
|
367
|
+
writeError(response, failure.status, failure.code, failure.message);
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
declare const ledgerVersion: 1;
|
|
2
|
+
export type ResourceReservationAction = 'estimate' | 'approve' | 'reserve' | 'release' | 'cancel' | 'consume' | 'start_external' | 'read';
|
|
3
|
+
export type ResourceReservationState = 'approved' | 'reserved' | 'released' | 'cancelled' | 'consumed';
|
|
4
|
+
export type ExternalActionStatus = 'not_started' | 'started' | 'completed' | 'unknown';
|
|
5
|
+
export interface ResourceReservationPeriod {
|
|
6
|
+
startsAt: string;
|
|
7
|
+
endsAt: string;
|
|
8
|
+
}
|
|
9
|
+
/** A fixed reference to Story05's immutable Problem snapshot. */
|
|
10
|
+
export interface ResourceReservationProblemReference {
|
|
11
|
+
problem_snapshot_id: string;
|
|
12
|
+
problem_id: string;
|
|
13
|
+
revision: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ResourceReservationRequest {
|
|
16
|
+
operationId: string;
|
|
17
|
+
tenantId: string;
|
|
18
|
+
principal: string;
|
|
19
|
+
scopeId: string;
|
|
20
|
+
resourceId: string;
|
|
21
|
+
period: ResourceReservationPeriod;
|
|
22
|
+
amount: number;
|
|
23
|
+
unit: string;
|
|
24
|
+
runId: string;
|
|
25
|
+
problem: ResourceReservationProblemReference;
|
|
26
|
+
}
|
|
27
|
+
export interface EstimateResourceReservationInput extends ResourceReservationRequest {
|
|
28
|
+
}
|
|
29
|
+
export interface ApproveResourceReservationInput extends ResourceReservationRequest {
|
|
30
|
+
}
|
|
31
|
+
export interface ReserveResourceReservationInput extends ResourceReservationRequest {
|
|
32
|
+
approvalId?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ResourceReservationMutationInput {
|
|
35
|
+
operationId: string;
|
|
36
|
+
tenantId: string;
|
|
37
|
+
principal: string;
|
|
38
|
+
scopeId: string;
|
|
39
|
+
reservationId: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ResourceReservationAuthorizationRequest {
|
|
42
|
+
action: ResourceReservationAction;
|
|
43
|
+
operationId: string;
|
|
44
|
+
tenantId: string;
|
|
45
|
+
principal: string;
|
|
46
|
+
scopeId: string;
|
|
47
|
+
resourceId?: string;
|
|
48
|
+
runId?: string;
|
|
49
|
+
problem?: ResourceReservationProblemReference;
|
|
50
|
+
approvalId?: string;
|
|
51
|
+
reservationId?: string;
|
|
52
|
+
reservation?: ResourceReservationRecord;
|
|
53
|
+
}
|
|
54
|
+
export interface ResourceReservationAuthorizationDecision {
|
|
55
|
+
status: 'approved' | 'denied' | 'expired' | 'revoked';
|
|
56
|
+
approvalId?: string;
|
|
57
|
+
expiresAt?: string | null;
|
|
58
|
+
evidence?: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
/** Organization roles, budgets, and approval workflow stay behind this port. */
|
|
61
|
+
export interface ResourceReservationAuthorizationPort {
|
|
62
|
+
authorize(request: ResourceReservationAuthorizationRequest): ResourceReservationAuthorizationDecision | Promise<ResourceReservationAuthorizationDecision>;
|
|
63
|
+
}
|
|
64
|
+
export interface ResourceReservationProblemSnapshotPort {
|
|
65
|
+
verify(input: {
|
|
66
|
+
tenantId: string;
|
|
67
|
+
principal: string;
|
|
68
|
+
scopeId: string;
|
|
69
|
+
reference: ResourceReservationProblemReference;
|
|
70
|
+
}): void | boolean | Promise<void | boolean>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The small part of Story05's immutable snapshot returned by its store.
|
|
74
|
+
*
|
|
75
|
+
* The reservation package keeps this structural so it can consume the
|
|
76
|
+
* snapshot package without copying its model or making the two packages
|
|
77
|
+
* depend on one another's source files.
|
|
78
|
+
*/
|
|
79
|
+
export interface ResourceReservationProblemSnapshotRecord {
|
|
80
|
+
snapshot_version: 'judgment-problem-snapshot.v1';
|
|
81
|
+
problem_id: string;
|
|
82
|
+
revision: string;
|
|
83
|
+
owner_scope: {
|
|
84
|
+
type: 'personal' | 'project' | 'organization';
|
|
85
|
+
id: string;
|
|
86
|
+
};
|
|
87
|
+
execution_permission: 'none';
|
|
88
|
+
}
|
|
89
|
+
/** Request shape passed to Story05's loadJudgmentProblemSnapshot function. */
|
|
90
|
+
export interface ResourceReservationProblemSnapshotLoadRequest {
|
|
91
|
+
root: string;
|
|
92
|
+
snapshot_id: string;
|
|
93
|
+
access: {
|
|
94
|
+
principal: string;
|
|
95
|
+
};
|
|
96
|
+
/** New approvals/reservations must revalidate canonical references. */
|
|
97
|
+
reference_resolution: 'current';
|
|
98
|
+
}
|
|
99
|
+
export type ResourceReservationProblemSnapshotLoader = (request: ResourceReservationProblemSnapshotLoadRequest) => Promise<ResourceReservationProblemSnapshotRecord>;
|
|
100
|
+
/**
|
|
101
|
+
* Adapt the real immutable ProblemSnapshot store to the reservation port.
|
|
102
|
+
*
|
|
103
|
+
* Story05 owns loading, integrity checks, and current read ACL checks. This
|
|
104
|
+
* adapter only binds the caller's principal, loads the current snapshot, and
|
|
105
|
+
* verifies the fixed problem/revision and scope reference. The loader must
|
|
106
|
+
* connect Story05's canonical reference provider. The tenant boundary and
|
|
107
|
+
* execution authorization remain the reservation/auth ports.
|
|
108
|
+
*/
|
|
109
|
+
export declare function createResourceReservationProblemSnapshotPort(options: {
|
|
110
|
+
root: string;
|
|
111
|
+
load: ResourceReservationProblemSnapshotLoader;
|
|
112
|
+
}): ResourceReservationProblemSnapshotPort;
|
|
113
|
+
export interface ResourceReservationCapacityPort {
|
|
114
|
+
read(input: {
|
|
115
|
+
tenantId: string;
|
|
116
|
+
principal: string;
|
|
117
|
+
scopeId: string;
|
|
118
|
+
resourceId: string;
|
|
119
|
+
period: ResourceReservationPeriod;
|
|
120
|
+
unit: string;
|
|
121
|
+
}): number | null | Promise<number | null>;
|
|
122
|
+
}
|
|
123
|
+
export interface ResourceReservationApproval {
|
|
124
|
+
approvalId: string;
|
|
125
|
+
approvedAt: string;
|
|
126
|
+
expiresAt?: string;
|
|
127
|
+
evidence: Record<string, string>;
|
|
128
|
+
}
|
|
129
|
+
export interface ResourceReservationExternalAction {
|
|
130
|
+
status: ExternalActionStatus;
|
|
131
|
+
startedAt?: string;
|
|
132
|
+
}
|
|
133
|
+
export interface ResourceReservationHistoryEntry {
|
|
134
|
+
action: Exclude<ResourceReservationAction, 'estimate' | 'read'>;
|
|
135
|
+
operationId: string;
|
|
136
|
+
principal: string;
|
|
137
|
+
occurredAt: string;
|
|
138
|
+
fromState?: ResourceReservationState;
|
|
139
|
+
toState: ResourceReservationState;
|
|
140
|
+
amount: number;
|
|
141
|
+
externalActionStatus: ExternalActionStatus;
|
|
142
|
+
}
|
|
143
|
+
export interface ResourceReservationRecord {
|
|
144
|
+
reservationId: string;
|
|
145
|
+
state: ResourceReservationState;
|
|
146
|
+
tenantId: string;
|
|
147
|
+
principal: string;
|
|
148
|
+
scopeId: string;
|
|
149
|
+
resourceId: string;
|
|
150
|
+
period: ResourceReservationPeriod;
|
|
151
|
+
amount: number;
|
|
152
|
+
unit: string;
|
|
153
|
+
runId: string;
|
|
154
|
+
problem: ResourceReservationProblemReference;
|
|
155
|
+
approval: ResourceReservationApproval;
|
|
156
|
+
externalAction: ResourceReservationExternalAction;
|
|
157
|
+
createdAt: string;
|
|
158
|
+
updatedAt: string;
|
|
159
|
+
history: ResourceReservationHistoryEntry[];
|
|
160
|
+
}
|
|
161
|
+
export interface ResourceReservationEstimate {
|
|
162
|
+
tenantId: string;
|
|
163
|
+
scopeId: string;
|
|
164
|
+
resourceId: string;
|
|
165
|
+
period: ResourceReservationPeriod;
|
|
166
|
+
unit: string;
|
|
167
|
+
requestedAmount: number;
|
|
168
|
+
capacity: number;
|
|
169
|
+
committedAmount: number;
|
|
170
|
+
availableAmount: number;
|
|
171
|
+
}
|
|
172
|
+
export interface ResourceReservationLedgerSnapshot {
|
|
173
|
+
version: typeof ledgerVersion;
|
|
174
|
+
reservations: ResourceReservationRecord[];
|
|
175
|
+
}
|
|
176
|
+
export interface ResourceReservationServiceOptions {
|
|
177
|
+
dataDir: string;
|
|
178
|
+
authorization: ResourceReservationAuthorizationPort;
|
|
179
|
+
problemSnapshot: ResourceReservationProblemSnapshotPort;
|
|
180
|
+
capacity: ResourceReservationCapacityPort;
|
|
181
|
+
clock?: () => Date;
|
|
182
|
+
sidecarPath?: string;
|
|
183
|
+
}
|
|
184
|
+
export type ResourceReservationErrorCode = 'validation_error' | 'authorization_denied' | 'approval_expired' | 'approval_revoked' | 'approval_required' | 'approval_mismatch' | 'capacity_unknown' | 'capacity_exceeded' | 'operation_conflict' | 'scope_mismatch' | 'reservation_not_found' | 'reservation_state_conflict' | 'external_action_reconciliation_required' | 'problem_snapshot_invalid' | 'ledger_invalid';
|
|
185
|
+
export declare class ResourceReservationError extends Error {
|
|
186
|
+
readonly code: ResourceReservationErrorCode;
|
|
187
|
+
constructor(code: ResourceReservationErrorCode, message: string);
|
|
188
|
+
}
|
|
189
|
+
export interface ResourceReservationMutationResult {
|
|
190
|
+
reservation: ResourceReservationRecord;
|
|
191
|
+
}
|
|
192
|
+
export declare class ResourceReservationService {
|
|
193
|
+
private readonly dataDir;
|
|
194
|
+
private readonly authorization;
|
|
195
|
+
private readonly problemSnapshot;
|
|
196
|
+
private readonly capacity;
|
|
197
|
+
private readonly clock;
|
|
198
|
+
private readonly sidecarPath;
|
|
199
|
+
constructor(options: ResourceReservationServiceOptions);
|
|
200
|
+
initialize(): Promise<void>;
|
|
201
|
+
estimate(input: EstimateResourceReservationInput): Promise<ResourceReservationEstimate>;
|
|
202
|
+
approve(input: ApproveResourceReservationInput): Promise<ResourceReservationMutationResult>;
|
|
203
|
+
reserve(input: ReserveResourceReservationInput): Promise<ResourceReservationMutationResult>;
|
|
204
|
+
release(input: ResourceReservationMutationInput): Promise<ResourceReservationMutationResult>;
|
|
205
|
+
cancel(input: ResourceReservationMutationInput): Promise<ResourceReservationMutationResult>;
|
|
206
|
+
consume(input: ResourceReservationMutationInput): Promise<ResourceReservationMutationResult>;
|
|
207
|
+
startExternalAction(input: ResourceReservationMutationInput): Promise<ResourceReservationMutationResult>;
|
|
208
|
+
readLedger(input: {
|
|
209
|
+
tenantId: string;
|
|
210
|
+
principal: string;
|
|
211
|
+
scopeId: string;
|
|
212
|
+
}): Promise<ResourceReservationLedgerSnapshot>;
|
|
213
|
+
private transition;
|
|
214
|
+
private mutate;
|
|
215
|
+
private reauthorizeReplay;
|
|
216
|
+
private authorize;
|
|
217
|
+
private verifyProblem;
|
|
218
|
+
private readCapacity;
|
|
219
|
+
private now;
|
|
220
|
+
}
|
|
221
|
+
export {};
|