@mercury-fw/channel-http 0.8.2 → 0.8.4
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 +20 -0
- package/README.md +2 -2
- package/dist/http-provider.d.ts +2 -0
- package/dist/http-server.d.ts +5 -1
- package/http-provider.ts +3 -0
- package/http-server.ts +21 -7
- package/index.ts +1 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @mercury-fw/channel-http
|
|
2
2
|
|
|
3
|
+
## 0.8.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 62bb3a3: - `persona.name` is the assistant's name, Mercury when left out. A name that's blank, longer than 40 characters or on more than one line stops the app at startup.
|
|
8
|
+
- Every text the model reads refers to the name as `{{assistant}}`, and the core puts the persona's name there: the persona's identity and tone, the system prompt, plugin fragments, skills and tool descriptions. The model no longer takes "Mercury" for its own name when the app calls its assistant something else.
|
|
9
|
+
- Where the one acting is the system around the model, the texts say "the system" or "this instance" instead of Mercury: the wiki tools, the cli-engine's errors, the jira skill and its issue lists.
|
|
10
|
+
- Plugins get `assistantName` in their `build()` context, and channels in their runtime context, for texts they build while running. The login messages, the linking reply and the HTTP login page name the assistant that way.
|
|
11
|
+
- The tools a person runs as the assistant itself are `<tool>AsSelf`, previously `<tool>AsMercury`. The `mercury.act-as-self` roles are unchanged.
|
|
12
|
+
- `mfw create` writes the assistant's name as `persona.name`, with `persona/identity.md` and `persona/tone.md` referring to it. An existing app keeps working as it is: to rename its assistant everywhere, add `name` to its persona and write `{{assistant}}` in its persona files.
|
|
13
|
+
|
|
14
|
+
## 0.8.3
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 3d73e59: - `auth` in `mercury.config.ts` takes a list of providers as well as one: they're tried in order and the first that recognizes the caller decides who they are. One that fails to load, or fails on a request, is logged and the others still serve; a name taken twice is refused.
|
|
19
|
+
- `createOidcAuth({ name })` is an OIDC provider of its own (`oidc-<name>`), configured by `OIDC_<NAME>_ISSUER` and `OIDC_<NAME>_AUDIENCE`, so an instance can accept several issuers. `oidcAuth` is unchanged.
|
|
20
|
+
- A principal's provider can be `oidc-<name>`; each provider's people keep their own keys.
|
|
21
|
+
- `zitadelDirectory` takes `ZITADEL_AUTH_PROVIDER`, the auth provider whose subjects are its users (default `oidc`).
|
|
22
|
+
|
|
3
23
|
## 0.8.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ channels: [httpChannel],
|
|
|
14
14
|
auth: oidcAuth,
|
|
15
15
|
```
|
|
16
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
|
|
17
|
+
It needs an auth provider next to it, or several (`auth: [a, b]`, tried in order): [`@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 when none of them loads, the channel doesn't start and the log says why.
|
|
18
18
|
|
|
19
19
|
| Variable | |
|
|
20
20
|
|---|---|
|
|
@@ -27,7 +27,7 @@ It needs an auth provider next to it: [`@mercury-fw/auth-oidc`](../../auth/auth-
|
|
|
27
27
|
|
|
28
28
|
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>`.
|
|
29
29
|
|
|
30
|
-
**Authentication.** Every route except `GET /openapi.yaml`, `GET /login/callback` and the `OPTIONS` preflights needs `Authorization: Bearer <token>`, and the auth
|
|
30
|
+
**Authentication.** Every route except `GET /openapi.yaml`, `GET /login/callback` and the `OPTIONS` preflights needs `Authorization: Bearer <token>`, and the auth providers decide who the token belongs to: the first that recognizes it. A missing or refused token gets `401` with `WWW-Authenticate: Bearer` before anything runs. Then the core decides whether it talks to that person at all: someone the app's directory doesn't know gets `403` (unless the app lets unknown people in), and `503` when the directory can't be reached, both with `{ ok: false, error, reason }`, where `error` is what to tell them.
|
|
31
31
|
|
|
32
32
|
**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.
|
|
33
33
|
|
package/dist/http-provider.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export type HttpProviderDeps = {
|
|
|
36
36
|
linking?: ChannelLinking;
|
|
37
37
|
/** The surface's budget for a reopened conversation (`HTTP_SURFACE_RESUME_MAX_CHARS`). */
|
|
38
38
|
resumeMaxChars?: number;
|
|
39
|
+
/** What the pages people read call the assistant. */
|
|
40
|
+
assistantName?: string;
|
|
39
41
|
};
|
|
40
42
|
export declare function createHttpProvider(deps: HttpProviderDeps): Provider & {
|
|
41
43
|
stop(): Promise<void>;
|
package/dist/http-server.d.ts
CHANGED
|
@@ -102,10 +102,12 @@ export type CompleteLoginFn = (state: string, code: string) => Promise<LoginOutc
|
|
|
102
102
|
* in, with `code` and `state` (or `error`, when they refused). Public: the
|
|
103
103
|
* person's browser carries no token here, and the single-use `state` the core
|
|
104
104
|
* issued for them is what ties the request to their login. Answers with a
|
|
105
|
-
* plain page telling them how it went
|
|
105
|
+
* plain page telling them how it went, naming the assistant as the app does
|
|
106
|
+
* (`Mercury` from an older core that doesn't say).
|
|
106
107
|
*/
|
|
107
108
|
export declare function handleLoginCallback(req: Request, deps: {
|
|
108
109
|
complete: CompleteLoginFn;
|
|
110
|
+
assistantName?: string;
|
|
109
111
|
}): Promise<Response>;
|
|
110
112
|
/** A single read route: its `GET` handler plus the shared `OPTIONS` preflight. */
|
|
111
113
|
type ReadRoute = {
|
|
@@ -133,6 +135,8 @@ export type HttpServerDeps = TurnRequestDeps & {
|
|
|
133
135
|
completeLogin?: CompleteLoginFn;
|
|
134
136
|
/** Linking another account to the caller; `/link` is mounted only with it. */
|
|
135
137
|
linking?: ChannelLinking;
|
|
138
|
+
/** What the pages people read call the assistant. */
|
|
139
|
+
assistantName?: string;
|
|
136
140
|
};
|
|
137
141
|
/** Starts the HTTP surface: `POST /turn` (4a) plus the read-only routes (4b)
|
|
138
142
|
* when `reads` is supplied. */
|
package/http-provider.ts
CHANGED
|
@@ -33,6 +33,8 @@ export type HttpProviderDeps = {
|
|
|
33
33
|
linking?: ChannelLinking;
|
|
34
34
|
/** The surface's budget for a reopened conversation (`HTTP_SURFACE_RESUME_MAX_CHARS`). */
|
|
35
35
|
resumeMaxChars?: number;
|
|
36
|
+
/** What the pages people read call the assistant. */
|
|
37
|
+
assistantName?: string;
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop(): Promise<void> } {
|
|
@@ -51,6 +53,7 @@ export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop():
|
|
|
51
53
|
completeLogin: deps.logins?.complete,
|
|
52
54
|
...(deps.linking === undefined ? {} : { linking: deps.linking }),
|
|
53
55
|
...(deps.resumeMaxChars === undefined ? {} : { resumeMaxChars: deps.resumeMaxChars }),
|
|
56
|
+
...(deps.assistantName === undefined ? {} : { assistantName: deps.assistantName }),
|
|
54
57
|
});
|
|
55
58
|
deps.logins?.accept(deps.logins.callbackUrl);
|
|
56
59
|
},
|
package/http-server.ts
CHANGED
|
@@ -352,18 +352,23 @@ function loginPage(text: string, status: number): Response {
|
|
|
352
352
|
* in, with `code` and `state` (or `error`, when they refused). Public: the
|
|
353
353
|
* person's browser carries no token here, and the single-use `state` the core
|
|
354
354
|
* issued for them is what ties the request to their login. Answers with a
|
|
355
|
-
* plain page telling them how it went
|
|
355
|
+
* plain page telling them how it went, naming the assistant as the app does
|
|
356
|
+
* (`Mercury` from an older core that doesn't say).
|
|
356
357
|
*/
|
|
357
|
-
export async function handleLoginCallback(
|
|
358
|
+
export async function handleLoginCallback(
|
|
359
|
+
req: Request,
|
|
360
|
+
deps: { complete: CompleteLoginFn; assistantName?: string },
|
|
361
|
+
): Promise<Response> {
|
|
362
|
+
const assistant = deps.assistantName ?? "Mercury";
|
|
358
363
|
const params = new URL(req.url).searchParams;
|
|
359
364
|
const error = params.get("error");
|
|
360
365
|
if (error !== null) {
|
|
361
366
|
const reason = error.replace(/[^A-Za-z0-9_.-]/g, "").slice(0, 64);
|
|
362
|
-
return loginPage(`The login didn't happen (${reason}). Ask
|
|
367
|
+
return loginPage(`The login didn't happen (${reason}). Ask ${assistant} again for a new link.`, 400);
|
|
363
368
|
}
|
|
364
369
|
const code = params.get("code");
|
|
365
370
|
const state = params.get("state");
|
|
366
|
-
if (!code || !state) return loginPage(
|
|
371
|
+
if (!code || !state) return loginPage(`The login link came back without a code or a state. Ask ${assistant} again for a new link.`, 400);
|
|
367
372
|
const outcome = await deps.complete(state, code);
|
|
368
373
|
return outcome.ok
|
|
369
374
|
? loginPage(`You're logged in to ${outcome.service}. Go back to the conversation and ask again.`, 200)
|
|
@@ -371,8 +376,15 @@ export async function handleLoginCallback(req: Request, deps: { complete: Comple
|
|
|
371
376
|
}
|
|
372
377
|
|
|
373
378
|
/** The login callback route, mounted when the server can complete logins. */
|
|
374
|
-
function loginRoutes(
|
|
375
|
-
|
|
379
|
+
function loginRoutes(
|
|
380
|
+
complete: CompleteLoginFn,
|
|
381
|
+
assistantName: string | undefined,
|
|
382
|
+
): Record<string, { GET: (req: Request) => Promise<Response> }> {
|
|
383
|
+
return {
|
|
384
|
+
"/login/callback": {
|
|
385
|
+
GET: (req) => handleLoginCallback(req, { complete, ...(assistantName === undefined ? {} : { assistantName }) }),
|
|
386
|
+
},
|
|
387
|
+
};
|
|
376
388
|
}
|
|
377
389
|
|
|
378
390
|
/** A single read route: its `GET` handler plus the shared `OPTIONS` preflight. */
|
|
@@ -471,6 +483,8 @@ export type HttpServerDeps = TurnRequestDeps & {
|
|
|
471
483
|
completeLogin?: CompleteLoginFn;
|
|
472
484
|
/** Linking another account to the caller; `/link` is mounted only with it. */
|
|
473
485
|
linking?: ChannelLinking;
|
|
486
|
+
/** What the pages people read call the assistant. */
|
|
487
|
+
assistantName?: string;
|
|
474
488
|
};
|
|
475
489
|
|
|
476
490
|
/** Starts the HTTP surface: `POST /turn` (4a) plus the read-only routes (4b)
|
|
@@ -502,7 +516,7 @@ export function startHttpServer(deps: HttpServerDeps): ReturnType<typeof Bun.ser
|
|
|
502
516
|
},
|
|
503
517
|
"/openapi.yaml": { GET: () => openApiResponse(origin), OPTIONS: () => preflight(origin) },
|
|
504
518
|
...(deps.reads ? readRoutes(deps.reads, deps.authenticate, origin, deps.admit) : {}),
|
|
505
|
-
...(deps.completeLogin ? loginRoutes(deps.completeLogin) : {}),
|
|
519
|
+
...(deps.completeLogin ? loginRoutes(deps.completeLogin, deps.assistantName) : {}),
|
|
506
520
|
...(deps.linking ? linkRoutes({ ...deps, start: deps.linking.start, corsOrigin: origin }) : {}),
|
|
507
521
|
},
|
|
508
522
|
error: (err) =>
|
package/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ export const httpChannel: ChannelPlugin = {
|
|
|
46
46
|
admit: ctx.admit,
|
|
47
47
|
...(ctx.linking === undefined ? {} : { linking: ctx.linking }),
|
|
48
48
|
...(resumeMaxChars === undefined ? {} : { resumeMaxChars }),
|
|
49
|
+
...(ctx.assistantName === undefined ? {} : { assistantName: ctx.assistantName }),
|
|
49
50
|
...(logins ? { logins: { callbackUrl: `${publicUrl}/login/callback`, accept: logins.accept, complete: logins.complete } } : {}),
|
|
50
51
|
});
|
|
51
52
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-http",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"@mercury-fw/channel-types": ">=0.45.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.48.0",
|
|
25
|
+
"@mercury-fw/plugin-types": "0.48.0",
|
|
26
26
|
"@mercury-fw/typescript-config": "*",
|
|
27
27
|
"@types/bun": "^1.4.2",
|
|
28
28
|
"typescript": "^6.0.3"
|