@arnilo/prism-server 0.2.4 → 0.2.6

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/dist/handler.js CHANGED
@@ -1,852 +1,11 @@
1
- import { AgentRunStateError, assertIdentityActive, assertIdentityMatchesOwnership, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, } from "@arnilo/prism";
2
- import { cancelWorkflowRun, createWorkflowEventBus, enqueueWorkflow, getWorkflowRun, replayWorkflow, resumeWorkflow, runWorkflow, } from "@arnilo/prism-workflows";
3
- import { isAdmitOperation } from "./drain.js";
4
- import { resolvePrismServerLimits } from "./limits.js";
5
- import { PrismServerError } from "./types.js";
6
- const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
7
- const SSE_HEADERS = {
8
- "content-type": "text/event-stream; charset=utf-8",
9
- "cache-control": "no-cache, no-transform",
10
- connection: "keep-alive",
11
- };
12
- export function createPrismHandler(options) {
13
- const limits = resolvePrismServerLimits(options.limits);
14
- const base = normalizeBasePath(options.basePath ?? "/prism");
15
- let activeRuns = 0;
16
- return async (request) => {
17
- const origin = request.headers.get("origin");
18
- const corsHeaders = origin && options.allowedOrigins?.includes(origin) ? { "access-control-allow-origin": origin, vary: "origin" } : undefined;
19
- const respond = (response) => addHeaders(response, corsHeaders);
20
- try {
21
- assertRequestPolicy(request, options.allowedHosts, options.allowedOrigins);
22
- const route = parseRoute(request, base);
23
- if (request.method === "OPTIONS") {
24
- if (!origin || !options.allowedOrigins?.includes(origin))
25
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
26
- return respond(new Response(null, {
27
- status: 204,
28
- headers: {
29
- "access-control-allow-origin": origin,
30
- "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
31
- "access-control-allow-headers": "content-type, authorization, last-event-id",
32
- vary: "origin",
33
- },
34
- }));
35
- }
36
- if (!route)
37
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
38
- const authorization = await authorize(options, request, route.operation, route.capabilityId, limits.requestTimeoutMs);
39
- if (!authorization)
40
- throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
41
- if (options.rateLimit) {
42
- const decision = await options.rateLimit({
43
- request,
44
- operation: route.operation,
45
- capabilityId: route.capabilityId,
46
- authorization,
47
- signal: request.signal,
48
- });
49
- if (decision !== true) {
50
- const headers = {};
51
- if (decision.retryAfterMs !== undefined && Number.isSafeInteger(decision.retryAfterMs) && decision.retryAfterMs > 0) {
52
- headers["retry-after"] = String(Math.ceil(decision.retryAfterMs / 1000));
53
- }
54
- throw new PrismServerError(decision.message ?? "Rate limit exceeded", 429, decision.code ?? "ERR_PRISM_SERVER_RATE_LIMIT", Object.keys(headers).length ? headers : undefined);
55
- }
56
- }
57
- if (options.drain && isAdmitOperation(route.operation))
58
- options.drain.assertAdmit();
59
- if (route.kind.startsWith("schedule-")) {
60
- const selectedSchedules = options.schedules;
61
- if (!selectedSchedules)
62
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
63
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
64
- try {
65
- const schedules = typeof selectedSchedules === "function"
66
- ? await awaitWithSignal(Promise.resolve(selectedSchedules(authorization, owned.signal)), owned.signal)
67
- : selectedSchedules;
68
- if (!sameOwnership(authorization.ownership, schedules.ownership)) {
69
- throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
70
- }
71
- if (route.kind === "schedule-list") {
72
- const query = new URL(request.url).searchParams;
73
- const status = query.get("status");
74
- const result = await awaitWithSignal(schedules.list({
75
- status: readScheduleStatus(status),
76
- cursor: query.get("cursor") ?? undefined,
77
- limit: query.has("limit") ? readPositiveInteger(query.get("limit"), "limit") : undefined,
78
- signal: owned.signal,
79
- }), owned.signal);
80
- return respond(json(result, 200, limits, options));
81
- }
82
- if (route.kind === "schedule-delete") {
83
- const result = await awaitWithSignal(schedules.delete(route.capabilityId, owned.signal), owned.signal);
84
- return respond(json({ deleted: result }, 200, limits, options));
85
- }
86
- const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
87
- if (route.kind === "schedule-create") {
88
- const result = await awaitWithSignal(schedules.create({
89
- id: route.capabilityId,
90
- workflowId: readRequiredId(body.workflowId, "workflowId"),
91
- nextRunAt: readRequiredString(body.nextRunAt, "nextRunAt"),
92
- input: body.input,
93
- intervalMs: body.intervalMs === undefined ? undefined : readPositiveInteger(body.intervalMs, "intervalMs"),
94
- calculatorId: readOptionalId(body.calculatorId, "calculatorId"),
95
- paused: body.paused === true,
96
- metadata: readOptionalObject(body.metadata, "metadata"),
97
- }, owned.signal), owned.signal);
98
- return respond(json(result, 201, limits, options));
99
- }
100
- if (route.kind === "schedule-pause") {
101
- return respond(json(await awaitWithSignal(schedules.pause(route.capabilityId, owned.signal), owned.signal), 200, limits, options));
102
- }
103
- if (route.kind === "schedule-resume") {
104
- const nextRunAt = body.nextRunAt === undefined ? undefined : readRequiredString(body.nextRunAt, "nextRunAt");
105
- return respond(json(await awaitWithSignal(schedules.resume(route.capabilityId, nextRunAt, owned.signal), owned.signal), 200, limits, options));
106
- }
107
- const idempotencyKey = readRequiredId(body.idempotencyKey, "idempotencyKey");
108
- return respond(json(await awaitWithSignal(schedules.trigger(route.capabilityId, { idempotencyKey, signal: owned.signal }), owned.signal), 200, limits, options));
109
- }
110
- finally {
111
- owned.dispose();
112
- }
113
- }
114
- if (route.kind === "agent-events") {
115
- const exposure = options.agents?.[route.capabilityId];
116
- if (!exposure || !("sessionFactory" in exposure) || !exposure.events || !exposure.resolveRun) {
117
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
118
- }
119
- if (!authorization.ownership.tenantId)
120
- throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
121
- acquire();
122
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
123
- try {
124
- const run = await awaitWithSignal(Promise.resolve(exposure.resolveRun({ runId: route.runId, authorization, signal: owned.signal })), owned.signal);
125
- if (!run?.sessionId)
126
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
127
- const after = replayCursor(request, limits.maxReplayCursorBytes);
128
- const events = exposure.events.subscribe({
129
- ownership: authorization.ownership,
130
- sessionId: run.sessionId,
131
- runId: run.runId,
132
- after,
133
- signal: owned.signal,
134
- });
135
- return respond(sseAgentEvents(events, owned, limits, options, release));
136
- }
137
- catch (error) {
138
- owned.dispose();
139
- release();
140
- throw error;
141
- }
142
- }
143
- if (route.kind === "agent-status" || route.kind === "agent-resume") {
144
- const exposure = options.agentRuns?.[route.capabilityId];
145
- if (!exposure)
146
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
147
- if (route.kind === "agent-status") {
148
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
149
- try {
150
- return respond(json(await awaitWithSignal(exposure.lifecycle.status({ runId: route.runId }, {
151
- ownership: authorization.ownership,
152
- signal: owned.signal,
153
- agentId: route.capabilityId,
154
- }), owned.signal), 200, limits, options));
155
- }
156
- finally {
157
- owned.dispose();
158
- }
159
- }
160
- acquire();
161
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
162
- try {
163
- const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
164
- return respond(json(await awaitWithSignal(exposure.lifecycle.resume({ runId: route.runId }, readAgentResume(body), {
165
- ownership: authorization.ownership,
166
- signal: owned.signal,
167
- agentId: route.capabilityId,
168
- }), owned.signal), 200, limits, options));
169
- }
170
- finally {
171
- owned.dispose();
172
- release();
173
- }
174
- }
175
- if (route.kind === "agent-run" || route.kind === "agent-stream") {
176
- const exposure = options.agents?.[route.capabilityId];
177
- if (!exposure)
178
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
179
- acquire();
180
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
181
- try {
182
- const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
183
- const input = readAgentInput(body.input);
184
- const { session, runOptions } = await awaitWithSignal(createSession(exposure, authorization), owned.signal);
185
- const runConfig = {
186
- ...runOptions,
187
- ownership: authorization.ownership,
188
- identity: authorization.identity,
189
- metadata: { ...runOptions?.metadata, ...authorization.metadata },
190
- redactor: options.redactor,
191
- signal: owned.signal,
192
- };
193
- if (route.kind === "agent-run") {
194
- const result = await awaitWithSignal(session.run(input, runConfig), owned.signal);
195
- const response = respond(json(result, 200, limits, options));
196
- owned.dispose();
197
- release();
198
- return response;
199
- }
200
- const events = session.stream(input, {
201
- ...runConfig,
202
- maxQueuedEvents: limits.maxQueuedEvents,
203
- overflow: "close",
204
- });
205
- return respond(sse(events, owned, limits, options, release));
206
- }
207
- catch (error) {
208
- owned.dispose();
209
- release();
210
- throw error;
211
- }
212
- }
213
- const exposure = options.workflows?.[route.capabilityId];
214
- if (!exposure)
215
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
216
- if (route.kind === "workflow-enqueue") {
217
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
218
- try {
219
- const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
220
- const result = await awaitWithSignal(enqueueWorkflow(exposure.definition, body.input, {
221
- checkpoints: exposure.checkpoints,
222
- ownership: authorization.ownership,
223
- runId: readOptionalId(body.runId, "runId"),
224
- metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
225
- signal: owned.signal,
226
- }), owned.signal);
227
- return respond(json(result, 202, limits, options));
228
- }
229
- finally {
230
- owned.dispose();
231
- }
232
- }
233
- if (route.kind === "workflow-replay") {
234
- acquire();
235
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
236
- try {
237
- const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
238
- const result = await awaitWithSignal(replayWorkflow(exposure.definition, {
239
- sourceRunId: route.runId,
240
- fromNodeId: readRequiredId(body.fromNodeId, "fromNodeId"),
241
- runId: readOptionalId(body.runId, "runId"),
242
- }, {
243
- ...exposure.runOptions,
244
- checkpoints: exposure.checkpoints,
245
- ownership: authorization.ownership,
246
- metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
247
- redactor: options.redactor,
248
- signal: owned.signal,
249
- }), owned.signal);
250
- return respond(json(result, 200, limits, options));
251
- }
252
- finally {
253
- owned.dispose();
254
- release();
255
- }
256
- }
257
- if (route.kind === "workflow-status") {
258
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
259
- try {
260
- const record = await awaitWithSignal(getWorkflowRun(exposure.checkpoints, {
261
- workflowId: exposure.definition.id,
262
- runId: route.runId,
263
- ownership: authorization.ownership,
264
- signal: owned.signal,
265
- }), owned.signal);
266
- if (!record)
267
- throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
268
- return respond(json(record, 200, limits, options));
269
- }
270
- finally {
271
- owned.dispose();
272
- }
273
- }
274
- if (route.kind === "workflow-cancel") {
275
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
276
- try {
277
- const result = await awaitWithSignal(cancelWorkflowRun({
278
- workflowId: exposure.definition.id,
279
- runId: route.runId,
280
- workflow: exposure.definition,
281
- checkpoints: exposure.checkpoints,
282
- ownership: authorization.ownership,
283
- signal: owned.signal,
284
- }), owned.signal);
285
- return respond(json(result, 200, limits, options));
286
- }
287
- finally {
288
- owned.dispose();
289
- }
290
- }
291
- if (route.kind === "workflow-resume") {
292
- acquire();
293
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
294
- try {
295
- const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
296
- const result = await awaitWithSignal(resumeWorkflow(exposure.definition, {
297
- workflowId: exposure.definition.id,
298
- runId: route.runId,
299
- }, {
300
- ...exposure.runOptions,
301
- checkpoints: exposure.checkpoints,
302
- ownership: authorization.ownership,
303
- metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
304
- redactor: options.redactor,
305
- signal: owned.signal,
306
- resume: readResume(body),
307
- }), owned.signal);
308
- return respond(json(result, 200, limits, options));
309
- }
310
- finally {
311
- owned.dispose();
312
- release();
313
- }
314
- }
315
- acquire();
316
- const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
317
- try {
318
- const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
319
- const runId = readOptionalId(body.runId, "runId") ?? crypto.randomUUID();
320
- const workflowOptions = {
321
- ...exposure.runOptions,
322
- checkpoints: exposure.checkpoints,
323
- ownership: authorization.ownership,
324
- metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
325
- redactor: options.redactor,
326
- signal: owned.signal,
327
- runId,
328
- };
329
- if (route.kind === "workflow-run") {
330
- const result = await awaitWithSignal(runWorkflow(exposure.definition, body.input, workflowOptions), owned.signal);
331
- const response = respond(json(result, 200, limits, options));
332
- owned.dispose();
333
- release();
334
- return response;
335
- }
336
- const bus = createWorkflowEventBus({
337
- workflowId: exposure.definition.id,
338
- runId,
339
- maxQueuedEvents: limits.maxQueuedEvents,
340
- overflow: "close",
341
- signal: owned.signal,
342
- });
343
- const events = bus.subscribe();
344
- void runWorkflow(exposure.definition, body.input, { ...workflowOptions, eventBus: bus })
345
- .catch(() => undefined)
346
- .finally(() => bus.close());
347
- return respond(sse(events, owned, limits, options, release));
348
- }
349
- catch (error) {
350
- owned.dispose();
351
- release();
352
- throw error;
353
- }
354
- }
355
- catch (error) {
356
- return respond(errorResponse(error, limits, options));
357
- }
358
- };
359
- function acquire() {
360
- if (activeRuns >= limits.maxConcurrentRuns) {
361
- throw new PrismServerError("Server is busy", 429, "ERR_PRISM_SERVER_CONCURRENCY");
362
- }
363
- activeRuns += 1;
364
- }
365
- function release() {
366
- activeRuns = Math.max(0, activeRuns - 1);
367
- }
368
- }
369
- function parseRoute(request, base) {
370
- const pathname = new URL(request.url).pathname;
371
- if (pathname !== base && !pathname.startsWith(`${base}/`))
372
- return undefined;
373
- let parts;
374
- try {
375
- parts = pathname.slice(base.length).split("/").filter(Boolean).map(decodeURIComponent);
376
- }
377
- catch {
378
- throw new PrismServerError("Invalid route", 400, "ERR_PRISM_SERVER_ROUTE");
379
- }
380
- const [group, id, segment, runId, action] = parts;
381
- if (group === "schedules" && parts.length === 1 && request.method === "GET") {
382
- return { kind: "schedule-list", operation: "schedule.list", capabilityId: "*" };
383
- }
384
- if (!id || !validId(id))
385
- return undefined;
386
- if (group === "schedules") {
387
- if (parts.length === 2 && request.method === "POST")
388
- return { kind: "schedule-create", operation: "schedule.create", capabilityId: id };
389
- if (parts.length === 2 && request.method === "DELETE")
390
- return { kind: "schedule-delete", operation: "schedule.delete", capabilityId: id };
391
- if (parts.length === 3 && segment === "pause" && request.method === "POST")
392
- return { kind: "schedule-pause", operation: "schedule.pause", capabilityId: id };
393
- if (parts.length === 3 && segment === "resume" && request.method === "POST")
394
- return { kind: "schedule-resume", operation: "schedule.resume", capabilityId: id };
395
- if (parts.length === 3 && segment === "trigger" && request.method === "POST")
396
- return { kind: "schedule-trigger", operation: "schedule.trigger", capabilityId: id };
397
- return undefined;
398
- }
399
- if (group === "agents" && segment === "runs" && parts.length === 3 && request.method === "POST") {
400
- return { kind: "agent-run", operation: "agent.run", capabilityId: id };
401
- }
402
- if (group === "agents" && segment === "stream" && parts.length === 3 && request.method === "POST") {
403
- return { kind: "agent-stream", operation: "agent.stream", capabilityId: id };
404
- }
405
- if (group === "agents" && segment === "runs" && runId && validId(runId)) {
406
- if (parts.length === 4 && request.method === "GET")
407
- return { kind: "agent-status", operation: "agent.status", capabilityId: id, runId };
408
- if (parts.length === 5 && action === "resume" && request.method === "POST")
409
- return { kind: "agent-resume", operation: "agent.resume", capabilityId: id, runId };
410
- if (parts.length === 5 && action === "events" && request.method === "GET")
411
- return { kind: "agent-events", operation: "agent.events", capabilityId: id, runId };
412
- }
413
- if (group !== "workflows")
414
- return undefined;
415
- if (segment === "runs" && parts.length === 3 && request.method === "POST") {
416
- return { kind: "workflow-run", operation: "workflow.run", capabilityId: id };
417
- }
418
- if (segment === "stream" && parts.length === 3 && request.method === "POST") {
419
- return { kind: "workflow-stream", operation: "workflow.stream", capabilityId: id };
420
- }
421
- if (segment === "enqueue" && parts.length === 3 && request.method === "POST") {
422
- return { kind: "workflow-enqueue", operation: "workflow.enqueue", capabilityId: id };
423
- }
424
- if (segment !== "runs" || !runId || !validId(runId))
425
- return undefined;
426
- if (parts.length === 4 && request.method === "GET") {
427
- return { kind: "workflow-status", operation: "workflow.status", capabilityId: id, runId };
428
- }
429
- if (parts.length === 4 && request.method === "DELETE") {
430
- return { kind: "workflow-cancel", operation: "workflow.cancel", capabilityId: id, runId };
431
- }
432
- if (parts.length === 5 && action === "resume" && request.method === "POST") {
433
- return { kind: "workflow-resume", operation: "workflow.resume", capabilityId: id, runId };
434
- }
435
- if (parts.length === 5 && action === "replay" && request.method === "POST") {
436
- return { kind: "workflow-replay", operation: "workflow.replay", capabilityId: id, runId };
437
- }
438
- return undefined;
439
- }
440
- async function authorize(options, request, operation, capabilityId, timeoutMs) {
441
- let result;
442
- let timeout;
443
- const controller = new AbortController();
444
- const abort = () => controller.abort(request.signal.reason);
445
- if (request.signal.aborted)
446
- abort();
447
- else
448
- request.signal.addEventListener("abort", abort, { once: true });
449
- try {
450
- result = await Promise.race([
451
- options.authorize({ request, operation, capabilityId, signal: controller.signal }),
452
- new Promise((resolve) => {
453
- timeout = setTimeout(() => {
454
- controller.abort(new Error("authorization timed out"));
455
- resolve(false);
456
- }, timeoutMs);
457
- }),
458
- ]);
459
- }
460
- catch {
461
- return false;
462
- }
463
- finally {
464
- if (timeout)
465
- clearTimeout(timeout);
466
- request.signal.removeEventListener("abort", abort);
467
- }
468
- if (!result || !hasOwnership(result.ownership))
469
- return false;
470
- if (result.identity) {
471
- try {
472
- assertIdentityActive(result.identity);
473
- assertIdentityMatchesOwnership(result.identity, result.ownership);
474
- }
475
- catch {
476
- return false;
477
- }
478
- }
479
- return result;
480
- }
481
- function hasOwnership(value) {
482
- return [value.tenantId, value.accountId, value.userId].some((item) => typeof item === "string" && item.length > 0);
483
- }
484
- async function createSession(exposure, authorization) {
485
- if ("sessionFactory" in exposure) {
486
- return { session: await exposure.sessionFactory(authorization), runOptions: exposure.runOptions };
487
- }
488
- return { session: exposure.createSession() };
489
- }
490
- async function readJsonObject(request, maxBytes, signal) {
491
- const type = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
492
- if (type !== "application/json")
493
- throw new PrismServerError("Content-Type must be application/json", 415, "ERR_PRISM_SERVER_CONTENT_TYPE");
494
- const declared = Number(request.headers.get("content-length"));
495
- if (Number.isFinite(declared) && declared > maxBytes)
496
- throw new PrismServerError("Request body too large", 413, "ERR_PRISM_SERVER_BODY_LIMIT");
497
- const reader = request.body?.getReader();
498
- if (!reader)
499
- throw new PrismServerError("JSON body is required", 400, "ERR_PRISM_SERVER_BODY");
500
- const chunks = [];
501
- let size = 0;
502
- const abort = () => {
503
- void reader.cancel(signal.reason);
504
- };
505
- if (signal.aborted)
506
- abort();
507
- else
508
- signal.addEventListener("abort", abort, { once: true });
509
- try {
510
- while (true) {
511
- const next = await reader.read();
512
- if (next.done)
513
- break;
514
- size += next.value.byteLength;
515
- if (size > maxBytes) {
516
- await reader.cancel();
517
- throw new PrismServerError("Request body too large", 413, "ERR_PRISM_SERVER_BODY_LIMIT");
518
- }
519
- chunks.push(next.value);
520
- }
521
- }
522
- finally {
523
- signal.removeEventListener("abort", abort);
524
- reader.releaseLock();
525
- }
526
- if (signal.aborted)
527
- throw new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED");
528
- const bytes = new Uint8Array(size);
529
- let offset = 0;
530
- for (const chunk of chunks) {
531
- bytes.set(chunk, offset);
532
- offset += chunk.byteLength;
533
- }
534
- try {
535
- const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
536
- if (!value || typeof value !== "object" || Array.isArray(value))
537
- throw new Error("object required");
538
- return value;
539
- }
540
- catch (error) {
541
- if (error instanceof PrismServerError)
542
- throw error;
543
- throw new PrismServerError("Invalid JSON object body", 400, "ERR_PRISM_SERVER_BODY");
544
- }
545
- }
546
- function readAgentInput(value) {
547
- if (typeof value === "string")
548
- return value;
549
- if (isMessage(value))
550
- return value;
551
- if (Array.isArray(value) && value.length > 0 && value.every(isMessage))
552
- return value;
553
- throw new PrismServerError("input must be a string, message, or non-empty message array", 400, "ERR_PRISM_SERVER_INPUT");
554
- }
555
- function isMessage(value) {
556
- if (!value || typeof value !== "object" || Array.isArray(value))
557
- return false;
558
- const item = value;
559
- return ["system", "user", "assistant", "tool"].includes(String(item.role)) && Array.isArray(item.content);
560
- }
561
- const RUN_DECISION_OUTCOMES = new Set(["allow_once", "allow_for_run", "reject_once", "reject_for_run"]);
562
- const RUN_DECISION_KEYS = new Set(["approvalId", "outcome", "reason", "modifiedArguments", "elicitation"]);
563
- /** Boundary validation for a client-supplied decision batch; core re-validates under CAS. */
564
- function readAgentDecisions(value) {
565
- if (!Array.isArray(value) || value.length === 0 || value.length > HARD_MAX_PENDING_DECISIONS) {
566
- throw new PrismServerError("decisions must be a non-empty bounded array", 400, "ERR_PRISM_SERVER_RESUME");
567
- }
568
- return value.map((entry) => {
569
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
570
- throw new PrismServerError("decision entry must be an object", 400, "ERR_PRISM_SERVER_RESUME");
571
- }
572
- const row = entry;
573
- if (Object.keys(row).some((key) => !RUN_DECISION_KEYS.has(key))) {
574
- throw new PrismServerError("decision entry has unknown keys", 400, "ERR_PRISM_SERVER_RESUME");
575
- }
576
- if (typeof row.approvalId !== "string" || row.approvalId.length === 0 || row.approvalId.length > 128) {
577
- throw new PrismServerError("decision approvalId is invalid", 400, "ERR_PRISM_SERVER_RESUME");
578
- }
579
- if (typeof row.outcome !== "string" || !RUN_DECISION_OUTCOMES.has(row.outcome)) {
580
- throw new PrismServerError("decision outcome is invalid", 400, "ERR_PRISM_SERVER_RESUME");
581
- }
582
- if (row.reason !== undefined &&
583
- (typeof row.reason !== "string" || Buffer.byteLength(row.reason, "utf8") > HARD_MAX_DECISION_REASON_BYTES)) {
584
- throw new PrismServerError("decision reason exceeds limits", 400, "ERR_PRISM_SERVER_RESUME");
585
- }
586
- for (const key of ["modifiedArguments", "elicitation"]) {
587
- const field = row[key];
588
- if (field === undefined)
589
- continue;
590
- if (!field || typeof field !== "object" || Array.isArray(field)) {
591
- throw new PrismServerError(`decision ${key} must be an object`, 400, "ERR_PRISM_SERVER_RESUME");
592
- }
593
- const text = JSON.stringify(field);
594
- if (text === undefined || Buffer.byteLength(text, "utf8") > HARD_MAX_ELICITATION_BYTES) {
595
- throw new PrismServerError(`decision ${key} exceeds limits`, 400, "ERR_PRISM_SERVER_RESUME");
596
- }
597
- }
598
- return entry;
599
- });
600
- }
601
- function readAgentResume(body) {
602
- if (Object.keys(body).some((key) => key !== "decision" && key !== "decisions" && key !== "expectedVersion")) {
603
- throw new PrismServerError("Invalid agent resume body", 400, "ERR_PRISM_SERVER_RESUME");
604
- }
605
- if (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1) {
606
- throw new PrismServerError("expectedVersion must be a positive safe integer", 400, "ERR_PRISM_SERVER_RESUME");
607
- }
608
- if (body.decision !== undefined && body.decisions !== undefined) {
609
- throw new PrismServerError("provide exactly one of decision or decisions", 400, "ERR_PRISM_SERVER_RESUME");
610
- }
611
- if (body.decision !== undefined) {
612
- if (body.decision !== "approve" && body.decision !== "deny") {
613
- throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
614
- }
615
- return { decision: body.decision, expectedVersion: Number(body.expectedVersion) };
616
- }
617
- if (body.decisions === undefined) {
618
- throw new PrismServerError("provide decision or decisions", 400, "ERR_PRISM_SERVER_RESUME");
619
- }
620
- return { decisions: readAgentDecisions(body.decisions), expectedVersion: Number(body.expectedVersion) };
621
- }
622
- function readResume(body) {
623
- if (body.decision !== "approve" && body.decision !== "deny") {
624
- throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
625
- }
626
- if (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1) {
627
- throw new PrismServerError("expectedVersion must be a positive safe integer", 400, "ERR_PRISM_SERVER_RESUME");
628
- }
629
- return { decision: body.decision, input: body.input, expectedVersion: Number(body.expectedVersion) };
630
- }
631
- function readRequiredString(value, name) {
632
- if (typeof value !== "string" || value.length === 0)
633
- throw new PrismServerError(`${name} is required`, 400, "ERR_PRISM_SERVER_INPUT");
634
- return value;
635
- }
636
- function readRequiredId(value, name) {
637
- const result = readOptionalId(value, name);
638
- if (!result)
639
- throw new PrismServerError(`${name} is required`, 400, "ERR_PRISM_SERVER_ID");
640
- return result;
641
- }
642
- function readPositiveInteger(value, name) {
643
- const number = typeof value === "string" ? Number(value) : value;
644
- if (!Number.isSafeInteger(number) || Number(number) < 1)
645
- throw new PrismServerError(`${name} must be a positive safe integer`, 400, "ERR_PRISM_SERVER_INPUT");
646
- return Number(number);
647
- }
648
- function readOptionalObject(value, name) {
649
- if (value === undefined)
650
- return undefined;
651
- if (!value || typeof value !== "object" || Array.isArray(value))
652
- throw new PrismServerError(`${name} must be an object`, 400, "ERR_PRISM_SERVER_INPUT");
653
- return value;
654
- }
655
- function readScheduleStatus(value) {
656
- if (value === null)
657
- return undefined;
658
- if (value === "active" || value === "paused" || value === "completed")
659
- return value;
660
- throw new PrismServerError("status is invalid", 400, "ERR_PRISM_SERVER_INPUT");
661
- }
662
- function sameOwnership(left, right) {
663
- return left.tenantId === right.tenantId && left.accountId === right.accountId && left.userId === right.userId;
664
- }
665
- function readOptionalId(value, name) {
666
- if (value === undefined)
667
- return undefined;
668
- if (typeof value !== "string" || !validId(value))
669
- throw new PrismServerError(`${name} is invalid`, 400, "ERR_PRISM_SERVER_ID");
670
- return value;
671
- }
672
- function validId(value) {
673
- return value.length <= 128 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value);
674
- }
675
- function replayCursor(request, maxBytes) {
676
- const query = new URL(request.url).searchParams.get("cursor") ?? undefined;
677
- const header = request.headers.get("last-event-id") ?? undefined;
678
- if (query !== undefined && header !== undefined && query !== header) {
679
- throw new PrismServerError("Conflicting event cursors", 400, "ERR_PRISM_SERVER_REPLAY_CURSOR");
680
- }
681
- const cursor = header ?? query;
682
- if (cursor !== undefined && (Buffer.byteLength(cursor, "utf8") > maxBytes || /\r|\n|\0/.test(cursor))) {
683
- throw new PrismServerError("Invalid event cursor", 400, "ERR_PRISM_SERVER_REPLAY_CURSOR");
684
- }
685
- return cursor;
686
- }
687
- function normalizeBasePath(value) {
688
- if (!value.startsWith("/") || value.includes("?") || value.includes("#"))
689
- throw new RangeError("basePath must be an absolute URL path");
690
- const normalized = value.length > 1 ? value.replace(/\/+$/, "") : value;
691
- if (normalized === "/")
692
- throw new RangeError("basePath cannot expose the URL root");
693
- return normalized;
694
- }
695
- function assertRequestPolicy(request, hosts, origins) {
696
- if (hosts) {
697
- const host = request.headers.get("host") ?? new URL(request.url).host;
698
- if (!hosts.includes(host))
699
- throw new PrismServerError("Forbidden host", 403, "ERR_PRISM_SERVER_HOST");
700
- }
701
- const origin = request.headers.get("origin");
702
- if (origin && origins && !origins.includes(origin))
703
- throw new PrismServerError("Forbidden origin", 403, "ERR_PRISM_SERVER_ORIGIN");
704
- }
705
- async function awaitWithSignal(promise, signal) {
706
- if (signal.aborted)
707
- throw new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED");
708
- return new Promise((resolve, reject) => {
709
- const abort = () => reject(new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED"));
710
- signal.addEventListener("abort", abort, { once: true });
711
- promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
712
- });
713
- }
714
- function ownedSignal(request, timeoutMs, disconnectAborts) {
715
- const controller = new AbortController();
716
- const abort = () => controller.abort(request.signal.reason ?? new Error("request disconnected"));
717
- if (disconnectAborts) {
718
- if (request.signal.aborted)
719
- abort();
720
- else
721
- request.signal.addEventListener("abort", abort, { once: true });
722
- }
723
- const timeout = setTimeout(() => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)), timeoutMs);
724
- return {
725
- signal: controller.signal,
726
- abort: (reason) => controller.abort(reason),
727
- dispose() {
728
- clearTimeout(timeout);
729
- request.signal.removeEventListener("abort", abort);
730
- },
731
- };
732
- }
733
- function sseAgentEvents(source, owned, limits, options, release) {
734
- return sseStream(source, ({ record, cursor }) => {
735
- if (/\r|\n|\0/.test(cursor) || Buffer.byteLength(cursor, "utf8") > limits.maxReplayCursorBytes) {
736
- throw new PrismServerError("Invalid event cursor", 500, "ERR_PRISM_SERVER_REPLAY_CURSOR");
737
- }
738
- const safe = options.redactor?.redact(record.event) ?? record.event;
739
- return `id: ${cursor}\ndata: ${JSON.stringify(safe)}\n\n`;
740
- }, owned, limits, release);
741
- }
742
- function sse(source, owned, limits, options, release) {
743
- return sseStream(source, (value) => `data: ${JSON.stringify(options.redactor?.redact(value) ?? value)}\n\n`, owned, limits, release);
744
- }
745
- function sseStream(source, serialize, owned, limits, release) {
746
- const iterator = source[Symbol.asyncIterator]();
747
- const encoder = new TextEncoder();
748
- let events = 0;
749
- let bytes = 0;
750
- let finished = false;
751
- const onAbort = () => {
752
- void finish(owned.signal.reason);
753
- };
754
- const finish = async (reason) => {
755
- if (finished)
756
- return;
757
- finished = true;
758
- owned.signal.removeEventListener("abort", onAbort);
759
- owned.abort(reason);
760
- owned.dispose();
761
- release();
762
- await iterator.return?.();
763
- };
764
- owned.signal.addEventListener("abort", onAbort, { once: true });
765
- const stream = new ReadableStream({
766
- async pull(controller) {
767
- try {
768
- const next = await iterator.next();
769
- if (next.done) {
770
- await finish();
771
- controller.close();
772
- return;
773
- }
774
- const chunk = encoder.encode(serialize(next.value));
775
- events += 1;
776
- bytes += chunk.byteLength;
777
- if (chunk.byteLength > limits.maxEventBytes || events > limits.maxStreamEvents || bytes > limits.maxStreamBytes) {
778
- const error = encoder.encode('data: {"type":"error","error":{"code":"ERR_PRISM_SERVER_STREAM_LIMIT","message":"stream limit exceeded"}}\n\n');
779
- if (error.byteLength <= limits.maxEventBytes)
780
- controller.enqueue(error);
781
- await finish(new Error("stream limit exceeded"));
782
- controller.close();
783
- return;
784
- }
785
- controller.enqueue(chunk);
786
- }
787
- catch {
788
- const error = encoder.encode('data: {"type":"error","error":{"code":"ERR_PRISM_SERVER_STREAM","message":"stream failed"}}\n\n');
789
- if (error.byteLength <= limits.maxEventBytes)
790
- controller.enqueue(error);
791
- await finish(new Error("stream failed"));
792
- controller.close();
793
- }
794
- },
795
- cancel(reason) {
796
- return finish(reason);
797
- },
798
- });
799
- return new Response(stream, { status: 200, headers: SSE_HEADERS });
800
- }
801
- function json(value, status, limits, options) {
802
- const safe = options.redactor?.redact(value) ?? value;
803
- const text = JSON.stringify(safe);
804
- if (text === undefined || new TextEncoder().encode(text).byteLength > limits.maxResponseBytes) {
805
- throw new PrismServerError("Response too large", 507, "ERR_PRISM_SERVER_RESPONSE_LIMIT");
806
- }
807
- return new Response(text, { status, headers: JSON_HEADERS });
808
- }
809
- function errorResponse(error, limits, options) {
810
- const workflowCode = error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : undefined;
811
- const mapped = workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE_BUSY"
812
- ? { status: 409, code: workflowCode, message: "Schedule is busy" }
813
- : workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE"
814
- ? { status: 400, code: workflowCode, message: error instanceof Error ? error.message : "Invalid schedule" }
815
- : workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE_OWNERSHIP"
816
- ? { status: 403, code: workflowCode, message: "Forbidden" }
817
- : workflowCode === "ERR_PRISM_WORKFLOW_NOT_FOUND"
818
- ? { status: 404, code: workflowCode, message: "Not found" }
819
- : workflowCode === "ERR_PRISM_WORKFLOW_CHECKPOINT"
820
- ? { status: 409, code: workflowCode, message: "Workflow checkpoint operation rejected" }
821
- : undefined;
822
- const known = error instanceof PrismServerError;
823
- const agentState = error instanceof AgentRunStateError;
824
- const status = mapped?.status ?? (agentState ? 404 : known ? error.status : error instanceof DOMException && error.name === "AbortError" ? 499 : 500);
825
- const code = mapped?.code ??
826
- (agentState
827
- ? "ERR_PRISM_SERVER_NOT_FOUND"
828
- : known
829
- ? error.code
830
- : status === 499
831
- ? "ERR_PRISM_SERVER_ABORTED"
832
- : "ERR_PRISM_SERVER_INTERNAL");
833
- const message = mapped?.message ?? (agentState ? "Not found" : known ? error.message : status === 499 ? "Request aborted" : "Internal server error");
834
- try {
835
- const response = json({ error: { code, message } }, status, limits, options);
836
- if (known && error.headers)
837
- return addHeaders(response, error.headers);
838
- return response;
839
- }
840
- catch {
841
- return new Response(null, { status });
842
- }
843
- }
844
- function addHeaders(response, extra) {
845
- if (!extra)
846
- return response;
847
- const headers = new Headers(response.headers);
848
- for (const [name, value] of Object.entries(extra))
849
- headers.set(name, value);
850
- return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
851
- }
1
+ /** handler.ts barrel (0.2.5 plan 025 Task 1 god-module split): re-exports the
2
+ * public surface from cohesive family modules (0.1.4 barrel precedent). */
3
+ export * from "./handler/consts.js";
4
+ export * from "./handler/core.js";
5
+ export * from "./handler/routing.js";
6
+ export * from "./handler/authorize.js";
7
+ export * from "./handler/readers.js";
8
+ export * from "./handler/policy.js";
9
+ export * from "./handler/sse.js";
10
+ export * from "./handler/respond.js";
852
11
  //# sourceMappingURL=handler.js.map