@mercury-fw/channel-http 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @mercury-fw/channel-http
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8bed46b: - A turn carries a `Principal` (the person behind it and the provider that vouched for them) instead of the opaque `userId` and `wikiUserId`; the core derives every per-person id from it.
8
+ - Episodic memory follows the principal, not the channel: a session is captured whenever a provider vouched for the person. Google Chat keeps the ids it had; the terminal and the HTTP channel (until it authenticates its callers) send a principal nobody vouched for, so they stay out of episodic memory as before.
9
+ - The HTTP channel accepts a `conversationId` on `/turn` and `/confirm` only made of letters, digits, `-` and `_`, up to 128 characters once trimmed, and answers `400` otherwise: a client-chosen id can no longer reach another user's wiki notes, forge log lines or break the turn. An existing HTTP conversation whose id has other characters can still be read with `/conversation`, but not continued. `/conversation` keeps opening any session key `/conversations` lists, whatever channel it came from.
10
+ - The model's wiki read tools refuse a per-user id that isn't one path segment, as writes already did.
11
+ - `CHANNEL_API_VERSION` is 2: a channel written for version 1 is refused at load.
12
+ - The first-party channels declare their channel api version as a literal, so an older channel can't claim a newer contract it doesn't implement.
13
+
3
14
  ## 0.1.1
4
15
 
5
16
  ### Patch Changes
package/README.md CHANGED
@@ -36,9 +36,9 @@ Runs one conversational turn; the reply streams back as Server-Sent Events.
36
36
  | field | type | required | description |
37
37
  |---|---|---|---|
38
38
  | `text` | string | yes | The user message. A bare confirmation token here confirms a staged action (see the `pending` event) without invoking the model. |
39
- | `conversationId` | string | no | Opaque, client-owned id that continues a conversation. Omitted ⇒ a fresh one-off session. |
39
+ | `conversationId` | string | no | Opaque, client-owned id that continues a conversation: letters, digits, `-` and `_`, up to 128 characters (a UUID fits) once surrounding whitespace is trimmed, `400` otherwise. Omitted ⇒ a fresh one-off session. |
40
40
 
41
- **Responses**: `200 text/event-stream` (the events below); `400` if `text` is missing or the body isn't JSON.
41
+ **Responses**: `200 text/event-stream` (the events below); `400` if `text` is missing, the body isn't JSON, or `conversationId` has other characters than the ones above.
42
42
 
43
43
  Reasoning and answer text arrive as **incremental deltas** — never one finished block — so a UI renders them live; treat the `final` event as the authoritative text. **To cancel** a turn (e.g. a generation that's diverging), close the connection: Mercury aborts the in-flight generation.
44
44
 
@@ -63,7 +63,7 @@ curl -N -X POST http://localhost:4100/turn \
63
63
 
64
64
  ## `POST /confirm`
65
65
 
66
- Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, conversationId }`; returns `{ ok: true, resolved: true, text }` when the token was a pending confirmation, `{ ok: true, resolved: false }` otherwise. `400` if `token` or `conversationId` is missing. Never invokes the model.
66
+ Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, conversationId }`; returns `{ ok: true, resolved: true, text }` when the token was a pending confirmation, `{ ok: true, resolved: false }` otherwise. `400` if `token` or `conversationId` is missing, or the id has other characters than `/turn` accepts. Never invokes the model.
67
67
 
68
68
  ## Read-only introspection
69
69
 
@@ -71,7 +71,7 @@ All `GET`, all JSON, all reporting state already held in-process.
71
71
 
72
72
  | endpoint | `data` on success |
73
73
  |---|---|
74
- | `GET /conversation?id=<conversationId>&limit=<n>&offset=<cursor>` | `{ messages: [{ role, content, timestamp }], nextOffset }` — a conversation's durable transcript in order; `400` if `id` is missing |
74
+ | `GET /conversation?id=<conversationId>&limit=<n>&offset=<cursor>` | `{ messages: [{ role, content, timestamp }], nextOffset }` — a conversation's durable transcript in order; `400` if `id` is missing. Any session key `/conversations` lists opens here, whatever channel it came from |
75
75
  | `GET /conversations?limit=<n>` | `{ conversations: [{ sessionKey, lastTimestamp, preview }] }` — known conversations, most-recently-active first |
76
76
  | `GET /manifest` | `{ manifest: { coreApiVersion, plugins: [{ name, apiVersion, active, skills, hasBuild, customStatus }], activeClis, skills } }` |
77
77
  | `GET /confirmations` | `{ pending: [{ sessionKey, binary, args, expiresAt }] }` — tokens are deliberately never included |
@@ -33,7 +33,7 @@ export type TurnRequestDeps = {
33
33
  };
34
34
  /**
35
35
  * Serves the OpenAPI document that describes this surface — the single source
36
- * of truth in `apps/mercury/openapi.yaml`, also rendered as the GitHub Pages
36
+ * of truth in this package's `openapi.yaml`, also rendered as the GitHub Pages
37
37
  * API docs. Served raw as `text/yaml` (no YAML parser needed at runtime); most
38
38
  * tooling (Redoc, Swagger UI, Postman) reads YAML directly.
39
39
  */
package/dist/index.d.ts CHANGED
@@ -11,6 +11,6 @@
11
11
  * inversion). Those two are optional on the contract but required here, so a
12
12
  * context missing them throws — the loader isolates it fail-soft.
13
13
  */
14
- import { type ChannelPlugin } from "@mercury-fw/channel-types";
14
+ import type { ChannelPlugin } from "@mercury-fw/channel-types";
15
15
  export { createHttpProvider, type HttpProviderDeps } from "./http-provider.ts";
16
16
  export declare const httpChannel: ChannelPlugin;
package/http-server.ts CHANGED
@@ -26,6 +26,13 @@ import {
26
26
  type ConfirmOutcome,
27
27
  } from "@mercury-fw/channel-types";
28
28
 
29
+ /**
30
+ * A client's conversation id becomes the session key, a log prefix and (until
31
+ * the channel authenticates) the per-person wiki id, so only letters, digits,
32
+ * `-` and `_` get in: no path segments, no line breaks, nothing to encode.
33
+ */
34
+ const CONVERSATION_ID = /^[A-Za-z0-9_-]{1,128}$/;
35
+
29
36
  /** Resolves a bare confirmation token to a reply string, or `null` if the input isn't a token. Injected by the core (`ctx.confirm`). */
30
37
  export type ConfirmFn = (token: string, sessionKey: string, userId: string) => Promise<string | null>;
31
38
  /** The structured sibling of {@link ConfirmFn}, for the `/confirm` `resolved` flag. Injected by the core (`ctx.resolveConfirmation`). */
@@ -68,7 +75,7 @@ function preflight(origin: string): Response {
68
75
 
69
76
  /**
70
77
  * Serves the OpenAPI document that describes this surface — the single source
71
- * of truth in `apps/mercury/openapi.yaml`, also rendered as the GitHub Pages
78
+ * of truth in this package's `openapi.yaml`, also rendered as the GitHub Pages
72
79
  * API docs. Served raw as `text/yaml` (no YAML parser needed at runtime); most
73
80
  * tooling (Redoc, Swagger UI, Postman) reads YAML directly.
74
81
  */
@@ -105,6 +112,9 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
105
112
  typeof body.conversationId === "string" && body.conversationId.trim().length > 0
106
113
  ? body.conversationId.trim()
107
114
  : (deps.newSessionKey ?? (() => crypto.randomUUID()))();
115
+ if (!CONVERSATION_ID.test(sessionKey)) {
116
+ return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
117
+ }
108
118
 
109
119
  // Aborted when the client disconnects (see the stream's `cancel` below) so
110
120
  // the in-flight turn stops instead of running to completion — the "stop"
@@ -164,7 +174,9 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
164
174
  multiUser: false,
165
175
  text,
166
176
  sessionKey,
167
- wikiUserId: sessionKey,
177
+ // Unauthenticated: the conversation id stands in for the caller,
178
+ // and nobody vouches for it.
179
+ principal: { id: sessionKey, provider: "none" },
168
180
  logPrefix: `[http:${sessionKey}] `,
169
181
  abortSignal: abort.signal,
170
182
  },
@@ -218,6 +230,9 @@ export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDep
218
230
  if (token.length === 0 || sessionKey.length === 0) {
219
231
  return Response.json({ ok: false, error: "missing token or conversationId" }, { status: 400, headers: cors });
220
232
  }
233
+ if (!CONVERSATION_ID.test(sessionKey)) {
234
+ return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
235
+ }
221
236
  const outcome = await deps.resolveConfirmation(token, sessionKey, sessionKey);
222
237
  switch (outcome.status) {
223
238
  case "not-a-token":
package/index.ts CHANGED
@@ -11,13 +11,15 @@
11
11
  * inversion). Those two are optional on the contract but required here, so a
12
12
  * context missing them throws — the loader isolates it fail-soft.
13
13
  */
14
- import { CHANNEL_API_VERSION, type ChannelPlugin } from "@mercury-fw/channel-types";
14
+ import type { ChannelPlugin } from "@mercury-fw/channel-types";
15
15
  import { createHttpProvider } from "./http-provider.ts";
16
16
 
17
17
  export { createHttpProvider, type HttpProviderDeps } from "./http-provider.ts";
18
18
 
19
19
  export const httpChannel: ChannelPlugin = {
20
- apiVersion: CHANNEL_API_VERSION,
20
+ // The contract this channel is written for, as a literal: importing
21
+ // CHANNEL_API_VERSION would report whichever contract is installed.
22
+ apiVersion: 2,
21
23
  name: "http",
22
24
  build: (ctx) => {
23
25
  if (!ctx.resolveConfirmation || !ctx.reads) {
package/openapi.yaml CHANGED
@@ -69,7 +69,7 @@ paths:
69
69
  schema:
70
70
  $ref: "#/components/schemas/TurnEvent"
71
71
  "400":
72
- description: Missing/invalid body.
72
+ description: Missing/invalid body, or a conversationId with characters outside the allowed ones.
73
73
  content:
74
74
  application/json:
75
75
  schema:
@@ -98,7 +98,7 @@ paths:
98
98
  schema:
99
99
  $ref: "#/components/schemas/ConfirmResult"
100
100
  "400":
101
- description: Missing token or conversationId.
101
+ description: Missing token or conversationId, or a conversationId with characters outside the allowed ones.
102
102
  content:
103
103
  application/json:
104
104
  schema:
@@ -110,7 +110,7 @@ paths:
110
110
  - name: id
111
111
  in: query
112
112
  required: true
113
- description: The conversationId (session key) whose messages to return.
113
+ description: The session key whose messages to return, as `/conversations` lists it (any channel's).
114
114
  schema:
115
115
  type: string
116
116
  - name: limit
@@ -304,7 +304,8 @@ components:
304
304
  description: The user's message, or a bare confirmation token.
305
305
  conversationId:
306
306
  type: string
307
- description: Opaque, client-owned. Omit to start a fresh ephemeral conversation.
307
+ pattern: "^[A-Za-z0-9_-]{1,128}$"
308
+ description: Opaque, client-owned; letters, digits, `-` and `_` (a UUID fits). Omit to start a fresh ephemeral conversation.
308
309
  TurnEvent:
309
310
  type: object
310
311
  description: The shape carried by each SSE `data:` line, per event type.
@@ -350,6 +351,7 @@ components:
350
351
  type: string
351
352
  conversationId:
352
353
  type: string
354
+ pattern: "^[A-Za-z0-9_-]{1,128}$"
353
355
  ConfirmResult:
354
356
  type: object
355
357
  required: [ok, resolved]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/channel-http",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,11 +18,11 @@
18
18
  "access": "public"
19
19
  },
20
20
  "peerDependencies": {
21
- "@mercury-fw/channel-types": ">=0.24.0 <1.0.0"
21
+ "@mercury-fw/channel-types": ">=0.34.0 <1.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@mercury-fw/channel-types": "0.31.1",
25
- "@mercury-fw/plugin-types": "0.31.1",
24
+ "@mercury-fw/channel-types": "0.34.0",
25
+ "@mercury-fw/plugin-types": "0.34.0",
26
26
  "@mercury-fw/typescript-config": "*",
27
27
  "@types/bun": "^1.4.2",
28
28
  "typescript": "^6.0.3"