@mercury-fw/channel-http 0.6.0 → 0.7.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 +14 -0
- package/README.md +4 -0
- package/dist/http-provider.d.ts +3 -1
- package/dist/http-server.d.ts +17 -1
- package/http-provider.ts +4 -1
- package/http-server.ts +31 -0
- package/index.ts +1 -0
- package/openapi.yaml +39 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @mercury-fw/channel-http
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2c63bbf: One person across accounts: linking them by a code.
|
|
8
|
+
|
|
9
|
+
- `POST /link` on the HTTP surface hands the caller a one-time code (`xxxx-xxxx-xxxx`, ten minutes, single use). They send it from the other account, on any channel, twice. The first time Mercury says whom the account would be linked to, the second time it links it.
|
|
10
|
+
- From then on the account is the person it's linked to: one private area, one memory, the same roles. Its earlier area stays where it was, and undoing the link gives it back.
|
|
11
|
+
- A link never touches the terminal, never chains, and is refused for an account the directory already knows as someone else.
|
|
12
|
+
- The links live on the vault's volume, out of the vault's git and out of reach of the model and the HTTP reads.
|
|
13
|
+
- `mfw identity links | link | unlink` lets the operator manage them, for instances where nobody can be shown a code.
|
|
14
|
+
- Channels get the code through the confirmation hook they already call (`ctx.confirm`), and the HTTP surface gets `ctx.linking`.
|
|
15
|
+
- Fixed: `ctx.confirm` refused someone the core doesn't admit before checking that their text was a token at all. On Google Chat, every message from an unknown sender came back as the refusal.
|
|
16
|
+
|
|
3
17
|
## 0.6.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -78,6 +78,10 @@ Where a person comes back after logging in to a service a plugin acts on as them
|
|
|
78
78
|
|
|
79
79
|
Whoever opens the link and accepts decides which account gets linked: a link belongs to the person it was shown to, and someone else accepting it would link their own account to that person's Mercury identity. A UI should say so next to the link. Checking the linked account against the person is [#179](https://github.com/lucabro81/mercury-fw/issues/179).
|
|
80
80
|
|
|
81
|
+
## `POST /link`
|
|
82
|
+
|
|
83
|
+
A code for linking another of the caller's accounts to them, for the UI to show: `{ ok: true, code, expiresAt }`, or `400` with the reason when there's none. The person sends the code from the other account, on any channel, twice: the first time Mercury says whom the account would be linked to, the second time it links it, and from then on both accounts are one person (one private area, the same roles). The code lasts ten minutes, works once, and only the caller sees it; the model never does. It belongs to the first account that sends it, so it should be sent in a one-to-one conversation, never in a shared space where someone else could send it first. Over HTTP only an account the instance already admits can send it (anyone else is refused before the turn), so an account a closed instance doesn't know links from a chat.
|
|
84
|
+
|
|
81
85
|
## `POST /confirm`
|
|
82
86
|
|
|
83
87
|
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.
|
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 AdmitFn, type CompleteLoginFn, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
|
|
14
|
-
import type { Authenticate, Provider, ChannelHostReads } from "@mercury-fw/channel-types";
|
|
14
|
+
import type { Authenticate, Provider, ChannelHostReads, ChannelLinking } from "@mercury-fw/channel-types";
|
|
15
15
|
export type HttpProviderDeps = {
|
|
16
16
|
port: number;
|
|
17
17
|
/** Bare-token interception before the model. */
|
|
@@ -32,6 +32,8 @@ export type HttpProviderDeps = {
|
|
|
32
32
|
accept: (callbackUrl: string) => void;
|
|
33
33
|
complete: CompleteLoginFn;
|
|
34
34
|
};
|
|
35
|
+
/** Linking another account to the caller (`POST /link`). */
|
|
36
|
+
linking?: ChannelLinking;
|
|
35
37
|
};
|
|
36
38
|
export declare function createHttpProvider(deps: HttpProviderDeps): Provider & {
|
|
37
39
|
stop(): Promise<void>;
|
package/dist/http-server.d.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Confirm resolution is injected too (`confirm`/`resolveConfirmation`), so this
|
|
23
23
|
* package never imports the app.
|
|
24
24
|
*/
|
|
25
|
-
import { type HandleTurn, type Authenticate, type ChannelHostReads, type ConfirmOutcome, type LoginOutcome, type Principal, type Admission } from "@mercury-fw/channel-types";
|
|
25
|
+
import { type HandleTurn, type Authenticate, type ChannelHostReads, type ConfirmOutcome, type LoginOutcome, type Principal, type Admission, type ChannelLinking } from "@mercury-fw/channel-types";
|
|
26
26
|
/** Resolves a bare confirmation token to a reply string, or `null` if the input isn't a token. Injected by the core (`ctx.confirm`). */
|
|
27
27
|
export type ConfirmFn = (token: string, sessionKey: string, principal: Principal) => Promise<string | null>;
|
|
28
28
|
/** The structured sibling of {@link ConfirmFn}, for the `/confirm` `resolved` flag. Injected by the core (`ctx.resolveConfirmation`). */
|
|
@@ -79,6 +79,20 @@ export type ConfirmRequestDeps = {
|
|
|
79
79
|
* even token-shaped). Never touches the model.
|
|
80
80
|
*/
|
|
81
81
|
export declare function handleConfirmRequest(req: Request, deps: ConfirmRequestDeps): Promise<Response>;
|
|
82
|
+
export type LinkRequestDeps = {
|
|
83
|
+
authenticate: Authenticate;
|
|
84
|
+
admit?: AdmitFn;
|
|
85
|
+
/** Hands the caller a link code, injected by the core (`ctx.linking.start`). */
|
|
86
|
+
start: ChannelLinking["start"];
|
|
87
|
+
corsOrigin?: string;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* `POST /link` — a one-time code for linking another account to the caller
|
|
91
|
+
* (the custom UI shows it). The person then sends it from the other account,
|
|
92
|
+
* on any channel. Nobody but the caller ever sees it, the model included, and
|
|
93
|
+
* it isn't cached. `400` with the core's reason when there's no code for them.
|
|
94
|
+
*/
|
|
95
|
+
export declare function handleLinkRequest(req: Request, deps: LinkRequestDeps): Promise<Response>;
|
|
82
96
|
/** Finishes a person's login, injected by the core (`ChannelLogins.complete`). */
|
|
83
97
|
export type CompleteLoginFn = (state: string, code: string) => Promise<LoginOutcome>;
|
|
84
98
|
/**
|
|
@@ -115,6 +129,8 @@ export type HttpServerDeps = TurnRequestDeps & {
|
|
|
115
129
|
resolveConfirmation: ResolveConfirmationFn;
|
|
116
130
|
/** Finishes people's logins; `/login/callback` is mounted only with it. */
|
|
117
131
|
completeLogin?: CompleteLoginFn;
|
|
132
|
+
/** Linking another account to the caller; `/link` is mounted only with it. */
|
|
133
|
+
linking?: ChannelLinking;
|
|
118
134
|
};
|
|
119
135
|
/** Starts the HTTP surface: `POST /turn` (4a) plus the read-only routes (4b)
|
|
120
136
|
* when `reads` is supplied. */
|
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 AdmitFn, type CompleteLoginFn, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
|
|
14
|
-
import type { Authenticate, Provider, HandleTurn, ChannelHostReads } from "@mercury-fw/channel-types";
|
|
14
|
+
import type { Authenticate, Provider, HandleTurn, ChannelHostReads, ChannelLinking } from "@mercury-fw/channel-types";
|
|
15
15
|
|
|
16
16
|
export type HttpProviderDeps = {
|
|
17
17
|
port: number;
|
|
@@ -29,6 +29,8 @@ export type HttpProviderDeps = {
|
|
|
29
29
|
/** People's logins, when the surface has a public URL to be sent back to:
|
|
30
30
|
* the callback is offered (`accept`) once the server listens. */
|
|
31
31
|
logins?: { callbackUrl: string; accept: (callbackUrl: string) => void; complete: CompleteLoginFn };
|
|
32
|
+
/** Linking another account to the caller (`POST /link`). */
|
|
33
|
+
linking?: ChannelLinking;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop(): Promise<void> } {
|
|
@@ -45,6 +47,7 @@ export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop():
|
|
|
45
47
|
...(deps.admit === undefined ? {} : { admit: deps.admit }),
|
|
46
48
|
corsOrigin: deps.corsOrigin,
|
|
47
49
|
completeLogin: deps.logins?.complete,
|
|
50
|
+
...(deps.linking === undefined ? {} : { linking: deps.linking }),
|
|
48
51
|
});
|
|
49
52
|
deps.logins?.accept(deps.logins.callbackUrl);
|
|
50
53
|
},
|
package/http-server.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type LoginOutcome,
|
|
35
35
|
type Principal,
|
|
36
36
|
type Admission,
|
|
37
|
+
type ChannelLinking,
|
|
37
38
|
} from "@mercury-fw/channel-types";
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -305,6 +306,33 @@ export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDep
|
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
export type LinkRequestDeps = {
|
|
310
|
+
authenticate: Authenticate;
|
|
311
|
+
admit?: AdmitFn;
|
|
312
|
+
/** Hands the caller a link code, injected by the core (`ctx.linking.start`). */
|
|
313
|
+
start: ChannelLinking["start"];
|
|
314
|
+
corsOrigin?: string;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* `POST /link` — a one-time code for linking another account to the caller
|
|
319
|
+
* (the custom UI shows it). The person then sends it from the other account,
|
|
320
|
+
* on any channel. Nobody but the caller ever sees it, the model included, and
|
|
321
|
+
* it isn't cached. `400` with the core's reason when there's no code for them.
|
|
322
|
+
*/
|
|
323
|
+
export async function handleLinkRequest(req: Request, deps: LinkRequestDeps): Promise<Response> {
|
|
324
|
+
const origin = deps.corsOrigin ?? "*";
|
|
325
|
+
const principal = await caller(req, deps, origin);
|
|
326
|
+
if (principal instanceof Response) return principal;
|
|
327
|
+
const started = await deps.start(principal);
|
|
328
|
+
return Response.json(started, { status: started.ok ? 200 : 400, headers: { ...corsHeaders(origin), "cache-control": "no-store" } });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** `POST /link` and its preflight, mounted when the core offers linking. */
|
|
332
|
+
function linkRoutes(deps: LinkRequestDeps): Record<string, { POST: (req: Request) => Promise<Response>; OPTIONS: () => Response }> {
|
|
333
|
+
return { "/link": { POST: (req) => handleLinkRequest(req, deps), OPTIONS: () => preflight(deps.corsOrigin ?? "*") } };
|
|
334
|
+
}
|
|
335
|
+
|
|
308
336
|
/** Finishes a person's login, injected by the core (`ChannelLogins.complete`). */
|
|
309
337
|
export type CompleteLoginFn = (state: string, code: string) => Promise<LoginOutcome>;
|
|
310
338
|
|
|
@@ -439,6 +467,8 @@ export type HttpServerDeps = TurnRequestDeps & {
|
|
|
439
467
|
resolveConfirmation: ResolveConfirmationFn;
|
|
440
468
|
/** Finishes people's logins; `/login/callback` is mounted only with it. */
|
|
441
469
|
completeLogin?: CompleteLoginFn;
|
|
470
|
+
/** Linking another account to the caller; `/link` is mounted only with it. */
|
|
471
|
+
linking?: ChannelLinking;
|
|
442
472
|
};
|
|
443
473
|
|
|
444
474
|
/** Starts the HTTP surface: `POST /turn` (4a) plus the read-only routes (4b)
|
|
@@ -471,6 +501,7 @@ export function startHttpServer(deps: HttpServerDeps): ReturnType<typeof Bun.ser
|
|
|
471
501
|
"/openapi.yaml": { GET: () => openApiResponse(origin), OPTIONS: () => preflight(origin) },
|
|
472
502
|
...(deps.reads ? readRoutes(deps.reads, deps.authenticate, origin, deps.admit) : {}),
|
|
473
503
|
...(deps.completeLogin ? loginRoutes(deps.completeLogin) : {}),
|
|
504
|
+
...(deps.linking ? linkRoutes({ ...deps, start: deps.linking.start, corsOrigin: origin }) : {}),
|
|
474
505
|
},
|
|
475
506
|
error: (err) =>
|
|
476
507
|
Response.json(
|
package/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ export const httpChannel: ChannelPlugin = {
|
|
|
43
43
|
reads: ctx.reads,
|
|
44
44
|
authenticate: ctx.authenticate,
|
|
45
45
|
admit: ctx.admit,
|
|
46
|
+
...(ctx.linking === undefined ? {} : { linking: ctx.linking }),
|
|
46
47
|
...(logins ? { logins: { callbackUrl: `${publicUrl}/login/callback`, accept: logins.accept, complete: logins.complete } } : {}),
|
|
47
48
|
});
|
|
48
49
|
},
|
package/openapi.yaml
CHANGED
|
@@ -146,6 +146,45 @@ paths:
|
|
|
146
146
|
application/json:
|
|
147
147
|
schema:
|
|
148
148
|
$ref: "#/components/schemas/Error"
|
|
149
|
+
/link:
|
|
150
|
+
post:
|
|
151
|
+
summary: A code for linking another account to the caller
|
|
152
|
+
description: >
|
|
153
|
+
A one-time code (`xxxx-xxxx-xxxx`, valid for ten minutes) the person
|
|
154
|
+
sends from another account, on any channel, twice: the first time
|
|
155
|
+
Mercury says whom the account would be linked to, the second time it
|
|
156
|
+
links it. From then on both accounts are one person. Only the caller
|
|
157
|
+
sees the code; the model never does.
|
|
158
|
+
responses:
|
|
159
|
+
"401":
|
|
160
|
+
$ref: "#/components/responses/Unauthorized"
|
|
161
|
+
"403":
|
|
162
|
+
$ref: "#/components/responses/NotAdmitted"
|
|
163
|
+
"503":
|
|
164
|
+
$ref: "#/components/responses/NotAdmitted"
|
|
165
|
+
"400":
|
|
166
|
+
description: No code for this caller, with the reason.
|
|
167
|
+
content:
|
|
168
|
+
application/json:
|
|
169
|
+
schema:
|
|
170
|
+
$ref: "#/components/schemas/Error"
|
|
171
|
+
"200":
|
|
172
|
+
description: The code and when it expires.
|
|
173
|
+
content:
|
|
174
|
+
application/json:
|
|
175
|
+
schema:
|
|
176
|
+
type: object
|
|
177
|
+
required: [ok, code, expiresAt]
|
|
178
|
+
properties:
|
|
179
|
+
ok:
|
|
180
|
+
type: boolean
|
|
181
|
+
enum: [true]
|
|
182
|
+
code:
|
|
183
|
+
type: string
|
|
184
|
+
pattern: "^[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$"
|
|
185
|
+
expiresAt:
|
|
186
|
+
type: string
|
|
187
|
+
format: date-time
|
|
149
188
|
/conversation:
|
|
150
189
|
get:
|
|
151
190
|
summary: A conversation's durable transcript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mercury-fw/channel-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"@mercury-fw/channel-types": ">=0.36.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.44.0",
|
|
25
|
+
"@mercury-fw/plugin-types": "0.44.0",
|
|
26
26
|
"@mercury-fw/typescript-config": "*",
|
|
27
27
|
"@types/bun": "^1.4.2",
|
|
28
28
|
"typescript": "^6.0.3"
|