@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,9 @@
|
|
|
1
|
+
/** authorize (0.2.5 plan 025 Task 1 split). Moved verbatim from handler.ts; public surface unchanged behind the barrel. */
|
|
2
|
+
import type { Agent, AgentSession } from "@arnilo/prism";
|
|
3
|
+
import type { CreatePrismHandlerOptions, PrismAgentExposure, PrismServerAuthorization, PrismServerOperation } from "../types.js";
|
|
4
|
+
export declare function authorize(options: CreatePrismHandlerOptions, request: Request, operation: PrismServerOperation, capabilityId: string, timeoutMs: number): Promise<PrismServerAuthorization | false>;
|
|
5
|
+
export declare function createSession(exposure: Agent | PrismAgentExposure, authorization: PrismServerAuthorization): Promise<{
|
|
6
|
+
readonly session: AgentSession;
|
|
7
|
+
readonly runOptions?: PrismAgentExposure["runOptions"];
|
|
8
|
+
}>;
|
|
9
|
+
export declare function sameOwnership(left: PrismServerAuthorization["ownership"], right: PrismServerAuthorization["ownership"]): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { assertIdentityActive, assertIdentityMatchesOwnership } from "@arnilo/prism";
|
|
2
|
+
export async function authorize(options, request, operation, capabilityId, timeoutMs) {
|
|
3
|
+
let result;
|
|
4
|
+
let timeout;
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
const abort = () => controller.abort(request.signal.reason);
|
|
7
|
+
if (request.signal.aborted)
|
|
8
|
+
abort();
|
|
9
|
+
else
|
|
10
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
11
|
+
try {
|
|
12
|
+
result = await Promise.race([
|
|
13
|
+
options.authorize({ request, operation, capabilityId, signal: controller.signal }),
|
|
14
|
+
new Promise((resolve) => {
|
|
15
|
+
timeout = setTimeout(() => {
|
|
16
|
+
controller.abort(new Error("authorization timed out"));
|
|
17
|
+
resolve(false);
|
|
18
|
+
}, timeoutMs);
|
|
19
|
+
}),
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (timeout)
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
request.signal.removeEventListener("abort", abort);
|
|
29
|
+
}
|
|
30
|
+
if (!result || !hasOwnership(result.ownership))
|
|
31
|
+
return false;
|
|
32
|
+
if (result.identity) {
|
|
33
|
+
try {
|
|
34
|
+
assertIdentityActive(result.identity);
|
|
35
|
+
assertIdentityMatchesOwnership(result.identity, result.ownership);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function hasOwnership(value) {
|
|
44
|
+
return [value.tenantId, value.accountId, value.userId].some((item) => typeof item === "string" && item.length > 0);
|
|
45
|
+
}
|
|
46
|
+
export async function createSession(exposure, authorization) {
|
|
47
|
+
if ("sessionFactory" in exposure) {
|
|
48
|
+
return { session: await exposure.sessionFactory(authorization), runOptions: exposure.runOptions };
|
|
49
|
+
}
|
|
50
|
+
return { session: exposure.createSession() };
|
|
51
|
+
}
|
|
52
|
+
export function sameOwnership(left, right) {
|
|
53
|
+
return left.tenantId === right.tenantId && left.accountId === right.accountId && left.userId === right.userId;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=authorize.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** consts (0.2.5 plan 025 Task 1 split). Moved verbatim from handler.ts; public surface unchanged behind the barrel. */
|
|
2
|
+
export declare const JSON_HEADERS: {
|
|
3
|
+
"content-type": string;
|
|
4
|
+
};
|
|
5
|
+
export declare const SSE_HEADERS: {
|
|
6
|
+
"content-type": string;
|
|
7
|
+
"cache-control": string;
|
|
8
|
+
connection: string;
|
|
9
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** consts (0.2.5 plan 025 Task 1 split). Moved verbatim from handler.ts; public surface unchanged behind the barrel. */
|
|
2
|
+
export const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
3
|
+
export const SSE_HEADERS = {
|
|
4
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
5
|
+
"cache-control": "no-cache, no-transform",
|
|
6
|
+
connection: "keep-alive",
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=consts.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** core (0.2.5 plan 025 Task 1 split). Moved verbatim from handler.ts; public surface unchanged behind the barrel. */
|
|
2
|
+
import type { CreatePrismHandlerOptions, PrismRequestHandler } from "../types.js";
|
|
3
|
+
export declare function createPrismHandler(options: CreatePrismHandlerOptions): PrismRequestHandler;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { PrismServerError } from "../types.js";
|
|
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 { addHeaders, errorResponse, json } from "./respond.js";
|
|
6
|
+
import { assertRequestPolicy, awaitWithSignal, ownedSignal } from "./policy.js";
|
|
7
|
+
import { authorize, createSession, sameOwnership } from "./authorize.js";
|
|
8
|
+
import { normalizeBasePath, parseRoute } from "./routing.js";
|
|
9
|
+
import { readAgentInput, readAgentResume, readJsonObject, readOptionalId, readOptionalObject, readPositiveInteger, readRequiredId, readRequiredString, readResume, readScheduleStatus, replayCursor, } from "./readers.js";
|
|
10
|
+
import { sse, sseAgentEvents } from "./sse.js";
|
|
11
|
+
export function createPrismHandler(options) {
|
|
12
|
+
const limits = resolvePrismServerLimits(options.limits);
|
|
13
|
+
const base = normalizeBasePath(options.basePath ?? "/prism");
|
|
14
|
+
let activeRuns = 0;
|
|
15
|
+
return async (request) => {
|
|
16
|
+
const origin = request.headers.get("origin");
|
|
17
|
+
const corsHeaders = origin && options.allowedOrigins?.includes(origin) ? { "access-control-allow-origin": origin, vary: "origin" } : undefined;
|
|
18
|
+
const respond = (response) => addHeaders(response, corsHeaders);
|
|
19
|
+
try {
|
|
20
|
+
assertRequestPolicy(request, options.allowedHosts, options.allowedOrigins);
|
|
21
|
+
const route = parseRoute(request, base);
|
|
22
|
+
if (request.method === "OPTIONS") {
|
|
23
|
+
if (!origin || !options.allowedOrigins?.includes(origin))
|
|
24
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
25
|
+
return respond(new Response(null, {
|
|
26
|
+
status: 204,
|
|
27
|
+
headers: {
|
|
28
|
+
"access-control-allow-origin": origin,
|
|
29
|
+
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
30
|
+
"access-control-allow-headers": "content-type, authorization, last-event-id",
|
|
31
|
+
vary: "origin",
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
if (!route)
|
|
36
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
37
|
+
const authorization = await authorize(options, request, route.operation, route.capabilityId, limits.requestTimeoutMs);
|
|
38
|
+
if (!authorization)
|
|
39
|
+
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
40
|
+
if (options.rateLimit) {
|
|
41
|
+
const decision = await options.rateLimit({
|
|
42
|
+
request,
|
|
43
|
+
operation: route.operation,
|
|
44
|
+
capabilityId: route.capabilityId,
|
|
45
|
+
authorization,
|
|
46
|
+
signal: request.signal,
|
|
47
|
+
});
|
|
48
|
+
if (decision !== true) {
|
|
49
|
+
const headers = {};
|
|
50
|
+
if (decision.retryAfterMs !== undefined && Number.isSafeInteger(decision.retryAfterMs) && decision.retryAfterMs > 0) {
|
|
51
|
+
headers["retry-after"] = String(Math.ceil(decision.retryAfterMs / 1000));
|
|
52
|
+
}
|
|
53
|
+
throw new PrismServerError(decision.message ?? "Rate limit exceeded", 429, decision.code ?? "ERR_PRISM_SERVER_RATE_LIMIT", Object.keys(headers).length ? headers : undefined);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (options.drain && isAdmitOperation(route.operation))
|
|
57
|
+
options.drain.assertAdmit();
|
|
58
|
+
if (route.kind.startsWith("schedule-")) {
|
|
59
|
+
const selectedSchedules = options.schedules;
|
|
60
|
+
if (!selectedSchedules)
|
|
61
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
62
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
63
|
+
try {
|
|
64
|
+
const schedules = typeof selectedSchedules === "function"
|
|
65
|
+
? await awaitWithSignal(Promise.resolve(selectedSchedules(authorization, owned.signal)), owned.signal)
|
|
66
|
+
: selectedSchedules;
|
|
67
|
+
if (!sameOwnership(authorization.ownership, schedules.ownership)) {
|
|
68
|
+
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
69
|
+
}
|
|
70
|
+
if (route.kind === "schedule-list") {
|
|
71
|
+
const query = new URL(request.url).searchParams;
|
|
72
|
+
const status = query.get("status");
|
|
73
|
+
const result = await awaitWithSignal(schedules.list({
|
|
74
|
+
status: readScheduleStatus(status),
|
|
75
|
+
cursor: query.get("cursor") ?? undefined,
|
|
76
|
+
limit: query.has("limit") ? readPositiveInteger(query.get("limit"), "limit") : undefined,
|
|
77
|
+
signal: owned.signal,
|
|
78
|
+
}), owned.signal);
|
|
79
|
+
return respond(json(result, 200, limits, options));
|
|
80
|
+
}
|
|
81
|
+
if (route.kind === "schedule-delete") {
|
|
82
|
+
const result = await awaitWithSignal(schedules.delete(route.capabilityId, owned.signal), owned.signal);
|
|
83
|
+
return respond(json({ deleted: result }, 200, limits, options));
|
|
84
|
+
}
|
|
85
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
86
|
+
if (route.kind === "schedule-create") {
|
|
87
|
+
const result = await awaitWithSignal(schedules.create({
|
|
88
|
+
id: route.capabilityId,
|
|
89
|
+
workflowId: readRequiredId(body.workflowId, "workflowId"),
|
|
90
|
+
nextRunAt: readRequiredString(body.nextRunAt, "nextRunAt"),
|
|
91
|
+
input: body.input,
|
|
92
|
+
intervalMs: body.intervalMs === undefined ? undefined : readPositiveInteger(body.intervalMs, "intervalMs"),
|
|
93
|
+
calculatorId: readOptionalId(body.calculatorId, "calculatorId"),
|
|
94
|
+
paused: body.paused === true,
|
|
95
|
+
metadata: readOptionalObject(body.metadata, "metadata"),
|
|
96
|
+
}, owned.signal), owned.signal);
|
|
97
|
+
return respond(json(result, 201, limits, options));
|
|
98
|
+
}
|
|
99
|
+
if (route.kind === "schedule-pause") {
|
|
100
|
+
return respond(json(await awaitWithSignal(schedules.pause(route.capabilityId, owned.signal), owned.signal), 200, limits, options));
|
|
101
|
+
}
|
|
102
|
+
if (route.kind === "schedule-resume") {
|
|
103
|
+
const nextRunAt = body.nextRunAt === undefined ? undefined : readRequiredString(body.nextRunAt, "nextRunAt");
|
|
104
|
+
return respond(json(await awaitWithSignal(schedules.resume(route.capabilityId, nextRunAt, owned.signal), owned.signal), 200, limits, options));
|
|
105
|
+
}
|
|
106
|
+
const idempotencyKey = readRequiredId(body.idempotencyKey, "idempotencyKey");
|
|
107
|
+
return respond(json(await awaitWithSignal(schedules.trigger(route.capabilityId, { idempotencyKey, signal: owned.signal }), owned.signal), 200, limits, options));
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
owned.dispose();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (route.kind === "agent-events") {
|
|
114
|
+
const exposure = options.agents?.[route.capabilityId];
|
|
115
|
+
if (!exposure || !("sessionFactory" in exposure) || !exposure.events || !exposure.resolveRun) {
|
|
116
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
117
|
+
}
|
|
118
|
+
if (!authorization.ownership.tenantId)
|
|
119
|
+
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
120
|
+
acquire();
|
|
121
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
122
|
+
try {
|
|
123
|
+
const run = await awaitWithSignal(Promise.resolve(exposure.resolveRun({ runId: route.runId, authorization, signal: owned.signal })), owned.signal);
|
|
124
|
+
if (!run?.sessionId)
|
|
125
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
126
|
+
const after = replayCursor(request, limits.maxReplayCursorBytes);
|
|
127
|
+
const events = exposure.events.subscribe({
|
|
128
|
+
ownership: authorization.ownership,
|
|
129
|
+
sessionId: run.sessionId,
|
|
130
|
+
runId: run.runId,
|
|
131
|
+
after,
|
|
132
|
+
signal: owned.signal,
|
|
133
|
+
});
|
|
134
|
+
return respond(sseAgentEvents(events, owned, limits, options, release));
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
owned.dispose();
|
|
138
|
+
release();
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (route.kind === "agent-status" || route.kind === "agent-resume") {
|
|
143
|
+
const exposure = options.agentRuns?.[route.capabilityId];
|
|
144
|
+
if (!exposure)
|
|
145
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
146
|
+
if (route.kind === "agent-status") {
|
|
147
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
148
|
+
try {
|
|
149
|
+
return respond(json(await awaitWithSignal(exposure.lifecycle.status({ runId: route.runId }, {
|
|
150
|
+
ownership: authorization.ownership,
|
|
151
|
+
signal: owned.signal,
|
|
152
|
+
agentId: route.capabilityId,
|
|
153
|
+
}), owned.signal), 200, limits, options));
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
owned.dispose();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
acquire();
|
|
160
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
161
|
+
try {
|
|
162
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
163
|
+
return respond(json(await awaitWithSignal(exposure.lifecycle.resume({ runId: route.runId }, readAgentResume(body), {
|
|
164
|
+
ownership: authorization.ownership,
|
|
165
|
+
signal: owned.signal,
|
|
166
|
+
agentId: route.capabilityId,
|
|
167
|
+
}), owned.signal), 200, limits, options));
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
owned.dispose();
|
|
171
|
+
release();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (route.kind === "agent-run" || route.kind === "agent-stream") {
|
|
175
|
+
const exposure = options.agents?.[route.capabilityId];
|
|
176
|
+
if (!exposure)
|
|
177
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
178
|
+
acquire();
|
|
179
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
180
|
+
try {
|
|
181
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
182
|
+
const input = readAgentInput(body.input);
|
|
183
|
+
const { session, runOptions } = await awaitWithSignal(createSession(exposure, authorization), owned.signal);
|
|
184
|
+
const runConfig = {
|
|
185
|
+
...runOptions,
|
|
186
|
+
ownership: authorization.ownership,
|
|
187
|
+
identity: authorization.identity,
|
|
188
|
+
metadata: { ...runOptions?.metadata, ...authorization.metadata },
|
|
189
|
+
redactor: options.redactor,
|
|
190
|
+
signal: owned.signal,
|
|
191
|
+
};
|
|
192
|
+
if (route.kind === "agent-run") {
|
|
193
|
+
const result = await awaitWithSignal(session.run(input, runConfig), owned.signal);
|
|
194
|
+
const response = respond(json(result, 200, limits, options));
|
|
195
|
+
owned.dispose();
|
|
196
|
+
release();
|
|
197
|
+
return response;
|
|
198
|
+
}
|
|
199
|
+
const events = session.stream(input, {
|
|
200
|
+
...runConfig,
|
|
201
|
+
maxQueuedEvents: limits.maxQueuedEvents,
|
|
202
|
+
overflow: "close",
|
|
203
|
+
});
|
|
204
|
+
return respond(sse(events, owned, limits, options, release));
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
owned.dispose();
|
|
208
|
+
release();
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const exposure = options.workflows?.[route.capabilityId];
|
|
213
|
+
if (!exposure)
|
|
214
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
215
|
+
if (route.kind === "workflow-enqueue") {
|
|
216
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
217
|
+
try {
|
|
218
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
219
|
+
const result = await awaitWithSignal(enqueueWorkflow(exposure.definition, body.input, {
|
|
220
|
+
checkpoints: exposure.checkpoints,
|
|
221
|
+
ownership: authorization.ownership,
|
|
222
|
+
runId: readOptionalId(body.runId, "runId"),
|
|
223
|
+
metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
|
|
224
|
+
signal: owned.signal,
|
|
225
|
+
}), owned.signal);
|
|
226
|
+
return respond(json(result, 202, limits, options));
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
owned.dispose();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (route.kind === "workflow-replay") {
|
|
233
|
+
acquire();
|
|
234
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
235
|
+
try {
|
|
236
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
237
|
+
const result = await awaitWithSignal(replayWorkflow(exposure.definition, {
|
|
238
|
+
sourceRunId: route.runId,
|
|
239
|
+
fromNodeId: readRequiredId(body.fromNodeId, "fromNodeId"),
|
|
240
|
+
runId: readOptionalId(body.runId, "runId"),
|
|
241
|
+
}, {
|
|
242
|
+
...exposure.runOptions,
|
|
243
|
+
checkpoints: exposure.checkpoints,
|
|
244
|
+
ownership: authorization.ownership,
|
|
245
|
+
metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
|
|
246
|
+
redactor: options.redactor,
|
|
247
|
+
signal: owned.signal,
|
|
248
|
+
}), owned.signal);
|
|
249
|
+
return respond(json(result, 200, limits, options));
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
owned.dispose();
|
|
253
|
+
release();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (route.kind === "workflow-status") {
|
|
257
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
258
|
+
try {
|
|
259
|
+
const record = await awaitWithSignal(getWorkflowRun(exposure.checkpoints, {
|
|
260
|
+
workflowId: exposure.definition.id,
|
|
261
|
+
runId: route.runId,
|
|
262
|
+
ownership: authorization.ownership,
|
|
263
|
+
signal: owned.signal,
|
|
264
|
+
}), owned.signal);
|
|
265
|
+
if (!record)
|
|
266
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
267
|
+
return respond(json(record, 200, limits, options));
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
owned.dispose();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (route.kind === "workflow-cancel") {
|
|
274
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
275
|
+
try {
|
|
276
|
+
const result = await awaitWithSignal(cancelWorkflowRun({
|
|
277
|
+
workflowId: exposure.definition.id,
|
|
278
|
+
runId: route.runId,
|
|
279
|
+
workflow: exposure.definition,
|
|
280
|
+
checkpoints: exposure.checkpoints,
|
|
281
|
+
ownership: authorization.ownership,
|
|
282
|
+
signal: owned.signal,
|
|
283
|
+
}), owned.signal);
|
|
284
|
+
return respond(json(result, 200, limits, options));
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
owned.dispose();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (route.kind === "workflow-resume") {
|
|
291
|
+
acquire();
|
|
292
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
293
|
+
try {
|
|
294
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
295
|
+
const result = await awaitWithSignal(resumeWorkflow(exposure.definition, {
|
|
296
|
+
workflowId: exposure.definition.id,
|
|
297
|
+
runId: route.runId,
|
|
298
|
+
}, {
|
|
299
|
+
...exposure.runOptions,
|
|
300
|
+
checkpoints: exposure.checkpoints,
|
|
301
|
+
ownership: authorization.ownership,
|
|
302
|
+
metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
|
|
303
|
+
redactor: options.redactor,
|
|
304
|
+
signal: owned.signal,
|
|
305
|
+
resume: readResume(body),
|
|
306
|
+
}), owned.signal);
|
|
307
|
+
return respond(json(result, 200, limits, options));
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
owned.dispose();
|
|
311
|
+
release();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
acquire();
|
|
315
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
316
|
+
try {
|
|
317
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
318
|
+
const runId = readOptionalId(body.runId, "runId") ?? crypto.randomUUID();
|
|
319
|
+
const workflowOptions = {
|
|
320
|
+
...exposure.runOptions,
|
|
321
|
+
checkpoints: exposure.checkpoints,
|
|
322
|
+
ownership: authorization.ownership,
|
|
323
|
+
metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
|
|
324
|
+
redactor: options.redactor,
|
|
325
|
+
signal: owned.signal,
|
|
326
|
+
runId,
|
|
327
|
+
};
|
|
328
|
+
if (route.kind === "workflow-run") {
|
|
329
|
+
const result = await awaitWithSignal(runWorkflow(exposure.definition, body.input, workflowOptions), owned.signal);
|
|
330
|
+
const response = respond(json(result, 200, limits, options));
|
|
331
|
+
owned.dispose();
|
|
332
|
+
release();
|
|
333
|
+
return response;
|
|
334
|
+
}
|
|
335
|
+
const bus = createWorkflowEventBus({
|
|
336
|
+
workflowId: exposure.definition.id,
|
|
337
|
+
runId,
|
|
338
|
+
maxQueuedEvents: limits.maxQueuedEvents,
|
|
339
|
+
overflow: "close",
|
|
340
|
+
signal: owned.signal,
|
|
341
|
+
});
|
|
342
|
+
const events = bus.subscribe();
|
|
343
|
+
void runWorkflow(exposure.definition, body.input, { ...workflowOptions, eventBus: bus })
|
|
344
|
+
.catch(() => undefined)
|
|
345
|
+
.finally(() => bus.close());
|
|
346
|
+
return respond(sse(events, owned, limits, options, release));
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
owned.dispose();
|
|
350
|
+
release();
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
return respond(errorResponse(error, limits, options));
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
function acquire() {
|
|
359
|
+
if (activeRuns >= limits.maxConcurrentRuns) {
|
|
360
|
+
throw new PrismServerError("Server is busy", 429, "ERR_PRISM_SERVER_CONCURRENCY");
|
|
361
|
+
}
|
|
362
|
+
activeRuns += 1;
|
|
363
|
+
}
|
|
364
|
+
function release() {
|
|
365
|
+
activeRuns = Math.max(0, activeRuns - 1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function assertRequestPolicy(request: Request, hosts?: readonly string[], origins?: readonly string[]): void;
|
|
2
|
+
export declare function awaitWithSignal<T>(promise: Promise<T>, signal: AbortSignal): Promise<T>;
|
|
3
|
+
export declare function ownedSignal(request: Request, timeoutMs: number, disconnectAborts: boolean): {
|
|
4
|
+
signal: AbortSignal;
|
|
5
|
+
abort: (reason?: unknown) => void;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** policy (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
|
+
export function assertRequestPolicy(request, hosts, origins) {
|
|
4
|
+
if (hosts) {
|
|
5
|
+
const host = request.headers.get("host") ?? new URL(request.url).host;
|
|
6
|
+
if (!hosts.includes(host))
|
|
7
|
+
throw new PrismServerError("Forbidden host", 403, "ERR_PRISM_SERVER_HOST");
|
|
8
|
+
}
|
|
9
|
+
const origin = request.headers.get("origin");
|
|
10
|
+
if (origin && origins && !origins.includes(origin))
|
|
11
|
+
throw new PrismServerError("Forbidden origin", 403, "ERR_PRISM_SERVER_ORIGIN");
|
|
12
|
+
}
|
|
13
|
+
export async function awaitWithSignal(promise, signal) {
|
|
14
|
+
if (signal.aborted)
|
|
15
|
+
throw new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED");
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const abort = () => reject(new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED"));
|
|
18
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
19
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function ownedSignal(request, timeoutMs, disconnectAborts) {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const abort = () => controller.abort(request.signal.reason ?? new Error("request disconnected"));
|
|
25
|
+
if (disconnectAborts) {
|
|
26
|
+
if (request.signal.aborted)
|
|
27
|
+
abort();
|
|
28
|
+
else
|
|
29
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
30
|
+
}
|
|
31
|
+
const timeout = setTimeout(() => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
32
|
+
return {
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
abort: (reason) => controller.abort(reason),
|
|
35
|
+
dispose() {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
request.signal.removeEventListener("abort", abort);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** readers (0.2.5 plan 025 Task 1 split). Moved verbatim from handler.ts; public surface unchanged behind the barrel. */
|
|
2
|
+
import type { JsonObject, Message, RunDecision } from "@arnilo/prism";
|
|
3
|
+
import type { WorkflowResumeRequest, WorkflowScheduleStatus } from "@arnilo/prism-workflows";
|
|
4
|
+
export declare function readJsonObject(request: Request, maxBytes: number, signal: AbortSignal): Promise<JsonObject>;
|
|
5
|
+
export declare function readAgentInput(value: unknown): string | Message | readonly Message[];
|
|
6
|
+
export declare function readAgentResume(body: JsonObject): {
|
|
7
|
+
readonly decision: "approve" | "deny";
|
|
8
|
+
readonly expectedVersion: number;
|
|
9
|
+
} | {
|
|
10
|
+
readonly decisions: readonly RunDecision[];
|
|
11
|
+
readonly expectedVersion: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function readResume(body: JsonObject): WorkflowResumeRequest;
|
|
14
|
+
export declare function readRequiredString(value: unknown, name: string): string;
|
|
15
|
+
export declare function readRequiredId(value: unknown, name: string): string;
|
|
16
|
+
export declare function readPositiveInteger(value: unknown, name: string): number;
|
|
17
|
+
export declare function readOptionalObject(value: unknown, name: string): JsonObject | undefined;
|
|
18
|
+
export declare function readScheduleStatus(value: string | null): WorkflowScheduleStatus | undefined;
|
|
19
|
+
export declare function readOptionalId(value: unknown, name: string): string | undefined;
|
|
20
|
+
export declare function validId(value: string): boolean;
|
|
21
|
+
export declare function replayCursor(request: Request, maxBytes: number): string | undefined;
|