@arnilo/prism-server 0.0.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/handler.d.ts +2 -0
- package/dist/handler.js +657 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/limits.d.ts +37 -0
- package/dist/limits.js +36 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +11 -0
- package/package.json +59 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.0.5] - 2026-07-16
|
|
6
|
+
|
|
7
|
+
- Added optional authorized, bounded Web-standard direct/SSE agent and durable workflow handling, including explicitly registered ownership-scoped schedules, background enqueue, and immutable-lineage replay.
|
|
8
|
+
|
|
9
|
+
## [0.0.4] - 2026-07-14
|
|
10
|
+
|
|
11
|
+
- Initial optional web-standard agent/workflow handler with explicit authorization, ownership, redaction, streaming, and resource bounds.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Prism contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @arnilo/prism-server
|
|
2
|
+
|
|
3
|
+
Optional framework-free Web `Request -> Response` exposure for selected Prism agents and workflows.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @arnilo/prism @arnilo/prism-workflows @arnilo/prism-server
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createPrismHandler } from "@arnilo/prism-server";
|
|
11
|
+
|
|
12
|
+
const handler = createPrismHandler({
|
|
13
|
+
agents: { support: agent },
|
|
14
|
+
workflows: { publish: { definition: workflow, checkpoints } },
|
|
15
|
+
authorize: async ({ request }) => validHostToken(request)
|
|
16
|
+
? { ownership: { tenantId: "tenant-1", userId: "user-1" } }
|
|
17
|
+
: false,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const response = await handler(new Request("https://api.example.test/prism/agents/support/runs", {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
body: JSON.stringify({ input: "Hello" }),
|
|
24
|
+
}));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Routes: direct/SSE agent run, direct/SSE workflow run, durable workflow enqueue/status/cancel/resume/replay, and optional ownership-scoped schedule create/list/pause/resume/trigger/delete. All bodies, responses, events, queues, concurrency, and timeouts are bounded.
|
|
28
|
+
|
|
29
|
+
Nothing is exposed by default. Authorization is required; ownership comes only from its result. No listener, framework, auth provider, user database, credential discovery, or hidden package activation ships.
|
|
30
|
+
|
|
31
|
+
Full API, route, limits, security, and deployment notes: [`docs/server.md`](../../docs/server.md).
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { cancelWorkflowRun, createWorkflowEventBus, enqueueWorkflow, getWorkflowRun, replayWorkflow, resumeWorkflow, runWorkflow, } from "@arnilo/prism-workflows";
|
|
2
|
+
import { resolvePrismServerLimits } from "./limits.js";
|
|
3
|
+
import { PrismServerError } from "./types.js";
|
|
4
|
+
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
5
|
+
const SSE_HEADERS = {
|
|
6
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
7
|
+
"cache-control": "no-cache, no-transform",
|
|
8
|
+
connection: "keep-alive",
|
|
9
|
+
};
|
|
10
|
+
export function createPrismHandler(options) {
|
|
11
|
+
const limits = resolvePrismServerLimits(options.limits);
|
|
12
|
+
const base = normalizeBasePath(options.basePath ?? "/prism");
|
|
13
|
+
let activeRuns = 0;
|
|
14
|
+
return async (request) => {
|
|
15
|
+
const origin = request.headers.get("origin");
|
|
16
|
+
const corsHeaders = origin && options.allowedOrigins?.includes(origin)
|
|
17
|
+
? { "access-control-allow-origin": origin, vary: "origin" }
|
|
18
|
+
: 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",
|
|
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 (route.kind.startsWith("schedule-")) {
|
|
42
|
+
const selectedSchedules = options.schedules;
|
|
43
|
+
if (!selectedSchedules)
|
|
44
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
45
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
46
|
+
try {
|
|
47
|
+
const schedules = typeof selectedSchedules === "function"
|
|
48
|
+
? await awaitWithSignal(Promise.resolve(selectedSchedules(authorization, owned.signal)), owned.signal)
|
|
49
|
+
: selectedSchedules;
|
|
50
|
+
if (!sameOwnership(authorization.ownership, schedules.ownership)) {
|
|
51
|
+
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
52
|
+
}
|
|
53
|
+
if (route.kind === "schedule-list") {
|
|
54
|
+
const query = new URL(request.url).searchParams;
|
|
55
|
+
const status = query.get("status");
|
|
56
|
+
const result = await awaitWithSignal(schedules.list({
|
|
57
|
+
status: readScheduleStatus(status),
|
|
58
|
+
cursor: query.get("cursor") ?? undefined,
|
|
59
|
+
limit: query.has("limit") ? readPositiveInteger(query.get("limit"), "limit") : undefined,
|
|
60
|
+
signal: owned.signal,
|
|
61
|
+
}), owned.signal);
|
|
62
|
+
return respond(json(result, 200, limits, options));
|
|
63
|
+
}
|
|
64
|
+
if (route.kind === "schedule-delete") {
|
|
65
|
+
const result = await awaitWithSignal(schedules.delete(route.capabilityId, owned.signal), owned.signal);
|
|
66
|
+
return respond(json({ deleted: result }, 200, limits, options));
|
|
67
|
+
}
|
|
68
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
69
|
+
if (route.kind === "schedule-create") {
|
|
70
|
+
const result = await awaitWithSignal(schedules.create({
|
|
71
|
+
id: route.capabilityId,
|
|
72
|
+
workflowId: readRequiredId(body.workflowId, "workflowId"),
|
|
73
|
+
nextRunAt: readRequiredString(body.nextRunAt, "nextRunAt"),
|
|
74
|
+
input: body.input,
|
|
75
|
+
intervalMs: body.intervalMs === undefined ? undefined : readPositiveInteger(body.intervalMs, "intervalMs"),
|
|
76
|
+
calculatorId: readOptionalId(body.calculatorId, "calculatorId"),
|
|
77
|
+
paused: body.paused === true,
|
|
78
|
+
metadata: readOptionalObject(body.metadata, "metadata"),
|
|
79
|
+
}, owned.signal), owned.signal);
|
|
80
|
+
return respond(json(result, 201, limits, options));
|
|
81
|
+
}
|
|
82
|
+
if (route.kind === "schedule-pause") {
|
|
83
|
+
return respond(json(await awaitWithSignal(schedules.pause(route.capabilityId, owned.signal), owned.signal), 200, limits, options));
|
|
84
|
+
}
|
|
85
|
+
if (route.kind === "schedule-resume") {
|
|
86
|
+
const nextRunAt = body.nextRunAt === undefined ? undefined : readRequiredString(body.nextRunAt, "nextRunAt");
|
|
87
|
+
return respond(json(await awaitWithSignal(schedules.resume(route.capabilityId, nextRunAt, owned.signal), owned.signal), 200, limits, options));
|
|
88
|
+
}
|
|
89
|
+
const idempotencyKey = readRequiredId(body.idempotencyKey, "idempotencyKey");
|
|
90
|
+
return respond(json(await awaitWithSignal(schedules.trigger(route.capabilityId, { idempotencyKey, signal: owned.signal }), owned.signal), 200, limits, options));
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
owned.dispose();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (route.kind === "agent-run" || route.kind === "agent-stream") {
|
|
97
|
+
const exposure = options.agents?.[route.capabilityId];
|
|
98
|
+
if (!exposure)
|
|
99
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
100
|
+
acquire();
|
|
101
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
102
|
+
try {
|
|
103
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
104
|
+
const input = readAgentInput(body.input);
|
|
105
|
+
const { session, runOptions } = await awaitWithSignal(createSession(exposure, authorization), owned.signal);
|
|
106
|
+
const runConfig = {
|
|
107
|
+
...runOptions,
|
|
108
|
+
ownership: authorization.ownership,
|
|
109
|
+
metadata: { ...runOptions?.metadata, ...authorization.metadata },
|
|
110
|
+
redactor: options.redactor,
|
|
111
|
+
signal: owned.signal,
|
|
112
|
+
};
|
|
113
|
+
if (route.kind === "agent-run") {
|
|
114
|
+
const result = await awaitWithSignal(session.run(input, runConfig), owned.signal);
|
|
115
|
+
const response = respond(json(result, 200, limits, options));
|
|
116
|
+
owned.dispose();
|
|
117
|
+
release();
|
|
118
|
+
return response;
|
|
119
|
+
}
|
|
120
|
+
const events = session.stream(input, {
|
|
121
|
+
...runConfig,
|
|
122
|
+
maxQueuedEvents: limits.maxQueuedEvents,
|
|
123
|
+
overflow: "close",
|
|
124
|
+
});
|
|
125
|
+
return respond(sse(events, owned, limits, options, release));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
owned.dispose();
|
|
129
|
+
release();
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const exposure = options.workflows?.[route.capabilityId];
|
|
134
|
+
if (!exposure)
|
|
135
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
136
|
+
if (route.kind === "workflow-enqueue") {
|
|
137
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
138
|
+
try {
|
|
139
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
140
|
+
const result = await awaitWithSignal(enqueueWorkflow(exposure.definition, body.input, {
|
|
141
|
+
checkpoints: exposure.checkpoints,
|
|
142
|
+
ownership: authorization.ownership,
|
|
143
|
+
runId: readOptionalId(body.runId, "runId"),
|
|
144
|
+
metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
|
|
145
|
+
signal: owned.signal,
|
|
146
|
+
}), owned.signal);
|
|
147
|
+
return respond(json(result, 202, limits, options));
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
owned.dispose();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (route.kind === "workflow-replay") {
|
|
154
|
+
acquire();
|
|
155
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
156
|
+
try {
|
|
157
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
158
|
+
const result = await awaitWithSignal(replayWorkflow(exposure.definition, {
|
|
159
|
+
sourceRunId: route.runId,
|
|
160
|
+
fromNodeId: readRequiredId(body.fromNodeId, "fromNodeId"),
|
|
161
|
+
runId: readOptionalId(body.runId, "runId"),
|
|
162
|
+
}, {
|
|
163
|
+
...exposure.runOptions,
|
|
164
|
+
checkpoints: exposure.checkpoints,
|
|
165
|
+
ownership: authorization.ownership,
|
|
166
|
+
metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
|
|
167
|
+
redactor: options.redactor,
|
|
168
|
+
signal: owned.signal,
|
|
169
|
+
}), owned.signal);
|
|
170
|
+
return respond(json(result, 200, limits, options));
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
owned.dispose();
|
|
174
|
+
release();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (route.kind === "workflow-status") {
|
|
178
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
179
|
+
try {
|
|
180
|
+
const record = await awaitWithSignal(getWorkflowRun(exposure.checkpoints, {
|
|
181
|
+
workflowId: exposure.definition.id,
|
|
182
|
+
runId: route.runId,
|
|
183
|
+
ownership: authorization.ownership,
|
|
184
|
+
signal: owned.signal,
|
|
185
|
+
}), owned.signal);
|
|
186
|
+
if (!record)
|
|
187
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
188
|
+
return respond(json(record, 200, limits, options));
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
owned.dispose();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (route.kind === "workflow-cancel") {
|
|
195
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
196
|
+
try {
|
|
197
|
+
const result = await awaitWithSignal(cancelWorkflowRun({
|
|
198
|
+
workflowId: exposure.definition.id,
|
|
199
|
+
runId: route.runId,
|
|
200
|
+
checkpoints: exposure.checkpoints,
|
|
201
|
+
ownership: authorization.ownership,
|
|
202
|
+
signal: owned.signal,
|
|
203
|
+
}), owned.signal);
|
|
204
|
+
return respond(json(result, 200, limits, options));
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
owned.dispose();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (route.kind === "workflow-resume") {
|
|
211
|
+
acquire();
|
|
212
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
213
|
+
try {
|
|
214
|
+
const body = await readJsonObject(request, limits.maxRequestBytes, owned.signal);
|
|
215
|
+
const result = await awaitWithSignal(resumeWorkflow(exposure.definition, {
|
|
216
|
+
workflowId: exposure.definition.id,
|
|
217
|
+
runId: route.runId,
|
|
218
|
+
}, {
|
|
219
|
+
...exposure.runOptions,
|
|
220
|
+
checkpoints: exposure.checkpoints,
|
|
221
|
+
ownership: authorization.ownership,
|
|
222
|
+
metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
|
|
223
|
+
redactor: options.redactor,
|
|
224
|
+
signal: owned.signal,
|
|
225
|
+
resume: readResume(body),
|
|
226
|
+
}), owned.signal);
|
|
227
|
+
return respond(json(result, 200, limits, options));
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
owned.dispose();
|
|
231
|
+
release();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
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 runId = readOptionalId(body.runId, "runId") ?? crypto.randomUUID();
|
|
239
|
+
const workflowOptions = {
|
|
240
|
+
...exposure.runOptions,
|
|
241
|
+
checkpoints: exposure.checkpoints,
|
|
242
|
+
ownership: authorization.ownership,
|
|
243
|
+
metadata: { ...exposure.runOptions?.metadata, ...authorization.metadata },
|
|
244
|
+
redactor: options.redactor,
|
|
245
|
+
signal: owned.signal,
|
|
246
|
+
runId,
|
|
247
|
+
};
|
|
248
|
+
if (route.kind === "workflow-run") {
|
|
249
|
+
const result = await awaitWithSignal(runWorkflow(exposure.definition, body.input, workflowOptions), owned.signal);
|
|
250
|
+
const response = respond(json(result, 200, limits, options));
|
|
251
|
+
owned.dispose();
|
|
252
|
+
release();
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
const bus = createWorkflowEventBus({
|
|
256
|
+
workflowId: exposure.definition.id,
|
|
257
|
+
runId,
|
|
258
|
+
maxQueuedEvents: limits.maxQueuedEvents,
|
|
259
|
+
overflow: "close",
|
|
260
|
+
signal: owned.signal,
|
|
261
|
+
});
|
|
262
|
+
const events = bus.subscribe();
|
|
263
|
+
void runWorkflow(exposure.definition, body.input, { ...workflowOptions, eventBus: bus })
|
|
264
|
+
.catch(() => undefined)
|
|
265
|
+
.finally(() => bus.close());
|
|
266
|
+
return respond(sse(events, owned, limits, options, release));
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
owned.dispose();
|
|
270
|
+
release();
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return respond(errorResponse(error, limits, options));
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
function acquire() {
|
|
279
|
+
if (activeRuns >= limits.maxConcurrentRuns) {
|
|
280
|
+
throw new PrismServerError("Server is busy", 429, "ERR_PRISM_SERVER_CONCURRENCY");
|
|
281
|
+
}
|
|
282
|
+
activeRuns += 1;
|
|
283
|
+
}
|
|
284
|
+
function release() {
|
|
285
|
+
activeRuns = Math.max(0, activeRuns - 1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function parseRoute(request, base) {
|
|
289
|
+
const pathname = new URL(request.url).pathname;
|
|
290
|
+
if (pathname !== base && !pathname.startsWith(`${base}/`))
|
|
291
|
+
return undefined;
|
|
292
|
+
let parts;
|
|
293
|
+
try {
|
|
294
|
+
parts = pathname.slice(base.length).split("/").filter(Boolean).map(decodeURIComponent);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
throw new PrismServerError("Invalid route", 400, "ERR_PRISM_SERVER_ROUTE");
|
|
298
|
+
}
|
|
299
|
+
const [group, id, segment, runId, action] = parts;
|
|
300
|
+
if (group === "schedules" && parts.length === 1 && request.method === "GET") {
|
|
301
|
+
return { kind: "schedule-list", operation: "schedule.list", capabilityId: "*" };
|
|
302
|
+
}
|
|
303
|
+
if (!id || !validId(id))
|
|
304
|
+
return undefined;
|
|
305
|
+
if (group === "schedules") {
|
|
306
|
+
if (parts.length === 2 && request.method === "POST")
|
|
307
|
+
return { kind: "schedule-create", operation: "schedule.create", capabilityId: id };
|
|
308
|
+
if (parts.length === 2 && request.method === "DELETE")
|
|
309
|
+
return { kind: "schedule-delete", operation: "schedule.delete", capabilityId: id };
|
|
310
|
+
if (parts.length === 3 && segment === "pause" && request.method === "POST")
|
|
311
|
+
return { kind: "schedule-pause", operation: "schedule.pause", capabilityId: id };
|
|
312
|
+
if (parts.length === 3 && segment === "resume" && request.method === "POST")
|
|
313
|
+
return { kind: "schedule-resume", operation: "schedule.resume", capabilityId: id };
|
|
314
|
+
if (parts.length === 3 && segment === "trigger" && request.method === "POST")
|
|
315
|
+
return { kind: "schedule-trigger", operation: "schedule.trigger", capabilityId: id };
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
if (group === "agents" && segment === "runs" && parts.length === 3 && request.method === "POST") {
|
|
319
|
+
return { kind: "agent-run", operation: "agent.run", capabilityId: id };
|
|
320
|
+
}
|
|
321
|
+
if (group === "agents" && segment === "stream" && parts.length === 3 && request.method === "POST") {
|
|
322
|
+
return { kind: "agent-stream", operation: "agent.stream", capabilityId: id };
|
|
323
|
+
}
|
|
324
|
+
if (group !== "workflows")
|
|
325
|
+
return undefined;
|
|
326
|
+
if (segment === "runs" && parts.length === 3 && request.method === "POST") {
|
|
327
|
+
return { kind: "workflow-run", operation: "workflow.run", capabilityId: id };
|
|
328
|
+
}
|
|
329
|
+
if (segment === "stream" && parts.length === 3 && request.method === "POST") {
|
|
330
|
+
return { kind: "workflow-stream", operation: "workflow.stream", capabilityId: id };
|
|
331
|
+
}
|
|
332
|
+
if (segment === "enqueue" && parts.length === 3 && request.method === "POST") {
|
|
333
|
+
return { kind: "workflow-enqueue", operation: "workflow.enqueue", capabilityId: id };
|
|
334
|
+
}
|
|
335
|
+
if (segment !== "runs" || !runId || !validId(runId))
|
|
336
|
+
return undefined;
|
|
337
|
+
if (parts.length === 4 && request.method === "GET") {
|
|
338
|
+
return { kind: "workflow-status", operation: "workflow.status", capabilityId: id, runId };
|
|
339
|
+
}
|
|
340
|
+
if (parts.length === 4 && request.method === "DELETE") {
|
|
341
|
+
return { kind: "workflow-cancel", operation: "workflow.cancel", capabilityId: id, runId };
|
|
342
|
+
}
|
|
343
|
+
if (parts.length === 5 && action === "resume" && request.method === "POST") {
|
|
344
|
+
return { kind: "workflow-resume", operation: "workflow.resume", capabilityId: id, runId };
|
|
345
|
+
}
|
|
346
|
+
if (parts.length === 5 && action === "replay" && request.method === "POST") {
|
|
347
|
+
return { kind: "workflow-replay", operation: "workflow.replay", capabilityId: id, runId };
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
async function authorize(options, request, operation, capabilityId, timeoutMs) {
|
|
352
|
+
let result;
|
|
353
|
+
let timeout;
|
|
354
|
+
const controller = new AbortController();
|
|
355
|
+
const abort = () => controller.abort(request.signal.reason);
|
|
356
|
+
if (request.signal.aborted)
|
|
357
|
+
abort();
|
|
358
|
+
else
|
|
359
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
360
|
+
try {
|
|
361
|
+
result = await Promise.race([
|
|
362
|
+
options.authorize({ request, operation, capabilityId, signal: controller.signal }),
|
|
363
|
+
new Promise((resolve) => {
|
|
364
|
+
timeout = setTimeout(() => {
|
|
365
|
+
controller.abort(new Error("authorization timed out"));
|
|
366
|
+
resolve(false);
|
|
367
|
+
}, timeoutMs);
|
|
368
|
+
}),
|
|
369
|
+
]);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
if (timeout)
|
|
376
|
+
clearTimeout(timeout);
|
|
377
|
+
request.signal.removeEventListener("abort", abort);
|
|
378
|
+
}
|
|
379
|
+
if (!result || !hasOwnership(result.ownership))
|
|
380
|
+
return false;
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
function hasOwnership(value) {
|
|
384
|
+
return [value.tenantId, value.accountId, value.userId].some((item) => typeof item === "string" && item.length > 0);
|
|
385
|
+
}
|
|
386
|
+
async function createSession(exposure, authorization) {
|
|
387
|
+
if ("sessionFactory" in exposure) {
|
|
388
|
+
return { session: await exposure.sessionFactory(authorization), runOptions: exposure.runOptions };
|
|
389
|
+
}
|
|
390
|
+
return { session: exposure.createSession() };
|
|
391
|
+
}
|
|
392
|
+
async function readJsonObject(request, maxBytes, signal) {
|
|
393
|
+
const type = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
394
|
+
if (type !== "application/json")
|
|
395
|
+
throw new PrismServerError("Content-Type must be application/json", 415, "ERR_PRISM_SERVER_CONTENT_TYPE");
|
|
396
|
+
const declared = Number(request.headers.get("content-length"));
|
|
397
|
+
if (Number.isFinite(declared) && declared > maxBytes)
|
|
398
|
+
throw new PrismServerError("Request body too large", 413, "ERR_PRISM_SERVER_BODY_LIMIT");
|
|
399
|
+
const reader = request.body?.getReader();
|
|
400
|
+
if (!reader)
|
|
401
|
+
throw new PrismServerError("JSON body is required", 400, "ERR_PRISM_SERVER_BODY");
|
|
402
|
+
const chunks = [];
|
|
403
|
+
let size = 0;
|
|
404
|
+
const abort = () => { void reader.cancel(signal.reason); };
|
|
405
|
+
if (signal.aborted)
|
|
406
|
+
abort();
|
|
407
|
+
else
|
|
408
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
409
|
+
try {
|
|
410
|
+
while (true) {
|
|
411
|
+
const next = await reader.read();
|
|
412
|
+
if (next.done)
|
|
413
|
+
break;
|
|
414
|
+
size += next.value.byteLength;
|
|
415
|
+
if (size > maxBytes) {
|
|
416
|
+
await reader.cancel();
|
|
417
|
+
throw new PrismServerError("Request body too large", 413, "ERR_PRISM_SERVER_BODY_LIMIT");
|
|
418
|
+
}
|
|
419
|
+
chunks.push(next.value);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
signal.removeEventListener("abort", abort);
|
|
424
|
+
reader.releaseLock();
|
|
425
|
+
}
|
|
426
|
+
if (signal.aborted)
|
|
427
|
+
throw new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED");
|
|
428
|
+
const bytes = new Uint8Array(size);
|
|
429
|
+
let offset = 0;
|
|
430
|
+
for (const chunk of chunks) {
|
|
431
|
+
bytes.set(chunk, offset);
|
|
432
|
+
offset += chunk.byteLength;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
436
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
437
|
+
throw new Error("object required");
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (error instanceof PrismServerError)
|
|
442
|
+
throw error;
|
|
443
|
+
throw new PrismServerError("Invalid JSON object body", 400, "ERR_PRISM_SERVER_BODY");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function readAgentInput(value) {
|
|
447
|
+
if (typeof value === "string")
|
|
448
|
+
return value;
|
|
449
|
+
if (isMessage(value))
|
|
450
|
+
return value;
|
|
451
|
+
if (Array.isArray(value) && value.length > 0 && value.every(isMessage))
|
|
452
|
+
return value;
|
|
453
|
+
throw new PrismServerError("input must be a string, message, or non-empty message array", 400, "ERR_PRISM_SERVER_INPUT");
|
|
454
|
+
}
|
|
455
|
+
function isMessage(value) {
|
|
456
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
457
|
+
return false;
|
|
458
|
+
const item = value;
|
|
459
|
+
return ["system", "user", "assistant", "tool"].includes(String(item.role)) && Array.isArray(item.content);
|
|
460
|
+
}
|
|
461
|
+
function readResume(body) {
|
|
462
|
+
if (body.decision !== "approve" && body.decision !== "deny") {
|
|
463
|
+
throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
|
|
464
|
+
}
|
|
465
|
+
if (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1) {
|
|
466
|
+
throw new PrismServerError("expectedVersion must be a positive safe integer", 400, "ERR_PRISM_SERVER_RESUME");
|
|
467
|
+
}
|
|
468
|
+
return { decision: body.decision, input: body.input, expectedVersion: Number(body.expectedVersion) };
|
|
469
|
+
}
|
|
470
|
+
function readRequiredString(value, name) {
|
|
471
|
+
if (typeof value !== "string" || value.length === 0)
|
|
472
|
+
throw new PrismServerError(`${name} is required`, 400, "ERR_PRISM_SERVER_INPUT");
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
function readRequiredId(value, name) {
|
|
476
|
+
const result = readOptionalId(value, name);
|
|
477
|
+
if (!result)
|
|
478
|
+
throw new PrismServerError(`${name} is required`, 400, "ERR_PRISM_SERVER_ID");
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
function readPositiveInteger(value, name) {
|
|
482
|
+
const number = typeof value === "string" ? Number(value) : value;
|
|
483
|
+
if (!Number.isSafeInteger(number) || Number(number) < 1)
|
|
484
|
+
throw new PrismServerError(`${name} must be a positive safe integer`, 400, "ERR_PRISM_SERVER_INPUT");
|
|
485
|
+
return Number(number);
|
|
486
|
+
}
|
|
487
|
+
function readOptionalObject(value, name) {
|
|
488
|
+
if (value === undefined)
|
|
489
|
+
return undefined;
|
|
490
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
491
|
+
throw new PrismServerError(`${name} must be an object`, 400, "ERR_PRISM_SERVER_INPUT");
|
|
492
|
+
return value;
|
|
493
|
+
}
|
|
494
|
+
function readScheduleStatus(value) {
|
|
495
|
+
if (value === null)
|
|
496
|
+
return undefined;
|
|
497
|
+
if (value === "active" || value === "paused" || value === "completed")
|
|
498
|
+
return value;
|
|
499
|
+
throw new PrismServerError("status is invalid", 400, "ERR_PRISM_SERVER_INPUT");
|
|
500
|
+
}
|
|
501
|
+
function sameOwnership(left, right) {
|
|
502
|
+
return left.tenantId === right.tenantId && left.accountId === right.accountId && left.userId === right.userId;
|
|
503
|
+
}
|
|
504
|
+
function readOptionalId(value, name) {
|
|
505
|
+
if (value === undefined)
|
|
506
|
+
return undefined;
|
|
507
|
+
if (typeof value !== "string" || !validId(value))
|
|
508
|
+
throw new PrismServerError(`${name} is invalid`, 400, "ERR_PRISM_SERVER_ID");
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
function validId(value) {
|
|
512
|
+
return value.length <= 128 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value);
|
|
513
|
+
}
|
|
514
|
+
function normalizeBasePath(value) {
|
|
515
|
+
if (!value.startsWith("/") || value.includes("?") || value.includes("#"))
|
|
516
|
+
throw new RangeError("basePath must be an absolute URL path");
|
|
517
|
+
const normalized = value.length > 1 ? value.replace(/\/+$/, "") : value;
|
|
518
|
+
if (normalized === "/")
|
|
519
|
+
throw new RangeError("basePath cannot expose the URL root");
|
|
520
|
+
return normalized;
|
|
521
|
+
}
|
|
522
|
+
function assertRequestPolicy(request, hosts, origins) {
|
|
523
|
+
if (hosts) {
|
|
524
|
+
const host = request.headers.get("host") ?? new URL(request.url).host;
|
|
525
|
+
if (!hosts.includes(host))
|
|
526
|
+
throw new PrismServerError("Forbidden host", 403, "ERR_PRISM_SERVER_HOST");
|
|
527
|
+
}
|
|
528
|
+
const origin = request.headers.get("origin");
|
|
529
|
+
if (origin && origins && !origins.includes(origin))
|
|
530
|
+
throw new PrismServerError("Forbidden origin", 403, "ERR_PRISM_SERVER_ORIGIN");
|
|
531
|
+
}
|
|
532
|
+
async function awaitWithSignal(promise, signal) {
|
|
533
|
+
if (signal.aborted)
|
|
534
|
+
throw new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED");
|
|
535
|
+
return new Promise((resolve, reject) => {
|
|
536
|
+
const abort = () => reject(new PrismServerError("Request timed out or disconnected", 408, "ERR_PRISM_SERVER_ABORTED"));
|
|
537
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
538
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function ownedSignal(request, timeoutMs, disconnectAborts) {
|
|
542
|
+
const controller = new AbortController();
|
|
543
|
+
const abort = () => controller.abort(request.signal.reason ?? new Error("request disconnected"));
|
|
544
|
+
if (disconnectAborts) {
|
|
545
|
+
if (request.signal.aborted)
|
|
546
|
+
abort();
|
|
547
|
+
else
|
|
548
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
549
|
+
}
|
|
550
|
+
const timeout = setTimeout(() => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
551
|
+
return {
|
|
552
|
+
signal: controller.signal,
|
|
553
|
+
abort: (reason) => controller.abort(reason),
|
|
554
|
+
dispose() {
|
|
555
|
+
clearTimeout(timeout);
|
|
556
|
+
request.signal.removeEventListener("abort", abort);
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function sse(source, owned, limits, options, release) {
|
|
561
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
562
|
+
const encoder = new TextEncoder();
|
|
563
|
+
let events = 0;
|
|
564
|
+
let bytes = 0;
|
|
565
|
+
let finished = false;
|
|
566
|
+
const onAbort = () => { void finish(owned.signal.reason); };
|
|
567
|
+
const finish = async (reason) => {
|
|
568
|
+
if (finished)
|
|
569
|
+
return;
|
|
570
|
+
finished = true;
|
|
571
|
+
owned.signal.removeEventListener("abort", onAbort);
|
|
572
|
+
owned.abort(reason);
|
|
573
|
+
owned.dispose();
|
|
574
|
+
release();
|
|
575
|
+
await iterator.return?.();
|
|
576
|
+
};
|
|
577
|
+
owned.signal.addEventListener("abort", onAbort, { once: true });
|
|
578
|
+
const stream = new ReadableStream({
|
|
579
|
+
async pull(controller) {
|
|
580
|
+
try {
|
|
581
|
+
const next = await iterator.next();
|
|
582
|
+
if (next.done) {
|
|
583
|
+
await finish();
|
|
584
|
+
controller.close();
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const safe = options.redactor?.redact(next.value) ?? next.value;
|
|
588
|
+
const chunk = encoder.encode(`data: ${JSON.stringify(safe)}\n\n`);
|
|
589
|
+
events += 1;
|
|
590
|
+
bytes += chunk.byteLength;
|
|
591
|
+
if (chunk.byteLength > limits.maxEventBytes || events > limits.maxStreamEvents || bytes > limits.maxStreamBytes) {
|
|
592
|
+
const error = encoder.encode('data: {"type":"error","error":{"code":"ERR_PRISM_SERVER_STREAM_LIMIT","message":"stream limit exceeded"}}\n\n');
|
|
593
|
+
if (error.byteLength <= limits.maxEventBytes)
|
|
594
|
+
controller.enqueue(error);
|
|
595
|
+
await finish(new Error("stream limit exceeded"));
|
|
596
|
+
controller.close();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
controller.enqueue(chunk);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
const error = encoder.encode('data: {"type":"error","error":{"code":"ERR_PRISM_SERVER_STREAM","message":"stream failed"}}\n\n');
|
|
603
|
+
if (error.byteLength <= limits.maxEventBytes)
|
|
604
|
+
controller.enqueue(error);
|
|
605
|
+
await finish(new Error("stream failed"));
|
|
606
|
+
controller.close();
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
cancel(reason) {
|
|
610
|
+
return finish(reason);
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
return new Response(stream, { status: 200, headers: SSE_HEADERS });
|
|
614
|
+
}
|
|
615
|
+
function json(value, status, limits, options) {
|
|
616
|
+
const safe = options.redactor?.redact(value) ?? value;
|
|
617
|
+
const text = JSON.stringify(safe);
|
|
618
|
+
if (text === undefined || new TextEncoder().encode(text).byteLength > limits.maxResponseBytes) {
|
|
619
|
+
throw new PrismServerError("Response too large", 507, "ERR_PRISM_SERVER_RESPONSE_LIMIT");
|
|
620
|
+
}
|
|
621
|
+
return new Response(text, { status, headers: JSON_HEADERS });
|
|
622
|
+
}
|
|
623
|
+
function errorResponse(error, limits, options) {
|
|
624
|
+
const workflowCode = error && typeof error === "object" && "code" in error && typeof error.code === "string"
|
|
625
|
+
? error.code
|
|
626
|
+
: undefined;
|
|
627
|
+
const mapped = workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE_BUSY"
|
|
628
|
+
? { status: 409, code: workflowCode, message: "Schedule is busy" }
|
|
629
|
+
: workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE"
|
|
630
|
+
? { status: 400, code: workflowCode, message: error instanceof Error ? error.message : "Invalid schedule" }
|
|
631
|
+
: workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE_OWNERSHIP"
|
|
632
|
+
? { status: 403, code: workflowCode, message: "Forbidden" }
|
|
633
|
+
: workflowCode === "ERR_PRISM_WORKFLOW_NOT_FOUND"
|
|
634
|
+
? { status: 404, code: workflowCode, message: "Not found" }
|
|
635
|
+
: workflowCode === "ERR_PRISM_WORKFLOW_CHECKPOINT"
|
|
636
|
+
? { status: 409, code: workflowCode, message: "Workflow checkpoint operation rejected" }
|
|
637
|
+
: undefined;
|
|
638
|
+
const known = error instanceof PrismServerError;
|
|
639
|
+
const status = mapped?.status ?? (known ? error.status : error instanceof DOMException && error.name === "AbortError" ? 499 : 500);
|
|
640
|
+
const code = mapped?.code ?? (known ? error.code : status === 499 ? "ERR_PRISM_SERVER_ABORTED" : "ERR_PRISM_SERVER_INTERNAL");
|
|
641
|
+
const message = mapped?.message ?? (known ? error.message : status === 499 ? "Request aborted" : "Internal server error");
|
|
642
|
+
try {
|
|
643
|
+
return json({ error: { code, message } }, status, limits, options);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
return new Response(null, { status });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function addHeaders(response, extra) {
|
|
650
|
+
if (!extra)
|
|
651
|
+
return response;
|
|
652
|
+
const headers = new Headers(response.headers);
|
|
653
|
+
for (const [name, value] of Object.entries(extra))
|
|
654
|
+
headers.set(name, value);
|
|
655
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
656
|
+
}
|
|
657
|
+
//# sourceMappingURL=handler.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createPrismHandler } from "./handler.js";
|
|
2
|
+
export { DEFAULT_MAX_REQUEST_BYTES, HARD_MAX_REQUEST_BYTES, DEFAULT_MAX_RESPONSE_BYTES, HARD_MAX_RESPONSE_BYTES, DEFAULT_MAX_EVENT_BYTES, HARD_MAX_EVENT_BYTES, DEFAULT_MAX_STREAM_BYTES, HARD_MAX_STREAM_BYTES, DEFAULT_MAX_STREAM_EVENTS, HARD_MAX_STREAM_EVENTS, DEFAULT_MAX_CONCURRENT_RUNS, HARD_MAX_CONCURRENT_RUNS, DEFAULT_MAX_QUEUED_EVENTS, HARD_MAX_QUEUED_EVENTS, DEFAULT_REQUEST_TIMEOUT_MS, HARD_REQUEST_TIMEOUT_MS, resolvePrismServerLimits, } from "./limits.js";
|
|
3
|
+
export type { PrismServerLimits, ResolvedPrismServerLimits, } from "./limits.js";
|
|
4
|
+
export type { PrismServerOperation, PrismServerAuthorization, PrismServerAuthorizationInput, PrismServerAuthorizer, PrismAgentExposure, PrismWorkflowExposure, PrismScheduleExposure, CreatePrismHandlerOptions, PrismRequestHandler, } from "./types.js";
|
|
5
|
+
export { PrismServerError } from "./types.js";
|
|
6
|
+
export declare const packageName = "@arnilo/prism-server";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createPrismHandler } from "./handler.js";
|
|
2
|
+
export { DEFAULT_MAX_REQUEST_BYTES, HARD_MAX_REQUEST_BYTES, DEFAULT_MAX_RESPONSE_BYTES, HARD_MAX_RESPONSE_BYTES, DEFAULT_MAX_EVENT_BYTES, HARD_MAX_EVENT_BYTES, DEFAULT_MAX_STREAM_BYTES, HARD_MAX_STREAM_BYTES, DEFAULT_MAX_STREAM_EVENTS, HARD_MAX_STREAM_EVENTS, DEFAULT_MAX_CONCURRENT_RUNS, HARD_MAX_CONCURRENT_RUNS, DEFAULT_MAX_QUEUED_EVENTS, HARD_MAX_QUEUED_EVENTS, DEFAULT_REQUEST_TIMEOUT_MS, HARD_REQUEST_TIMEOUT_MS, resolvePrismServerLimits, } from "./limits.js";
|
|
3
|
+
export { PrismServerError } from "./types.js";
|
|
4
|
+
export const packageName = "@arnilo/prism-server";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const DEFAULT_MAX_REQUEST_BYTES: number;
|
|
2
|
+
export declare const HARD_MAX_REQUEST_BYTES: number;
|
|
3
|
+
export declare const DEFAULT_MAX_RESPONSE_BYTES: number;
|
|
4
|
+
export declare const HARD_MAX_RESPONSE_BYTES: number;
|
|
5
|
+
export declare const DEFAULT_MAX_EVENT_BYTES: number;
|
|
6
|
+
export declare const HARD_MAX_EVENT_BYTES: number;
|
|
7
|
+
export declare const DEFAULT_MAX_STREAM_BYTES: number;
|
|
8
|
+
export declare const HARD_MAX_STREAM_BYTES: number;
|
|
9
|
+
export declare const DEFAULT_MAX_STREAM_EVENTS = 10000;
|
|
10
|
+
export declare const HARD_MAX_STREAM_EVENTS = 100000;
|
|
11
|
+
export declare const DEFAULT_MAX_CONCURRENT_RUNS = 16;
|
|
12
|
+
export declare const HARD_MAX_CONCURRENT_RUNS = 256;
|
|
13
|
+
export declare const DEFAULT_MAX_QUEUED_EVENTS = 128;
|
|
14
|
+
export declare const HARD_MAX_QUEUED_EVENTS = 4096;
|
|
15
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
|
|
16
|
+
export declare const HARD_REQUEST_TIMEOUT_MS: number;
|
|
17
|
+
export interface PrismServerLimits {
|
|
18
|
+
readonly maxRequestBytes?: number;
|
|
19
|
+
readonly maxResponseBytes?: number;
|
|
20
|
+
readonly maxEventBytes?: number;
|
|
21
|
+
readonly maxStreamBytes?: number;
|
|
22
|
+
readonly maxStreamEvents?: number;
|
|
23
|
+
readonly maxConcurrentRuns?: number;
|
|
24
|
+
readonly maxQueuedEvents?: number;
|
|
25
|
+
readonly requestTimeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ResolvedPrismServerLimits {
|
|
28
|
+
readonly maxRequestBytes: number;
|
|
29
|
+
readonly maxResponseBytes: number;
|
|
30
|
+
readonly maxEventBytes: number;
|
|
31
|
+
readonly maxStreamBytes: number;
|
|
32
|
+
readonly maxStreamEvents: number;
|
|
33
|
+
readonly maxConcurrentRuns: number;
|
|
34
|
+
readonly maxQueuedEvents: number;
|
|
35
|
+
readonly requestTimeoutMs: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function resolvePrismServerLimits(input?: PrismServerLimits): ResolvedPrismServerLimits;
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const DEFAULT_MAX_REQUEST_BYTES = 64 * 1024;
|
|
2
|
+
export const HARD_MAX_REQUEST_BYTES = 1024 * 1024;
|
|
3
|
+
export const DEFAULT_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
4
|
+
export const HARD_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
export const DEFAULT_MAX_EVENT_BYTES = 64 * 1024;
|
|
6
|
+
export const HARD_MAX_EVENT_BYTES = 1024 * 1024;
|
|
7
|
+
export const DEFAULT_MAX_STREAM_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
export const HARD_MAX_STREAM_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
export const DEFAULT_MAX_STREAM_EVENTS = 10_000;
|
|
10
|
+
export const HARD_MAX_STREAM_EVENTS = 100_000;
|
|
11
|
+
export const DEFAULT_MAX_CONCURRENT_RUNS = 16;
|
|
12
|
+
export const HARD_MAX_CONCURRENT_RUNS = 256;
|
|
13
|
+
export const DEFAULT_MAX_QUEUED_EVENTS = 128;
|
|
14
|
+
export const HARD_MAX_QUEUED_EVENTS = 4096;
|
|
15
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
16
|
+
export const HARD_REQUEST_TIMEOUT_MS = 30 * 60_000;
|
|
17
|
+
export function resolvePrismServerLimits(input = {}) {
|
|
18
|
+
return {
|
|
19
|
+
maxRequestBytes: bounded(input.maxRequestBytes, DEFAULT_MAX_REQUEST_BYTES, HARD_MAX_REQUEST_BYTES, "maxRequestBytes"),
|
|
20
|
+
maxResponseBytes: bounded(input.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES, HARD_MAX_RESPONSE_BYTES, "maxResponseBytes"),
|
|
21
|
+
maxEventBytes: bounded(input.maxEventBytes, DEFAULT_MAX_EVENT_BYTES, HARD_MAX_EVENT_BYTES, "maxEventBytes"),
|
|
22
|
+
maxStreamBytes: bounded(input.maxStreamBytes, DEFAULT_MAX_STREAM_BYTES, HARD_MAX_STREAM_BYTES, "maxStreamBytes"),
|
|
23
|
+
maxStreamEvents: bounded(input.maxStreamEvents, DEFAULT_MAX_STREAM_EVENTS, HARD_MAX_STREAM_EVENTS, "maxStreamEvents"),
|
|
24
|
+
maxConcurrentRuns: bounded(input.maxConcurrentRuns, DEFAULT_MAX_CONCURRENT_RUNS, HARD_MAX_CONCURRENT_RUNS, "maxConcurrentRuns"),
|
|
25
|
+
maxQueuedEvents: bounded(input.maxQueuedEvents, DEFAULT_MAX_QUEUED_EVENTS, HARD_MAX_QUEUED_EVENTS, "maxQueuedEvents"),
|
|
26
|
+
requestTimeoutMs: bounded(input.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, HARD_REQUEST_TIMEOUT_MS, "requestTimeoutMs"),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function bounded(value, fallback, cap, name) {
|
|
30
|
+
const resolved = value ?? fallback;
|
|
31
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > cap) {
|
|
32
|
+
throw new RangeError(`${name} must be a positive safe integer <= ${cap}`);
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=limits.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Agent, AgentSession, OwnershipScope, RunOptions, SecretRedactor } from "@arnilo/prism";
|
|
2
|
+
import type { RunWorkflowOptions, WorkflowCheckpointAdapter, WorkflowDefinition, WorkflowSchedules } from "@arnilo/prism-workflows";
|
|
3
|
+
import type { PrismServerLimits } from "./limits.js";
|
|
4
|
+
export type PrismServerOperation = "agent.run" | "agent.stream" | "workflow.run" | "workflow.stream" | "workflow.status" | "workflow.cancel" | "workflow.resume" | "workflow.enqueue" | "workflow.replay" | "schedule.create" | "schedule.list" | "schedule.pause" | "schedule.resume" | "schedule.trigger" | "schedule.delete";
|
|
5
|
+
export interface PrismServerAuthorization {
|
|
6
|
+
readonly ownership: OwnershipScope;
|
|
7
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
8
|
+
}
|
|
9
|
+
export interface PrismServerAuthorizationInput {
|
|
10
|
+
readonly request: Request;
|
|
11
|
+
readonly operation: PrismServerOperation;
|
|
12
|
+
readonly capabilityId: string;
|
|
13
|
+
readonly signal: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
export type PrismServerAuthorizer = (input: PrismServerAuthorizationInput) => false | PrismServerAuthorization | Promise<false | PrismServerAuthorization>;
|
|
16
|
+
export interface PrismAgentExposure {
|
|
17
|
+
readonly sessionFactory: (authorization: PrismServerAuthorization) => AgentSession | Promise<AgentSession>;
|
|
18
|
+
readonly runOptions?: Omit<RunOptions, "ownership" | "signal" | "redactor">;
|
|
19
|
+
}
|
|
20
|
+
export interface PrismWorkflowExposure {
|
|
21
|
+
readonly definition: WorkflowDefinition;
|
|
22
|
+
readonly checkpoints: WorkflowCheckpointAdapter;
|
|
23
|
+
readonly runOptions?: Omit<RunWorkflowOptions, "checkpoints" | "ownership" | "signal" | "redactor" | "eventBus" | "runId">;
|
|
24
|
+
}
|
|
25
|
+
export type PrismScheduleExposure = WorkflowSchedules | ((authorization: PrismServerAuthorization, signal: AbortSignal) => WorkflowSchedules | Promise<WorkflowSchedules>);
|
|
26
|
+
export interface CreatePrismHandlerOptions {
|
|
27
|
+
readonly agents?: Readonly<Record<string, Agent | PrismAgentExposure>>;
|
|
28
|
+
readonly workflows?: Readonly<Record<string, PrismWorkflowExposure>>;
|
|
29
|
+
readonly schedules?: PrismScheduleExposure;
|
|
30
|
+
readonly authorize: PrismServerAuthorizer;
|
|
31
|
+
readonly basePath?: string;
|
|
32
|
+
readonly allowedHosts?: readonly string[];
|
|
33
|
+
readonly allowedOrigins?: readonly string[];
|
|
34
|
+
readonly redactor?: SecretRedactor;
|
|
35
|
+
readonly limits?: PrismServerLimits;
|
|
36
|
+
readonly disconnectAborts?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export type PrismRequestHandler = (request: Request) => Promise<Response>;
|
|
39
|
+
export declare class PrismServerError extends Error {
|
|
40
|
+
readonly status: number;
|
|
41
|
+
readonly code: string;
|
|
42
|
+
constructor(message: string, status?: number, code?: string);
|
|
43
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class PrismServerError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
constructor(message, status = 500, code = "ERR_PRISM_SERVER") {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = "PrismServerError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arnilo/prism-server",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Optional framework-free Web Request-to-Response handler for explicitly selected Prism agents and workflows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"!dist/__tests__",
|
|
17
|
+
"!dist/**/*.map",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "node --test dist/__tests__/*.test.js",
|
|
25
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@arnilo/prism": "0.0.5",
|
|
29
|
+
"@arnilo/prism-workflows": "0.0.5"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@arnilo/prism": "file:../..",
|
|
33
|
+
"@arnilo/prism-workflows": "file:../workflows"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/ashiqrniloy/prism.git",
|
|
42
|
+
"directory": "packages/server"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/ashiqrniloy/prism/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/ashiqrniloy/prism/tree/main/packages/server#readme",
|
|
48
|
+
"keywords": [
|
|
49
|
+
"prism",
|
|
50
|
+
"server",
|
|
51
|
+
"fetch",
|
|
52
|
+
"agent",
|
|
53
|
+
"workflow"
|
|
54
|
+
],
|
|
55
|
+
"sideEffects": false,
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|