@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/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 = () => { void reader.cancel(signal.reason); };
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
- return { decision: body.decision, expectedVersion: Number(body.expectedVersion) };
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 = () => { void finish(owned.signal.reason); };
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 safe = options.redactor?.redact(next.value) ?? next.value;
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 ?? (agentState ? "ERR_PRISM_SERVER_NOT_FOUND" : known ? error.code : status === 499 ? "ERR_PRISM_SERVER_ABORTED" : "ERR_PRISM_SERVER_INTERNAL");
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
- return json({ error: { code, message } }, status, limits, options);
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 });
@@ -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 { 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, PrismAgentRunExposure, PrismWorkflowExposure, PrismScheduleExposure, CreatePrismHandlerOptions, PrismRequestHandler, } from "./types.js";
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 { 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";
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;