@arnilo/prism-server 0.0.96 → 0.1.1
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 +128 -1
- package/README.md +25 -2
- package/dist/artifact-bodies-s3.d.ts +75 -0
- package/dist/artifact-bodies-s3.js +104 -0
- package/dist/artifact-bodies.d.ts +70 -0
- package/dist/artifact-bodies.js +391 -0
- package/dist/artifacts.d.ts +207 -0
- package/dist/artifacts.js +784 -0
- package/dist/conversations.d.ts +149 -0
- package/dist/conversations.js +558 -0
- package/dist/deployment.d.ts +31 -0
- package/dist/deployment.js +49 -0
- package/dist/drain.d.ts +23 -0
- package/dist/drain.js +57 -0
- package/dist/handler.js +161 -19
- package/dist/health.d.ts +20 -0
- package/dist/health.js +103 -0
- package/dist/index.d.ts +17 -3
- package/dist/index.js +8 -1
- package/dist/limits.d.ts +24 -0
- package/dist/limits.js +18 -0
- package/dist/rate-limit.d.ts +26 -0
- package/dist/rate-limit.js +41 -0
- package/dist/replay.d.ts +31 -0
- package/dist/replay.js +144 -0
- package/dist/types.d.ts +21 -3
- package/dist/types.js +3 -1
- package/package.json +8 -3
package/dist/drain.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { DEFAULT_DRAIN_DEADLINE_MS, HARD_DRAIN_DEADLINE_MS, resolvePrismDeploymentLimits } from "./limits.js";
|
|
2
|
+
import { PrismServerError } from "./types.js";
|
|
3
|
+
export function createPrismDrainController(options = {}) {
|
|
4
|
+
const defaults = resolvePrismDeploymentLimits({ drainDeadlineMs: options.deadlineMs });
|
|
5
|
+
let draining = false;
|
|
6
|
+
let startedAt;
|
|
7
|
+
let deadlineAt;
|
|
8
|
+
let deadlineMs = defaults.drainDeadlineMs;
|
|
9
|
+
return {
|
|
10
|
+
get isDraining() {
|
|
11
|
+
return draining;
|
|
12
|
+
},
|
|
13
|
+
beginDrain(input) {
|
|
14
|
+
if (!draining) {
|
|
15
|
+
draining = true;
|
|
16
|
+
startedAt = new Date().toISOString();
|
|
17
|
+
deadlineMs = resolveDeadline(input?.deadlineMs ?? options.deadlineMs ?? DEFAULT_DRAIN_DEADLINE_MS);
|
|
18
|
+
deadlineAt = new Date(Date.now() + deadlineMs).toISOString();
|
|
19
|
+
}
|
|
20
|
+
return this.snapshot();
|
|
21
|
+
},
|
|
22
|
+
assertAdmit() {
|
|
23
|
+
if (draining) {
|
|
24
|
+
throw new PrismServerError("Server is draining", 503, "ERR_PRISM_SERVER_DRAINING");
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
snapshot() {
|
|
28
|
+
return {
|
|
29
|
+
status: draining ? "draining" : "serving",
|
|
30
|
+
draining,
|
|
31
|
+
...(startedAt === undefined ? {} : { startedAt }),
|
|
32
|
+
...(deadlineAt === undefined ? {} : { deadlineAt }),
|
|
33
|
+
deadlineMs,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function resolveDeadline(value) {
|
|
39
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > HARD_DRAIN_DEADLINE_MS) {
|
|
40
|
+
throw new RangeError(`drainDeadlineMs must be a positive safe integer <= ${HARD_DRAIN_DEADLINE_MS}`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
/** Operations that create or continue work; blocked during drain. Status/cancel/list remain open. */
|
|
45
|
+
export function isAdmitOperation(operation) {
|
|
46
|
+
return (operation === "agent.run" ||
|
|
47
|
+
operation === "agent.stream" ||
|
|
48
|
+
operation === "agent.resume" ||
|
|
49
|
+
operation === "workflow.run" ||
|
|
50
|
+
operation === "workflow.stream" ||
|
|
51
|
+
operation === "workflow.enqueue" ||
|
|
52
|
+
operation === "workflow.resume" ||
|
|
53
|
+
operation === "workflow.replay" ||
|
|
54
|
+
operation === "schedule.create" ||
|
|
55
|
+
operation === "schedule.trigger");
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=drain.js.map
|
package/dist/handler.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { AgentRunStateError } from "@arnilo/prism";
|
|
1
|
+
import { AgentRunStateError, assertIdentityActive, assertIdentityMatchesOwnership, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, } from "@arnilo/prism";
|
|
2
2
|
import { cancelWorkflowRun, createWorkflowEventBus, enqueueWorkflow, getWorkflowRun, replayWorkflow, resumeWorkflow, runWorkflow, } from "@arnilo/prism-workflows";
|
|
3
|
+
import { isAdmitOperation } from "./drain.js";
|
|
3
4
|
import { resolvePrismServerLimits } from "./limits.js";
|
|
4
5
|
import { PrismServerError } from "./types.js";
|
|
5
6
|
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
@@ -14,9 +15,7 @@ export function createPrismHandler(options) {
|
|
|
14
15
|
let activeRuns = 0;
|
|
15
16
|
return async (request) => {
|
|
16
17
|
const origin = request.headers.get("origin");
|
|
17
|
-
const corsHeaders = origin && options.allowedOrigins?.includes(origin)
|
|
18
|
-
? { "access-control-allow-origin": origin, vary: "origin" }
|
|
19
|
-
: undefined;
|
|
18
|
+
const corsHeaders = origin && options.allowedOrigins?.includes(origin) ? { "access-control-allow-origin": origin, vary: "origin" } : undefined;
|
|
20
19
|
const respond = (response) => addHeaders(response, corsHeaders);
|
|
21
20
|
try {
|
|
22
21
|
assertRequestPolicy(request, options.allowedHosts, options.allowedOrigins);
|
|
@@ -29,7 +28,7 @@ export function createPrismHandler(options) {
|
|
|
29
28
|
headers: {
|
|
30
29
|
"access-control-allow-origin": origin,
|
|
31
30
|
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
|
|
32
|
-
"access-control-allow-headers": "content-type, authorization",
|
|
31
|
+
"access-control-allow-headers": "content-type, authorization, last-event-id",
|
|
33
32
|
vary: "origin",
|
|
34
33
|
},
|
|
35
34
|
}));
|
|
@@ -39,6 +38,24 @@ export function createPrismHandler(options) {
|
|
|
39
38
|
const authorization = await authorize(options, request, route.operation, route.capabilityId, limits.requestTimeoutMs);
|
|
40
39
|
if (!authorization)
|
|
41
40
|
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
41
|
+
if (options.rateLimit) {
|
|
42
|
+
const decision = await options.rateLimit({
|
|
43
|
+
request,
|
|
44
|
+
operation: route.operation,
|
|
45
|
+
capabilityId: route.capabilityId,
|
|
46
|
+
authorization,
|
|
47
|
+
signal: request.signal,
|
|
48
|
+
});
|
|
49
|
+
if (decision !== true) {
|
|
50
|
+
const headers = {};
|
|
51
|
+
if (decision.retryAfterMs !== undefined && Number.isSafeInteger(decision.retryAfterMs) && decision.retryAfterMs > 0) {
|
|
52
|
+
headers["retry-after"] = String(Math.ceil(decision.retryAfterMs / 1000));
|
|
53
|
+
}
|
|
54
|
+
throw new PrismServerError(decision.message ?? "Rate limit exceeded", 429, decision.code ?? "ERR_PRISM_SERVER_RATE_LIMIT", Object.keys(headers).length ? headers : undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (options.drain && isAdmitOperation(route.operation))
|
|
58
|
+
options.drain.assertAdmit();
|
|
42
59
|
if (route.kind.startsWith("schedule-")) {
|
|
43
60
|
const selectedSchedules = options.schedules;
|
|
44
61
|
if (!selectedSchedules)
|
|
@@ -94,6 +111,35 @@ export function createPrismHandler(options) {
|
|
|
94
111
|
owned.dispose();
|
|
95
112
|
}
|
|
96
113
|
}
|
|
114
|
+
if (route.kind === "agent-events") {
|
|
115
|
+
const exposure = options.agents?.[route.capabilityId];
|
|
116
|
+
if (!exposure || !("sessionFactory" in exposure) || !exposure.events || !exposure.resolveRun) {
|
|
117
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
118
|
+
}
|
|
119
|
+
if (!authorization.ownership.tenantId)
|
|
120
|
+
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
121
|
+
acquire();
|
|
122
|
+
const owned = ownedSignal(request, limits.requestTimeoutMs, options.disconnectAborts ?? true);
|
|
123
|
+
try {
|
|
124
|
+
const run = await awaitWithSignal(Promise.resolve(exposure.resolveRun({ runId: route.runId, authorization, signal: owned.signal })), owned.signal);
|
|
125
|
+
if (!run?.sessionId)
|
|
126
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
127
|
+
const after = replayCursor(request, limits.maxReplayCursorBytes);
|
|
128
|
+
const events = exposure.events.subscribe({
|
|
129
|
+
ownership: authorization.ownership,
|
|
130
|
+
sessionId: run.sessionId,
|
|
131
|
+
runId: run.runId,
|
|
132
|
+
after,
|
|
133
|
+
signal: owned.signal,
|
|
134
|
+
});
|
|
135
|
+
return respond(sseAgentEvents(events, owned, limits, options, release));
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
owned.dispose();
|
|
139
|
+
release();
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
97
143
|
if (route.kind === "agent-status" || route.kind === "agent-resume") {
|
|
98
144
|
const exposure = options.agentRuns?.[route.capabilityId];
|
|
99
145
|
if (!exposure)
|
|
@@ -139,6 +185,7 @@ export function createPrismHandler(options) {
|
|
|
139
185
|
const runConfig = {
|
|
140
186
|
...runOptions,
|
|
141
187
|
ownership: authorization.ownership,
|
|
188
|
+
identity: authorization.identity,
|
|
142
189
|
metadata: { ...runOptions?.metadata, ...authorization.metadata },
|
|
143
190
|
redactor: options.redactor,
|
|
144
191
|
signal: owned.signal,
|
|
@@ -360,6 +407,8 @@ function parseRoute(request, base) {
|
|
|
360
407
|
return { kind: "agent-status", operation: "agent.status", capabilityId: id, runId };
|
|
361
408
|
if (parts.length === 5 && action === "resume" && request.method === "POST")
|
|
362
409
|
return { kind: "agent-resume", operation: "agent.resume", capabilityId: id, runId };
|
|
410
|
+
if (parts.length === 5 && action === "events" && request.method === "GET")
|
|
411
|
+
return { kind: "agent-events", operation: "agent.events", capabilityId: id, runId };
|
|
363
412
|
}
|
|
364
413
|
if (group !== "workflows")
|
|
365
414
|
return undefined;
|
|
@@ -418,6 +467,15 @@ async function authorize(options, request, operation, capabilityId, timeoutMs) {
|
|
|
418
467
|
}
|
|
419
468
|
if (!result || !hasOwnership(result.ownership))
|
|
420
469
|
return false;
|
|
470
|
+
if (result.identity) {
|
|
471
|
+
try {
|
|
472
|
+
assertIdentityActive(result.identity);
|
|
473
|
+
assertIdentityMatchesOwnership(result.identity, result.ownership);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
421
479
|
return result;
|
|
422
480
|
}
|
|
423
481
|
function hasOwnership(value) {
|
|
@@ -441,7 +499,9 @@ async function readJsonObject(request, maxBytes, signal) {
|
|
|
441
499
|
throw new PrismServerError("JSON body is required", 400, "ERR_PRISM_SERVER_BODY");
|
|
442
500
|
const chunks = [];
|
|
443
501
|
let size = 0;
|
|
444
|
-
const abort = () => {
|
|
502
|
+
const abort = () => {
|
|
503
|
+
void reader.cancel(signal.reason);
|
|
504
|
+
};
|
|
445
505
|
if (signal.aborted)
|
|
446
506
|
abort();
|
|
447
507
|
else
|
|
@@ -498,17 +558,66 @@ function isMessage(value) {
|
|
|
498
558
|
const item = value;
|
|
499
559
|
return ["system", "user", "assistant", "tool"].includes(String(item.role)) && Array.isArray(item.content);
|
|
500
560
|
}
|
|
561
|
+
const RUN_DECISION_OUTCOMES = new Set(["allow_once", "allow_for_run", "reject_once", "reject_for_run"]);
|
|
562
|
+
const RUN_DECISION_KEYS = new Set(["approvalId", "outcome", "reason", "modifiedArguments", "elicitation"]);
|
|
563
|
+
/** Boundary validation for a client-supplied decision batch; core re-validates under CAS. */
|
|
564
|
+
function readAgentDecisions(value) {
|
|
565
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > HARD_MAX_PENDING_DECISIONS) {
|
|
566
|
+
throw new PrismServerError("decisions must be a non-empty bounded array", 400, "ERR_PRISM_SERVER_RESUME");
|
|
567
|
+
}
|
|
568
|
+
return value.map((entry) => {
|
|
569
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
570
|
+
throw new PrismServerError("decision entry must be an object", 400, "ERR_PRISM_SERVER_RESUME");
|
|
571
|
+
}
|
|
572
|
+
const row = entry;
|
|
573
|
+
if (Object.keys(row).some((key) => !RUN_DECISION_KEYS.has(key))) {
|
|
574
|
+
throw new PrismServerError("decision entry has unknown keys", 400, "ERR_PRISM_SERVER_RESUME");
|
|
575
|
+
}
|
|
576
|
+
if (typeof row.approvalId !== "string" || row.approvalId.length === 0 || row.approvalId.length > 128) {
|
|
577
|
+
throw new PrismServerError("decision approvalId is invalid", 400, "ERR_PRISM_SERVER_RESUME");
|
|
578
|
+
}
|
|
579
|
+
if (typeof row.outcome !== "string" || !RUN_DECISION_OUTCOMES.has(row.outcome)) {
|
|
580
|
+
throw new PrismServerError("decision outcome is invalid", 400, "ERR_PRISM_SERVER_RESUME");
|
|
581
|
+
}
|
|
582
|
+
if (row.reason !== undefined &&
|
|
583
|
+
(typeof row.reason !== "string" || Buffer.byteLength(row.reason, "utf8") > HARD_MAX_DECISION_REASON_BYTES)) {
|
|
584
|
+
throw new PrismServerError("decision reason exceeds limits", 400, "ERR_PRISM_SERVER_RESUME");
|
|
585
|
+
}
|
|
586
|
+
for (const key of ["modifiedArguments", "elicitation"]) {
|
|
587
|
+
const field = row[key];
|
|
588
|
+
if (field === undefined)
|
|
589
|
+
continue;
|
|
590
|
+
if (!field || typeof field !== "object" || Array.isArray(field)) {
|
|
591
|
+
throw new PrismServerError(`decision ${key} must be an object`, 400, "ERR_PRISM_SERVER_RESUME");
|
|
592
|
+
}
|
|
593
|
+
const text = JSON.stringify(field);
|
|
594
|
+
if (text === undefined || Buffer.byteLength(text, "utf8") > HARD_MAX_ELICITATION_BYTES) {
|
|
595
|
+
throw new PrismServerError(`decision ${key} exceeds limits`, 400, "ERR_PRISM_SERVER_RESUME");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return entry;
|
|
599
|
+
});
|
|
600
|
+
}
|
|
501
601
|
function readAgentResume(body) {
|
|
502
|
-
if (Object.keys(body).some((key) => key !== "decision" && key !== "expectedVersion")) {
|
|
602
|
+
if (Object.keys(body).some((key) => key !== "decision" && key !== "decisions" && key !== "expectedVersion")) {
|
|
503
603
|
throw new PrismServerError("Invalid agent resume body", 400, "ERR_PRISM_SERVER_RESUME");
|
|
504
604
|
}
|
|
505
|
-
if (body.decision !== "approve" && body.decision !== "deny") {
|
|
506
|
-
throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
|
|
507
|
-
}
|
|
508
605
|
if (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1) {
|
|
509
606
|
throw new PrismServerError("expectedVersion must be a positive safe integer", 400, "ERR_PRISM_SERVER_RESUME");
|
|
510
607
|
}
|
|
511
|
-
|
|
608
|
+
if (body.decision !== undefined && body.decisions !== undefined) {
|
|
609
|
+
throw new PrismServerError("provide exactly one of decision or decisions", 400, "ERR_PRISM_SERVER_RESUME");
|
|
610
|
+
}
|
|
611
|
+
if (body.decision !== undefined) {
|
|
612
|
+
if (body.decision !== "approve" && body.decision !== "deny") {
|
|
613
|
+
throw new PrismServerError("decision must be approve or deny", 400, "ERR_PRISM_SERVER_RESUME");
|
|
614
|
+
}
|
|
615
|
+
return { decision: body.decision, expectedVersion: Number(body.expectedVersion) };
|
|
616
|
+
}
|
|
617
|
+
if (body.decisions === undefined) {
|
|
618
|
+
throw new PrismServerError("provide decision or decisions", 400, "ERR_PRISM_SERVER_RESUME");
|
|
619
|
+
}
|
|
620
|
+
return { decisions: readAgentDecisions(body.decisions), expectedVersion: Number(body.expectedVersion) };
|
|
512
621
|
}
|
|
513
622
|
function readResume(body) {
|
|
514
623
|
if (body.decision !== "approve" && body.decision !== "deny") {
|
|
@@ -563,6 +672,18 @@ function readOptionalId(value, name) {
|
|
|
563
672
|
function validId(value) {
|
|
564
673
|
return value.length <= 128 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value);
|
|
565
674
|
}
|
|
675
|
+
function replayCursor(request, maxBytes) {
|
|
676
|
+
const query = new URL(request.url).searchParams.get("cursor") ?? undefined;
|
|
677
|
+
const header = request.headers.get("last-event-id") ?? undefined;
|
|
678
|
+
if (query !== undefined && header !== undefined && query !== header) {
|
|
679
|
+
throw new PrismServerError("Conflicting event cursors", 400, "ERR_PRISM_SERVER_REPLAY_CURSOR");
|
|
680
|
+
}
|
|
681
|
+
const cursor = header ?? query;
|
|
682
|
+
if (cursor !== undefined && (Buffer.byteLength(cursor, "utf8") > maxBytes || /\r|\n|\0/.test(cursor))) {
|
|
683
|
+
throw new PrismServerError("Invalid event cursor", 400, "ERR_PRISM_SERVER_REPLAY_CURSOR");
|
|
684
|
+
}
|
|
685
|
+
return cursor;
|
|
686
|
+
}
|
|
566
687
|
function normalizeBasePath(value) {
|
|
567
688
|
if (!value.startsWith("/") || value.includes("?") || value.includes("#"))
|
|
568
689
|
throw new RangeError("basePath must be an absolute URL path");
|
|
@@ -609,13 +730,27 @@ function ownedSignal(request, timeoutMs, disconnectAborts) {
|
|
|
609
730
|
},
|
|
610
731
|
};
|
|
611
732
|
}
|
|
733
|
+
function sseAgentEvents(source, owned, limits, options, release) {
|
|
734
|
+
return sseStream(source, ({ record, cursor }) => {
|
|
735
|
+
if (/\r|\n|\0/.test(cursor) || Buffer.byteLength(cursor, "utf8") > limits.maxReplayCursorBytes) {
|
|
736
|
+
throw new PrismServerError("Invalid event cursor", 500, "ERR_PRISM_SERVER_REPLAY_CURSOR");
|
|
737
|
+
}
|
|
738
|
+
const safe = options.redactor?.redact(record.event) ?? record.event;
|
|
739
|
+
return `id: ${cursor}\ndata: ${JSON.stringify(safe)}\n\n`;
|
|
740
|
+
}, owned, limits, release);
|
|
741
|
+
}
|
|
612
742
|
function sse(source, owned, limits, options, release) {
|
|
743
|
+
return sseStream(source, (value) => `data: ${JSON.stringify(options.redactor?.redact(value) ?? value)}\n\n`, owned, limits, release);
|
|
744
|
+
}
|
|
745
|
+
function sseStream(source, serialize, owned, limits, release) {
|
|
613
746
|
const iterator = source[Symbol.asyncIterator]();
|
|
614
747
|
const encoder = new TextEncoder();
|
|
615
748
|
let events = 0;
|
|
616
749
|
let bytes = 0;
|
|
617
750
|
let finished = false;
|
|
618
|
-
const onAbort = () => {
|
|
751
|
+
const onAbort = () => {
|
|
752
|
+
void finish(owned.signal.reason);
|
|
753
|
+
};
|
|
619
754
|
const finish = async (reason) => {
|
|
620
755
|
if (finished)
|
|
621
756
|
return;
|
|
@@ -636,8 +771,7 @@ function sse(source, owned, limits, options, release) {
|
|
|
636
771
|
controller.close();
|
|
637
772
|
return;
|
|
638
773
|
}
|
|
639
|
-
const
|
|
640
|
-
const chunk = encoder.encode(`data: ${JSON.stringify(safe)}\n\n`);
|
|
774
|
+
const chunk = encoder.encode(serialize(next.value));
|
|
641
775
|
events += 1;
|
|
642
776
|
bytes += chunk.byteLength;
|
|
643
777
|
if (chunk.byteLength > limits.maxEventBytes || events > limits.maxStreamEvents || bytes > limits.maxStreamBytes) {
|
|
@@ -673,9 +807,7 @@ function json(value, status, limits, options) {
|
|
|
673
807
|
return new Response(text, { status, headers: JSON_HEADERS });
|
|
674
808
|
}
|
|
675
809
|
function errorResponse(error, limits, options) {
|
|
676
|
-
const workflowCode = error && typeof error === "object" && "code" in error && typeof error.code === "string"
|
|
677
|
-
? error.code
|
|
678
|
-
: undefined;
|
|
810
|
+
const workflowCode = error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
679
811
|
const mapped = workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE_BUSY"
|
|
680
812
|
? { status: 409, code: workflowCode, message: "Schedule is busy" }
|
|
681
813
|
: workflowCode === "ERR_PRISM_WORKFLOW_SCHEDULE"
|
|
@@ -690,10 +822,20 @@ function errorResponse(error, limits, options) {
|
|
|
690
822
|
const known = error instanceof PrismServerError;
|
|
691
823
|
const agentState = error instanceof AgentRunStateError;
|
|
692
824
|
const status = mapped?.status ?? (agentState ? 404 : known ? error.status : error instanceof DOMException && error.name === "AbortError" ? 499 : 500);
|
|
693
|
-
const code = mapped?.code ??
|
|
825
|
+
const code = mapped?.code ??
|
|
826
|
+
(agentState
|
|
827
|
+
? "ERR_PRISM_SERVER_NOT_FOUND"
|
|
828
|
+
: known
|
|
829
|
+
? error.code
|
|
830
|
+
: status === 499
|
|
831
|
+
? "ERR_PRISM_SERVER_ABORTED"
|
|
832
|
+
: "ERR_PRISM_SERVER_INTERNAL");
|
|
694
833
|
const message = mapped?.message ?? (agentState ? "Not found" : known ? error.message : status === 499 ? "Request aborted" : "Internal server error");
|
|
695
834
|
try {
|
|
696
|
-
|
|
835
|
+
const response = json({ error: { code, message } }, status, limits, options);
|
|
836
|
+
if (known && error.headers)
|
|
837
|
+
return addHeaders(response, error.headers);
|
|
838
|
+
return response;
|
|
697
839
|
}
|
|
698
840
|
catch {
|
|
699
841
|
return new Response(null, { status });
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PrismDrainController } from "./drain.js";
|
|
2
|
+
import { type PrismDeploymentLimits } from "./limits.js";
|
|
3
|
+
import { type PrismRequestHandler } from "./types.js";
|
|
4
|
+
export interface CreatePrismHealthHandlerOptions {
|
|
5
|
+
/** Path prefix. Default `/health`. Routes: `/livez`, `/readyz`, and prefix itself. */
|
|
6
|
+
readonly basePath?: string;
|
|
7
|
+
/** Liveness probe. Default always true. Must stay O(1)/bounded. */
|
|
8
|
+
readonly live?: () => boolean | Promise<boolean>;
|
|
9
|
+
/** Readiness probe (deps). Default true. Must stay O(1)/bounded. */
|
|
10
|
+
readonly ready?: () => boolean | Promise<boolean>;
|
|
11
|
+
readonly drain?: PrismDrainController;
|
|
12
|
+
/**
|
|
13
|
+
* Optional extra fields for `?detail=1`. Never include secrets/tenant payloads.
|
|
14
|
+
* Emitted only when `authorizeDetail` returns true.
|
|
15
|
+
*/
|
|
16
|
+
readonly detail?: () => Readonly<Record<string, unknown>> | Promise<Readonly<Record<string, unknown>>>;
|
|
17
|
+
readonly authorizeDetail?: (request: Request) => boolean | Promise<boolean>;
|
|
18
|
+
readonly limits?: PrismDeploymentLimits;
|
|
19
|
+
}
|
|
20
|
+
export declare function createPrismHealthHandler(options?: CreatePrismHealthHandlerOptions): PrismRequestHandler;
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { resolvePrismDeploymentLimits } from "./limits.js";
|
|
2
|
+
import { PrismServerError } from "./types.js";
|
|
3
|
+
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
4
|
+
export function createPrismHealthHandler(options = {}) {
|
|
5
|
+
const limits = resolvePrismDeploymentLimits(options.limits);
|
|
6
|
+
const base = normalizeHealthBase(options.basePath ?? "/health");
|
|
7
|
+
return async (request) => {
|
|
8
|
+
try {
|
|
9
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
10
|
+
throw new PrismServerError("Method not allowed", 405, "ERR_PRISM_SERVER_METHOD");
|
|
11
|
+
}
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
14
|
+
const livePath = `${base}/livez`;
|
|
15
|
+
const readyPath = `${base}/readyz`;
|
|
16
|
+
if (path !== base && path !== livePath && path !== readyPath) {
|
|
17
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
18
|
+
}
|
|
19
|
+
const wantDetail = url.searchParams.get("detail") === "1";
|
|
20
|
+
if (wantDetail) {
|
|
21
|
+
const allowed = options.authorizeDetail ? await options.authorizeDetail(request) : false;
|
|
22
|
+
if (!allowed)
|
|
23
|
+
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
24
|
+
}
|
|
25
|
+
const live = options.live ? await options.live() : true;
|
|
26
|
+
const readyCheck = options.ready ? await options.ready() : true;
|
|
27
|
+
const drainSnap = options.drain?.snapshot();
|
|
28
|
+
const draining = drainSnap?.draining === true;
|
|
29
|
+
const ready = readyCheck && !draining;
|
|
30
|
+
if (path === livePath) {
|
|
31
|
+
return healthJson({ status: live ? "ok" : "fail", live }, live ? 200 : 503, limits, request.method);
|
|
32
|
+
}
|
|
33
|
+
if (path === readyPath) {
|
|
34
|
+
return healthJson({
|
|
35
|
+
status: ready ? "ok" : "fail",
|
|
36
|
+
ready,
|
|
37
|
+
...(drainSnap ? { draining: drainSnap.draining } : {}),
|
|
38
|
+
}, ready ? 200 : 503, limits, request.method);
|
|
39
|
+
}
|
|
40
|
+
const body = {
|
|
41
|
+
status: live && ready ? "ok" : "fail",
|
|
42
|
+
live,
|
|
43
|
+
ready,
|
|
44
|
+
};
|
|
45
|
+
if (drainSnap) {
|
|
46
|
+
body.drain = {
|
|
47
|
+
status: drainSnap.status,
|
|
48
|
+
draining: drainSnap.draining,
|
|
49
|
+
...(drainSnap.deadlineAt === undefined ? {} : { deadlineAt: drainSnap.deadlineAt }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (wantDetail && options.detail) {
|
|
53
|
+
const extra = await options.detail();
|
|
54
|
+
assertSafeDetail(extra);
|
|
55
|
+
body.detail = extra;
|
|
56
|
+
}
|
|
57
|
+
const ok = live && ready;
|
|
58
|
+
return healthJson(body, ok ? 200 : 503, limits, request.method);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof PrismServerError) {
|
|
62
|
+
return new Response(JSON.stringify({ error: { code: error.code, message: error.message } }), {
|
|
63
|
+
status: error.status,
|
|
64
|
+
headers: JSON_HEADERS,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return new Response(JSON.stringify({ error: { code: "ERR_PRISM_SERVER", message: "Health check failed" } }), {
|
|
68
|
+
status: 500,
|
|
69
|
+
headers: JSON_HEADERS,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function healthJson(body, status, limits, method) {
|
|
75
|
+
const text = JSON.stringify(body);
|
|
76
|
+
if (Buffer.byteLength(text, "utf8") > limits.maxHealthBytes) {
|
|
77
|
+
throw new PrismServerError("Health response too large", 507, "ERR_PRISM_SERVER_HEALTH_LIMIT");
|
|
78
|
+
}
|
|
79
|
+
if (method === "HEAD")
|
|
80
|
+
return new Response(null, { status, headers: JSON_HEADERS });
|
|
81
|
+
return new Response(text, { status, headers: JSON_HEADERS });
|
|
82
|
+
}
|
|
83
|
+
function normalizeHealthBase(basePath) {
|
|
84
|
+
if (!basePath.startsWith("/") || basePath.includes("?") || basePath.includes("#")) {
|
|
85
|
+
throw new PrismServerError("Invalid health basePath", 500, "ERR_PRISM_SERVER_CONFIG");
|
|
86
|
+
}
|
|
87
|
+
return basePath.replace(/\/+$/, "") || "/health";
|
|
88
|
+
}
|
|
89
|
+
function assertSafeDetail(value) {
|
|
90
|
+
for (const key of Object.keys(value)) {
|
|
91
|
+
const lower = key.toLowerCase();
|
|
92
|
+
if (lower.includes("secret") ||
|
|
93
|
+
lower.includes("token") ||
|
|
94
|
+
lower.includes("password") ||
|
|
95
|
+
lower.includes("credential") ||
|
|
96
|
+
lower === "authorization" ||
|
|
97
|
+
lower === "prompt" ||
|
|
98
|
+
lower === "body") {
|
|
99
|
+
throw new PrismServerError("Health detail key not allowed", 500, "ERR_PRISM_SERVER_HEALTH_DETAIL");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=health.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
export type { ArtifactAttachInput, ArtifactAuthorizationInput, ArtifactAuthorizer, ArtifactCompareInput, ArtifactCompareResult, ArtifactDecisionEvent, ArtifactDecisionInput, ArtifactDeliveryInput, ArtifactDeliveryResult, ArtifactLimits, ArtifactListInput, ArtifactOperation, ArtifactRefInput, ArtifactReviseInput, ArtifactService, ArtifactServiceInput, CreateArtifactHandlerOptions, CreateArtifactServiceOptions, ResolvedArtifactLimits, } from "./artifacts.js";
|
|
2
|
+
export { createArtifactHandler, createArtifactService, DEFAULT_ARTIFACT_CITATION_BYTES, DEFAULT_ARTIFACT_CITATIONS, DEFAULT_ARTIFACT_HASH_BYTES, DEFAULT_ARTIFACT_LIST_PAGE_LIMIT, DEFAULT_ARTIFACT_MIME_BYTES, DEFAULT_ARTIFACT_NOTE_BYTES, DEFAULT_ARTIFACT_PREVIEW_BYTES, DEFAULT_ARTIFACT_RECORD_BYTES, DEFAULT_ARTIFACT_REQUEST_BYTES, DEFAULT_ARTIFACT_REVISIONS, DEFAULT_ARTIFACT_TITLE_BYTES, DEFAULT_ARTIFACT_URI_BYTES, DEFAULT_ARTIFACTS_PER_THREAD, DEFAULT_DELIVERY_LINK_TOKEN_BYTES, DEFAULT_DELIVERY_LINK_TTL_SECONDS, HARD_ARTIFACT_CITATION_BYTES, HARD_ARTIFACT_CITATIONS, HARD_ARTIFACT_HASH_BYTES, HARD_ARTIFACT_LIST_PAGE_LIMIT, HARD_ARTIFACT_MIME_BYTES, HARD_ARTIFACT_NOTE_BYTES, HARD_ARTIFACT_PREVIEW_BYTES, HARD_ARTIFACT_RECORD_BYTES, HARD_ARTIFACT_REQUEST_BYTES, HARD_ARTIFACT_REVISIONS, HARD_ARTIFACT_TITLE_BYTES, HARD_ARTIFACT_URI_BYTES, HARD_ARTIFACTS_PER_THREAD, HARD_DELIVERY_LINK_TOKEN_BYTES, HARD_DELIVERY_LINK_TTL_SECONDS, resolveArtifactLimits, signArtifactDeliveryLink, verifyArtifactDeliveryLink, } from "./artifacts.js";
|
|
3
|
+
export type { ConversationAuthorizationInput, ConversationAuthorizer, ConversationBranchInput, ConversationContinueInput, ConversationCreateInput, ConversationExportInput, ConversationExportPage, ConversationLimits, ConversationListInput, ConversationOperation, ConversationRefInput, ConversationReplayInput, ConversationReplayPage, ConversationService, ConversationServiceInput, ConversationServiceStore, ConversationSessionFactoryInput, CreateConversationHandlerOptions, CreateConversationServiceOptions, ResolvedConversationLimits, } from "./conversations.js";
|
|
4
|
+
export { createConversationHandler, createConversationService, DEFAULT_CONVERSATION_CURSOR_BYTES, DEFAULT_CONVERSATION_EXPORT_BYTES, DEFAULT_CONVERSATION_EXPORT_PAGES, DEFAULT_CONVERSATION_MAX_ACTIVE_BRANCHES, DEFAULT_CONVERSATION_REPLAY_PAGE_LIMIT, DEFAULT_CONVERSATION_REQUEST_BYTES, DEFAULT_CONVERSATION_REQUEST_ID_BYTES, DEFAULT_CONVERSATION_THREAD_PAGE_LIMIT, DEFAULT_CONVERSATION_TITLE_BYTES, HARD_CONVERSATION_CURSOR_BYTES, HARD_CONVERSATION_EXPORT_BYTES, HARD_CONVERSATION_EXPORT_PAGES, HARD_CONVERSATION_MAX_ACTIVE_BRANCHES, HARD_CONVERSATION_REPLAY_PAGE_LIMIT, HARD_CONVERSATION_REQUEST_BYTES, HARD_CONVERSATION_REQUEST_ID_BYTES, HARD_CONVERSATION_THREAD_PAGE_LIMIT, HARD_CONVERSATION_TITLE_BYTES, resolveConversationLimits, } from "./conversations.js";
|
|
5
|
+
export type { PrismDeploymentLease, PrismDeploymentLeaseOptions } from "./deployment.js";
|
|
6
|
+
export { createPrismDeploymentLease, PRISM_DEPLOYMENT_LEASE_NAMESPACE } from "./deployment.js";
|
|
7
|
+
export type { PrismDrainController, PrismDrainControllerOptions, PrismDrainSnapshot } from "./drain.js";
|
|
8
|
+
export { createPrismDrainController, isAdmitOperation } from "./drain.js";
|
|
1
9
|
export { createPrismHandler } from "./handler.js";
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
4
|
-
export type {
|
|
10
|
+
export type { CreatePrismHealthHandlerOptions } from "./health.js";
|
|
11
|
+
export { createPrismHealthHandler } from "./health.js";
|
|
12
|
+
export type { PrismDeploymentLimits, PrismServerLimits, ResolvedPrismDeploymentLimits, ResolvedPrismServerLimits, } from "./limits.js";
|
|
13
|
+
export { DEFAULT_DRAIN_DEADLINE_MS, DEFAULT_MAX_CONCURRENT_RUNS, DEFAULT_MAX_EVENT_BYTES, DEFAULT_MAX_HEALTH_BYTES, DEFAULT_MAX_QUEUED_EVENTS, DEFAULT_MAX_REPLAY_CURSOR_BYTES, DEFAULT_MAX_REPLAY_EVENTS, DEFAULT_MAX_REQUEST_BYTES, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_MAX_STREAM_BYTES, DEFAULT_MAX_STREAM_EVENTS, DEFAULT_REQUEST_TIMEOUT_MS, HARD_DRAIN_DEADLINE_MS, HARD_MAX_CONCURRENT_RUNS, HARD_MAX_EVENT_BYTES, HARD_MAX_HEALTH_BYTES, HARD_MAX_QUEUED_EVENTS, HARD_MAX_REPLAY_CURSOR_BYTES, HARD_MAX_REPLAY_EVENTS, HARD_MAX_REQUEST_BYTES, HARD_MAX_RESPONSE_BYTES, HARD_MAX_STREAM_BYTES, HARD_MAX_STREAM_EVENTS, HARD_REQUEST_TIMEOUT_MS, resolvePrismDeploymentLimits, resolvePrismServerLimits, } from "./limits.js";
|
|
14
|
+
export type { MemoryRateLimiterOptions, PrismServerRateLimitDenial, PrismServerRateLimiter, PrismServerRateLimitInput, } from "./rate-limit.js";
|
|
15
|
+
export { createMemoryRateLimiter } from "./rate-limit.js";
|
|
16
|
+
export type { CreatePrismEventReplayOptions, CreatePrismReplayHandlerOptions, PrismAgentEventReplay, PrismEventReplay, PrismEventReplayRequest, } from "./replay.js";
|
|
17
|
+
export { createPrismAgentEventReplay, createPrismEventReplay, createPrismReplayHandler } from "./replay.js";
|
|
18
|
+
export type { CreatePrismHandlerOptions, PrismAgentEventResolutionInput, PrismAgentExposure, PrismAgentRunExposure, PrismRequestHandler, PrismScheduleExposure, PrismServerAuthorization, PrismServerAuthorizationInput, PrismServerAuthorizer, PrismServerOperation, PrismWorkflowExposure, } from "./types.js";
|
|
5
19
|
export { PrismServerError } from "./types.js";
|
|
6
20
|
export declare const packageName = "@arnilo/prism-server";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
export { createArtifactHandler, createArtifactService, DEFAULT_ARTIFACT_CITATION_BYTES, DEFAULT_ARTIFACT_CITATIONS, DEFAULT_ARTIFACT_HASH_BYTES, DEFAULT_ARTIFACT_LIST_PAGE_LIMIT, DEFAULT_ARTIFACT_MIME_BYTES, DEFAULT_ARTIFACT_NOTE_BYTES, DEFAULT_ARTIFACT_PREVIEW_BYTES, DEFAULT_ARTIFACT_RECORD_BYTES, DEFAULT_ARTIFACT_REQUEST_BYTES, DEFAULT_ARTIFACT_REVISIONS, DEFAULT_ARTIFACT_TITLE_BYTES, DEFAULT_ARTIFACT_URI_BYTES, DEFAULT_ARTIFACTS_PER_THREAD, DEFAULT_DELIVERY_LINK_TOKEN_BYTES, DEFAULT_DELIVERY_LINK_TTL_SECONDS, HARD_ARTIFACT_CITATION_BYTES, HARD_ARTIFACT_CITATIONS, HARD_ARTIFACT_HASH_BYTES, HARD_ARTIFACT_LIST_PAGE_LIMIT, HARD_ARTIFACT_MIME_BYTES, HARD_ARTIFACT_NOTE_BYTES, HARD_ARTIFACT_PREVIEW_BYTES, HARD_ARTIFACT_RECORD_BYTES, HARD_ARTIFACT_REQUEST_BYTES, HARD_ARTIFACT_REVISIONS, HARD_ARTIFACT_TITLE_BYTES, HARD_ARTIFACT_URI_BYTES, HARD_ARTIFACTS_PER_THREAD, HARD_DELIVERY_LINK_TOKEN_BYTES, HARD_DELIVERY_LINK_TTL_SECONDS, resolveArtifactLimits, signArtifactDeliveryLink, verifyArtifactDeliveryLink, } from "./artifacts.js";
|
|
2
|
+
export { createConversationHandler, createConversationService, DEFAULT_CONVERSATION_CURSOR_BYTES, DEFAULT_CONVERSATION_EXPORT_BYTES, DEFAULT_CONVERSATION_EXPORT_PAGES, DEFAULT_CONVERSATION_MAX_ACTIVE_BRANCHES, DEFAULT_CONVERSATION_REPLAY_PAGE_LIMIT, DEFAULT_CONVERSATION_REQUEST_BYTES, DEFAULT_CONVERSATION_REQUEST_ID_BYTES, DEFAULT_CONVERSATION_THREAD_PAGE_LIMIT, DEFAULT_CONVERSATION_TITLE_BYTES, HARD_CONVERSATION_CURSOR_BYTES, HARD_CONVERSATION_EXPORT_BYTES, HARD_CONVERSATION_EXPORT_PAGES, HARD_CONVERSATION_MAX_ACTIVE_BRANCHES, HARD_CONVERSATION_REPLAY_PAGE_LIMIT, HARD_CONVERSATION_REQUEST_BYTES, HARD_CONVERSATION_REQUEST_ID_BYTES, HARD_CONVERSATION_THREAD_PAGE_LIMIT, HARD_CONVERSATION_TITLE_BYTES, resolveConversationLimits, } from "./conversations.js";
|
|
3
|
+
export { createPrismDeploymentLease, PRISM_DEPLOYMENT_LEASE_NAMESPACE } from "./deployment.js";
|
|
4
|
+
export { createPrismDrainController, isAdmitOperation } from "./drain.js";
|
|
1
5
|
export { createPrismHandler } from "./handler.js";
|
|
2
|
-
export {
|
|
6
|
+
export { createPrismHealthHandler } from "./health.js";
|
|
7
|
+
export { DEFAULT_DRAIN_DEADLINE_MS, DEFAULT_MAX_CONCURRENT_RUNS, DEFAULT_MAX_EVENT_BYTES, DEFAULT_MAX_HEALTH_BYTES, DEFAULT_MAX_QUEUED_EVENTS, DEFAULT_MAX_REPLAY_CURSOR_BYTES, DEFAULT_MAX_REPLAY_EVENTS, DEFAULT_MAX_REQUEST_BYTES, DEFAULT_MAX_RESPONSE_BYTES, DEFAULT_MAX_STREAM_BYTES, DEFAULT_MAX_STREAM_EVENTS, DEFAULT_REQUEST_TIMEOUT_MS, HARD_DRAIN_DEADLINE_MS, HARD_MAX_CONCURRENT_RUNS, HARD_MAX_EVENT_BYTES, HARD_MAX_HEALTH_BYTES, HARD_MAX_QUEUED_EVENTS, HARD_MAX_REPLAY_CURSOR_BYTES, HARD_MAX_REPLAY_EVENTS, HARD_MAX_REQUEST_BYTES, HARD_MAX_RESPONSE_BYTES, HARD_MAX_STREAM_BYTES, HARD_MAX_STREAM_EVENTS, HARD_REQUEST_TIMEOUT_MS, resolvePrismDeploymentLimits, resolvePrismServerLimits, } from "./limits.js";
|
|
8
|
+
export { createMemoryRateLimiter } from "./rate-limit.js";
|
|
9
|
+
export { createPrismAgentEventReplay, createPrismEventReplay, createPrismReplayHandler } from "./replay.js";
|
|
3
10
|
export { PrismServerError } from "./types.js";
|
|
4
11
|
export const packageName = "@arnilo/prism-server";
|
|
5
12
|
//# sourceMappingURL=index.js.map
|
package/dist/limits.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export declare const DEFAULT_MAX_QUEUED_EVENTS = 128;
|
|
|
14
14
|
export declare const HARD_MAX_QUEUED_EVENTS = 4096;
|
|
15
15
|
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
|
|
16
16
|
export declare const HARD_REQUEST_TIMEOUT_MS: number;
|
|
17
|
+
/** Phase 8 freeze: health body 4 KiB / 64 KiB; drain deadline 30 s / 5 min; replay page 100 / 500; cursor 4 / 16 KiB. */
|
|
18
|
+
export declare const DEFAULT_MAX_HEALTH_BYTES: number;
|
|
19
|
+
export declare const HARD_MAX_HEALTH_BYTES: number;
|
|
20
|
+
export declare const DEFAULT_DRAIN_DEADLINE_MS = 30000;
|
|
21
|
+
export declare const HARD_DRAIN_DEADLINE_MS: number;
|
|
22
|
+
export declare const DEFAULT_MAX_REPLAY_EVENTS = 100;
|
|
23
|
+
export declare const HARD_MAX_REPLAY_EVENTS = 500;
|
|
24
|
+
export declare const DEFAULT_MAX_REPLAY_CURSOR_BYTES: number;
|
|
25
|
+
export declare const HARD_MAX_REPLAY_CURSOR_BYTES: number;
|
|
17
26
|
export interface PrismServerLimits {
|
|
18
27
|
readonly maxRequestBytes?: number;
|
|
19
28
|
readonly maxResponseBytes?: number;
|
|
@@ -22,6 +31,7 @@ export interface PrismServerLimits {
|
|
|
22
31
|
readonly maxStreamEvents?: number;
|
|
23
32
|
readonly maxConcurrentRuns?: number;
|
|
24
33
|
readonly maxQueuedEvents?: number;
|
|
34
|
+
readonly maxReplayCursorBytes?: number;
|
|
25
35
|
readonly requestTimeoutMs?: number;
|
|
26
36
|
}
|
|
27
37
|
export interface ResolvedPrismServerLimits {
|
|
@@ -32,6 +42,20 @@ export interface ResolvedPrismServerLimits {
|
|
|
32
42
|
readonly maxStreamEvents: number;
|
|
33
43
|
readonly maxConcurrentRuns: number;
|
|
34
44
|
readonly maxQueuedEvents: number;
|
|
45
|
+
readonly maxReplayCursorBytes: number;
|
|
35
46
|
readonly requestTimeoutMs: number;
|
|
36
47
|
}
|
|
48
|
+
export interface PrismDeploymentLimits {
|
|
49
|
+
readonly maxHealthBytes?: number;
|
|
50
|
+
readonly drainDeadlineMs?: number;
|
|
51
|
+
readonly maxReplayEvents?: number;
|
|
52
|
+
readonly maxReplayCursorBytes?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface ResolvedPrismDeploymentLimits {
|
|
55
|
+
readonly maxHealthBytes: number;
|
|
56
|
+
readonly drainDeadlineMs: number;
|
|
57
|
+
readonly maxReplayEvents: number;
|
|
58
|
+
readonly maxReplayCursorBytes: number;
|
|
59
|
+
}
|
|
37
60
|
export declare function resolvePrismServerLimits(input?: PrismServerLimits): ResolvedPrismServerLimits;
|
|
61
|
+
export declare function resolvePrismDeploymentLimits(input?: PrismDeploymentLimits): ResolvedPrismDeploymentLimits;
|
package/dist/limits.js
CHANGED
|
@@ -14,6 +14,15 @@ export const DEFAULT_MAX_QUEUED_EVENTS = 128;
|
|
|
14
14
|
export const HARD_MAX_QUEUED_EVENTS = 4096;
|
|
15
15
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
16
16
|
export const HARD_REQUEST_TIMEOUT_MS = 30 * 60_000;
|
|
17
|
+
/** Phase 8 freeze: health body 4 KiB / 64 KiB; drain deadline 30 s / 5 min; replay page 100 / 500; cursor 4 / 16 KiB. */
|
|
18
|
+
export const DEFAULT_MAX_HEALTH_BYTES = 4 * 1024;
|
|
19
|
+
export const HARD_MAX_HEALTH_BYTES = 64 * 1024;
|
|
20
|
+
export const DEFAULT_DRAIN_DEADLINE_MS = 30_000;
|
|
21
|
+
export const HARD_DRAIN_DEADLINE_MS = 5 * 60_000;
|
|
22
|
+
export const DEFAULT_MAX_REPLAY_EVENTS = 100;
|
|
23
|
+
export const HARD_MAX_REPLAY_EVENTS = 500;
|
|
24
|
+
export const DEFAULT_MAX_REPLAY_CURSOR_BYTES = 4 * 1024;
|
|
25
|
+
export const HARD_MAX_REPLAY_CURSOR_BYTES = 16 * 1024;
|
|
17
26
|
export function resolvePrismServerLimits(input = {}) {
|
|
18
27
|
return {
|
|
19
28
|
maxRequestBytes: bounded(input.maxRequestBytes, DEFAULT_MAX_REQUEST_BYTES, HARD_MAX_REQUEST_BYTES, "maxRequestBytes"),
|
|
@@ -23,9 +32,18 @@ export function resolvePrismServerLimits(input = {}) {
|
|
|
23
32
|
maxStreamEvents: bounded(input.maxStreamEvents, DEFAULT_MAX_STREAM_EVENTS, HARD_MAX_STREAM_EVENTS, "maxStreamEvents"),
|
|
24
33
|
maxConcurrentRuns: bounded(input.maxConcurrentRuns, DEFAULT_MAX_CONCURRENT_RUNS, HARD_MAX_CONCURRENT_RUNS, "maxConcurrentRuns"),
|
|
25
34
|
maxQueuedEvents: bounded(input.maxQueuedEvents, DEFAULT_MAX_QUEUED_EVENTS, HARD_MAX_QUEUED_EVENTS, "maxQueuedEvents"),
|
|
35
|
+
maxReplayCursorBytes: bounded(input.maxReplayCursorBytes, DEFAULT_MAX_REPLAY_CURSOR_BYTES, HARD_MAX_REPLAY_CURSOR_BYTES, "maxReplayCursorBytes"),
|
|
26
36
|
requestTimeoutMs: bounded(input.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, HARD_REQUEST_TIMEOUT_MS, "requestTimeoutMs"),
|
|
27
37
|
};
|
|
28
38
|
}
|
|
39
|
+
export function resolvePrismDeploymentLimits(input = {}) {
|
|
40
|
+
return {
|
|
41
|
+
maxHealthBytes: bounded(input.maxHealthBytes, DEFAULT_MAX_HEALTH_BYTES, HARD_MAX_HEALTH_BYTES, "maxHealthBytes"),
|
|
42
|
+
drainDeadlineMs: bounded(input.drainDeadlineMs, DEFAULT_DRAIN_DEADLINE_MS, HARD_DRAIN_DEADLINE_MS, "drainDeadlineMs"),
|
|
43
|
+
maxReplayEvents: bounded(input.maxReplayEvents, DEFAULT_MAX_REPLAY_EVENTS, HARD_MAX_REPLAY_EVENTS, "maxReplayEvents"),
|
|
44
|
+
maxReplayCursorBytes: bounded(input.maxReplayCursorBytes, DEFAULT_MAX_REPLAY_CURSOR_BYTES, HARD_MAX_REPLAY_CURSOR_BYTES, "maxReplayCursorBytes"),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
29
47
|
function bounded(value, fallback, cap, name) {
|
|
30
48
|
const resolved = value ?? fallback;
|
|
31
49
|
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > cap) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PrismServerAuthorization, PrismServerOperation } from "./types.js";
|
|
2
|
+
export interface PrismServerRateLimitDenial {
|
|
3
|
+
readonly retryAfterMs?: number;
|
|
4
|
+
/** Attributable denial code for hosts/logs. Default ERR_PRISM_SERVER_RATE_LIMIT. */
|
|
5
|
+
readonly code?: string;
|
|
6
|
+
readonly message?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PrismServerRateLimitInput {
|
|
9
|
+
readonly request: Request;
|
|
10
|
+
readonly operation: PrismServerOperation;
|
|
11
|
+
readonly capabilityId: string;
|
|
12
|
+
readonly authorization: PrismServerAuthorization;
|
|
13
|
+
readonly signal: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
/** Return `true` to admit; return a denial object to short-circuit with 429. */
|
|
16
|
+
export type PrismServerRateLimiter = (input: PrismServerRateLimitInput) => true | PrismServerRateLimitDenial | Promise<true | PrismServerRateLimitDenial>;
|
|
17
|
+
export interface MemoryRateLimiterOptions {
|
|
18
|
+
readonly maxRequests: number;
|
|
19
|
+
readonly windowMs: number;
|
|
20
|
+
/** Default: identity tenant/account/user + operation. */
|
|
21
|
+
readonly key?: (input: PrismServerRateLimitInput) => string;
|
|
22
|
+
/** Cap distinct keys retained (oldest eviction). Default 1024. */
|
|
23
|
+
readonly maxKeys?: number;
|
|
24
|
+
}
|
|
25
|
+
/** Tiny in-memory sliding window for tests/single-process hosts. Not a distributed limiter. */
|
|
26
|
+
export declare function createMemoryRateLimiter(options: MemoryRateLimiterOptions): PrismServerRateLimiter;
|