@arnilo/prism-server 0.2.3 → 0.2.5
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/authorize.d.ts +9 -0
- package/dist/handler/authorize.js +55 -0
- package/dist/handler/consts.d.ts +9 -0
- package/dist/handler/consts.js +8 -0
- package/dist/handler/core.d.ts +3 -0
- package/dist/handler/core.js +368 -0
- package/dist/handler/policy.d.ts +7 -0
- package/dist/handler/policy.js +41 -0
- package/dist/handler/readers.d.ts +21 -0
- package/dist/handler/readers.js +197 -0
- package/dist/handler/respond.d.ts +5 -0
- package/dist/handler/respond.js +56 -0
- package/dist/handler/routing.d.ts +71 -0
- package/dist/handler/routing.js +83 -0
- package/dist/handler/sse.d.ts +8 -0
- package/dist/handler/sse.js +71 -0
- package/dist/handler.d.ts +10 -2
- package/dist/handler.js +10 -851
- package/package.json +3 -3
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS } from "@arnilo/prism";
|
|
2
|
+
import { PrismServerError } from "../types.js";
|
|
3
|
+
export async function readJsonObject(request, maxBytes, signal) {
|
|
4
|
+
const type = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
5
|
+
if (type !== "application/json")
|
|
6
|
+
throw new PrismServerError("Content-Type must be application/json", 415, "ERR_PRISM_SERVER_CONTENT_TYPE");
|
|
7
|
+
const declared = Number(request.headers.get("content-length"));
|
|
8
|
+
if (Number.isFinite(declared) && declared > maxBytes)
|
|
9
|
+
throw new PrismServerError("Request body too large", 413, "ERR_PRISM_SERVER_BODY_LIMIT");
|
|
10
|
+
const reader = request.body?.getReader();
|
|
11
|
+
if (!reader)
|
|
12
|
+
throw new PrismServerError("JSON body is required", 400, "ERR_PRISM_SERVER_BODY");
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let size = 0;
|
|
15
|
+
const abort = () => {
|
|
16
|
+
void reader.cancel(signal.reason);
|
|
17
|
+
};
|
|
18
|
+
if (signal.aborted)
|
|
19
|
+
abort();
|
|
20
|
+
else
|
|
21
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
22
|
+
try {
|
|
23
|
+
while (true) {
|
|
24
|
+
const next = await reader.read();
|
|
25
|
+
if (next.done)
|
|
26
|
+
break;
|
|
27
|
+
size += next.value.byteLength;
|
|
28
|
+
if (size > maxBytes) {
|
|
29
|
+
await reader.cancel();
|
|
30
|
+
throw new PrismServerError("Request body too large", 413, "ERR_PRISM_SERVER_BODY_LIMIT");
|
|
31
|
+
}
|
|
32
|
+
chunks.push(next.value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
signal.removeEventListener("abort", abort);
|
|
37
|
+
reader.releaseLock();
|
|
38
|
+
}
|
|
39
|
+
if (signal.aborted)
|
|
40
|
+
throw new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED");
|
|
41
|
+
const bytes = new Uint8Array(size);
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const chunk of chunks) {
|
|
44
|
+
bytes.set(chunk, offset);
|
|
45
|
+
offset += chunk.byteLength;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
49
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
50
|
+
throw new Error("object required");
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof PrismServerError)
|
|
55
|
+
throw error;
|
|
56
|
+
throw new PrismServerError("Invalid JSON object body", 400, "ERR_PRISM_SERVER_BODY");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function readAgentInput(value) {
|
|
60
|
+
if (typeof value === "string")
|
|
61
|
+
return value;
|
|
62
|
+
if (isMessage(value))
|
|
63
|
+
return value;
|
|
64
|
+
if (Array.isArray(value) && value.length > 0 && value.every(isMessage))
|
|
65
|
+
return value;
|
|
66
|
+
throw new PrismServerError("input must be a string, message, or non-empty message array", 400, "ERR_PRISM_SERVER_INPUT");
|
|
67
|
+
}
|
|
68
|
+
function isMessage(value) {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
70
|
+
return false;
|
|
71
|
+
const item = value;
|
|
72
|
+
return ["system", "user", "assistant", "tool"].includes(String(item.role)) && Array.isArray(item.content);
|
|
73
|
+
}
|
|
74
|
+
const RUN_DECISION_OUTCOMES = new Set(["allow_once", "allow_for_run", "reject_once", "reject_for_run"]);
|
|
75
|
+
const RUN_DECISION_KEYS = new Set(["approvalId", "outcome", "reason", "modifiedArguments", "elicitation"]);
|
|
76
|
+
/** Boundary validation for a client-supplied decision batch; core re-validates under CAS. */
|
|
77
|
+
function readAgentDecisions(value) {
|
|
78
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > HARD_MAX_PENDING_DECISIONS) {
|
|
79
|
+
throw new PrismServerError("decisions must be a non-empty bounded array", 400, "ERR_PRISM_SERVER_RESUME");
|
|
80
|
+
}
|
|
81
|
+
return value.map((entry) => {
|
|
82
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
83
|
+
throw new PrismServerError("decision entry must be an object", 400, "ERR_PRISM_SERVER_RESUME");
|
|
84
|
+
}
|
|
85
|
+
const row = entry;
|
|
86
|
+
if (Object.keys(row).some((key) => !RUN_DECISION_KEYS.has(key))) {
|
|
87
|
+
throw new PrismServerError("decision entry has unknown keys", 400, "ERR_PRISM_SERVER_RESUME");
|
|
88
|
+
}
|
|
89
|
+
if (typeof row.approvalId !== "string" || row.approvalId.length === 0 || row.approvalId.length > 128) {
|
|
90
|
+
throw new PrismServerError("decision approvalId is invalid", 400, "ERR_PRISM_SERVER_RESUME");
|
|
91
|
+
}
|
|
92
|
+
if (typeof row.outcome !== "string" || !RUN_DECISION_OUTCOMES.has(row.outcome)) {
|
|
93
|
+
throw new PrismServerError("decision outcome is invalid", 400, "ERR_PRISM_SERVER_RESUME");
|
|
94
|
+
}
|
|
95
|
+
if (row.reason !== undefined &&
|
|
96
|
+
(typeof row.reason !== "string" || Buffer.byteLength(row.reason, "utf8") > HARD_MAX_DECISION_REASON_BYTES)) {
|
|
97
|
+
throw new PrismServerError("decision reason exceeds limits", 400, "ERR_PRISM_SERVER_RESUME");
|
|
98
|
+
}
|
|
99
|
+
for (const key of ["modifiedArguments", "elicitation"]) {
|
|
100
|
+
const field = row[key];
|
|
101
|
+
if (field === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
if (!field || typeof field !== "object" || Array.isArray(field)) {
|
|
104
|
+
throw new PrismServerError(`decision ${key} must be an object`, 400, "ERR_PRISM_SERVER_RESUME");
|
|
105
|
+
}
|
|
106
|
+
const text = JSON.stringify(field);
|
|
107
|
+
if (text === undefined || Buffer.byteLength(text, "utf8") > HARD_MAX_ELICITATION_BYTES) {
|
|
108
|
+
throw new PrismServerError(`decision ${key} exceeds limits`, 400, "ERR_PRISM_SERVER_RESUME");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return entry;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function readAgentResume(body) {
|
|
115
|
+
if (Object.keys(body).some((key) => key !== "decision" && key !== "decisions" && key !== "expectedVersion")) {
|
|
116
|
+
throw new PrismServerError("Invalid agent resume body", 400, "ERR_PRISM_SERVER_RESUME");
|
|
117
|
+
}
|
|
118
|
+
if (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1) {
|
|
119
|
+
throw new PrismServerError("expectedVersion must be a positive safe integer", 400, "ERR_PRISM_SERVER_RESUME");
|
|
120
|
+
}
|
|
121
|
+
if (body.decision !== undefined && body.decisions !== undefined) {
|
|
122
|
+
throw new PrismServerError("provide exactly one of decision or decisions", 400, "ERR_PRISM_SERVER_RESUME");
|
|
123
|
+
}
|
|
124
|
+
if (body.decision !== undefined) {
|
|
125
|
+
if (body.decision !== "approve" && body.decision !== "deny") {
|
|
126
|
+
throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
|
|
127
|
+
}
|
|
128
|
+
return { decision: body.decision, expectedVersion: Number(body.expectedVersion) };
|
|
129
|
+
}
|
|
130
|
+
if (body.decisions === undefined) {
|
|
131
|
+
throw new PrismServerError("provide decision or decisions", 400, "ERR_PRISM_SERVER_RESUME");
|
|
132
|
+
}
|
|
133
|
+
return { decisions: readAgentDecisions(body.decisions), expectedVersion: Number(body.expectedVersion) };
|
|
134
|
+
}
|
|
135
|
+
export function readResume(body) {
|
|
136
|
+
if (body.decision !== "approve" && body.decision !== "deny") {
|
|
137
|
+
throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
|
|
138
|
+
}
|
|
139
|
+
if (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1) {
|
|
140
|
+
throw new PrismServerError("expectedVersion must be a positive safe integer", 400, "ERR_PRISM_SERVER_RESUME");
|
|
141
|
+
}
|
|
142
|
+
return { decision: body.decision, input: body.input, expectedVersion: Number(body.expectedVersion) };
|
|
143
|
+
}
|
|
144
|
+
export function readRequiredString(value, name) {
|
|
145
|
+
if (typeof value !== "string" || value.length === 0)
|
|
146
|
+
throw new PrismServerError(`${name} is required`, 400, "ERR_PRISM_SERVER_INPUT");
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
export function readRequiredId(value, name) {
|
|
150
|
+
const result = readOptionalId(value, name);
|
|
151
|
+
if (!result)
|
|
152
|
+
throw new PrismServerError(`${name} is required`, 400, "ERR_PRISM_SERVER_ID");
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
export function readPositiveInteger(value, name) {
|
|
156
|
+
const number = typeof value === "string" ? Number(value) : value;
|
|
157
|
+
if (!Number.isSafeInteger(number) || Number(number) < 1)
|
|
158
|
+
throw new PrismServerError(`${name} must be a positive safe integer`, 400, "ERR_PRISM_SERVER_INPUT");
|
|
159
|
+
return Number(number);
|
|
160
|
+
}
|
|
161
|
+
export function readOptionalObject(value, name) {
|
|
162
|
+
if (value === undefined)
|
|
163
|
+
return undefined;
|
|
164
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
165
|
+
throw new PrismServerError(`${name} must be an object`, 400, "ERR_PRISM_SERVER_INPUT");
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
export function readScheduleStatus(value) {
|
|
169
|
+
if (value === null)
|
|
170
|
+
return undefined;
|
|
171
|
+
if (value === "active" || value === "paused" || value === "completed")
|
|
172
|
+
return value;
|
|
173
|
+
throw new PrismServerError("status is invalid", 400, "ERR_PRISM_SERVER_INPUT");
|
|
174
|
+
}
|
|
175
|
+
export function readOptionalId(value, name) {
|
|
176
|
+
if (value === undefined)
|
|
177
|
+
return undefined;
|
|
178
|
+
if (typeof value !== "string" || !validId(value))
|
|
179
|
+
throw new PrismServerError(`${name} is invalid`, 400, "ERR_PRISM_SERVER_ID");
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
export function validId(value) {
|
|
183
|
+
return value.length <= 128 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value);
|
|
184
|
+
}
|
|
185
|
+
export function replayCursor(request, maxBytes) {
|
|
186
|
+
const query = new URL(request.url).searchParams.get("cursor") ?? undefined;
|
|
187
|
+
const header = request.headers.get("last-event-id") ?? undefined;
|
|
188
|
+
if (query !== undefined && header !== undefined && query !== header) {
|
|
189
|
+
throw new PrismServerError("Conflicting event cursors", 400, "ERR_PRISM_SERVER_REPLAY_CURSOR");
|
|
190
|
+
}
|
|
191
|
+
const cursor = header ?? query;
|
|
192
|
+
if (cursor !== undefined && (Buffer.byteLength(cursor, "utf8") > maxBytes || /\r|\n|\0/.test(cursor))) {
|
|
193
|
+
throw new PrismServerError("Invalid event cursor", 400, "ERR_PRISM_SERVER_REPLAY_CURSOR");
|
|
194
|
+
}
|
|
195
|
+
return cursor;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=readers.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CreatePrismHandlerOptions } from "../types.js";
|
|
2
|
+
import type { ResolvedPrismServerLimits } from "../limits.js";
|
|
3
|
+
export declare function json(value: unknown, status: number, limits: ResolvedPrismServerLimits, options: CreatePrismHandlerOptions): Response;
|
|
4
|
+
export declare function errorResponse(error: unknown, limits: ResolvedPrismServerLimits, options: CreatePrismHandlerOptions): Response;
|
|
5
|
+
export declare function addHeaders(response: Response, extra?: Readonly<Record<string, string>>): Response;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** respond (0.2.5 plan 025 Task 1 split). Moved verbatim from handler.ts; public surface unchanged behind the barrel. */
|
|
2
|
+
import { AgentRunStateError } from "@arnilo/prism";
|
|
3
|
+
import { PrismServerError } from "../types.js";
|
|
4
|
+
import { JSON_HEADERS } from "./consts.js";
|
|
5
|
+
export function json(value, status, limits, options) {
|
|
6
|
+
const safe = options.redactor?.redact(value) ?? value;
|
|
7
|
+
const text = JSON.stringify(safe);
|
|
8
|
+
if (text === undefined || new TextEncoder().encode(text).byteLength > limits.maxResponseBytes) {
|
|
9
|
+
throw new PrismServerError("Response too large", 507, "ERR_PRISM_SERVER_RESPONSE_LIMIT");
|
|
10
|
+
}
|
|
11
|
+
return new Response(text, { status, headers: JSON_HEADERS });
|
|
12
|
+
}
|
|
13
|
+
export function errorResponse(error, limits, options) {
|
|
14
|
+
const workflowCode = error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
15
|
+
const mapped = workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE_BUSY"
|
|
16
|
+
? { status: 409, code: workflowCode, message: "Schedule is busy" }
|
|
17
|
+
: workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE"
|
|
18
|
+
? { status: 400, code: workflowCode, message: error instanceof Error ? error.message : "Invalid schedule" }
|
|
19
|
+
: workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE_OWNERSHIP"
|
|
20
|
+
? { status: 403, code: workflowCode, message: "Forbidden" }
|
|
21
|
+
: workflowCode === "ERR_PRISM_WORKFLOW_NOT_FOUND"
|
|
22
|
+
? { status: 404, code: workflowCode, message: "Not found" }
|
|
23
|
+
: workflowCode === "ERR_PRISM_WORKFLOW_CHECKPOINT"
|
|
24
|
+
? { status: 409, code: workflowCode, message: "Workflow checkpoint operation rejected" }
|
|
25
|
+
: undefined;
|
|
26
|
+
const known = error instanceof PrismServerError;
|
|
27
|
+
const agentState = error instanceof AgentRunStateError;
|
|
28
|
+
const status = mapped?.status ?? (agentState ? 404 : known ? error.status : error instanceof DOMException && error.name === "AbortError" ? 499 : 500);
|
|
29
|
+
const code = mapped?.code ??
|
|
30
|
+
(agentState
|
|
31
|
+
? "ERR_PRISM_SERVER_NOT_FOUND"
|
|
32
|
+
: known
|
|
33
|
+
? error.code
|
|
34
|
+
: status === 499
|
|
35
|
+
? "ERR_PRISM_SERVER_ABORTED"
|
|
36
|
+
: "ERR_PRISM_SERVER_INTERNAL");
|
|
37
|
+
const message = mapped?.message ?? (agentState ? "Not found" : known ? error.message : status === 499 ? "Request aborted" : "Internal server error");
|
|
38
|
+
try {
|
|
39
|
+
const response = json({ error: { code, message } }, status, limits, options);
|
|
40
|
+
if (known && error.headers)
|
|
41
|
+
return addHeaders(response, error.headers);
|
|
42
|
+
return response;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return new Response(null, { status });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function addHeaders(response, extra) {
|
|
49
|
+
if (!extra)
|
|
50
|
+
return response;
|
|
51
|
+
const headers = new Headers(response.headers);
|
|
52
|
+
for (const [name, value] of Object.entries(extra))
|
|
53
|
+
headers.set(name, value);
|
|
54
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=respond.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
type Route = {
|
|
2
|
+
readonly kind: "agent-run" | "agent-stream";
|
|
3
|
+
readonly operation: "agent.run" | "agent.stream";
|
|
4
|
+
readonly capabilityId: string;
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: "agent-status";
|
|
7
|
+
readonly operation: "agent.status";
|
|
8
|
+
readonly capabilityId: string;
|
|
9
|
+
readonly runId: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "agent-resume";
|
|
12
|
+
readonly operation: "agent.resume";
|
|
13
|
+
readonly capabilityId: string;
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: "agent-events";
|
|
17
|
+
readonly operation: "agent.events";
|
|
18
|
+
readonly capabilityId: string;
|
|
19
|
+
readonly runId: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "workflow-run" | "workflow-stream" | "workflow-enqueue";
|
|
22
|
+
readonly operation: "workflow.run" | "workflow.stream" | "workflow.enqueue";
|
|
23
|
+
readonly capabilityId: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "workflow-status";
|
|
26
|
+
readonly operation: "workflow.status";
|
|
27
|
+
readonly capabilityId: string;
|
|
28
|
+
readonly runId: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: "workflow-cancel";
|
|
31
|
+
readonly operation: "workflow.cancel";
|
|
32
|
+
readonly capabilityId: string;
|
|
33
|
+
readonly runId: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: "workflow-resume";
|
|
36
|
+
readonly operation: "workflow.resume";
|
|
37
|
+
readonly capabilityId: string;
|
|
38
|
+
readonly runId: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly kind: "workflow-replay";
|
|
41
|
+
readonly operation: "workflow.replay";
|
|
42
|
+
readonly capabilityId: string;
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: "schedule-list";
|
|
46
|
+
readonly operation: "schedule.list";
|
|
47
|
+
readonly capabilityId: "*";
|
|
48
|
+
} | {
|
|
49
|
+
readonly kind: "schedule-create";
|
|
50
|
+
readonly operation: "schedule.create";
|
|
51
|
+
readonly capabilityId: string;
|
|
52
|
+
} | {
|
|
53
|
+
readonly kind: "schedule-pause";
|
|
54
|
+
readonly operation: "schedule.pause";
|
|
55
|
+
readonly capabilityId: string;
|
|
56
|
+
} | {
|
|
57
|
+
readonly kind: "schedule-resume";
|
|
58
|
+
readonly operation: "schedule.resume";
|
|
59
|
+
readonly capabilityId: string;
|
|
60
|
+
} | {
|
|
61
|
+
readonly kind: "schedule-trigger";
|
|
62
|
+
readonly operation: "schedule.trigger";
|
|
63
|
+
readonly capabilityId: string;
|
|
64
|
+
} | {
|
|
65
|
+
readonly kind: "schedule-delete";
|
|
66
|
+
readonly operation: "schedule.delete";
|
|
67
|
+
readonly capabilityId: string;
|
|
68
|
+
};
|
|
69
|
+
export declare function parseRoute(request: Request, base: string): Route | undefined;
|
|
70
|
+
export declare function normalizeBasePath(value: string): string;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** routing (0.2.5 plan 025 Task 1 split). Moved verbatim from handler.ts; public surface unchanged behind the barrel. */
|
|
2
|
+
import { PrismServerError } from "../types.js";
|
|
3
|
+
import { validId } from "./readers.js";
|
|
4
|
+
export function parseRoute(request, base) {
|
|
5
|
+
const pathname = new URL(request.url).pathname;
|
|
6
|
+
if (pathname !== base && !pathname.startsWith(`${base}/`))
|
|
7
|
+
return undefined;
|
|
8
|
+
let parts;
|
|
9
|
+
try {
|
|
10
|
+
parts = pathname.slice(base.length).split("/").filter(Boolean).map(decodeURIComponent);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new PrismServerError("Invalid route", 400, "ERR_PRISM_SERVER_ROUTE");
|
|
14
|
+
}
|
|
15
|
+
const [group, id, segment, runId, action] = parts;
|
|
16
|
+
if (group === "schedules" && parts.length === 1 && request.method === "GET") {
|
|
17
|
+
return { kind: "schedule-list", operation: "schedule.list", capabilityId: "*" };
|
|
18
|
+
}
|
|
19
|
+
if (!id || !validId(id))
|
|
20
|
+
return undefined;
|
|
21
|
+
if (group === "schedules") {
|
|
22
|
+
if (parts.length === 2 && request.method === "POST")
|
|
23
|
+
return { kind: "schedule-create", operation: "schedule.create", capabilityId: id };
|
|
24
|
+
if (parts.length === 2 && request.method === "DELETE")
|
|
25
|
+
return { kind: "schedule-delete", operation: "schedule.delete", capabilityId: id };
|
|
26
|
+
if (parts.length === 3 && segment === "pause" && request.method === "POST")
|
|
27
|
+
return { kind: "schedule-pause", operation: "schedule.pause", capabilityId: id };
|
|
28
|
+
if (parts.length === 3 && segment === "resume" && request.method === "POST")
|
|
29
|
+
return { kind: "schedule-resume", operation: "schedule.resume", capabilityId: id };
|
|
30
|
+
if (parts.length === 3 && segment === "trigger" && request.method === "POST")
|
|
31
|
+
return { kind: "schedule-trigger", operation: "schedule.trigger", capabilityId: id };
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (group === "agents" && segment === "runs" && parts.length === 3 && request.method === "POST") {
|
|
35
|
+
return { kind: "agent-run", operation: "agent.run", capabilityId: id };
|
|
36
|
+
}
|
|
37
|
+
if (group === "agents" && segment === "stream" && parts.length === 3 && request.method === "POST") {
|
|
38
|
+
return { kind: "agent-stream", operation: "agent.stream", capabilityId: id };
|
|
39
|
+
}
|
|
40
|
+
if (group === "agents" && segment === "runs" && runId && validId(runId)) {
|
|
41
|
+
if (parts.length === 4 && request.method === "GET")
|
|
42
|
+
return { kind: "agent-status", operation: "agent.status", capabilityId: id, runId };
|
|
43
|
+
if (parts.length === 5 && action === "resume" && request.method === "POST")
|
|
44
|
+
return { kind: "agent-resume", operation: "agent.resume", capabilityId: id, runId };
|
|
45
|
+
if (parts.length === 5 && action === "events" && request.method === "GET")
|
|
46
|
+
return { kind: "agent-events", operation: "agent.events", capabilityId: id, runId };
|
|
47
|
+
}
|
|
48
|
+
if (group !== "workflows")
|
|
49
|
+
return undefined;
|
|
50
|
+
if (segment === "runs" && parts.length === 3 && request.method === "POST") {
|
|
51
|
+
return { kind: "workflow-run", operation: "workflow.run", capabilityId: id };
|
|
52
|
+
}
|
|
53
|
+
if (segment === "stream" && parts.length === 3 && request.method === "POST") {
|
|
54
|
+
return { kind: "workflow-stream", operation: "workflow.stream", capabilityId: id };
|
|
55
|
+
}
|
|
56
|
+
if (segment === "enqueue" && parts.length === 3 && request.method === "POST") {
|
|
57
|
+
return { kind: "workflow-enqueue", operation: "workflow.enqueue", capabilityId: id };
|
|
58
|
+
}
|
|
59
|
+
if (segment !== "runs" || !runId || !validId(runId))
|
|
60
|
+
return undefined;
|
|
61
|
+
if (parts.length === 4 && request.method === "GET") {
|
|
62
|
+
return { kind: "workflow-status", operation: "workflow.status", capabilityId: id, runId };
|
|
63
|
+
}
|
|
64
|
+
if (parts.length === 4 && request.method === "DELETE") {
|
|
65
|
+
return { kind: "workflow-cancel", operation: "workflow.cancel", capabilityId: id, runId };
|
|
66
|
+
}
|
|
67
|
+
if (parts.length === 5 && action === "resume" && request.method === "POST") {
|
|
68
|
+
return { kind: "workflow-resume", operation: "workflow.resume", capabilityId: id, runId };
|
|
69
|
+
}
|
|
70
|
+
if (parts.length === 5 && action === "replay" && request.method === "POST") {
|
|
71
|
+
return { kind: "workflow-replay", operation: "workflow.replay", capabilityId: id, runId };
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
export function normalizeBasePath(value) {
|
|
76
|
+
if (!value.startsWith("/") || value.includes("?") || value.includes("#"))
|
|
77
|
+
throw new RangeError("basePath must be an absolute URL path");
|
|
78
|
+
const normalized = value.length > 1 ? value.replace(/\/+$/, "") : value;
|
|
79
|
+
if (normalized === "/")
|
|
80
|
+
throw new RangeError("basePath cannot expose the URL root");
|
|
81
|
+
return normalized;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=routing.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** sse (0.2.5 plan 025 Task 1 split). Moved verbatim from handler.ts; public surface unchanged behind the barrel. */
|
|
2
|
+
import type { AgentEvent, AgentEventEnvelope } from "@arnilo/prism";
|
|
3
|
+
import type { CreatePrismHandlerOptions } from "../types.js";
|
|
4
|
+
import type { ResolvedPrismServerLimits } from "../limits.js";
|
|
5
|
+
import type { WorkflowEvent } from "@arnilo/prism-workflows";
|
|
6
|
+
import { ownedSignal } from "./policy.js";
|
|
7
|
+
export declare function sseAgentEvents(source: AsyncIterable<AgentEventEnvelope>, owned: ReturnType<typeof ownedSignal>, limits: ResolvedPrismServerLimits, options: CreatePrismHandlerOptions, release: () => void): Response;
|
|
8
|
+
export declare function sse(source: AsyncIterable<AgentEvent | WorkflowEvent>, owned: ReturnType<typeof ownedSignal>, limits: ResolvedPrismServerLimits, options: CreatePrismHandlerOptions, release: () => void): Response;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { PrismServerError } from "../types.js";
|
|
2
|
+
import { SSE_HEADERS } from "./consts.js";
|
|
3
|
+
export function sseAgentEvents(source, owned, limits, options, release) {
|
|
4
|
+
return sseStream(source, ({ record, cursor }) => {
|
|
5
|
+
if (/\r|\n|\0/.test(cursor) || Buffer.byteLength(cursor, "utf8") > limits.maxReplayCursorBytes) {
|
|
6
|
+
throw new PrismServerError("Invalid event cursor", 500, "ERR_PRISM_SERVER_REPLAY_CURSOR");
|
|
7
|
+
}
|
|
8
|
+
const safe = options.redactor?.redact(record.event) ?? record.event;
|
|
9
|
+
return `id: ${cursor}\ndata: ${JSON.stringify(safe)}\n\n`;
|
|
10
|
+
}, owned, limits, release);
|
|
11
|
+
}
|
|
12
|
+
export function sse(source, owned, limits, options, release) {
|
|
13
|
+
return sseStream(source, (value) => `data: ${JSON.stringify(options.redactor?.redact(value) ?? value)}\n\n`, owned, limits, release);
|
|
14
|
+
}
|
|
15
|
+
function sseStream(source, serialize, owned, limits, release) {
|
|
16
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
let events = 0;
|
|
19
|
+
let bytes = 0;
|
|
20
|
+
let finished = false;
|
|
21
|
+
const onAbort = () => {
|
|
22
|
+
void finish(owned.signal.reason);
|
|
23
|
+
};
|
|
24
|
+
const finish = async (reason) => {
|
|
25
|
+
if (finished)
|
|
26
|
+
return;
|
|
27
|
+
finished = true;
|
|
28
|
+
owned.signal.removeEventListener("abort", onAbort);
|
|
29
|
+
owned.abort(reason);
|
|
30
|
+
owned.dispose();
|
|
31
|
+
release();
|
|
32
|
+
await iterator.return?.();
|
|
33
|
+
};
|
|
34
|
+
owned.signal.addEventListener("abort", onAbort, { once: true });
|
|
35
|
+
const stream = new ReadableStream({
|
|
36
|
+
async pull(controller) {
|
|
37
|
+
try {
|
|
38
|
+
const next = await iterator.next();
|
|
39
|
+
if (next.done) {
|
|
40
|
+
await finish();
|
|
41
|
+
controller.close();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const chunk = encoder.encode(serialize(next.value));
|
|
45
|
+
events += 1;
|
|
46
|
+
bytes += chunk.byteLength;
|
|
47
|
+
if (chunk.byteLength > limits.maxEventBytes || events > limits.maxStreamEvents || bytes > limits.maxStreamBytes) {
|
|
48
|
+
const error = encoder.encode('data: {"type":"error","error":{"code":"ERR_PRISM_SERVER_STREAM_LIMIT","message":"stream limit exceeded"}}\n\n');
|
|
49
|
+
if (error.byteLength <= limits.maxEventBytes)
|
|
50
|
+
controller.enqueue(error);
|
|
51
|
+
await finish(new Error("stream limit exceeded"));
|
|
52
|
+
controller.close();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
controller.enqueue(chunk);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
const error = encoder.encode('data: {"type":"error","error":{"code":"ERR_PRISM_SERVER_STREAM","message":"stream failed"}}\n\n');
|
|
59
|
+
if (error.byteLength <= limits.maxEventBytes)
|
|
60
|
+
controller.enqueue(error);
|
|
61
|
+
await finish(new Error("stream failed"));
|
|
62
|
+
controller.close();
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
cancel(reason) {
|
|
66
|
+
return finish(reason);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return new Response(stream, { status: 200, headers: SSE_HEADERS });
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=sse.js.map
|
package/dist/handler.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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";
|