@arnilo/prism-server 0.0.96 → 0.1.0
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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Tiny in-memory sliding window for tests/single-process hosts. Not a distributed limiter. */
|
|
2
|
+
export function createMemoryRateLimiter(options) {
|
|
3
|
+
if (!Number.isSafeInteger(options.maxRequests) || options.maxRequests < 1) {
|
|
4
|
+
throw new RangeError("maxRequests must be a positive safe integer");
|
|
5
|
+
}
|
|
6
|
+
if (!Number.isSafeInteger(options.windowMs) || options.windowMs < 1) {
|
|
7
|
+
throw new RangeError("windowMs must be a positive safe integer");
|
|
8
|
+
}
|
|
9
|
+
const maxKeys = options.maxKeys ?? 1024;
|
|
10
|
+
if (!Number.isSafeInteger(maxKeys) || maxKeys < 1)
|
|
11
|
+
throw new RangeError("maxKeys must be a positive safe integer");
|
|
12
|
+
const buckets = new Map();
|
|
13
|
+
return (input) => {
|
|
14
|
+
input.signal.throwIfAborted();
|
|
15
|
+
const key = options.key?.(input) ?? defaultKey(input);
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const windowStart = now - options.windowMs;
|
|
18
|
+
let stamps = (buckets.get(key) ?? []).filter((t) => t > windowStart);
|
|
19
|
+
if (stamps.length >= options.maxRequests) {
|
|
20
|
+
const oldest = stamps[0] ?? now;
|
|
21
|
+
return {
|
|
22
|
+
retryAfterMs: Math.max(1, oldest + options.windowMs - now),
|
|
23
|
+
code: "ERR_PRISM_SERVER_RATE_LIMIT",
|
|
24
|
+
message: "Rate limit exceeded",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
stamps = [...stamps, now];
|
|
28
|
+
if (!buckets.has(key) && buckets.size >= maxKeys) {
|
|
29
|
+
const first = buckets.keys().next().value;
|
|
30
|
+
if (first !== undefined)
|
|
31
|
+
buckets.delete(first);
|
|
32
|
+
}
|
|
33
|
+
buckets.set(key, stamps);
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function defaultKey(input) {
|
|
38
|
+
const o = input.authorization.ownership;
|
|
39
|
+
return `${o.tenantId ?? ""}\0${o.accountId ?? ""}\0${o.userId ?? ""}\0${input.operation}\0${input.capabilityId}`;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=rate-limit.js.map
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AgentEventEnvelope, AgentEventRecord, AgentEventSource, AgentEventSourcePage, OwnershipScope, PersistencePage, ProductionPersistenceStore } from "@arnilo/prism";
|
|
2
|
+
import { type PrismDeploymentLimits } from "./limits.js";
|
|
3
|
+
export interface PrismEventReplayRequest {
|
|
4
|
+
readonly ownership: OwnershipScope;
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
readonly runId: string;
|
|
7
|
+
readonly cursor?: string;
|
|
8
|
+
readonly signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
export interface PrismEventReplay {
|
|
11
|
+
page(input: PrismEventReplayRequest): Promise<PersistencePage<AgentEventRecord>>;
|
|
12
|
+
}
|
|
13
|
+
export interface CreatePrismEventReplayOptions {
|
|
14
|
+
readonly limits?: PrismDeploymentLimits;
|
|
15
|
+
}
|
|
16
|
+
export interface PrismAgentEventReplay {
|
|
17
|
+
page(input: PrismEventReplayRequest): Promise<AgentEventSourcePage>;
|
|
18
|
+
subscribe(input: PrismEventReplayRequest): AsyncIterable<AgentEventEnvelope>;
|
|
19
|
+
}
|
|
20
|
+
/** Shared source-backed page/follow adapter. Authorization supplies ownership before each call. */
|
|
21
|
+
export declare function createPrismAgentEventReplay(source: AgentEventSource, options?: CreatePrismEventReplayOptions): PrismAgentEventReplay;
|
|
22
|
+
/** Ownership-scoped, cursor-paginated legacy persistence replay. Does not re-run work. */
|
|
23
|
+
export declare function createPrismEventReplay(store: Pick<ProductionPersistenceStore, "queryEvents">, options?: CreatePrismEventReplayOptions): PrismEventReplay;
|
|
24
|
+
export interface CreatePrismReplayHandlerOptions {
|
|
25
|
+
readonly replay: PrismEventReplay;
|
|
26
|
+
readonly authorize: (request: Request) => false | OwnershipScope | Promise<false | OwnershipScope>;
|
|
27
|
+
readonly basePath?: string;
|
|
28
|
+
readonly limits?: PrismDeploymentLimits;
|
|
29
|
+
}
|
|
30
|
+
/** Optional HTTP adapter: POST `{ sessionId, runId, cursor? }` → ownership-scoped page. */
|
|
31
|
+
export declare function createPrismReplayHandler(options: CreatePrismReplayHandlerOptions): import("./types.js").PrismRequestHandler;
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { resolvePrismDeploymentLimits } from "./limits.js";
|
|
2
|
+
import { PrismServerError } from "./types.js";
|
|
3
|
+
/** Shared source-backed page/follow adapter. Authorization supplies ownership before each call. */
|
|
4
|
+
export function createPrismAgentEventReplay(source, options = {}) {
|
|
5
|
+
const limits = resolvePrismDeploymentLimits(options.limits);
|
|
6
|
+
const read = (input) => {
|
|
7
|
+
input.signal?.throwIfAborted();
|
|
8
|
+
assertOwnership(input.ownership);
|
|
9
|
+
if (!input.sessionId || !input.runId)
|
|
10
|
+
throw new PrismServerError("Run is unavailable", 404, "ERR_PRISM_SERVER_REPLAY");
|
|
11
|
+
if (input.cursor !== undefined)
|
|
12
|
+
assertCursor(input.cursor, limits);
|
|
13
|
+
return {
|
|
14
|
+
ownership: input.ownership,
|
|
15
|
+
sessionId: input.sessionId,
|
|
16
|
+
runId: input.runId,
|
|
17
|
+
after: input.cursor,
|
|
18
|
+
limit: limits.maxReplayEvents,
|
|
19
|
+
signal: input.signal,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
page: (input) => source.page(read(input)),
|
|
24
|
+
subscribe(input) {
|
|
25
|
+
return {
|
|
26
|
+
[Symbol.asyncIterator]: () => source.subscribe(read(input))[Symbol.asyncIterator](),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Ownership-scoped, cursor-paginated legacy persistence replay. Does not re-run work. */
|
|
32
|
+
export function createPrismEventReplay(store, options = {}) {
|
|
33
|
+
const limits = resolvePrismDeploymentLimits(options.limits);
|
|
34
|
+
return {
|
|
35
|
+
async page(input) {
|
|
36
|
+
input.signal?.throwIfAborted();
|
|
37
|
+
assertOwnership(input.ownership);
|
|
38
|
+
if (!input.sessionId || !input.runId) {
|
|
39
|
+
throw new PrismServerError("sessionId and runId are required", 400, "ERR_PRISM_SERVER_REPLAY");
|
|
40
|
+
}
|
|
41
|
+
if (input.cursor !== undefined)
|
|
42
|
+
assertCursor(input.cursor, limits);
|
|
43
|
+
const page = await store.queryEvents({
|
|
44
|
+
sessionId: input.sessionId,
|
|
45
|
+
runId: input.runId,
|
|
46
|
+
cursor: input.cursor,
|
|
47
|
+
limit: limits.maxReplayEvents,
|
|
48
|
+
order: "asc",
|
|
49
|
+
redacted: true,
|
|
50
|
+
...input.ownership,
|
|
51
|
+
});
|
|
52
|
+
if (page.items.length > limits.maxReplayEvents) {
|
|
53
|
+
throw new PrismServerError("Replay page exceeds limit", 507, "ERR_PRISM_SERVER_REPLAY_LIMIT");
|
|
54
|
+
}
|
|
55
|
+
if (page.items.some((record) => !record.redacted)) {
|
|
56
|
+
throw new PrismServerError("Replay page must be redacted", 500, "ERR_PRISM_SERVER_REPLAY");
|
|
57
|
+
}
|
|
58
|
+
if (page.nextCursor !== undefined)
|
|
59
|
+
assertCursor(page.nextCursor, limits);
|
|
60
|
+
return page;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Optional HTTP adapter: POST `{ sessionId, runId, cursor? }` → ownership-scoped page. */
|
|
65
|
+
export function createPrismReplayHandler(options) {
|
|
66
|
+
const base = (options.basePath ?? "/prism/replay/events").replace(/\/+$/, "");
|
|
67
|
+
const limits = resolvePrismDeploymentLimits(options.limits);
|
|
68
|
+
return async (request) => {
|
|
69
|
+
try {
|
|
70
|
+
if (request.method !== "POST") {
|
|
71
|
+
throw new PrismServerError("Method not allowed", 405, "ERR_PRISM_SERVER_METHOD");
|
|
72
|
+
}
|
|
73
|
+
const path = new URL(request.url).pathname.replace(/\/+$/, "");
|
|
74
|
+
if (path !== base)
|
|
75
|
+
throw new PrismServerError("Not found", 404, "ERR_PRISM_SERVER_NOT_FOUND");
|
|
76
|
+
const ownership = await options.authorize(request);
|
|
77
|
+
if (!ownership)
|
|
78
|
+
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
79
|
+
assertOwnership(ownership);
|
|
80
|
+
const body = await readSmallJson(request, limits.maxReplayCursorBytes + 1024);
|
|
81
|
+
const page = await options.replay.page({
|
|
82
|
+
ownership,
|
|
83
|
+
sessionId: readId(body.sessionId, "sessionId"),
|
|
84
|
+
runId: readId(body.runId, "runId"),
|
|
85
|
+
cursor: body.cursor === undefined ? undefined : readCursor(body.cursor, limits),
|
|
86
|
+
signal: request.signal,
|
|
87
|
+
});
|
|
88
|
+
return new Response(JSON.stringify(page), {
|
|
89
|
+
status: 200,
|
|
90
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof PrismServerError) {
|
|
95
|
+
return new Response(JSON.stringify({ error: { code: error.code, message: error.message } }), {
|
|
96
|
+
status: error.status,
|
|
97
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return new Response(JSON.stringify({ error: { code: "ERR_PRISM_SERVER", message: "Replay failed" } }), {
|
|
101
|
+
status: 500,
|
|
102
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function assertOwnership(ownership) {
|
|
108
|
+
if (![ownership.tenantId, ownership.accountId, ownership.userId].some((v) => typeof v === "string" && v.length > 0)) {
|
|
109
|
+
throw new PrismServerError("Forbidden", 403, "ERR_PRISM_SERVER_FORBIDDEN");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function assertCursor(cursor, limits) {
|
|
113
|
+
if (typeof cursor !== "string" || Buffer.byteLength(cursor, "utf8") > limits.maxReplayCursorBytes) {
|
|
114
|
+
throw new PrismServerError("Replay cursor exceeds limit", 400, "ERR_PRISM_SERVER_REPLAY_CURSOR");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function readCursor(value, limits) {
|
|
118
|
+
if (typeof value !== "string")
|
|
119
|
+
throw new PrismServerError("cursor must be a string", 400, "ERR_PRISM_SERVER_BODY");
|
|
120
|
+
assertCursor(value, limits);
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
function readId(value, name) {
|
|
124
|
+
if (typeof value !== "string" || !value || value.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(value)) {
|
|
125
|
+
throw new PrismServerError(`Invalid ${name}`, 400, "ERR_PRISM_SERVER_BODY");
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
async function readSmallJson(request, maxBytes) {
|
|
130
|
+
const text = await request.text();
|
|
131
|
+
if (Buffer.byteLength(text, "utf8") > maxBytes) {
|
|
132
|
+
throw new PrismServerError("Request body too large", 413, "ERR_PRISM_SERVER_BODY_LIMIT");
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const value = JSON.parse(text);
|
|
136
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
137
|
+
throw new Error("object");
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw new PrismServerError("Invalid JSON object body", 400, "ERR_PRISM_SERVER_BODY");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=replay.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { Agent, AgentRunLifecycle, AgentSession, OwnershipScope, RunOptions, SecretRedactor } from "@arnilo/prism";
|
|
1
|
+
import type { Agent, AgentEventSource, AgentIdentity, AgentRunLifecycle, AgentRunRef, AgentSession, OwnershipScope, RunOptions, SecretRedactor } from "@arnilo/prism";
|
|
2
2
|
import type { RunWorkflowOptions, WorkflowCheckpointAdapter, WorkflowDefinition, WorkflowSchedules } from "@arnilo/prism-workflows";
|
|
3
|
+
import type { PrismDrainController } from "./drain.js";
|
|
3
4
|
import type { PrismServerLimits } from "./limits.js";
|
|
4
|
-
|
|
5
|
+
import type { PrismServerRateLimiter } from "./rate-limit.js";
|
|
6
|
+
export type PrismServerOperation = "agent.run" | "agent.stream" | "agent.status" | "agent.resume" | "agent.events" | "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
7
|
export interface PrismServerAuthorization {
|
|
6
8
|
readonly ownership: OwnershipScope;
|
|
9
|
+
/** Host-verified identity; when set must project onto `ownership` without widening. */
|
|
10
|
+
readonly identity?: AgentIdentity;
|
|
7
11
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
8
12
|
}
|
|
9
13
|
export interface PrismServerAuthorizationInput {
|
|
@@ -13,9 +17,18 @@ export interface PrismServerAuthorizationInput {
|
|
|
13
17
|
readonly signal: AbortSignal;
|
|
14
18
|
}
|
|
15
19
|
export type PrismServerAuthorizer = (input: PrismServerAuthorizationInput) => false | PrismServerAuthorization | Promise<false | PrismServerAuthorization>;
|
|
20
|
+
export interface PrismAgentEventResolutionInput {
|
|
21
|
+
readonly runId: string;
|
|
22
|
+
readonly authorization: PrismServerAuthorization;
|
|
23
|
+
readonly signal: AbortSignal;
|
|
24
|
+
}
|
|
16
25
|
export interface PrismAgentExposure {
|
|
17
26
|
readonly sessionFactory: (authorization: PrismServerAuthorization) => AgentSession | Promise<AgentSession>;
|
|
18
27
|
readonly runOptions?: Omit<RunOptions, "ownership" | "signal" | "redactor">;
|
|
28
|
+
/** Optional durable cross-replica event source. Requires resolveRun. */
|
|
29
|
+
readonly events?: AgentEventSource;
|
|
30
|
+
/** Resolves an authorized public run selector to exact internal session/run IDs. */
|
|
31
|
+
readonly resolveRun?: (input: PrismAgentEventResolutionInput) => AgentRunRef | undefined | Promise<AgentRunRef | undefined>;
|
|
19
32
|
}
|
|
20
33
|
/** Explicit durable status/resume capability. Omit it to expose no agent lifecycle routes. */
|
|
21
34
|
export interface PrismAgentRunExposure {
|
|
@@ -40,10 +53,15 @@ export interface CreatePrismHandlerOptions {
|
|
|
40
53
|
readonly redactor?: SecretRedactor;
|
|
41
54
|
readonly limits?: PrismServerLimits;
|
|
42
55
|
readonly disconnectAborts?: boolean;
|
|
56
|
+
/** Optional graceful drain; blocks admit operations with 503 while draining. */
|
|
57
|
+
readonly drain?: PrismDrainController;
|
|
58
|
+
/** Optional host rate-limit adapter; runs after authorize, before admit/session create. */
|
|
59
|
+
readonly rateLimit?: PrismServerRateLimiter;
|
|
43
60
|
}
|
|
44
61
|
export type PrismRequestHandler = (request: Request) => Promise<Response>;
|
|
45
62
|
export declare class PrismServerError extends Error {
|
|
46
63
|
readonly status: number;
|
|
47
64
|
readonly code: string;
|
|
48
|
-
|
|
65
|
+
readonly headers?: Readonly<Record<string, string>> | undefined;
|
|
66
|
+
constructor(message: string, status?: number, code?: string, headers?: Readonly<Record<string, string>> | undefined);
|
|
49
67
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export class PrismServerError extends Error {
|
|
2
2
|
status;
|
|
3
3
|
code;
|
|
4
|
-
|
|
4
|
+
headers;
|
|
5
|
+
constructor(message, status = 500, code = "ERR_PRISM_SERVER", headers) {
|
|
5
6
|
super(message);
|
|
6
7
|
this.status = status;
|
|
7
8
|
this.code = code;
|
|
9
|
+
this.headers = headers;
|
|
8
10
|
this.name = "PrismServerError";
|
|
9
11
|
}
|
|
10
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism-server",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Optional framework-free Web Request-to-Response handler for explicitly selected Prism agents and workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./artifact-bodies": {
|
|
14
|
+
"types": "./dist/artifact-bodies.d.ts",
|
|
15
|
+
"default": "./dist/artifact-bodies.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
@@ -25,11 +29,12 @@
|
|
|
25
29
|
"pack:dry-run": "npm pack --dry-run"
|
|
26
30
|
},
|
|
27
31
|
"peerDependencies": {
|
|
28
|
-
"@arnilo/prism": "0.0
|
|
29
|
-
"@arnilo/prism-workflows": "0.0
|
|
32
|
+
"@arnilo/prism": "0.1.0",
|
|
33
|
+
"@arnilo/prism-workflows": "0.1.0"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
32
36
|
"@arnilo/prism": "file:../..",
|
|
37
|
+
"@arnilo/prism-session-store-sqlite": "file:../session-store-sqlite",
|
|
33
38
|
"@arnilo/prism-workflows": "file:../workflows"
|
|
34
39
|
},
|
|
35
40
|
"engines": {
|