@orkestrel/tool 0.0.6 → 0.0.8

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.
@@ -1,167 +0,0 @@
1
- import { StreamInterface } from '@orkestrel/server';
2
- import { TerminalManagerInterface } from '@orkestrel/terminal';
3
- import { TimerHandler } from '@orkestrel/terminal';
4
-
5
- /**
6
- * Build the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
7
- *
8
- * @remarks
9
- * Both routes share the configured `:name` path and optional token gate. The GET route replays
10
- * pending prompts, forwards live pending/expire events, and owns abort/keepalive teardown. The
11
- * POST route bounds the request body before parsing and maps answer outcomes to HTTP statuses.
12
- *
13
- * @param manager - The terminal manager whose endpoints are bridged
14
- * @param options - Route path, token, keepalive, timer, and body-limit options
15
- * @returns The GET route followed by the POST route
16
- *
17
- * @example
18
- * ```ts
19
- * import { createTerminalRoutes } from '@src/server'
20
- * import { createTerminalManager } from '@orkestrel/terminal'
21
- *
22
- * const manager = createTerminalManager()
23
- * manager.add('assistant')
24
- * const routes = createTerminalRoutes(manager, { token: 'secret' })
25
- * ```
26
- */
27
- export declare function createTerminalRoutes(manager: TerminalManagerInterface, options?: TerminalRoutesOptions): readonly TerminalRoute[];
28
-
29
- /** The HTTP method literal a {@link TerminalRoute} declares — the exact 7-literal union `@orkestrel/router`'s `Method` accepts. */
30
- export declare type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
31
-
32
- /**
33
- * The default SSE keepalive interval (in milliseconds)
34
- * {@link import('./factories.js').createTerminalRoutes} arms per open connection — a `: `
35
- * comment ping a conforming SSE parser ignores, keeping intermediary proxies from timing out an
36
- * otherwise-idle stream.
37
- */
38
- export declare const TERMINAL_KEEPALIVE_MS = 15000;
39
-
40
- /**
41
- * The default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
42
- * mounts its GET (SSE) + POST (answer) routes under.
43
- */
44
- export declare const TERMINAL_ROUTES_PATH = "/terminals/:name";
45
-
46
- /**
47
- * Own one terminal SSE connection's replay, subscriptions, keepalive, and teardown.
48
- *
49
- * @example
50
- * ```ts
51
- * import { TerminalConnection } from '@orkestrel/tool/server'
52
- *
53
- * const connection = new TerminalConnection(manager, name, request, stream, accepts, timer, 15_000)
54
- * const response = connection.open()
55
- * ```
56
- */
57
- export declare class TerminalConnection {
58
- #private;
59
- /**
60
- * Create a terminal stream connection.
61
- *
62
- * @param manager - Terminal manager supplying pending prompts and lifecycle events
63
- * @param name - Terminal endpoint streamed by this connection
64
- * @param request - Request whose abort signal owns the connection lifetime
65
- * @param stream - Open SSE stream
66
- * @param accepts - Presented-token validator
67
- * @param timer - Keepalive timer implementation
68
- * @param keepalive - Keepalive interval in milliseconds
69
- */
70
- constructor(manager: TerminalManagerInterface, name: string, request: Request, stream: StreamInterface, accepts: (presented: string | undefined) => boolean, timer: TimerHandler, keepalive: number);
71
- /**
72
- * Open the connection by replaying pending prompts, subscribing, and arming keepalive handling.
73
- *
74
- * @returns The SSE response
75
- */
76
- open(): Response;
77
- }
78
-
79
- /**
80
- * One structural route record {@link import('./factories.js').createTerminalRoutes} returns — a
81
- * plain `{ method, path, handler }` shape carrying NO dependency on `@orkestrel/router`'s own
82
- * `Route` type, so a consumer mounts it against any router that accepts a two-arg
83
- * `(request, context) => Response | Promise<Response>` handler keyed by `method` + `path`.
84
- */
85
- export declare interface TerminalRoute {
86
- readonly method: Method;
87
- readonly path: string;
88
- readonly handler: (request: Request, context: TerminalRouteContext) => Response | Promise<Response>;
89
- }
90
-
91
- /**
92
- * The minimal route-dispatch context a {@link TerminalRoute} handler reads — exactly the frozen,
93
- * URL-decoded `:name` path param slice a router hands a matched handler.
94
- */
95
- export declare interface TerminalRouteContext {
96
- readonly params: Readonly<Record<string, string>>;
97
- }
98
-
99
- /**
100
- * Build and serve the terminal manager's GET stream and POST answer routes.
101
- *
102
- * @example
103
- * ```ts
104
- * import { TerminalRoutes } from '@orkestrel/tool/server'
105
- *
106
- * const routes = new TerminalRoutes(manager).routes()
107
- * ```
108
- */
109
- export declare class TerminalRoutes {
110
- #private;
111
- /**
112
- * Create a terminal route owner.
113
- *
114
- * @param manager - Terminal manager bridged onto HTTP
115
- * @param options - Shared route, authorization, keepalive, timer, and body-limit options
116
- */
117
- constructor(manager: TerminalManagerInterface, options?: TerminalRoutesOptions);
118
- /**
119
- * Project the bound GET and POST route records.
120
- *
121
- * @returns The GET stream route followed by the POST answer route
122
- */
123
- routes(): readonly TerminalRoute[];
124
- }
125
-
126
- /**
127
- * Options for {@link import('./factories.js').createTerminalRoutes}.
128
- *
129
- * @remarks
130
- * - `path` — the shared `:name`-templated path both the GET (SSE) and POST (answer) routes
131
- * mount under; defaults to {@link import('./constants.js').TERMINAL_ROUTES_PATH}.
132
- * - `token` — a {@link TerminalToken}: a string is compared for equality against the
133
- * `x-orkestrel-token` header; a function receives the header's value (`undefined` when absent)
134
- * and returns whether it validates, letting the consumer roll/expire tokens out-of-band.
135
- * Validated at GET connect, on EVERY POST, and RE-VALIDATED on every keepalive tick of a live
136
- * SSE stream — a stream whose presented token stops validating (rotated, expired, revoked) is
137
- * torn down (the abort/self-heal teardown path, no `shutdown` frame) rather than left open
138
- * forever; the client reconnects and re-authenticates. Omitted ⇒ no auth check. Because
139
- * re-validation only happens on the keepalive tick, the revocation window equals the keepalive
140
- * interval — a token rejected/expired between ticks keeps streaming until the next one. A
141
- * validator function that THROWS is treated as rejection (fail-closed) at every call site.
142
- * - `keepalive` — the SSE comment-ping interval in milliseconds; defaults to
143
- * {@link import('./constants.js').TERMINAL_KEEPALIVE_MS}.
144
- * - `timer` — the injected {@link TimerHandler} driving the keepalive interval (default the host
145
- * `setTimeout`/`clearTimeout`), so a test drives the keepalive deterministically.
146
- * - `limit` — the maximum POST answer body size in bytes, streamed and enforced BEFORE JSON
147
- * parsing (ignoring any `Content-Length` header, so a lying header can never bypass the cap);
148
- * a body exceeding it is rejected `413` and `manager.answer` is never called. Defaults to
149
- * `@orkestrel/server`'s own `DEFAULT_BODY_LIMIT` (1 MiB).
150
- */
151
- export declare interface TerminalRoutesOptions {
152
- readonly path?: string;
153
- readonly token?: TerminalToken;
154
- readonly keepalive?: number;
155
- readonly timer?: TimerHandler;
156
- readonly limit?: number;
157
- }
158
-
159
- /**
160
- * The `token` gate a {@link TerminalRoutesOptions} may configure — a plain string compared for
161
- * equality against the `x-orkestrel-token` header, OR a validator function the consumer fully
162
- * controls, enabling expiry/rotation (a JWT `exp` check, a revocation-list lookup, anything
163
- * time-varying) that a fixed string cannot express. `undefined` disables the auth check entirely.
164
- */
165
- export declare type TerminalToken = string | ((value: string | undefined) => boolean);
166
-
167
- export { }
@@ -1,262 +0,0 @@
1
- import { HEADER_TOKEN, defaultTimer, isAnswerPayload, serializeExpire, serializePending } from "@orkestrel/terminal";
2
- import { ContentTooLargeError, DEFAULT_BODY_LIMIT, collectRequestBody, openStream } from "@orkestrel/server";
3
- //#region src/server/constants.ts
4
- /**
5
- * The default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
6
- * mounts its GET (SSE) + POST (answer) routes under.
7
- */
8
- var TERMINAL_ROUTES_PATH = "/terminals/:name";
9
- /**
10
- * The default SSE keepalive interval (in milliseconds)
11
- * {@link import('./factories.js').createTerminalRoutes} arms per open connection — a `: `
12
- * comment ping a conforming SSE parser ignores, keeping intermediary proxies from timing out an
13
- * otherwise-idle stream.
14
- */
15
- var TERMINAL_KEEPALIVE_MS = 15e3;
16
- //#endregion
17
- //#region src/server/routes/TerminalConnection.ts
18
- /**
19
- * Own one terminal SSE connection's replay, subscriptions, keepalive, and teardown.
20
- *
21
- * @example
22
- * ```ts
23
- * import { TerminalConnection } from '@orkestrel/tool/server'
24
- *
25
- * const connection = new TerminalConnection(manager, name, request, stream, accepts, timer, 15_000)
26
- * const response = connection.open()
27
- * ```
28
- */
29
- var TerminalConnection = class {
30
- #manager;
31
- #name;
32
- #request;
33
- #stream;
34
- #accepts;
35
- #timer;
36
- #keepalive;
37
- #presented;
38
- #cancel;
39
- #destroyHandler;
40
- #pendingHandler;
41
- #expireHandler;
42
- #tickHandler;
43
- /**
44
- * Create a terminal stream connection.
45
- *
46
- * @param manager - Terminal manager supplying pending prompts and lifecycle events
47
- * @param name - Terminal endpoint streamed by this connection
48
- * @param request - Request whose abort signal owns the connection lifetime
49
- * @param stream - Open SSE stream
50
- * @param accepts - Presented-token validator
51
- * @param timer - Keepalive timer implementation
52
- * @param keepalive - Keepalive interval in milliseconds
53
- */
54
- constructor(manager, name, request, stream, accepts, timer, keepalive) {
55
- this.#manager = manager;
56
- this.#name = name;
57
- this.#request = request;
58
- this.#stream = stream;
59
- this.#accepts = accepts;
60
- this.#timer = timer;
61
- this.#keepalive = keepalive;
62
- this.#presented = request.headers.get(HEADER_TOKEN) ?? void 0;
63
- this.#destroyHandler = this.#destroy.bind(this);
64
- this.#pendingHandler = this.#pending.bind(this);
65
- this.#expireHandler = this.#expire.bind(this);
66
- this.#tickHandler = this.#tick.bind(this);
67
- }
68
- /**
69
- * Open the connection by replaying pending prompts, subscribing, and arming keepalive handling.
70
- *
71
- * @returns The SSE response
72
- */
73
- open() {
74
- if (this.#stream.closed || this.#request.signal.aborted) {
75
- this.#destroy();
76
- return this.#stream.response;
77
- }
78
- if (this.#cancel !== void 0) return this.#stream.response;
79
- for (const prompt of this.#manager.pending(this.#name)) this.#write(serializePending(prompt));
80
- this.#manager.emitter.on("pending", this.#pendingHandler);
81
- this.#manager.emitter.on("expire", this.#expireHandler);
82
- this.#cancel = this.#timer(this.#tickHandler, this.#keepalive);
83
- this.#request.signal.addEventListener("abort", this.#destroyHandler);
84
- return this.#stream.response;
85
- }
86
- #write(wire) {
87
- this.#stream.write({
88
- event: wire.event,
89
- data: wire.data,
90
- ...wire.id === void 0 ? {} : { id: wire.id }
91
- });
92
- }
93
- #pending(prompt) {
94
- if (prompt.to !== this.#name) return;
95
- if (this.#stream.closed) {
96
- this.#destroy();
97
- return;
98
- }
99
- this.#write(serializePending(prompt));
100
- }
101
- #expire(to, id) {
102
- if (to !== this.#name) return;
103
- if (this.#stream.closed) {
104
- this.#destroy();
105
- return;
106
- }
107
- this.#write(serializeExpire(id));
108
- }
109
- #tick() {
110
- if (this.#stream.closed || !this.#accepted()) {
111
- this.#destroy();
112
- return;
113
- }
114
- this.#stream.comment("");
115
- this.#cancel = this.#timer(this.#tickHandler, this.#keepalive);
116
- }
117
- #accepted() {
118
- try {
119
- return this.#accepts(this.#presented);
120
- } catch {
121
- return false;
122
- }
123
- }
124
- #destroy() {
125
- const cancel = this.#cancel;
126
- this.#cancel = void 0;
127
- cancel?.();
128
- this.#manager.emitter.off("pending", this.#pendingHandler);
129
- this.#manager.emitter.off("expire", this.#expireHandler);
130
- this.#request.signal.removeEventListener("abort", this.#destroyHandler);
131
- this.#stream.end();
132
- }
133
- };
134
- //#endregion
135
- //#region src/server/routes/TerminalRoutes.ts
136
- /**
137
- * Build and serve the terminal manager's GET stream and POST answer routes.
138
- *
139
- * @example
140
- * ```ts
141
- * import { TerminalRoutes } from '@orkestrel/tool/server'
142
- *
143
- * const routes = new TerminalRoutes(manager).routes()
144
- * ```
145
- */
146
- var TerminalRoutes = class {
147
- #manager;
148
- #path;
149
- #token;
150
- #keepalive;
151
- #timer;
152
- #limit;
153
- #accepts;
154
- #get;
155
- #post;
156
- /**
157
- * Create a terminal route owner.
158
- *
159
- * @param manager - Terminal manager bridged onto HTTP
160
- * @param options - Shared route, authorization, keepalive, timer, and body-limit options
161
- */
162
- constructor(manager, options) {
163
- this.#manager = manager;
164
- this.#path = options?.path ?? "/terminals/:name";
165
- this.#token = options?.token;
166
- this.#keepalive = options?.keepalive ?? 15e3;
167
- this.#timer = options?.timer ?? defaultTimer;
168
- const limit = options?.limit;
169
- this.#limit = limit === void 0 || !Number.isFinite(limit) ? DEFAULT_BODY_LIMIT : Math.max(0, Math.floor(limit));
170
- this.#accepts = this.#valid.bind(this);
171
- this.#get = this.#handleGet.bind(this);
172
- this.#post = this.#handlePost.bind(this);
173
- }
174
- /**
175
- * Project the bound GET and POST route records.
176
- *
177
- * @returns The GET stream route followed by the POST answer route
178
- */
179
- routes() {
180
- return [{
181
- method: "GET",
182
- path: this.#path,
183
- handler: this.#get
184
- }, {
185
- method: "POST",
186
- path: this.#path,
187
- handler: this.#post
188
- }];
189
- }
190
- #valid(presented) {
191
- if (this.#token === void 0) return true;
192
- try {
193
- return typeof this.#token === "function" ? this.#token(presented) : presented === this.#token;
194
- } catch {
195
- return false;
196
- }
197
- }
198
- #authorized(request) {
199
- return this.#valid(request.headers.get(HEADER_TOKEN) ?? void 0);
200
- }
201
- #handleGet(request, context) {
202
- if (!this.#authorized(request)) return new Response(null, { status: 401 });
203
- const name = context.params.name;
204
- if (name === void 0 || this.#manager.terminal(name) === void 0) return new Response(null, { status: 404 });
205
- return new TerminalConnection(this.#manager, name, request, openStream(), this.#accepts, this.#timer, this.#keepalive).open();
206
- }
207
- async #handlePost(request, context) {
208
- if (!this.#authorized(request)) return new Response(null, { status: 401 });
209
- const name = context.params.name;
210
- if (name === void 0 || this.#manager.terminal(name) === void 0) return new Response(null, { status: 404 });
211
- let bytes;
212
- try {
213
- bytes = await collectRequestBody(request, Math.max(1, this.#limit));
214
- } catch (error) {
215
- if (error instanceof ContentTooLargeError) return new Response(null, { status: 413 });
216
- throw error;
217
- }
218
- if (bytes.byteLength > 0 && bytes.byteLength > this.#limit) return new Response(null, { status: 413 });
219
- let body;
220
- try {
221
- body = JSON.parse(new TextDecoder().decode(bytes));
222
- } catch {
223
- return new Response(null, { status: 400 });
224
- }
225
- if (!isAnswerPayload(body)) return new Response(null, { status: 422 });
226
- const result = this.#manager.answer(name, body.id, body.value);
227
- if (result.success) return new Response(null, { status: 204 });
228
- if (result.error === "terminal") return new Response(result.error, { status: 404 });
229
- return new Response(result.error, { status: 422 });
230
- }
231
- };
232
- //#endregion
233
- //#region src/server/factories.ts
234
- /**
235
- * Build the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
236
- *
237
- * @remarks
238
- * Both routes share the configured `:name` path and optional token gate. The GET route replays
239
- * pending prompts, forwards live pending/expire events, and owns abort/keepalive teardown. The
240
- * POST route bounds the request body before parsing and maps answer outcomes to HTTP statuses.
241
- *
242
- * @param manager - The terminal manager whose endpoints are bridged
243
- * @param options - Route path, token, keepalive, timer, and body-limit options
244
- * @returns The GET route followed by the POST route
245
- *
246
- * @example
247
- * ```ts
248
- * import { createTerminalRoutes } from '@src/server'
249
- * import { createTerminalManager } from '@orkestrel/terminal'
250
- *
251
- * const manager = createTerminalManager()
252
- * manager.add('assistant')
253
- * const routes = createTerminalRoutes(manager, { token: 'secret' })
254
- * ```
255
- */
256
- function createTerminalRoutes(manager, options) {
257
- return new TerminalRoutes(manager, options).routes();
258
- }
259
- //#endregion
260
- export { TERMINAL_KEEPALIVE_MS, TERMINAL_ROUTES_PATH, TerminalConnection, TerminalRoutes, createTerminalRoutes };
261
-
262
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":["#manager","#name","#request","#stream","#accepts","#timer","#keepalive","#presented","#destroyHandler","#pendingHandler","#expireHandler","#tickHandler","#destroy","#pending","#expire","#tick","#cancel","#write","#accepted","#manager","#path","#token","#keepalive","#timer","#limit","#accepts","#get","#post","#valid","#handleGet","#handlePost","#authorized"],"sources":["../../../src/server/constants.ts","../../../src/server/routes/TerminalConnection.ts","../../../src/server/routes/TerminalRoutes.ts","../../../src/server/factories.ts"],"sourcesContent":["// Server-package constants — UPPER_SNAKE, `Object.freeze`d where structural, every member\n// exported (AGENTS §5).\n\n/**\n * The default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}\n * mounts its GET (SSE) + POST (answer) routes under.\n */\nexport const TERMINAL_ROUTES_PATH = '/terminals/:name'\n\n/**\n * The default SSE keepalive interval (in milliseconds)\n * {@link import('./factories.js').createTerminalRoutes} arms per open connection — a `: `\n * comment ping a conforming SSE parser ignores, keeping intermediary proxies from timing out an\n * otherwise-idle stream.\n */\nexport const TERMINAL_KEEPALIVE_MS = 15_000\n","import type {\n\tPendingPrompt,\n\tTerminalManagerInterface,\n\tTimerCancel,\n\tTimerHandler,\n\tWireEvent,\n} from '@orkestrel/terminal'\nimport type { StreamInterface } from '@orkestrel/server'\nimport { HEADER_TOKEN, serializeExpire, serializePending } from '@orkestrel/terminal'\n\n/**\n * Own one terminal SSE connection's replay, subscriptions, keepalive, and teardown.\n *\n * @example\n * ```ts\n * import { TerminalConnection } from '@orkestrel/tool/server'\n *\n * const connection = new TerminalConnection(manager, name, request, stream, accepts, timer, 15_000)\n * const response = connection.open()\n * ```\n */\nexport class TerminalConnection {\n\treadonly #manager: TerminalManagerInterface\n\treadonly #name: string\n\treadonly #request: Request\n\treadonly #stream: StreamInterface\n\treadonly #accepts: (presented: string | undefined) => boolean\n\treadonly #timer: TimerHandler\n\treadonly #keepalive: number\n\treadonly #presented: string | undefined\n\t#cancel: TimerCancel | undefined\n\treadonly #destroyHandler: () => void\n\treadonly #pendingHandler: (prompt: PendingPrompt) => void\n\treadonly #expireHandler: (to: string, id: string) => void\n\treadonly #tickHandler: () => void\n\n\t/**\n\t * Create a terminal stream connection.\n\t *\n\t * @param manager - Terminal manager supplying pending prompts and lifecycle events\n\t * @param name - Terminal endpoint streamed by this connection\n\t * @param request - Request whose abort signal owns the connection lifetime\n\t * @param stream - Open SSE stream\n\t * @param accepts - Presented-token validator\n\t * @param timer - Keepalive timer implementation\n\t * @param keepalive - Keepalive interval in milliseconds\n\t */\n\tconstructor(\n\t\tmanager: TerminalManagerInterface,\n\t\tname: string,\n\t\trequest: Request,\n\t\tstream: StreamInterface,\n\t\taccepts: (presented: string | undefined) => boolean,\n\t\ttimer: TimerHandler,\n\t\tkeepalive: number,\n\t) {\n\t\tthis.#manager = manager\n\t\tthis.#name = name\n\t\tthis.#request = request\n\t\tthis.#stream = stream\n\t\tthis.#accepts = accepts\n\t\tthis.#timer = timer\n\t\tthis.#keepalive = keepalive\n\t\tthis.#presented = request.headers.get(HEADER_TOKEN) ?? undefined\n\t\tthis.#destroyHandler = this.#destroy.bind(this)\n\t\tthis.#pendingHandler = this.#pending.bind(this)\n\t\tthis.#expireHandler = this.#expire.bind(this)\n\t\tthis.#tickHandler = this.#tick.bind(this)\n\t}\n\n\t/**\n\t * Open the connection by replaying pending prompts, subscribing, and arming keepalive handling.\n\t *\n\t * @returns The SSE response\n\t */\n\topen(): Response {\n\t\tif (this.#stream.closed || this.#request.signal.aborted) {\n\t\t\tthis.#destroy()\n\t\t\treturn this.#stream.response\n\t\t}\n\t\tif (this.#cancel !== undefined) return this.#stream.response\n\t\tfor (const prompt of this.#manager.pending(this.#name)) {\n\t\t\tthis.#write(serializePending(prompt))\n\t\t}\n\t\tthis.#manager.emitter.on('pending', this.#pendingHandler)\n\t\tthis.#manager.emitter.on('expire', this.#expireHandler)\n\t\tthis.#cancel = this.#timer(this.#tickHandler, this.#keepalive)\n\t\tthis.#request.signal.addEventListener('abort', this.#destroyHandler)\n\t\treturn this.#stream.response\n\t}\n\n\t#write(wire: WireEvent): void {\n\t\tthis.#stream.write({\n\t\t\tevent: wire.event,\n\t\t\tdata: wire.data,\n\t\t\t...(wire.id === undefined ? {} : { id: wire.id }),\n\t\t})\n\t}\n\n\t#pending(prompt: PendingPrompt): void {\n\t\tif (prompt.to !== this.#name) return\n\t\tif (this.#stream.closed) {\n\t\t\tthis.#destroy()\n\t\t\treturn\n\t\t}\n\t\tthis.#write(serializePending(prompt))\n\t}\n\n\t#expire(to: string, id: string): void {\n\t\tif (to !== this.#name) return\n\t\tif (this.#stream.closed) {\n\t\t\tthis.#destroy()\n\t\t\treturn\n\t\t}\n\t\tthis.#write(serializeExpire(id))\n\t}\n\n\t#tick(): void {\n\t\tif (this.#stream.closed || !this.#accepted()) {\n\t\t\tthis.#destroy()\n\t\t\treturn\n\t\t}\n\t\tthis.#stream.comment('')\n\t\tthis.#cancel = this.#timer(this.#tickHandler, this.#keepalive)\n\t}\n\n\t#accepted(): boolean {\n\t\ttry {\n\t\t\treturn this.#accepts(this.#presented)\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t#destroy(): void {\n\t\tconst cancel = this.#cancel\n\t\tthis.#cancel = undefined\n\t\tcancel?.()\n\t\tthis.#manager.emitter.off('pending', this.#pendingHandler)\n\t\tthis.#manager.emitter.off('expire', this.#expireHandler)\n\t\tthis.#request.signal.removeEventListener('abort', this.#destroyHandler)\n\t\tthis.#stream.end()\n\t}\n}\n","import type { TerminalManagerInterface, TimerHandler } from '@orkestrel/terminal'\nimport type {\n\tTerminalRoute,\n\tTerminalRouteContext,\n\tTerminalRoutesOptions,\n\tTerminalToken,\n} from '../types.js'\nimport { defaultTimer, HEADER_TOKEN, isAnswerPayload } from '@orkestrel/terminal'\nimport {\n\tcollectRequestBody,\n\tContentTooLargeError,\n\tDEFAULT_BODY_LIMIT,\n\topenStream,\n} from '@orkestrel/server'\nimport { TERMINAL_KEEPALIVE_MS, TERMINAL_ROUTES_PATH } from '../constants.js'\nimport { TerminalConnection } from './TerminalConnection.js'\n\n/**\n * Build and serve the terminal manager's GET stream and POST answer routes.\n *\n * @example\n * ```ts\n * import { TerminalRoutes } from '@orkestrel/tool/server'\n *\n * const routes = new TerminalRoutes(manager).routes()\n * ```\n */\nexport class TerminalRoutes {\n\treadonly #manager: TerminalManagerInterface\n\treadonly #path: string\n\treadonly #token: TerminalToken | undefined\n\treadonly #keepalive: number\n\treadonly #timer: TimerHandler\n\treadonly #limit: number\n\treadonly #accepts: (presented: string | undefined) => boolean\n\treadonly #get: TerminalRoute['handler']\n\treadonly #post: TerminalRoute['handler']\n\n\t/**\n\t * Create a terminal route owner.\n\t *\n\t * @param manager - Terminal manager bridged onto HTTP\n\t * @param options - Shared route, authorization, keepalive, timer, and body-limit options\n\t */\n\tconstructor(manager: TerminalManagerInterface, options?: TerminalRoutesOptions) {\n\t\tthis.#manager = manager\n\t\tthis.#path = options?.path ?? TERMINAL_ROUTES_PATH\n\t\tthis.#token = options?.token\n\t\tthis.#keepalive = options?.keepalive ?? TERMINAL_KEEPALIVE_MS\n\t\tthis.#timer = options?.timer ?? defaultTimer\n\t\tconst limit = options?.limit\n\t\tthis.#limit =\n\t\t\tlimit === undefined || !Number.isFinite(limit)\n\t\t\t\t? DEFAULT_BODY_LIMIT\n\t\t\t\t: Math.max(0, Math.floor(limit))\n\t\tthis.#accepts = this.#valid.bind(this)\n\t\tthis.#get = this.#handleGet.bind(this)\n\t\tthis.#post = this.#handlePost.bind(this)\n\t}\n\n\t/**\n\t * Project the bound GET and POST route records.\n\t *\n\t * @returns The GET stream route followed by the POST answer route\n\t */\n\troutes(): readonly TerminalRoute[] {\n\t\treturn [\n\t\t\t{ method: 'GET', path: this.#path, handler: this.#get },\n\t\t\t{ method: 'POST', path: this.#path, handler: this.#post },\n\t\t]\n\t}\n\n\t#valid(presented: string | undefined): boolean {\n\t\tif (this.#token === undefined) return true\n\t\ttry {\n\t\t\treturn typeof this.#token === 'function' ? this.#token(presented) : presented === this.#token\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t#authorized(request: Request): boolean {\n\t\treturn this.#valid(request.headers.get(HEADER_TOKEN) ?? undefined)\n\t}\n\n\t#handleGet(request: Request, context: TerminalRouteContext): Response {\n\t\tif (!this.#authorized(request)) return new Response(null, { status: 401 })\n\t\tconst name = context.params.name\n\t\tif (name === undefined || this.#manager.terminal(name) === undefined) {\n\t\t\treturn new Response(null, { status: 404 })\n\t\t}\n\t\tconst connection = new TerminalConnection(\n\t\t\tthis.#manager,\n\t\t\tname,\n\t\t\trequest,\n\t\t\topenStream(),\n\t\t\tthis.#accepts,\n\t\t\tthis.#timer,\n\t\t\tthis.#keepalive,\n\t\t)\n\t\treturn connection.open()\n\t}\n\n\tasync #handlePost(request: Request, context: TerminalRouteContext): Promise<Response> {\n\t\tif (!this.#authorized(request)) return new Response(null, { status: 401 })\n\t\tconst name = context.params.name\n\t\tif (name === undefined || this.#manager.terminal(name) === undefined) {\n\t\t\treturn new Response(null, { status: 404 })\n\t\t}\n\t\tlet bytes: Uint8Array\n\t\ttry {\n\t\t\tbytes = await collectRequestBody(request, Math.max(1, this.#limit))\n\t\t} catch (error) {\n\t\t\tif (error instanceof ContentTooLargeError) return new Response(null, { status: 413 })\n\t\t\tthrow error\n\t\t}\n\t\tif (bytes.byteLength > 0 && bytes.byteLength > this.#limit) {\n\t\t\treturn new Response(null, { status: 413 })\n\t\t}\n\n\t\tlet body: unknown\n\t\ttry {\n\t\t\tbody = JSON.parse(new TextDecoder().decode(bytes))\n\t\t} catch {\n\t\t\treturn new Response(null, { status: 400 })\n\t\t}\n\t\tif (!isAnswerPayload(body)) return new Response(null, { status: 422 })\n\n\t\tconst result = this.#manager.answer(name, body.id, body.value)\n\t\tif (result.success) return new Response(null, { status: 204 })\n\t\tif (result.error === 'terminal') return new Response(result.error, { status: 404 })\n\t\treturn new Response(result.error, { status: 422 })\n\t}\n}\n","import type { TerminalManagerInterface } from '@orkestrel/terminal'\nimport type { TerminalRoute, TerminalRoutesOptions } from './types.js'\nimport { TerminalRoutes } from './routes/TerminalRoutes.js'\n\n/**\n * Build the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.\n *\n * @remarks\n * Both routes share the configured `:name` path and optional token gate. The GET route replays\n * pending prompts, forwards live pending/expire events, and owns abort/keepalive teardown. The\n * POST route bounds the request body before parsing and maps answer outcomes to HTTP statuses.\n *\n * @param manager - The terminal manager whose endpoints are bridged\n * @param options - Route path, token, keepalive, timer, and body-limit options\n * @returns The GET route followed by the POST route\n *\n * @example\n * ```ts\n * import { createTerminalRoutes } from '@src/server'\n * import { createTerminalManager } from '@orkestrel/terminal'\n *\n * const manager = createTerminalManager()\n * manager.add('assistant')\n * const routes = createTerminalRoutes(manager, { token: 'secret' })\n * ```\n */\nexport function createTerminalRoutes(\n\tmanager: TerminalManagerInterface,\n\toptions?: TerminalRoutesOptions,\n): readonly TerminalRoute[] {\n\treturn new TerminalRoutes(manager, options).routes()\n}\n"],"mappings":"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"}