@cat-factory/server 0.267.0 → 0.269.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.
Files changed (64) hide show
  1. package/dist/agents/containerAgentBody.d.ts +18 -5
  2. package/dist/agents/containerAgentBody.d.ts.map +1 -1
  3. package/dist/agents/containerAgentBody.js +84 -21
  4. package/dist/agents/containerAgentBody.js.map +1 -1
  5. package/dist/agents/containerAgentResult.js +3 -3
  6. package/dist/agents/containerAgentResult.js.map +1 -1
  7. package/dist/agents/jobBody.d.ts +8 -2
  8. package/dist/agents/jobBody.d.ts.map +1 -1
  9. package/dist/agents/jobBody.js +15 -4
  10. package/dist/agents/jobBody.js.map +1 -1
  11. package/dist/app.d.ts.map +1 -1
  12. package/dist/app.js +12 -0
  13. package/dist/app.js.map +1 -1
  14. package/dist/github/GitHubMergeabilityProvider.d.ts.map +1 -1
  15. package/dist/github/GitHubMergeabilityProvider.js +6 -2
  16. package/dist/github/GitHubMergeabilityProvider.js.map +1 -1
  17. package/dist/github/GitHubPrReportPublisher.d.ts +17 -6
  18. package/dist/github/GitHubPrReportPublisher.d.ts.map +1 -1
  19. package/dist/github/GitHubPrReportPublisher.js +65 -36
  20. package/dist/github/GitHubPrReportPublisher.js.map +1 -1
  21. package/dist/http/authGate.d.ts.map +1 -1
  22. package/dist/http/authGate.js +10 -0
  23. package/dist/http/authGate.js.map +1 -1
  24. package/dist/http/cors.d.ts +16 -0
  25. package/dist/http/cors.d.ts.map +1 -1
  26. package/dist/http/cors.js +48 -1
  27. package/dist/http/cors.js.map +1 -1
  28. package/dist/http/env.d.ts +32 -1
  29. package/dist/http/env.d.ts.map +1 -1
  30. package/dist/http/errorHandler.d.ts.map +1 -1
  31. package/dist/http/errorHandler.js +22 -1
  32. package/dist/http/errorHandler.js.map +1 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +6 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/modules/llmProxy/LlmProxyController.d.ts +16 -0
  38. package/dist/modules/llmProxy/LlmProxyController.d.ts.map +1 -1
  39. package/dist/modules/llmProxy/LlmProxyController.js +30 -7
  40. package/dist/modules/llmProxy/LlmProxyController.js.map +1 -1
  41. package/dist/modules/mcpAuthServer/McpAuthorizationConsentController.d.ts +4 -0
  42. package/dist/modules/mcpAuthServer/McpAuthorizationConsentController.d.ts.map +1 -0
  43. package/dist/modules/mcpAuthServer/McpAuthorizationConsentController.js +119 -0
  44. package/dist/modules/mcpAuthServer/McpAuthorizationConsentController.js.map +1 -0
  45. package/dist/modules/mcpAuthServer/McpAuthorizationController.d.ts +4 -0
  46. package/dist/modules/mcpAuthServer/McpAuthorizationController.d.ts.map +1 -0
  47. package/dist/modules/mcpAuthServer/McpAuthorizationController.js +235 -0
  48. package/dist/modules/mcpAuthServer/McpAuthorizationController.js.map +1 -0
  49. package/dist/modules/mcpAuthServer/consentRedirect.d.ts +42 -0
  50. package/dist/modules/mcpAuthServer/consentRedirect.d.ts.map +1 -0
  51. package/dist/modules/mcpAuthServer/consentRedirect.js +46 -0
  52. package/dist/modules/mcpAuthServer/consentRedirect.js.map +1 -0
  53. package/dist/modules/mcpAuthServer/containerFields.d.ts +37 -0
  54. package/dist/modules/mcpAuthServer/containerFields.d.ts.map +1 -0
  55. package/dist/modules/mcpAuthServer/containerFields.js +36 -0
  56. package/dist/modules/mcpAuthServer/containerFields.js.map +1 -0
  57. package/dist/modules/publicApi/PublicMcpController.d.ts.map +1 -1
  58. package/dist/modules/publicApi/PublicMcpController.js +7 -0
  59. package/dist/modules/publicApi/PublicMcpController.js.map +1 -1
  60. package/dist/modules/publicApi/openapiDocument.generated.d.ts +1 -1
  61. package/dist/modules/publicApi/openapiDocument.generated.d.ts.map +1 -1
  62. package/dist/modules/publicApi/openapiDocument.generated.js +1 -1
  63. package/dist/modules/publicApi/openapiDocument.generated.js.map +1 -1
  64. package/package.json +8 -8
@@ -0,0 +1,235 @@
1
+ import { McpOAuthProtocolError, McpOAuthRedirectableError, authorizationServerMetadata, mcpOAuthErrorRedirect, mcpResourceIdentifier, protectedResourceMetadata, AUTHORIZATION_SERVER_METADATA_PATH, OAUTH_ENDPOINT_PATHS, PROTECTED_RESOURCE_METADATA_PATH, } from '@cat-factory/integrations';
2
+ import { describeError } from '@cat-factory/kernel';
3
+ import { Hono } from 'hono';
4
+ import { assertCapability, requireCapability } from '../../http/guards.js';
5
+ import { requestLogger } from '../../http/requestLogging.js';
6
+ import { consentUrlFor } from './consentRedirect.js';
7
+ // ---------------------------------------------------------------------------
8
+ // The unauthenticated half of MCP authorization on the SERVING side: two metadata documents, a
9
+ // dynamic client registration, the authorization redirect, and the token exchange.
10
+ //
11
+ // Everything here is reached WITHOUT a session, by construction rather than by exemption. A host
12
+ // reading metadata has no credential yet (that is what it is here to obtain), a registration is the
13
+ // act of becoming known at all, and the token exchange is a machine-to-machine call carrying the
14
+ // code and the PKCE verifier instead of a session. The one step that needs a signed-in human is the
15
+ // consent screen, and it is deliberately NOT here: `GET /oauth/authorize` validates and then sends
16
+ // the browser to the SPA, whose approval call is ordinary session-gated API. That is the same shape
17
+ // the CONSUMING side settled on for its vendor callback, arrived at from the other direction: a
18
+ // third party's browser navigation cannot carry a bearer token, so any identity check written on a
19
+ // route that receives one is unreachable code that reads like protection.
20
+ //
21
+ // Refusals answer in OAUTH's own vocabulary (RFC 6749 §5.2's `{ error, error_description }`),
22
+ // not the deployment's error envelope. Same split the hosted MCP endpoint already makes between an
23
+ // HTTP-level auth failure (the envelope, with its correlation id) and a protocol-level refusal (the
24
+ // transport's own frame): a client here parses `error` and branches on it, and `invalid_grant`
25
+ // against `invalid_client` is a distinction the envelope's status class cannot carry.
26
+ // ---------------------------------------------------------------------------
27
+ /** What every route here needs, and the message naming what a deployment has not wired. */
28
+ const AUTH_SERVER_UNWIRED = 'This deployment does not serve MCP authorization. It needs ENCRYPTION_KEY (the flow seals ' +
29
+ 'every value it carries) and the public API enabled, since what a host is issued is a ' +
30
+ 'public-API key.';
31
+ /** The authorization server, or the 503 naming what a deployment has not wired. */
32
+ function requireAuthServer(c) {
33
+ return requireCapability(c.get('container').mcpAuthServer, AUTH_SERVER_UNWIRED);
34
+ }
35
+ /**
36
+ * The same refusal where the route has no use for the value: the two metadata documents.
37
+ *
38
+ * They are gated for the reason a signpost is taken down when the road behind it is closed. Left
39
+ * ungated they describe a complete authorization server (endpoints, grant types, PKCE method) on a
40
+ * deployment whose `/oauth/register` and `/oauth/token` can only answer 503, which is strictly
41
+ * worse than answering nothing: a host that reads discovery believes the flow is available, walks
42
+ * it, and reports a broken server. A host that cannot discover falls back to asking for a key,
43
+ * which is exactly the behaviour that existed before any of this, and is the honest answer for a
44
+ * deployment that has not wired what the documents promise.
45
+ */
46
+ function assertAuthServer(c) {
47
+ assertCapability(c.get('container').mcpAuthServer, AUTH_SERVER_UNWIRED);
48
+ }
49
+ export function mcpAuthorizationController() {
50
+ const app = new Hono();
51
+ // Both well-known paths answer the same document. RFC 9728 INSERTS the resource's path, which is
52
+ // correct and is what a client built to the current spec asks for; the bare path is what several
53
+ // shipped clients ask for, and Figma's live MCP server answers there too. This deployment serves
54
+ // exactly one protected resource, so there is no second document either path could mean, and
55
+ // answering both costs nothing but takes a whole class of "connects everywhere except here" out.
56
+ for (const path of [PROTECTED_RESOURCE_METADATA_PATH, '/.well-known/oauth-protected-resource']) {
57
+ app.get(path, (c) => {
58
+ assertAuthServer(c);
59
+ return metadataResponse(c, protectedResourceMetadata(originOf(c)));
60
+ });
61
+ }
62
+ app.get(AUTHORIZATION_SERVER_METADATA_PATH, (c) => {
63
+ assertAuthServer(c);
64
+ return metadataResponse(c, authorizationServerMetadata(originOf(c)));
65
+ });
66
+ // Dynamic client registration (RFC 7591). Unauthenticated, which is the specified shape for an
67
+ // OPEN registration endpoint and is what makes a host nobody configured able to connect at all.
68
+ // What bounds it is that a registration grants NOTHING: no scope, no board, no token, until a
69
+ // signed-in human with `secrets.manage` approves a specific board on the consent screen.
70
+ app.post(OAUTH_ENDPOINT_PATHS.register, async (c) => {
71
+ const server = requireAuthServer(c);
72
+ const body = await readJsonBody(c);
73
+ try {
74
+ const registration = await server.registerClient({
75
+ clientName: typeof body.client_name === 'string' ? body.client_name : undefined,
76
+ redirectUris: body.redirect_uris,
77
+ });
78
+ return c.json({
79
+ client_id: registration.clientId,
80
+ client_name: registration.clientName,
81
+ redirect_uris: registration.redirectUris,
82
+ client_id_issued_at: Math.floor(registration.issuedAt / 1000),
83
+ grant_types: ['authorization_code'],
84
+ response_types: ['code'],
85
+ // Stated rather than left to be inferred: these are PUBLIC clients, so a host that
86
+ // expected a secret learns it here instead of at a token endpoint that ignores one.
87
+ token_endpoint_auth_method: 'none',
88
+ }, 201);
89
+ }
90
+ catch (error) {
91
+ return oauthErrorResponse(c, error);
92
+ }
93
+ });
94
+ // The authorization endpoint: a top-level browser navigation the host opens. It validates
95
+ // everything that can be validated without a human, then hands off to the consent screen.
96
+ app.get(OAUTH_ENDPOINT_PATHS.authorize, async (c) => {
97
+ const server = requireAuthServer(c);
98
+ const query = new URL(c.req.url).searchParams;
99
+ const redirectUri = query.get('redirect_uri') ?? '';
100
+ try {
101
+ const { sealedRequest } = await server.beginAuthorization({
102
+ clientId: query.get('client_id') ?? '',
103
+ redirectUri,
104
+ responseType: query.get('response_type') ?? '',
105
+ codeChallenge: query.get('code_challenge') ?? '',
106
+ codeChallengeMethod: query.get('code_challenge_method') ?? '',
107
+ ...(query.get('state') ? { state: query.get('state') } : {}),
108
+ ...(query.get('scope') ? { scope: query.get('scope') } : {}),
109
+ ...(query.get('resource') ? { resource: query.get('resource') } : {}),
110
+ expectedResource: mcpResourceIdentifier(originOf(c)),
111
+ });
112
+ return c.redirect(consentUrlFor(c, sealedRequest), 302);
113
+ }
114
+ catch (error) {
115
+ // WHERE a refusal goes is the service's judgement, not this route's, and it turns on one
116
+ // question: has the redirect URI been matched against the registration yet?
117
+ //
118
+ // Until it has, the refusal has nowhere safe to go. Bouncing it to the supplied
119
+ // `redirect_uri` would be the open redirect the registration check exists to prevent: an
120
+ // attacker would hold a URL on this deployment's origin that forwards a browser anywhere,
121
+ // with error text of their choosing on the end of it. The human standing in front of the
122
+ // browser reads that refusal as a page instead.
123
+ //
124
+ // Once it has, the opposite holds and RFC 6749 §4.1.2.1 requires it: the fault is in the
125
+ // host's own request and it is the host that must hear about it, at an address it registered
126
+ // itself. Rendered as a page, a conforming client waits out its timeout and reports that this
127
+ // deployment never answered.
128
+ if (error instanceof McpOAuthRedirectableError) {
129
+ requestLogger(c).warn('mcp authorization refused, reporting to the client', {
130
+ oauthError: error.oauthError,
131
+ ...describeError(error),
132
+ });
133
+ return c.redirect(mcpOAuthErrorRedirect(error), 302);
134
+ }
135
+ return oauthErrorResponse(c, error);
136
+ }
137
+ });
138
+ // The token endpoint: the one call a host makes with no browser involved.
139
+ app.post(OAUTH_ENDPOINT_PATHS.token, async (c) => {
140
+ const server = requireAuthServer(c);
141
+ const form = await readFormBody(c);
142
+ const grantType = form.get('grant_type') ?? '';
143
+ try {
144
+ if (grantType !== 'authorization_code') {
145
+ throw new McpOAuthProtocolError('unsupported_grant_type', `This authorization server serves the authorization_code grant; got '${grantType}'. ` +
146
+ 'There is no refresh grant: the issued token does not expire, so there is nothing ' +
147
+ 'to refresh.');
148
+ }
149
+ const issued = await server.redeemCode({
150
+ code: form.get('code') ?? '',
151
+ clientId: form.get('client_id') ?? '',
152
+ ...(form.get('redirect_uri') ? { redirectUri: form.get('redirect_uri') } : {}),
153
+ codeVerifier: form.get('code_verifier') ?? '',
154
+ });
155
+ requestLogger(c).info('mcp authorization token issued', { keyId: issued.keyId });
156
+ // `expires_in` is ABSENT rather than a large number, and the difference is the whole honesty
157
+ // of the token model: RFC 6749 §5.1 makes it optional precisely so a server that issues a
158
+ // non-expiring token says so by omission. A generous number here would have every client
159
+ // schedule a refresh this server does not serve.
160
+ return c.json({ access_token: issued.accessToken, token_type: 'Bearer', scope: issued.scope }, 200,
161
+ // RFC 6749 §5.1 requires both: this body is a bearer credential, and a cache between the
162
+ // host and this deployment holding it is the one storage nobody can revoke.
163
+ { 'cache-control': 'no-store', pragma: 'no-cache' });
164
+ }
165
+ catch (error) {
166
+ return oauthErrorResponse(c, error);
167
+ }
168
+ });
169
+ return app;
170
+ }
171
+ /** This deployment's own origin, taken from the request being answered. */
172
+ function originOf(c) {
173
+ return new URL(c.req.url).origin;
174
+ }
175
+ /**
176
+ * A metadata document, cacheable.
177
+ *
178
+ * Readable from any browser origin, but NOT by a header set here: `isPubliclyReadablePath` in the
179
+ * shared CORS layer covers `/.well-known` and `/oauth` together, because the routes that ACT on
180
+ * these documents are preflighted and a preflight never reaches a handler. Setting the header here
181
+ * as well would answer discovery from two places and leave the registration it points at answering
182
+ * from neither.
183
+ */
184
+ function metadataResponse(c, document) {
185
+ return c.json(document, 200, { 'cache-control': 'public, max-age=300' });
186
+ }
187
+ /**
188
+ * The OAuth error body, at the status the protocol assigns.
189
+ *
190
+ * `invalid_client` is a 401 and everything else a 400 (RFC 6749 §5.2). Anything that is NOT a
191
+ * protocol error is re-thrown to `handleError`, which is the whole point of narrowing here: an
192
+ * unexpected fault must keep its correlation id and its 500 rather than being flattened into a
193
+ * protocol code no client can act on.
194
+ */
195
+ function oauthErrorResponse(c, error) {
196
+ if (!(error instanceof McpOAuthProtocolError))
197
+ throw error;
198
+ requestLogger(c).warn('mcp authorization refused', {
199
+ oauthError: error.oauthError,
200
+ ...describeError(error),
201
+ });
202
+ return c.json({ error: error.oauthError, error_description: error.message }, error.oauthError === 'invalid_client' ? 401 : 400);
203
+ }
204
+ /** A JSON request body, or an empty object: a malformed one is refused by the validation below it. */
205
+ async function readJsonBody(c) {
206
+ try {
207
+ const body = (await c.req.json());
208
+ return body && typeof body === 'object' ? body : {};
209
+ }
210
+ catch {
211
+ // silent-catch-ok: an unparseable body reaches the same refusal an empty one does, and that
212
+ // refusal names the missing field, which is more use than "invalid JSON" to whoever sent it.
213
+ return {};
214
+ }
215
+ }
216
+ /**
217
+ * A token-request body, from either encoding.
218
+ *
219
+ * OAuth specifies `application/x-www-form-urlencoded` and that is what most clients send, but JSON
220
+ * is common enough in MCP hosts that refusing it would be refusing working clients over a content
221
+ * type. Read through one accessor so the handler above states each field once.
222
+ */
223
+ async function readFormBody(c) {
224
+ const contentType = c.req.header('content-type') ?? '';
225
+ const values = contentType.includes('json')
226
+ ? await readJsonBody(c)
227
+ : Object.fromEntries(new URLSearchParams(await c.req.text()));
228
+ return {
229
+ get: (name) => {
230
+ const value = values[name];
231
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
232
+ },
233
+ };
234
+ }
235
+ //# sourceMappingURL=McpAuthorizationController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpAuthorizationController.js","sourceRoot":"","sources":["../../../src/modules/mcpAuthServer/McpAuthorizationController.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { Context } from 'hono';
2
+ import type { AppEnv } from '../../http/env.js';
3
+ /**
4
+ * The page in the SPA that asks a human what an MCP host may have.
5
+ *
6
+ * A page in the APP rather than a route on this backend, for the same reason the consuming side's
7
+ * vendor callback is: the decision needs a SESSION, and this URL is reached by a top-level browser
8
+ * navigation the host triggers, which carries no bearer token. Rendering the screen server-side
9
+ * would mean a route that cannot tell who is looking at it, and an approval taken there would be
10
+ * an approval by nobody. The page instead re-presents the sealed request over the authenticated API
11
+ * (`describeMcpAuthorizationContract` / `approveMcpAuthorizationContract`), where the session, the
12
+ * board choice and the `secrets.manage` check all actually run.
13
+ *
14
+ * Exported so both halves derive the string from one place. The SPA page at this path
15
+ * (`pages/mcp-authorize.vue`) is the other half, and nothing pins the two together: renaming the
16
+ * page must change this constant too, or `GET /oauth/authorize` sends every host to a 404 while
17
+ * everything on this side stays green.
18
+ */
19
+ export declare const MCP_AUTHORIZE_PATH = "/mcp-authorize";
20
+ /**
21
+ * Where to send the browser for consent, with the sealed request on it.
22
+ *
23
+ * The base is the deployment's configured app URL when it has one, and the REQUEST's own origin
24
+ * otherwise. That fallback is right rather than lazy: a deployment serving the SPA and the API from
25
+ * one origin (which is every default install) needs no configuration for this to work, and one
26
+ * serving the SPA elsewhere has already had to set `APP_BASE_URL` for its invite and
27
+ * password-reset links to arrive anywhere. Unlike the consuming side's redirect URL, no third party
28
+ * holds this string on file, so nothing breaks if it differs between two deployments of the same
29
+ * app: the host never sees it, the browser simply follows it.
30
+ *
31
+ * The sealed request rides the query string, where a browser history keeps it. It carries no
32
+ * credential and no code (the code is minted only after a human approves), and it is useless to
33
+ * anyone who cannot both open a session here and hold `secrets.manage` on a board.
34
+ *
35
+ * The path is APPENDED to the configured base rather than resolved against it, which is the same
36
+ * join `notificationDeepLink` makes and for the same reason: `new URL('/mcp-authorize', base)`
37
+ * resolves an ABSOLUTE path, so it keeps the base's origin and discards any path component of it. A
38
+ * deployment serving the SPA under a prefix (`APP_BASE_URL=https://example.test/cat-factory`) would
39
+ * have every host sent to a 404 one segment above the app, with nothing on this side failing.
40
+ */
41
+ export declare function consentUrlFor<E extends AppEnv>(c: Context<E>, sealedRequest: string): string;
42
+ //# sourceMappingURL=consentRedirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consentRedirect.d.ts","sourceRoot":"","sources":["../../../src/modules/mcpAuthServer/consentRedirect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,mBAAmB,CAAA;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAM5F"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The page in the SPA that asks a human what an MCP host may have.
3
+ *
4
+ * A page in the APP rather than a route on this backend, for the same reason the consuming side's
5
+ * vendor callback is: the decision needs a SESSION, and this URL is reached by a top-level browser
6
+ * navigation the host triggers, which carries no bearer token. Rendering the screen server-side
7
+ * would mean a route that cannot tell who is looking at it, and an approval taken there would be
8
+ * an approval by nobody. The page instead re-presents the sealed request over the authenticated API
9
+ * (`describeMcpAuthorizationContract` / `approveMcpAuthorizationContract`), where the session, the
10
+ * board choice and the `secrets.manage` check all actually run.
11
+ *
12
+ * Exported so both halves derive the string from one place. The SPA page at this path
13
+ * (`pages/mcp-authorize.vue`) is the other half, and nothing pins the two together: renaming the
14
+ * page must change this constant too, or `GET /oauth/authorize` sends every host to a 404 while
15
+ * everything on this side stays green.
16
+ */
17
+ export const MCP_AUTHORIZE_PATH = '/mcp-authorize';
18
+ /**
19
+ * Where to send the browser for consent, with the sealed request on it.
20
+ *
21
+ * The base is the deployment's configured app URL when it has one, and the REQUEST's own origin
22
+ * otherwise. That fallback is right rather than lazy: a deployment serving the SPA and the API from
23
+ * one origin (which is every default install) needs no configuration for this to work, and one
24
+ * serving the SPA elsewhere has already had to set `APP_BASE_URL` for its invite and
25
+ * password-reset links to arrive anywhere. Unlike the consuming side's redirect URL, no third party
26
+ * holds this string on file, so nothing breaks if it differs between two deployments of the same
27
+ * app: the host never sees it, the browser simply follows it.
28
+ *
29
+ * The sealed request rides the query string, where a browser history keeps it. It carries no
30
+ * credential and no code (the code is minted only after a human approves), and it is useless to
31
+ * anyone who cannot both open a session here and hold `secrets.manage` on a board.
32
+ *
33
+ * The path is APPENDED to the configured base rather than resolved against it, which is the same
34
+ * join `notificationDeepLink` makes and for the same reason: `new URL('/mcp-authorize', base)`
35
+ * resolves an ABSOLUTE path, so it keeps the base's origin and discards any path component of it. A
36
+ * deployment serving the SPA under a prefix (`APP_BASE_URL=https://example.test/cat-factory`) would
37
+ * have every host sent to a 404 one segment above the app, with nothing on this side failing.
38
+ */
39
+ export function consentUrlFor(c, sealedRequest) {
40
+ const configured = c.get('container').appBaseUrl?.trim();
41
+ const base = configured || new URL(c.req.url).origin;
42
+ const url = new URL(`${base.replace(/\/+$/, '')}${MCP_AUTHORIZE_PATH}`);
43
+ url.searchParams.set('request', sealedRequest);
44
+ return url.toString();
45
+ }
46
+ //# sourceMappingURL=consentRedirect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consentRedirect.js","sourceRoot":"","sources":["../../../src/modules/mcpAuthServer/consentRedirect.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,gBAAgB,CAAA;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,aAAa,CAAmB,CAAa,EAAE,aAAqB;IAClF,MAAM,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,CAAA;IACxD,MAAM,IAAI,GAAG,UAAU,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAA;IACpD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,kBAAkB,EAAE,CAAC,CAAA;IACvE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;IAC9C,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;AACvB,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { McpAuthorizationServer, type PublicApiKeyService } from '@cat-factory/integrations';
2
+ import type { Clock, Logger } from '@cat-factory/kernel';
3
+ /**
4
+ * The one field an MCP-authorization-capable facade contributes to its `ServerContainer`.
5
+ *
6
+ * Projected through a shared function rather than assembled at each composition root, for the
7
+ * reason `mcpOAuthContainerFields` beside it exists: the capability has TWO preconditions that a
8
+ * facade could satisfy by halves, and `requireCapability` narrows on presence, so a facade
9
+ * spreading the key unconditionally would turn "not wired" into a value that reads as wired.
10
+ */
11
+ export interface McpAuthServerContainerFields {
12
+ mcpAuthServer?: McpAuthorizationServer;
13
+ }
14
+ /**
15
+ * Build the authorization server for a facade that can serve one, or nothing.
16
+ *
17
+ * The two preconditions are not arbitrary and neither is optional:
18
+ *
19
+ * - an `ENCRYPTION_KEY`, because every value this flow carries between two requests is sealed
20
+ * under it. With no key there is nowhere to keep a PKCE challenge or an approved board, and the
21
+ * only alternative designs are a table or a signed-but-readable value, both refused for reasons
22
+ * recorded on the service itself.
23
+ * - the public-API key store, because the token a host is issued IS a public-API key. A deployment
24
+ * with the public API unwired has nothing to issue, and an authorization server that authorizes
25
+ * access to a surface its own deployment does not serve would be a consent screen leading
26
+ * nowhere.
27
+ *
28
+ * Absent, every route refuses with a 503 naming both, which is the honest answer: this is a
29
+ * capability a deployment has not enabled rather than a fault.
30
+ */
31
+ export declare function mcpAuthServerContainerFields(input: {
32
+ encryptionKey?: string | undefined;
33
+ publicApiKeys?: PublicApiKeyService | undefined;
34
+ clock: Clock;
35
+ logger: Logger;
36
+ }): McpAuthServerContainerFields;
37
+ //# sourceMappingURL=containerFields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"containerFields.d.ts","sourceRoot":"","sources":["../../../src/modules/mcpAuthServer/containerFields.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EAEtB,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAA;AAClC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAGxD;;;;;;;GAOG;AACH,MAAM,WAAW,4BAA4B;IAC3C,aAAa,CAAC,EAAE,sBAAsB,CAAA;CACvC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,aAAa,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAA;IAC/C,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,4BAA4B,CAc/B"}
@@ -0,0 +1,36 @@
1
+ import { McpAuthorizationServer, MCP_AUTH_SERVER_CIPHER_INFO, } from '@cat-factory/integrations';
2
+ import { WebCryptoSecretCipher } from '../../crypto/WebCryptoSecretCipher.js';
3
+ /**
4
+ * Build the authorization server for a facade that can serve one, or nothing.
5
+ *
6
+ * The two preconditions are not arbitrary and neither is optional:
7
+ *
8
+ * - an `ENCRYPTION_KEY`, because every value this flow carries between two requests is sealed
9
+ * under it. With no key there is nowhere to keep a PKCE challenge or an approved board, and the
10
+ * only alternative designs are a table or a signed-but-readable value, both refused for reasons
11
+ * recorded on the service itself.
12
+ * - the public-API key store, because the token a host is issued IS a public-API key. A deployment
13
+ * with the public API unwired has nothing to issue, and an authorization server that authorizes
14
+ * access to a surface its own deployment does not serve would be a consent screen leading
15
+ * nowhere.
16
+ *
17
+ * Absent, every route refuses with a 503 naming both, which is the honest answer: this is a
18
+ * capability a deployment has not enabled rather than a fault.
19
+ */
20
+ export function mcpAuthServerContainerFields(input) {
21
+ const key = input.encryptionKey?.trim();
22
+ if (!key || !input.publicApiKeys)
23
+ return {};
24
+ return {
25
+ mcpAuthServer: new McpAuthorizationServer({
26
+ secretCipher: new WebCryptoSecretCipher({
27
+ masterKeyBase64: key,
28
+ info: MCP_AUTH_SERVER_CIPHER_INFO,
29
+ }),
30
+ publicApiKeys: input.publicApiKeys,
31
+ clock: input.clock,
32
+ logger: input.logger,
33
+ }),
34
+ };
35
+ }
36
+ //# sourceMappingURL=containerFields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"containerFields.js","sourceRoot":"","sources":["../../../src/modules/mcpAuthServer/containerFields.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,2BAA2B,GAE5B,MAAM,2BAA2B,CAAA;AAElC,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAc7E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAK5C;IACC,MAAM,GAAG,GAAG,KAAK,CAAC,aAAa,EAAE,IAAI,EAAE,CAAA;IACvC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa;QAAE,OAAO,EAAE,CAAA;IAC3C,OAAO;QACL,aAAa,EAAE,IAAI,sBAAsB,CAAC;YACxC,YAAY,EAAE,IAAI,qBAAqB,CAAC;gBACtC,eAAe,EAAE,GAAG;gBACpB,IAAI,EAAE,2BAA2B;aAClC,CAAC;YACF,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,KAAK,CAAC,MAAM;SACrB,CAAC;KACH,CAAA;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"PublicMcpController.d.ts","sourceRoot":"","sources":["../../../src/modules/publicApi/PublicMcpController.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AA+BzD,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CA0CvF"}
1
+ {"version":3,"file":"PublicMcpController.d.ts","sourceRoot":"","sources":["../../../src/modules/publicApi/PublicMcpController.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AA+BzD,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAiDvF"}
@@ -1,6 +1,7 @@
1
1
  // The `./http` entry point, NOT the package root: the root re-exports the stdio boot, whose
2
2
  // transport imports `node:process`, and this package is bundled into the Worker facade.
3
3
  import { handleMcpHttpRequest, refuseMcpMethod } from '@cat-factory/mcp-server/http';
4
+ import { bearerChallenge } from '@cat-factory/integrations';
4
5
  import { describeError } from '@cat-factory/kernel';
5
6
  import { Hono } from 'hono';
6
7
  import { requestLogger } from '../../http/requestLogging.js';
@@ -39,6 +40,12 @@ export function publicMcpController(loopback) {
39
40
  const refused = refuseMcpMethod(c.req.method);
40
41
  if (refused)
41
42
  return refused;
43
+ // The discovery entry point, set BEFORE the gate below can refuse: a host that presented no
44
+ // credential (or one too narrow) is answered with the challenge naming this deployment's
45
+ // protected-resource metadata, which is where the MCP authorization spec has a client look for
46
+ // its authorization server. Without it the whole chain is served and unreachable, and the only
47
+ // way to connect is a key pasted into a config file.
48
+ c.set('bearerChallenge', bearerChallenge(new URL(c.req.url).origin));
42
49
  // `read` is the FLOOR, not a per-tool requirement: the ladder is inclusive, so this refuses only
43
50
  // an absent, unknown or revoked key. What each tool may do stays enforced where it already is, by
44
51
  // the `/api/v1` handler the loopback reaches — a scope table restated here would be a second
@@ -1 +1 @@
1
- {"version":3,"file":"PublicMcpController.js","sourceRoot":"","sources":["../../../src/modules/publicApi/PublicMcpController.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,wFAAwF;AACxF,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAA;AAC5D,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAElE,oGAAoG;AACpG,kDAAkD;AAClD,EAAE;AACF,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,sGAAsG;AACtG,+FAA+F;AAC/F,2EAA2E;AAC3E,EAAE;AACF,oGAAoG;AACpG,qGAAqG;AACrG,+FAA+F;AAC/F,sGAAsG;AACtG,gFAAgF;AAChF,EAAE;AACF,qGAAqG;AACrG,mGAAmG;AACnG,qGAAqG;AACrG,mFAAmF;AACnF,4EAA4E;AAC5E,EAAE;AACF,kGAAkG;AAClG,oGAAoG;AACpG,oGAAoG;AACpG,4FAA4F;AAE5F,MAAM,UAAU,mBAAmB,CAAmB,QAAwB;IAC5E,MAAM,GAAG,GAAG,IAAI,IAAI,EAAK,CAAA;IAEzB,gGAAgG;IAChG,mEAAmE;IACnE,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACjC,MAAM,OAAO,GAAG,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAC7C,IAAI,OAAO;YAAE,OAAO,OAAO,CAAA;QAE3B,iGAAiG;QACjG,kGAAkG;QAClG,6FAA6F;QAC7F,mEAAmE;QACnE,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;QAC9C,MAAM,GAAG,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;QAE5B,OAAO,oBAAoB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE;YACrC,8FAA8F;YAC9F,gGAAgG;YAChG,wBAAwB;YACxB,OAAO,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM;YAClC,6FAA6F;YAC7F,6FAA6F;YAC7F,yFAAyF;YACzF,mEAAmE;YACnE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE;YAC5B,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7D,gGAAgG;YAChG,+FAA+F;YAC/F,qEAAqE;YACrE,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,EAAE,WAAoB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1F,gBAAgB,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC1B,4FAA4F;gBAC5F,wFAAwF;gBACxF,0FAA0F;gBAC1F,uFAAuF;gBACvF,GAAG,CAAC,IAAI,CAAC,4BAA4B,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA;YAC9D,CAAC;SACF,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,OAAO,GAAG,CAAA;AACZ,CAAC"}
1
+ {"version":3,"file":"PublicMcpController.js","sourceRoot":"","sources":["../../../src/modules/publicApi/PublicMcpController.ts"],"names":[],"mappings":"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"}