@obov/mext 0.0.0-stage → 0.2.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/dist/build-cli.d.ts +39 -0
- package/dist/build-cli.js +167 -0
- package/dist/build-cli.js.map +1 -0
- package/dist/build.d.ts +65 -0
- package/dist/build.js +252 -0
- package/dist/build.js.map +1 -0
- package/dist/cli/args.d.ts +107 -0
- package/dist/cli/args.js +621 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +13 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/build.d.ts +11 -0
- package/dist/cli/build.js +87 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/dev-lock.d.ts +23 -0
- package/dist/cli/dev-lock.js +68 -0
- package/dist/cli/dev-lock.js.map +1 -0
- package/dist/cli/dev-server.d.ts +20 -0
- package/dist/cli/dev-server.js +75 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/dev.d.ts +21 -0
- package/dist/cli/dev.js +565 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/help.d.ts +31 -0
- package/dist/cli/help.js +238 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/io.d.ts +26 -0
- package/dist/cli/io.js +74 -0
- package/dist/cli/io.js.map +1 -0
- package/dist/cli/login.d.ts +32 -0
- package/dist/cli/login.js +284 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/logout.d.ts +9 -0
- package/dist/cli/logout.js +105 -0
- package/dist/cli/logout.js.map +1 -0
- package/dist/cli/main.d.ts +19 -0
- package/dist/cli/main.js +77 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/mcp.d.ts +16 -0
- package/dist/cli/mcp.js +92 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/output.d.ts +78 -0
- package/dist/cli/output.js +229 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/remote.d.ts +17 -0
- package/dist/cli/remote.js +35 -0
- package/dist/cli/remote.js.map +1 -0
- package/dist/cli/run.d.ts +14 -0
- package/dist/cli/run.js +218 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +37 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/client/cache.d.ts +104 -0
- package/dist/client/cache.js +208 -0
- package/dist/client/cache.js.map +1 -0
- package/dist/client/main.d.ts +31 -0
- package/dist/client/main.js +101 -0
- package/dist/client/main.js.map +1 -0
- package/dist/client/manifest.d.ts +19 -0
- package/dist/client/manifest.js +68 -0
- package/dist/client/manifest.js.map +1 -0
- package/dist/client/runner.d.ts +52 -0
- package/dist/client/runner.js +563 -0
- package/dist/client/runner.js.map +1 -0
- package/dist/command.d.ts +75 -0
- package/dist/command.js +44 -0
- package/dist/command.js.map +1 -0
- package/dist/config.d.ts +111 -0
- package/dist/config.js +197 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.js +12 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.d.ts +97 -0
- package/dist/instrumentation.js +165 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/invoke.d.ts +64 -0
- package/dist/invoke.js +152 -0
- package/dist/invoke.js.map +1 -0
- package/dist/lookup.d.ts +9 -0
- package/dist/lookup.js +27 -0
- package/dist/lookup.js.map +1 -0
- package/dist/manifest.d.ts +68 -0
- package/dist/manifest.js +77 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mcp/constants.d.ts +9 -0
- package/dist/mcp/constants.js +10 -0
- package/dist/mcp/constants.js.map +1 -0
- package/dist/mcp/dispatch.d.ts +66 -0
- package/dist/mcp/dispatch.js +249 -0
- package/dist/mcp/dispatch.js.map +1 -0
- package/dist/mcp/help.d.ts +57 -0
- package/dist/mcp/help.js +136 -0
- package/dist/mcp/help.js.map +1 -0
- package/dist/mcp/http.d.ts +96 -0
- package/dist/mcp/http.js +368 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/install.d.ts +17 -0
- package/dist/mcp/install.js +36 -0
- package/dist/mcp/install.js.map +1 -0
- package/dist/mcp/mrtr.d.ts +82 -0
- package/dist/mcp/mrtr.js +130 -0
- package/dist/mcp/mrtr.js.map +1 -0
- package/dist/mcp/server.d.ts +63 -0
- package/dist/mcp/server.js +210 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +75 -0
- package/dist/mcp/stdio.js +222 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/principal.d.ts +16 -0
- package/dist/principal.js +24 -0
- package/dist/principal.js.map +1 -0
- package/dist/registry.d.ts +90 -0
- package/dist/registry.js +169 -0
- package/dist/registry.js.map +1 -0
- package/dist/remote/browser.d.ts +15 -0
- package/dist/remote/browser.js +47 -0
- package/dist/remote/browser.js.map +1 -0
- package/dist/remote/client.d.ts +113 -0
- package/dist/remote/client.js +312 -0
- package/dist/remote/client.js.map +1 -0
- package/dist/remote/credentials.d.ts +48 -0
- package/dist/remote/credentials.js +196 -0
- package/dist/remote/credentials.js.map +1 -0
- package/dist/remote/index.d.ts +8 -0
- package/dist/remote/index.js +5 -0
- package/dist/remote/index.js.map +1 -0
- package/dist/remote/oauth.d.ts +186 -0
- package/dist/remote/oauth.js +507 -0
- package/dist/remote/oauth.js.map +1 -0
- package/dist/scan.d.ts +34 -0
- package/dist/scan.js +155 -0
- package/dist/scan.js.map +1 -0
- package/dist/start.d.ts +63 -0
- package/dist/start.js +269 -0
- package/dist/start.js.map +1 -0
- package/dist/suggest.d.ts +7 -0
- package/dist/suggest.js +45 -0
- package/dist/suggest.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -4
- package/README.md +0 -3
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { canonicalServerUrl } from "./credentials.js";
|
|
5
|
+
/**
|
|
6
|
+
* MCP authorization (2026-07-28) 클라이언트 쪽: OAuth 2.1 authorization code + PKCE (S256) + loopback redirect.
|
|
7
|
+
* 보호 자원 메타데이터 (RFC 9728: WWW-Authenticate resource_metadata → 경로 삽입 well-known → 루트) →
|
|
8
|
+
* 인가 서버 메타데이터 (RFC 8414 · OIDC Discovery, issuer 일치 검증) → 클라이언트 등록 (사전 등록 client_id ·
|
|
9
|
+
* Client ID Metadata Document URL · RFC 7591 동적 등록) → 인가 요청 (resource = RFC 8707) → iss 검증 (RFC 9207) →
|
|
10
|
+
* 토큰 교환. refresh_token 갱신.
|
|
11
|
+
* headless: redirect URL 붙여넣기 (loopback 과 먼저 온 쪽) · device authorization grant (RFC 8628, 인가 서버 메타데이터에
|
|
12
|
+
* device_authorization_endpoint 가 있을 때만. MCP 사양 밖). logout = token revocation (RFC 7009).
|
|
13
|
+
*/
|
|
14
|
+
export class OAuthFlowError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "OAuthFlowError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/* 보호 자원 메타데이터 (RFC 9728) */
|
|
21
|
+
/** 보호 자원 메타데이터 후보 URL: challenge 의 resource_metadata, 없으면 경로 삽입 well-known → 루트 well-known */
|
|
22
|
+
export function protectedResourceMetadataUrls(serverUrl, challenge) {
|
|
23
|
+
if (challenge?.resourceMetadata)
|
|
24
|
+
return [challenge.resourceMetadata];
|
|
25
|
+
const u = new URL(serverUrl);
|
|
26
|
+
const path = u.pathname.replace(/\/$/, "");
|
|
27
|
+
const urls = [];
|
|
28
|
+
if (path !== "")
|
|
29
|
+
urls.push(`${u.origin}/.well-known/oauth-protected-resource${path}`);
|
|
30
|
+
urls.push(`${u.origin}/.well-known/oauth-protected-resource`);
|
|
31
|
+
return urls;
|
|
32
|
+
}
|
|
33
|
+
export async function discoverProtectedResource(serverUrl, challenge, doFetch = fetch) {
|
|
34
|
+
const canonical = canonicalServerUrl(serverUrl);
|
|
35
|
+
for (const url of protectedResourceMetadataUrls(serverUrl, challenge)) {
|
|
36
|
+
const doc = await getJson(url, doFetch);
|
|
37
|
+
if (!doc)
|
|
38
|
+
continue;
|
|
39
|
+
const servers = doc.authorization_servers;
|
|
40
|
+
if (!Array.isArray(servers) || servers.length === 0 || !servers.every((s) => typeof s === "string")) {
|
|
41
|
+
throw new OAuthFlowError(`${url}: protected resource metadata has no authorization_servers.`);
|
|
42
|
+
}
|
|
43
|
+
if (typeof doc.resource === "string") {
|
|
44
|
+
const resource = canonicalServerUrl(doc.resource);
|
|
45
|
+
if (resource !== canonical && resource !== new URL(canonical).origin) {
|
|
46
|
+
throw new OAuthFlowError(`${url}: resource ${doc.resource} does not match ${canonical}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return doc;
|
|
50
|
+
}
|
|
51
|
+
throw new OAuthFlowError(`${serverUrl} does not publish OAuth protected resource metadata (/.well-known/oauth-protected-resource). ` +
|
|
52
|
+
"Use an API key (MEXT_TOKEN) instead.");
|
|
53
|
+
}
|
|
54
|
+
/* 인가 서버 메타데이터 (RFC 8414 · OIDC Discovery) */
|
|
55
|
+
/** issuer → 메타데이터 후보 URL (MCP authorization-server-discovery 순서) */
|
|
56
|
+
export function authorizationServerMetadataUrls(issuer) {
|
|
57
|
+
const u = new URL(issuer);
|
|
58
|
+
const path = u.pathname.replace(/\/$/, "");
|
|
59
|
+
if (path === "") {
|
|
60
|
+
return [`${u.origin}/.well-known/oauth-authorization-server`, `${u.origin}/.well-known/openid-configuration`];
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
`${u.origin}/.well-known/oauth-authorization-server${path}`,
|
|
64
|
+
`${u.origin}/.well-known/openid-configuration${path}`,
|
|
65
|
+
`${u.origin}${path}/.well-known/openid-configuration`,
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
export async function discoverAuthorizationServer(issuer, doFetch = fetch) {
|
|
69
|
+
for (const url of authorizationServerMetadataUrls(issuer)) {
|
|
70
|
+
const doc = await getJson(url, doFetch);
|
|
71
|
+
if (!doc)
|
|
72
|
+
continue;
|
|
73
|
+
// issuer 는 메타데이터 URL 을 만든 값과 정확히 같아야 한다 (다르면 사용 금지)
|
|
74
|
+
if (doc.issuer !== issuer)
|
|
75
|
+
throw new OAuthFlowError(`${url}: issuer ${String(doc.issuer)} does not match ${issuer}.`);
|
|
76
|
+
if (typeof doc.authorization_endpoint !== "string" || typeof doc.token_endpoint !== "string") {
|
|
77
|
+
throw new OAuthFlowError(`${url}: authorization_endpoint and token_endpoint are required.`);
|
|
78
|
+
}
|
|
79
|
+
const methods = doc.code_challenge_methods_supported;
|
|
80
|
+
if (!Array.isArray(methods) || !methods.includes("S256")) {
|
|
81
|
+
throw new OAuthFlowError(`${issuer} does not advertise PKCE S256 (code_challenge_methods_supported).`);
|
|
82
|
+
}
|
|
83
|
+
return doc;
|
|
84
|
+
}
|
|
85
|
+
throw new OAuthFlowError(`Authorization server metadata not found for ${issuer}.`);
|
|
86
|
+
}
|
|
87
|
+
export const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
88
|
+
/**
|
|
89
|
+
* 등록 순서: 사전 등록 client_id (https URL 이면 Client ID Metadata Document) → 동적 등록 (registration_endpoint).
|
|
90
|
+
* 둘 다 없으면 오류 (--client-id 안내).
|
|
91
|
+
*/
|
|
92
|
+
export async function registerClient(as, options, doFetch = fetch) {
|
|
93
|
+
if (options.clientId !== undefined) {
|
|
94
|
+
if (/^https:\/\//.test(options.clientId) && as.client_id_metadata_document_supported !== true) {
|
|
95
|
+
throw new OAuthFlowError(`${as.issuer} does not support Client ID Metadata Documents (client_id ${options.clientId}).`);
|
|
96
|
+
}
|
|
97
|
+
return { client_id: options.clientId, token_endpoint_auth_method: "none" };
|
|
98
|
+
}
|
|
99
|
+
if (typeof as.registration_endpoint !== "string") {
|
|
100
|
+
throw new OAuthFlowError(`${as.issuer} has no dynamic client registration. Pass a pre-registered client ID with --client-id.`);
|
|
101
|
+
}
|
|
102
|
+
const body = options.redirectUri !== undefined
|
|
103
|
+
? {
|
|
104
|
+
client_name: options.clientName ?? "mext",
|
|
105
|
+
redirect_uris: [options.redirectUri],
|
|
106
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
107
|
+
response_types: ["code"],
|
|
108
|
+
token_endpoint_auth_method: "none",
|
|
109
|
+
application_type: "native",
|
|
110
|
+
}
|
|
111
|
+
: {
|
|
112
|
+
client_name: options.clientName ?? "mext",
|
|
113
|
+
grant_types: [DEVICE_CODE_GRANT, "refresh_token"],
|
|
114
|
+
response_types: [],
|
|
115
|
+
token_endpoint_auth_method: "none",
|
|
116
|
+
application_type: "native",
|
|
117
|
+
};
|
|
118
|
+
if (options.scope)
|
|
119
|
+
body.scope = options.scope;
|
|
120
|
+
const res = await doFetch(as.registration_endpoint, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
});
|
|
125
|
+
const doc = (await res.json().catch(() => undefined));
|
|
126
|
+
if (!res.ok || typeof doc?.client_id !== "string") {
|
|
127
|
+
throw new OAuthFlowError(`Client registration failed (HTTP ${res.status})${describeOAuthError(doc)}.`);
|
|
128
|
+
}
|
|
129
|
+
const client = { client_id: doc.client_id };
|
|
130
|
+
if (typeof doc.client_secret === "string")
|
|
131
|
+
client.client_secret = doc.client_secret;
|
|
132
|
+
if (typeof doc.token_endpoint_auth_method === "string")
|
|
133
|
+
client.token_endpoint_auth_method = doc.token_endpoint_auth_method;
|
|
134
|
+
return client;
|
|
135
|
+
}
|
|
136
|
+
export function createPkce() {
|
|
137
|
+
const verifier = base64url(randomBytes(32));
|
|
138
|
+
return { verifier, challenge: base64url(createHash("sha256").update(verifier).digest()) };
|
|
139
|
+
}
|
|
140
|
+
export const CALLBACK_PATH = "/callback";
|
|
141
|
+
/** 127.0.0.1 의 임의 포트 (port 지정 가능) 에 callback 수신 서버 */
|
|
142
|
+
export async function startLoopback(options = {}) {
|
|
143
|
+
let settle;
|
|
144
|
+
const callback = new Promise((resolve, reject) => (settle = { resolve, reject }));
|
|
145
|
+
callback.catch(() => { });
|
|
146
|
+
let done = false;
|
|
147
|
+
const server = createServer((req, res) => {
|
|
148
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
149
|
+
if (req.method !== "GET" || url.pathname !== CALLBACK_PATH || done) {
|
|
150
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }).end("Not found\n");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
done = true;
|
|
154
|
+
const ok = url.searchParams.has("code") && !url.searchParams.has("error");
|
|
155
|
+
res
|
|
156
|
+
.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8", Connection: "close" })
|
|
157
|
+
.end(ok
|
|
158
|
+
? "<!doctype html><title>mext login</title><p>Login complete. You can close this window.</p>\n"
|
|
159
|
+
: "<!doctype html><title>mext login</title><p>Login failed. Return to the terminal.</p>\n");
|
|
160
|
+
settle.resolve(url.searchParams);
|
|
161
|
+
});
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
server.once("error", reject);
|
|
164
|
+
server.listen(options.port ?? 0, "127.0.0.1", () => {
|
|
165
|
+
server.off("error", reject);
|
|
166
|
+
resolve();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
const port = server.address().port;
|
|
170
|
+
const timer = setTimeout(() => settle.reject(new OAuthFlowError("Timed out waiting for the authorization callback.")), options.timeoutMs ?? 300_000);
|
|
171
|
+
const onAbort = () => settle.reject(options.signal?.reason ?? new Error("aborted"));
|
|
172
|
+
if (options.signal?.aborted)
|
|
173
|
+
onAbort();
|
|
174
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
175
|
+
const close = async () => {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
178
|
+
server.closeAllConnections();
|
|
179
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
180
|
+
};
|
|
181
|
+
return { redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`, port, callback, close };
|
|
182
|
+
}
|
|
183
|
+
export function authorizationRequest(as, client, options) {
|
|
184
|
+
const pkce = createPkce();
|
|
185
|
+
const state = base64url(randomBytes(16));
|
|
186
|
+
const url = new URL(as.authorization_endpoint);
|
|
187
|
+
url.searchParams.set("response_type", "code");
|
|
188
|
+
url.searchParams.set("client_id", client.client_id);
|
|
189
|
+
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
190
|
+
url.searchParams.set("code_challenge", pkce.challenge);
|
|
191
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
192
|
+
url.searchParams.set("state", state);
|
|
193
|
+
url.searchParams.set("resource", options.resource);
|
|
194
|
+
if (options.scope)
|
|
195
|
+
url.searchParams.set("scope", options.scope);
|
|
196
|
+
return { url: url.href, state, pkce };
|
|
197
|
+
}
|
|
198
|
+
/** callback query → code. state · iss (RFC 9207) 검증 후 error 를 본다 (iss 불일치면 error 내용도 쓰지 않음) */
|
|
199
|
+
export function authorizationCode(query, as, expectedState) {
|
|
200
|
+
const iss = query.get("iss");
|
|
201
|
+
if (iss !== null && iss !== as.issuer)
|
|
202
|
+
throw new OAuthFlowError("Authorization response issuer mismatch.");
|
|
203
|
+
if (iss === null && as.authorization_response_iss_parameter_supported === true) {
|
|
204
|
+
throw new OAuthFlowError("Authorization response is missing iss.");
|
|
205
|
+
}
|
|
206
|
+
if (query.get("state") !== expectedState)
|
|
207
|
+
throw new OAuthFlowError("Authorization response state mismatch.");
|
|
208
|
+
const error = query.get("error");
|
|
209
|
+
if (error !== null) {
|
|
210
|
+
const description = query.get("error_description");
|
|
211
|
+
throw new OAuthFlowError(`Authorization failed: ${error}${description ? ` (${description})` : ""}.`);
|
|
212
|
+
}
|
|
213
|
+
const code = query.get("code");
|
|
214
|
+
if (code === null || code === "")
|
|
215
|
+
throw new OAuthFlowError("Authorization response has no code.");
|
|
216
|
+
return code;
|
|
217
|
+
}
|
|
218
|
+
export async function exchangeCode(context, options, doFetch = fetch) {
|
|
219
|
+
return tokenRequest(context, { grant_type: "authorization_code", code: options.code, redirect_uri: options.redirectUri, code_verifier: options.verifier }, undefined, doFetch);
|
|
220
|
+
}
|
|
221
|
+
/** refresh_token 갱신. 새 refresh_token 이 없으면 기존 값 유지 */
|
|
222
|
+
export async function refreshCredentials(credentials, doFetch = fetch) {
|
|
223
|
+
if (!credentials.refresh_token)
|
|
224
|
+
throw new OAuthFlowError("No refresh token.");
|
|
225
|
+
if (credentials.issuer === undefined || credentials.token_endpoint === undefined || credentials.client_id === undefined) {
|
|
226
|
+
throw new OAuthFlowError("Stored credentials have no OAuth client.");
|
|
227
|
+
}
|
|
228
|
+
return tokenRequest({
|
|
229
|
+
issuer: credentials.issuer,
|
|
230
|
+
token_endpoint: credentials.token_endpoint,
|
|
231
|
+
client: storedClient(credentials, credentials.client_id),
|
|
232
|
+
...(credentials.resource !== undefined ? { resource: credentials.resource } : {}),
|
|
233
|
+
}, { grant_type: "refresh_token", refresh_token: credentials.refresh_token }, credentials, doFetch);
|
|
234
|
+
}
|
|
235
|
+
function storedClient(credentials, clientId) {
|
|
236
|
+
const client = { client_id: clientId };
|
|
237
|
+
if (credentials.client_secret !== undefined)
|
|
238
|
+
client.client_secret = credentials.client_secret;
|
|
239
|
+
if (credentials.token_endpoint_auth_method !== undefined)
|
|
240
|
+
client.token_endpoint_auth_method = credentials.token_endpoint_auth_method;
|
|
241
|
+
return client;
|
|
242
|
+
}
|
|
243
|
+
/** 클라이언트 인증 (token · revocation · device authorization 공통): secret 없음 = client_id 본문, basic · post */
|
|
244
|
+
function clientAuth(client, body, headers) {
|
|
245
|
+
const method = client.token_endpoint_auth_method ?? (client.client_secret ? "client_secret_basic" : "none");
|
|
246
|
+
if (client.client_secret && method === "client_secret_basic") {
|
|
247
|
+
const pair = `${encodeURIComponent(client.client_id)}:${encodeURIComponent(client.client_secret)}`;
|
|
248
|
+
headers.Authorization = `Basic ${Buffer.from(pair).toString("base64")}`;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
body.set("client_id", client.client_id);
|
|
252
|
+
if (client.client_secret && method === "client_secret_post")
|
|
253
|
+
body.set("client_secret", client.client_secret);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function formHeaders() {
|
|
257
|
+
return { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" };
|
|
258
|
+
}
|
|
259
|
+
async function tokenRequest(context, grant, previous, doFetch) {
|
|
260
|
+
const res = await postTokenEndpoint(context, grant, doFetch);
|
|
261
|
+
const doc = res.doc;
|
|
262
|
+
if (!res.ok || typeof doc?.access_token !== "string") {
|
|
263
|
+
throw new OAuthFlowError(`Token request failed (HTTP ${res.status})${describeOAuthError(doc)}.`);
|
|
264
|
+
}
|
|
265
|
+
return credentialsFrom(context, doc, previous);
|
|
266
|
+
}
|
|
267
|
+
async function postTokenEndpoint(context, grant, doFetch) {
|
|
268
|
+
const body = new URLSearchParams(grant);
|
|
269
|
+
if (context.resource !== undefined)
|
|
270
|
+
body.set("resource", context.resource);
|
|
271
|
+
const headers = formHeaders();
|
|
272
|
+
clientAuth(context.client, body, headers);
|
|
273
|
+
const res = await doFetch(context.token_endpoint, { method: "POST", headers, body });
|
|
274
|
+
const doc = (await res.json().catch(() => undefined));
|
|
275
|
+
return { ok: res.ok, status: res.status, doc };
|
|
276
|
+
}
|
|
277
|
+
function credentialsFrom(context, doc, previous) {
|
|
278
|
+
const { client } = context;
|
|
279
|
+
const out = {
|
|
280
|
+
access_token: doc.access_token,
|
|
281
|
+
issuer: context.issuer,
|
|
282
|
+
token_endpoint: context.token_endpoint,
|
|
283
|
+
client_id: client.client_id,
|
|
284
|
+
};
|
|
285
|
+
if (context.resource !== undefined)
|
|
286
|
+
out.resource = context.resource;
|
|
287
|
+
if (typeof doc.token_type === "string")
|
|
288
|
+
out.token_type = doc.token_type;
|
|
289
|
+
const refresh = typeof doc.refresh_token === "string" ? doc.refresh_token : previous?.refresh_token;
|
|
290
|
+
if (refresh !== undefined)
|
|
291
|
+
out.refresh_token = refresh;
|
|
292
|
+
if (typeof doc.expires_in === "number")
|
|
293
|
+
out.expires_at = Date.now() + doc.expires_in * 1000;
|
|
294
|
+
const scope = typeof doc.scope === "string" ? doc.scope : previous?.scope;
|
|
295
|
+
if (scope !== undefined)
|
|
296
|
+
out.scope = scope;
|
|
297
|
+
if (client.client_secret !== undefined)
|
|
298
|
+
out.client_secret = client.client_secret;
|
|
299
|
+
if (client.token_endpoint_auth_method !== undefined)
|
|
300
|
+
out.token_endpoint_auth_method = client.token_endpoint_auth_method;
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
/** 만료 30초 전부터 만료로 본다 */
|
|
304
|
+
export function isExpired(credentials, now = Date.now()) {
|
|
305
|
+
return credentials.expires_at !== undefined && credentials.expires_at - 30_000 <= now;
|
|
306
|
+
}
|
|
307
|
+
export async function discoverLoginTarget(options) {
|
|
308
|
+
const doFetch = options.fetch ?? fetch;
|
|
309
|
+
const prm = await discoverProtectedResource(options.serverUrl, options.challenge, doFetch);
|
|
310
|
+
// 여러 인가 서버 중 첫 번째 (RFC 9728 7.6: 선택은 클라이언트 몫)
|
|
311
|
+
const as = await discoverAuthorizationServer(prm.authorization_servers[0], doFetch);
|
|
312
|
+
const resource = typeof prm.resource === "string" ? prm.resource : canonicalServerUrl(options.serverUrl);
|
|
313
|
+
const scope = options.scope ??
|
|
314
|
+
options.challenge?.scope ??
|
|
315
|
+
(Array.isArray(prm.scopes_supported) && prm.scopes_supported.length > 0 ? prm.scopes_supported.join(" ") : undefined);
|
|
316
|
+
return { serverUrl: options.serverUrl, as, resource, ...(scope !== undefined ? { scope } : {}) };
|
|
317
|
+
}
|
|
318
|
+
/** authorization code + PKCE. redirect = loopback callback 또는 붙여넣은 URL. 둘 다 state · iss 검증 */
|
|
319
|
+
export async function authorizationCodeLogin(target, options) {
|
|
320
|
+
const doFetch = options.fetch ?? fetch;
|
|
321
|
+
const { as, resource, scope } = target;
|
|
322
|
+
const loopback = await startLoopback({
|
|
323
|
+
...(options.port !== undefined ? { port: options.port } : {}),
|
|
324
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
325
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
326
|
+
});
|
|
327
|
+
const pasteAbort = new AbortController();
|
|
328
|
+
try {
|
|
329
|
+
const client = await registerClient(as, {
|
|
330
|
+
redirectUri: loopback.redirectUri,
|
|
331
|
+
...(options.clientId !== undefined ? { clientId: options.clientId } : {}),
|
|
332
|
+
...(options.clientName !== undefined ? { clientName: options.clientName } : {}),
|
|
333
|
+
...(scope !== undefined ? { scope } : {}),
|
|
334
|
+
}, doFetch);
|
|
335
|
+
const request = authorizationRequest(as, client, {
|
|
336
|
+
redirectUri: loopback.redirectUri,
|
|
337
|
+
resource,
|
|
338
|
+
...(scope !== undefined ? { scope } : {}),
|
|
339
|
+
});
|
|
340
|
+
options.onAuthorizationUrl(request.url, { redirectUri: loopback.redirectUri, port: loopback.port });
|
|
341
|
+
const never = new Promise(() => { });
|
|
342
|
+
const pasted = options.readRedirect
|
|
343
|
+
? options.readRedirect(pasteAbort.signal).then((q) => q ?? never)
|
|
344
|
+
: never;
|
|
345
|
+
const query = await Promise.race([loopback.callback, pasted]);
|
|
346
|
+
const code = authorizationCode(query, as, request.state);
|
|
347
|
+
return await exchangeCode({ issuer: as.issuer, token_endpoint: as.token_endpoint, client, resource }, { code, redirectUri: loopback.redirectUri, verifier: request.pkce.verifier }, doFetch);
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
pasteAbort.abort();
|
|
351
|
+
await loopback.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** 붙여넣은 redirect URL (또는 query 문자열) → query. code · error 가 없으면 undefined */
|
|
355
|
+
export function parseRedirectUrl(text) {
|
|
356
|
+
const trimmed = text.trim();
|
|
357
|
+
if (trimmed === "")
|
|
358
|
+
return undefined;
|
|
359
|
+
let query;
|
|
360
|
+
try {
|
|
361
|
+
query = new URL(trimmed).searchParams;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
if (!trimmed.includes("="))
|
|
365
|
+
return undefined;
|
|
366
|
+
query = new URLSearchParams(trimmed.replace(/^[^?]*\?/, ""));
|
|
367
|
+
}
|
|
368
|
+
return query.has("code") || query.has("error") ? query : undefined;
|
|
369
|
+
}
|
|
370
|
+
export async function deviceLogin(target, options) {
|
|
371
|
+
const doFetch = options.fetch ?? fetch;
|
|
372
|
+
const sleep = options.sleep ?? ((ms, signal) => delay(ms, undefined, signal ? { signal } : {}));
|
|
373
|
+
const { as, resource, scope } = target;
|
|
374
|
+
const endpoint = as.device_authorization_endpoint;
|
|
375
|
+
if (typeof endpoint !== "string") {
|
|
376
|
+
throw new OAuthFlowError(`${as.issuer} does not support the device authorization grant (no device_authorization_endpoint in its metadata).`);
|
|
377
|
+
}
|
|
378
|
+
const client = await registerClient(as, {
|
|
379
|
+
...(options.clientId !== undefined ? { clientId: options.clientId } : {}),
|
|
380
|
+
...(options.clientName !== undefined ? { clientName: options.clientName } : {}),
|
|
381
|
+
...(scope !== undefined ? { scope } : {}),
|
|
382
|
+
}, doFetch);
|
|
383
|
+
const body = new URLSearchParams({ resource });
|
|
384
|
+
if (scope !== undefined)
|
|
385
|
+
body.set("scope", scope);
|
|
386
|
+
const headers = formHeaders();
|
|
387
|
+
clientAuth(client, body, headers);
|
|
388
|
+
const res = await doFetch(endpoint, { method: "POST", headers, body });
|
|
389
|
+
const doc = (await res.json().catch(() => undefined));
|
|
390
|
+
if (!res.ok ||
|
|
391
|
+
typeof doc?.device_code !== "string" ||
|
|
392
|
+
typeof doc.user_code !== "string" ||
|
|
393
|
+
typeof doc.verification_uri !== "string" ||
|
|
394
|
+
typeof doc.expires_in !== "number") {
|
|
395
|
+
throw new OAuthFlowError(`Device authorization request failed (HTTP ${res.status})${describeOAuthError(doc)}.`);
|
|
396
|
+
}
|
|
397
|
+
const device = {
|
|
398
|
+
verification_uri: doc.verification_uri,
|
|
399
|
+
user_code: doc.user_code,
|
|
400
|
+
expires_in: doc.expires_in,
|
|
401
|
+
interval: typeof doc.interval === "number" && doc.interval >= 0 ? doc.interval : 5,
|
|
402
|
+
};
|
|
403
|
+
if (typeof doc.verification_uri_complete === "string")
|
|
404
|
+
device.verification_uri_complete = doc.verification_uri_complete;
|
|
405
|
+
const deviceCode = doc.device_code;
|
|
406
|
+
options.onDeviceCode(device);
|
|
407
|
+
const context = { issuer: as.issuer, token_endpoint: as.token_endpoint, client, resource };
|
|
408
|
+
const deadline = Date.now() + device.expires_in * 1000;
|
|
409
|
+
let interval = device.interval * 1000;
|
|
410
|
+
const expired = () => new OAuthFlowError("The device code expired before authorization. Run login again.");
|
|
411
|
+
for (;;) {
|
|
412
|
+
if (Date.now() >= deadline)
|
|
413
|
+
throw expired();
|
|
414
|
+
await sleep(interval, options.signal);
|
|
415
|
+
const token = await postTokenEndpoint(context, { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode }, doFetch);
|
|
416
|
+
if (token.ok && typeof token.doc?.access_token === "string") {
|
|
417
|
+
return credentialsFrom(context, token.doc, undefined);
|
|
418
|
+
}
|
|
419
|
+
const error = token.doc?.error;
|
|
420
|
+
if (error === "authorization_pending")
|
|
421
|
+
continue;
|
|
422
|
+
if (error === "slow_down") {
|
|
423
|
+
interval += 5000; // RFC 8628 3.5
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (error === "expired_token")
|
|
427
|
+
throw expired();
|
|
428
|
+
if (error === "access_denied")
|
|
429
|
+
throw new OAuthFlowError("Authorization was denied.");
|
|
430
|
+
throw new OAuthFlowError(`Token request failed (HTTP ${token.status})${describeOAuthError(token.doc)}.`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** 저장된 OAuth 토큰 폐기. refresh token 먼저 (RFC 7009 2.1: 연결된 access token 도 무효화될 수 있음), 그다음 access token */
|
|
434
|
+
export async function revokeCredentials(credentials, options = {}) {
|
|
435
|
+
const doFetch = options.fetch ?? fetch;
|
|
436
|
+
const result = { supported: false, revoked: [], errors: [] };
|
|
437
|
+
if (credentials.issuer === undefined || credentials.client_id === undefined)
|
|
438
|
+
return result;
|
|
439
|
+
let as;
|
|
440
|
+
try {
|
|
441
|
+
as = await discoverAuthorizationServer(credentials.issuer, doFetch);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
const endpoint = as.revocation_endpoint;
|
|
448
|
+
if (typeof endpoint !== "string")
|
|
449
|
+
return result;
|
|
450
|
+
result.supported = true;
|
|
451
|
+
const client = storedClient(credentials, credentials.client_id);
|
|
452
|
+
const tokens = [
|
|
453
|
+
["refresh_token", credentials.refresh_token],
|
|
454
|
+
["access_token", credentials.access_token],
|
|
455
|
+
];
|
|
456
|
+
for (const [hint, token] of tokens) {
|
|
457
|
+
if (token === undefined || token === "")
|
|
458
|
+
continue;
|
|
459
|
+
const body = new URLSearchParams({ token, token_type_hint: hint });
|
|
460
|
+
const headers = formHeaders();
|
|
461
|
+
clientAuth(client, body, headers);
|
|
462
|
+
try {
|
|
463
|
+
const res = await doFetch(endpoint, { method: "POST", headers, body });
|
|
464
|
+
if (res.ok) {
|
|
465
|
+
await res.body?.cancel().catch(() => { });
|
|
466
|
+
result.revoked.push(hint);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const doc = (await res.json().catch(() => undefined));
|
|
470
|
+
result.errors.push(`${hint} revocation failed (HTTP ${res.status})${describeOAuthError(doc)}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
result.errors.push(`${hint} revocation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
export async function login(options) {
|
|
480
|
+
const target = await discoverLoginTarget(options);
|
|
481
|
+
return authorizationCodeLogin(target, options);
|
|
482
|
+
}
|
|
483
|
+
/* 공통 */
|
|
484
|
+
async function getJson(url, doFetch) {
|
|
485
|
+
let res;
|
|
486
|
+
try {
|
|
487
|
+
res = await doFetch(url, { headers: { Accept: "application/json" } });
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
throw new OAuthFlowError(`Cannot reach ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
491
|
+
}
|
|
492
|
+
if (!res.ok) {
|
|
493
|
+
await res.body?.cancel().catch(() => { });
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
const doc = (await res.json().catch(() => undefined));
|
|
497
|
+
return typeof doc === "object" && doc !== null && !Array.isArray(doc) ? doc : undefined;
|
|
498
|
+
}
|
|
499
|
+
function describeOAuthError(doc) {
|
|
500
|
+
if (typeof doc?.error !== "string")
|
|
501
|
+
return "";
|
|
502
|
+
return `: ${doc.error}${typeof doc.error_description === "string" ? ` (${doc.error_description})` : ""}`;
|
|
503
|
+
}
|
|
504
|
+
function base64url(buf) {
|
|
505
|
+
return buf.toString("base64url");
|
|
506
|
+
}
|
|
507
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/remote/oauth.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from \"node:crypto\"\nimport { createServer, type Server } from \"node:http\"\nimport type { AddressInfo } from \"node:net\"\nimport { setTimeout as delay } from \"node:timers/promises\"\nimport type { BearerChallenge } from \"./client.ts\"\nimport { canonicalServerUrl, type StoredCredentials } from \"./credentials.ts\"\n\n/**\n * MCP authorization (2026-07-28) 클라이언트 쪽: OAuth 2.1 authorization code + PKCE (S256) + loopback redirect.\n * 보호 자원 메타데이터 (RFC 9728: WWW-Authenticate resource_metadata → 경로 삽입 well-known → 루트) →\n * 인가 서버 메타데이터 (RFC 8414 · OIDC Discovery, issuer 일치 검증) → 클라이언트 등록 (사전 등록 client_id ·\n * Client ID Metadata Document URL · RFC 7591 동적 등록) → 인가 요청 (resource = RFC 8707) → iss 검증 (RFC 9207) →\n * 토큰 교환. refresh_token 갱신.\n * headless: redirect URL 붙여넣기 (loopback 과 먼저 온 쪽) · device authorization grant (RFC 8628, 인가 서버 메타데이터에\n * device_authorization_endpoint 가 있을 때만. MCP 사양 밖). logout = token revocation (RFC 7009).\n */\n\nexport class OAuthFlowError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"OAuthFlowError\"\n }\n}\n\nexport interface ProtectedResourceMetadata {\n resource?: string\n authorization_servers: string[]\n scopes_supported?: string[]\n [key: string]: unknown\n}\n\nexport interface AuthorizationServerMetadata {\n issuer: string\n authorization_endpoint: string\n token_endpoint: string\n registration_endpoint?: string\n code_challenge_methods_supported?: string[]\n scopes_supported?: string[]\n authorization_response_iss_parameter_supported?: boolean\n client_id_metadata_document_supported?: boolean\n /** RFC 8628 */\n device_authorization_endpoint?: string\n /** RFC 7009 */\n revocation_endpoint?: string\n [key: string]: unknown\n}\n\ntype Fetch = typeof fetch\n\n/* 보호 자원 메타데이터 (RFC 9728) */\n\n/** 보호 자원 메타데이터 후보 URL: challenge 의 resource_metadata, 없으면 경로 삽입 well-known → 루트 well-known */\nexport function protectedResourceMetadataUrls(serverUrl: string, challenge?: BearerChallenge): string[] {\n if (challenge?.resourceMetadata) return [challenge.resourceMetadata]\n const u = new URL(serverUrl)\n const path = u.pathname.replace(/\\/$/, \"\")\n const urls: string[] = []\n if (path !== \"\") urls.push(`${u.origin}/.well-known/oauth-protected-resource${path}`)\n urls.push(`${u.origin}/.well-known/oauth-protected-resource`)\n return urls\n}\n\nexport async function discoverProtectedResource(\n serverUrl: string,\n challenge: BearerChallenge | undefined,\n doFetch: Fetch = fetch,\n): Promise<ProtectedResourceMetadata> {\n const canonical = canonicalServerUrl(serverUrl)\n for (const url of protectedResourceMetadataUrls(serverUrl, challenge)) {\n const doc = await getJson(url, doFetch)\n if (!doc) continue\n const servers = doc.authorization_servers\n if (!Array.isArray(servers) || servers.length === 0 || !servers.every((s) => typeof s === \"string\")) {\n throw new OAuthFlowError(`${url}: protected resource metadata has no authorization_servers.`)\n }\n if (typeof doc.resource === \"string\") {\n const resource = canonicalServerUrl(doc.resource)\n if (resource !== canonical && resource !== new URL(canonical).origin) {\n throw new OAuthFlowError(`${url}: resource ${doc.resource} does not match ${canonical}.`)\n }\n }\n return doc as ProtectedResourceMetadata\n }\n throw new OAuthFlowError(\n `${serverUrl} does not publish OAuth protected resource metadata (/.well-known/oauth-protected-resource). ` +\n \"Use an API key (MEXT_TOKEN) instead.\",\n )\n}\n\n/* 인가 서버 메타데이터 (RFC 8414 · OIDC Discovery) */\n\n/** issuer → 메타데이터 후보 URL (MCP authorization-server-discovery 순서) */\nexport function authorizationServerMetadataUrls(issuer: string): string[] {\n const u = new URL(issuer)\n const path = u.pathname.replace(/\\/$/, \"\")\n if (path === \"\") {\n return [`${u.origin}/.well-known/oauth-authorization-server`, `${u.origin}/.well-known/openid-configuration`]\n }\n return [\n `${u.origin}/.well-known/oauth-authorization-server${path}`,\n `${u.origin}/.well-known/openid-configuration${path}`,\n `${u.origin}${path}/.well-known/openid-configuration`,\n ]\n}\n\nexport async function discoverAuthorizationServer(issuer: string, doFetch: Fetch = fetch): Promise<AuthorizationServerMetadata> {\n for (const url of authorizationServerMetadataUrls(issuer)) {\n const doc = await getJson(url, doFetch)\n if (!doc) continue\n // issuer 는 메타데이터 URL 을 만든 값과 정확히 같아야 한다 (다르면 사용 금지)\n if (doc.issuer !== issuer) throw new OAuthFlowError(`${url}: issuer ${String(doc.issuer)} does not match ${issuer}.`)\n if (typeof doc.authorization_endpoint !== \"string\" || typeof doc.token_endpoint !== \"string\") {\n throw new OAuthFlowError(`${url}: authorization_endpoint and token_endpoint are required.`)\n }\n const methods = doc.code_challenge_methods_supported\n if (!Array.isArray(methods) || !methods.includes(\"S256\")) {\n throw new OAuthFlowError(`${issuer} does not advertise PKCE S256 (code_challenge_methods_supported).`)\n }\n return doc as AuthorizationServerMetadata\n }\n throw new OAuthFlowError(`Authorization server metadata not found for ${issuer}.`)\n}\n\n/* 클라이언트 등록 */\n\nexport interface OAuthClient {\n client_id: string\n client_secret?: string\n token_endpoint_auth_method?: string\n}\n\nexport interface RegisterOptions {\n /** 사전 등록 client_id 또는 Client ID Metadata Document URL (https) */\n clientId?: string\n clientName?: string\n /** 없으면 redirect 없는 등록 (device flow) */\n redirectUri?: string\n scope?: string\n}\n\nexport const DEVICE_CODE_GRANT = \"urn:ietf:params:oauth:grant-type:device_code\"\n\n/**\n * 등록 순서: 사전 등록 client_id (https URL 이면 Client ID Metadata Document) → 동적 등록 (registration_endpoint).\n * 둘 다 없으면 오류 (--client-id 안내).\n */\nexport async function registerClient(\n as: AuthorizationServerMetadata,\n options: RegisterOptions,\n doFetch: Fetch = fetch,\n): Promise<OAuthClient> {\n if (options.clientId !== undefined) {\n if (/^https:\\/\\//.test(options.clientId) && as.client_id_metadata_document_supported !== true) {\n throw new OAuthFlowError(`${as.issuer} does not support Client ID Metadata Documents (client_id ${options.clientId}).`)\n }\n return { client_id: options.clientId, token_endpoint_auth_method: \"none\" }\n }\n if (typeof as.registration_endpoint !== \"string\") {\n throw new OAuthFlowError(\n `${as.issuer} has no dynamic client registration. Pass a pre-registered client ID with --client-id.`,\n )\n }\n const body: Record<string, unknown> =\n options.redirectUri !== undefined\n ? {\n client_name: options.clientName ?? \"mext\",\n redirect_uris: [options.redirectUri],\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n application_type: \"native\",\n }\n : {\n client_name: options.clientName ?? \"mext\",\n grant_types: [DEVICE_CODE_GRANT, \"refresh_token\"],\n response_types: [],\n token_endpoint_auth_method: \"none\",\n application_type: \"native\",\n }\n if (options.scope) body.scope = options.scope\n const res = await doFetch(as.registration_endpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\" },\n body: JSON.stringify(body),\n })\n const doc = (await res.json().catch(() => undefined)) as Record<string, unknown> | undefined\n if (!res.ok || typeof doc?.client_id !== \"string\") {\n throw new OAuthFlowError(`Client registration failed (HTTP ${res.status})${describeOAuthError(doc)}.`)\n }\n const client: OAuthClient = { client_id: doc.client_id }\n if (typeof doc.client_secret === \"string\") client.client_secret = doc.client_secret\n if (typeof doc.token_endpoint_auth_method === \"string\") client.token_endpoint_auth_method = doc.token_endpoint_auth_method\n return client\n}\n\n/* PKCE */\n\nexport interface Pkce {\n verifier: string\n challenge: string\n}\n\nexport function createPkce(): Pkce {\n const verifier = base64url(randomBytes(32))\n return { verifier, challenge: base64url(createHash(\"sha256\").update(verifier).digest()) }\n}\n\n/* loopback redirect */\n\nexport interface Loopback {\n redirectUri: string\n port: number\n /** 첫 callback 요청의 query. signal abort · 시간 초과 = reject */\n callback: Promise<URLSearchParams>\n close(): Promise<void>\n}\n\nexport const CALLBACK_PATH = \"/callback\"\n\n/** 127.0.0.1 의 임의 포트 (port 지정 가능) 에 callback 수신 서버 */\nexport async function startLoopback(options: { port?: number; signal?: AbortSignal; timeoutMs?: number } = {}): Promise<Loopback> {\n let settle!: { resolve: (q: URLSearchParams) => void; reject: (e: unknown) => void }\n const callback = new Promise<URLSearchParams>((resolve, reject) => (settle = { resolve, reject }))\n callback.catch(() => {})\n let done = false\n const server: Server = createServer((req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\")\n if (req.method !== \"GET\" || url.pathname !== CALLBACK_PATH || done) {\n res.writeHead(404, { \"Content-Type\": \"text/plain; charset=utf-8\" }).end(\"Not found\\n\")\n return\n }\n done = true\n const ok = url.searchParams.has(\"code\") && !url.searchParams.has(\"error\")\n res\n .writeHead(ok ? 200 : 400, { \"Content-Type\": \"text/html; charset=utf-8\", Connection: \"close\" })\n .end(\n ok\n ? \"<!doctype html><title>mext login</title><p>Login complete. You can close this window.</p>\\n\"\n : \"<!doctype html><title>mext login</title><p>Login failed. Return to the terminal.</p>\\n\",\n )\n settle.resolve(url.searchParams)\n })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, \"127.0.0.1\", () => {\n server.off(\"error\", reject)\n resolve()\n })\n })\n const port = (server.address() as AddressInfo).port\n const timer = setTimeout(\n () => settle.reject(new OAuthFlowError(\"Timed out waiting for the authorization callback.\")),\n options.timeoutMs ?? 300_000,\n )\n const onAbort = () => settle.reject(options.signal?.reason ?? new Error(\"aborted\"))\n if (options.signal?.aborted) onAbort()\n options.signal?.addEventListener(\"abort\", onAbort, { once: true })\n const close = async () => {\n clearTimeout(timer)\n options.signal?.removeEventListener(\"abort\", onAbort)\n server.closeAllConnections()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n }\n return { redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`, port, callback, close }\n}\n\n/* 인가 요청 · 응답 검증 · 토큰 */\n\nexport interface AuthorizationRequest {\n url: string\n state: string\n pkce: Pkce\n}\n\nexport function authorizationRequest(\n as: AuthorizationServerMetadata,\n client: OAuthClient,\n options: { redirectUri: string; resource: string; scope?: string },\n): AuthorizationRequest {\n const pkce = createPkce()\n const state = base64url(randomBytes(16))\n const url = new URL(as.authorization_endpoint)\n url.searchParams.set(\"response_type\", \"code\")\n url.searchParams.set(\"client_id\", client.client_id)\n url.searchParams.set(\"redirect_uri\", options.redirectUri)\n url.searchParams.set(\"code_challenge\", pkce.challenge)\n url.searchParams.set(\"code_challenge_method\", \"S256\")\n url.searchParams.set(\"state\", state)\n url.searchParams.set(\"resource\", options.resource)\n if (options.scope) url.searchParams.set(\"scope\", options.scope)\n return { url: url.href, state, pkce }\n}\n\n/** callback query → code. state · iss (RFC 9207) 검증 후 error 를 본다 (iss 불일치면 error 내용도 쓰지 않음) */\nexport function authorizationCode(\n query: URLSearchParams,\n as: AuthorizationServerMetadata,\n expectedState: string,\n): string {\n const iss = query.get(\"iss\")\n if (iss !== null && iss !== as.issuer) throw new OAuthFlowError(\"Authorization response issuer mismatch.\")\n if (iss === null && as.authorization_response_iss_parameter_supported === true) {\n throw new OAuthFlowError(\"Authorization response is missing iss.\")\n }\n if (query.get(\"state\") !== expectedState) throw new OAuthFlowError(\"Authorization response state mismatch.\")\n const error = query.get(\"error\")\n if (error !== null) {\n const description = query.get(\"error_description\")\n throw new OAuthFlowError(`Authorization failed: ${error}${description ? ` (${description})` : \"\"}.`)\n }\n const code = query.get(\"code\")\n if (code === null || code === \"\") throw new OAuthFlowError(\"Authorization response has no code.\")\n return code\n}\n\ninterface TokenContext {\n issuer: string\n token_endpoint: string\n client: OAuthClient\n resource?: string\n}\n\nexport async function exchangeCode(\n context: TokenContext,\n options: { code: string; redirectUri: string; verifier: string },\n doFetch: Fetch = fetch,\n): Promise<StoredCredentials> {\n return tokenRequest(\n context,\n { grant_type: \"authorization_code\", code: options.code, redirect_uri: options.redirectUri, code_verifier: options.verifier },\n undefined,\n doFetch,\n )\n}\n\n/** refresh_token 갱신. 새 refresh_token 이 없으면 기존 값 유지 */\nexport async function refreshCredentials(credentials: StoredCredentials, doFetch: Fetch = fetch): Promise<StoredCredentials> {\n if (!credentials.refresh_token) throw new OAuthFlowError(\"No refresh token.\")\n if (credentials.issuer === undefined || credentials.token_endpoint === undefined || credentials.client_id === undefined) {\n throw new OAuthFlowError(\"Stored credentials have no OAuth client.\")\n }\n return tokenRequest(\n {\n issuer: credentials.issuer,\n token_endpoint: credentials.token_endpoint,\n client: storedClient(credentials, credentials.client_id),\n ...(credentials.resource !== undefined ? { resource: credentials.resource } : {}),\n },\n { grant_type: \"refresh_token\", refresh_token: credentials.refresh_token },\n credentials,\n doFetch,\n )\n}\n\nfunction storedClient(credentials: StoredCredentials, clientId: string): OAuthClient {\n const client: OAuthClient = { client_id: clientId }\n if (credentials.client_secret !== undefined) client.client_secret = credentials.client_secret\n if (credentials.token_endpoint_auth_method !== undefined) client.token_endpoint_auth_method = credentials.token_endpoint_auth_method\n return client\n}\n\n/** 클라이언트 인증 (token · revocation · device authorization 공통): secret 없음 = client_id 본문, basic · post */\nfunction clientAuth(client: OAuthClient, body: URLSearchParams, headers: Record<string, string>): void {\n const method = client.token_endpoint_auth_method ?? (client.client_secret ? \"client_secret_basic\" : \"none\")\n if (client.client_secret && method === \"client_secret_basic\") {\n const pair = `${encodeURIComponent(client.client_id)}:${encodeURIComponent(client.client_secret)}`\n headers.Authorization = `Basic ${Buffer.from(pair).toString(\"base64\")}`\n } else {\n body.set(\"client_id\", client.client_id)\n if (client.client_secret && method === \"client_secret_post\") body.set(\"client_secret\", client.client_secret)\n }\n}\n\nfunction formHeaders(): Record<string, string> {\n return { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" }\n}\n\nasync function tokenRequest(\n context: TokenContext,\n grant: Record<string, string>,\n previous: StoredCredentials | undefined,\n doFetch: Fetch,\n): Promise<StoredCredentials> {\n const res = await postTokenEndpoint(context, grant, doFetch)\n const doc = res.doc\n if (!res.ok || typeof doc?.access_token !== \"string\") {\n throw new OAuthFlowError(`Token request failed (HTTP ${res.status})${describeOAuthError(doc)}.`)\n }\n return credentialsFrom(context, doc as Record<string, unknown> & { access_token: string }, previous)\n}\n\nasync function postTokenEndpoint(\n context: TokenContext,\n grant: Record<string, string>,\n doFetch: Fetch,\n): Promise<{ ok: boolean; status: number; doc: Record<string, unknown> | undefined }> {\n const body = new URLSearchParams(grant)\n if (context.resource !== undefined) body.set(\"resource\", context.resource)\n const headers = formHeaders()\n clientAuth(context.client, body, headers)\n const res = await doFetch(context.token_endpoint, { method: \"POST\", headers, body })\n const doc = (await res.json().catch(() => undefined)) as Record<string, unknown> | undefined\n return { ok: res.ok, status: res.status, doc }\n}\n\nfunction credentialsFrom(\n context: TokenContext,\n doc: Record<string, unknown> & { access_token: string },\n previous: StoredCredentials | undefined,\n): StoredCredentials {\n const { client } = context\n const out: StoredCredentials = {\n access_token: doc.access_token,\n issuer: context.issuer,\n token_endpoint: context.token_endpoint,\n client_id: client.client_id,\n }\n if (context.resource !== undefined) out.resource = context.resource\n if (typeof doc.token_type === \"string\") out.token_type = doc.token_type\n const refresh = typeof doc.refresh_token === \"string\" ? doc.refresh_token : previous?.refresh_token\n if (refresh !== undefined) out.refresh_token = refresh\n if (typeof doc.expires_in === \"number\") out.expires_at = Date.now() + doc.expires_in * 1000\n const scope = typeof doc.scope === \"string\" ? doc.scope : previous?.scope\n if (scope !== undefined) out.scope = scope\n if (client.client_secret !== undefined) out.client_secret = client.client_secret\n if (client.token_endpoint_auth_method !== undefined) out.token_endpoint_auth_method = client.token_endpoint_auth_method\n return out\n}\n\n/** 만료 30초 전부터 만료로 본다 */\nexport function isExpired(credentials: StoredCredentials, now = Date.now()): boolean {\n return credentials.expires_at !== undefined && credentials.expires_at - 30_000 <= now\n}\n\n/* 전체 흐름 */\n\n/** login 대상: 보호 자원 메타데이터 → 인가 서버 메타데이터 · resource · scope */\nexport interface LoginTarget {\n serverUrl: string\n as: AuthorizationServerMetadata\n /** RFC 8707 resource */\n resource: string\n scope?: string\n}\n\nexport interface DiscoverLoginOptions {\n serverUrl: string\n /** 서버 401 의 WWW-Authenticate (없으면 well-known 탐색) */\n challenge?: BearerChallenge\n /** 기본: challenge 의 scope → 보호 자원 메타데이터 scopes_supported → 생략 */\n scope?: string\n fetch?: Fetch\n}\n\nexport async function discoverLoginTarget(options: DiscoverLoginOptions): Promise<LoginTarget> {\n const doFetch = options.fetch ?? fetch\n const prm = await discoverProtectedResource(options.serverUrl, options.challenge, doFetch)\n // 여러 인가 서버 중 첫 번째 (RFC 9728 7.6: 선택은 클라이언트 몫)\n const as = await discoverAuthorizationServer(prm.authorization_servers[0]!, doFetch)\n const resource = typeof prm.resource === \"string\" ? prm.resource : canonicalServerUrl(options.serverUrl)\n const scope =\n options.scope ??\n options.challenge?.scope ??\n (Array.isArray(prm.scopes_supported) && prm.scopes_supported.length > 0 ? prm.scopes_supported.join(\" \") : undefined)\n return { serverUrl: options.serverUrl, as, resource, ...(scope !== undefined ? { scope } : {}) }\n}\n\nexport interface AuthorizationCodeLoginOptions {\n clientId?: string\n /** 동적 등록 client_name. 기본 mext */\n clientName?: string\n /** loopback 포트. 기본 임의 */\n port?: number\n signal?: AbortSignal\n timeoutMs?: number\n fetch?: Fetch\n /** 인가 URL 준비됨 (브라우저 열기 · 안내 출력) */\n onAuthorizationUrl(url: string, loopback: { redirectUri: string; port: number }): void\n /**\n * 붙여넣은 redirect URL 의 query (stdin). loopback callback 과 먼저 온 쪽을 쓰고, 다른 쪽은 signal 로 중단.\n * undefined = 입력 끝 (loopback 만 대기)\n */\n readRedirect?(signal: AbortSignal): Promise<URLSearchParams | undefined>\n}\n\n/** authorization code + PKCE. redirect = loopback callback 또는 붙여넣은 URL. 둘 다 state · iss 검증 */\nexport async function authorizationCodeLogin(\n target: LoginTarget,\n options: AuthorizationCodeLoginOptions,\n): Promise<StoredCredentials> {\n const doFetch = options.fetch ?? fetch\n const { as, resource, scope } = target\n const loopback = await startLoopback({\n ...(options.port !== undefined ? { port: options.port } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n })\n const pasteAbort = new AbortController()\n try {\n const client = await registerClient(\n as,\n {\n redirectUri: loopback.redirectUri,\n ...(options.clientId !== undefined ? { clientId: options.clientId } : {}),\n ...(options.clientName !== undefined ? { clientName: options.clientName } : {}),\n ...(scope !== undefined ? { scope } : {}),\n },\n doFetch,\n )\n const request = authorizationRequest(as, client, {\n redirectUri: loopback.redirectUri,\n resource,\n ...(scope !== undefined ? { scope } : {}),\n })\n options.onAuthorizationUrl(request.url, { redirectUri: loopback.redirectUri, port: loopback.port })\n const never = new Promise<never>(() => {})\n const pasted = options.readRedirect\n ? options.readRedirect(pasteAbort.signal).then((q) => q ?? never)\n : never\n const query = await Promise.race([loopback.callback, pasted])\n const code = authorizationCode(query, as, request.state)\n return await exchangeCode(\n { issuer: as.issuer, token_endpoint: as.token_endpoint, client, resource },\n { code, redirectUri: loopback.redirectUri, verifier: request.pkce.verifier },\n doFetch,\n )\n } finally {\n pasteAbort.abort()\n await loopback.close()\n }\n}\n\n/** 붙여넣은 redirect URL (또는 query 문자열) → query. code · error 가 없으면 undefined */\nexport function parseRedirectUrl(text: string): URLSearchParams | undefined {\n const trimmed = text.trim()\n if (trimmed === \"\") return undefined\n let query: URLSearchParams\n try {\n query = new URL(trimmed).searchParams\n } catch {\n if (!trimmed.includes(\"=\")) return undefined\n query = new URLSearchParams(trimmed.replace(/^[^?]*\\?/, \"\"))\n }\n return query.has(\"code\") || query.has(\"error\") ? query : undefined\n}\n\n/* device authorization grant (RFC 8628) */\n\nexport interface DeviceAuthorization {\n verification_uri: string\n verification_uri_complete?: string\n user_code: string\n /** 초 */\n expires_in: number\n /** polling 간격 (초). 기본 5 */\n interval: number\n}\n\nexport interface DeviceLoginOptions {\n clientId?: string\n /** 동적 등록 client_name. 기본 mext */\n clientName?: string\n signal?: AbortSignal\n fetch?: Fetch\n /** polling 대기 (테스트 주입). 기본 setTimeout */\n sleep?(ms: number, signal?: AbortSignal): Promise<void>\n /** 사용자 코드 준비됨 (안내 출력 · 브라우저 열기) */\n onDeviceCode(device: DeviceAuthorization): void\n}\n\nexport async function deviceLogin(target: LoginTarget, options: DeviceLoginOptions): Promise<StoredCredentials> {\n const doFetch = options.fetch ?? fetch\n const sleep = options.sleep ?? ((ms: number, signal?: AbortSignal) => delay(ms, undefined, signal ? { signal } : {}))\n const { as, resource, scope } = target\n const endpoint = as.device_authorization_endpoint\n if (typeof endpoint !== \"string\") {\n throw new OAuthFlowError(\n `${as.issuer} does not support the device authorization grant (no device_authorization_endpoint in its metadata).`,\n )\n }\n const client = await registerClient(\n as,\n {\n ...(options.clientId !== undefined ? { clientId: options.clientId } : {}),\n ...(options.clientName !== undefined ? { clientName: options.clientName } : {}),\n ...(scope !== undefined ? { scope } : {}),\n },\n doFetch,\n )\n const body = new URLSearchParams({ resource })\n if (scope !== undefined) body.set(\"scope\", scope)\n const headers = formHeaders()\n clientAuth(client, body, headers)\n const res = await doFetch(endpoint, { method: \"POST\", headers, body })\n const doc = (await res.json().catch(() => undefined)) as Record<string, unknown> | undefined\n if (\n !res.ok ||\n typeof doc?.device_code !== \"string\" ||\n typeof doc.user_code !== \"string\" ||\n typeof doc.verification_uri !== \"string\" ||\n typeof doc.expires_in !== \"number\"\n ) {\n throw new OAuthFlowError(`Device authorization request failed (HTTP ${res.status})${describeOAuthError(doc)}.`)\n }\n const device: DeviceAuthorization = {\n verification_uri: doc.verification_uri,\n user_code: doc.user_code,\n expires_in: doc.expires_in,\n interval: typeof doc.interval === \"number\" && doc.interval >= 0 ? doc.interval : 5,\n }\n if (typeof doc.verification_uri_complete === \"string\") device.verification_uri_complete = doc.verification_uri_complete\n const deviceCode = doc.device_code\n options.onDeviceCode(device)\n\n const context: TokenContext = { issuer: as.issuer, token_endpoint: as.token_endpoint, client, resource }\n const deadline = Date.now() + device.expires_in * 1000\n let interval = device.interval * 1000\n const expired = () => new OAuthFlowError(\"The device code expired before authorization. Run login again.\")\n for (;;) {\n if (Date.now() >= deadline) throw expired()\n await sleep(interval, options.signal)\n const token = await postTokenEndpoint(context, { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode }, doFetch)\n if (token.ok && typeof token.doc?.access_token === \"string\") {\n return credentialsFrom(context, token.doc as Record<string, unknown> & { access_token: string }, undefined)\n }\n const error = token.doc?.error\n if (error === \"authorization_pending\") continue\n if (error === \"slow_down\") {\n interval += 5000 // RFC 8628 3.5\n continue\n }\n if (error === \"expired_token\") throw expired()\n if (error === \"access_denied\") throw new OAuthFlowError(\"Authorization was denied.\")\n throw new OAuthFlowError(`Token request failed (HTTP ${token.status})${describeOAuthError(token.doc)}.`)\n }\n}\n\n/* token revocation (RFC 7009) */\n\nexport interface RevokeResult {\n /** 인가 서버에 revocation_endpoint 가 있음 */\n supported: boolean\n /** 폐기 요청이 성공한 토큰 종류 (refresh_token → access_token 순서) */\n revoked: (\"refresh_token\" | \"access_token\")[]\n /** 실패 (메타데이터 조회 · 요청). 실패해도 저장분 삭제는 호출자가 진행 */\n errors: string[]\n}\n\n/** 저장된 OAuth 토큰 폐기. refresh token 먼저 (RFC 7009 2.1: 연결된 access token 도 무효화될 수 있음), 그다음 access token */\nexport async function revokeCredentials(credentials: StoredCredentials, options: { fetch?: Fetch } = {}): Promise<RevokeResult> {\n const doFetch = options.fetch ?? fetch\n const result: RevokeResult = { supported: false, revoked: [], errors: [] }\n if (credentials.issuer === undefined || credentials.client_id === undefined) return result\n let as: AuthorizationServerMetadata\n try {\n as = await discoverAuthorizationServer(credentials.issuer, doFetch)\n } catch (error) {\n result.errors.push(error instanceof Error ? error.message : String(error))\n return result\n }\n const endpoint = as.revocation_endpoint\n if (typeof endpoint !== \"string\") return result\n result.supported = true\n const client = storedClient(credentials, credentials.client_id)\n const tokens: [RevokeResult[\"revoked\"][number], string | undefined][] = [\n [\"refresh_token\", credentials.refresh_token],\n [\"access_token\", credentials.access_token],\n ]\n for (const [hint, token] of tokens) {\n if (token === undefined || token === \"\") continue\n const body = new URLSearchParams({ token, token_type_hint: hint })\n const headers = formHeaders()\n clientAuth(client, body, headers)\n try {\n const res = await doFetch(endpoint, { method: \"POST\", headers, body })\n if (res.ok) {\n await res.body?.cancel().catch(() => {})\n result.revoked.push(hint)\n } else {\n const doc = (await res.json().catch(() => undefined)) as Record<string, unknown> | undefined\n result.errors.push(`${hint} revocation failed (HTTP ${res.status})${describeOAuthError(doc)}`)\n }\n } catch (error) {\n result.errors.push(`${hint} revocation failed: ${error instanceof Error ? error.message : String(error)}`)\n }\n }\n return result\n}\n\n/** 이전 API: 탐색 + authorization code (loopback) */\nexport interface LoginOptions extends DiscoverLoginOptions, Omit<AuthorizationCodeLoginOptions, \"fetch\"> {}\n\nexport async function login(options: LoginOptions): Promise<StoredCredentials> {\n const target = await discoverLoginTarget(options)\n return authorizationCodeLogin(target, options)\n}\n\n/* 공통 */\n\nasync function getJson(url: string, doFetch: Fetch): Promise<Record<string, unknown> | undefined> {\n let res: Response\n try {\n res = await doFetch(url, { headers: { Accept: \"application/json\" } })\n } catch (error) {\n throw new OAuthFlowError(`Cannot reach ${url}: ${error instanceof Error ? error.message : String(error)}`)\n }\n if (!res.ok) {\n await res.body?.cancel().catch(() => {})\n return undefined\n }\n const doc = (await res.json().catch(() => undefined)) as unknown\n return typeof doc === \"object\" && doc !== null && !Array.isArray(doc) ? (doc as Record<string, unknown>) : undefined\n}\n\nfunction describeOAuthError(doc: Record<string, unknown> | undefined): string {\n if (typeof doc?.error !== \"string\") return \"\"\n return `: ${doc.error}${typeof doc.error_description === \"string\" ? ` (${doc.error_description})` : \"\"}`\n}\n\nfunction base64url(buf: Buffer): string {\n return buf.toString(\"base64url\")\n}\n"]}
|