@tpsdev-ai/flair 0.32.0 → 0.34.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/README.md +64 -64
- package/SECURITY.md +7 -0
- package/config.yaml +34 -0
- package/dist/cli.js +659 -111
- package/dist/component-env.js +286 -0
- package/dist/deploy.js +190 -3
- package/dist/doctor-client.js +357 -7
- package/dist/hook-install.js +39 -9
- package/dist/lib/auth-resolve.js +85 -2
- package/dist/lib/launchd-management.js +328 -0
- package/dist/lib/mcp-enable.js +19 -0
- package/dist/resources/AdminInstance.js +20 -2
- package/dist/resources/Memory.js +24 -2
- package/dist/resources/OAuth.js +41 -25
- package/dist/resources/auth-middleware.js +26 -0
- package/dist/resources/dcr-gate.js +194 -0
- package/dist/resources/in-process-api.js +5 -1
- package/dist/resources/mcp-handler.js +91 -4
- package/dist/resources/mcp-oauth.js +89 -7
- package/dist/resources/mcp-tools.js +40 -0
- package/dist/resources/oauth-discovery.js +242 -0
- package/dist/resources/oauth-wellknown.js +111 -0
- package/dist/resources/rate-limit.js +400 -0
- package/docs/auth.md +122 -5
- package/docs/deploying-on-fabric.md +35 -2
- package/docs/deployment.md +1 -1
- package/docs/embedding-in-a-harper-app.md +6 -1
- package/docs/hosted-on-fabric.md +1 -1
- package/docs/mcp-clients.md +28 -4
- package/docs/quickstart.md +29 -4
- package/docs/the-team.md +8 -4
- package/docs/troubleshooting.md +37 -1
- package/package.json +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oauth-discovery.ts — the ONE builder for every OAuth discovery document
|
|
3
|
+
* flair serves, the paths they are served at, and the request handlers that
|
|
4
|
+
* serve them. Deliberately free of any `harper` import so all of it is
|
|
5
|
+
* directly unit-testable; the Harper route registration lives next door in
|
|
6
|
+
* resources/oauth-wellknown.ts.
|
|
7
|
+
*
|
|
8
|
+
* Three surfaces render these documents:
|
|
9
|
+
*
|
|
10
|
+
* - `GET /OAuthMetadata` (resources/OAuth.ts, historical path)
|
|
11
|
+
* - `GET /.well-known/oauth-authorization-server` (RFC 8414)
|
|
12
|
+
* - `GET /.well-known/oauth-protected-resource` (RFC 9728)
|
|
13
|
+
*
|
|
14
|
+
* The first two return the SAME object from the SAME function — `/OAuthMetadata`
|
|
15
|
+
* is an ALIAS, not a second implementation. Two endpoints that can drift apart
|
|
16
|
+
* is the defect this module exists to make impossible: a field added to the
|
|
17
|
+
* authorization-server document appears at both paths or at neither, and there
|
|
18
|
+
* is no code path that can produce one without the other.
|
|
19
|
+
*
|
|
20
|
+
* ── Issuer derivation is NOT redefined here ─────────────────────────────────
|
|
21
|
+
* `oauthPublicBaseUrl()` is the expression `OAuthMetadata.get()` already used,
|
|
22
|
+
* moved verbatim: `FLAIR_PUBLIC_URL`, else the loopback bind address. Operators
|
|
23
|
+
* MUST set `FLAIR_PUBLIC_URL` on any non-loopback deployment (docs/deploying-on-
|
|
24
|
+
* fabric.md, resources/AdminInstance.ts) or every URL in every one of these
|
|
25
|
+
* documents points at the CLIENT's own localhost. That requirement, and the
|
|
26
|
+
* `loadEnv` declaration that makes a component `.env` actually reach
|
|
27
|
+
* `process.env`, shipped in flair#1005 — deliberately untouched here.
|
|
28
|
+
*/
|
|
29
|
+
import { mcpOAuthEnabled } from "./mcp-oauth-flag.js";
|
|
30
|
+
import { dcrEnabled } from "./dcr-gate.js";
|
|
31
|
+
/** RFC 9728 §3.1 — Protected Resource Metadata well-known path. */
|
|
32
|
+
export const PRM_PATH = "/.well-known/oauth-protected-resource";
|
|
33
|
+
/** RFC 8414 §3 — Authorization Server Metadata well-known path. */
|
|
34
|
+
export const AS_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
35
|
+
/**
|
|
36
|
+
* The public origin every URL in every discovery document derives from.
|
|
37
|
+
*
|
|
38
|
+
* Verbatim the expression `OAuthMetadata.get()` carried before this module
|
|
39
|
+
* existed — moved, not changed. See the header note on flair#1005.
|
|
40
|
+
*/
|
|
41
|
+
export function oauthPublicBaseUrl() {
|
|
42
|
+
return process.env.FLAIR_PUBLIC_URL || `http://127.0.0.1:${process.env.HTTP_PORT || 19926}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The RFC 8707 resource identifier of flair's MCP surface: `<base>/mcp`.
|
|
46
|
+
*
|
|
47
|
+
* Matches `mcpResource()` in resources/mcp-oauth-flag.ts by construction —
|
|
48
|
+
* that is the URI `withMCPAuth` audience-binds tokens to when the Model-2
|
|
49
|
+
* surface is enabled, so the protected-resource document must name the same
|
|
50
|
+
* string or a client would audience-bind its token to something `/mcp` rejects.
|
|
51
|
+
*
|
|
52
|
+
* It names `/mcp` even when `FLAIR_MCP_OAUTH` is off and `/mcp` therefore
|
|
53
|
+
* 404s. That is a coherent state, not a lie: RFC 9728 metadata describes how a
|
|
54
|
+
* resource WOULD be authorized; a client that discovers the authorization
|
|
55
|
+
* server and then finds no `/mcp` has learned something true. The alternative —
|
|
56
|
+
* naming the origin itself as the protected resource — would be the actual
|
|
57
|
+
* lie, because no flair REST resource accepts a bearer token (see
|
|
58
|
+
* resources/oauth-wellknown.ts's header for that measurement).
|
|
59
|
+
*/
|
|
60
|
+
export function mcpResourceUri(baseUrl = oauthPublicBaseUrl()) {
|
|
61
|
+
return `${baseUrl.replace(/\/+$/, "")}/mcp`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* RFC 8414 Authorization Server Metadata — flair's own OAuth 2.1 AS
|
|
65
|
+
* (resources/OAuth.ts): `/OAuthAuthorize`, `/OAuthToken`, `/OAuthRegister`,
|
|
66
|
+
* `/OAuthRevoke`.
|
|
67
|
+
*
|
|
68
|
+
* Served at BOTH `/.well-known/oauth-authorization-server` and `/OAuthMetadata`.
|
|
69
|
+
*
|
|
70
|
+
* `registration_endpoint` is present ONLY while dynamic client registration is
|
|
71
|
+
* actually enabled (resources/dcr-gate.ts). RFC 8414 s2 makes the field
|
|
72
|
+
* OPTIONAL, and advertising an endpoint that refuses every request is a
|
|
73
|
+
* discovery document that misdirects: a client would follow it, be refused, and
|
|
74
|
+
* have learned nothing it can act on. Omitting it tells the truth — this server
|
|
75
|
+
* does not do dynamic registration — which is a state a spec-compliant client
|
|
76
|
+
* already knows how to handle.
|
|
77
|
+
*/
|
|
78
|
+
export function buildAuthorizationServerMetadata(baseUrl = oauthPublicBaseUrl()) {
|
|
79
|
+
return {
|
|
80
|
+
issuer: baseUrl,
|
|
81
|
+
authorization_endpoint: `${baseUrl}/OAuthAuthorize`,
|
|
82
|
+
token_endpoint: `${baseUrl}/OAuthToken`,
|
|
83
|
+
...(dcrEnabled() ? { registration_endpoint: `${baseUrl}/OAuthRegister` } : {}),
|
|
84
|
+
revocation_endpoint: `${baseUrl}/OAuthRevoke`,
|
|
85
|
+
response_types_supported: ["code"],
|
|
86
|
+
grant_types_supported: [
|
|
87
|
+
"authorization_code",
|
|
88
|
+
"refresh_token",
|
|
89
|
+
"urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
90
|
+
],
|
|
91
|
+
token_endpoint_auth_methods_supported: ["none", "client_secret_basic"],
|
|
92
|
+
code_challenge_methods_supported: ["S256"],
|
|
93
|
+
scopes_supported: [
|
|
94
|
+
"memory:read", "memory:write", "memory:admin",
|
|
95
|
+
"principal:read", "principal:admin",
|
|
96
|
+
"connector:read", "connector:admin",
|
|
97
|
+
],
|
|
98
|
+
extensions_supported: [
|
|
99
|
+
"io.modelcontextprotocol/enterprise-managed-authorization",
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* RFC 9728 Protected Resource Metadata for flair's MCP surface.
|
|
105
|
+
*
|
|
106
|
+
* `authorization_servers` names the SAME `issuer` string the authorization-
|
|
107
|
+
* server document publishes — both come from `oauthPublicBaseUrl()`, so a
|
|
108
|
+
* client that follows `authorization_servers[0]` +
|
|
109
|
+
* `/.well-known/oauth-authorization-server` lands on a document whose `issuer`
|
|
110
|
+
* matches what sent it there. That loop closing is asserted end-to-end in
|
|
111
|
+
* test/integration/oauth-wellknown-e2e.test.ts.
|
|
112
|
+
*
|
|
113
|
+
* `scopes_supported` is deliberately the same list the AS advertises rather
|
|
114
|
+
* than a second, narrower one — a resource advertising scopes the AS cannot
|
|
115
|
+
* issue would send clients into a guaranteed `invalid_scope`.
|
|
116
|
+
*/
|
|
117
|
+
export function buildProtectedResourceMetadata(baseUrl = oauthPublicBaseUrl()) {
|
|
118
|
+
const as = buildAuthorizationServerMetadata(baseUrl);
|
|
119
|
+
return {
|
|
120
|
+
resource: mcpResourceUri(baseUrl),
|
|
121
|
+
authorization_servers: [as.issuer],
|
|
122
|
+
// RFC 9728 §2 — the only presentation method `withMCPAuth` accepts is the
|
|
123
|
+
// Authorization header; it never reads a form or query-parameter token.
|
|
124
|
+
bearer_methods_supported: ["header"],
|
|
125
|
+
scopes_supported: as.scopes_supported,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ─── Path screening ──────────────────────────────────────────────────────────
|
|
129
|
+
//
|
|
130
|
+
// Harper's `server.http({ urlPath })` matching is prefix-based and passes the
|
|
131
|
+
// path RELATIVE to the mount, so every sub-path of a mount reaches its handler
|
|
132
|
+
// and has to be screened here — otherwise
|
|
133
|
+
// `/.well-known/oauth-protected-resource/anything` would answer with flair's
|
|
134
|
+
// document.
|
|
135
|
+
/**
|
|
136
|
+
* Does a request address the protected-resource document?
|
|
137
|
+
*
|
|
138
|
+
* Accepted:
|
|
139
|
+
* - `/` the bare well-known path
|
|
140
|
+
* - `/mcp` RFC 9728 §3.1 path-insertion for the resource `<base>/mcp`. MCP
|
|
141
|
+
* clients (Claude.ai among them) build the PRM URL by inserting
|
|
142
|
+
* the resource's path component and fetch THIS form, so without
|
|
143
|
+
* it the discovery loop 404s on the only URL a real client asks for.
|
|
144
|
+
* - the absolute forms of both, for Harper builds that pass an unstripped path.
|
|
145
|
+
*
|
|
146
|
+
* Exact-after-normalization, never a prefix test — `/mcp-evil` must not match
|
|
147
|
+
* `/mcp`.
|
|
148
|
+
*/
|
|
149
|
+
export function prmPathMatches(relativePath, baseUrl = oauthPublicBaseUrl()) {
|
|
150
|
+
const path = normalizePath(relativePath);
|
|
151
|
+
if (path === "/" || path === PRM_PATH)
|
|
152
|
+
return true;
|
|
153
|
+
const resourcePath = resourcePathOf(baseUrl);
|
|
154
|
+
if (!resourcePath)
|
|
155
|
+
return false;
|
|
156
|
+
return path === resourcePath || path === PRM_PATH + resourcePath;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Same screen for the authorization-server document. RFC 8414 §3.1 path-
|
|
160
|
+
* insertion applies to an issuer that CARRIES a path component; flair's issuer
|
|
161
|
+
* is always an origin, so only the bare path is valid here and every sub-path
|
|
162
|
+
* is a 404.
|
|
163
|
+
*/
|
|
164
|
+
export function asMetadataPathMatches(relativePath) {
|
|
165
|
+
const path = normalizePath(relativePath);
|
|
166
|
+
return path === "/" || path === AS_METADATA_PATH;
|
|
167
|
+
}
|
|
168
|
+
/** Strip a single trailing slash so `/mcp/` and `/mcp` screen identically. */
|
|
169
|
+
function normalizePath(path) {
|
|
170
|
+
if (!path)
|
|
171
|
+
return "/";
|
|
172
|
+
return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
173
|
+
}
|
|
174
|
+
/** The path component of the protected resource URI (`/mcp`), or "" if unparseable. */
|
|
175
|
+
function resourcePathOf(baseUrl) {
|
|
176
|
+
try {
|
|
177
|
+
const { pathname } = new URL(mcpResourceUri(baseUrl));
|
|
178
|
+
return pathname && pathname !== "/" ? pathname.replace(/\/+$/, "") : "";
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ─── Handlers ────────────────────────────────────────────────────────────────
|
|
185
|
+
function discoveryResponse(body) {
|
|
186
|
+
return new Response(JSON.stringify(body), {
|
|
187
|
+
status: 200,
|
|
188
|
+
headers: {
|
|
189
|
+
"content-type": "application/json",
|
|
190
|
+
// Discovery documents are unauthenticated, non-secret, and fetched
|
|
191
|
+
// cross-origin by browser-based MCP clients and inspectors. Simple `*` is
|
|
192
|
+
// sufficient because no credentials are ever involved. Matches what
|
|
193
|
+
// @harperfast/oauth sets on the same documents.
|
|
194
|
+
"access-control-allow-origin": "*",
|
|
195
|
+
"access-control-allow-methods": "GET, OPTIONS",
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function notFound() {
|
|
200
|
+
return new Response(JSON.stringify({ error: "not_found" }), {
|
|
201
|
+
status: 404,
|
|
202
|
+
headers: { "content-type": "application/json" },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** The mount-relative path Harper hands the handler, query string removed. */
|
|
206
|
+
export function relativePathOf(request) {
|
|
207
|
+
const raw = request?.pathname ?? request?.url ?? "/";
|
|
208
|
+
const q = raw.indexOf("?");
|
|
209
|
+
return q >= 0 ? raw.slice(0, q) : raw;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Build the handler for one well-known document.
|
|
213
|
+
*
|
|
214
|
+
* Three behaviours, in order:
|
|
215
|
+
*
|
|
216
|
+
* 1. A path under the mount that is NOT one of the accepted discovery forms
|
|
217
|
+
* gets a 404 from HERE — it is never passed to `next`. A urlPath mount has
|
|
218
|
+
* its OWN dispatch chain that contains neither flair's auth-middleware nor
|
|
219
|
+
* Harper's `authentication`, and `next` strips the mount prefix, so passing
|
|
220
|
+
* an arbitrary sub-path onward is how a discovery mount turns into a path-
|
|
221
|
+
* confusion hole. Screen, then answer; never forward.
|
|
222
|
+
* 2. `FLAIR_MCP_OAUTH` on → fall through for the accepted forms, so
|
|
223
|
+
* @harperfast/oauth's own well-known handlers (identical urlPath, same
|
|
224
|
+
* dispatch group) answer for the surface they actually guard. See
|
|
225
|
+
* resources/oauth-wellknown.ts's header for why flair's AS must not be
|
|
226
|
+
* advertised in that state.
|
|
227
|
+
* 3. Otherwise serve the document.
|
|
228
|
+
*
|
|
229
|
+
* Non-GET/HEAD is a 404 too: a POST to a discovery path is not a discovery
|
|
230
|
+
* request and must not be answered with a 200 body.
|
|
231
|
+
*/
|
|
232
|
+
export function makeWellKnownHandler(matches, build, isMcpOAuthEnabled = mcpOAuthEnabled) {
|
|
233
|
+
return async (request, next) => {
|
|
234
|
+
const method = String(request?.method ?? "GET").toUpperCase();
|
|
235
|
+
if (!matches(relativePathOf(request)) || (method !== "GET" && method !== "HEAD")) {
|
|
236
|
+
return notFound();
|
|
237
|
+
}
|
|
238
|
+
if (isMcpOAuthEnabled())
|
|
239
|
+
return next(request);
|
|
240
|
+
return discoveryResponse(build());
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oauth-wellknown.ts — mounts flair's OAuth discovery documents at the two
|
|
3
|
+
* well-known paths every spec-compliant client probes (flair#1000 item 2).
|
|
4
|
+
*
|
|
5
|
+
* Before this, flair published a correct authorization-server document at
|
|
6
|
+
* `/OAuthMetadata` — a path nothing in the ecosystem asks for — and 404'd at
|
|
7
|
+
* `/.well-known/oauth-authorization-server` (RFC 8414) and
|
|
8
|
+
* `/.well-known/oauth-protected-resource` (RFC 9728, which the MCP
|
|
9
|
+
* authorization specification makes a MUST). A remote MCP client could not
|
|
10
|
+
* discover flair no matter what else was configured.
|
|
11
|
+
*
|
|
12
|
+
* The documents themselves, and every path-screening decision, live in
|
|
13
|
+
* resources/oauth-discovery.ts — this file is only the Harper wiring.
|
|
14
|
+
*
|
|
15
|
+
* ── Why `server.http({ urlPath })` and not a Resource ───────────────────────
|
|
16
|
+
* Harper's REST layer maps a Resource CLASS NAME to a path segment; no class
|
|
17
|
+
* name produces `/.well-known/oauth-protected-resource`. A urlPath mount is the
|
|
18
|
+
* supported route for paths outside the Resource naming scheme — the same
|
|
19
|
+
* mechanism resources/mcp-oauth.ts uses for `/mcp` and @harperfast/oauth uses
|
|
20
|
+
* for its own well-known documents.
|
|
21
|
+
*
|
|
22
|
+
* A urlPath mount gets its OWN dispatch chain, so neither flair's
|
|
23
|
+
* auth-middleware nor Harper's `authentication` middleware runs for these
|
|
24
|
+
* paths. That is required, not incidental: RFC 8414 §3 and RFC 9728 §3 both
|
|
25
|
+
* require the documents be retrievable WITHOUT authentication, and Harper's
|
|
26
|
+
* auth layer stamps `WWW-Authenticate: Basic` on any 401 it wraps.
|
|
27
|
+
* (`/.well-known/oauth-authorization-server` remains in auth-middleware.ts's
|
|
28
|
+
* public allowlist; that entry is now belt-and-braces — a request for it never
|
|
29
|
+
* reaches the default chain.)
|
|
30
|
+
*
|
|
31
|
+
* ── Deference to @harperfast/oauth when the Model-2 surface is on ───────────
|
|
32
|
+
* `FLAIR_MCP_OAUTH` mounts an OAuth-guarded `/mcp` wrapped in the plugin's
|
|
33
|
+
* `withMCPAuth` (resources/mcp-oauth.ts). Tokens for THAT surface are minted by
|
|
34
|
+
* the PLUGIN's authorization server (`/oauth/mcp/token`, JWTs verified against
|
|
35
|
+
* its JWKS), NOT by flair's own OAuth 2.1 AS, which mints opaque `flair_at_…`
|
|
36
|
+
* strings the guard rejects. Advertising flair's AS as the authorization server
|
|
37
|
+
* for `/mcp` in that state would hand a client a token `/mcp` is guaranteed to
|
|
38
|
+
* refuse.
|
|
39
|
+
*
|
|
40
|
+
* So when the flag is on these handlers serve nothing and fall through, and the
|
|
41
|
+
* plugin's own well-known handlers (registered by its `handleApplication` when
|
|
42
|
+
* an operator declares the `'@harperfast/oauth'` component) answer with the
|
|
43
|
+
* document describing the AS that actually guards the surface. Making this a
|
|
44
|
+
* function of flair's own flag keeps it deterministic: two handlers mounted on
|
|
45
|
+
* an identical urlPath share one dispatch group and run in REGISTRATION order,
|
|
46
|
+
* so without an explicit rule the answer would depend on where an operator
|
|
47
|
+
* happened to put a key in config.yaml.
|
|
48
|
+
*
|
|
49
|
+
* Gap named rather than papered over: flag ON with the plugin component NOT
|
|
50
|
+
* declared serves neither document (404, exactly as before this change). That
|
|
51
|
+
* instance is already non-functional — `/mcp` is guarded by a verifier with no
|
|
52
|
+
* authorization server behind it, so there is no token to discover.
|
|
53
|
+
*
|
|
54
|
+
* ── What this does NOT change: the 401 challenge (flair#1000 item 3) ────────
|
|
55
|
+
* flair's REST surface deliberately keeps the challenge it has, because a
|
|
56
|
+
* Bearer challenge there would be FALSE. A bearer token cannot reach a flair
|
|
57
|
+
* resource at all: Harper's own auth layer claims every `Bearer …` header for
|
|
58
|
+
* itself and validates it as a Harper OPERATION token, so
|
|
59
|
+
* `Authorization: Bearer <anything>` answers 401 `{"error":"invalid token"}`
|
|
60
|
+
* before any code under resources/ runs (measured against a live instance; the
|
|
61
|
+
* strategy switch is in node_modules/harper/dist/security/auth.js). flair's own
|
|
62
|
+
* `/OAuthToken` mints opaque `flair_at_…` values that nothing under resources/
|
|
63
|
+
* ever validates, so there is not even a token that WOULD work.
|
|
64
|
+
*
|
|
65
|
+
* Advertising `WWW-Authenticate: Bearer resource_metadata=…` on `/Memory` would
|
|
66
|
+
* therefore announce, per RFC 7235 §4.1, a scheme usable at that resource, and
|
|
67
|
+
* send every MCP client that believed it around a loop that ends in exactly the
|
|
68
|
+
* same 401 with no recovery — strictly worse than the honest challenge, because
|
|
69
|
+
* it misdirects instead of refusing.
|
|
70
|
+
*
|
|
71
|
+
* It would also mean rewriting headers on EVERY 401 in the instance. Harper's
|
|
72
|
+
* auth layer owns that header for every 401 raised at or below it —
|
|
73
|
+
* `response.headers.set('WWW-Authenticate', 'Basic')`, a set and not an append
|
|
74
|
+
* (security/auth.js). flair's middleware is registered ahead of it (config.yaml
|
|
75
|
+
* orders `jsResource` before `authentication`) so it *could* override on the way
|
|
76
|
+
* out — verified by `/Admin` keeping its own `Basic realm="Flair Admin"` — but
|
|
77
|
+
* only by awaiting and inspecting every response on the hottest path in the
|
|
78
|
+
* chain, for every existing Basic and Ed25519 caller, to publish a scheme none
|
|
79
|
+
* of them can use.
|
|
80
|
+
*
|
|
81
|
+
* The challenge belongs on the one surface that DOES take a bearer token —
|
|
82
|
+
* `/mcp` — and it is already there: with `FLAIR_MCP_OAUTH` on, `withMCPAuth`
|
|
83
|
+
* answers `POST /mcp` with
|
|
84
|
+
* `401 WWW-Authenticate: Bearer resource_metadata="<issuer>/.well-known/oauth-protected-resource/mcp"`.
|
|
85
|
+
* Until this change that URL 404'd, which is the sense in which item 3 was
|
|
86
|
+
* broken end to end: the challenge existed and pointed at nothing. flair now
|
|
87
|
+
* answers that exact path (and @harperfast/oauth answers it when the flag is on
|
|
88
|
+
* and the plugin is declared), so the discovery loop closes.
|
|
89
|
+
*/
|
|
90
|
+
import { server } from "harper";
|
|
91
|
+
import { AS_METADATA_PATH, PRM_PATH, asMetadataPathMatches, buildAuthorizationServerMetadata, buildProtectedResourceMetadata, makeWellKnownHandler, prmPathMatches, } from "./oauth-discovery.js";
|
|
92
|
+
import { mcpOAuthEnabled } from "./mcp-oauth-flag.js";
|
|
93
|
+
/**
|
|
94
|
+
* Mount both documents. Called once at module load, and directly from tests.
|
|
95
|
+
* Returns the urlPaths mounted so a caller can assert on them.
|
|
96
|
+
*/
|
|
97
|
+
export function registerOAuthWellKnownRoutes(deps = {}) {
|
|
98
|
+
const srv = deps.server ?? server;
|
|
99
|
+
if (typeof srv?.http !== "function")
|
|
100
|
+
return [];
|
|
101
|
+
const enabled = deps.isMcpOAuthEnabled ?? mcpOAuthEnabled;
|
|
102
|
+
srv.http(makeWellKnownHandler(prmPathMatches, buildProtectedResourceMetadata, enabled), { urlPath: PRM_PATH });
|
|
103
|
+
srv.http(makeWellKnownHandler(asMetadataPathMatches, buildAuthorizationServerMetadata, enabled), { urlPath: AS_METADATA_PATH });
|
|
104
|
+
return [PRM_PATH, AS_METADATA_PATH];
|
|
105
|
+
}
|
|
106
|
+
// Registered at module load, like resources/auth-middleware.ts. The opt-out
|
|
107
|
+
// exists only so a unit test can import this module under a partial `harper`
|
|
108
|
+
// mock without registering routes; production never sets it.
|
|
109
|
+
if (process.env.FLAIR_WELLKNOWN_NO_AUTOSTART == null) {
|
|
110
|
+
registerOAuthWellKnownRoutes();
|
|
111
|
+
}
|