@orkestrel/toolbox 0.0.11 → 0.0.13

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.
@@ -5,24 +5,22 @@ let _orkestrel_contract = require("@orkestrel/contract");
5
5
  let _orkestrel_server = require("@orkestrel/server");
6
6
  //#region src/server/constants.ts
7
7
  /**
8
- * The default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
9
- * mounts its GET (SSE) + POST (answer) routes under.
8
+ * Holds the default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
9
+ * mounts its GET (SSE) and POST (answer) routes under, `/terminals/:name`.
10
10
  */
11
11
  var TERMINAL_ROUTES_PATH = "/terminals/:name";
12
12
  /**
13
- * The default SSE keepalive interval (in milliseconds)
14
- * {@link import('./factories.js').createTerminalRoutes} arms per open connection — a `: `
15
- * comment ping a conforming SSE parser ignores, keeping intermediary proxies from timing out an
16
- * otherwise-idle stream.
13
+ * Holds the default SSE keepalive interval {@link import('./factories.js').createTerminalRoutes}
14
+ * arms per open connection, `15_000` ms — a `: ` comment ping a conforming SSE parser ignores,
15
+ * keeping intermediary proxies from timing out an otherwise-idle stream.
17
16
  */
18
17
  var TERMINAL_KEEPALIVE_MS = 15e3;
19
18
  //#endregion
20
- //#region src/server/routes/TerminalConnection.ts
19
+ //#region src/server/terminals/TerminalConnection.ts
21
20
  /**
22
- * Own one terminal SSE connection's replay, subscriptions, keepalive, and teardown.
21
+ * Owns one terminal SSE connection's replay, subscriptions, keepalive, and teardown.
23
22
  *
24
- * Internal to this module: `TerminalRoutes` builds the `accepts` predicate from its own token
25
- * option and opens the stream, so no consumer can construct one. Reach this behaviour through
23
+ * Not exported from the `@orkestrel/toolbox/server` barrel — reach this behaviour through
26
24
  * `createTerminalRoutes`.
27
25
  */
28
26
  var TerminalConnection = class {
@@ -40,7 +38,7 @@ var TerminalConnection = class {
40
38
  #expireHandler;
41
39
  #tickHandler;
42
40
  /**
43
- * Create a terminal stream connection.
41
+ * Creates a terminal stream connection.
44
42
  *
45
43
  * @param manager - Terminal manager supplying pending forms and lifecycle events
46
44
  * @param name - Terminal endpoint streamed by this connection
@@ -65,7 +63,7 @@ var TerminalConnection = class {
65
63
  this.#tickHandler = this.#tick.bind(this);
66
64
  }
67
65
  /**
68
- * Open the connection by replaying pending forms, subscribing, and arming keepalive handling.
66
+ * Opens the connection by replaying pending forms, subscribing, and arming keepalive handling.
69
67
  *
70
68
  * @returns The SSE response
71
69
  */
@@ -131,18 +129,15 @@ var TerminalConnection = class {
131
129
  }
132
130
  };
133
131
  //#endregion
134
- //#region src/server/routes/TerminalRoutes.ts
132
+ //#region src/server/terminals/TerminalBridge.ts
135
133
  /**
136
- * Build and serve the terminal manager's GET stream and POST answer routes.
134
+ * Bridges a terminal manager onto the wire — owns the shared route options, the token gate, and
135
+ * the bound GET stream and POST answer handlers.
137
136
  *
138
- * @example
139
- * ```ts
140
- * import { TerminalRoutes } from '@orkestrel/toolbox/server'
141
- *
142
- * const routes = new TerminalRoutes(manager).routes()
143
- * ```
137
+ * Not exported from the `@orkestrel/toolbox/server` barrel — reach this behaviour through
138
+ * `createTerminalRoutes`.
144
139
  */
145
- var TerminalRoutes = class {
140
+ var TerminalBridge = class {
146
141
  #manager;
147
142
  #path;
148
143
  #token;
@@ -153,7 +148,7 @@ var TerminalRoutes = class {
153
148
  #get;
154
149
  #post;
155
150
  /**
156
- * Create a terminal route owner.
151
+ * Creates a terminal bridge.
157
152
  *
158
153
  * @param manager - Terminal manager bridged onto HTTP
159
154
  * @param options - Shared route, authorization, keepalive, timer, and body-limit options
@@ -171,7 +166,7 @@ var TerminalRoutes = class {
171
166
  this.#post = this.#handlePost.bind(this);
172
167
  }
173
168
  /**
174
- * Project the bound GET and POST route records.
169
+ * Projects the bound GET and POST route records.
175
170
  *
176
171
  * @returns The GET stream route followed by the POST answer route
177
172
  */
@@ -201,7 +196,7 @@ var TerminalRoutes = class {
201
196
  if (!this.#authorized(request)) return new Response(null, { status: 401 });
202
197
  const name = context.params.name;
203
198
  if (name === void 0 || this.#manager.terminal(name) === void 0) return new Response(null, { status: 404 });
204
- return new TerminalConnection(this.#manager, name, request, (0, _orkestrel_server.openStream)(), this.#accepts, this.#timer, this.#keepalive).open();
199
+ return new TerminalConnection(this.#manager, name, request, (0, _orkestrel_server.createStream)(), this.#accepts, this.#timer, this.#keepalive).open();
205
200
  }
206
201
  async #handlePost(request, context) {
207
202
  if (!this.#authorized(request)) return new Response(null, { status: 401 });
@@ -224,16 +219,20 @@ var TerminalRoutes = class {
224
219
  if (!(0, _orkestrel_contract.isRecord)(body) || !(0, _orkestrel_contract.isNonEmptyString)(body.id) || !(0, _orkestrel_form.isFormValues)(body.values)) return new Response(null, { status: 422 });
225
220
  const result = this.#manager.answer(name, body.id, body.values);
226
221
  if (result.success) return Response.json(result);
227
- if (result.error.reason === "terminal") return Response.json(result, { status: 404 });
222
+ if (result.error.reason === "target") return Response.json(result, { status: 404 });
228
223
  return Response.json(result, { status: 422 });
229
224
  }
230
225
  };
231
226
  //#endregion
232
227
  //#region src/server/factories.ts
233
228
  /**
234
- * Build the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
229
+ * Builds the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
235
230
  *
236
231
  * @remarks
232
+ * The pair stays byte-compatible with `@orkestrel/terminal`'s own `PromptClient`: the same GET url
233
+ * streams, the same POST url answers, the same `{ id, values }` request body, the same JSON answer
234
+ * `Result` response body, and the same `x-orkestrel-token` header.
235
+ *
237
236
  * Both routes share the configured `:name` path and optional token gate. The GET route replays
238
237
  * pending prompts, forwards live pending/expire events, and owns abort/keepalive teardown. The
239
238
  * POST route bounds the request body before parsing and maps answer outcomes to HTTP statuses.
@@ -253,12 +252,11 @@ var TerminalRoutes = class {
253
252
  * ```
254
253
  */
255
254
  function createTerminalRoutes(manager, options) {
256
- return new TerminalRoutes(manager, options).routes();
255
+ return new TerminalBridge(manager, options).routes();
257
256
  }
258
257
  //#endregion
259
258
  exports.TERMINAL_KEEPALIVE_MS = TERMINAL_KEEPALIVE_MS;
260
259
  exports.TERMINAL_ROUTES_PATH = TERMINAL_ROUTES_PATH;
261
- exports.TerminalRoutes = TerminalRoutes;
262
260
  exports.createTerminalRoutes = createTerminalRoutes;
263
261
 
264
262
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","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\tPendingForm,\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 * Internal to this module: `TerminalRoutes` builds the `accepts` predicate from its own token\n * option and opens the stream, so no consumer can construct one. Reach this behaviour through\n * `createTerminalRoutes`.\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: (form: PendingForm) => 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 forms 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 forms, 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 form of this.#manager.pending(this.#name)) {\n\t\t\tthis.#write(serializePending(form))\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(form: PendingForm): void {\n\t\tif (form.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(form))\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 } from '@orkestrel/terminal'\nimport { isFormValues } from '@orkestrel/form'\nimport { isNonEmptyString, isRecord } from '@orkestrel/contract'\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/toolbox/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 (!isRecord(body) || !isNonEmptyString(body.id) || !isFormValues(body.values)) {\n\t\t\treturn new Response(null, { status: 422 })\n\t\t}\n\n\t\tconst result = this.#manager.answer(name, body.id, body.values)\n\t\tif (result.success) return Response.json(result)\n\t\tif (result.error.reason === 'terminal') return Response.json(result, { status: 404 })\n\t\treturn Response.json(result, { 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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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/terminals/TerminalConnection.ts","../../../src/server/terminals/TerminalBridge.ts","../../../src/server/factories.ts"],"sourcesContent":["// Server-package constants — UPPER_SNAKE, `Object.freeze`d where structural, every member\n// exported.\n\n/**\n * Holds the default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}\n * mounts its GET (SSE) and POST (answer) routes under, `/terminals/:name`.\n */\nexport const TERMINAL_ROUTES_PATH = '/terminals/:name'\n\n/**\n * Holds the default SSE keepalive interval {@link import('./factories.js').createTerminalRoutes}\n * arms per open connection, `15_000` ms — a `: ` comment ping a conforming SSE parser ignores,\n * keeping intermediary proxies from timing out an otherwise-idle stream.\n */\nexport const TERMINAL_KEEPALIVE_MS = 15_000\n","import type {\n\tPendingForm,\n\tTerminalManagerInterface,\n\tTimerCancelFunction,\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 * Owns one terminal SSE connection's replay, subscriptions, keepalive, and teardown.\n *\n * Not exported from the `@orkestrel/toolbox/server` barrel — reach this behaviour through\n * `createTerminalRoutes`.\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: TimerCancelFunction | undefined\n\treadonly #destroyHandler: () => void\n\treadonly #pendingHandler: (form: PendingForm) => void\n\treadonly #expireHandler: (to: string, id: string) => void\n\treadonly #tickHandler: () => void\n\n\t/**\n\t * Creates a terminal stream connection.\n\t *\n\t * @param manager - Terminal manager supplying pending forms 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 * Opens the connection by replaying pending forms, 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 form of this.#manager.pending(this.#name)) {\n\t\t\tthis.#write(serializePending(form))\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(form: PendingForm): void {\n\t\tif (form.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(form))\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 } from '@orkestrel/terminal'\nimport { isFormValues } from '@orkestrel/form'\nimport { isNonEmptyString, isRecord } from '@orkestrel/contract'\nimport {\n\tcollectRequestBody,\n\tContentTooLargeError,\n\tcreateStream,\n\tDEFAULT_BODY_LIMIT,\n} from '@orkestrel/server'\nimport { TERMINAL_KEEPALIVE_MS, TERMINAL_ROUTES_PATH } from '../constants.js'\nimport { TerminalConnection } from './TerminalConnection.js'\n\n/**\n * Bridges a terminal manager onto the wire — owns the shared route options, the token gate, and\n * the bound GET stream and POST answer handlers.\n *\n * Not exported from the `@orkestrel/toolbox/server` barrel — reach this behaviour through\n * `createTerminalRoutes`.\n */\nexport class TerminalBridge {\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 * Creates a terminal bridge.\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 * Projects 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\tcreateStream(),\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 (!isRecord(body) || !isNonEmptyString(body.id) || !isFormValues(body.values)) {\n\t\t\treturn new Response(null, { status: 422 })\n\t\t}\n\n\t\tconst result = this.#manager.answer(name, body.id, body.values)\n\t\tif (result.success) return Response.json(result)\n\t\tif (result.error.reason === 'target') return Response.json(result, { status: 404 })\n\t\treturn Response.json(result, { status: 422 })\n\t}\n}\n","import type { TerminalManagerInterface } from '@orkestrel/terminal'\nimport type { TerminalRoute, TerminalRoutesOptions } from './types.js'\nimport { TerminalBridge } from './terminals/TerminalBridge.js'\n\n/**\n * Builds the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.\n *\n * @remarks\n * The pair stays byte-compatible with `@orkestrel/terminal`'s own `PromptClient`: the same GET url\n * streams, the same POST url answers, the same `{ id, values }` request body, the same JSON answer\n * `Result` response body, and the same `x-orkestrel-token` header.\n *\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 TerminalBridge(manager, options).routes()\n}\n"],"mappings":"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"}
@@ -1,10 +1,14 @@
1
- import { TerminalManagerInterface } from '@orkestrel/terminal';
2
- import { TimerHandler } from '@orkestrel/terminal';
1
+ import type { TerminalManagerInterface } from '@orkestrel/terminal';
2
+ import type { TimerHandler } from '@orkestrel/terminal';
3
3
 
4
4
  /**
5
- * Build the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
5
+ * Builds the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
6
6
  *
7
7
  * @remarks
8
+ * The pair stays byte-compatible with `@orkestrel/terminal`'s own `PromptClient`: the same GET url
9
+ * streams, the same POST url answers, the same `{ id, values }` request body, the same JSON answer
10
+ * `Result` response body, and the same `x-orkestrel-token` header.
11
+ *
8
12
  * Both routes share the configured `:name` path and optional token gate. The GET route replays
9
13
  * pending prompts, forwards live pending/expire events, and owns abort/keepalive teardown. The
10
14
  * POST route bounds the request body before parsing and maps answer outcomes to HTTP statuses.
@@ -25,72 +29,46 @@ import { TimerHandler } from '@orkestrel/terminal';
25
29
  */
26
30
  export declare function createTerminalRoutes(manager: TerminalManagerInterface, options?: TerminalRoutesOptions): readonly TerminalRoute[];
27
31
 
28
- /** The HTTP method literal a {@link TerminalRoute} declares — the exact 7-literal union `@orkestrel/router`'s `Method` accepts. */
29
- export declare type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
30
-
31
32
  /**
32
- * The default SSE keepalive interval (in milliseconds)
33
- * {@link import('./factories.js').createTerminalRoutes} arms per open connection — a `: `
34
- * comment ping a conforming SSE parser ignores, keeping intermediary proxies from timing out an
35
- * otherwise-idle stream.
33
+ * Holds the default SSE keepalive interval {@link import('./factories.js').createTerminalRoutes}
34
+ * arms per open connection, `15_000` ms — a `: ` comment ping a conforming SSE parser ignores,
35
+ * keeping intermediary proxies from timing out an otherwise-idle stream.
36
36
  */
37
37
  export declare const TERMINAL_KEEPALIVE_MS = 15000;
38
38
 
39
39
  /**
40
- * The default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
41
- * mounts its GET (SSE) + POST (answer) routes under.
40
+ * Holds the default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
41
+ * mounts its GET (SSE) and POST (answer) routes under, `/terminals/:name`.
42
42
  */
43
43
  export declare const TERMINAL_ROUTES_PATH = "/terminals/:name";
44
44
 
45
45
  /**
46
- * One structural route record {@link import('./factories.js').createTerminalRoutes} returns — a
47
- * plain `{ method, path, handler }` shape carrying NO dependency on `@orkestrel/router`'s own
48
- * `Route` type, so a consumer mounts it against any router that accepts a two-arg
49
- * `(request, context) => Response | Promise<Response>` handler keyed by `method` + `path`.
46
+ * Represents one structural route record {@link import('./factories.js').createTerminalRoutes}
47
+ * returns — a plain `{ method, path, handler }` shape carrying no dependency on
48
+ * `@orkestrel/router`'s own `Route` type, so a consumer mounts it against any router that accepts a
49
+ * two-arg `(request, context) => Response | Promise<Response>` handler keyed by `method` and
50
+ * `path`.
50
51
  */
51
52
  export declare interface TerminalRoute {
52
- readonly method: Method;
53
+ readonly method: TerminalRouteMethod;
53
54
  readonly path: string;
54
55
  readonly handler: (request: Request, context: TerminalRouteContext) => Response | Promise<Response>;
55
56
  }
56
57
 
57
58
  /**
58
- * The minimal route-dispatch context a {@link TerminalRoute} handler reads — exactly the frozen,
59
+ * Represents the minimal route-dispatch context a {@link TerminalRoute} handler reads — exactly the frozen,
59
60
  * URL-decoded `:name` path param slice a router hands a matched handler.
60
61
  */
61
62
  export declare interface TerminalRouteContext {
62
63
  readonly params: Readonly<Record<string, string>>;
63
64
  }
64
65
 
65
- /**
66
- * Build and serve the terminal manager's GET stream and POST answer routes.
67
- *
68
- * @example
69
- * ```ts
70
- * import { TerminalRoutes } from '@orkestrel/toolbox/server'
71
- *
72
- * const routes = new TerminalRoutes(manager).routes()
73
- * ```
74
- */
75
- export declare class TerminalRoutes {
76
- #private;
77
- /**
78
- * Create a terminal route owner.
79
- *
80
- * @param manager - Terminal manager bridged onto HTTP
81
- * @param options - Shared route, authorization, keepalive, timer, and body-limit options
82
- */
83
- constructor(manager: TerminalManagerInterface, options?: TerminalRoutesOptions);
84
- /**
85
- * Project the bound GET and POST route records.
86
- *
87
- * @returns The GET stream route followed by the POST answer route
88
- */
89
- routes(): readonly TerminalRoute[];
90
- }
66
+ /** Represents the HTTP method literal a {@link TerminalRoute} declares — the same union `@orkestrel/router`'s `Method` type accepts. */
67
+ export declare type TerminalRouteMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
91
68
 
92
69
  /**
93
- * Options for {@link import('./factories.js').createTerminalRoutes}.
70
+ * Represents the options {@link import('./factories.js').createTerminalRoutes} takes — the shared
71
+ * route, authorization, keepalive, timer, and body-limit configuration both routes read.
94
72
  *
95
73
  * @remarks
96
74
  * - `path` — the shared `:name`-templated path both the GET (SSE) and POST (answer) routes
@@ -98,18 +76,18 @@ export declare class TerminalRoutes {
98
76
  * - `token` — a {@link TerminalToken}: a string is compared for equality against the
99
77
  * `x-orkestrel-token` header; a function receives the header's value (`undefined` when absent)
100
78
  * and returns whether it validates, letting the consumer roll/expire tokens out-of-band.
101
- * Validated at GET connect, on EVERY POST, and RE-VALIDATED on every keepalive tick of a live
79
+ * Validated at GET connect, on every POST, and re-validated on every keepalive tick of a live
102
80
  * SSE stream — a stream whose presented token stops validating (rotated, expired, revoked) is
103
81
  * torn down (the abort/self-heal teardown path, no `shutdown` frame) rather than left open
104
82
  * forever; the client reconnects and re-authenticates. Omitted ⇒ no auth check. Because
105
83
  * re-validation only happens on the keepalive tick, the revocation window equals the keepalive
106
84
  * interval — a token rejected/expired between ticks keeps streaming until the next one. A
107
- * validator function that THROWS is treated as rejection (fail-closed) at every call site.
85
+ * validator function that throws is treated as rejection (fail-closed) at every call site.
108
86
  * - `keepalive` — the SSE comment-ping interval in milliseconds; defaults to
109
87
  * {@link import('./constants.js').TERMINAL_KEEPALIVE_MS}.
110
88
  * - `timer` — the injected {@link TimerHandler} driving the keepalive interval (default the host
111
89
  * `setTimeout`/`clearTimeout`), so a test drives the keepalive deterministically.
112
- * - `limit` — the maximum POST answer body size in bytes, streamed and enforced BEFORE JSON
90
+ * - `limit` — the maximum POST answer body size in bytes, streamed and enforced before JSON
113
91
  * parsing (ignoring any `Content-Length` header, so a lying header can never bypass the cap);
114
92
  * a body exceeding it is rejected `413` and `manager.answer` is never called. Defaults to
115
93
  * `@orkestrel/server`'s own `DEFAULT_BODY_LIMIT` (1 MiB).
@@ -123,10 +101,10 @@ export declare interface TerminalRoutesOptions {
123
101
  }
124
102
 
125
103
  /**
126
- * The `token` gate a {@link TerminalRoutesOptions} may configure — a plain string compared for
127
- * equality against the `x-orkestrel-token` header, OR a validator function the consumer fully
128
- * controls, enabling expiry/rotation (a JWT `exp` check, a revocation-list lookup, anything
129
- * time-varying) that a fixed string cannot express. `undefined` disables the auth check entirely.
104
+ * Represents the `token` gate {@link TerminalRoutesOptions} may configure — a plain string compared
105
+ * for equality against the `x-orkestrel-token` header, or a validator function the consumer fully
106
+ * controls, enabling the expiry and rotation a fixed string cannot express (a JWT `exp` check, a
107
+ * revocation-list lookup, anything time-varying). `undefined` disables the auth check entirely.
130
108
  */
131
109
  export declare type TerminalToken = string | ((value: string | undefined) => boolean);
132
110
 
@@ -1,10 +1,14 @@
1
- import { TerminalManagerInterface } from '@orkestrel/terminal';
2
- import { TimerHandler } from '@orkestrel/terminal';
1
+ import type { TerminalManagerInterface } from '@orkestrel/terminal';
2
+ import type { TimerHandler } from '@orkestrel/terminal';
3
3
 
4
4
  /**
5
- * Build the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
5
+ * Builds the GET SSE stream and POST answer routes that bridge a terminal manager onto the wire.
6
6
  *
7
7
  * @remarks
8
+ * The pair stays byte-compatible with `@orkestrel/terminal`'s own `PromptClient`: the same GET url
9
+ * streams, the same POST url answers, the same `{ id, values }` request body, the same JSON answer
10
+ * `Result` response body, and the same `x-orkestrel-token` header.
11
+ *
8
12
  * Both routes share the configured `:name` path and optional token gate. The GET route replays
9
13
  * pending prompts, forwards live pending/expire events, and owns abort/keepalive teardown. The
10
14
  * POST route bounds the request body before parsing and maps answer outcomes to HTTP statuses.
@@ -25,72 +29,46 @@ import { TimerHandler } from '@orkestrel/terminal';
25
29
  */
26
30
  export declare function createTerminalRoutes(manager: TerminalManagerInterface, options?: TerminalRoutesOptions): readonly TerminalRoute[];
27
31
 
28
- /** The HTTP method literal a {@link TerminalRoute} declares — the exact 7-literal union `@orkestrel/router`'s `Method` accepts. */
29
- export declare type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
30
-
31
32
  /**
32
- * The default SSE keepalive interval (in milliseconds)
33
- * {@link import('./factories.js').createTerminalRoutes} arms per open connection — a `: `
34
- * comment ping a conforming SSE parser ignores, keeping intermediary proxies from timing out an
35
- * otherwise-idle stream.
33
+ * Holds the default SSE keepalive interval {@link import('./factories.js').createTerminalRoutes}
34
+ * arms per open connection, `15_000` ms — a `: ` comment ping a conforming SSE parser ignores,
35
+ * keeping intermediary proxies from timing out an otherwise-idle stream.
36
36
  */
37
37
  export declare const TERMINAL_KEEPALIVE_MS = 15000;
38
38
 
39
39
  /**
40
- * The default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
41
- * mounts its GET (SSE) + POST (answer) routes under.
40
+ * Holds the default `:name`-templated path {@link import('./factories.js').createTerminalRoutes}
41
+ * mounts its GET (SSE) and POST (answer) routes under, `/terminals/:name`.
42
42
  */
43
43
  export declare const TERMINAL_ROUTES_PATH = "/terminals/:name";
44
44
 
45
45
  /**
46
- * One structural route record {@link import('./factories.js').createTerminalRoutes} returns — a
47
- * plain `{ method, path, handler }` shape carrying NO dependency on `@orkestrel/router`'s own
48
- * `Route` type, so a consumer mounts it against any router that accepts a two-arg
49
- * `(request, context) => Response | Promise<Response>` handler keyed by `method` + `path`.
46
+ * Represents one structural route record {@link import('./factories.js').createTerminalRoutes}
47
+ * returns — a plain `{ method, path, handler }` shape carrying no dependency on
48
+ * `@orkestrel/router`'s own `Route` type, so a consumer mounts it against any router that accepts a
49
+ * two-arg `(request, context) => Response | Promise<Response>` handler keyed by `method` and
50
+ * `path`.
50
51
  */
51
52
  export declare interface TerminalRoute {
52
- readonly method: Method;
53
+ readonly method: TerminalRouteMethod;
53
54
  readonly path: string;
54
55
  readonly handler: (request: Request, context: TerminalRouteContext) => Response | Promise<Response>;
55
56
  }
56
57
 
57
58
  /**
58
- * The minimal route-dispatch context a {@link TerminalRoute} handler reads — exactly the frozen,
59
+ * Represents the minimal route-dispatch context a {@link TerminalRoute} handler reads — exactly the frozen,
59
60
  * URL-decoded `:name` path param slice a router hands a matched handler.
60
61
  */
61
62
  export declare interface TerminalRouteContext {
62
63
  readonly params: Readonly<Record<string, string>>;
63
64
  }
64
65
 
65
- /**
66
- * Build and serve the terminal manager's GET stream and POST answer routes.
67
- *
68
- * @example
69
- * ```ts
70
- * import { TerminalRoutes } from '@orkestrel/toolbox/server'
71
- *
72
- * const routes = new TerminalRoutes(manager).routes()
73
- * ```
74
- */
75
- export declare class TerminalRoutes {
76
- #private;
77
- /**
78
- * Create a terminal route owner.
79
- *
80
- * @param manager - Terminal manager bridged onto HTTP
81
- * @param options - Shared route, authorization, keepalive, timer, and body-limit options
82
- */
83
- constructor(manager: TerminalManagerInterface, options?: TerminalRoutesOptions);
84
- /**
85
- * Project the bound GET and POST route records.
86
- *
87
- * @returns The GET stream route followed by the POST answer route
88
- */
89
- routes(): readonly TerminalRoute[];
90
- }
66
+ /** Represents the HTTP method literal a {@link TerminalRoute} declares — the same union `@orkestrel/router`'s `Method` type accepts. */
67
+ export declare type TerminalRouteMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
91
68
 
92
69
  /**
93
- * Options for {@link import('./factories.js').createTerminalRoutes}.
70
+ * Represents the options {@link import('./factories.js').createTerminalRoutes} takes — the shared
71
+ * route, authorization, keepalive, timer, and body-limit configuration both routes read.
94
72
  *
95
73
  * @remarks
96
74
  * - `path` — the shared `:name`-templated path both the GET (SSE) and POST (answer) routes
@@ -98,18 +76,18 @@ export declare class TerminalRoutes {
98
76
  * - `token` — a {@link TerminalToken}: a string is compared for equality against the
99
77
  * `x-orkestrel-token` header; a function receives the header's value (`undefined` when absent)
100
78
  * and returns whether it validates, letting the consumer roll/expire tokens out-of-band.
101
- * Validated at GET connect, on EVERY POST, and RE-VALIDATED on every keepalive tick of a live
79
+ * Validated at GET connect, on every POST, and re-validated on every keepalive tick of a live
102
80
  * SSE stream — a stream whose presented token stops validating (rotated, expired, revoked) is
103
81
  * torn down (the abort/self-heal teardown path, no `shutdown` frame) rather than left open
104
82
  * forever; the client reconnects and re-authenticates. Omitted ⇒ no auth check. Because
105
83
  * re-validation only happens on the keepalive tick, the revocation window equals the keepalive
106
84
  * interval — a token rejected/expired between ticks keeps streaming until the next one. A
107
- * validator function that THROWS is treated as rejection (fail-closed) at every call site.
85
+ * validator function that throws is treated as rejection (fail-closed) at every call site.
108
86
  * - `keepalive` — the SSE comment-ping interval in milliseconds; defaults to
109
87
  * {@link import('./constants.js').TERMINAL_KEEPALIVE_MS}.
110
88
  * - `timer` — the injected {@link TimerHandler} driving the keepalive interval (default the host
111
89
  * `setTimeout`/`clearTimeout`), so a test drives the keepalive deterministically.
112
- * - `limit` — the maximum POST answer body size in bytes, streamed and enforced BEFORE JSON
90
+ * - `limit` — the maximum POST answer body size in bytes, streamed and enforced before JSON
113
91
  * parsing (ignoring any `Content-Length` header, so a lying header can never bypass the cap);
114
92
  * a body exceeding it is rejected `413` and `manager.answer` is never called. Defaults to
115
93
  * `@orkestrel/server`'s own `DEFAULT_BODY_LIMIT` (1 MiB).
@@ -123,10 +101,10 @@ export declare interface TerminalRoutesOptions {
123
101
  }
124
102
 
125
103
  /**
126
- * The `token` gate a {@link TerminalRoutesOptions} may configure — a plain string compared for
127
- * equality against the `x-orkestrel-token` header, OR a validator function the consumer fully
128
- * controls, enabling expiry/rotation (a JWT `exp` check, a revocation-list lookup, anything
129
- * time-varying) that a fixed string cannot express. `undefined` disables the auth check entirely.
104
+ * Represents the `token` gate {@link TerminalRoutesOptions} may configure — a plain string compared
105
+ * for equality against the `x-orkestrel-token` header, or a validator function the consumer fully
106
+ * controls, enabling the expiry and rotation a fixed string cannot express (a JWT `exp` check, a
107
+ * revocation-list lookup, anything time-varying). `undefined` disables the auth check entirely.
130
108
  */
131
109
  export declare type TerminalToken = string | ((value: string | undefined) => boolean);
132
110