@slashfi/agents-sdk 0.39.0 → 0.40.1

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.
@@ -0,0 +1,691 @@
1
+ /**
2
+ * ADK Config Store — programmatic API for managing registries and refs.
3
+ *
4
+ * Provides a `createAdk(fs, options?)` factory that returns an object
5
+ * with `registry.*` and `ref.*` namespaces. Backed by a pluggable
6
+ * FsStore so it works with local filesystem (CLI) or VCS (atlas).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { createAdk, createLocalFsStore } from '@slashfi/agents-sdk';
11
+ *
12
+ * const adk = createAdk(createLocalFsStore());
13
+ * await adk.registry.add({ url: 'https://registry.slash.com', name: 'slash' });
14
+ * await adk.registry.browse('slash');
15
+ * await adk.ref.add({ ref: 'notion', registry: 'slash' });
16
+ * await adk.ref.call('notion', 'notion-search', { query: 'hello' });
17
+ * ```
18
+ */
19
+ import { normalizeRef } from "./define-config.js";
20
+ import { createRegistryConsumer } from "./registry-consumer.js";
21
+ import { decryptSecret, encryptSecret } from "./crypto.js";
22
+ import { discoverOAuthMetadata, dynamicClientRegistration, buildOAuthAuthorizeUrl, exchangeCodeForTokens, } from "./mcp-client.js";
23
+ const CONFIG_PATH = "consumer-config.json";
24
+ const SECRET_PREFIX = "secret:";
25
+ // ============================================
26
+ // Internal helpers
27
+ // ============================================
28
+ function refName(entry) {
29
+ return normalizeRef(entry).name;
30
+ }
31
+ function registryDisplayName(r) {
32
+ return typeof r === "string" ? r : (r.name ?? r.url);
33
+ }
34
+ function registryUrl(r) {
35
+ return typeof r === "string" ? r : r.url;
36
+ }
37
+ function findRegistry(registries, nameOrUrl) {
38
+ return registries.find((r) => {
39
+ if (typeof r === "string")
40
+ return r === nameOrUrl;
41
+ return (r.name ?? r.url) === nameOrUrl || r.url === nameOrUrl;
42
+ });
43
+ }
44
+ /**
45
+ * Walk an object and decrypt any string values starting with "secret:".
46
+ */
47
+ async function decryptConfigSecrets(obj, encryptionKey) {
48
+ const result = {};
49
+ for (const [key, value] of Object.entries(obj)) {
50
+ if (typeof value === "string" && value.startsWith(SECRET_PREFIX)) {
51
+ result[key] = await decryptSecret(value.slice(SECRET_PREFIX.length), encryptionKey);
52
+ }
53
+ else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
54
+ result[key] = await decryptConfigSecrets(value, encryptionKey);
55
+ }
56
+ else {
57
+ result[key] = value;
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+ // ============================================
63
+ // Factory
64
+ // ============================================
65
+ export function createAdk(fs, options = {}) {
66
+ async function readConfig() {
67
+ const content = await fs.readFile(CONFIG_PATH);
68
+ if (!content)
69
+ return {};
70
+ try {
71
+ return JSON.parse(content);
72
+ }
73
+ catch {
74
+ return {};
75
+ }
76
+ }
77
+ async function writeConfig(config) {
78
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
79
+ }
80
+ /**
81
+ * Store a secret value in a ref's config, encrypted if encryptionKey is set.
82
+ * The value is stored inline as "secret:<encrypted>" in consumer-config.json.
83
+ */
84
+ async function storeRefSecret(name, key, value) {
85
+ const stored = options.encryptionKey
86
+ ? `${SECRET_PREFIX}${await encryptSecret(value, options.encryptionKey)}`
87
+ : value;
88
+ const config = await readConfig();
89
+ const refs = (config.refs ?? []).map((r) => {
90
+ if (refName(r) !== name)
91
+ return r;
92
+ return { ...r, config: { ...r.config, [key]: stored } };
93
+ });
94
+ await writeConfig({ ...config, refs });
95
+ }
96
+ async function readRefSecret(name, key) {
97
+ const config = await readConfig();
98
+ const entry = (config.refs ?? []).find((r) => refName(r) === name);
99
+ const value = entry?.config?.[key];
100
+ if (typeof value !== "string")
101
+ return null;
102
+ if (value.startsWith(SECRET_PREFIX) && options.encryptionKey) {
103
+ return decryptSecret(value.slice(SECRET_PREFIX.length), options.encryptionKey);
104
+ }
105
+ return value;
106
+ }
107
+ const PENDING_OAUTH_PATH = "pending-oauth.json";
108
+ async function readPendingOAuth() {
109
+ const content = await fs.readFile(PENDING_OAUTH_PATH);
110
+ if (!content)
111
+ return {};
112
+ try {
113
+ return JSON.parse(content);
114
+ }
115
+ catch {
116
+ return {};
117
+ }
118
+ }
119
+ async function writePendingOAuth(pending) {
120
+ await fs.writeFile(PENDING_OAUTH_PATH, JSON.stringify(pending, null, 2));
121
+ }
122
+ async function storePendingOAuth(state, data) {
123
+ const pending = await readPendingOAuth();
124
+ pending[state] = data;
125
+ await writePendingOAuth(pending);
126
+ }
127
+ async function consumePendingOAuth(state) {
128
+ const pending = await readPendingOAuth();
129
+ const data = pending[state] ?? null;
130
+ if (data) {
131
+ delete pending[state];
132
+ await writePendingOAuth(pending);
133
+ }
134
+ return data;
135
+ }
136
+ /** Call an MCP server directly with a bearer token (bypasses registry). */
137
+ async function callMcpDirect(serverUrl, toolName, params, token) {
138
+ const url = serverUrl.replace(/\/$/, "");
139
+ const headers = {
140
+ "Content-Type": "application/json",
141
+ Accept: "application/json, text/event-stream",
142
+ Authorization: `Bearer ${token}`,
143
+ };
144
+ let reqId = 0;
145
+ let sessionId;
146
+ async function rpc(method, rpcParams) {
147
+ const reqHeaders = { ...headers, ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}) };
148
+ const res = await globalThis.fetch(url, {
149
+ method: "POST",
150
+ headers: reqHeaders,
151
+ body: JSON.stringify({
152
+ jsonrpc: "2.0",
153
+ id: ++reqId,
154
+ method,
155
+ ...(rpcParams && { params: rpcParams }),
156
+ }),
157
+ });
158
+ if (!res.ok) {
159
+ throw new Error(`MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`);
160
+ }
161
+ const contentType = res.headers.get("content-type") ?? "";
162
+ // Capture session ID from response
163
+ const newSessionId = res.headers.get("mcp-session-id");
164
+ if (newSessionId)
165
+ sessionId = newSessionId;
166
+ // SSE response — parse events to find the JSON-RPC result
167
+ if (contentType.includes("text/event-stream")) {
168
+ const text = await res.text();
169
+ const lines = text.split("\n");
170
+ for (const line of lines) {
171
+ if (line.startsWith("data: ")) {
172
+ try {
173
+ const json = JSON.parse(line.slice(6));
174
+ if (json.id === reqId) {
175
+ if (json.error)
176
+ throw new Error(`MCP RPC error: ${json.error.message}`);
177
+ return json.result;
178
+ }
179
+ }
180
+ catch (e) {
181
+ if (e instanceof Error && e.message.startsWith("MCP RPC"))
182
+ throw e;
183
+ }
184
+ }
185
+ }
186
+ return undefined;
187
+ }
188
+ const json = await res.json();
189
+ if (json.error)
190
+ throw new Error(`MCP RPC error: ${json.error.message}`);
191
+ return json.result;
192
+ }
193
+ try {
194
+ await rpc("initialize", {
195
+ protocolVersion: "2024-11-05",
196
+ capabilities: {},
197
+ clientInfo: { name: "adk", version: "1.0.0" },
198
+ });
199
+ await rpc("notifications/initialized").catch(() => { });
200
+ const result = await rpc("tools/call", { name: toolName, arguments: params });
201
+ const textContent = result?.content?.find((c) => c.type === "text");
202
+ if (textContent?.text) {
203
+ try {
204
+ return { success: true, result: JSON.parse(textContent.text) };
205
+ }
206
+ catch {
207
+ return { success: true, result: textContent.text };
208
+ }
209
+ }
210
+ return { success: true, result };
211
+ }
212
+ catch (err) {
213
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
214
+ }
215
+ }
216
+ function callbackUrl() {
217
+ const port = options.oauthCallbackPort ?? 8919;
218
+ return options.oauthCallbackUrl ?? `http://localhost:${port}/callback`;
219
+ }
220
+ /** Try fetching a URL directly as OAuth metadata (it may already be a discovery URL). */
221
+ async function tryFetchOAuthMetadata(url) {
222
+ try {
223
+ const res = await globalThis.fetch(url);
224
+ if (!res.ok)
225
+ return null;
226
+ const data = await res.json();
227
+ if (data.authorization_endpoint && data.token_endpoint) {
228
+ return data;
229
+ }
230
+ return null;
231
+ }
232
+ catch {
233
+ return null;
234
+ }
235
+ }
236
+ /**
237
+ * Build a registryConsumer from the current config.
238
+ * Decrypts secret: values in registry headers/auth before connecting.
239
+ */
240
+ async function buildConsumer(registryFilter) {
241
+ const config = await readConfig();
242
+ let registries = config.registries ?? [];
243
+ if (registryFilter) {
244
+ const target = findRegistry(registries, registryFilter);
245
+ if (!target) {
246
+ throw new Error(`Registry "${registryFilter}" not found. Available: ${registries.map(registryDisplayName).join(", ")}`);
247
+ }
248
+ registries = [target];
249
+ }
250
+ // Decrypt secret: values in registry entries if encryption key is set
251
+ const resolved = options.encryptionKey
252
+ ? await Promise.all(registries.map(async (r) => {
253
+ if (typeof r === "string")
254
+ return r;
255
+ const decrypted = await decryptConfigSecrets(r, options.encryptionKey);
256
+ return decrypted;
257
+ }))
258
+ : registries;
259
+ return createRegistryConsumer({ registries: resolved, refs: config.refs ?? [] }, { token: options.token });
260
+ }
261
+ // ==========================================
262
+ // Registry API
263
+ // ==========================================
264
+ const registry = {
265
+ async add(entry) {
266
+ const config = await readConfig();
267
+ const alias = entry.name ?? entry.url;
268
+ const registries = (config.registries ?? []).filter((r) => registryDisplayName(r) !== alias);
269
+ registries.push(entry);
270
+ await writeConfig({ ...config, registries });
271
+ },
272
+ async remove(nameOrUrl) {
273
+ const config = await readConfig();
274
+ if (!config.registries?.length)
275
+ return false;
276
+ const before = config.registries.length;
277
+ const registries = config.registries.filter((r) => registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl);
278
+ if (registries.length === before)
279
+ return false;
280
+ await writeConfig({ ...config, registries });
281
+ return true;
282
+ },
283
+ async list() {
284
+ const config = await readConfig();
285
+ return (config.registries ?? []).map((r) => typeof r === "string" ? { url: r } : r);
286
+ },
287
+ async get(name) {
288
+ const config = await readConfig();
289
+ const target = findRegistry(config.registries ?? [], name);
290
+ if (!target)
291
+ return null;
292
+ return typeof target === "string" ? { url: target } : target;
293
+ },
294
+ async update(name, updates) {
295
+ const config = await readConfig();
296
+ if (!config.registries?.length)
297
+ return false;
298
+ let found = false;
299
+ const registries = config.registries.map((r) => {
300
+ const rName = registryDisplayName(r);
301
+ if (rName !== name && registryUrl(r) !== name)
302
+ return r;
303
+ found = true;
304
+ const existing = typeof r === "string" ? { url: r } : { ...r };
305
+ if (updates.url)
306
+ existing.url = updates.url;
307
+ if (updates.name)
308
+ existing.name = updates.name;
309
+ if (updates.auth)
310
+ existing.auth = updates.auth;
311
+ if (updates.headers)
312
+ existing.headers = { ...existing.headers, ...updates.headers };
313
+ return existing;
314
+ });
315
+ if (!found)
316
+ return false;
317
+ await writeConfig({ ...config, registries });
318
+ return true;
319
+ },
320
+ async browse(name, query) {
321
+ const consumer = await buildConsumer(name);
322
+ const config = await readConfig();
323
+ const target = findRegistry(config.registries ?? [], name);
324
+ const url = target ? registryUrl(target) : name;
325
+ return consumer.browse(url, query);
326
+ },
327
+ async inspect(name) {
328
+ const consumer = await buildConsumer(name);
329
+ const config = await readConfig();
330
+ const target = findRegistry(config.registries ?? [], name);
331
+ const url = target ? registryUrl(target) : name;
332
+ return consumer.discover(url);
333
+ },
334
+ async test(name) {
335
+ const config = await readConfig();
336
+ const registries = config.registries ?? [];
337
+ const targets = name
338
+ ? registries.filter((r) => registryDisplayName(r) === name || registryUrl(r) === name)
339
+ : registries;
340
+ const results = await Promise.allSettled(targets.map(async (r) => {
341
+ const url = registryUrl(r);
342
+ const rName = registryDisplayName(r);
343
+ try {
344
+ const consumer = await createRegistryConsumer({ registries: [r] }, { token: options.token });
345
+ const disc = await consumer.discover(url);
346
+ return { name: rName, url, status: "active", issuer: disc.issuer };
347
+ }
348
+ catch (err) {
349
+ const msg = err instanceof Error ? err.message : "unknown";
350
+ return { name: rName, url, status: "error", error: msg };
351
+ }
352
+ }));
353
+ return results.map((r) => r.status === "fulfilled"
354
+ ? r.value
355
+ : { name: "unknown", url: "unknown", status: "error", error: "unknown" });
356
+ },
357
+ };
358
+ // ==========================================
359
+ // Ref API
360
+ // ==========================================
361
+ const ref = {
362
+ async add(entry) {
363
+ const config = await readConfig();
364
+ const name = refName(entry);
365
+ const refs = (config.refs ?? []).filter((r) => refName(r) !== name);
366
+ refs.push(entry);
367
+ await writeConfig({ ...config, refs });
368
+ // Check security requirements
369
+ let security = null;
370
+ try {
371
+ const consumer = await buildConsumer();
372
+ const info = await consumer.inspect(entry.ref);
373
+ if (info?.security)
374
+ security = info.security;
375
+ }
376
+ catch {
377
+ // Non-fatal — registry might be unreachable
378
+ }
379
+ return { security };
380
+ },
381
+ async remove(name) {
382
+ const config = await readConfig();
383
+ if (!config.refs?.length)
384
+ return false;
385
+ const before = config.refs.length;
386
+ const refs = config.refs.filter((r) => refName(r) !== name);
387
+ if (refs.length === before)
388
+ return false;
389
+ await writeConfig({ ...config, refs });
390
+ return true;
391
+ },
392
+ async list() {
393
+ const config = await readConfig();
394
+ return (config.refs ?? []).map(normalizeRef);
395
+ },
396
+ async get(name) {
397
+ const config = await readConfig();
398
+ return (config.refs ?? []).find((r) => refName(r) === name) ?? null;
399
+ },
400
+ async update(name, updates) {
401
+ const config = await readConfig();
402
+ if (!config.refs?.length)
403
+ return false;
404
+ let found = false;
405
+ const refs = config.refs.map((r) => {
406
+ if (refName(r) !== name)
407
+ return r;
408
+ found = true;
409
+ const updated = { ...r };
410
+ if (updates.url)
411
+ updated.url = updates.url;
412
+ if (updates.as)
413
+ updated.as = updates.as;
414
+ if (updates.scheme)
415
+ updated.scheme = updates.scheme;
416
+ if (updates.config)
417
+ updated.config = { ...updated.config, ...updates.config };
418
+ if (updates.sourceRegistry)
419
+ updated.sourceRegistry = updates.sourceRegistry;
420
+ return updated;
421
+ });
422
+ if (!found)
423
+ return false;
424
+ await writeConfig({ ...config, refs });
425
+ return true;
426
+ },
427
+ async inspect(name, opts) {
428
+ const config = await readConfig();
429
+ const entry = (config.refs ?? []).find((r) => refName(r) === name);
430
+ if (!entry)
431
+ throw new Error(`Ref "${name}" not found`);
432
+ const consumer = await buildConsumer();
433
+ return consumer.inspect(entry.ref, undefined, opts);
434
+ },
435
+ async call(name, tool, params) {
436
+ const config = await readConfig();
437
+ const entry = (config.refs ?? []).find((r) => refName(r) === name);
438
+ if (!entry)
439
+ throw new Error(`Ref "${name}" not found`);
440
+ const accessToken = await readRefSecret(name, "access_token");
441
+ // If we have a direct access_token from OAuth, call the agent's MCP server
442
+ // directly instead of going through the registry
443
+ if (accessToken) {
444
+ const info = await ref.inspect(name);
445
+ // Use upstream URL from registry if available, fall back to deriving from OAuth URL
446
+ const upstream = info?.upstream;
447
+ const security = info?.security;
448
+ const mcpUrl = upstream
449
+ ?? (security?.flows?.authorizationCode?.authorizationUrl
450
+ ? `${new URL(security.flows.authorizationCode.authorizationUrl).origin}/mcp`
451
+ : null);
452
+ if (mcpUrl) {
453
+ return callMcpDirect(mcpUrl, tool, params ?? {}, accessToken);
454
+ }
455
+ }
456
+ const consumer = await buildConsumer();
457
+ const reg = consumer.registries()[0];
458
+ if (!reg)
459
+ throw new Error("No registry available");
460
+ return consumer.callRegistry(reg, {
461
+ action: "execute_tool",
462
+ path: entry.ref,
463
+ tool,
464
+ params: params ?? {},
465
+ });
466
+ },
467
+ async resources(name) {
468
+ const config = await readConfig();
469
+ const entry = (config.refs ?? []).find((r) => refName(r) === name);
470
+ if (!entry)
471
+ throw new Error(`Ref "${name}" not found`);
472
+ const consumer = await buildConsumer();
473
+ const reg = consumer.registries()[0];
474
+ if (!reg)
475
+ throw new Error("No registry available");
476
+ return consumer.callRegistry(reg, {
477
+ action: "list_resources",
478
+ path: entry.ref,
479
+ });
480
+ },
481
+ async read(name, uris) {
482
+ const config = await readConfig();
483
+ const entry = (config.refs ?? []).find((r) => refName(r) === name);
484
+ if (!entry)
485
+ throw new Error(`Ref "${name}" not found`);
486
+ const consumer = await buildConsumer();
487
+ const reg = consumer.registries()[0];
488
+ if (!reg)
489
+ throw new Error("No registry available");
490
+ return consumer.callRegistry(reg, {
491
+ action: "read_resources",
492
+ path: entry.ref,
493
+ uris,
494
+ });
495
+ },
496
+ async authStatus(name) {
497
+ const config = await readConfig();
498
+ const entry = (config.refs ?? []).find((r) => refName(r) === name);
499
+ if (!entry)
500
+ throw new Error(`Ref "${name}" not found`);
501
+ let security = null;
502
+ try {
503
+ const consumer = await buildConsumer();
504
+ const info = await consumer.inspect(entry.ref);
505
+ if (info?.security)
506
+ security = info.security;
507
+ }
508
+ catch {
509
+ // Can't reach registry
510
+ }
511
+ const configKeys = Object.keys(entry.config ?? {});
512
+ if (!security || security.type === "none") {
513
+ return { name, security, complete: true, missing: [], present: configKeys };
514
+ }
515
+ const requiredFields = (() => {
516
+ switch (security.type) {
517
+ case "oauth2": return ["access_token"];
518
+ case "apiKey": return ["api_key"];
519
+ case "http": return ["token"];
520
+ default: return [];
521
+ }
522
+ })();
523
+ const missing = requiredFields.filter((f) => !configKeys.includes(f));
524
+ const present = requiredFields.filter((f) => configKeys.includes(f));
525
+ return { name, security, complete: missing.length === 0, missing, present };
526
+ },
527
+ async auth(name, opts) {
528
+ const status = await ref.authStatus(name);
529
+ const security = status.security;
530
+ if (!security || security.type === "none") {
531
+ return { type: "none", complete: true };
532
+ }
533
+ if (security.type === "apiKey") {
534
+ if (!opts?.apiKey)
535
+ return { type: "apiKey", complete: false };
536
+ await storeRefSecret(name, "api_key", opts.apiKey);
537
+ return { type: "apiKey", complete: true };
538
+ }
539
+ if (security.type === "http") {
540
+ if (!opts?.apiKey)
541
+ return { type: "http", complete: false };
542
+ await storeRefSecret(name, "token", opts.apiKey);
543
+ return { type: "http", complete: true };
544
+ }
545
+ if (security.type === "oauth2") {
546
+ const flows = security.flows;
547
+ const authCodeFlow = flows?.authorizationCode;
548
+ if (!authCodeFlow?.authorizationUrl) {
549
+ return { type: "oauth2", complete: false };
550
+ }
551
+ // The authorizationUrl might be the discovery URL itself or a base URL
552
+ const authUrl = authCodeFlow.authorizationUrl;
553
+ let metadata = await tryFetchOAuthMetadata(authUrl);
554
+ if (!metadata) {
555
+ // Try base origin
556
+ const origin = new URL(authUrl).origin;
557
+ metadata = await discoverOAuthMetadata(origin);
558
+ }
559
+ if (!metadata) {
560
+ throw new Error(`Could not discover OAuth metadata from ${authUrl}`);
561
+ }
562
+ const redirectUri = callbackUrl();
563
+ // Dynamic client registration if supported
564
+ let clientId;
565
+ let clientSecret;
566
+ if (metadata.registration_endpoint) {
567
+ const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
568
+ const preferredMethod = supportedAuthMethods.includes("none")
569
+ ? "none"
570
+ : supportedAuthMethods[0] ?? "client_secret_post";
571
+ const securityClientName = security.clientName;
572
+ const reg = await dynamicClientRegistration(metadata.registration_endpoint, {
573
+ clientName: securityClientName ?? options.oauthClientName ?? "adk",
574
+ redirectUris: [redirectUri],
575
+ grantTypes: ["authorization_code"],
576
+ tokenEndpointAuthMethod: preferredMethod,
577
+ });
578
+ clientId = reg.clientId;
579
+ clientSecret = reg.clientSecret;
580
+ await storeRefSecret(name, "client_id", clientId);
581
+ if (clientSecret) {
582
+ await storeRefSecret(name, "client_secret", clientSecret);
583
+ }
584
+ }
585
+ else {
586
+ const stored = await readRefSecret(name, "client_id");
587
+ if (!stored) {
588
+ throw new Error("OAuth server doesn't support dynamic client registration. " +
589
+ "Store a client_id first.");
590
+ }
591
+ clientId = stored;
592
+ }
593
+ // State ties the callback back to this ref
594
+ const state = `${name}:${Date.now()}`;
595
+ const { url: authorizeUrl, codeVerifier } = await buildOAuthAuthorizeUrl({
596
+ authorizationEndpoint: metadata.authorization_endpoint,
597
+ clientId,
598
+ redirectUri,
599
+ scopes: metadata.scopes_supported,
600
+ state,
601
+ });
602
+ // Persist pending state so handleCallback works across processes
603
+ await storePendingOAuth(state, {
604
+ refName: name,
605
+ codeVerifier,
606
+ clientId,
607
+ clientSecret,
608
+ tokenEndpoint: metadata.token_endpoint,
609
+ redirectUri,
610
+ createdAt: Date.now(),
611
+ });
612
+ return { type: "oauth2", complete: false, authorizeUrl };
613
+ }
614
+ return { type: security.type, complete: false };
615
+ },
616
+ async authLocal(name, opts) {
617
+ const result = await ref.auth(name);
618
+ if (result.complete)
619
+ return { complete: true };
620
+ if (result.type !== "oauth2" || !result.authorizeUrl) {
621
+ throw new Error(`authLocal only handles OAuth2. Auth type: ${result.type}`);
622
+ }
623
+ if (opts?.onAuthorizeUrl) {
624
+ opts.onAuthorizeUrl(result.authorizeUrl);
625
+ }
626
+ // Spin up local callback server
627
+ const port = options.oauthCallbackPort ?? 8919;
628
+ const timeout = opts?.timeoutMs ?? 300_000;
629
+ const { createServer } = await import("node:http");
630
+ return new Promise((resolve, reject) => {
631
+ const server = createServer(async (req, res) => {
632
+ const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
633
+ if (reqUrl.pathname !== "/callback")
634
+ return;
635
+ const code = reqUrl.searchParams.get("code");
636
+ const state = reqUrl.searchParams.get("state");
637
+ if (!code || !state) {
638
+ const error = reqUrl.searchParams.get("error") ?? "missing code/state";
639
+ res.writeHead(400, { "Content-Type": "text/html" });
640
+ res.end(`<h1>Error</h1><p>${error}</p>`);
641
+ server.close();
642
+ reject(new Error(`OAuth denied: ${error}`));
643
+ return;
644
+ }
645
+ try {
646
+ const cbResult = await handleCallback({ code, state });
647
+ res.writeHead(200, { "Content-Type": "text/html" });
648
+ res.end("<h1>Authorized!</h1><p>You can close this tab.</p>");
649
+ server.close();
650
+ resolve({ complete: cbResult.complete });
651
+ }
652
+ catch (err) {
653
+ res.writeHead(500, { "Content-Type": "text/html" });
654
+ res.end(`<h1>Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`);
655
+ server.close();
656
+ reject(err);
657
+ }
658
+ });
659
+ server.listen(port);
660
+ const timer = setTimeout(() => {
661
+ server.close();
662
+ reject(new Error("OAuth callback timed out"));
663
+ }, timeout);
664
+ server.on("close", () => clearTimeout(timer));
665
+ });
666
+ },
667
+ };
668
+ // ==========================================
669
+ // Top-level callback handler
670
+ // ==========================================
671
+ async function handleCallback(params) {
672
+ const pending = await consumePendingOAuth(params.state);
673
+ if (!pending) {
674
+ throw new Error(`No pending OAuth flow for state "${params.state}".`);
675
+ }
676
+ const tokens = await exchangeCodeForTokens(pending.tokenEndpoint, {
677
+ code: params.code,
678
+ codeVerifier: pending.codeVerifier,
679
+ clientId: pending.clientId,
680
+ clientSecret: pending.clientSecret,
681
+ redirectUri: pending.redirectUri,
682
+ });
683
+ await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
684
+ if (tokens.refreshToken) {
685
+ await storeRefSecret(pending.refName, "refresh_token", tokens.refreshToken);
686
+ }
687
+ return { refName: pending.refName, complete: true };
688
+ }
689
+ return { registry, ref, readConfig, writeConfig, handleCallback };
690
+ }
691
+ //# sourceMappingURL=config-store.js.map