@corenel/mcp 0.1.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 (74) hide show
  1. package/LICENSE +93 -0
  2. package/dist/asAgentTools.d.ts +12 -0
  3. package/dist/asAgentTools.d.ts.map +1 -0
  4. package/dist/asAgentTools.js +71 -0
  5. package/dist/asAgentTools.js.map +1 -0
  6. package/dist/auth.d.ts +25 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +97 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/cache.d.ts +17 -0
  11. package/dist/cache.d.ts.map +1 -0
  12. package/dist/cache.js +81 -0
  13. package/dist/cache.js.map +1 -0
  14. package/dist/config.d.ts +71 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +239 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/gatedAgentTools.d.ts +12 -0
  19. package/dist/gatedAgentTools.d.ts.map +1 -0
  20. package/dist/gatedAgentTools.js +36 -0
  21. package/dist/gatedAgentTools.js.map +1 -0
  22. package/dist/httpClient.d.ts +47 -0
  23. package/dist/httpClient.d.ts.map +1 -0
  24. package/dist/httpClient.js +196 -0
  25. package/dist/httpClient.js.map +1 -0
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/oauth/discovery.d.ts +55 -0
  31. package/dist/oauth/discovery.d.ts.map +1 -0
  32. package/dist/oauth/discovery.js +211 -0
  33. package/dist/oauth/discovery.js.map +1 -0
  34. package/dist/oauth/login.d.ts +41 -0
  35. package/dist/oauth/login.d.ts.map +1 -0
  36. package/dist/oauth/login.js +206 -0
  37. package/dist/oauth/login.js.map +1 -0
  38. package/dist/oauth/loopback.d.ts +58 -0
  39. package/dist/oauth/loopback.d.ts.map +1 -0
  40. package/dist/oauth/loopback.js +165 -0
  41. package/dist/oauth/loopback.js.map +1 -0
  42. package/dist/oauth/pkce.d.ts +45 -0
  43. package/dist/oauth/pkce.d.ts.map +1 -0
  44. package/dist/oauth/pkce.js +67 -0
  45. package/dist/oauth/pkce.js.map +1 -0
  46. package/dist/oauth/register.d.ts +34 -0
  47. package/dist/oauth/register.d.ts.map +1 -0
  48. package/dist/oauth/register.js +57 -0
  49. package/dist/oauth/register.js.map +1 -0
  50. package/dist/oauth/tokens.d.ts +62 -0
  51. package/dist/oauth/tokens.d.ts.map +1 -0
  52. package/dist/oauth/tokens.js +118 -0
  53. package/dist/oauth/tokens.js.map +1 -0
  54. package/dist/policy.d.ts +10 -0
  55. package/dist/policy.d.ts.map +1 -0
  56. package/dist/policy.js +71 -0
  57. package/dist/policy.js.map +1 -0
  58. package/dist/registry.d.ts +124 -0
  59. package/dist/registry.d.ts.map +1 -0
  60. package/dist/registry.js +265 -0
  61. package/dist/registry.js.map +1 -0
  62. package/dist/timedFetch.d.ts +10 -0
  63. package/dist/timedFetch.d.ts.map +1 -0
  64. package/dist/timedFetch.js +39 -0
  65. package/dist/timedFetch.js.map +1 -0
  66. package/dist/toolNames.d.ts +2 -0
  67. package/dist/toolNames.d.ts.map +1 -0
  68. package/dist/toolNames.js +14 -0
  69. package/dist/toolNames.js.map +1 -0
  70. package/dist/types.d.ts +62 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +19 -0
  73. package/dist/types.js.map +1 -0
  74. package/package.json +44 -0
@@ -0,0 +1,55 @@
1
+ export interface AuthMetadata {
2
+ authorizationEndpoint: string;
3
+ tokenEndpoint: string;
4
+ registrationEndpoint?: string;
5
+ scopesSupported?: string[];
6
+ }
7
+ /** Shape of a fetch Response this module actually uses -- narrow on purpose
8
+ * so the injected fetch in tests can return a minimal stub. */
9
+ type MinimalResponse = {
10
+ ok: boolean;
11
+ status: number;
12
+ headers: {
13
+ get(name: string): string | null;
14
+ };
15
+ json(): Promise<unknown>;
16
+ };
17
+ type FetchLike = (url: string, init?: {
18
+ headers?: Record<string, string>;
19
+ }) => Promise<MinimalResponse>;
20
+ /**
21
+ * Discover the authorization server an MCP server trusts.
22
+ *
23
+ * Tries, in order:
24
+ * 1. GET `<origin>/.well-known/oauth-protected-resource` directly.
25
+ * 2. GET the MCP URL itself; if it 401s with a `WWW-Authenticate:
26
+ * Bearer resource_metadata="<url>"` header, follow that URL instead --
27
+ * this is the MORE RELIABLE path when present, since it is the server
28
+ * naming its own document rather than us guessing the well-known path
29
+ * relative to its origin.
30
+ *
31
+ * Returns `null` when NEITHER path yields a document. That is not an error:
32
+ * it means the server takes a static token (headers credential), which is a
33
+ * fully supported configuration -- see McpHeadersCredential in ../types.ts.
34
+ * Callers must not report a null return as a failure.
35
+ */
36
+ export interface DiscoveredAuthServer {
37
+ authServer: string;
38
+ resource: string;
39
+ /** The RESOURCE's `scopes_supported` (RFC 9728 s2) -- authoritative for this
40
+ * resource, and not the same list as the authorization server's own. */
41
+ scopes?: string[];
42
+ }
43
+ export declare function discoverAuthServer(mcpUrl: string, fetchImpl?: FetchLike): Promise<DiscoveredAuthServer | null>;
44
+ /**
45
+ * Fetch and validate an authorization server's own metadata (RFC 8414).
46
+ *
47
+ * A document missing `authorization_endpoint` or `token_endpoint` is rejected
48
+ * outright, naming `authServer` in the error, rather than returned as a
49
+ * partial `AuthMetadata` -- an authorization server we cannot get a token
50
+ * from is useless, and failing here with a clear message beats failing later
51
+ * inside a token exchange with no context about which server was at fault.
52
+ */
53
+ export declare function discoverAuthMetadata(authServer: string, fetchImpl?: FetchLike): Promise<AuthMetadata>;
54
+ export {};
55
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/oauth/discovery.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,YAAY;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;gEACgE;AAChE,KAAK,eAAe,GAAG;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC9C,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B,CAAC;AAEF,KAAK,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AA8ExG;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB;6EACyE;IACzE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,SAAyC,GACnD,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA4CtC;AAiCD;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,SAAyC,GACnD,OAAO,CAAC,YAAY,CAAC,CAmCvB"}
@@ -0,0 +1,211 @@
1
+ /* FINDING AN MCP SERVER'S AUTHORIZATION SERVER, AND THAT SERVER'S ENDPOINTS.
2
+ *
3
+ * This is discovery ONLY -- it answers "who do I ask for a token, and where".
4
+ * It does not register a client, run PKCE, or exchange/refresh a token; those
5
+ * are later tasks that consume `AuthMetadata`'s `authorizationEndpoint` /
6
+ * `tokenEndpoint`. Keeping this module to just the two GETs means a later
7
+ * change to the login flow never has to touch the part that already works.
8
+ *
9
+ * `fetch` is injected (last parameter, default global fetch) so tests never
10
+ * open a socket -- see discovery.test.ts.
11
+ */
12
+ /** RFC 9728 well-known path, resolved against the MCP server's own origin. */
13
+ const PROTECTED_RESOURCE_PATH = '/.well-known/oauth-protected-resource';
14
+ /** RFC 8414 well-known path, resolved against the authorization server's
15
+ * issuer URL. */
16
+ const AUTH_SERVER_METADATA_PATH = '/.well-known/oauth-authorization-server';
17
+ /** GET `url` as JSON, or null on any failure (network error, non-2xx, not
18
+ * JSON, not an object). Discovery documents are best-effort by design --
19
+ * everywhere this is called, a missing document is a legitimate outcome
20
+ * (see the design note on `discoverAuthServer`), not a bug to surface. */
21
+ async function tryFetchJson(url, fetchImpl) {
22
+ let res;
23
+ try {
24
+ res = await fetchImpl(url);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ if (!res.ok)
30
+ return null;
31
+ let body;
32
+ try {
33
+ body = await res.json();
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ if (typeof body !== 'object' || body === null || Array.isArray(body))
39
+ return null;
40
+ return body;
41
+ }
42
+ /** Pull the first `authorization_servers` entry out of a parsed protected-
43
+ * resource document, or null if the document doesn't have one. */
44
+ function firstAuthServer(doc) {
45
+ const list = doc.authorization_servers;
46
+ if (!Array.isArray(list) || list.length === 0)
47
+ return null;
48
+ const first = list[0];
49
+ return typeof first === 'string' && first.length > 0 ? first : null;
50
+ }
51
+ /** A discovery document's `scopes_supported`, or undefined unless it is a
52
+ * non-empty list of strings.
53
+ *
54
+ * Undefined rather than `[]` on purpose: callers distinguish "this resource
55
+ * advertises no scopes" (monday, legitimately) from "it advertises these",
56
+ * and an empty array would make the first look like the second and put an
57
+ * empty `scope=` on the authorization URL. */
58
+ function scopesFrom(doc) {
59
+ const list = doc.scopes_supported;
60
+ if (!Array.isArray(list) || list.length === 0)
61
+ return undefined;
62
+ return list.every((s) => typeof s === 'string') ? list : undefined;
63
+ }
64
+ /** Build the discovery result, carrying the resource's own scope list. */
65
+ function resourceResult(doc, authServer, mcpUrl) {
66
+ const scopes = scopesFrom(doc);
67
+ return { authServer, resource: resourceFor(doc, mcpUrl), ...(scopes ? { scopes } : {}) };
68
+ }
69
+ /** Extract the `resource_metadata` URL from a `WWW-Authenticate: Bearer
70
+ * resource_metadata="..."` header value, or null if the header is absent or
71
+ * doesn't carry that parameter. RFC 9728 section 5.1. */
72
+ function resourceMetadataUrlFromHeader(headerValue) {
73
+ if (!headerValue)
74
+ return null;
75
+ const match = /resource_metadata="([^"]+)"/.exec(headerValue);
76
+ return match ? match[1] : null;
77
+ }
78
+ /**
79
+ * The RFC 8707 resource indicator to bind a token to -- NEVER the address of
80
+ * the metadata document that was just fetched.
81
+ *
82
+ * A protected-resource document may name itself via its own `resource` field
83
+ * (RFC 9728 section 2, the canonical identifier for the resource server).
84
+ * When it does, that value wins. When it doesn't, `mcpUrl` -- the MCP server
85
+ * address `discoverAuthServer` was actually asked about -- is the only other
86
+ * value that identifies the resource server itself; the `.well-known`
87
+ * document URL identifies the METADATA, not the resource, and a token bound
88
+ * to it is a token the resource server has no reason to recognize as its
89
+ * own.
90
+ */
91
+ function resourceFor(doc, mcpUrl) {
92
+ return typeof doc.resource === 'string' && doc.resource.length > 0 ? doc.resource : mcpUrl;
93
+ }
94
+ export async function discoverAuthServer(mcpUrl, fetchImpl = fetch) {
95
+ const origin = new URL(mcpUrl).origin;
96
+ const wellKnownUrl = new URL(PROTECTED_RESOURCE_PATH, origin).toString();
97
+ const direct = await tryFetchJson(wellKnownUrl, fetchImpl);
98
+ if (direct) {
99
+ const authServer = firstAuthServer(direct);
100
+ if (authServer)
101
+ return resourceResult(direct, authServer, mcpUrl);
102
+ }
103
+ // No document at the well-known path (or it didn't name an auth server) --
104
+ // fall back to asking the MCP endpoint itself and reading the 401 hint.
105
+ const viaHint = await hintedResource(mcpUrl, fetchImpl);
106
+ if (viaHint) {
107
+ const authServer = firstAuthServer(viaHint);
108
+ if (authServer)
109
+ return resourceResult(viaHint, authServer, mcpUrl);
110
+ }
111
+ /* LAST RESORT: the resource may BE its own authorization server.
112
+ *
113
+ * Nothing above found a protected-resource document, which used to end the
114
+ * search and report null -- and a null return means "static token only", so
115
+ * the user was told the service has no automatic sign-in. Measured
116
+ * 2026-09-14, that was false for two of our catalogue: Atlassian and
117
+ * Intercom publish no RFC 9728 document and hint at none on their 401, yet
118
+ * both serve RFC 8414 metadata at their own origin complete with a
119
+ * registration_endpoint.
120
+ *
121
+ * Tried ONLY here, after both RFC 9728 routes have failed, so a server that
122
+ * names its authorization server is always believed over this guess. That
123
+ * ordering is load-bearing: Trello's issuer is a per-tenant path on
124
+ * auth.atlassian.com, and that host ALSO answers the bare root well-known
125
+ * with a different issuer carrying no registration_endpoint. Letting this
126
+ * pre-empt a named authorization server would cost Trello its DCR.
127
+ *
128
+ * The origin is used as the issuer only when the document actually proves
129
+ * itself one, by carrying an authorization_endpoint. */
130
+ const own = await tryFetchJson(authServerMetadataUrl(origin), fetchImpl);
131
+ if (own && typeof own.authorization_endpoint === 'string' && own.authorization_endpoint.length > 0) {
132
+ const issuer = typeof own.issuer === 'string' && own.issuer.length > 0 ? own.issuer : origin;
133
+ return resourceResult(own, issuer, mcpUrl);
134
+ }
135
+ return null;
136
+ }
137
+ /** The protected-resource document an MCP endpoint points at from its own 401
138
+ * (RFC 9728 s5.1), or null if it does not 401, does not carry the hint, or
139
+ * the hinted document cannot be read. */
140
+ async function hintedResource(mcpUrl, fetchImpl) {
141
+ let probe;
142
+ try {
143
+ probe = await fetchImpl(mcpUrl);
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ if (probe.status !== 401)
149
+ return null;
150
+ const hinted = resourceMetadataUrlFromHeader(probe.headers.get('www-authenticate'));
151
+ if (!hinted)
152
+ return null;
153
+ return tryFetchJson(hinted, fetchImpl);
154
+ }
155
+ /**
156
+ * The RFC 8414 metadata URL for an issuer, with the well-known segment inserted
157
+ * ahead of the issuer's path.
158
+ *
159
+ * `https://host` -> `https://host/.well-known/oauth-authorization-server`
160
+ * `https://host/mcp` -> `https://host/.well-known/oauth-authorization-server/mcp`
161
+ */
162
+ function authServerMetadataUrl(authServer) {
163
+ const u = new URL(authServer);
164
+ // Trailing slashes are not part of the issuer's path for this purpose; a
165
+ // doubled slash in the result is a 404 on servers that route strictly.
166
+ const issuerPath = u.pathname.replace(/\/+$/, '');
167
+ return `${u.origin}${AUTH_SERVER_METADATA_PATH}${issuerPath}`;
168
+ }
169
+ /**
170
+ * Fetch and validate an authorization server's own metadata (RFC 8414).
171
+ *
172
+ * A document missing `authorization_endpoint` or `token_endpoint` is rejected
173
+ * outright, naming `authServer` in the error, rather than returned as a
174
+ * partial `AuthMetadata` -- an authorization server we cannot get a token
175
+ * from is useless, and failing here with a clear message beats failing later
176
+ * inside a token exchange with no context about which server was at fault.
177
+ */
178
+ export async function discoverAuthMetadata(authServer, fetchImpl = fetch) {
179
+ /* RFC 8414 section 3.1: the well-known segment is INSERTED between the
180
+ * issuer's host and its path, not appended and not substituted. For issuer
181
+ * `https://auth.monday.com/mcp` the document is at
182
+ * `https://auth.monday.com/.well-known/oauth-authorization-server/mcp`.
183
+ *
184
+ * `new URL('/.well-known/...', issuer)` gets this wrong in the quiet way: a
185
+ * LEADING SLASH is root-relative, so the issuer's own `/mcp` is discarded and
186
+ * we ask the root for a document scoped to something else. monday serves both
187
+ * with identical endpoints, so it resolved by luck; a host serving per-tenant
188
+ * documents would have sent the user to another tenant's authorize endpoint.
189
+ *
190
+ * The root form is still tried second, because an issuer with no path
191
+ * produces the same URL either way and some servers only publish there. */
192
+ const doc = await tryFetchJson(authServerMetadataUrl(authServer), fetchImpl)
193
+ ?? await tryFetchJson(new URL(AUTH_SERVER_METADATA_PATH, authServer).toString(), fetchImpl);
194
+ if (!doc) {
195
+ throw new Error(`No OAuth authorization server metadata found at ${authServer}`);
196
+ }
197
+ const authorizationEndpoint = doc.authorization_endpoint;
198
+ const tokenEndpoint = doc.token_endpoint;
199
+ if (typeof authorizationEndpoint !== 'string' || authorizationEndpoint.length === 0) {
200
+ throw new Error(`Authorization server ${authServer} metadata is missing authorization_endpoint`);
201
+ }
202
+ if (typeof tokenEndpoint !== 'string' || tokenEndpoint.length === 0) {
203
+ throw new Error(`Authorization server ${authServer} metadata is missing token_endpoint`);
204
+ }
205
+ const registrationEndpoint = typeof doc.registration_endpoint === 'string' ? doc.registration_endpoint : undefined;
206
+ const scopesSupported = Array.isArray(doc.scopes_supported) && doc.scopes_supported.every((s) => typeof s === 'string')
207
+ ? doc.scopes_supported
208
+ : undefined;
209
+ return { authorizationEndpoint, tokenEndpoint, registrationEndpoint, scopesSupported };
210
+ }
211
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/oauth/discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { McpServer } from '../types';
2
+ /** Why a sign-in did not happen, when it did not. */
3
+ export type McpLoginOutcome = {
4
+ ok: true;
5
+ }
6
+ /** The server advertises no authorization server: it wants a static token. */
7
+ | {
8
+ ok: false;
9
+ reason: 'no-oauth';
10
+ message: string;
11
+ authServer?: undefined;
12
+ }
13
+ /** It advertises one, but that server will not register a client for us. */
14
+ | {
15
+ ok: false;
16
+ reason: 'no-dcr';
17
+ message: string;
18
+ authServer: string;
19
+ }
20
+ /** Something broke mid-flow. */
21
+ | {
22
+ ok: false;
23
+ reason: 'failed';
24
+ message: string;
25
+ authServer?: undefined;
26
+ };
27
+ export interface McpLoginDeps {
28
+ fetchImpl?: typeof fetch;
29
+ openBrowser?: (url: string) => void | Promise<void>;
30
+ now?: () => number;
31
+ /** The consent URL, as soon as it is built and before the browser opens.
32
+ *
33
+ * Safe to show in full: it carries a client id, a redirect URI, a code
34
+ * CHALLENGE (never the verifier) and a one-time state. Callers surface it so
35
+ * a user whose browser did not open has something to click -- and so a user
36
+ * driving a REMOTE daemon can see the address they must open on THAT
37
+ * machine, since the redirect lands on its loopback and nowhere else. */
38
+ onAuthUrl?: (url: string) => void;
39
+ }
40
+ export declare function loginToMcpServer(stateDir: string, server: McpServer, deps?: McpLoginDeps): Promise<McpLoginOutcome>;
41
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/oauth/login.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAuB1C,qDAAqD;AACrD,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE;AACd,8EAA8E;GAC5E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,SAAS,CAAA;CAAE;AAC5E,4EAA4E;GAC1E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE;AACtE,gCAAgC;GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,SAAS,CAAA;CAAE,CAAC;AAE7E,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;;;;;8EAM0E;IAC1E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,EACjB,IAAI,GAAE,YAAiB,GACtB,OAAO,CAAC,eAAe,CAAC,CAiN1B"}
@@ -0,0 +1,206 @@
1
+ import { writeMcpCredential, readMcpClient, writeMcpClient, clearMcpClient } from '../config.js';
2
+ import { discoverAuthServer, discoverAuthMetadata } from './discovery.js';
3
+ import { registerClient } from './register.js';
4
+ import { makePkce, makeState, authorizationUrl } from './pkce.js';
5
+ import { awaitRedirect, getFreePort, portFree } from './loopback.js';
6
+ import { exchangeCode } from './tokens.js';
7
+ /* IMPORTED LAZILY, AND THAT IS LOAD-BEARING. This module is reached from
8
+ * wirePort.ts, which the DAEMON constructs at startup -- so a module-scope
9
+ * `import ... from '@corenel/tools-node'` puts node:child_process into the
10
+ * daemon's startup graph, for a function only ever called once a human clicks
11
+ * Sign in.
12
+ *
13
+ * Measured, not assumed: adding it at module scope took p2c.e2e from 3/3 to
14
+ * 1/3 on this machine. The daemon is transpiled on the fly, so every module on
15
+ * the boot path costs real milliseconds, and a timing-sensitive e2e is the
16
+ * thing that notices first. */
17
+ const defaultOpenBrowser = async (url) => {
18
+ const { openBrowser } = await import('@corenel/tools-node');
19
+ await openBrowser(url);
20
+ };
21
+ export async function loginToMcpServer(stateDir, server, deps = {}) {
22
+ const fetchImpl = deps.fetchImpl ?? fetch;
23
+ const openBrowserFn = deps.openBrowser ?? defaultOpenBrowser;
24
+ const now = deps.now ?? Date.now;
25
+ /* Tears the redirect listener down on every exit path that is NOT a
26
+ * completed redirect -- see the catch block below, and loopback.ts's
27
+ * `signal` doc. Aborting before `awaitRedirect` is ever called (nothing
28
+ * has listened yet) or after it has already settled (the listener is
29
+ * already closed) is a harmless no-op either way. */
30
+ const redirectAbort = new AbortController();
31
+ /* SIGN-IN ADDRESS, which is not always the call address. For a relayed
32
+ * server, `url` points at the Corenel relay -- a POST-only route whose
33
+ * upstream answers 405 to the GET that discovery probes with, so probing it
34
+ * concludes the server offers no OAuth at all. `authUrl` is the hosted
35
+ * service's own endpoint, whose gate leaves the OAuth paths public precisely
36
+ * so this can happen directly. Every other server has one address and this
37
+ * falls back to it. */
38
+ const signInUrl = server.authUrl ?? server.url;
39
+ try {
40
+ const discovered = await discoverAuthServer(signInUrl, fetchImpl);
41
+ if (!discovered) {
42
+ return {
43
+ ok: false,
44
+ reason: 'no-oauth',
45
+ message: `"${server.name}" does not advertise an OAuth authorization server. If it takes a static token instead, store that as a header.`,
46
+ };
47
+ }
48
+ const asMeta = await discoverAuthMetadata(discovered.authServer, fetchImpl);
49
+ /* THE RESOURCE'S SCOPE LIST WINS over the authorization server's.
50
+ *
51
+ * RFC 9728 s2: a protected-resource document's `scopes_supported` is the
52
+ * list for THAT resource. RFC 8414's is everything the authorization
53
+ * server offers across every resource it fronts. Reading only the latter
54
+ * was wrong in both directions at once -- Trello, Canva, PayPal and Asana
55
+ * advertise scopes only on the resource, so they were sent to consent with
56
+ * NO scope at all, while Dropbox's authorization server lists 39 and we
57
+ * asked for all of them.
58
+ *
59
+ * The empty-list case is already excluded upstream (`scopes` is undefined,
60
+ * never `[]`), so a resource that genuinely advertises none -- monday --
61
+ * still falls through to the authorization server's list and then to no
62
+ * `scope` parameter, which is what it had before. */
63
+ const meta = discovered.scopes
64
+ ? { ...asMeta, scopesSupported: discovered.scopes }
65
+ : asMeta;
66
+ /* The port must be known BEFORE the authorization URL is built:
67
+ * `redirect_uri` has to name the exact address the listener will bind, and
68
+ * the authorization server will refuse a mismatch. */
69
+ /* A PINNED port when the stored client carries one, an ephemeral port
70
+ * otherwise. RFC 8252 s7.3 obliges an authorization server to accept any
71
+ * port here, and most do -- Entra ignores it entirely, which is why one
72
+ * registered `http://localhost` covers every attempt. Slack matches
73
+ * redirect URLs exactly, so an operator who registered one URL needs the
74
+ * daemon to keep landing on that exact port. */
75
+ const cached = await readMcpClient(stateDir, server.name);
76
+ const port = cached?.redirectPort ?? await getFreePort();
77
+ /* A pinned port that something else already holds cannot be quietly swapped
78
+ * for a free one: the whole reason it is pinned is that a server matches the
79
+ * redirect URI exactly, so a different port fails the same way, one step
80
+ * later and less legibly. Say which port and why, rather than letting the
81
+ * listener reject with a bare EADDRINUSE from inside the flow. */
82
+ if (cached?.redirectPort !== undefined && !(await portFree(cached.redirectPort))) {
83
+ return {
84
+ ok: false,
85
+ reason: 'failed',
86
+ message: `the redirect port ${cached.redirectPort} is in use, and "${server.name}" is registered against exactly that port `
87
+ + '-- free it and sign in again',
88
+ };
89
+ }
90
+ /* `localhost`, NOT `127.0.0.1`, and the difference is entirely about what
91
+ * authorization servers will let you register.
92
+ *
93
+ * Entra's portal REFUSES to add an `http://127.0.0.1` reply URL at all --
94
+ * it can only be added by hand-editing the application manifest's
95
+ * `replyUrlsWithType`. `http://localhost` it accepts directly, and it
96
+ * IGNORES THE PORT when matching, so a single registered
97
+ * `http://localhost` covers every ephemeral port this picks. Sending
98
+ * 127.0.0.1 meant every user had to do manifest surgery before a sign-in
99
+ * could work, and a mismatch there bounces the browser back to the account
100
+ * picker with nothing explaining why.
101
+ *
102
+ * The listener still binds loopback and nothing else -- see loopback.ts,
103
+ * which now answers on BOTH 127.0.0.1 and ::1 so it does not matter which
104
+ * one the browser resolves `localhost` to. */
105
+ const redirectUri = `http://localhost:${port}/callback`;
106
+ /* REUSE THE CLIENT WE ALREADY REGISTERED, if any.
107
+ *
108
+ * This used to mint a brand-new OAuth client on EVERY sign-in, so each
109
+ * click on Sign in created another client at the vendor. Servers rate-limit
110
+ * that -- Figma starts answering 403 after a few -- and the refusal arrives
111
+ * as a sign-in failure on a service that works perfectly, with nothing to
112
+ * suggest our own retrying caused it.
113
+ *
114
+ * Safe across the random loopback port picked per attempt: RFC 8252 s7.3
115
+ * requires an authorization server to accept ANY port for a loopback
116
+ * redirect URI. The stored `authServer` guards the one case where reuse
117
+ * would be wrong -- a server that later points somewhere else, where a
118
+ * cached client_id would be presented to something that never issued it. */
119
+ const registered = cached && cached.authServer === discovered.authServer
120
+ ? { clientId: cached.clientId, ...(cached.clientSecret ? { clientSecret: cached.clientSecret } : {}) }
121
+ : await registerClient(meta, redirectUri, fetchImpl);
122
+ if (registered && !cached) {
123
+ await writeMcpClient(stateDir, server.name, {
124
+ clientId: registered.clientId,
125
+ ...(registered.clientSecret ? { clientSecret: registered.clientSecret } : {}),
126
+ authServer: discovered.authServer,
127
+ /* PIN THE PORT WE JUST REGISTERED WITH.
128
+ *
129
+ * RFC 8252 s7.3 obliges an authorization server to accept any port on a
130
+ * loopback redirect, and most do -- but not all, and the ones that do
131
+ * not fail in the worst possible way. Atlassian's `auth.atlassian.com`
132
+ * (Trello's authorization server, distinct from `mcp.atlassian.com`)
133
+ * matches the redirect URI exactly. Registering with one ephemeral port
134
+ * and then authorizing with a fresh one on the next sign-in produced
135
+ * "The app's callback URL is invalid", from the vendor's own page,
136
+ * BEFORE any token exchange -- so nothing came back for the
137
+ * invalid_client recovery below to catch, and the flow simply timed out
138
+ * waiting for a redirect that was never going to arrive.
139
+ *
140
+ * Persisting it costs nothing where the port is ignored, and is the
141
+ * whole difference where it is not. The field already existed for
142
+ * operator-set ports (`corenel mcp set-client --redirect-port`); it was
143
+ * read here and never written by this path. */
144
+ redirectPort: port,
145
+ });
146
+ }
147
+ if (!registered) {
148
+ return {
149
+ ok: false,
150
+ reason: 'no-dcr',
151
+ authServer: discovered.authServer,
152
+ message: `"${server.name}"'s authorization server (${discovered.authServer}) supports OAuth but not dynamic client registration, so no client can be created on demand. Store a static token as a header instead -- a server in this position normally issues one from its own settings.`,
153
+ };
154
+ }
155
+ const pkce = makePkce();
156
+ const state = makeState();
157
+ const authUrl = authorizationUrl(meta, {
158
+ clientId: registered.clientId,
159
+ redirectUri,
160
+ codeChallenge: pkce.challenge,
161
+ state,
162
+ resource: discovered.resource,
163
+ });
164
+ deps.onAuthUrl?.(authUrl);
165
+ const redirectP = awaitRedirect(port, state, { signal: redirectAbort.signal });
166
+ /* A passive handler attached immediately: `openBrowserFn` below (a real
167
+ * browser, or a test's stand-in) can settle -- and reject -- this promise
168
+ * before the `await redirectP` subscribes to it, which Node otherwise
169
+ * flags as an unhandled rejection even though that later await consumes
170
+ * it. */
171
+ redirectP.catch(() => { });
172
+ await openBrowserFn(authUrl);
173
+ const { code } = await redirectP;
174
+ const cred = await exchangeCode(meta, {
175
+ clientId: registered.clientId,
176
+ ...(registered.clientSecret ? { clientSecret: registered.clientSecret } : {}),
177
+ code,
178
+ verifier: pkce.verifier,
179
+ redirectUri,
180
+ resource: discovered.resource,
181
+ authServer: discovered.authServer,
182
+ }, fetchImpl, now);
183
+ await writeMcpCredential(stateDir, server.name, cred);
184
+ return { ok: true };
185
+ }
186
+ catch (e) {
187
+ /* Whatever failed -- the browser never opened, the user closed the tab,
188
+ * the token exchange rejected -- the redirect listener must not be left
189
+ * bound. It is what a pinned `redirectPort` (persisted above) would find
190
+ * "in use" on the very next sign-in, mistaking our own leaked listener
191
+ * for a foreign process and refusing a retry that should just work. */
192
+ redirectAbort.abort();
193
+ const message = e instanceof Error ? e.message : String(e);
194
+ /* A REJECTED CLIENT is the one failure a stored registration can cause, and
195
+ * it would otherwise be permanent: the server has forgotten (or revoked)
196
+ * the client we keep presenting, so every future attempt reproduces this.
197
+ * Dropping it means the next sign-in registers afresh. Not retried inline --
198
+ * the browser consent has already happened by this point and cannot be
199
+ * replayed without the user. */
200
+ if (/invalid_client|unauthorized_client|client.{0,20}not.{0,10}found/i.test(message)) {
201
+ await clearMcpClient(stateDir, server.name).catch(() => { });
202
+ }
203
+ return { ok: false, reason: 'failed', message };
204
+ }
205
+ }
206
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/oauth/login.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ export interface RedirectResult {
2
+ code: string;
3
+ }
4
+ export interface AwaitRedirectOptions {
5
+ timeoutMs?: number;
6
+ /** Test-only hook: called with the address/port the listener actually
7
+ * bound, before any redirect arrives. `awaitRedirect`'s return value
8
+ * never exposes this (a caller has no legitimate need to learn the bind
9
+ * address after the fact), so a test that must assert "bound to
10
+ * 127.0.0.1, not 0.0.0.0" has no way to observe it otherwise. */
11
+ onListening?: (address: string, port: number) => void;
12
+ /** Lets a caller tear the listener down when IT gives up -- the browser
13
+ * never opened, the user closed the tab, anything after `openBrowser`
14
+ * threw. Without this the two servers below stay bound until their own
15
+ * five-minute timeout, and the next sign-in on the same (now-pinned, see
16
+ * login.ts) port finds ITS OWN orphaned listener sitting on it and
17
+ * refuses, mistaking itself for a foreign process.
18
+ *
19
+ * Aborting after the promise has already settled is a no-op -- `finish`
20
+ * is idempotent -- so a caller can always abort on the way out without
21
+ * checking whether the redirect already landed. */
22
+ signal?: AbortSignal;
23
+ }
24
+ /**
25
+ * Listen on 127.0.0.1:`port` for exactly one OAuth redirect, and resolve
26
+ * with its authorization code -- or reject. Either way the listener closes
27
+ * before the returned promise settles.
28
+ *
29
+ * `expectedState` must equal the redirect's `state` query parameter or the
30
+ * request is refused and the promise rejects, with the `code` parameter
31
+ * never read into a variable that could be exchanged. Without this check the
32
+ * listener would accept a code from anyone who can reach the port, which on
33
+ * a shared machine is everyone with a login shell.
34
+ */
35
+ export declare function awaitRedirect(port: number, expectedState: string, opts?: AwaitRedirectOptions): Promise<RedirectResult>;
36
+ /**
37
+ * A free ephemeral port on 127.0.0.1, for a caller to build a `redirect_uri`
38
+ * from before starting `awaitRedirect` on the same port -- the authorization
39
+ * URL needs the real port before the browser opens.
40
+ *
41
+ * There is an inherent, unavoidable race between this probe closing and the
42
+ * listener re-binding the same port: any "find a free port, then use it"
43
+ * strategy on a shared loopback interface has it. Acceptable here because
44
+ * this is a local, single-user, one-shot login flow, not a long-lived
45
+ * service placing a hard dependency on the exact port.
46
+ */
47
+ /**
48
+ * Whether `port` can be bound on 127.0.0.1 right now.
49
+ *
50
+ * Only meaningful for a PINNED port -- one an operator registered with a
51
+ * service, or one a previous registration named. An ephemeral port comes from
52
+ * `getFreePort` and is free by construction. The same inherent race as
53
+ * `getFreePort` applies and is acceptable for the same reason: this answers
54
+ * "is something already sitting on it", not "reserve it for me".
55
+ */
56
+ export declare function portFree(port: number): Promise<boolean>;
57
+ export declare function getFreePort(): Promise<number>;
58
+ //# sourceMappingURL=loopback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../src/oauth/loopback.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAcD,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;sEAIkE;IAClE,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD;;;;;;;;;wDASoD;IACpD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,cAAc,CAAC,CA2FzB;AAED;;;;;;;;;;GAUG;AACH;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQvD;AAED,wBAAgB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAS7C"}