@mercury-fw/channel-http 0.5.0 → 0.6.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,27 @@
1
1
  # @mercury-fw/channel-http
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a7f2f13: A user directory decides who people are and what they may make Mercury do.
8
+
9
+ - An app can declare a `directory` in `mercury.config.ts`. Given whoever a channel says is talking, it returns the person with their roles. The person's key is `<directory name>:<id>`, so one person reached from several channels keeps one private area, one memory and one set of confirmations.
10
+ - With a directory the instance is closed. Someone it doesn't know gets a refusal and nothing runs, unless `access.unknown` is `"allow"`; `access.unknownMessage` sets what they're told. A directory that can't be reached, or fails to load, refuses everyone but the terminal. Answers are cached for five minutes, so a revoked role stops counting within that time.
11
+ - `mercury.act-as-self` (every plugin) or `mercury.act-as-self.<plugin>` (one) lets a person make Mercury act with its own identity.
12
+ - A plugin acting as Mercury, such as atlassian-admin, is offered only to those people, and on the terminal.
13
+ - A plugin acting as the person also gives them each tool a second time, as `<tool>AsMercury`.
14
+ - Every call made as Mercury for someone is logged with who asked.
15
+ - `access.plugins` restricts a plugin to some roles. Prompts, skills and the manifest follow what each person is offered.
16
+ - New package `@mercury-fw/directory-static`: people, their identities on each channel and their roles, listed in `DIRECTORY_STATIC_PEOPLE`.
17
+ - The channel contract is at version 5:
18
+ - `ChannelRuntimeContext.admit` says whether the core talks to a caller;
19
+ - the reads are per person and async, the manifest included;
20
+ - `DirectoryPlugin` is the directory contract;
21
+ - `Principal` no longer carries `roles`.
22
+ - `channel-http` answers `403` to a caller the directory doesn't know and `503` when it can't be reached, on every route.
23
+ - `auth-static` refuses `roles` in `AUTH_STATIC_TOKENS`: roles come from the directory.
24
+
3
25
  ## 0.5.0
4
26
 
5
27
  ### Minor Changes
package/README.md CHANGED
@@ -26,7 +26,7 @@ It needs an auth provider next to it: [`@mercury-fw/auth-oidc`](../../auth/auth-
26
26
 
27
27
  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>`.
28
28
 
29
- **Authentication.** Every route except `GET /openapi.yaml`, `GET /login/callback` 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
+ **Authentication.** Every route except `GET /openapi.yaml`, `GET /login/callback` 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. 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.
30
30
 
31
31
  **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.
32
32
 
@@ -84,7 +84,7 @@ Explicit alternative to re-sending a token as `/turn` `text`. Body `{ token, con
84
84
 
85
85
  ## Read-only introspection
86
86
 
87
- All `GET`, all JSON, all reporting state already held in-process. They need an authenticated caller and show only what belongs to them: their conversations, confirmations, tool calls and memory, and the wiki as they see it (the team's `curated/` plus their own notes as `personal/`). Someone else's data doesn't exist as far as a caller can tell; only `/manifest` and `/health` are the same for everyone.
87
+ All `GET`, all JSON, all reporting state already held in-process. They need an authenticated caller and show only what belongs to them: their conversations, confirmations, tool calls and memory, and the wiki as they see it (the team's `curated/` plus their own notes as `personal/`). Someone else's data doesn't exist as far as a caller can tell. `/manifest` lists what the caller is offered (a plugin acting as Mercury only for someone allowed to use it), and only `/health` is the same for everyone.
88
88
 
89
89
  | endpoint | `data` on success |
90
90
  |---|---|
@@ -10,7 +10,7 @@
10
10
  * (that stays with Google Chat). `stop` closes the socket on shutdown. Confirm is
11
11
  * injected (`confirm`/`resolveConfirmation`), so this package never imports the app.
12
12
  */
13
- import { type CompleteLoginFn, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
13
+ import { type AdmitFn, type CompleteLoginFn, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
14
14
  import type { Authenticate, Provider, ChannelHostReads } from "@mercury-fw/channel-types";
15
15
  export type HttpProviderDeps = {
16
16
  port: number;
@@ -21,6 +21,8 @@ export type HttpProviderDeps = {
21
21
  reads?: ChannelHostReads;
22
22
  /** Who is calling, from the app's auth provider. */
23
23
  authenticate: Authenticate;
24
+ /** Whether the core talks to them, from the core. */
25
+ admit?: AdmitFn;
24
26
  /** Allowed CORS origin echoed to a browser UI; defaults to `*` in the server. */
25
27
  corsOrigin?: string;
26
28
  /** People's logins, when the surface has a public URL to be sent back to:
@@ -22,15 +22,18 @@
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 } from "@mercury-fw/channel-types";
25
+ import { type HandleTurn, type Authenticate, type ChannelHostReads, type ConfirmOutcome, type LoginOutcome, type Principal, type Admission } 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`). */
29
29
  export type ResolveConfirmationFn = (token: string, sessionKey: string, principal: Principal) => Promise<ConfirmOutcome>;
30
+ /** Whether the core talks to an authenticated caller at all, injected by the core (`ctx.admit`); everyone when absent. */
31
+ export type AdmitFn = (principal: Principal) => Promise<Admission>;
30
32
  export type TurnRequestDeps = {
31
33
  handleTurn: HandleTurn;
32
34
  /** Who is calling, injected from the app's auth provider; `null` = refused. */
33
35
  authenticate: Authenticate;
36
+ admit?: AdmitFn;
34
37
  /** Bare-token interception before the model, injected by the core. */
35
38
  confirm: ConfirmFn;
36
39
  /** Allowed CORS origin echoed back to a browser UI; defaults to `*`. */
@@ -59,6 +62,7 @@ export declare function openApiResponse(corsOrigin?: string): Response;
59
62
  export declare function handleTurnRequest(req: Request, deps: TurnRequestDeps): Promise<Response>;
60
63
  export type ConfirmRequestDeps = {
61
64
  authenticate: Authenticate;
65
+ admit?: AdmitFn;
62
66
  resolveConfirmation: ResolveConfirmationFn;
63
67
  corsOrigin?: string;
64
68
  };
@@ -103,7 +107,7 @@ type ReadRoute = {
103
107
  * `json` JSON-encodes with the CORS headers merged in; `badRequest` does the
104
108
  * same for a 400.
105
109
  */
106
- export declare function readRoutes(reads: ChannelHostReads, authenticate: Authenticate, corsOrigin?: string): Record<string, ReadRoute>;
110
+ export declare function readRoutes(reads: ChannelHostReads, authenticate: Authenticate, corsOrigin?: string, admit?: AdmitFn): Record<string, ReadRoute>;
107
111
  export type HttpServerDeps = TurnRequestDeps & {
108
112
  port: number;
109
113
  reads?: ChannelHostReads;
package/http-provider.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * (that stays with Google Chat). `stop` closes the socket on shutdown. Confirm is
11
11
  * injected (`confirm`/`resolveConfirmation`), so this package never imports the app.
12
12
  */
13
- import { startHttpServer, type CompleteLoginFn, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
13
+ import { startHttpServer, type AdmitFn, type CompleteLoginFn, type ConfirmFn, type ResolveConfirmationFn } from "./http-server.ts";
14
14
  import type { Authenticate, Provider, HandleTurn, ChannelHostReads } from "@mercury-fw/channel-types";
15
15
 
16
16
  export type HttpProviderDeps = {
@@ -22,6 +22,8 @@ export type HttpProviderDeps = {
22
22
  reads?: ChannelHostReads;
23
23
  /** Who is calling, from the app's auth provider. */
24
24
  authenticate: Authenticate;
25
+ /** Whether the core talks to them, from the core. */
26
+ admit?: AdmitFn;
25
27
  /** Allowed CORS origin echoed to a browser UI; defaults to `*` in the server. */
26
28
  corsOrigin?: string;
27
29
  /** People's logins, when the surface has a public URL to be sent back to:
@@ -40,6 +42,7 @@ export function createHttpProvider(deps: HttpProviderDeps): Provider & { stop():
40
42
  resolveConfirmation: deps.resolveConfirmation,
41
43
  reads: deps.reads,
42
44
  authenticate: deps.authenticate,
45
+ ...(deps.admit === undefined ? {} : { admit: deps.admit }),
43
46
  corsOrigin: deps.corsOrigin,
44
47
  completeLogin: deps.logins?.complete,
45
48
  });
package/http-server.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  type ConfirmOutcome,
34
34
  type LoginOutcome,
35
35
  type Principal,
36
+ type Admission,
36
37
  } from "@mercury-fw/channel-types";
37
38
 
38
39
  /**
@@ -53,10 +54,14 @@ export type ConfirmFn = (token: string, sessionKey: string, principal: Principal
53
54
  /** The structured sibling of {@link ConfirmFn}, for the `/confirm` `resolved` flag. Injected by the core (`ctx.resolveConfirmation`). */
54
55
  export type ResolveConfirmationFn = (token: string, sessionKey: string, principal: Principal) => Promise<ConfirmOutcome>;
55
56
 
57
+ /** Whether the core talks to an authenticated caller at all, injected by the core (`ctx.admit`); everyone when absent. */
58
+ export type AdmitFn = (principal: Principal) => Promise<Admission>;
59
+
56
60
  export type TurnRequestDeps = {
57
61
  handleTurn: HandleTurn;
58
62
  /** Who is calling, injected from the app's auth provider; `null` = refused. */
59
63
  authenticate: Authenticate;
64
+ admit?: AdmitFn;
60
65
  /** Bare-token interception before the model, injected by the core. */
61
66
  confirm: ConfirmFn;
62
67
  /** Allowed CORS origin echoed back to a browser UI; defaults to `*`. */
@@ -95,6 +100,22 @@ function unauthorized(origin: string): Response {
95
100
  );
96
101
  }
97
102
 
103
+ /** The core's refusal of an authenticated caller: 403 for someone it doesn't know, 503 when it couldn't tell, with what to tell them. */
104
+ function notAdmitted(admission: Exclude<Admission, { ok: true }>, origin: string): Response {
105
+ return Response.json(
106
+ { ok: false, error: admission.message, reason: admission.reason },
107
+ { status: admission.reason === "unknown" ? 403 : 503, headers: corsHeaders(origin) },
108
+ );
109
+ }
110
+
111
+ /** The caller of `req`, authenticated and admitted, or the response refusing them. */
112
+ async function caller(req: Request, deps: { authenticate: Authenticate; admit?: AdmitFn }, origin: string): Promise<Principal | Response> {
113
+ const principal = await deps.authenticate(req);
114
+ if (principal === null) return unauthorized(origin);
115
+ const admission = deps.admit ? await deps.admit(principal) : { ok: true as const };
116
+ return admission.ok ? principal : notAdmitted(admission, origin);
117
+ }
118
+
98
119
  /** 204 preflight response for an `OPTIONS` request, carrying only CORS headers. */
99
120
  function preflight(origin: string): Response {
100
121
  return new Response(null, { status: 204, headers: corsHeaders(origin) });
@@ -127,8 +148,8 @@ export function openApiResponse(corsOrigin = "*"): Response {
127
148
  export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Promise<Response> {
128
149
  const origin = deps.corsOrigin ?? "*";
129
150
  const cors = corsHeaders(origin);
130
- const principal = await deps.authenticate(req);
131
- if (principal === null) return unauthorized(origin);
151
+ const principal = await caller(req, deps, origin);
152
+ if (principal instanceof Response) return principal;
132
153
  let body: { text?: unknown; conversationId?: unknown };
133
154
  try {
134
155
  body = (await req.json()) as typeof body;
@@ -236,6 +257,7 @@ export async function handleTurnRequest(req: Request, deps: TurnRequestDeps): Pr
236
257
 
237
258
  export type ConfirmRequestDeps = {
238
259
  authenticate: Authenticate;
260
+ admit?: AdmitFn;
239
261
  resolveConfirmation: ResolveConfirmationFn;
240
262
  corsOrigin?: string;
241
263
  };
@@ -255,8 +277,8 @@ export type ConfirmRequestDeps = {
255
277
  export async function handleConfirmRequest(req: Request, deps: ConfirmRequestDeps): Promise<Response> {
256
278
  const origin = deps.corsOrigin ?? "*";
257
279
  const cors = corsHeaders(origin);
258
- const principal = await deps.authenticate(req);
259
- if (principal === null) return unauthorized(origin);
280
+ const principal = await caller(req, deps, origin);
281
+ if (principal instanceof Response) return principal;
260
282
  let body: { token?: unknown; conversationId?: unknown };
261
283
  try {
262
284
  body = (await req.json()) as typeof body;
@@ -340,7 +362,12 @@ type ReadRoute = {
340
362
  * `json` JSON-encodes with the CORS headers merged in; `badRequest` does the
341
363
  * same for a 400.
342
364
  */
343
- export function readRoutes(reads: ChannelHostReads, authenticate: Authenticate, corsOrigin = "*"): Record<string, ReadRoute> {
365
+ export function readRoutes(
366
+ reads: ChannelHostReads,
367
+ authenticate: Authenticate,
368
+ corsOrigin = "*",
369
+ admit?: AdmitFn,
370
+ ): Record<string, ReadRoute> {
344
371
  const cors = corsHeaders(corsOrigin);
345
372
  const json = (payload: object, status = 200): Response =>
346
373
  Response.json(payload, { status, headers: cors });
@@ -349,14 +376,14 @@ export function readRoutes(reads: ChannelHostReads, authenticate: Authenticate,
349
376
  const options = () => preflight(corsOrigin);
350
377
  const route = (GET: (req: Request, principal: Principal) => Response | Promise<Response>): ReadRoute => ({
351
378
  GET: async (req) => {
352
- const principal = await authenticate(req);
353
- return principal === null ? unauthorized(corsOrigin) : GET(req, principal);
379
+ const principal = await caller(req, { authenticate, ...(admit === undefined ? {} : { admit }) }, corsOrigin);
380
+ return principal instanceof Response ? principal : GET(req, principal);
354
381
  },
355
382
  OPTIONS: options,
356
383
  });
357
384
  return {
358
- "/manifest": route(() => json({ ok: true, manifest: reads.manifest() })),
359
- "/confirmations": route((_req, principal) => json({ ok: true, pending: reads.pendingConfirmations(principal) })),
385
+ "/manifest": route(async (_req, principal) => json({ ok: true, manifest: await reads.manifest(principal) })),
386
+ "/confirmations": route(async (_req, principal) => json({ ok: true, pending: await reads.pendingConfirmations(principal) })),
360
387
  "/conversation": route(async (req, principal) => {
361
388
  const url = new URL(req.url);
362
389
  const id = url.searchParams.get("id");
@@ -379,7 +406,7 @@ export function readRoutes(reads: ChannelHostReads, authenticate: Authenticate,
379
406
  .map((c) => ({ conversationId: c.sessionKey.slice(prefix.length), lastTimestamp: c.lastTimestamp, preview: c.preview })),
380
407
  });
381
408
  }),
382
- "/tool-log": route((_req, principal) => json({ ok: true, entries: reads.toolLog(principal) })),
409
+ "/tool-log": route(async (_req, principal) => json({ ok: true, entries: await reads.toolLog(principal) })),
383
410
  "/health": route(async () => json({ ok: true, ...(await reads.health() as object) })),
384
411
  "/wiki/list": route(async (_req, principal) => json({ ok: true, files: await reads.wikiList(principal) })),
385
412
  "/wiki/read": route(async (req, principal) => {
@@ -433,11 +460,16 @@ export function startHttpServer(deps: HttpServerDeps): ReturnType<typeof Bun.ser
433
460
  "/turn": { POST: (req) => handleTurnRequest(req, deps), OPTIONS: () => preflight(origin) },
434
461
  "/confirm": {
435
462
  POST: (req) =>
436
- handleConfirmRequest(req, { authenticate: deps.authenticate, resolveConfirmation: deps.resolveConfirmation, corsOrigin: origin }),
463
+ handleConfirmRequest(req, {
464
+ authenticate: deps.authenticate,
465
+ ...(deps.admit === undefined ? {} : { admit: deps.admit }),
466
+ resolveConfirmation: deps.resolveConfirmation,
467
+ corsOrigin: origin,
468
+ }),
437
469
  OPTIONS: () => preflight(origin),
438
470
  },
439
471
  "/openapi.yaml": { GET: () => openApiResponse(origin), OPTIONS: () => preflight(origin) },
440
- ...(deps.reads ? readRoutes(deps.reads, deps.authenticate, origin) : {}),
472
+ ...(deps.reads ? readRoutes(deps.reads, deps.authenticate, origin, deps.admit) : {}),
441
473
  ...(deps.completeLogin ? loginRoutes(deps.completeLogin) : {}),
442
474
  },
443
475
  error: (err) =>
package/index.ts CHANGED
@@ -22,7 +22,7 @@ export { createHttpProvider, type HttpProviderDeps } from "./http-provider.ts";
22
22
  export const httpChannel: ChannelPlugin = {
23
23
  // The contract this channel is written for, as a literal: importing
24
24
  // CHANNEL_API_VERSION would report whichever contract is installed.
25
- apiVersion: 4,
25
+ apiVersion: 5,
26
26
  name: "http",
27
27
  build: (ctx) => {
28
28
  if (!ctx.resolveConfirmation || !ctx.reads) {
@@ -42,6 +42,7 @@ export const httpChannel: ChannelPlugin = {
42
42
  resolveConfirmation: ctx.resolveConfirmation,
43
43
  reads: ctx.reads,
44
44
  authenticate: ctx.authenticate,
45
+ admit: ctx.admit,
45
46
  ...(logins ? { logins: { callbackUrl: `${publicUrl}/login/callback`, accept: logins.accept, complete: logins.complete } } : {}),
46
47
  });
47
48
  },
package/openapi.yaml CHANGED
@@ -20,14 +20,20 @@ info:
20
20
  missing or refused one gets `401` with `WWW-Authenticate: Bearer`, before
21
21
  anything runs.
22
22
 
23
+ Then the core decides whether it talks to that person at all: someone the
24
+ app's directory doesn't know gets `403` (unless the app lets unknown people
25
+ in), and `503` when the directory can't be reached. Both carry
26
+ `{ ok: false, error, reason }`, `error` being what to tell them.
27
+
23
28
 
24
29
  ## What a caller sees
25
30
 
26
31
  Every read is scoped to the caller: their own conversations, pending
27
32
  confirmations, tool calls and memory, and the wiki as they see it, the
28
33
  team's common area (`curated/`) plus their own notes (`personal/`).
29
- Someone else's data doesn't exist as far as a caller can tell. Only
30
- `/manifest` and `/health` are the same for everyone.
34
+ Someone else's data doesn't exist as far as a caller can tell.
35
+ `/manifest` lists what the caller is offered, and only `/health` is the
36
+ same for everyone.
31
37
 
32
38
 
33
39
  ## Conversations
@@ -82,6 +88,10 @@ paths:
82
88
  responses:
83
89
  "401":
84
90
  $ref: "#/components/responses/Unauthorized"
91
+ "403":
92
+ $ref: "#/components/responses/NotAdmitted"
93
+ "503":
94
+ $ref: "#/components/responses/NotAdmitted"
85
95
  "200":
86
96
  description: >
87
97
  An SSE stream. Each event is one of the types in `TurnEvent`:
@@ -117,6 +127,10 @@ paths:
117
127
  responses:
118
128
  "401":
119
129
  $ref: "#/components/responses/Unauthorized"
130
+ "403":
131
+ $ref: "#/components/responses/NotAdmitted"
132
+ "503":
133
+ $ref: "#/components/responses/NotAdmitted"
120
134
  "200":
121
135
  description: >
122
136
  `resolved: true` (with the reply `text`) when the token was a pending
@@ -158,6 +172,10 @@ paths:
158
172
  responses:
159
173
  "401":
160
174
  $ref: "#/components/responses/Unauthorized"
175
+ "403":
176
+ $ref: "#/components/responses/NotAdmitted"
177
+ "503":
178
+ $ref: "#/components/responses/NotAdmitted"
161
179
  "200":
162
180
  description: The conversation's messages in chronological order; none when it isn't one of the caller's.
163
181
  content:
@@ -183,6 +201,10 @@ paths:
183
201
  responses:
184
202
  "401":
185
203
  $ref: "#/components/responses/Unauthorized"
204
+ "403":
205
+ $ref: "#/components/responses/NotAdmitted"
206
+ "503":
207
+ $ref: "#/components/responses/NotAdmitted"
186
208
  "200":
187
209
  description: The conversation list.
188
210
  content:
@@ -195,6 +217,10 @@ paths:
195
217
  responses:
196
218
  "401":
197
219
  $ref: "#/components/responses/Unauthorized"
220
+ "403":
221
+ $ref: "#/components/responses/NotAdmitted"
222
+ "503":
223
+ $ref: "#/components/responses/NotAdmitted"
198
224
  "200":
199
225
  description: Health snapshot.
200
226
  content:
@@ -203,10 +229,14 @@ paths:
203
229
  type: object
204
230
  /manifest:
205
231
  get:
206
- summary: The loaded plugins, tools and skills
232
+ summary: The plugins, tools and skills the caller is offered
207
233
  responses:
208
234
  "401":
209
235
  $ref: "#/components/responses/Unauthorized"
236
+ "403":
237
+ $ref: "#/components/responses/NotAdmitted"
238
+ "503":
239
+ $ref: "#/components/responses/NotAdmitted"
210
240
  "200":
211
241
  description: Plugin manifest.
212
242
  content:
@@ -219,6 +249,10 @@ paths:
219
249
  responses:
220
250
  "401":
221
251
  $ref: "#/components/responses/Unauthorized"
252
+ "403":
253
+ $ref: "#/components/responses/NotAdmitted"
254
+ "503":
255
+ $ref: "#/components/responses/NotAdmitted"
222
256
  "200":
223
257
  description: Pending confirmations.
224
258
  content:
@@ -231,6 +265,10 @@ paths:
231
265
  responses:
232
266
  "401":
233
267
  $ref: "#/components/responses/Unauthorized"
268
+ "403":
269
+ $ref: "#/components/responses/NotAdmitted"
270
+ "503":
271
+ $ref: "#/components/responses/NotAdmitted"
234
272
  "200":
235
273
  description: Tool log.
236
274
  content:
@@ -243,6 +281,10 @@ paths:
243
281
  responses:
244
282
  "401":
245
283
  $ref: "#/components/responses/Unauthorized"
284
+ "403":
285
+ $ref: "#/components/responses/NotAdmitted"
286
+ "503":
287
+ $ref: "#/components/responses/NotAdmitted"
246
288
  "200":
247
289
  description: File list.
248
290
  content:
@@ -262,6 +304,10 @@ paths:
262
304
  responses:
263
305
  "401":
264
306
  $ref: "#/components/responses/Unauthorized"
307
+ "403":
308
+ $ref: "#/components/responses/NotAdmitted"
309
+ "503":
310
+ $ref: "#/components/responses/NotAdmitted"
265
311
  "200":
266
312
  description: File content.
267
313
  content:
@@ -293,6 +339,10 @@ paths:
293
339
  responses:
294
340
  "401":
295
341
  $ref: "#/components/responses/Unauthorized"
342
+ "403":
343
+ $ref: "#/components/responses/NotAdmitted"
344
+ "503":
345
+ $ref: "#/components/responses/NotAdmitted"
296
346
  "200":
297
347
  description: Matches.
298
348
  content:
@@ -329,6 +379,10 @@ paths:
329
379
  responses:
330
380
  "401":
331
381
  $ref: "#/components/responses/Unauthorized"
382
+ "403":
383
+ $ref: "#/components/responses/NotAdmitted"
384
+ "503":
385
+ $ref: "#/components/responses/NotAdmitted"
332
386
  "200":
333
387
  description: A page of points.
334
388
  content:
@@ -396,6 +450,26 @@ components:
396
450
  application/json:
397
451
  schema:
398
452
  $ref: "#/components/schemas/Error"
453
+ NotAdmitted:
454
+ description: >
455
+ An authenticated caller the core won't talk to: `403` when the app's
456
+ directory doesn't know them, `503` when it can't be reached. Nothing
457
+ behind the route runs.
458
+ content:
459
+ application/json:
460
+ schema:
461
+ type: object
462
+ required: [ok, error, reason]
463
+ properties:
464
+ ok:
465
+ type: boolean
466
+ enum: [false]
467
+ error:
468
+ type: string
469
+ description: What to tell the caller.
470
+ reason:
471
+ type: string
472
+ enum: [unknown, unavailable]
399
473
  schemas:
400
474
  TurnRequest:
401
475
  type: object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/channel-http",
3
- "version": "0.5.0",
3
+ "version": "0.6.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.40.0",
25
- "@mercury-fw/plugin-types": "0.40.0",
24
+ "@mercury-fw/channel-types": "0.42.0",
25
+ "@mercury-fw/plugin-types": "0.42.0",
26
26
  "@mercury-fw/typescript-config": "*",
27
27
  "@types/bun": "^1.4.2",
28
28
  "typescript": "^6.0.3"