@cogitator-ai/tetsu 0.4.11 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,17 +52,19 @@ A run that waits on a slow tool or model can stay silent for longer than the `id
52
52
 
53
53
  A Tetsu controller named `Cogitator`: `operationId`s in the OpenAPI document are `cogitatorRunAgent`, `cogitatorGetThread` and so on.
54
54
 
55
- | Option | Type | Description |
56
- | ----------------- | ------------------------------------------- | -------------------------------------------------------------------------------- |
57
- | `cogitator` | `Cogitator` | **Required.** The runtime |
58
- | `agents` | `Record<string, Agent>` | Agents by name |
59
- | `workflows` | `Record<string, Workflow>` | Workflows by name |
60
- | `swarms` | `Record<string, SwarmConfig>` | Swarms by name |
61
- | `auth` | `(ctx) => AuthContext \| undefined` or hook | Establishes the caller; see [Authentication](#authentication) |
62
- | `authorizeThread` | `(auth, threadId) => boolean` | Decides who may use a memory thread; see [Users and threads](#users-and-threads) |
63
- | `websocket` | `boolean \| { path?: string }` | Serve the WebSocket endpoint (default path `/ws`) |
64
- | `until` | `AbortSignal \| (() => AbortSignal)` | Ends open streams and sockets, such as `draining` from `@tetsujs/lifecycle` |
65
- | `sseHeartbeatMs` | `number` | Heartbeat comment interval of SSE streams (default `5000`, `0` turns it off) |
55
+ | Option | Type | Description |
56
+ | -------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
57
+ | `cogitator` | `Cogitator` | **Required.** The runtime |
58
+ | `agents` | `Record<string, Agent>` | Agents by name |
59
+ | `workflows` | `Record<string, Workflow>` | Workflows by name |
60
+ | `swarms` | `Record<string, SwarmConfig>` | Swarms by name |
61
+ | `auth` | `(ctx) => AuthContext \| undefined` or hook | Establishes the caller, see [Authentication](#authentication) |
62
+ | `authorizeThread` | `(auth, threadId) => boolean` | Decides who may use a memory thread, see [Users and threads](#users-and-threads) |
63
+ | `websocket` | `boolean \| { path?: string }` | Serve the WebSocket endpoint (default path `/ws`) |
64
+ | `until` | `AbortSignal \| (() => AbortSignal)` | Ends open streams and sockets, such as `draining` from `@tetsujs/lifecycle` |
65
+ | `sseHeartbeatMs` | `number` | Heartbeat comment interval of SSE streams (default `5000`, `0` turns it off) |
66
+ | `acceptContext` | `boolean \| string[]` | Keys of a run `context` clients may send: none by default, a list, or `true` for any (it goes into the system prompt) |
67
+ | `threadMessageRoles` | `ThreadMessageRole[]` | Roles `POST /threads/:id/messages` accepts (default `user` and `assistant`) |
66
68
 
67
69
  ## Endpoints
68
70
 
@@ -91,7 +93,7 @@ Request bodies:
91
93
 
92
94
  ```typescript
93
95
  // POST /agents/:name/run, /agents/:name/stream
94
- { input: string; context?: Record<string, unknown>; threadId?: string } // input must contain more than whitespace
96
+ { input: string; context?: Record<string, unknown>; threadId?: string } // input and threadId must contain more than whitespace
95
97
 
96
98
  // POST /agents/:name/resume, /agents/:name/resume/stream
97
99
  {
@@ -101,15 +103,19 @@ Request bodies:
101
103
  }
102
104
 
103
105
  // POST /swarms/:name/run, /swarms/:name/stream
104
- { input: string; context?: Record<string, unknown>; threadId?: string; timeout?: number }
106
+ { input: string; context?: Record<string, unknown>; threadId?: string; timeout?: number } // timeout at most 2147483647 ms
105
107
 
106
108
  // POST /workflows/:name/run, /workflows/:name/stream
107
- { input?: Record<string, unknown>; options?: { maxConcurrency?: number; maxIterations?: number; checkpoint?: boolean } }
109
+ { input?: Record<string, unknown>; options?: { maxConcurrency?: number; maxIterations?: number; checkpoint?: false } }
108
110
 
109
111
  // POST /threads/:id/messages
110
- { role: 'user' | 'assistant' | 'system'; content: string; metadata?: Record<string, unknown> }
112
+ { role: 'user' | 'assistant' | 'system'; content: string; metadata?: Record<string, unknown> } // system only with threadMessageRoles
111
113
  ```
112
114
 
115
+ The schemas are refined with the validators every Cogitator adapter shares (`@cogitator-ai/server-shared`), so Tetsu refuses the same bodies as Express, Fastify, Hono, Koa and Next, under its own `VALIDATION_FAILED` envelope. A run puts `context` into the system prompt and the model reads a `system` thread message as operator instructions, so both are refused unless the controller accepts them: `acceptContext` lists the `context` keys clients may send (or `true` for any), and `threadMessageRoles` the roles they may add (`user` and `assistant` by default). A body that is not JSON (`text/plain`, a form) is refused with `415 UNSUPPORTED_MEDIA_TYPE` before it is read, since browsers send those across origins without a CORS preflight. `options.checkpoint: true` is refused: the controller runs each workflow on a fresh executor and keeps no checkpoint store to resume from.
116
+
117
+ `POST /agents/:name/run` and `/resume` answer `{ output, threadId, usage, toolCalls, status, traceId }` plus, when they apply, `reasoning`, `pendingApprovals`, `structured`, `structuredError`, `truncated`, `blocked` and `iterationLimitReached`: the same object as every other adapter, built by `toAgentRunResponse()` of `@cogitator-ai/server-shared`. It never carries the system prompt, the history, trace spans or the checkpoint of a paused run. Swarm routes close the swarm they build once the run ends, so a distributed swarm leaves no Redis connections behind, and `GET /swarms` lists every agent, a router and pipeline stages included.
118
+
113
119
  ## Errors
114
120
 
115
121
  Every error is answered in Tetsu's envelope:
@@ -118,20 +124,23 @@ Every error is answered in Tetsu's envelope:
118
124
  { "status": 404, "message": "Agent 'ghost' not found", "error": "AGENT_NOT_FOUND" }
119
125
  ```
120
126
 
121
- | Status | `error` | When |
122
- | ------- | ---------------------------------------------------------- | -------------------------------------------------------------------- |
123
- | 401 | `UNAUTHORIZED` | `auth` returned `undefined` |
124
- | 403 | `THREAD_ACCESS_DENIED` | The thread belongs to another user |
125
- | 403 | `THREAD_FORBIDDEN` | `authorizeThread` refused the thread |
126
- | 404 | `AGENT_NOT_FOUND`, `WORKFLOW_NOT_FOUND`, `SWARM_NOT_FOUND` | No such name |
127
- | 409 | `BLACKBOARD_DISABLED` | The swarm has no blackboard |
128
- | 409 | `RUN_NOT_PAUSED` | A resume named a thread without a paused run |
129
- | 499 | `CLIENT_CLOSED_REQUEST` | The client left before a `/run` or `/resume` answer was ready |
130
- | 422 | `VALIDATION_FAILED` | The body or the path failed its schema; `issues` lists every problem |
131
- | 501 | `PACKAGE_NOT_INSTALLED` | `@cogitator-ai/workflows` or `@cogitator-ai/swarms` is missing |
132
- | 503 | `MEMORY_NOT_CONFIGURED` | A thread endpoint was called on a runtime with no `memory` config |
133
- | 4xx/5xx | the `CogitatorError` code | The run failed, e.g. `429 LLM_RATE_LIMITED` with `Retry-After` |
134
- | 500 | `INTERNAL_SERVER_ERROR` | Anything else; the detail goes to `reportError`, never to the client |
127
+ | Status | `error` | When |
128
+ | ------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
129
+ | 400 | `MALFORMED_JSON` | The body is not valid JSON |
130
+ | 401 | `UNAUTHORIZED` | `auth` returned `undefined`, or threw something other than an `HttpError` (an expired token) |
131
+ | 403 | `THREAD_ACCESS_DENIED` | The thread belongs to another user |
132
+ | 403 | `THREAD_FORBIDDEN` | `authorizeThread` refused the thread |
133
+ | 404 | `AGENT_NOT_FOUND`, `WORKFLOW_NOT_FOUND`, `SWARM_NOT_FOUND` | No such name |
134
+ | 409 | `BLACKBOARD_DISABLED` | The swarm has no blackboard |
135
+ | 409 | `RUN_NOT_PAUSED` | A resume named a thread without a paused run |
136
+ | 413 | `BODY_TOO_LARGE` | The body is over the application's body limit |
137
+ | 415 | `UNSUPPORTED_MEDIA_TYPE` | The body is not JSON (`text/plain`, a form) |
138
+ | 499 | `CLIENT_CLOSED_REQUEST` | The client left before a `/run` or `/resume` answer was ready |
139
+ | 422 | `VALIDATION_FAILED` | The body or the path failed its schema, a `context` key or a thread message role the controller does not accept, `issues` lists every problem |
140
+ | 501 | `PACKAGE_NOT_INSTALLED` | `@cogitator-ai/workflows` or `@cogitator-ai/swarms` is missing |
141
+ | 503 | `MEMORY_NOT_CONFIGURED` | A thread endpoint was called on a runtime with no `memory` config |
142
+ | 4xx/5xx | the `CogitatorError` code | The run failed, e.g. `429 LLM_RATE_LIMITED` with `Retry-After` |
143
+ | 500 | `INTERNAL_SERVER_ERROR` | Anything else, the detail goes to `reportError`, never to the client |
135
144
 
136
145
  Only a `CogitatorError` keeps its message and code. Any other error reaches the client only as a generic internal error: `500 INTERNAL_SERVER_ERROR` in JSON, and `Internal server error` in stream `error` events, WebSocket errors and the `error` field of workflow `node_error` and swarm `agent_error` events. Its text, which can carry connection strings or file paths, never leaves the server.
137
146
 
@@ -145,7 +154,7 @@ createApp({ hooks: { onError: [cogitatorErrors()] }, routes });
145
154
 
146
155
  ## Authentication
147
156
 
148
- `auth` runs as a `beforeParse` hook, so a refused request costs no body parsing. It receives the Tetsu context and returns the caller, or `undefined` to answer `401`. Throw an `HttpError` to answer with another status. `/health` and `/ready` stay open for probes.
157
+ `auth` runs as a `beforeParse` hook, so a refused request costs no body parsing. It receives the Tetsu context and returns the caller, or `undefined` to answer `401`. Any other error it throws or rejects with, such as a token that fails to verify, answers `401` too, never a `500`. Throw an `HttpError` to answer with another status. `/health` and `/ready` stay open for probes.
149
158
 
150
159
  ```typescript
151
160
  cogitatorController({
@@ -257,23 +266,28 @@ Approved calls run, declined ones answer the model with the `reason`, and calls
257
266
  `/stream` endpoints answer with `text/event-stream` through `@tetsujs/sse`: keep-alive comments every `sseHeartbeatMs` (5 seconds by default, under the 10 second `idleTimeout` of `Bun.serve`), backpressure, and the run is aborted when the client goes away. Events follow the Cogitator stream protocol shared with the other adapters, one JSON object per `data:` line, ending with `data: [DONE]`:
258
267
 
259
268
  ```
260
- data: {"type":"start","messageId":"msg_…"}
261
- data: {"type":"text-start","id":"txt_…"}
262
- data: {"type":"text-delta","id":"txt_…","delta":"Hel"}
269
+ data: {"type":"start","messageId":"msg_…","threadId":"thread_…"}
270
+ data: {"type":"text-start","id":"txt_1"}
271
+ data: {"type":"text-delta","id":"txt_1","delta":"Let me check."}
272
+ data: {"type":"text-end","id":"txt_1"}
263
273
  data: {"type":"tool-call-start","id":"call_1","toolName":"get_weather"}
264
274
  data: {"type":"tool-call-delta","id":"call_1","argsTextDelta":"{\"city\":\"Paris\"}"}
265
275
  data: {"type":"tool-call-end","id":"call_1"}
266
276
  data: {"type":"tool-result","id":"res_…","toolCallId":"call_1","result":"Sunny"}
267
- data: {"type":"text-end","id":"txt_…"}
268
- data: {"type":"finish","messageId":"msg_…","usage":{"inputTokens":12,"outputTokens":30,"totalTokens":42}}
277
+ data: {"type":"text-start","id":"txt_2"}
278
+ data: {"type":"text-delta","id":"txt_2","delta":"Sunny in Paris."}
279
+ data: {"type":"text-end","id":"txt_2"}
280
+ data: {"type":"finish","messageId":"msg_…","usage":{"inputTokens":12,"outputTokens":30,"totalTokens":42},"threadId":"thread_…","status":"completed"}
269
281
  data: [DONE]
270
282
  ```
271
283
 
272
- An agent with `reasoning: { summary: true }` also streams its reasoning summary as `reasoning-start`, `reasoning-delta` and `reasoning-end` events. A text or reasoning part opens with its first delta and is closed before a part of the other kind, a tool call or `finish`, so parts never overlap. `POST /agents/:name/run` returns the summary as `reasoning`, and `usage` gains `reasoningTokens`, `cachedInputTokens` and `cacheWriteTokens` when the provider reports them. The `finish` event of an agent stream carries the same `usage` as the JSON answer, these counts included.
284
+ `start` names the run's thread: the `threadId` of the request, or the thread the controller opened for it, so a client that sent none continues the conversation with this one even if the stream breaks. `finish` repeats it with `status` and the other outcome fields of the JSON answer.
285
+
286
+ An agent with `reasoning: { summary: true }` also streams its reasoning summary as `reasoning-start`, `reasoning-delta` and `reasoning-end` events. A text or reasoning part opens with its first delta and is closed before a part of the other kind, a tool call or `finish`, so parts never overlap and text after a tool call starts a new part. An answer the model gave in one piece arrives as one text part before `finish`, and a resumed run that pauses again sends `approval-required` before `finish`. `POST /agents/:name/run` returns the summary as `reasoning`, and `usage` gains `reasoningTokens`, `cachedInputTokens` and `cacheWriteTokens` when the provider reports them. The `finish` event of an agent stream carries the same `usage` as the JSON answer, these counts included.
273
287
 
274
- A run that fails ends with `{"type":"error","message":"…","code":"…"}` instead of `finish`. A `CogitatorError` keeps its message and code; anything else is sent as `"message":"Internal server error","code":"INTERNAL_SERVER_ERROR"` and reported to the application's `reportError` with `source: "stream"`. Workflow streams send `workflow` events (`node_started`, `node_completed`, `node_error`, `node_progress`, `workflow_completed`), swarm streams send `swarm` events (`agent_start`, `agent_complete`, `agent_error`, `message`, `swarm_completed`).
288
+ A run that fails ends with `{"type":"error","message":"…","code":"…"}` instead of `finish`. A `CogitatorError` keeps its message and code, anything else is sent as `"message":"Internal server error","code":"INTERNAL_SERVER_ERROR"` and reported to the application's `reportError` with `source: "stream"`. Workflow streams send `workflow` events (`node_started`, `node_completed`, `node_error`, `node_progress`, `workflow_completed`), swarm streams send `swarm` events (`agent_start`, `agent_complete`, `agent_error`, `message`, every event the swarm itself emits under its own name, such as `swarm:start` or `agent:message`, and `swarm_completed`).
275
289
 
276
- Validation, `401`, `403` and `404` are answered as JSON before the stream opens.
290
+ Validation (`415`, `422`), `401`, `404` and the `403 THREAD_FORBIDDEN` of `authorizeThread` are answered as JSON before the stream opens. Whether the thread belongs to the caller is checked by the run itself, so another user's thread ends an open stream with an `error` event, `"code":"THREAD_ACCESS_DENIED"`, instead of `finish`.
277
291
 
278
292
  ## WebSocket
279
293
 
@@ -283,12 +297,12 @@ cogitatorController({ cogitator, agents, auth, websocket: true });
283
297
 
284
298
  The handshake runs `auth` like any route. Each socket runs one agent, workflow or swarm at a time; a `run` or `resume` sent while one is in progress gets an error with `code: 'RUN_IN_PROGRESS'`.
285
299
 
286
- | Client sends | Server answers |
287
- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
288
- | `{ type: 'run', id?, payload: { type: 'agent' \| 'workflow' \| 'swarm', name, input, context?, threadId? } }` | `event` frames: `token`, `reasoning`, `tool-call`, `tool-result`, then `complete` with the result, or `cancelled` |
289
- | `{ type: 'resume', id?, payload: { name, threadId, decisions?, defaultDecision? } }` | the same frames for an agent run paused for [approvals](#approvals) |
290
- | `{ type: 'stop' }` | cancels the current run |
291
- | `{ type: 'ping', id? }` | `{ type: 'pong', id }` |
300
+ | Client sends | Server answers |
301
+ | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
302
+ | `{ type: 'run', id?, payload: { type: 'agent' \| 'workflow' \| 'swarm', name, input, context?, threadId? } }` | `event` frames: `token`, `reasoning`, `tool-call`, `tool-result`, then `complete` with the result (for an agent, the JSON answer of `/run`), or `cancelled` |
303
+ | `{ type: 'resume', id?, payload: { name, threadId, decisions?, defaultDecision? } }` | the same frames for an agent run paused for [approvals](#approvals) |
304
+ | `{ type: 'stop' }` | cancels the current run |
305
+ | `{ type: 'ping', id? }` | `{ type: 'pong', id }` |
292
306
 
293
307
  Errors arrive as `{ type: 'error', id, error, code }` and leave the socket open; an invalid frame gets `code: 'INVALID_MESSAGE'`, and an error that is not a `CogitatorError` is sent as `Internal server error` with `code: 'INTERNAL_SERVER_ERROR'`. Closing the socket cancels its run. Bun's server-level options such as `maxPayloadLength` are set where the app is served:
294
308
 
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3E,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,WAAW,GAAG,SAAS,CAAC;CACxC;AASD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,YAAY,EAAE,YAAY,GAAG,SAAS;;;;wCAMhE;AAED,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,UAAU,CAErE"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3E,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,WAAW,GAAG,SAAS,CAAC;CACxC;AASD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,YAAY,EAAE,YAAY,GAAG,SAAS;;;;wCAehE;AAWD,4EAA4E;AAC5E,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,UAAU,CAErE"}
package/dist/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- import { hook, httpError } from '@tetsujs/core';
1
+ import { hook, HttpError, httpError } from '@tetsujs/core';
2
2
  const ANONYMOUS = { cogitatorAuth: undefined };
3
3
  function admit(auth) {
4
4
  if (!auth)
@@ -15,10 +15,28 @@ export function callerHook(authenticate) {
15
15
  return hook.beforeParse((ctx) => {
16
16
  if (!authenticate)
17
17
  return { ...ANONYMOUS };
18
- const auth = authenticate(ctx);
19
- return auth instanceof Promise ? auth.then(admit) : admit(auth);
18
+ let auth;
19
+ try {
20
+ auth = authenticate(ctx);
21
+ }
22
+ catch (error) {
23
+ throw refusal(error);
24
+ }
25
+ return auth instanceof Promise
26
+ ? auth.then(admit, (error) => {
27
+ throw refusal(error);
28
+ })
29
+ : admit(auth);
20
30
  });
21
31
  }
32
+ /**
33
+ * What an `authenticate` function that failed answers: an `HttpError` it threw on purpose as
34
+ * it is, anything else (a token that does not verify, an expired session) as `401`, since the
35
+ * caller could not be established. It is not a fault of the server.
36
+ */
37
+ function refusal(error) {
38
+ return error instanceof HttpError ? error : httpError(401, 'UNAUTHORIZED', 'Unauthorized');
39
+ }
22
40
  /**
23
41
  * The caller hook for `auth`: built from a function, or taken as it is when it is
24
42
  * already a hook — a `callerHook()` wrapped in `secured()` from `@tetsujs/openapi`,
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAQhD,MAAM,SAAS,GAAiB,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;AAE7D,SAAS,KAAK,CAAC,IAA6B;IAC1C,IAAI,CAAC,IAAI;QAAE,MAAM,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;IAChE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;AACjC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,YAAsC;IAC/D,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,EAAwC,EAAE;QACpE,IAAI,CAAC,YAAY;YAAE,OAAO,EAAE,GAAG,SAAS,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,IAAI,YAAY,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACL,CAAC;AAKD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAA2B;IACvD,OAAO,OAAO,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACpF,CAAC"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAQ3D,MAAM,SAAS,GAAiB,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC;AAE7D,SAAS,KAAK,CAAC,IAA6B;IAC1C,IAAI,CAAC,IAAI;QAAE,MAAM,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;IAChE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;AACjC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,YAAsC;IAC/D,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,EAAwC,EAAE;QACpE,IAAI,CAAC,YAAY;YAAE,OAAO,EAAE,GAAG,SAAS,EAAE,CAAC;QAC3C,IAAI,IAA8B,CAAC;QACnC,IAAI,CAAC;YACH,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;QACD,OAAO,IAAI,YAAY,OAAO;YAC5B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAc,EAAE,EAAE;gBAClC,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC,CAAC;YACJ,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,KAAK,YAAY,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;AAC7F,CAAC;AAKD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAA2B;IACvD,OAAO,OAAO,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACpF,CAAC"}
@@ -9,4 +9,17 @@
9
9
  * hook: their heartbeats keep the connection busy.
10
10
  */
11
11
  export declare const holdConnection: import("@tetsujs/core").Hook<"beforeHandle", import("@tetsujs/core").BaseCtx, unknown>;
12
+ /**
13
+ * The `beforeParse` hook of every route with a JSON body: a body that is not JSON is refused
14
+ * with `415 UNSUPPORTED_MEDIA_TYPE` before it is read.
15
+ *
16
+ * Tetsu parses a body as the route declares it, whatever the `content-type` says. Browsers
17
+ * send `text/plain` and form bodies across origins without a CORS preflight, so without this
18
+ * hook any web page could start a run on a server without auth, or with cookies.
19
+ */
20
+ export declare const jsonBody: import("@tetsujs/core").Hook<"beforeParse", import("@tetsujs/core").BaseCtx & {
21
+ readonly params: {
22
+ [x: string]: string;
23
+ };
24
+ }, unknown>;
12
25
  //# sourceMappingURL=connection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,wFAEzB,CAAC"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,wFAEzB,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ;;;;WAGnB,CAAC"}
@@ -1,4 +1,5 @@
1
- import { hook } from '@tetsujs/core';
1
+ import { hook, httpError } from '@tetsujs/core';
2
+ import { refuseNonJsonBody } from '@cogitator-ai/server-shared';
2
3
  /**
3
4
  * The `beforeHandle` hook of the routes that answer a run with JSON: lifts the idle
4
5
  * timeout of `Bun.serve` for this one request.
@@ -12,4 +13,17 @@ import { hook } from '@tetsujs/core';
12
13
  export const holdConnection = hook.beforeHandle((ctx) => {
13
14
  ctx.server.timeout(ctx.req, 0);
14
15
  });
16
+ /**
17
+ * The `beforeParse` hook of every route with a JSON body: a body that is not JSON is refused
18
+ * with `415 UNSUPPORTED_MEDIA_TYPE` before it is read.
19
+ *
20
+ * Tetsu parses a body as the route declares it, whatever the `content-type` says. Browsers
21
+ * send `text/plain` and form bodies across origins without a CORS preflight, so without this
22
+ * hook any web page could start a run on a server without auth, or with cookies.
23
+ */
24
+ export const jsonBody = hook.beforeParse((ctx) => {
25
+ const refusal = refuseNonJsonBody(ctx.req.method, ctx.req.headers);
26
+ if (refusal)
27
+ throw httpError(refusal.status, refusal.code, refusal.message);
28
+ });
15
29
  //# sourceMappingURL=connection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,EAAE;IACtD,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,EAAE;IACtD,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,EAAE;IAC/C,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnE,IAAI,OAAO;QAAE,MAAM,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;AAC9E,CAAC,CAAC,CAAC"}