@mercury-fw/channel-http 0.1.1 → 0.3.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,37 @@
1
1
  # @mercury-fw/channel-http
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8d3e5af: Authentication for the HTTP surface.
8
+
9
+ - An app declares an auth provider as `auth` in `mercury.config.ts`. The core builds it and hands channels `ctx.authenticate`. A provider that fails to load leaves the channel without it, so the surface stays closed.
10
+ - New contract in `@mercury-fw/channel-types`: `AuthPlugin`, `AUTH_API_VERSION` and `Authenticate`. `PrincipalProvider` gains `oidc` and `static`.
11
+ - New package `@mercury-fw/auth-oidc` verifies an OpenID Connect bearer token against the issuer's keys and checks `iss`, `aud` and expiry. It reads `OIDC_ISSUER` and `OIDC_AUDIENCE`.
12
+ - New package `@mercury-fw/auth-static` maps fixed test tokens from `AUTH_STATIC_TOKENS` to users, for test apps and e2e tests.
13
+ - Breaking, `@mercury-fw/channel-http`: the channel doesn't start without an auth provider.
14
+ - `@mercury-fw/channel-http`: every route except `GET /openapi.yaml` and the `OPTIONS` preflights needs `Authorization: Bearer <token>`, and a missing or refused token gets `401` before anything runs.
15
+ - Breaking, `@mercury-fw/channel-http`: a conversation belongs to whoever opened it. The session key is `<caller id>:<conversationId>`, so the same id from someone else is a conversation of their own. Existing HTTP conversations stay readable but can't be continued.
16
+ - `@mercury-fw/channel-http`: a confirmation token is checked against the caller's own session, so it can't be confirmed from someone else's.
17
+ - `@mercury-fw/channel-http`: authenticated callers get their own episodic memory and wiki area.
18
+ - `@mercury-fw/channel-http`: the read routes (`/conversations`, `/conversation`, `/wiki/*`, `/memory/scroll`, `/tool-log`, `/confirmations`) need an authenticated caller, but still show every person's data. Scoping them per person comes next, so don't give the surface to people who shouldn't see each other's conversations yet. Confirmation tokens are never listed.
19
+ - The HTTP `tool` event carries the name of the tool the model called. `TurnSink.onToolStart` gets it as an optional fourth argument.
20
+ - `mfw create` pairs the HTTP channel with an auth provider: `--auth oidc|static`, which the wizard asks for once HTTP is chosen.
21
+ - `mfw e2e` runs cases on the HTTP surface (`channel: "http"`) as one of the test's `users`, each a token the app accepts. A turn can name its own user, and it reports the response's status.
22
+ - `mfw create --local-packages <folder>` makes an app on packed tarballs, as `mfw local-packages` does on an existing one. Versions come from the tarballs, so a package that isn't published yet works too.
23
+
24
+ ## 0.2.0
25
+
26
+ ### Minor Changes
27
+
28
+ - 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.
29
+ - 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.
30
+ - 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.
31
+ - The model's wiki read tools refuse a per-user id that isn't one path segment, as writes already did.
32
+ - `CHANNEL_API_VERSION` is 2: a channel written for version 1 is refused at load.
33
+ - 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.
34
+
3
35
  ## 0.1.1
4
36
 
5
37
  ### Patch Changes
package/README.md CHANGED
@@ -8,10 +8,14 @@ bun add @mercury-fw/channel-http
8
8
 
9
9
  ```ts
10
10
  import { httpChannel } from "@mercury-fw/channel-http";
11
+ import { oidcAuth } from "@mercury-fw/auth-oidc";
11
12
 
12
13
  channels: [httpChannel],
14
+ auth: oidcAuth,
13
15
  ```
14
16
 
17
+ It needs an auth provider next to it: [`@mercury-fw/auth-oidc`](../../auth/auth-oidc) for real users, [`@mercury-fw/auth-static`](../../auth/auth-static) for the test bed and e2e tests. Without one, or with one that fails to load, the channel doesn't start and the log says why.
18
+
15
19
  | Variable | |
16
20
  |---|---|
17
21
  | `HTTP_SURFACE_PORT` | The port (default `4100`). |
@@ -19,13 +23,17 @@ channels: [httpChannel],
19
23
 
20
24
  ## API
21
25
 
22
- It's active whenever it's declared in `mercury.config.ts`'s `channels`; remove it there to turn the surface off. It listens on `HTTP_SURFACE_PORT` (default `4100`). **No authentication** — do not publish the port outside the container network (same posture as the admin panel). Base URL `http://<host>:<port>`.
26
+ It's active whenever it's declared in `mercury.config.ts`'s `channels` with an auth provider; remove it there to turn the surface off. It listens on `HTTP_SURFACE_PORT` (default `4100`). Base URL `http://<host>:<port>`.
27
+
28
+ **Authentication.** Every route except `GET /openapi.yaml` and the `OPTIONS` preflights needs `Authorization: Bearer <token>`, and the auth provider decides who the token belongs to. A missing or refused token gets `401` with `WWW-Authenticate: Bearer` before anything runs.
29
+
30
+ **Conversations belong to whoever opened them.** The session key is `<caller id>:<conversationId>`, so the same `conversationId` sent by someone else is a conversation of their own, and a confirmation token staged in your conversation can't be confirmed from theirs. Each authenticated caller also gets their own episodic memory and wiki area, as on Google Chat.
23
31
 
24
- **CORS** is enabled on every response and every route answers an `OPTIONS` preflight, so a browser UI on another origin can call it. The allowed origin is `HTTP_SURFACE_CORS_ORIGIN` (default `*`; no credentials are used).
32
+ **CORS** is enabled on every response and every route answers an `OPTIONS` preflight, so a browser UI on another origin can call it. The allowed origin is `HTTP_SURFACE_CORS_ORIGIN` (default `*`: the token travels in a header, never in a cookie the browser would send on its own).
25
33
 
26
34
  The full contract is described by an **OpenAPI document**, served raw at `GET /openapi.yaml` and published as a rendered docs page on GitHub Pages (see `packages/channels/channel-http/openapi.yaml`, the single source of truth).
27
35
 
28
- All responses are JSON except `POST /turn`, which streams `text/event-stream`. Every JSON response is either `{ "ok": true, ... }` or, on error, `{ "ok": false, "error": "<message>" }` with HTTP `400`/`500`.
36
+ All responses are JSON except `POST /turn`, which streams `text/event-stream`. Every JSON response is either `{ "ok": true, ... }` or, on error, `{ "ok": false, "error": "<message>" }` with HTTP `400`/`401`/`500`.
29
37
 
30
38
  ## `POST /turn`
31
39
 
@@ -36,9 +44,9 @@ Runs one conversational turn; the reply streams back as Server-Sent Events.
36
44
  | field | type | required | description |
37
45
  |---|---|---|---|
38
46
  | `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. |
47
+ | `conversationId` | string | no | Opaque, client-owned id that continues one of your conversations: letters, digits, `-` and `_`, up to 128 characters (a UUID fits) once surrounding whitespace is trimmed, `400` otherwise. Omitted ⇒ a fresh one-off session. |
40
48
 
41
- **Responses**: `200 text/event-stream` (the events below); `400` if `text` is missing or the body isn't JSON.
49
+ **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
50
 
43
51
  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
52
 
@@ -48,7 +56,7 @@ Reasoning and answer text arrive as **incremental deltas** — never one finishe
48
56
  |---|---|---|
49
57
  | `reasoning` | `{ chunk, id }` | a model reasoning delta |
50
58
  | `reasoning_end` | `{ id, failed }` | a reasoning block ends |
51
- | `tool` | `{ label, detail, toolCallId }` | a tool call starts |
59
+ | `tool` | `{ label, detail, toolCallId, name }` | a tool call starts; `name` is the tool the model called, absent when Mercury is saving something to memory |
52
60
  | `tool_finish` | `{ toolCallId, outcome }` | a tool call settles (`outcome`: `success` \| `failed` \| `pending`) |
53
61
  | `text` | `{ chunk }` | an answer-text delta |
54
62
  | `pending` | `{ command, token }` | a confirm-required action was staged; send `token` back to confirm it — as a later `/turn` `text`, or via `POST /confirm` |
@@ -58,20 +66,21 @@ Reasoning and answer text arrive as **incremental deltas** — never one finishe
58
66
  ```bash
59
67
  curl -N -X POST http://localhost:4100/turn \
60
68
  -H 'content-type: application/json' \
69
+ -H "authorization: Bearer $TOKEN" \
61
70
  -d '{"text":"Quante issue nel progetto KAN?","conversationId":"c1"}'
62
71
  ```
63
72
 
64
73
  ## `POST /confirm`
65
74
 
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.
75
+ 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
76
 
68
77
  ## Read-only introspection
69
78
 
70
- All `GET`, all JSON, all reporting state already held in-process.
79
+ All `GET`, all JSON, all reporting state already held in-process. They need an authenticated caller, but each one still shows every person's data: scoping them per person is the next step.
71
80
 
72
81
  | endpoint | `data` on success |
73
82
  |---|---|
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 |
83
+ | `GET /conversation?id=<sessionKey>&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
84
  | `GET /conversations?limit=<n>` | `{ conversations: [{ sessionKey, lastTimestamp, preview }] }` — known conversations, most-recently-active first |
76
85
  | `GET /manifest` | `{ manifest: { coreApiVersion, plugins: [{ name, apiVersion, active, skills, hasBuild, customStatus }], activeClis, skills } }` |
77
86
  | `GET /confirmations` | `{ pending: [{ sessionKey, binary, args, expiresAt }] }` — tokens are deliberately never included |
@@ -11,7 +11,7 @@
11
11
  * injected (`confirm`/`resolveConfirmation`), so this package never imports the app.
12
12
  */
13
13
  import { type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
14
- import type { Provider, ChannelHostReads } from "@mercury-fw/channel-types";
14
+ import type { Authenticate, Provider, ChannelHostReads } from "@mercury-fw/channel-types";
15
15
  export type HttpProviderDeps = {
16
16
  port: number;
17
17
  /** Bare-token interception before the model. */
@@ -19,6 +19,8 @@ export type HttpProviderDeps = {
19
19
  /** Structured resolver for the `/confirm` endpoint's `resolved` flag. */
20
20
  resolveConfirmation: ResolveConfirmationFn;
21
21
  reads?: ChannelHostReads;
22
+ /** Who is calling, from the app's auth provider. */
23
+ authenticate: Authenticate;
22
24
  /** Allowed CORS origin echoed to a browser UI; defaults to `*` in the server. */
23
25
  corsOrigin?: string;
24
26
  };
@@ -12,18 +12,22 @@
12
12
  * is unit-tested without standing up a socket. The read-only routes (4b) mount
13
13
  * alongside `/turn` here, driven by the injected `reads` getters.
14
14
  *
15
- * No auth by design — this surface is opt-in (enabled by listing this channel in
16
- * `mercury.config.ts`) and must not be reachable from outside the container
17
- * network, the same posture as the admin panel. Confirm resolution is injected
18
- * (`confirm`/`resolveConfirmation`), so this package never imports the app.
15
+ * Every route but the OpenAPI document and the CORS preflights asks the
16
+ * injected `authenticate` (the auth provider the app declares) who is calling
17
+ * before anything runs, and answers 401 when nobody is. A conversation belongs
18
+ * to whoever opened it: the session key is `<principal.id>:<conversationId>`.
19
+ * Confirm resolution is injected too (`confirm`/`resolveConfirmation`), so this
20
+ * package never imports the app.
19
21
  */
20
- import { type HandleTurn, type ChannelHostReads, type ConfirmOutcome } from "@mercury-fw/channel-types";
22
+ import { type HandleTurn, type Authenticate, type ChannelHostReads, type ConfirmOutcome } from "@mercury-fw/channel-types";
21
23
  /** Resolves a bare confirmation token to a reply string, or `null` if the input isn't a token. Injected by the core (`ctx.confirm`). */
22
24
  export type ConfirmFn = (token: string, sessionKey: string, userId: string) => Promise<string | null>;
23
25
  /** The structured sibling of {@link ConfirmFn}, for the `/confirm` `resolved` flag. Injected by the core (`ctx.resolveConfirmation`). */
24
26
  export type ResolveConfirmationFn = (token: string, sessionKey: string, userId: string) => Promise<ConfirmOutcome>;
25
27
  export type TurnRequestDeps = {
26
28
  handleTurn: HandleTurn;
29
+ /** Who is calling, injected from the app's auth provider; `null` = refused. */
30
+ authenticate: Authenticate;
27
31
  /** Bare-token interception before the model, injected by the core. */
28
32
  confirm: ConfirmFn;
29
33
  /** Allowed CORS origin echoed back to a browser UI; defaults to `*`. */
@@ -33,31 +37,35 @@ export type TurnRequestDeps = {
33
37
  };
34
38
  /**
35
39
  * 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
40
+ * of truth in this package's `openapi.yaml`, also rendered as the GitHub Pages
37
41
  * API docs. Served raw as `text/yaml` (no YAML parser needed at runtime); most
38
42
  * tooling (Redoc, Swagger UI, Postman) reads YAML directly.
39
43
  */
40
44
  export declare function openApiResponse(corsOrigin?: string): Response;
41
45
  /**
42
- * Runs one `POST /turn` request and returns an SSE stream Response. The body is
43
- * `{ text, conversationId? }`; `conversationId` (opaque, client-owned) becomes
44
- * the session key so a client can continue a conversation — Mercury already
45
- * keys its histories by session, so many conversations run concurrently, each
46
+ * Runs one `POST /turn` request and returns an SSE stream Response. The caller
47
+ * is authenticated first. The body is `{ text, conversationId? }`;
48
+ * `conversationId` (opaque, client-owned) continues one of the caller's
49
+ * conversations: the session key is `<principal.id>:<conversationId>`, so the
50
+ * same id from someone else is a session of their own. Mercury keys its
51
+ * histories by session, so many conversations run concurrently, each
46
52
  * one-on-one (`multiUser: false`). No `conversationId` means a fresh ephemeral
47
- * session. Never throws: a bad body is a 400, a mid-turn failure is an `error`
48
- * event on the stream.
53
+ * session. Never throws: a refused caller is a 401, a bad body a 400, a
54
+ * mid-turn failure an `error` event on the stream.
49
55
  */
50
56
  export declare function handleTurnRequest(req: Request, deps: TurnRequestDeps): Promise<Response>;
51
57
  export type ConfirmRequestDeps = {
58
+ authenticate: Authenticate;
52
59
  resolveConfirmation: ResolveConfirmationFn;
53
60
  corsOrigin?: string;
54
61
  };
55
62
  /**
56
63
  * `POST /confirm { token, conversationId }` — the explicit confirmation
57
64
  * endpoint. A nicer contract for a UI than re-POSTing the bare token as `text`
58
- * to `/turn`, but the exact same mechanism underneath: it resolves the token
59
- * through the injected `resolveConfirmation`, keyed on
60
- * `conversationId` as the session. `resolved: true` means the token matched a
65
+ * to `/turn`, but the exact same mechanism underneath: it authenticates the
66
+ * caller and resolves the token through the injected `resolveConfirmation`,
67
+ * keyed on the caller's own session (`<principal.id>:<conversationId>`), so a
68
+ * token staged in someone else's conversation is never found. `resolved: true` means the token matched a
61
69
  * pending confirmation and its staged action was consumed and run; the `text`
62
70
  * then reports whether that execution succeeded. `resolved: false` means the
63
71
  * token was not a pending confirmation (unknown, expired, already used, or not
@@ -72,10 +80,12 @@ type ReadRoute = {
72
80
  /**
73
81
  * Builds the read-only routes, each carrying CORS headers on its `GET` and a
74
82
  * shared `OPTIONS` preflight so a browser UI on `corsOrigin` (default `*`) can
75
- * reach them. `jsonRoute` wraps a getter into a `GET` that always JSON-encodes
76
- * with the CORS headers merged in; `badRequest` does the same for a 400.
83
+ * reach them. Every `GET` asks `authenticate` first and answers 401 without
84
+ * calling its getter when nobody is calling; the preflight doesn't ask.
85
+ * `json` JSON-encodes with the CORS headers merged in; `badRequest` does the
86
+ * same for a 400.
77
87
  */
78
- export declare function readRoutes(reads: ChannelHostReads, corsOrigin?: string): Record<string, ReadRoute>;
88
+ export declare function readRoutes(reads: ChannelHostReads, authenticate: Authenticate, corsOrigin?: string): Record<string, ReadRoute>;
79
89
  export type HttpServerDeps = TurnRequestDeps & {
80
90
  port: number;
81
91
  reads?: ChannelHostReads;
package/dist/index.d.ts CHANGED
@@ -7,10 +7,11 @@
7
7
  * declared, it starts.
8
8
  *
9
9
  * `build` reads its port and CORS origin from `env` and takes the confirm
10
- * capabilities and in-process reads from the injected context (dependency
11
- * inversion). Those two are optional on the contract but required here, so a
12
- * context missing them throws — the loader isolates it fail-soft.
10
+ * capabilities, the in-process reads and the app's auth provider from the
11
+ * injected context (dependency inversion). Those are optional on the contract
12
+ * but required here, so a context missing them throws — the loader isolates it
13
+ * fail-soft, and without an auth provider the surface never opens.
13
14
  */
14
- import { type ChannelPlugin } from "@mercury-fw/channel-types";
15
+ import type { ChannelPlugin } from "@mercury-fw/channel-types";
15
16
  export { createHttpProvider, type HttpProviderDeps } from "./http-provider.ts";
16
17
  export declare const httpChannel: ChannelPlugin;
package/http-provider.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * injected (`confirm`/`resolveConfirmation`), so this package never imports the app.
12
12
  */
13
13
  import { startHttpServer, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
14
- import type { Provider, HandleTurn, ChannelHostReads } from "@mercury-fw/channel-types";
14
+ import type { Authenticate, Provider, HandleTurn, ChannelHostReads } from "@mercury-fw/channel-types";
15
15
 
16
16
  export type HttpProviderDeps = {
17
17
  port: number;
@@ -20,6 +20,8 @@ export type HttpProviderDeps = {
20
20
  /** Structured resolver for the `/confirm` endpoint's `resolved` flag. */
21
21
  resolveConfirmation: ResolveConfirmationFn;
22
22
  reads?: ChannelHostReads;
23
+ /** Who is calling, from the app's auth provider. */
24
+ authenticate: Authenticate;
23
25
  /** Allowed CORS origin echoed to a browser UI; defaults to `*` in the server. */
24
26
  corsOrigin?: string;
25
27
  };
@@ -34,6 +36,7 @@ export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop():
34
36
  confirm: deps.confirm,
35
37
  resolveConfirmation: deps.resolveConfirmation,
36
38
  reads: deps.reads,
39
+ authenticate: deps.authenticate,
37
40
  corsOrigin: deps.corsOrigin,
38
41
  });
39
42
  },
package/http-server.ts CHANGED
@@ -12,20 +12,36 @@
12
12
  * is unit-tested without standing up a socket. The read-only routes (4b) mount
13
13
  * alongside `/turn` here, driven by the injected `reads` getters.
14
14
  *
15
- * No auth by design — this surface is opt-in (enabled by listing this channel in
16
- * `mercury.config.ts`) and must not be reachable from outside the container
17
- * network, the same posture as the admin panel. Confirm resolution is injected
18
- * (`confirm`/`resolveConfirmation`), so this package never imports the app.
15
+ * Every route but the OpenAPI document and the CORS preflights asks the
16
+ * injected `authenticate` (the auth provider the app declares) who is calling
17
+ * before anything runs, and answers 401 when nobody is. A conversation belongs
18
+ * to whoever opened it: the session key is `<principal.id>:<conversationId>`.
19
+ * Confirm resolution is injected too (`confirm`/`resolveConfirmation`), so this
20
+ * package never imports the app.
19
21
  */
20
22
  import {
21
23
  detectPendingConfirmation,
22
24
  PENDING_CONFIRMATION_NOTE,
23
25
  type HandleTurn,
24
26
  type TurnSink,
27
+ type Authenticate,
25
28
  type ChannelHostReads,
26
29
  type ConfirmOutcome,
27
30
  } from "@mercury-fw/channel-types";
28
31
 
32
+ /**
33
+ * A client's conversation id becomes part of the session key and the log
34
+ * prefix, so only letters, digits, `-` and `_` get in: no path segments, no
35
+ * line breaks, nothing to encode, and no `:`, which separates it from the
36
+ * caller's id in the session key.
37
+ */
38
+ const CONVERSATION_ID = /^[A-Za-z0-9_-]{1,128}$/;
39
+
40
+ /** `text` with every control character (line breaks included) as `?`, for a log line nobody can forge. */
41
+ function forLog(text: string): string {
42
+ return text.replace(/[\u0000-\u001f\u007f]/g, "?");
43
+ }
44
+
29
45
  /** 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
46
  export type ConfirmFn = (token: string, sessionKey: string, userId: string) => Promise<string | null>;
31
47
  /** The structured sibling of {@link ConfirmFn}, for the `/confirm` `resolved` flag. Injected by the core (`ctx.resolveConfirmation`). */
@@ -33,6 +49,8 @@ export type ResolveConfirmationFn = (token: string, sessionKey: string, userId:
33
49
 
34
50
  export type TurnRequestDeps = {
35
51
  handleTurn: HandleTurn;
52
+ /** Who is calling, injected from the app's auth provider; `null` = refused. */
53
+ authenticate: Authenticate;
36
54
  /** Bare-token interception before the model, injected by the core. */
37
55
  confirm: ConfirmFn;
38
56
  /** Allowed CORS origin echoed back to a browser UI; defaults to `*`. */
@@ -50,17 +68,27 @@ const SSE_HEADERS = {
50
68
  /**
51
69
  * The CORS headers echoed on every response so a browser UI served from a
52
70
  * different origin (the separate custom-UI project) can call this surface.
53
- * No credentials are ever used here, so a wildcard origin is safe; a specific
54
- * origin can still be pinned via `HTTP_SURFACE_CORS_ORIGIN`.
71
+ * The caller's token travels in an explicit `Authorization` header, never in a
72
+ * cookie the browser would attach on its own, so a wildcard origin is safe: a
73
+ * page on another origin can't borrow anyone's credentials. A specific origin
74
+ * can still be pinned via `HTTP_SURFACE_CORS_ORIGIN`.
55
75
  */
56
76
  function corsHeaders(origin: string): Record<string, string> {
57
77
  return {
58
78
  "access-control-allow-origin": origin,
59
79
  "access-control-allow-methods": "GET, POST, OPTIONS",
60
- "access-control-allow-headers": "content-type",
80
+ "access-control-allow-headers": "content-type, authorization",
61
81
  };
62
82
  }
63
83
 
84
+ /** 401 for a caller the auth provider refused, with the Bearer challenge and CORS headers (so a browser UI can read it). */
85
+ function unauthorized(origin: string): Response {
86
+ return Response.json(
87
+ { ok: false, error: "unauthorized" },
88
+ { status: 401, headers: { ...corsHeaders(origin), "www-authenticate": "Bearer" } },
89
+ );
90
+ }
91
+
64
92
  /** 204 preflight response for an `OPTIONS` request, carrying only CORS headers. */
65
93
  function preflight(origin: string): Response {
66
94
  return new Response(null, { status: 204, headers: corsHeaders(origin) });
@@ -68,7 +96,7 @@ function preflight(origin: string): Response {
68
96
 
69
97
  /**
70
98
  * 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
99
+ * of truth in this package's `openapi.yaml`, also rendered as the GitHub Pages
72
100
  * API docs. Served raw as `text/yaml` (no YAML parser needed at runtime); most
73
101
  * tooling (Redoc, Swagger UI, Postman) reads YAML directly.
74
102
  */
@@ -80,17 +108,21 @@ export function openApiResponse(corsOrigin = "*"): Response {
80
108
  }
81
109
 
82
110
  /**
83
- * Runs one `POST /turn` request and returns an SSE stream Response. The body is
84
- * `{ text, conversationId? }`; `conversationId` (opaque, client-owned) becomes
85
- * the session key so a client can continue a conversation — Mercury already
86
- * keys its histories by session, so many conversations run concurrently, each
111
+ * Runs one `POST /turn` request and returns an SSE stream Response. The caller
112
+ * is authenticated first. The body is `{ text, conversationId? }`;
113
+ * `conversationId` (opaque, client-owned) continues one of the caller's
114
+ * conversations: the session key is `<principal.id>:<conversationId>`, so the
115
+ * same id from someone else is a session of their own. Mercury keys its
116
+ * histories by session, so many conversations run concurrently, each
87
117
  * one-on-one (`multiUser: false`). No `conversationId` means a fresh ephemeral
88
- * session. Never throws: a bad body is a 400, a mid-turn failure is an `error`
89
- * event on the stream.
118
+ * session. Never throws: a refused caller is a 401, a bad body a 400, a
119
+ * mid-turn failure an `error` event on the stream.
90
120
  */
91
121
  export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Promise<Response> {
92
122
  const origin = deps.corsOrigin ?? "*";
93
123
  const cors = corsHeaders(origin);
124
+ const principal = await deps.authenticate(req);
125
+ if (principal === null) return unauthorized(origin);
94
126
  let body: { text?: unknown; conversationId?: unknown };
95
127
  try {
96
128
  body = (await req.json()) as typeof body;
@@ -101,10 +133,14 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
101
133
  return Response.json({ ok: false, error: "missing text" }, { status: 400, headers: cors });
102
134
  }
103
135
  const text = body.text;
104
- const sessionKey =
136
+ const conversationId =
105
137
  typeof body.conversationId === "string" && body.conversationId.trim().length > 0
106
138
  ? body.conversationId.trim()
107
139
  : (deps.newSessionKey ?? (() => crypto.randomUUID()))();
140
+ if (!CONVERSATION_ID.test(conversationId)) {
141
+ return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
142
+ }
143
+ const sessionKey = `${principal.id}:${conversationId}`;
108
144
 
109
145
  // Aborted when the client disconnects (see the stream's `cancel` below) so
110
146
  // the in-flight turn stops instead of running to completion — the "stop"
@@ -133,7 +169,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
133
169
 
134
170
  // Same deterministic interception as every other channel — a
135
171
  // previously-approved mutation must never depend on the model.
136
- const confirmReply = await deps.confirm(text, sessionKey, sessionKey);
172
+ const confirmReply = await deps.confirm(text, sessionKey, principal.id);
137
173
  if (confirmReply !== null) {
138
174
  send("final", { text: confirmReply });
139
175
  close();
@@ -141,7 +177,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
141
177
  }
142
178
 
143
179
  const sink: TurnSink = {
144
- onToolStart: (label, detail, toolCallId) => send("tool", { label, detail, toolCallId }),
180
+ onToolStart: (label, detail, toolCallId, toolName) => send("tool", { label, detail, toolCallId, name: toolName }),
145
181
  onToolFinish: (toolCallId, outcome) => send("tool_finish", { toolCallId, outcome }),
146
182
  onTextChunk: (chunk) => {
147
183
  if (chunk !== PENDING_CONFIRMATION_NOTE) send("text", { chunk });
@@ -164,8 +200,9 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
164
200
  multiUser: false,
165
201
  text,
166
202
  sessionKey,
167
- wikiUserId: sessionKey,
168
- logPrefix: `[http:${sessionKey}] `,
203
+ principal,
204
+ // The caller's id comes from the auth provider, unchecked here.
205
+ logPrefix: `[http:${forLog(sessionKey)}] `,
169
206
  abortSignal: abort.signal,
170
207
  },
171
208
  sink,
@@ -190,6 +227,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
190
227
  }
191
228
 
192
229
  export type ConfirmRequestDeps = {
230
+ authenticate: Authenticate;
193
231
  resolveConfirmation: ResolveConfirmationFn;
194
232
  corsOrigin?: string;
195
233
  };
@@ -197,16 +235,20 @@ export type ConfirmRequestDeps = {
197
235
  /**
198
236
  * `POST /confirm { token, conversationId }` — the explicit confirmation
199
237
  * endpoint. A nicer contract for a UI than re-POSTing the bare token as `text`
200
- * to `/turn`, but the exact same mechanism underneath: it resolves the token
201
- * through the injected `resolveConfirmation`, keyed on
202
- * `conversationId` as the session. `resolved: true` means the token matched a
238
+ * to `/turn`, but the exact same mechanism underneath: it authenticates the
239
+ * caller and resolves the token through the injected `resolveConfirmation`,
240
+ * keyed on the caller's own session (`<principal.id>:<conversationId>`), so a
241
+ * token staged in someone else's conversation is never found. `resolved: true` means the token matched a
203
242
  * pending confirmation and its staged action was consumed and run; the `text`
204
243
  * then reports whether that execution succeeded. `resolved: false` means the
205
244
  * token was not a pending confirmation (unknown, expired, already used, or not
206
245
  * even token-shaped). Never touches the model.
207
246
  */
208
247
  export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDeps): Promise<Response> {
209
- const cors = corsHeaders(deps.corsOrigin ?? "*");
248
+ const origin = deps.corsOrigin ?? "*";
249
+ const cors = corsHeaders(origin);
250
+ const principal = await deps.authenticate(req);
251
+ if (principal === null) return unauthorized(origin);
210
252
  let body: { token?: unknown; conversationId?: unknown };
211
253
  try {
212
254
  body = (await req.json()) as typeof body;
@@ -214,11 +256,14 @@ export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDep
214
256
  return Response.json({ ok: false, error: "body must be JSON" }, { status: 400, headers: cors });
215
257
  }
216
258
  const token = typeof body.token === "string" ? body.token.trim() : "";
217
- const sessionKey = typeof body.conversationId === "string" ? body.conversationId.trim() : "";
218
- if (token.length === 0 || sessionKey.length === 0) {
259
+ const conversationId = typeof body.conversationId === "string" ? body.conversationId.trim() : "";
260
+ if (token.length === 0 || conversationId.length === 0) {
219
261
  return Response.json({ ok: false, error: "missing token or conversationId" }, { status: 400, headers: cors });
220
262
  }
221
- const outcome = await deps.resolveConfirmation(token, sessionKey, sessionKey);
263
+ if (!CONVERSATION_ID.test(conversationId)) {
264
+ return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
265
+ }
266
+ const outcome = await deps.resolveConfirmation(token, `${principal.id}:${conversationId}`, principal.id);
222
267
  switch (outcome.status) {
223
268
  case "not-a-token":
224
269
  case "not-found":
@@ -239,17 +284,22 @@ type ReadRoute = {
239
284
  /**
240
285
  * Builds the read-only routes, each carrying CORS headers on its `GET` and a
241
286
  * shared `OPTIONS` preflight so a browser UI on `corsOrigin` (default `*`) can
242
- * reach them. `jsonRoute` wraps a getter into a `GET` that always JSON-encodes
243
- * with the CORS headers merged in; `badRequest` does the same for a 400.
287
+ * reach them. Every `GET` asks `authenticate` first and answers 401 without
288
+ * calling its getter when nobody is calling; the preflight doesn't ask.
289
+ * `json` JSON-encodes with the CORS headers merged in; `badRequest` does the
290
+ * same for a 400.
244
291
  */
245
- export function readRoutes(reads: ChannelHostReads, corsOrigin = "*"): Record<string, ReadRoute> {
292
+ export function readRoutes(reads: ChannelHostReads, authenticate: Authenticate, corsOrigin = "*"): Record<string, ReadRoute> {
246
293
  const cors = corsHeaders(corsOrigin);
247
294
  const json = (payload: object, status = 200): Response =>
248
295
  Response.json(payload, { status, headers: cors });
249
296
  const badRequest = (message: string): Response => json({ ok: false, error: message }, 400);
250
297
  const requireParam = (req: Request, name: string): string | null => new URL(req.url).searchParams.get(name);
251
298
  const options = () => preflight(corsOrigin);
252
- const route = (GET: ReadRoute["GET"]): ReadRoute => ({ GET, OPTIONS: options });
299
+ const route = (GET: ReadRoute["GET"]): ReadRoute => ({
300
+ GET: async (req) => ((await authenticate(req)) === null ? unauthorized(corsOrigin) : GET(req)),
301
+ OPTIONS: options,
302
+ });
253
303
  return {
254
304
  "/manifest": route(() => json({ ok: true, manifest: reads.manifest() })),
255
305
  "/confirmations": route(() => json({ ok: true, pending: reads.pendingConfirmations() })),
@@ -314,11 +364,12 @@ export function startHttpServer(deps: HttpServerDeps): ReturnType<typeof Bun.ser
314
364
  routes: {
315
365
  "/turn": { POST: (req) => handleTurnRequest(req, deps), OPTIONS: () => preflight(origin) },
316
366
  "/confirm": {
317
- POST: (req) => handleConfirmRequest(req, { resolveConfirmation: deps.resolveConfirmation, corsOrigin: origin }),
367
+ POST: (req) =>
368
+ handleConfirmRequest(req, { authenticate: deps.authenticate, resolveConfirmation: deps.resolveConfirmation, corsOrigin: origin }),
318
369
  OPTIONS: () => preflight(origin),
319
370
  },
320
371
  "/openapi.yaml": { GET: () => openApiResponse(origin), OPTIONS: () => preflight(origin) },
321
- ...(deps.reads ? readRoutes(deps.reads, origin) : {}),
372
+ ...(deps.reads ? readRoutes(deps.reads, deps.authenticate, origin) : {}),
322
373
  },
323
374
  error: (err) =>
324
375
  Response.json(
package/index.ts CHANGED
@@ -7,28 +7,35 @@
7
7
  * declared, it starts.
8
8
  *
9
9
  * `build` reads its port and CORS origin from `env` and takes the confirm
10
- * capabilities and in-process reads from the injected context (dependency
11
- * inversion). Those two are optional on the contract but required here, so a
12
- * context missing them throws — the loader isolates it fail-soft.
10
+ * capabilities, the in-process reads and the app's auth provider from the
11
+ * injected context (dependency inversion). Those are optional on the contract
12
+ * but required here, so a context missing them throws — the loader isolates it
13
+ * fail-soft, and without an auth provider the surface never opens.
13
14
  */
14
- import { CHANNEL_API_VERSION, type ChannelPlugin } from "@mercury-fw/channel-types";
15
+ import type { ChannelPlugin } from "@mercury-fw/channel-types";
15
16
  import { createHttpProvider } from "./http-provider.ts";
16
17
 
17
18
  export { createHttpProvider, type HttpProviderDeps } from "./http-provider.ts";
18
19
 
19
20
  export const httpChannel: ChannelPlugin = {
20
- apiVersion: CHANNEL_API_VERSION,
21
+ // The contract this channel is written for, as a literal: importing
22
+ // CHANNEL_API_VERSION would report whichever contract is installed.
23
+ apiVersion: 2,
21
24
  name: "http",
22
25
  build: (ctx) => {
23
26
  if (!ctx.resolveConfirmation || !ctx.reads) {
24
27
  throw new Error("http channel requires the resolveConfirmation and reads capabilities on the runtime context");
25
28
  }
29
+ if (!ctx.authenticate) {
30
+ throw new Error("http channel requires an auth provider (auth in mercury.config.ts)");
31
+ }
26
32
  return createHttpProvider({
27
33
  port: Number(ctx.env.HTTP_SURFACE_PORT ?? "4100"),
28
34
  corsOrigin: ctx.env.HTTP_SURFACE_CORS_ORIGIN ?? "*",
29
35
  confirm: ctx.confirm,
30
36
  resolveConfirmation: ctx.resolveConfirmation,
31
37
  reads: ctx.reads,
38
+ authenticate: ctx.authenticate,
32
39
  });
33
40
  },
34
41
  };
package/openapi.yaml CHANGED
@@ -5,19 +5,30 @@ info:
5
5
  description: >
6
6
  The HTTP surface of Mercury — the intended primary channel for a
7
7
  custom web UI. It is enabled by declaring the `http` channel in
8
- `mercury.config.ts` and, by design,
9
- carries **no authentication**: it must not be published outside the trusted
10
- container network (production simply does not expose the port). CORS is
8
+ `mercury.config.ts`, together with an auth provider (`auth`): without one
9
+ the surface doesn't start. CORS is
11
10
  enabled on every response (`Access-Control-Allow-Origin`, configurable via
12
11
  `HTTP_SURFACE_CORS_ORIGIN`, default `*`) and every route answers an `OPTIONS`
13
12
  preflight, so a browser UI on another origin can call it.
14
13
 
15
14
 
15
+ ## Authentication
16
+
17
+ Every route except this document and the `OPTIONS` preflights needs
18
+ `Authorization: Bearer <token>`. The app's auth provider decides what a
19
+ valid token is (an OpenID Connect access token, a static test token); a
20
+ missing or refused one gets `401` with `WWW-Authenticate: Bearer`, before
21
+ anything runs.
22
+
23
+
16
24
  ## Conversations
17
25
 
18
- A conversation is identified by an opaque, client-owned `conversationId`,
19
- which becomes Mercury's session key. Omitting it on `POST /turn` starts a
20
- fresh ephemeral conversation. The durable transcript of a conversation is
26
+ A conversation is identified by an opaque, client-owned `conversationId`
27
+ and belongs to whoever opened it: Mercury's session key is
28
+ `<caller id>:<conversationId>`, so the same id sent by someone else is a
29
+ conversation of their own, and a confirmation token staged in one person's
30
+ conversation is never found from another's. Omitting `conversationId` on
31
+ `POST /turn` starts a fresh ephemeral conversation. The durable transcript of a conversation is
21
32
  readable via `GET /conversation`, and the list of known conversations via
22
33
  `GET /conversations` (both backed by the verbatim archive; they degrade to
23
34
  empty when the vector store is unreachable).
@@ -38,6 +49,8 @@ info:
38
49
  emits a `pending` event carrying a one-time `token`. The client confirms by
39
50
  sending that token back — either as the `text` of a follow-up `POST /turn`,
40
51
  or via `POST /confirm`.
52
+ security:
53
+ - bearerAuth: []
41
54
  servers:
42
55
  - url: http://localhost:4100
43
56
  description: Local/dev surface (HTTP_SURFACE_PORT, default 4100)
@@ -57,6 +70,8 @@ paths:
57
70
  schema:
58
71
  $ref: "#/components/schemas/TurnRequest"
59
72
  responses:
73
+ "401":
74
+ $ref: "#/components/responses/Unauthorized"
60
75
  "200":
61
76
  description: >
62
77
  An SSE stream. Each event is one of the types in `TurnEvent`:
@@ -69,7 +84,7 @@ paths:
69
84
  schema:
70
85
  $ref: "#/components/schemas/TurnEvent"
71
86
  "400":
72
- description: Missing/invalid body.
87
+ description: Missing/invalid body, or a conversationId with characters outside the allowed ones.
73
88
  content:
74
89
  application/json:
75
90
  schema:
@@ -79,8 +94,8 @@ paths:
79
94
  summary: Confirm a staged irreversible action
80
95
  description: >
81
96
  Runs a one-time confirmation token through the same mechanism the
82
- bare-token-in-`text` path uses, keyed on `conversationId`. Never touches
83
- the model.
97
+ bare-token-in-`text` path uses, keyed on the caller's own conversation
98
+ (`<caller id>:<conversationId>`). Never touches the model.
84
99
  requestBody:
85
100
  required: true
86
101
  content:
@@ -88,6 +103,8 @@ paths:
88
103
  schema:
89
104
  $ref: "#/components/schemas/ConfirmRequest"
90
105
  responses:
106
+ "401":
107
+ $ref: "#/components/responses/Unauthorized"
91
108
  "200":
92
109
  description: >
93
110
  `resolved: true` (with the reply `text`) when the token was a pending
@@ -98,7 +115,7 @@ paths:
98
115
  schema:
99
116
  $ref: "#/components/schemas/ConfirmResult"
100
117
  "400":
101
- description: Missing token or conversationId.
118
+ description: Missing token or conversationId, or a conversationId with characters outside the allowed ones.
102
119
  content:
103
120
  application/json:
104
121
  schema:
@@ -110,7 +127,7 @@ paths:
110
127
  - name: id
111
128
  in: query
112
129
  required: true
113
- description: The conversationId (session key) whose messages to return.
130
+ description: The session key whose messages to return, as `/conversations` lists it (any channel's; an HTTP one is `<caller id>:<conversationId>`).
114
131
  schema:
115
132
  type: string
116
133
  - name: limit
@@ -126,6 +143,8 @@ paths:
126
143
  schema:
127
144
  type: string
128
145
  responses:
146
+ "401":
147
+ $ref: "#/components/responses/Unauthorized"
129
148
  "200":
130
149
  description: The conversation's messages in chronological order.
131
150
  content:
@@ -149,6 +168,8 @@ paths:
149
168
  type: integer
150
169
  default: 50
151
170
  responses:
171
+ "401":
172
+ $ref: "#/components/responses/Unauthorized"
152
173
  "200":
153
174
  description: The conversation list.
154
175
  content:
@@ -159,6 +180,8 @@ paths:
159
180
  get:
160
181
  summary: Liveness/health of Mercury and its dependencies
161
182
  responses:
183
+ "401":
184
+ $ref: "#/components/responses/Unauthorized"
162
185
  "200":
163
186
  description: Health snapshot.
164
187
  content:
@@ -169,6 +192,8 @@ paths:
169
192
  get:
170
193
  summary: The loaded plugins, tools and skills
171
194
  responses:
195
+ "401":
196
+ $ref: "#/components/responses/Unauthorized"
172
197
  "200":
173
198
  description: Plugin manifest.
174
199
  content:
@@ -179,6 +204,8 @@ paths:
179
204
  get:
180
205
  summary: Currently pending confirmations (tokens redacted)
181
206
  responses:
207
+ "401":
208
+ $ref: "#/components/responses/Unauthorized"
182
209
  "200":
183
210
  description: Pending confirmations.
184
211
  content:
@@ -189,6 +216,8 @@ paths:
189
216
  get:
190
217
  summary: Recent tool-call log entries
191
218
  responses:
219
+ "401":
220
+ $ref: "#/components/responses/Unauthorized"
192
221
  "200":
193
222
  description: Tool log.
194
223
  content:
@@ -199,6 +228,8 @@ paths:
199
228
  get:
200
229
  summary: List the wiki vault's files
201
230
  responses:
231
+ "401":
232
+ $ref: "#/components/responses/Unauthorized"
202
233
  "200":
203
234
  description: File list.
204
235
  content:
@@ -215,6 +246,8 @@ paths:
215
246
  schema:
216
247
  type: string
217
248
  responses:
249
+ "401":
250
+ $ref: "#/components/responses/Unauthorized"
218
251
  "200":
219
252
  description: File content.
220
253
  content:
@@ -238,6 +271,8 @@ paths:
238
271
  schema:
239
272
  type: string
240
273
  responses:
274
+ "401":
275
+ $ref: "#/components/responses/Unauthorized"
241
276
  "200":
242
277
  description: Matches.
243
278
  content:
@@ -271,6 +306,8 @@ paths:
271
306
  schema:
272
307
  type: string
273
308
  responses:
309
+ "401":
310
+ $ref: "#/components/responses/Unauthorized"
274
311
  "200":
275
312
  description: A page of points.
276
313
  content:
@@ -286,6 +323,7 @@ paths:
286
323
  /openapi.yaml:
287
324
  get:
288
325
  summary: This OpenAPI document
326
+ security: []
289
327
  responses:
290
328
  "200":
291
329
  description: The raw OpenAPI YAML.
@@ -294,6 +332,25 @@ paths:
294
332
  schema:
295
333
  type: string
296
334
  components:
335
+ securitySchemes:
336
+ bearerAuth:
337
+ type: http
338
+ scheme: bearer
339
+ description: A token the app's auth provider accepts.
340
+ responses:
341
+ Unauthorized:
342
+ description: >
343
+ No `Authorization` header, or a token the auth provider refused.
344
+ Carries `WWW-Authenticate: Bearer`.
345
+ headers:
346
+ WWW-Authenticate:
347
+ schema:
348
+ type: string
349
+ enum: [Bearer]
350
+ content:
351
+ application/json:
352
+ schema:
353
+ $ref: "#/components/schemas/Error"
297
354
  schemas:
298
355
  TurnRequest:
299
356
  type: object
@@ -304,7 +361,8 @@ components:
304
361
  description: The user's message, or a bare confirmation token.
305
362
  conversationId:
306
363
  type: string
307
- description: Opaque, client-owned. Omit to start a fresh ephemeral conversation.
364
+ pattern: "^[A-Za-z0-9_-]{1,128}$"
365
+ description: Opaque, client-owned; letters, digits, `-` and `_` (a UUID fits). Omit to start a fresh ephemeral conversation.
308
366
  TurnEvent:
309
367
  type: object
310
368
  description: The shape carried by each SSE `data:` line, per event type.
@@ -324,6 +382,9 @@ components:
324
382
  detail:
325
383
  type: string
326
384
  description: On `tool` — optional detail.
385
+ name:
386
+ type: string
387
+ description: On `tool` — the tool the model called (e.g. `jiraCommand`); absent for Mercury's own memory notes.
327
388
  toolCallId:
328
389
  type: string
329
390
  description: On `tool`/`tool_finish`.
@@ -350,6 +411,7 @@ components:
350
411
  type: string
351
412
  conversationId:
352
413
  type: string
414
+ pattern: "^[A-Za-z0-9_-]{1,128}$"
353
415
  ConfirmResult:
354
416
  type: object
355
417
  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.3.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.35.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.35.0",
25
+ "@mercury-fw/plugin-types": "0.35.0",
26
26
  "@mercury-fw/typescript-config": "*",
27
27
  "@types/bun": "^1.4.2",
28
28
  "typescript": "^6.0.3"