@unson/brainbase-mcp 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/contracts/judgment-dag/digest.json +2 -2
- 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 +809 -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 +52 -0
- package/dist/company-os-objectives.js +197 -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-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-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -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 +185 -0
- package/dist/judgment-problem-snapshot.js +777 -0
- 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/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/personal-knowledge.d.ts +120 -0
- package/dist/personal-knowledge.js +750 -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 +97 -0
- package/dist/server.js +180 -1
- 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/world-model.d.ts +181 -0
- package/dist/world-model.js +873 -0
- package/package.json +145 -3
- package/ui/icons/mana/LICENSE.tabler.txt +21 -0
- package/ui/icons/mana/affiliate.svg +20 -0
- package/ui/icons/mana/brand-github.svg +15 -0
- package/ui/icons/mana/brand-google-drive.svg +17 -0
- package/ui/icons/mana/brand-slack.svg +18 -0
- package/ui/icons/mana/cpu.svg +24 -0
- package/ui/judgment-view.css +238 -0
- package/ui/judgment-view.js +508 -0
- package/ui/objective-editor.css +180 -0
- package/ui/objective-editor.js +1139 -0
- package/ui/outcome-knowledge.css +365 -0
- package/ui/outcome-knowledge.js +1570 -0
- package/ui/outcome-mana.css +255 -0
- package/ui/outcome-mana.js +2325 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { DURABLE_WAIT_VERSION, DurableWaitError, } from './durable-waits.js';
|
|
4
|
+
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
5
|
+
const DEFAULT_BASE_PATH = '/api/v1/durable-waits';
|
|
6
|
+
const snapshotIdPattern = /^sha256:[0-9a-f]{64}$/u;
|
|
7
|
+
/** Version of the HTTP boundary. The store version is returned separately. */
|
|
8
|
+
export const DURABLE_WAIT_HTTP_VERSION = 'durable-wait-http.v1';
|
|
9
|
+
const boundedText = z.string().min(1).max(512).refine((value) => !/[\u0000-\u001f\u007f]/u.test(value), 'must be a bounded text value');
|
|
10
|
+
const scopeSchema = z.object({
|
|
11
|
+
type: z.enum(['personal', 'project', 'organization']),
|
|
12
|
+
id: boundedText,
|
|
13
|
+
}).strict();
|
|
14
|
+
const aclSchema = z.object({
|
|
15
|
+
visibility: z.enum(['private', 'project', 'organization', 'public']),
|
|
16
|
+
ownerId: boundedText,
|
|
17
|
+
readerIds: z.array(boundedText),
|
|
18
|
+
writerIds: z.array(boundedText),
|
|
19
|
+
}).strict();
|
|
20
|
+
const problemSchema = z.object({
|
|
21
|
+
snapshot_id: z.string().min(1).max(512).regex(snapshotIdPattern),
|
|
22
|
+
problem_id: boundedText,
|
|
23
|
+
revision: boundedText,
|
|
24
|
+
}).strict();
|
|
25
|
+
const conditionSchema = z.object({
|
|
26
|
+
event: z.object({
|
|
27
|
+
event_type: boundedText,
|
|
28
|
+
event_id: boundedText.optional(),
|
|
29
|
+
predicate: boundedText.optional(),
|
|
30
|
+
}).strict().optional(),
|
|
31
|
+
expression: boundedText.optional(),
|
|
32
|
+
}).strict();
|
|
33
|
+
const deadlineSchema = z.object({
|
|
34
|
+
due_at: boundedText.optional(),
|
|
35
|
+
review_interval_ms: z.number().int().positive().finite().optional(),
|
|
36
|
+
next_review_at: boundedText.optional(),
|
|
37
|
+
}).strict();
|
|
38
|
+
const responsibleSchema = z.object({
|
|
39
|
+
principal: boundedText,
|
|
40
|
+
scope: boundedText.optional(),
|
|
41
|
+
}).strict();
|
|
42
|
+
const runRefSchema = z.object({
|
|
43
|
+
run_id: boundedText,
|
|
44
|
+
node_id: boundedText.optional(),
|
|
45
|
+
}).strict();
|
|
46
|
+
const identitySchema = z.object({
|
|
47
|
+
tenantId: boundedText,
|
|
48
|
+
principal: boundedText,
|
|
49
|
+
scopeId: boundedText,
|
|
50
|
+
});
|
|
51
|
+
const createSchema = identitySchema.extend({
|
|
52
|
+
wait_id: boundedText.optional(),
|
|
53
|
+
owner_scope: scopeSchema,
|
|
54
|
+
read_policy: aclSchema,
|
|
55
|
+
problem_snapshot: problemSchema.optional(),
|
|
56
|
+
problem: problemSchema.optional(),
|
|
57
|
+
condition: conditionSchema,
|
|
58
|
+
deadline: deadlineSchema.optional(),
|
|
59
|
+
responsible: responsibleSchema,
|
|
60
|
+
resume_method: z.enum(['resume_run', 'new_problem', 'reconcile_external_effect', 'manual_review']),
|
|
61
|
+
failure_policy: z.enum(['handoff', 'new_problem', 'reconciliation_wait', 'fail']),
|
|
62
|
+
run_ref: runRefSchema,
|
|
63
|
+
created_at: boundedText.optional(),
|
|
64
|
+
}).strict();
|
|
65
|
+
const claimSchema = identitySchema.extend({
|
|
66
|
+
wait_id: boundedText,
|
|
67
|
+
request_id: boundedText,
|
|
68
|
+
trigger: z.enum(['event', 'timer', 'manual']),
|
|
69
|
+
event_type: boundedText.optional(),
|
|
70
|
+
event_id: boundedText.optional(),
|
|
71
|
+
occurred_at: boundedText.optional(),
|
|
72
|
+
lease_ms: z.number().int().positive().finite().optional(),
|
|
73
|
+
}).strict();
|
|
74
|
+
const resumeSchema = identitySchema.extend({
|
|
75
|
+
wait_id: boundedText,
|
|
76
|
+
request_id: boundedText,
|
|
77
|
+
claim_id: boundedText,
|
|
78
|
+
lease_id: boundedText,
|
|
79
|
+
lease_token: boundedText,
|
|
80
|
+
}).strict();
|
|
81
|
+
const handoffSchema = identitySchema.extend({
|
|
82
|
+
wait_id: boundedText,
|
|
83
|
+
request_id: boundedText,
|
|
84
|
+
responsible: responsibleSchema,
|
|
85
|
+
lease_ms: z.number().int().positive().finite().optional(),
|
|
86
|
+
now: boundedText.optional(),
|
|
87
|
+
reason: boundedText.optional(),
|
|
88
|
+
}).strict();
|
|
89
|
+
const premiseChangedSchema = identitySchema.extend({
|
|
90
|
+
wait_id: boundedText,
|
|
91
|
+
new_problem_snapshot: problemSchema.optional(),
|
|
92
|
+
new_problem: problemSchema.optional(),
|
|
93
|
+
responsible: responsibleSchema.optional(),
|
|
94
|
+
reason: boundedText,
|
|
95
|
+
}).strict();
|
|
96
|
+
const effectUnknownSchema = identitySchema.extend({
|
|
97
|
+
wait_id: boundedText,
|
|
98
|
+
reason: boundedText,
|
|
99
|
+
responsible: responsibleSchema.optional(),
|
|
100
|
+
}).strict();
|
|
101
|
+
class HttpInputError extends Error {
|
|
102
|
+
constructor(message) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.name = 'HttpInputError';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
class BodyLimitError extends Error {
|
|
108
|
+
constructor() {
|
|
109
|
+
super('Request body exceeds the configured limit');
|
|
110
|
+
this.name = 'BodyLimitError';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
class BodyReadError extends Error {
|
|
114
|
+
constructor(message) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = 'BodyReadError';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function requestPath(request) {
|
|
120
|
+
try {
|
|
121
|
+
return new URL(request.url ?? '/', 'http://localhost').pathname;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return '/';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function headerValue(request, name) {
|
|
128
|
+
const value = request.headers[name.toLowerCase()];
|
|
129
|
+
return Array.isArray(value) ? value[0] : value;
|
|
130
|
+
}
|
|
131
|
+
function writeJson(response, statusCode, body) {
|
|
132
|
+
if (response.headersSent)
|
|
133
|
+
return;
|
|
134
|
+
response.statusCode = statusCode;
|
|
135
|
+
response.setHeader('Content-Type', 'application/json');
|
|
136
|
+
response.setHeader('X-Brainbase-Durable-Wait-Protocol', DURABLE_WAIT_HTTP_VERSION);
|
|
137
|
+
response.end(JSON.stringify(body));
|
|
138
|
+
}
|
|
139
|
+
async function readJsonBody(request, limitBytes) {
|
|
140
|
+
const contentLengthHeader = headerValue(request, 'content-length');
|
|
141
|
+
if (contentLengthHeader !== undefined) {
|
|
142
|
+
const contentLength = Number(contentLengthHeader);
|
|
143
|
+
if (Number.isFinite(contentLength) && contentLength > limitBytes) {
|
|
144
|
+
request.resume();
|
|
145
|
+
throw new BodyLimitError();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const chunks = [];
|
|
149
|
+
let totalBytes = 0;
|
|
150
|
+
await new Promise((resolve, reject) => {
|
|
151
|
+
let settled = false;
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
request.removeListener('data', onData);
|
|
154
|
+
request.removeListener('end', onEnd);
|
|
155
|
+
request.removeListener('error', onError);
|
|
156
|
+
request.removeListener('aborted', onAborted);
|
|
157
|
+
};
|
|
158
|
+
const finish = (error) => {
|
|
159
|
+
if (settled)
|
|
160
|
+
return;
|
|
161
|
+
settled = true;
|
|
162
|
+
cleanup();
|
|
163
|
+
if (error)
|
|
164
|
+
reject(error);
|
|
165
|
+
else
|
|
166
|
+
resolve();
|
|
167
|
+
};
|
|
168
|
+
const onData = (chunk) => {
|
|
169
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
170
|
+
totalBytes += buffer.byteLength;
|
|
171
|
+
if (totalBytes > limitBytes) {
|
|
172
|
+
request.resume();
|
|
173
|
+
finish(new BodyLimitError());
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
chunks.push(buffer);
|
|
177
|
+
};
|
|
178
|
+
const onEnd = () => finish();
|
|
179
|
+
const onError = () => finish(new BodyReadError('Request body could not be read'));
|
|
180
|
+
const onAborted = () => finish(new BodyReadError('Request was aborted'));
|
|
181
|
+
request.on('data', onData);
|
|
182
|
+
request.once('end', onEnd);
|
|
183
|
+
request.once('error', onError);
|
|
184
|
+
request.once('aborted', onAborted);
|
|
185
|
+
});
|
|
186
|
+
if (chunks.length === 0)
|
|
187
|
+
throw new HttpInputError('Request body must be a JSON object');
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
throw new HttpInputError('Request body must contain valid JSON');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function normalizeBasePath(basePath) {
|
|
196
|
+
const value = basePath ?? DEFAULT_BASE_PATH;
|
|
197
|
+
if (typeof value !== 'string' || !value.startsWith('/') || value === '/' || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
198
|
+
throw new TypeError('basePath must be an absolute URL path');
|
|
199
|
+
}
|
|
200
|
+
const normalized = value.replace(/\/+$/u, '');
|
|
201
|
+
if (normalized.length === 0)
|
|
202
|
+
throw new TypeError('basePath must not be root');
|
|
203
|
+
return normalized;
|
|
204
|
+
}
|
|
205
|
+
function normalizePath(path) {
|
|
206
|
+
if (path.length > 1)
|
|
207
|
+
return path.replace(/\/+$/u, '');
|
|
208
|
+
return path;
|
|
209
|
+
}
|
|
210
|
+
function matchRoute(path, basePath) {
|
|
211
|
+
const normalized = normalizePath(path);
|
|
212
|
+
if (normalized === basePath)
|
|
213
|
+
return { operation: null };
|
|
214
|
+
const prefix = `${basePath}/`;
|
|
215
|
+
if (!normalized.startsWith(prefix))
|
|
216
|
+
return null;
|
|
217
|
+
const suffix = normalized.slice(prefix.length);
|
|
218
|
+
const operationNames = [
|
|
219
|
+
'create', 'claim', 'resume', 'handoff', 'premise-changed', 'effect-unknown',
|
|
220
|
+
];
|
|
221
|
+
if (operationNames.includes(suffix))
|
|
222
|
+
return { operation: suffix };
|
|
223
|
+
if (!suffix.includes('/')) {
|
|
224
|
+
try {
|
|
225
|
+
const waitId = decodeURIComponent(suffix);
|
|
226
|
+
if (waitId.length > 0 && waitId.length <= 512)
|
|
227
|
+
return { operation: 'read', waitId };
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return { operation: null };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { operation: null };
|
|
234
|
+
}
|
|
235
|
+
function isRecord(value) {
|
|
236
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
237
|
+
}
|
|
238
|
+
function hasOwn(value, key) {
|
|
239
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
240
|
+
}
|
|
241
|
+
function assertContext(context) {
|
|
242
|
+
if (!context
|
|
243
|
+
|| typeof context.tenantId !== 'string' || context.tenantId.length === 0
|
|
244
|
+
|| typeof context.principal !== 'string' || context.principal.length === 0
|
|
245
|
+
|| typeof context.scopeId !== 'string' || context.scopeId.length === 0) {
|
|
246
|
+
throw new HttpInputError('Authenticated durable-wait context is required');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Bind all identity fields to the host-authenticated context; body identity never widens it. */
|
|
250
|
+
function bindTrustedIdentity(body, context) {
|
|
251
|
+
if (!isRecord(body))
|
|
252
|
+
throw new HttpInputError('Request body must be a JSON object');
|
|
253
|
+
const input = { ...body };
|
|
254
|
+
if (hasOwn(input, 'authority') || hasOwn(input, 'authorities')) {
|
|
255
|
+
throw new HttpInputError('authority is not accepted in the request body');
|
|
256
|
+
}
|
|
257
|
+
const identityFields = [
|
|
258
|
+
['tenantId', ['tenantId', 'tenant_id', 'tenant'], context.tenantId],
|
|
259
|
+
['principal', ['principal', 'actorId', 'actor_id', 'actor'], context.principal],
|
|
260
|
+
['scopeId', ['scopeId', 'scope_id'], context.scopeId],
|
|
261
|
+
];
|
|
262
|
+
for (const [canonical, aliases, expected] of identityFields) {
|
|
263
|
+
for (const alias of aliases) {
|
|
264
|
+
if (!hasOwn(input, alias))
|
|
265
|
+
continue;
|
|
266
|
+
if (typeof input[alias] !== 'string' || input[alias] !== expected) {
|
|
267
|
+
throw new HttpInputError(`${canonical} does not match the trusted request context`);
|
|
268
|
+
}
|
|
269
|
+
delete input[alias];
|
|
270
|
+
}
|
|
271
|
+
input[canonical] = expected;
|
|
272
|
+
}
|
|
273
|
+
return input;
|
|
274
|
+
}
|
|
275
|
+
function assertOwnerScope(result, context) {
|
|
276
|
+
if (!isRecord(result) || !isRecord(result.owner_scope) || result.owner_scope.id !== context.scopeId) {
|
|
277
|
+
throw new DurableWaitError('unauthorized', 'Durable wait scope does not match the trusted request context');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function serviceError(error) {
|
|
281
|
+
if (!(error instanceof DurableWaitError)) {
|
|
282
|
+
return { status: 500, code: 'internal_error', message: 'Durable wait operation failed' };
|
|
283
|
+
}
|
|
284
|
+
const status = (() => {
|
|
285
|
+
switch (error.code) {
|
|
286
|
+
case 'unauthorized': return 403;
|
|
287
|
+
case 'not_found': return 404;
|
|
288
|
+
case 'invalid_request':
|
|
289
|
+
case 'problem_snapshot_invalid': return 400;
|
|
290
|
+
case 'reconciliation_required':
|
|
291
|
+
case 'condition_unmet':
|
|
292
|
+
case 'lease_active':
|
|
293
|
+
case 'lease_expired':
|
|
294
|
+
case 'state_conflict': return 409;
|
|
295
|
+
case 'ledger_invalid': return 500;
|
|
296
|
+
default: return 500;
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
299
|
+
return { status, code: error.code, message: error.message };
|
|
300
|
+
}
|
|
301
|
+
function writeError(response, status, code, message) {
|
|
302
|
+
writeJson(response, status, {
|
|
303
|
+
protocol_version: DURABLE_WAIT_HTTP_VERSION,
|
|
304
|
+
error: { code, message },
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function methodAllowed(operation, method) {
|
|
308
|
+
return operation === 'read' ? method === 'GET' : operation !== null && method === 'POST';
|
|
309
|
+
}
|
|
310
|
+
async function verifyMutationRequest(options, request, context) {
|
|
311
|
+
if (typeof context.verifiedMutationOrigin === 'string' && context.verifiedMutationOrigin.trim().length > 0)
|
|
312
|
+
return true;
|
|
313
|
+
if (!options.verifyMutationRequest)
|
|
314
|
+
return false;
|
|
315
|
+
try {
|
|
316
|
+
return Boolean(await options.verifyMutationRequest({ request, context }));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function success(context, action, result) {
|
|
323
|
+
return {
|
|
324
|
+
protocol_version: DURABLE_WAIT_HTTP_VERSION,
|
|
325
|
+
store_version: DURABLE_WAIT_VERSION,
|
|
326
|
+
tenant_id: context.tenantId,
|
|
327
|
+
scope_id: context.scopeId,
|
|
328
|
+
action,
|
|
329
|
+
result,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function parseBody(operation, body, context) {
|
|
333
|
+
const bound = bindTrustedIdentity(body, context);
|
|
334
|
+
const parsed = (() => {
|
|
335
|
+
switch (operation) {
|
|
336
|
+
case 'create': return createSchema.safeParse(bound);
|
|
337
|
+
case 'claim': return claimSchema.safeParse(bound);
|
|
338
|
+
case 'resume': return resumeSchema.safeParse(bound);
|
|
339
|
+
case 'handoff': return handoffSchema.safeParse(bound);
|
|
340
|
+
case 'premise-changed': return premiseChangedSchema.safeParse(bound);
|
|
341
|
+
case 'effect-unknown': return effectUnknownSchema.safeParse(bound);
|
|
342
|
+
}
|
|
343
|
+
})();
|
|
344
|
+
if (!parsed.success)
|
|
345
|
+
throw new HttpInputError(parsed.error.issues[0]?.message ?? 'Request body is invalid');
|
|
346
|
+
const value = parsed.data;
|
|
347
|
+
if ('owner_scope' in value && isRecord(value.owner_scope) && value.owner_scope.id !== context.scopeId) {
|
|
348
|
+
throw new HttpInputError('owner_scope does not match the trusted request context');
|
|
349
|
+
}
|
|
350
|
+
const { tenantId: _tenantId, scopeId: _scopeId, ...storeInput } = value;
|
|
351
|
+
return storeInput;
|
|
352
|
+
}
|
|
353
|
+
export function createDurableWaitHttpHandler(options) {
|
|
354
|
+
if (!options || typeof options !== 'object' || typeof options.storeFactory !== 'function') {
|
|
355
|
+
throw new TypeError('storeFactory is required');
|
|
356
|
+
}
|
|
357
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
358
|
+
const bodyLimitBytes = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
359
|
+
if (!Number.isInteger(bodyLimitBytes) || bodyLimitBytes <= 0) {
|
|
360
|
+
throw new TypeError('bodyLimitBytes must be a positive integer');
|
|
361
|
+
}
|
|
362
|
+
return async (request, response, context) => {
|
|
363
|
+
const route = matchRoute(requestPath(request), basePath);
|
|
364
|
+
if (!route)
|
|
365
|
+
return false;
|
|
366
|
+
if (!context) {
|
|
367
|
+
writeError(response, 401, 'unauthorized', 'Authenticated durable-wait context is required');
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
assertContext(context);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
writeError(response, 401, 'unauthorized', error instanceof Error ? error.message : 'Authenticated durable-wait context is required');
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
if (!route.operation) {
|
|
378
|
+
writeError(response, 404, 'not_found', 'Durable wait route was not found');
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
382
|
+
if (!methodAllowed(route.operation, method)) {
|
|
383
|
+
writeError(response, 405, 'method_not_allowed', 'Method is not allowed for this durable-wait route');
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
if (route.operation === 'read') {
|
|
387
|
+
try {
|
|
388
|
+
const store = await options.storeFactory(context);
|
|
389
|
+
const result = await store.read({ wait_id: route.waitId, principal: context.principal });
|
|
390
|
+
assertOwnerScope(result, context);
|
|
391
|
+
writeJson(response, 200, success(context, route.operation, result));
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
const failure = serviceError(error);
|
|
395
|
+
writeError(response, failure.status, failure.code, failure.message);
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
let body;
|
|
400
|
+
try {
|
|
401
|
+
body = await readJsonBody(request, bodyLimitBytes);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (error instanceof BodyLimitError)
|
|
405
|
+
writeError(response, 413, 'body_too_large', error.message);
|
|
406
|
+
else
|
|
407
|
+
writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
let parsed;
|
|
411
|
+
try {
|
|
412
|
+
parsed = parseBody(route.operation, body, context);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
writeError(response, 400, 'invalid_request', error instanceof Error ? error.message : 'Request body is invalid');
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
if (!await verifyMutationRequest(options, request, context)) {
|
|
419
|
+
writeError(response, 403, 'mutation_origin_unverified', 'Mutation origin could not be verified');
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const store = await options.storeFactory(context);
|
|
424
|
+
if (route.operation !== 'create') {
|
|
425
|
+
// owner_scope is immutable after creation. Resolve it through the
|
|
426
|
+
// current ACL before entering any mutation so a principal shared by
|
|
427
|
+
// two scopes cannot mutate a wait and receive a 403 only afterwards.
|
|
428
|
+
// The store's own compare-and-swap preflight then rechecks the current
|
|
429
|
+
// record while holding its mutation boundary for concurrent updates.
|
|
430
|
+
const waitId = parsed.wait_id;
|
|
431
|
+
const current = await store.read({ wait_id: waitId, principal: context.principal });
|
|
432
|
+
assertOwnerScope(current, context);
|
|
433
|
+
}
|
|
434
|
+
let result;
|
|
435
|
+
switch (route.operation) {
|
|
436
|
+
case 'create':
|
|
437
|
+
result = await store.create(parsed);
|
|
438
|
+
assertOwnerScope(result, context);
|
|
439
|
+
break;
|
|
440
|
+
case 'claim': {
|
|
441
|
+
result = await store.claim(parsed);
|
|
442
|
+
assertOwnerScope(result.wait, context);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
case 'resume':
|
|
446
|
+
result = await store.resume(parsed);
|
|
447
|
+
break;
|
|
448
|
+
case 'handoff':
|
|
449
|
+
result = await store.handoff(parsed);
|
|
450
|
+
assertOwnerScope(result, context);
|
|
451
|
+
break;
|
|
452
|
+
case 'premise-changed':
|
|
453
|
+
result = await store.markPremiseChanged(parsed);
|
|
454
|
+
assertOwnerScope(result, context);
|
|
455
|
+
break;
|
|
456
|
+
case 'effect-unknown':
|
|
457
|
+
result = await store.markEffectUnknown(parsed);
|
|
458
|
+
assertOwnerScope(result, context);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
writeJson(response, 200, success(context, route.operation, result));
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
const failure = serviceError(error);
|
|
465
|
+
writeError(response, failure.status, failure.code, failure.message);
|
|
466
|
+
}
|
|
467
|
+
return true;
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Create a startable Node HTTP host around the canonical handler.
|
|
472
|
+
*
|
|
473
|
+
* The caller owns `server.listen()` and the authentication implementation.
|
|
474
|
+
* A route outside the durable-wait base path is answered with a generic 404 so
|
|
475
|
+
* a standalone fixture cannot leave the request hanging; larger hosts can use
|
|
476
|
+
* `createDurableWaitHttpHandler` directly when they need route delegation.
|
|
477
|
+
*/
|
|
478
|
+
export function createDurableWaitHttpHost(options) {
|
|
479
|
+
if (!options || typeof options !== 'object' || typeof options.resolveContext !== 'function') {
|
|
480
|
+
throw new TypeError('resolveContext is required');
|
|
481
|
+
}
|
|
482
|
+
const handler = createDurableWaitHttpHandler(options);
|
|
483
|
+
return createServer((request, response) => {
|
|
484
|
+
void Promise.resolve()
|
|
485
|
+
.then(() => options.resolveContext(request))
|
|
486
|
+
.catch(() => null)
|
|
487
|
+
.then((context) => handler(request, response, context))
|
|
488
|
+
.then((handled) => {
|
|
489
|
+
if (handled || response.headersSent)
|
|
490
|
+
return;
|
|
491
|
+
writeError(response, 404, 'not_found', 'Route was not found');
|
|
492
|
+
})
|
|
493
|
+
.catch(() => {
|
|
494
|
+
writeError(response, 500, 'internal_error', 'Durable wait host failed');
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|