@mercury-fw/channel-http 0.2.0 → 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 +21 -0
- package/README.md +16 -7
- package/dist/http-provider.d.ts +3 -1
- package/dist/http-server.d.ts +27 -17
- package/dist/index.d.ts +4 -3
- package/http-provider.ts +4 -1
- package/http-server.ts +74 -38
- package/index.ts +8 -3
- package/openapi.yaml +69 -9
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
3
24
|
## 0.2.0
|
|
4
25
|
|
|
5
26
|
### Minor 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
|
|
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
|
|
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,7 +44,7 @@ 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
|
|
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
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
|
|
|
@@ -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,6 +66,7 @@ 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
|
|
|
@@ -67,11 +76,11 @@ Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, con
|
|
|
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=<
|
|
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 |
|
package/dist/http-provider.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/http-server.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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 `*`. */
|
|
@@ -39,25 +43,29 @@ export type TurnRequestDeps = {
|
|
|
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
|
|
43
|
-
* `{ text, conversationId? }`;
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
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
|
|
59
|
-
* through the injected `resolveConfirmation`,
|
|
60
|
-
*
|
|
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. `
|
|
76
|
-
*
|
|
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,9 +7,10 @@
|
|
|
7
7
|
* declared, it starts.
|
|
8
8
|
*
|
|
9
9
|
* `build` reads its port and CORS origin from `env` and takes the confirm
|
|
10
|
-
* capabilities
|
|
11
|
-
* inversion). Those
|
|
12
|
-
* context missing them throws — the loader isolates it
|
|
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
15
|
import type { ChannelPlugin } from "@mercury-fw/channel-types";
|
|
15
16
|
export { createHttpProvider, type HttpProviderDeps } from "./http-provider.ts";
|
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,27 +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
|
-
*
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
|
29
32
|
/**
|
|
30
|
-
* A client's conversation id becomes the session key
|
|
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.
|
|
33
37
|
*/
|
|
34
38
|
const CONVERSATION_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
35
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
|
+
|
|
36
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`). */
|
|
37
46
|
export type ConfirmFn = (token: string, sessionKey: string, userId: string) => Promise<string | null>;
|
|
38
47
|
/** The structured sibling of {@link ConfirmFn}, for the `/confirm` `resolved` flag. Injected by the core (`ctx.resolveConfirmation`). */
|
|
@@ -40,6 +49,8 @@ export type ResolveConfirmationFn = (token: string, sessionKey: string, userId:
|
|
|
40
49
|
|
|
41
50
|
export type TurnRequestDeps = {
|
|
42
51
|
handleTurn: HandleTurn;
|
|
52
|
+
/** Who is calling, injected from the app's auth provider; `null` = refused. */
|
|
53
|
+
authenticate: Authenticate;
|
|
43
54
|
/** Bare-token interception before the model, injected by the core. */
|
|
44
55
|
confirm: ConfirmFn;
|
|
45
56
|
/** Allowed CORS origin echoed back to a browser UI; defaults to `*`. */
|
|
@@ -57,17 +68,27 @@ const SSE_HEADERS = {
|
|
|
57
68
|
/**
|
|
58
69
|
* The CORS headers echoed on every response so a browser UI served from a
|
|
59
70
|
* different origin (the separate custom-UI project) can call this surface.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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`.
|
|
62
75
|
*/
|
|
63
76
|
function corsHeaders(origin: string): Record<string, string> {
|
|
64
77
|
return {
|
|
65
78
|
"access-control-allow-origin": origin,
|
|
66
79
|
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
67
|
-
"access-control-allow-headers": "content-type",
|
|
80
|
+
"access-control-allow-headers": "content-type, authorization",
|
|
68
81
|
};
|
|
69
82
|
}
|
|
70
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
|
+
|
|
71
92
|
/** 204 preflight response for an `OPTIONS` request, carrying only CORS headers. */
|
|
72
93
|
function preflight(origin: string): Response {
|
|
73
94
|
return new Response(null, { status: 204, headers: corsHeaders(origin) });
|
|
@@ -87,17 +108,21 @@ export function openApiResponse(corsOrigin = "*"): Response {
|
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
/**
|
|
90
|
-
* Runs one `POST /turn` request and returns an SSE stream Response. The
|
|
91
|
-
* `{ text, conversationId? }`;
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
|
94
117
|
* one-on-one (`multiUser: false`). No `conversationId` means a fresh ephemeral
|
|
95
|
-
* session. Never throws: a
|
|
96
|
-
* 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.
|
|
97
120
|
*/
|
|
98
121
|
export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Promise<Response> {
|
|
99
122
|
const origin = deps.corsOrigin ?? "*";
|
|
100
123
|
const cors = corsHeaders(origin);
|
|
124
|
+
const principal = await deps.authenticate(req);
|
|
125
|
+
if (principal === null) return unauthorized(origin);
|
|
101
126
|
let body: { text?: unknown; conversationId?: unknown };
|
|
102
127
|
try {
|
|
103
128
|
body = (await req.json()) as typeof body;
|
|
@@ -108,13 +133,14 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
108
133
|
return Response.json({ ok: false, error: "missing text" }, { status: 400, headers: cors });
|
|
109
134
|
}
|
|
110
135
|
const text = body.text;
|
|
111
|
-
const
|
|
136
|
+
const conversationId =
|
|
112
137
|
typeof body.conversationId === "string" && body.conversationId.trim().length > 0
|
|
113
138
|
? body.conversationId.trim()
|
|
114
139
|
: (deps.newSessionKey ?? (() => crypto.randomUUID()))();
|
|
115
|
-
if (!CONVERSATION_ID.test(
|
|
140
|
+
if (!CONVERSATION_ID.test(conversationId)) {
|
|
116
141
|
return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
|
|
117
142
|
}
|
|
143
|
+
const sessionKey = `${principal.id}:${conversationId}`;
|
|
118
144
|
|
|
119
145
|
// Aborted when the client disconnects (see the stream's `cancel` below) so
|
|
120
146
|
// the in-flight turn stops instead of running to completion — the "stop"
|
|
@@ -143,7 +169,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
143
169
|
|
|
144
170
|
// Same deterministic interception as every other channel — a
|
|
145
171
|
// previously-approved mutation must never depend on the model.
|
|
146
|
-
const confirmReply = await deps.confirm(text, sessionKey,
|
|
172
|
+
const confirmReply = await deps.confirm(text, sessionKey, principal.id);
|
|
147
173
|
if (confirmReply !== null) {
|
|
148
174
|
send("final", { text: confirmReply });
|
|
149
175
|
close();
|
|
@@ -151,7 +177,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
151
177
|
}
|
|
152
178
|
|
|
153
179
|
const sink: TurnSink = {
|
|
154
|
-
onToolStart: (label, detail, toolCallId) => send("tool", { label, detail, toolCallId }),
|
|
180
|
+
onToolStart: (label, detail, toolCallId, toolName) => send("tool", { label, detail, toolCallId, name: toolName }),
|
|
155
181
|
onToolFinish: (toolCallId, outcome) => send("tool_finish", { toolCallId, outcome }),
|
|
156
182
|
onTextChunk: (chunk) => {
|
|
157
183
|
if (chunk !== PENDING_CONFIRMATION_NOTE) send("text", { chunk });
|
|
@@ -174,10 +200,9 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
174
200
|
multiUser: false,
|
|
175
201
|
text,
|
|
176
202
|
sessionKey,
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
logPrefix: `[http:${sessionKey}] `,
|
|
203
|
+
principal,
|
|
204
|
+
// The caller's id comes from the auth provider, unchecked here.
|
|
205
|
+
logPrefix: `[http:${forLog(sessionKey)}] `,
|
|
181
206
|
abortSignal: abort.signal,
|
|
182
207
|
},
|
|
183
208
|
sink,
|
|
@@ -202,6 +227,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
|
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
export type ConfirmRequestDeps = {
|
|
230
|
+
authenticate: Authenticate;
|
|
205
231
|
resolveConfirmation: ResolveConfirmationFn;
|
|
206
232
|
corsOrigin?: string;
|
|
207
233
|
};
|
|
@@ -209,16 +235,20 @@ export type ConfirmRequestDeps = {
|
|
|
209
235
|
/**
|
|
210
236
|
* `POST /confirm { token, conversationId }` — the explicit confirmation
|
|
211
237
|
* endpoint. A nicer contract for a UI than re-POSTing the bare token as `text`
|
|
212
|
-
* to `/turn`, but the exact same mechanism underneath: it
|
|
213
|
-
* through the injected `resolveConfirmation`,
|
|
214
|
-
*
|
|
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
|
|
215
242
|
* pending confirmation and its staged action was consumed and run; the `text`
|
|
216
243
|
* then reports whether that execution succeeded. `resolved: false` means the
|
|
217
244
|
* token was not a pending confirmation (unknown, expired, already used, or not
|
|
218
245
|
* even token-shaped). Never touches the model.
|
|
219
246
|
*/
|
|
220
247
|
export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDeps): Promise<Response> {
|
|
221
|
-
const
|
|
248
|
+
const origin = deps.corsOrigin ?? "*";
|
|
249
|
+
const cors = corsHeaders(origin);
|
|
250
|
+
const principal = await deps.authenticate(req);
|
|
251
|
+
if (principal === null) return unauthorized(origin);
|
|
222
252
|
let body: { token?: unknown; conversationId?: unknown };
|
|
223
253
|
try {
|
|
224
254
|
body = (await req.json()) as typeof body;
|
|
@@ -226,14 +256,14 @@ export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDep
|
|
|
226
256
|
return Response.json({ ok: false, error: "body must be JSON" }, { status: 400, headers: cors });
|
|
227
257
|
}
|
|
228
258
|
const token = typeof body.token === "string" ? body.token.trim() : "";
|
|
229
|
-
const
|
|
230
|
-
if (token.length === 0 ||
|
|
259
|
+
const conversationId = typeof body.conversationId === "string" ? body.conversationId.trim() : "";
|
|
260
|
+
if (token.length === 0 || conversationId.length === 0) {
|
|
231
261
|
return Response.json({ ok: false, error: "missing token or conversationId" }, { status: 400, headers: cors });
|
|
232
262
|
}
|
|
233
|
-
if (!CONVERSATION_ID.test(
|
|
263
|
+
if (!CONVERSATION_ID.test(conversationId)) {
|
|
234
264
|
return Response.json({ ok: false, error: "invalid conversationId" }, { status: 400, headers: cors });
|
|
235
265
|
}
|
|
236
|
-
const outcome = await deps.resolveConfirmation(token,
|
|
266
|
+
const outcome = await deps.resolveConfirmation(token, `${principal.id}:${conversationId}`, principal.id);
|
|
237
267
|
switch (outcome.status) {
|
|
238
268
|
case "not-a-token":
|
|
239
269
|
case "not-found":
|
|
@@ -254,17 +284,22 @@ type ReadRoute = {
|
|
|
254
284
|
/**
|
|
255
285
|
* Builds the read-only routes, each carrying CORS headers on its `GET` and a
|
|
256
286
|
* shared `OPTIONS` preflight so a browser UI on `corsOrigin` (default `*`) can
|
|
257
|
-
* reach them. `
|
|
258
|
-
*
|
|
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.
|
|
259
291
|
*/
|
|
260
|
-
export function readRoutes(reads: ChannelHostReads, corsOrigin = "*"): Record<string, ReadRoute> {
|
|
292
|
+
export function readRoutes(reads: ChannelHostReads, authenticate: Authenticate, corsOrigin = "*"): Record<string, ReadRoute> {
|
|
261
293
|
const cors = corsHeaders(corsOrigin);
|
|
262
294
|
const json = (payload: object, status = 200): Response =>
|
|
263
295
|
Response.json(payload, { status, headers: cors });
|
|
264
296
|
const badRequest = (message: string): Response => json({ ok: false, error: message }, 400);
|
|
265
297
|
const requireParam = (req: Request, name: string): string | null => new URL(req.url).searchParams.get(name);
|
|
266
298
|
const options = () => preflight(corsOrigin);
|
|
267
|
-
const route = (GET: ReadRoute["GET"]): ReadRoute => ({
|
|
299
|
+
const route = (GET: ReadRoute["GET"]): ReadRoute => ({
|
|
300
|
+
GET: async (req) => ((await authenticate(req)) === null ? unauthorized(corsOrigin) : GET(req)),
|
|
301
|
+
OPTIONS: options,
|
|
302
|
+
});
|
|
268
303
|
return {
|
|
269
304
|
"/manifest": route(() => json({ ok: true, manifest: reads.manifest() })),
|
|
270
305
|
"/confirmations": route(() => json({ ok: true, pending: reads.pendingConfirmations() })),
|
|
@@ -329,11 +364,12 @@ export function startHttpServer(deps: HttpServerDeps): ReturnType<typeof Bun.ser
|
|
|
329
364
|
routes: {
|
|
330
365
|
"/turn": { POST: (req) => handleTurnRequest(req, deps), OPTIONS: () => preflight(origin) },
|
|
331
366
|
"/confirm": {
|
|
332
|
-
POST: (req) =>
|
|
367
|
+
POST: (req) =>
|
|
368
|
+
handleConfirmRequest(req, { authenticate: deps.authenticate, resolveConfirmation: deps.resolveConfirmation, corsOrigin: origin }),
|
|
333
369
|
OPTIONS: () => preflight(origin),
|
|
334
370
|
},
|
|
335
371
|
"/openapi.yaml": { GET: () => openApiResponse(origin), OPTIONS: () => preflight(origin) },
|
|
336
|
-
...(deps.reads ? readRoutes(deps.reads, origin) : {}),
|
|
372
|
+
...(deps.reads ? readRoutes(deps.reads, deps.authenticate, origin) : {}),
|
|
337
373
|
},
|
|
338
374
|
error: (err) =>
|
|
339
375
|
Response.json(
|
package/index.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* declared, it starts.
|
|
8
8
|
*
|
|
9
9
|
* `build` reads its port and CORS origin from `env` and takes the confirm
|
|
10
|
-
* capabilities
|
|
11
|
-
* inversion). Those
|
|
12
|
-
* context missing them throws — the loader isolates it
|
|
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
15
|
import type { ChannelPlugin } from "@mercury-fw/channel-types";
|
|
15
16
|
import { createHttpProvider } from "./http-provider.ts";
|
|
@@ -25,12 +26,16 @@ export const httpChannel: ChannelPlugin = {
|
|
|
25
26
|
if (!ctx.resolveConfirmation || !ctx.reads) {
|
|
26
27
|
throw new Error("http channel requires the resolveConfirmation and reads capabilities on the runtime context");
|
|
27
28
|
}
|
|
29
|
+
if (!ctx.authenticate) {
|
|
30
|
+
throw new Error("http channel requires an auth provider (auth in mercury.config.ts)");
|
|
31
|
+
}
|
|
28
32
|
return createHttpProvider({
|
|
29
33
|
port: Number(ctx.env.HTTP_SURFACE_PORT ?? "4100"),
|
|
30
34
|
corsOrigin: ctx.env.HTTP_SURFACE_CORS_ORIGIN ?? "*",
|
|
31
35
|
confirm: ctx.confirm,
|
|
32
36
|
resolveConfirmation: ctx.resolveConfirmation,
|
|
33
37
|
reads: ctx.reads,
|
|
38
|
+
authenticate: ctx.authenticate,
|
|
34
39
|
});
|
|
35
40
|
},
|
|
36
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`
|
|
9
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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`:
|
|
@@ -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
|
|
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
|
|
@@ -110,7 +127,7 @@ paths:
|
|
|
110
127
|
- name: id
|
|
111
128
|
in: query
|
|
112
129
|
required: true
|
|
113
|
-
description: The session key whose messages to return, as `/conversations` lists it (any channel's).
|
|
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
|
|
@@ -325,6 +382,9 @@ components:
|
|
|
325
382
|
detail:
|
|
326
383
|
type: string
|
|
327
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.
|
|
328
388
|
toolCallId:
|
|
329
389
|
type: string
|
|
330
390
|
description: On `tool`/`tool_finish`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-http",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
21
|
+
"@mercury-fw/channel-types": ">=0.35.0 <1.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@mercury-fw/channel-types": "0.
|
|
25
|
-
"@mercury-fw/plugin-types": "0.
|
|
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"
|