@slashfi/agents-sdk 0.77.3 → 0.79.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/call-agent-schema.d.ts +12 -12
- package/dist/cjs/config-store.js +340 -68
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/config-store.d.ts +90 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +339 -68
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +28 -4
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config-store.test.ts +499 -28
- package/src/config-store.ts +846 -250
- package/src/define-config.ts +47 -21
- package/src/index.ts +18 -13
package/src/config-store.ts
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { AdkError } from "./adk-error.js";
|
|
20
21
|
import type { FsStore } from "./agent-definitions/config.js";
|
|
22
|
+
import { decryptSecret, encryptSecret } from "./crypto.js";
|
|
21
23
|
import type {
|
|
22
24
|
ConsumerConfig,
|
|
23
25
|
RefAddInput,
|
|
@@ -27,31 +29,141 @@ import type {
|
|
|
27
29
|
ResolvedRegistry,
|
|
28
30
|
} from "./define-config.js";
|
|
29
31
|
import { normalizeRef } from "./define-config.js";
|
|
32
|
+
import type { RegistryAuthRequirement } from "./define-config.js";
|
|
30
33
|
import type { FetchFn } from "./fetch-types.js";
|
|
31
34
|
import type { Logger } from "./logger.js";
|
|
32
|
-
import { createRegistryConsumer } from "./registry-consumer.js";
|
|
33
|
-
import type {
|
|
34
|
-
AgentListEntry,
|
|
35
|
-
AgentInspection,
|
|
36
|
-
RegistryConfiguration,
|
|
37
|
-
RegistryConsumer,
|
|
38
|
-
} from "./registry-consumer.js";
|
|
39
|
-
import type { CallAgentResponse, SecuritySchemeSummary } from "./types.js";
|
|
40
|
-
import { decryptSecret, encryptSecret } from "./crypto.js";
|
|
41
|
-
import { AdkError } from "./adk-error.js";
|
|
42
35
|
import {
|
|
36
|
+
buildOAuthAuthorizeUrl,
|
|
43
37
|
discoverOAuthMetadata,
|
|
44
38
|
dynamicClientRegistration,
|
|
45
|
-
buildOAuthAuthorizeUrl,
|
|
46
39
|
exchangeCodeForTokens,
|
|
47
40
|
probeRegistryAuth,
|
|
48
41
|
refreshAccessToken,
|
|
49
42
|
} from "./mcp-client.js";
|
|
50
|
-
import
|
|
43
|
+
import { createRegistryConsumer } from "./registry-consumer.js";
|
|
44
|
+
import type {
|
|
45
|
+
AgentInspection,
|
|
46
|
+
AgentListEntry,
|
|
47
|
+
RegistryConfiguration,
|
|
48
|
+
RegistryConsumer,
|
|
49
|
+
} from "./registry-consumer.js";
|
|
50
|
+
import type { CallAgentResponse, SecuritySchemeSummary } from "./types.js";
|
|
51
51
|
|
|
52
52
|
const CONFIG_PATH = "consumer-config.json";
|
|
53
|
+
const REGISTRY_CACHE_PATH = "registry-cache.json";
|
|
53
54
|
const SECRET_PREFIX = "secret:";
|
|
54
55
|
|
|
56
|
+
// ============================================
|
|
57
|
+
// Registry cache types
|
|
58
|
+
// ============================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Slim tool summary stored in the registry cache. Mirrors the shape returned
|
|
62
|
+
* by `consumer.inspect()` (sans `inputSchema` and `fullTokens`) so the LLM
|
|
63
|
+
* can discover an agent's surface without a network round-trip.
|
|
64
|
+
*/
|
|
65
|
+
export interface RegistryCacheToolSummary {
|
|
66
|
+
name: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Slim auth-field metadata cached so hosts can locally answer "is this
|
|
72
|
+
* ref ready to call?" without a registry round-trip. Mirrors the
|
|
73
|
+
* authoritative shape `auth-status` produces — same source of truth,
|
|
74
|
+
* just persisted.
|
|
75
|
+
*
|
|
76
|
+
* For each field name in the security scheme:
|
|
77
|
+
* - `required` — must end up satisfied for `ref.call` to work.
|
|
78
|
+
* - `automated` — adk fills this in itself (e.g. dynamic OAuth
|
|
79
|
+
* client registration). Doesn't need to be `present`
|
|
80
|
+
* in the user's config to count as satisfied.
|
|
81
|
+
*/
|
|
82
|
+
export interface RegistryCacheAuthField {
|
|
83
|
+
required: boolean;
|
|
84
|
+
automated: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Per-ref cache entry. Updated as a side-effect of `ref.add()`,
|
|
89
|
+
* `ref.inspect()`, and `ref.authStatus()` whenever the registry
|
|
90
|
+
* response carries description / tool / security-scheme info.
|
|
91
|
+
* Identity-relative (lives next to the consumer-config that issued
|
|
92
|
+
* the registry call), so permission-filtered views stay consistent.
|
|
93
|
+
*/
|
|
94
|
+
export interface RegistryCacheEntry {
|
|
95
|
+
/** Canonical agent path (e.g. `notion`). Stored for sanity/debug. */
|
|
96
|
+
ref: string;
|
|
97
|
+
description?: string;
|
|
98
|
+
tools?: RegistryCacheToolSummary[];
|
|
99
|
+
/**
|
|
100
|
+
* Auth field requirements derived from the registry's security
|
|
101
|
+
* scheme (extracted by `auth-status`). When present, hosts can
|
|
102
|
+
* compute "is this ref callable?" by intersecting these with the
|
|
103
|
+
* entry's `config` — no network round-trip needed. Absent when the
|
|
104
|
+
* scheme couldn't be fetched (e.g. registry was offline at add
|
|
105
|
+
* time); fall back to whatever heuristic the caller chooses.
|
|
106
|
+
*
|
|
107
|
+
* Note on proxy refs: when the entry is in `proxy` mode the
|
|
108
|
+
* security scheme is exposed by the *proxy*, and the answer to
|
|
109
|
+
* "is this callable?" lives server-side — `authFields` is omitted
|
|
110
|
+
* locally and hosts should treat proxy refs as authoritative
|
|
111
|
+
* regardless of entry-side fields.
|
|
112
|
+
*/
|
|
113
|
+
authFields?: Record<string, RegistryCacheAuthField>;
|
|
114
|
+
/** ISO timestamp of the most recent registry round-trip that wrote this. */
|
|
115
|
+
fetchedAt: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* On-disk shape of `registry-cache.json`. Keyed by `RefEntry.name` (local
|
|
120
|
+
* identifier) — the same key consumer-config uses, so hydration is a 1:1
|
|
121
|
+
* lookup.
|
|
122
|
+
*/
|
|
123
|
+
export interface RegistryCache {
|
|
124
|
+
refs: Record<string, RegistryCacheEntry>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* "Is this ref ready to call?" answered locally using the cached
|
|
129
|
+
* security-scheme requirements. Mirrors the `complete` boolean
|
|
130
|
+
* `auth-status` returns, but doesn't need a network round-trip — the
|
|
131
|
+
* cached `authFields` capture what the registry said is required, and
|
|
132
|
+
* we evaluate satisfaction against the entry's current `config`.
|
|
133
|
+
*
|
|
134
|
+
* Behavior:
|
|
135
|
+
* - `mode: 'proxy'` refs → always true. Auth lives server-side; the
|
|
136
|
+
* proxy is the source of truth, no entry-side fields involved.
|
|
137
|
+
* - Cache miss (no `authFields` for this ref yet) → returns `null`,
|
|
138
|
+
* signaling "I don't know — caller should fall back to its own
|
|
139
|
+
* heuristic or call `auth-status` to populate the cache".
|
|
140
|
+
* - Cache hit → for every required, non-automated field, checks
|
|
141
|
+
* presence in `entry.config`. Mirrors the `present || resolvable`
|
|
142
|
+
* check in `auth-status` but evaluates against current config.
|
|
143
|
+
* `automated` fields (e.g. dynamic OAuth client_id) count as
|
|
144
|
+
* satisfied even when absent — adk supplies them at call time.
|
|
145
|
+
*
|
|
146
|
+
* Returning `null` for cache miss is intentional. A boolean would
|
|
147
|
+
* force callers to choose a default that's wrong half the time;
|
|
148
|
+
* `null` lets them branch explicitly.
|
|
149
|
+
*/
|
|
150
|
+
export function isRefAuthComplete(
|
|
151
|
+
entry: RefEntry,
|
|
152
|
+
cacheEntry: RegistryCacheEntry | undefined,
|
|
153
|
+
): boolean | null {
|
|
154
|
+
if (typeof entry === "string") return false;
|
|
155
|
+
if ((entry as { mode?: unknown }).mode === "proxy") return true;
|
|
156
|
+
const authFields = cacheEntry?.authFields;
|
|
157
|
+
if (!authFields) return null;
|
|
158
|
+
const config = entry.config ?? {};
|
|
159
|
+
for (const [field, info] of Object.entries(authFields)) {
|
|
160
|
+
if (!info.required) continue;
|
|
161
|
+
if (info.automated) continue;
|
|
162
|
+
if (!(field in config)) return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
55
167
|
// ============================================
|
|
56
168
|
// Types
|
|
57
169
|
// ============================================
|
|
@@ -128,7 +240,9 @@ export interface RegistryTestResult {
|
|
|
128
240
|
}
|
|
129
241
|
|
|
130
242
|
export interface AdkRegistryApi {
|
|
131
|
-
add(
|
|
243
|
+
add(
|
|
244
|
+
entry: RegistryEntry,
|
|
245
|
+
): Promise<{ authRequirement?: RegistryAuthRequirement }>;
|
|
132
246
|
remove(nameOrUrl: string): Promise<boolean>;
|
|
133
247
|
list(): Promise<RegistryEntry[]>;
|
|
134
248
|
get(name: string): Promise<RegistryEntry | null>;
|
|
@@ -230,7 +344,7 @@ export interface AuthStartResult {
|
|
|
230
344
|
* When populated, call() rejects unknown agent paths and tool names at compile time.
|
|
231
345
|
*/
|
|
232
346
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
233
|
-
export
|
|
347
|
+
export type AdkAgentRegistry = {};
|
|
234
348
|
|
|
235
349
|
/** @internal Helper types for conditional call() signature */
|
|
236
350
|
type AgentPath = keyof AdkAgentRegistry;
|
|
@@ -244,9 +358,17 @@ type ParamsOf<
|
|
|
244
358
|
|
|
245
359
|
type AdkRefCallFn = keyof AdkAgentRegistry extends never
|
|
246
360
|
? // No registry — loose fallback
|
|
247
|
-
(
|
|
361
|
+
(
|
|
362
|
+
name: string,
|
|
363
|
+
tool: string,
|
|
364
|
+
params?: Record<string, unknown>,
|
|
365
|
+
) => Promise<CallAgentResponse>
|
|
248
366
|
: // Registry populated — strict typed overload
|
|
249
|
-
<A extends AgentPath, T extends ToolsOf<A>>(
|
|
367
|
+
<A extends AgentPath, T extends ToolsOf<A>>(
|
|
368
|
+
name: A,
|
|
369
|
+
tool: T,
|
|
370
|
+
params: ParamsOf<A, T>,
|
|
371
|
+
) => Promise<CallAgentResponse>;
|
|
250
372
|
|
|
251
373
|
export interface AdkRefApi {
|
|
252
374
|
add(entry: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }>;
|
|
@@ -254,7 +376,10 @@ export interface AdkRefApi {
|
|
|
254
376
|
list(): Promise<ResolvedRef[]>;
|
|
255
377
|
get(name: string): Promise<ResolvedRef | null>;
|
|
256
378
|
update(name: string, updates: Partial<RefEntry>): Promise<boolean>;
|
|
257
|
-
inspect(
|
|
379
|
+
inspect(
|
|
380
|
+
name: string,
|
|
381
|
+
options?: { full?: boolean },
|
|
382
|
+
): Promise<AgentInspection | null>;
|
|
258
383
|
call: AdkRefCallFn;
|
|
259
384
|
resources(name: string): Promise<CallAgentResponse>;
|
|
260
385
|
read(name: string, uris: string[]): Promise<CallAgentResponse>;
|
|
@@ -265,37 +390,43 @@ export interface AdkRefApi {
|
|
|
265
390
|
* Call adk.handleCallback() when the callback arrives, or use
|
|
266
391
|
* adk.ref.authLocal() to spin up a local server and block.
|
|
267
392
|
*/
|
|
268
|
-
auth(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
393
|
+
auth(
|
|
394
|
+
name: string,
|
|
395
|
+
opts?: {
|
|
396
|
+
/** For API key / bearer auth: the key/token value (single-key shorthand) */
|
|
397
|
+
apiKey?: string;
|
|
398
|
+
/**
|
|
399
|
+
* Credentials map for multi-field auth. Keys match the `name` field
|
|
400
|
+
* from AuthChallengeField (e.g. { "api_key": "xxx", "app_key": "yyy" }).
|
|
401
|
+
* For single-key apiKey or http bearer, `apiKey` shorthand also works.
|
|
402
|
+
*/
|
|
403
|
+
credentials?: Record<string, string>;
|
|
404
|
+
/** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
|
|
405
|
+
stateContext?: Record<string, unknown>;
|
|
406
|
+
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
407
|
+
scopes?: string[];
|
|
408
|
+
/**
|
|
409
|
+
* Opt out of proxy routing when the ref's source registry has
|
|
410
|
+
* `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
|
|
411
|
+
* Defaults to `false` — if a registry offers a proxy we use it.
|
|
412
|
+
*/
|
|
413
|
+
preferLocal?: boolean;
|
|
414
|
+
},
|
|
415
|
+
): Promise<AuthStartResult>;
|
|
288
416
|
/**
|
|
289
417
|
* Run the full OAuth flow locally: start auth, spin up a callback
|
|
290
418
|
* server, open the browser, wait for the redirect, exchange tokens.
|
|
291
419
|
* Resolves when auth is complete or times out.
|
|
292
420
|
*/
|
|
293
|
-
authLocal(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
421
|
+
authLocal(
|
|
422
|
+
name: string,
|
|
423
|
+
opts?: {
|
|
424
|
+
/** Called with the authorize URL (e.g. to open in browser) */
|
|
425
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
426
|
+
/** Timeout in ms (default 300_000 = 5 min) */
|
|
427
|
+
timeoutMs?: number;
|
|
428
|
+
},
|
|
429
|
+
): Promise<{ complete: boolean }>;
|
|
299
430
|
/**
|
|
300
431
|
* Refresh an OAuth access token using a stored refresh_token.
|
|
301
432
|
* Returns the new access_token, or null if refresh is not possible
|
|
@@ -317,7 +448,11 @@ export interface Adk {
|
|
|
317
448
|
* Parse the callback query params and pass them here.
|
|
318
449
|
* @returns the ref name and whether auth is complete
|
|
319
450
|
*/
|
|
320
|
-
handleCallback(params: { code: string; state: string }): Promise<{
|
|
451
|
+
handleCallback(params: { code: string; state: string }): Promise<{
|
|
452
|
+
refName: string;
|
|
453
|
+
complete: boolean;
|
|
454
|
+
stateContext?: Record<string, unknown>;
|
|
455
|
+
}>;
|
|
321
456
|
}
|
|
322
457
|
|
|
323
458
|
// ============================================
|
|
@@ -379,9 +514,19 @@ async function decryptConfigSecrets(
|
|
|
379
514
|
const result: Record<string, unknown> = {};
|
|
380
515
|
for (const [key, value] of Object.entries(obj)) {
|
|
381
516
|
if (typeof value === "string" && value.startsWith(SECRET_PREFIX)) {
|
|
382
|
-
result[key] = await decryptSecret(
|
|
383
|
-
|
|
384
|
-
|
|
517
|
+
result[key] = await decryptSecret(
|
|
518
|
+
value.slice(SECRET_PREFIX.length),
|
|
519
|
+
encryptionKey,
|
|
520
|
+
);
|
|
521
|
+
} else if (
|
|
522
|
+
value !== null &&
|
|
523
|
+
typeof value === "object" &&
|
|
524
|
+
!Array.isArray(value)
|
|
525
|
+
) {
|
|
526
|
+
result[key] = await decryptConfigSecrets(
|
|
527
|
+
value as Record<string, unknown>,
|
|
528
|
+
encryptionKey,
|
|
529
|
+
);
|
|
385
530
|
} else {
|
|
386
531
|
result[key] = value;
|
|
387
532
|
}
|
|
@@ -399,7 +544,7 @@ async function decryptConfigSecrets(
|
|
|
399
544
|
* Fallback: _httpStatus from tool result body
|
|
400
545
|
*/
|
|
401
546
|
function isUnauthorized(result: unknown): boolean {
|
|
402
|
-
if (!result || typeof result !==
|
|
547
|
+
if (!result || typeof result !== "object") return false;
|
|
403
548
|
const r = result as Record<string, unknown>;
|
|
404
549
|
// Primary: HTTP status forwarded by the registry and set by callRegistry
|
|
405
550
|
if (r.httpStatus === 401) return true;
|
|
@@ -414,19 +559,29 @@ function isUnauthorized(result: unknown): boolean {
|
|
|
414
559
|
// ============================================
|
|
415
560
|
|
|
416
561
|
const esc = (s: string) =>
|
|
417
|
-
s
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
562
|
+
s
|
|
563
|
+
.replace(/&/g, "&")
|
|
564
|
+
.replace(/</g, "<")
|
|
565
|
+
.replace(/>/g, ">")
|
|
566
|
+
.replace(/"/g, """);
|
|
567
|
+
|
|
568
|
+
function renderCredentialForm(
|
|
569
|
+
name: string,
|
|
570
|
+
fields: AuthChallengeField[],
|
|
571
|
+
error?: string,
|
|
572
|
+
): string {
|
|
573
|
+
const fieldHtml = fields
|
|
574
|
+
.map(
|
|
575
|
+
(f) => `
|
|
421
576
|
<div class="field">
|
|
422
577
|
<label for="${esc(f.name)}">${esc(f.label)}</label>
|
|
423
578
|
${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
|
|
424
579
|
<input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
|
|
425
|
-
</div
|
|
580
|
+
</div>`,
|
|
581
|
+
)
|
|
582
|
+
.join("");
|
|
426
583
|
|
|
427
|
-
const errorHtml = error
|
|
428
|
-
? `<div class="error">${esc(error)}</div>`
|
|
429
|
-
: "";
|
|
584
|
+
const errorHtml = error ? `<div class="error">${esc(error)}</div>` : "";
|
|
430
585
|
|
|
431
586
|
return `<!DOCTYPE html>
|
|
432
587
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -478,7 +633,6 @@ p{font-size:14px;color:#a3a3a3}
|
|
|
478
633
|
}
|
|
479
634
|
|
|
480
635
|
export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
481
|
-
|
|
482
636
|
async function readConfig(): Promise<ConsumerConfig> {
|
|
483
637
|
const content = await fs.readFile(CONFIG_PATH);
|
|
484
638
|
if (!content) return {};
|
|
@@ -493,11 +647,138 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
493
647
|
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
494
648
|
}
|
|
495
649
|
|
|
650
|
+
// -------------------------------------------------------------------------
|
|
651
|
+
// Registry cache helpers
|
|
652
|
+
//
|
|
653
|
+
// The cache is purely an internal optimization for the adk's read paths
|
|
654
|
+
// (`ref.list()`, `ref.get()`). Writes happen as side-effects of methods
|
|
655
|
+
// that already call the registry (`ref.add()`, `ref.inspect()`); the
|
|
656
|
+
// public surface never grows new methods. Cache failures (missing file,
|
|
657
|
+
// malformed JSON, fs errors during write) are swallowed so the registry
|
|
658
|
+
// cache can never break a registry operation.
|
|
659
|
+
// -------------------------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
async function readRegistryCache(): Promise<RegistryCache> {
|
|
662
|
+
try {
|
|
663
|
+
const content = await fs.readFile(REGISTRY_CACHE_PATH);
|
|
664
|
+
if (!content) return { refs: {} };
|
|
665
|
+
const parsed = JSON.parse(content) as RegistryCache;
|
|
666
|
+
return { refs: parsed.refs ?? {} };
|
|
667
|
+
} catch {
|
|
668
|
+
return { refs: {} };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function writeRegistryCache(cache: RegistryCache): Promise<void> {
|
|
673
|
+
try {
|
|
674
|
+
await fs.writeFile(REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2));
|
|
675
|
+
} catch {
|
|
676
|
+
// Best-effort. A failed cache write should never break the operation
|
|
677
|
+
// that triggered it.
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Project an inspect/list response into the slim shape we cache. Drops
|
|
683
|
+
* `inputSchema` (too large) and `fullTokens` (registry-internal). Returns
|
|
684
|
+
* undefined if the response carries nothing worth caching.
|
|
685
|
+
*/
|
|
686
|
+
function buildCacheEntry(
|
|
687
|
+
ref: string,
|
|
688
|
+
info:
|
|
689
|
+
| {
|
|
690
|
+
description?: string;
|
|
691
|
+
tools?: Array<{ name: string; description?: string }>;
|
|
692
|
+
toolSummaries?: Array<{ name: string; description?: string }>;
|
|
693
|
+
}
|
|
694
|
+
| null
|
|
695
|
+
| undefined,
|
|
696
|
+
): RegistryCacheEntry | undefined {
|
|
697
|
+
if (!info) return undefined;
|
|
698
|
+
const toolSource = info.tools ?? info.toolSummaries;
|
|
699
|
+
const tools = toolSource?.map((t) => {
|
|
700
|
+
const slim: RegistryCacheToolSummary = { name: t.name };
|
|
701
|
+
if (t.description !== undefined) slim.description = t.description;
|
|
702
|
+
return slim;
|
|
703
|
+
});
|
|
704
|
+
if (info.description === undefined && (!tools || tools.length === 0)) {
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
const entry: RegistryCacheEntry = {
|
|
708
|
+
ref,
|
|
709
|
+
fetchedAt: new Date().toISOString(),
|
|
710
|
+
};
|
|
711
|
+
if (info.description !== undefined) entry.description = info.description;
|
|
712
|
+
if (tools && tools.length > 0) entry.tools = tools;
|
|
713
|
+
return entry;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function upsertRegistryCacheEntry(
|
|
717
|
+
name: string,
|
|
718
|
+
entry: RegistryCacheEntry | undefined,
|
|
719
|
+
): Promise<void> {
|
|
720
|
+
if (!entry) return;
|
|
721
|
+
const cache = await readRegistryCache();
|
|
722
|
+
cache.refs[name] = entry;
|
|
723
|
+
await writeRegistryCache(cache);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Merge `authFields` into an existing cache entry without clobbering
|
|
728
|
+
* description/tools, or create a minimal entry if one doesn't exist
|
|
729
|
+
* yet. Called from `authStatus` so the slim {required, automated}
|
|
730
|
+
* shape is always available for `isRefAuthComplete` to answer
|
|
731
|
+
* locally on subsequent calls.
|
|
732
|
+
*/
|
|
733
|
+
async function upsertRegistryCacheAuthFields(
|
|
734
|
+
name: string,
|
|
735
|
+
ref: string,
|
|
736
|
+
authFields: Record<string, RegistryCacheAuthField>,
|
|
737
|
+
): Promise<void> {
|
|
738
|
+
const cache = await readRegistryCache();
|
|
739
|
+
const existing = cache.refs[name];
|
|
740
|
+
cache.refs[name] = {
|
|
741
|
+
...(existing ?? { ref, fetchedAt: new Date().toISOString() }),
|
|
742
|
+
authFields,
|
|
743
|
+
// Refresh fetchedAt so freshness telemetry stays accurate.
|
|
744
|
+
fetchedAt: new Date().toISOString(),
|
|
745
|
+
};
|
|
746
|
+
await writeRegistryCache(cache);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async function removeRegistryCacheEntry(name: string): Promise<void> {
|
|
750
|
+
const cache = await readRegistryCache();
|
|
751
|
+
if (!(name in cache.refs)) return;
|
|
752
|
+
delete cache.refs[name];
|
|
753
|
+
await writeRegistryCache(cache);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Hydrate a `ResolvedRef` with cached registry metadata when available.
|
|
758
|
+
* Pure: never mutates input. Leaves `description` / `tools` undefined when
|
|
759
|
+
* the cache has no entry, so callers can apply their own UX fallback.
|
|
760
|
+
*/
|
|
761
|
+
function hydrateFromCache(
|
|
762
|
+
ref: ResolvedRef,
|
|
763
|
+
cache: RegistryCache,
|
|
764
|
+
): ResolvedRef {
|
|
765
|
+
const cached = cache.refs[ref.name];
|
|
766
|
+
if (!cached) return ref;
|
|
767
|
+
const next: ResolvedRef = { ...ref };
|
|
768
|
+
if (cached.description !== undefined) next.description = cached.description;
|
|
769
|
+
if (cached.tools !== undefined) next.tools = cached.tools;
|
|
770
|
+
return next;
|
|
771
|
+
}
|
|
772
|
+
|
|
496
773
|
/**
|
|
497
774
|
* Store a secret value in a ref's config, encrypted if encryptionKey is set.
|
|
498
775
|
* The value is stored inline as "secret:<encrypted>" in consumer-config.json.
|
|
499
776
|
*/
|
|
500
|
-
async function storeRefSecret(
|
|
777
|
+
async function storeRefSecret(
|
|
778
|
+
name: string,
|
|
779
|
+
key: string,
|
|
780
|
+
value: string,
|
|
781
|
+
): Promise<void> {
|
|
501
782
|
const stored = options.encryptionKey
|
|
502
783
|
? `${SECRET_PREFIX}${await encryptSecret(value, options.encryptionKey)}`
|
|
503
784
|
: value;
|
|
@@ -510,13 +791,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
510
791
|
await writeConfig({ ...config, refs });
|
|
511
792
|
}
|
|
512
793
|
|
|
513
|
-
async function readRefSecret(
|
|
794
|
+
async function readRefSecret(
|
|
795
|
+
name: string,
|
|
796
|
+
key: string,
|
|
797
|
+
): Promise<string | null> {
|
|
514
798
|
const config = await readConfig();
|
|
515
799
|
const entry = findRef(config.refs ?? [], name);
|
|
516
800
|
const value = entry?.config?.[key];
|
|
517
801
|
if (typeof value !== "string") return null;
|
|
518
802
|
if (value.startsWith(SECRET_PREFIX) && options.encryptionKey) {
|
|
519
|
-
return decryptSecret(
|
|
803
|
+
return decryptSecret(
|
|
804
|
+
value.slice(SECRET_PREFIX.length),
|
|
805
|
+
options.encryptionKey,
|
|
806
|
+
);
|
|
520
807
|
}
|
|
521
808
|
return value;
|
|
522
809
|
}
|
|
@@ -601,23 +888,36 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
601
888
|
createdAt: number;
|
|
602
889
|
}
|
|
603
890
|
|
|
604
|
-
async function readPendingOAuth(): Promise<
|
|
891
|
+
async function readPendingOAuth(): Promise<
|
|
892
|
+
Record<string, PendingOAuthState>
|
|
893
|
+
> {
|
|
605
894
|
const content = await fs.readFile(PENDING_OAUTH_PATH);
|
|
606
895
|
if (!content) return {};
|
|
607
|
-
try {
|
|
896
|
+
try {
|
|
897
|
+
return JSON.parse(content);
|
|
898
|
+
} catch {
|
|
899
|
+
return {};
|
|
900
|
+
}
|
|
608
901
|
}
|
|
609
902
|
|
|
610
|
-
async function writePendingOAuth(
|
|
903
|
+
async function writePendingOAuth(
|
|
904
|
+
pending: Record<string, PendingOAuthState>,
|
|
905
|
+
): Promise<void> {
|
|
611
906
|
await fs.writeFile(PENDING_OAUTH_PATH, JSON.stringify(pending, null, 2));
|
|
612
907
|
}
|
|
613
908
|
|
|
614
|
-
async function storePendingOAuth(
|
|
909
|
+
async function storePendingOAuth(
|
|
910
|
+
state: string,
|
|
911
|
+
data: PendingOAuthState,
|
|
912
|
+
): Promise<void> {
|
|
615
913
|
const pending = await readPendingOAuth();
|
|
616
914
|
pending[state] = data;
|
|
617
915
|
await writePendingOAuth(pending);
|
|
618
916
|
}
|
|
619
917
|
|
|
620
|
-
async function consumePendingOAuth(
|
|
918
|
+
async function consumePendingOAuth(
|
|
919
|
+
state: string,
|
|
920
|
+
): Promise<PendingOAuthState | null> {
|
|
621
921
|
const pending = await readPendingOAuth();
|
|
622
922
|
const data = pending[state] ?? null;
|
|
623
923
|
if (data) {
|
|
@@ -646,7 +946,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
646
946
|
let reqId = 0;
|
|
647
947
|
let sessionId: string | undefined;
|
|
648
948
|
async function rpc(method: string, rpcParams?: Record<string, unknown>) {
|
|
649
|
-
const reqHeaders = {
|
|
949
|
+
const reqHeaders = {
|
|
950
|
+
...headers,
|
|
951
|
+
...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
|
|
952
|
+
};
|
|
650
953
|
const res = await globalThis.fetch(url, {
|
|
651
954
|
method: "POST",
|
|
652
955
|
headers: reqHeaders,
|
|
@@ -658,7 +961,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
658
961
|
}),
|
|
659
962
|
});
|
|
660
963
|
if (!res.ok) {
|
|
661
|
-
throw new Error(
|
|
964
|
+
throw new Error(
|
|
965
|
+
`MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`,
|
|
966
|
+
);
|
|
662
967
|
}
|
|
663
968
|
|
|
664
969
|
const contentType = res.headers.get("content-type") ?? "";
|
|
@@ -676,18 +981,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
676
981
|
try {
|
|
677
982
|
const json = JSON.parse(line.slice(6));
|
|
678
983
|
if (json.id === reqId) {
|
|
679
|
-
if (json.error)
|
|
984
|
+
if (json.error)
|
|
985
|
+
throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
680
986
|
return json.result;
|
|
681
987
|
}
|
|
682
988
|
} catch (e) {
|
|
683
|
-
if (e instanceof Error && e.message.startsWith("MCP RPC"))
|
|
989
|
+
if (e instanceof Error && e.message.startsWith("MCP RPC"))
|
|
990
|
+
throw e;
|
|
684
991
|
}
|
|
685
992
|
}
|
|
686
993
|
}
|
|
687
994
|
return undefined;
|
|
688
995
|
}
|
|
689
996
|
|
|
690
|
-
const json = await res.json() as {
|
|
997
|
+
const json = (await res.json()) as {
|
|
998
|
+
result?: unknown;
|
|
999
|
+
error?: { message: string };
|
|
1000
|
+
};
|
|
691
1001
|
if (json.error) throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
692
1002
|
return json.result;
|
|
693
1003
|
}
|
|
@@ -700,17 +1010,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
700
1010
|
});
|
|
701
1011
|
await rpc("notifications/initialized").catch(() => {});
|
|
702
1012
|
|
|
703
|
-
const result = await rpc("tools/call", {
|
|
704
|
-
|
|
1013
|
+
const result = (await rpc("tools/call", {
|
|
1014
|
+
name: toolName,
|
|
1015
|
+
arguments: params,
|
|
1016
|
+
})) as {
|
|
1017
|
+
content?: Array<{ type: string; text?: string }>;
|
|
1018
|
+
isError?: boolean;
|
|
1019
|
+
};
|
|
705
1020
|
|
|
706
1021
|
const textContent = result?.content?.find((c) => c.type === "text");
|
|
707
1022
|
if (textContent?.text) {
|
|
708
|
-
try {
|
|
709
|
-
|
|
1023
|
+
try {
|
|
1024
|
+
return {
|
|
1025
|
+
success: true,
|
|
1026
|
+
result: JSON.parse(textContent.text),
|
|
1027
|
+
} as CallAgentResponse;
|
|
1028
|
+
} catch {
|
|
1029
|
+
return {
|
|
1030
|
+
success: true,
|
|
1031
|
+
result: textContent.text,
|
|
1032
|
+
} as CallAgentResponse;
|
|
1033
|
+
}
|
|
710
1034
|
}
|
|
711
1035
|
return { success: true, result } as CallAgentResponse;
|
|
712
1036
|
} catch (err) {
|
|
713
|
-
return {
|
|
1037
|
+
return {
|
|
1038
|
+
success: false,
|
|
1039
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1040
|
+
} as CallAgentResponse;
|
|
714
1041
|
}
|
|
715
1042
|
}
|
|
716
1043
|
|
|
@@ -720,11 +1047,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
720
1047
|
}
|
|
721
1048
|
|
|
722
1049
|
/** Try fetching a URL directly as OAuth metadata (it may already be a discovery URL). */
|
|
723
|
-
async function tryFetchOAuthMetadata(
|
|
1050
|
+
async function tryFetchOAuthMetadata(
|
|
1051
|
+
url: string,
|
|
1052
|
+
): Promise<import("./mcp-client.js").OAuthServerMetadata | null> {
|
|
724
1053
|
try {
|
|
725
1054
|
const res = await globalThis.fetch(url);
|
|
726
1055
|
if (!res.ok) return null;
|
|
727
|
-
const data = await res.json() as Record<string, unknown>;
|
|
1056
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
728
1057
|
if (data.authorization_endpoint && data.token_endpoint) {
|
|
729
1058
|
return data as unknown as import("./mcp-client.js").OAuthServerMetadata;
|
|
730
1059
|
}
|
|
@@ -778,7 +1107,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
778
1107
|
* Build a consumer that includes the ref's sourceRegistry if present.
|
|
779
1108
|
* This ensures calls/inspect route to the correct registry endpoint.
|
|
780
1109
|
*/
|
|
781
|
-
async function buildConsumerForRef(
|
|
1110
|
+
async function buildConsumerForRef(
|
|
1111
|
+
entry: RefEntry,
|
|
1112
|
+
): Promise<RegistryConsumer> {
|
|
782
1113
|
const config = await readConfig();
|
|
783
1114
|
let registries = config.registries ?? [];
|
|
784
1115
|
|
|
@@ -816,7 +1147,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
816
1147
|
* Resolve the correct registry for a ref.
|
|
817
1148
|
* If the ref has a sourceRegistry, use that; otherwise fall back to the first registry.
|
|
818
1149
|
*/
|
|
819
|
-
function resolveRegistryForRef(
|
|
1150
|
+
function resolveRegistryForRef(
|
|
1151
|
+
consumer: RegistryConsumer,
|
|
1152
|
+
entry: RefEntry,
|
|
1153
|
+
): ResolvedRegistry {
|
|
820
1154
|
const regs = consumer.registries();
|
|
821
1155
|
if (entry.sourceRegistry?.url) {
|
|
822
1156
|
const match = regs.find((r) => r.url === entry.sourceRegistry!.url);
|
|
@@ -837,7 +1171,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
837
1171
|
* the ref is sourced from a raw URL (no registry), in which case proxy routing
|
|
838
1172
|
* does not apply.
|
|
839
1173
|
*/
|
|
840
|
-
async function findRegistryEntryForRef(
|
|
1174
|
+
async function findRegistryEntryForRef(
|
|
1175
|
+
entry: RefEntry,
|
|
1176
|
+
): Promise<RegistryEntry | null> {
|
|
841
1177
|
const sourceUrl = entry.sourceRegistry?.url;
|
|
842
1178
|
if (!sourceUrl) return null;
|
|
843
1179
|
const config = await readConfig();
|
|
@@ -890,7 +1226,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
890
1226
|
sourceRegistry: { url: reg.url, agentPath: agent },
|
|
891
1227
|
});
|
|
892
1228
|
const resolved = consumer.registries().find((r) => r.url === reg.url);
|
|
893
|
-
if (!resolved)
|
|
1229
|
+
if (!resolved)
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
`Registry ${reg.url} not resolvable for proxy forwarding`,
|
|
1232
|
+
);
|
|
894
1233
|
|
|
895
1234
|
const response = await consumer.callRegistry(resolved, {
|
|
896
1235
|
action: "execute_tool",
|
|
@@ -900,8 +1239,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
900
1239
|
});
|
|
901
1240
|
|
|
902
1241
|
if (!response.success) {
|
|
903
|
-
const errResponse = response as {
|
|
904
|
-
|
|
1242
|
+
const errResponse = response as {
|
|
1243
|
+
success: false;
|
|
1244
|
+
error?: string;
|
|
1245
|
+
code?: string;
|
|
1246
|
+
};
|
|
1247
|
+
const msg =
|
|
1248
|
+
errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
|
|
905
1249
|
throw new Error(msg);
|
|
906
1250
|
}
|
|
907
1251
|
return (response as { success: true; result: unknown }).result as T;
|
|
@@ -970,7 +1314,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
970
1314
|
const rName = registryDisplayName(r);
|
|
971
1315
|
if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl) return r;
|
|
972
1316
|
found = true;
|
|
973
|
-
const existing: RegistryEntry =
|
|
1317
|
+
const existing: RegistryEntry =
|
|
1318
|
+
typeof r === "string" ? { url: r } : { ...r };
|
|
974
1319
|
mutate(existing);
|
|
975
1320
|
return existing;
|
|
976
1321
|
});
|
|
@@ -983,11 +1328,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
983
1328
|
* Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
|
|
984
1329
|
* values pass through unchanged so dev configs keep working.
|
|
985
1330
|
*/
|
|
986
|
-
async function revealSecret(
|
|
1331
|
+
async function revealSecret(
|
|
1332
|
+
value: string | undefined,
|
|
1333
|
+
): Promise<string | undefined> {
|
|
987
1334
|
if (!value) return value;
|
|
988
1335
|
if (!value.startsWith(SECRET_PREFIX)) return value;
|
|
989
1336
|
if (!options.encryptionKey) return undefined;
|
|
990
|
-
return decryptSecret(
|
|
1337
|
+
return decryptSecret(
|
|
1338
|
+
value.slice(SECRET_PREFIX.length),
|
|
1339
|
+
options.encryptionKey,
|
|
1340
|
+
);
|
|
991
1341
|
}
|
|
992
1342
|
|
|
993
1343
|
/**
|
|
@@ -1002,7 +1352,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1002
1352
|
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
1003
1353
|
if (!target || typeof target === "string") return false;
|
|
1004
1354
|
const oauth = target.oauth;
|
|
1005
|
-
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
|
|
1355
|
+
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
|
|
1356
|
+
return false;
|
|
1006
1357
|
|
|
1007
1358
|
const refreshToken = await revealSecret(oauth.refreshToken);
|
|
1008
1359
|
const clientSecret = await revealSecret(oauth.clientSecret);
|
|
@@ -1044,7 +1395,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1044
1395
|
try {
|
|
1045
1396
|
return await fn();
|
|
1046
1397
|
} catch (err) {
|
|
1047
|
-
if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
|
|
1398
|
+
if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
|
|
1399
|
+
throw err;
|
|
1048
1400
|
let refreshed = false;
|
|
1049
1401
|
try {
|
|
1050
1402
|
refreshed = await refreshRegistryToken(nameOrUrl);
|
|
@@ -1088,7 +1440,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1088
1440
|
}
|
|
1089
1441
|
|
|
1090
1442
|
const registry: AdkRegistryApi = {
|
|
1091
|
-
async add(
|
|
1443
|
+
async add(
|
|
1444
|
+
entry: RegistryEntry,
|
|
1445
|
+
): Promise<{ authRequirement?: RegistryAuthRequirement }> {
|
|
1092
1446
|
const config = await readConfig();
|
|
1093
1447
|
const alias = entry.name ?? entry.url;
|
|
1094
1448
|
const registries = (config.registries ?? []).filter(
|
|
@@ -1135,7 +1489,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1135
1489
|
...final,
|
|
1136
1490
|
proxy: {
|
|
1137
1491
|
mode: discovered.proxy.mode,
|
|
1138
|
-
...(discovered.proxy.agent && {
|
|
1492
|
+
...(discovered.proxy.agent && {
|
|
1493
|
+
agent: discovered.proxy.agent,
|
|
1494
|
+
}),
|
|
1139
1495
|
},
|
|
1140
1496
|
};
|
|
1141
1497
|
}
|
|
@@ -1156,7 +1512,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1156
1512
|
if (!config.registries?.length) return false;
|
|
1157
1513
|
const before = config.registries.length;
|
|
1158
1514
|
const registries = config.registries.filter(
|
|
1159
|
-
(r) =>
|
|
1515
|
+
(r) =>
|
|
1516
|
+
registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
|
|
1160
1517
|
);
|
|
1161
1518
|
if (registries.length === before) return false;
|
|
1162
1519
|
await writeConfig({ ...config, registries });
|
|
@@ -1177,7 +1534,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1177
1534
|
return typeof target === "string" ? { url: target } : target;
|
|
1178
1535
|
},
|
|
1179
1536
|
|
|
1180
|
-
async update(
|
|
1537
|
+
async update(
|
|
1538
|
+
name: string,
|
|
1539
|
+
updates: Partial<RegistryEntry>,
|
|
1540
|
+
): Promise<boolean> {
|
|
1181
1541
|
const config = await readConfig();
|
|
1182
1542
|
if (!config.registries?.length) return false;
|
|
1183
1543
|
let found = false;
|
|
@@ -1185,11 +1545,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1185
1545
|
const rName = registryDisplayName(r);
|
|
1186
1546
|
if (rName !== name && registryUrl(r) !== name) return r;
|
|
1187
1547
|
found = true;
|
|
1188
|
-
const existing: RegistryEntry =
|
|
1548
|
+
const existing: RegistryEntry =
|
|
1549
|
+
typeof r === "string" ? { url: r } : { ...r };
|
|
1189
1550
|
if (updates.url) existing.url = updates.url;
|
|
1190
1551
|
if (updates.name) existing.name = updates.name;
|
|
1191
1552
|
if (updates.auth) existing.auth = updates.auth;
|
|
1192
|
-
if (updates.headers)
|
|
1553
|
+
if (updates.headers)
|
|
1554
|
+
existing.headers = { ...existing.headers, ...updates.headers };
|
|
1193
1555
|
if (updates.proxy !== undefined) existing.proxy = updates.proxy;
|
|
1194
1556
|
return existing;
|
|
1195
1557
|
});
|
|
@@ -1201,7 +1563,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1201
1563
|
async browse(name: string, query?: string): Promise<AgentListEntry[]> {
|
|
1202
1564
|
const config = await readConfig();
|
|
1203
1565
|
const target = findRegistry(config.registries ?? [], name);
|
|
1204
|
-
if (target && typeof target !== "string")
|
|
1566
|
+
if (target && typeof target !== "string")
|
|
1567
|
+
assertRegistryAuthorized(target);
|
|
1205
1568
|
return callWithRefresh(name, async () => {
|
|
1206
1569
|
const consumer = await buildConsumer(name);
|
|
1207
1570
|
const url = target ? registryUrl(target) : name;
|
|
@@ -1212,7 +1575,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1212
1575
|
async inspect(name: string): Promise<RegistryConfiguration> {
|
|
1213
1576
|
const config = await readConfig();
|
|
1214
1577
|
const target = findRegistry(config.registries ?? [], name);
|
|
1215
|
-
if (target && typeof target !== "string")
|
|
1578
|
+
if (target && typeof target !== "string")
|
|
1579
|
+
assertRegistryAuthorized(target);
|
|
1216
1580
|
return callWithRefresh(name, async () => {
|
|
1217
1581
|
const consumer = await buildConsumer(name);
|
|
1218
1582
|
const url = target ? registryUrl(target) : name;
|
|
@@ -1224,7 +1588,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1224
1588
|
const config = await readConfig();
|
|
1225
1589
|
const registries = config.registries ?? [];
|
|
1226
1590
|
const targets = name
|
|
1227
|
-
? registries.filter(
|
|
1591
|
+
? registries.filter(
|
|
1592
|
+
(r) => registryDisplayName(r) === name || registryUrl(r) === name,
|
|
1593
|
+
)
|
|
1228
1594
|
: registries;
|
|
1229
1595
|
|
|
1230
1596
|
const results = await Promise.allSettled(
|
|
@@ -1265,7 +1631,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1265
1631
|
return results.map((r) =>
|
|
1266
1632
|
r.status === "fulfilled"
|
|
1267
1633
|
? r.value
|
|
1268
|
-
: {
|
|
1634
|
+
: {
|
|
1635
|
+
name: "unknown",
|
|
1636
|
+
url: "unknown",
|
|
1637
|
+
status: "error" as const,
|
|
1638
|
+
error: "unknown",
|
|
1639
|
+
},
|
|
1269
1640
|
);
|
|
1270
1641
|
},
|
|
1271
1642
|
|
|
@@ -1337,7 +1708,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1337
1708
|
});
|
|
1338
1709
|
// Re-read so the flow below sees the fresh requirement.
|
|
1339
1710
|
const refreshed = await readConfig();
|
|
1340
|
-
const refreshedTarget = findRegistry(
|
|
1711
|
+
const refreshedTarget = findRegistry(
|
|
1712
|
+
refreshed.registries ?? [],
|
|
1713
|
+
nameOrUrl,
|
|
1714
|
+
);
|
|
1341
1715
|
if (refreshedTarget && typeof refreshedTarget !== "string") {
|
|
1342
1716
|
Object.assign(target, refreshedTarget);
|
|
1343
1717
|
}
|
|
@@ -1403,17 +1777,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1403
1777
|
);
|
|
1404
1778
|
|
|
1405
1779
|
const state = crypto.randomUUID();
|
|
1406
|
-
const { url: authorizeUrl, codeVerifier } =
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1780
|
+
const { url: authorizeUrl, codeVerifier } =
|
|
1781
|
+
await buildOAuthAuthorizeUrl({
|
|
1782
|
+
authorizationEndpoint: metadata.authorization_endpoint,
|
|
1783
|
+
clientId: registration.clientId,
|
|
1784
|
+
redirectUri,
|
|
1785
|
+
scopes: req.scopes,
|
|
1786
|
+
state,
|
|
1787
|
+
});
|
|
1413
1788
|
|
|
1414
1789
|
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1415
1790
|
const server = createServer(async (reqIn, resOut) => {
|
|
1416
|
-
const reqUrl = new URL(
|
|
1791
|
+
const reqUrl = new URL(
|
|
1792
|
+
reqIn.url ?? "/",
|
|
1793
|
+
`http://localhost:${port}`,
|
|
1794
|
+
);
|
|
1417
1795
|
if (reqUrl.pathname !== "/callback") {
|
|
1418
1796
|
resOut.writeHead(404);
|
|
1419
1797
|
resOut.end();
|
|
@@ -1423,7 +1801,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1423
1801
|
const code = reqUrl.searchParams.get("code");
|
|
1424
1802
|
const returnedState = reqUrl.searchParams.get("state");
|
|
1425
1803
|
if (!code || returnedState !== state) {
|
|
1426
|
-
const error =
|
|
1804
|
+
const error =
|
|
1805
|
+
reqUrl.searchParams.get("error") ?? "missing code/state";
|
|
1427
1806
|
resOut.writeHead(400, { "Content-Type": "text/html" });
|
|
1428
1807
|
resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
|
|
1429
1808
|
server.close();
|
|
@@ -1439,13 +1818,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1439
1818
|
}
|
|
1440
1819
|
|
|
1441
1820
|
try {
|
|
1442
|
-
const tokens = await exchangeCodeForTokens(
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1821
|
+
const tokens = await exchangeCodeForTokens(
|
|
1822
|
+
metadata.token_endpoint,
|
|
1823
|
+
{
|
|
1824
|
+
code,
|
|
1825
|
+
codeVerifier,
|
|
1826
|
+
clientId: registration.clientId,
|
|
1827
|
+
clientSecret: registration.clientSecret,
|
|
1828
|
+
redirectUri,
|
|
1829
|
+
},
|
|
1830
|
+
);
|
|
1449
1831
|
const expiresAt = tokens.expiresIn
|
|
1450
1832
|
? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
|
|
1451
1833
|
: undefined;
|
|
@@ -1527,7 +1909,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1527
1909
|
const token = params.get("token");
|
|
1528
1910
|
if (!token) {
|
|
1529
1911
|
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1530
|
-
resOut.end(
|
|
1912
|
+
resOut.end(
|
|
1913
|
+
renderCredentialForm(displayName, fields, "Token is required."),
|
|
1914
|
+
);
|
|
1531
1915
|
return;
|
|
1532
1916
|
}
|
|
1533
1917
|
try {
|
|
@@ -1571,7 +1955,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1571
1955
|
// ==========================================
|
|
1572
1956
|
|
|
1573
1957
|
const ref: AdkRefApi = {
|
|
1574
|
-
async add(
|
|
1958
|
+
async add(
|
|
1959
|
+
entryInput: RefAddInput,
|
|
1960
|
+
): Promise<{ security: SecuritySchemeSummary | null }> {
|
|
1575
1961
|
let security: SecuritySchemeSummary | null = null;
|
|
1576
1962
|
|
|
1577
1963
|
const config = await readConfig();
|
|
@@ -1593,7 +1979,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1593
1979
|
if (entry.sourceRegistry?.url) {
|
|
1594
1980
|
entry = { ...entry, scheme: "registry" };
|
|
1595
1981
|
} else if (entry.url) {
|
|
1596
|
-
entry = {
|
|
1982
|
+
entry = {
|
|
1983
|
+
...entry,
|
|
1984
|
+
scheme: entry.url.startsWith("http") ? "https" : "mcp",
|
|
1985
|
+
};
|
|
1597
1986
|
} else {
|
|
1598
1987
|
throw new AdkError({
|
|
1599
1988
|
code: "REF_INVALID",
|
|
@@ -1623,6 +2012,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1623
2012
|
});
|
|
1624
2013
|
}
|
|
1625
2014
|
|
|
2015
|
+
let cacheEntry: RegistryCacheEntry | undefined;
|
|
1626
2016
|
if (hasRegistries || entry.sourceRegistry?.url) {
|
|
1627
2017
|
try {
|
|
1628
2018
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1631,11 +2021,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1631
2021
|
|
|
1632
2022
|
const requiresValidation = !!entry.sourceRegistry;
|
|
1633
2023
|
if (requiresValidation) {
|
|
1634
|
-
const hasContent =
|
|
1635
|
-
info
|
|
1636
|
-
(info.
|
|
1637
|
-
|
|
1638
|
-
|
|
2024
|
+
const hasContent =
|
|
2025
|
+
info &&
|
|
2026
|
+
(info.description ||
|
|
2027
|
+
(info.tools && info.tools.length > 0) ||
|
|
2028
|
+
(info.toolSummaries && info.toolSummaries.length > 0));
|
|
1639
2029
|
if (!hasContent) {
|
|
1640
2030
|
// Inspect returned empty — fall back to browse to check if agent exists
|
|
1641
2031
|
const registryUrl = entry.sourceRegistry?.url;
|
|
@@ -1644,7 +2034,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1644
2034
|
try {
|
|
1645
2035
|
const agents = await consumer.browse(registryUrl);
|
|
1646
2036
|
const stripAt = (s: string) => s.replace(/^@/, "");
|
|
1647
|
-
const refKey = stripAt(
|
|
2037
|
+
const refKey = stripAt(
|
|
2038
|
+
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
2039
|
+
);
|
|
1648
2040
|
foundInBrowse = agents.some(
|
|
1649
2041
|
(a) => a.path === entry.ref || stripAt(a.path) === refKey,
|
|
1650
2042
|
);
|
|
@@ -1658,7 +2050,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1658
2050
|
code: "REF_NOT_FOUND",
|
|
1659
2051
|
message: `Agent "${entry.ref}" not found on ${registryHint}`,
|
|
1660
2052
|
hint: "Check available agents with: adk registry browse",
|
|
1661
|
-
details: {
|
|
2053
|
+
details: {
|
|
2054
|
+
ref: entry.ref,
|
|
2055
|
+
sourceRegistry: entry.sourceRegistry,
|
|
2056
|
+
scheme: entry.scheme,
|
|
2057
|
+
},
|
|
1662
2058
|
});
|
|
1663
2059
|
}
|
|
1664
2060
|
}
|
|
@@ -1667,17 +2063,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1667
2063
|
if (info?.security) security = info.security;
|
|
1668
2064
|
const agentMode = (info as any)?.mode;
|
|
1669
2065
|
if (agentMode) (entry as any).mode = agentMode;
|
|
1670
|
-
if (info?.upstream && !entry.url && agentMode !==
|
|
2066
|
+
if (info?.upstream && !entry.url && agentMode !== "api") {
|
|
1671
2067
|
entry.url = info.upstream as string;
|
|
1672
2068
|
entry.scheme = entry.scheme ?? "mcp";
|
|
1673
2069
|
}
|
|
2070
|
+
|
|
2071
|
+
cacheEntry = buildCacheEntry(entry.ref, info);
|
|
1674
2072
|
} catch (err) {
|
|
1675
2073
|
if (err instanceof AdkError) throw err;
|
|
1676
2074
|
throw new AdkError({
|
|
1677
2075
|
code: "REGISTRY_UNREACHABLE",
|
|
1678
2076
|
message: `Could not reach registry to validate "${entry.ref}"`,
|
|
1679
2077
|
hint: "Check your registry connection with: adk registry test",
|
|
1680
|
-
details: {
|
|
2078
|
+
details: {
|
|
2079
|
+
ref: entry.ref,
|
|
2080
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2081
|
+
},
|
|
1681
2082
|
cause: err,
|
|
1682
2083
|
});
|
|
1683
2084
|
}
|
|
@@ -1685,6 +2086,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1685
2086
|
|
|
1686
2087
|
const refs = [...(config.refs ?? []), entry];
|
|
1687
2088
|
await writeConfig({ ...config, refs });
|
|
2089
|
+
await upsertRegistryCacheEntry(name, cacheEntry);
|
|
1688
2090
|
|
|
1689
2091
|
return { security };
|
|
1690
2092
|
},
|
|
@@ -1696,17 +2098,28 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1696
2098
|
const refs = config.refs.filter((r) => !refNameMatches(r, name));
|
|
1697
2099
|
if (refs.length === before) return false;
|
|
1698
2100
|
await writeConfig({ ...config, refs });
|
|
2101
|
+
await removeRegistryCacheEntry(name);
|
|
1699
2102
|
return true;
|
|
1700
2103
|
},
|
|
1701
2104
|
|
|
1702
2105
|
async list(): Promise<ResolvedRef[]> {
|
|
1703
|
-
const config = await
|
|
1704
|
-
|
|
2106
|
+
const [config, cache] = await Promise.all([
|
|
2107
|
+
readConfig(),
|
|
2108
|
+
readRegistryCache(),
|
|
2109
|
+
]);
|
|
2110
|
+
return (config.refs ?? [])
|
|
2111
|
+
.map(normalizeRef)
|
|
2112
|
+
.map((r) => hydrateFromCache(r, cache));
|
|
1705
2113
|
},
|
|
1706
2114
|
|
|
1707
2115
|
async get(name: string): Promise<ResolvedRef | null> {
|
|
1708
|
-
const config = await
|
|
1709
|
-
|
|
2116
|
+
const [config, cache] = await Promise.all([
|
|
2117
|
+
readConfig(),
|
|
2118
|
+
readRegistryCache(),
|
|
2119
|
+
]);
|
|
2120
|
+
const found = findRef(config.refs ?? [], name);
|
|
2121
|
+
if (!found) return null;
|
|
2122
|
+
return hydrateFromCache(found, cache);
|
|
1710
2123
|
},
|
|
1711
2124
|
|
|
1712
2125
|
async update(name: string, updates: Partial<RefEntry>): Promise<boolean> {
|
|
@@ -1735,8 +2148,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1735
2148
|
updated.name = updates.name;
|
|
1736
2149
|
}
|
|
1737
2150
|
if (updates.scheme) updated.scheme = updates.scheme;
|
|
1738
|
-
if (updates.config)
|
|
1739
|
-
|
|
2151
|
+
if (updates.config)
|
|
2152
|
+
updated.config = { ...updated.config, ...updates.config };
|
|
2153
|
+
if (updates.sourceRegistry)
|
|
2154
|
+
updated.sourceRegistry = updates.sourceRegistry;
|
|
1740
2155
|
return updated;
|
|
1741
2156
|
});
|
|
1742
2157
|
if (!found) return false;
|
|
@@ -1744,38 +2159,63 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1744
2159
|
return true;
|
|
1745
2160
|
},
|
|
1746
2161
|
|
|
1747
|
-
async inspect(
|
|
2162
|
+
async inspect(
|
|
2163
|
+
name: string,
|
|
2164
|
+
opts?: { full?: boolean },
|
|
2165
|
+
): Promise<AgentInspection | null> {
|
|
1748
2166
|
const config = await readConfig();
|
|
1749
2167
|
const entry = findRef(config.refs ?? [], name);
|
|
1750
2168
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
1751
2169
|
|
|
1752
2170
|
const consumer = await buildConsumerForRef(entry);
|
|
1753
|
-
|
|
2171
|
+
const result = await consumer.inspect(
|
|
1754
2172
|
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
1755
2173
|
entry.sourceRegistry?.url,
|
|
1756
2174
|
opts,
|
|
1757
2175
|
);
|
|
2176
|
+
|
|
2177
|
+
// Side-effect: refresh the registry cache so subsequent ref.list()
|
|
2178
|
+
// / ref.get() calls see the latest description and tool summaries
|
|
2179
|
+
// without another network round-trip. Strips inputSchema (caller's
|
|
2180
|
+
// `result` is unaffected — it still carries the full data).
|
|
2181
|
+
await upsertRegistryCacheEntry(name, buildCacheEntry(entry.ref, result));
|
|
2182
|
+
|
|
2183
|
+
return result;
|
|
1758
2184
|
},
|
|
1759
2185
|
|
|
1760
|
-
async call(
|
|
2186
|
+
async call(
|
|
2187
|
+
name: string,
|
|
2188
|
+
tool: string,
|
|
2189
|
+
params?: Record<string, unknown>,
|
|
2190
|
+
): Promise<CallAgentResponse> {
|
|
1761
2191
|
const config = await readConfig();
|
|
1762
2192
|
const entry = findRef(config.refs ?? [], name);
|
|
1763
2193
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
1764
2194
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2195
|
+
const accessToken =
|
|
2196
|
+
(await readRefSecret(name, "access_token")) ??
|
|
2197
|
+
(await readRefSecret(name, "api_key")) ??
|
|
2198
|
+
(await readRefSecret(name, "token"));
|
|
1768
2199
|
|
|
1769
2200
|
// Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
|
|
1770
2201
|
const refConfig = (entry.config ?? {}) as Record<string, unknown>;
|
|
1771
|
-
const rawHeaders = refConfig.headers as
|
|
2202
|
+
const rawHeaders = refConfig.headers as
|
|
2203
|
+
| Record<string, string>
|
|
2204
|
+
| undefined;
|
|
1772
2205
|
let resolvedHeaders: Record<string, string> | undefined;
|
|
1773
|
-
if (rawHeaders && typeof rawHeaders ===
|
|
2206
|
+
if (rawHeaders && typeof rawHeaders === "object") {
|
|
1774
2207
|
resolvedHeaders = {};
|
|
1775
2208
|
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
1776
|
-
if (
|
|
1777
|
-
|
|
1778
|
-
|
|
2209
|
+
if (
|
|
2210
|
+
typeof v === "string" &&
|
|
2211
|
+
v.startsWith(SECRET_PREFIX) &&
|
|
2212
|
+
options.encryptionKey
|
|
2213
|
+
) {
|
|
2214
|
+
resolvedHeaders[k] = await decryptSecret(
|
|
2215
|
+
v.slice(SECRET_PREFIX.length),
|
|
2216
|
+
options.encryptionKey,
|
|
2217
|
+
);
|
|
2218
|
+
} else if (typeof v === "string") {
|
|
1779
2219
|
resolvedHeaders[k] = v;
|
|
1780
2220
|
}
|
|
1781
2221
|
}
|
|
@@ -1784,9 +2224,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1784
2224
|
const doCall = async (token: string | null) => {
|
|
1785
2225
|
// Direct MCP only for redirect/proxy agents with an MCP upstream.
|
|
1786
2226
|
// API-mode agents must go through the registry (it does REST translation).
|
|
1787
|
-
const agentMode = (entry as any).mode ??
|
|
1788
|
-
if (token && entry.url && agentMode !==
|
|
1789
|
-
return callMcpDirect(
|
|
2227
|
+
const agentMode = (entry as any).mode ?? "redirect";
|
|
2228
|
+
if (token && entry.url && agentMode !== "api") {
|
|
2229
|
+
return callMcpDirect(
|
|
2230
|
+
entry.url,
|
|
2231
|
+
tool,
|
|
2232
|
+
params ?? {},
|
|
2233
|
+
token,
|
|
2234
|
+
resolvedHeaders,
|
|
2235
|
+
);
|
|
1790
2236
|
}
|
|
1791
2237
|
|
|
1792
2238
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1855,13 +2301,20 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1855
2301
|
// server-side so local inspection would always return "missing").
|
|
1856
2302
|
const proxy = await resolveProxyForRef(entry);
|
|
1857
2303
|
if (proxy) {
|
|
1858
|
-
return forwardRefOpToProxy<RefAuthStatus>(
|
|
2304
|
+
return forwardRefOpToProxy<RefAuthStatus>(
|
|
2305
|
+
proxy.reg,
|
|
2306
|
+
proxy.agent,
|
|
2307
|
+
"auth-status",
|
|
2308
|
+
{ name },
|
|
2309
|
+
);
|
|
1859
2310
|
}
|
|
1860
2311
|
|
|
1861
2312
|
let security: SecuritySchemeSummary | null = null;
|
|
1862
2313
|
try {
|
|
1863
2314
|
const consumer = await buildConsumerForRef(entry);
|
|
1864
|
-
const info = await consumer.inspect(
|
|
2315
|
+
const info = await consumer.inspect(
|
|
2316
|
+
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
2317
|
+
);
|
|
1865
2318
|
if (info?.security) security = info.security;
|
|
1866
2319
|
} catch {
|
|
1867
2320
|
// Can't reach registry
|
|
@@ -1889,12 +2342,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1889
2342
|
};
|
|
1890
2343
|
const hasRegistration = !!securityExt.dynamicRegistration;
|
|
1891
2344
|
|
|
1892
|
-
let oauthMetadata:
|
|
2345
|
+
let oauthMetadata:
|
|
2346
|
+
| import("./mcp-client.js").OAuthServerMetadata
|
|
2347
|
+
| null = null;
|
|
1893
2348
|
let needsSecret = false;
|
|
1894
2349
|
if (securityExt.discoveryUrl) {
|
|
1895
2350
|
oauthMetadata = await tryFetchOAuthMetadata(securityExt.discoveryUrl);
|
|
1896
2351
|
if (oauthMetadata) {
|
|
1897
|
-
const authMethods =
|
|
2352
|
+
const authMethods =
|
|
2353
|
+
oauthMetadata.token_endpoint_auth_methods_supported ?? [];
|
|
1898
2354
|
needsSecret = !authMethods.includes("none");
|
|
1899
2355
|
}
|
|
1900
2356
|
}
|
|
@@ -1921,15 +2377,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1921
2377
|
};
|
|
1922
2378
|
} else if (security.type === "apiKey") {
|
|
1923
2379
|
const apiKeySec = security as {
|
|
1924
|
-
name?: string;
|
|
2380
|
+
name?: string;
|
|
2381
|
+
headers?: Record<string, { description?: string }>;
|
|
1925
2382
|
};
|
|
1926
2383
|
const toStorageKey = (headerName: string) =>
|
|
1927
|
-
headerName
|
|
2384
|
+
headerName
|
|
2385
|
+
.toLowerCase()
|
|
2386
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
2387
|
+
.replace(/^_|_$/g, "");
|
|
1928
2388
|
|
|
1929
2389
|
// config.headers: { "Header-Name": "value" } — check by header name (case-insensitive)
|
|
1930
|
-
const configHeaders = (
|
|
1931
|
-
Record<string, unknown> | undefined
|
|
1932
|
-
|
|
2390
|
+
const configHeaders = (
|
|
2391
|
+
entry?.config as Record<string, unknown> | undefined
|
|
2392
|
+
)?.headers as Record<string, unknown> | undefined;
|
|
2393
|
+
const configHeaderKeys = configHeaders
|
|
2394
|
+
? Object.keys(configHeaders)
|
|
2395
|
+
: [];
|
|
1933
2396
|
const hasConfigHeader = (name: string) =>
|
|
1934
2397
|
configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
|
|
1935
2398
|
|
|
@@ -1943,7 +2406,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1943
2406
|
for (const headerName of declaredHeaders) {
|
|
1944
2407
|
const storageKey = toStorageKey(headerName);
|
|
1945
2408
|
const inConfigHeaders = hasConfigHeader(headerName);
|
|
1946
|
-
const inLegacyKeys =
|
|
2409
|
+
const inLegacyKeys =
|
|
2410
|
+
configKeys.includes(storageKey) || configKeys.includes("api_key");
|
|
1947
2411
|
fields[storageKey] = {
|
|
1948
2412
|
required: true,
|
|
1949
2413
|
automated: false,
|
|
@@ -1974,22 +2438,40 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1974
2438
|
(f) => !f.required || f.present || f.resolvable,
|
|
1975
2439
|
);
|
|
1976
2440
|
|
|
2441
|
+
// Persist the slim {required, automated} per-field shape into the
|
|
2442
|
+
// registry cache so `isRefAuthComplete` can answer subsequent
|
|
2443
|
+
// host-side "is this ref ready?" checks without re-fetching the
|
|
2444
|
+
// security scheme. We deliberately omit `present`/`resolvable`
|
|
2445
|
+
// because those are computed against the current entry.config and
|
|
2446
|
+
// host environment — caching them would go stale immediately.
|
|
2447
|
+
const authFields: Record<string, RegistryCacheAuthField> = {};
|
|
2448
|
+
for (const [field, info] of Object.entries(fields)) {
|
|
2449
|
+
authFields[field] = {
|
|
2450
|
+
required: info.required,
|
|
2451
|
+
automated: info.automated,
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
|
|
2455
|
+
|
|
1977
2456
|
return { name, security, complete, fields };
|
|
1978
2457
|
},
|
|
1979
2458
|
|
|
1980
|
-
async auth(
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2459
|
+
async auth(
|
|
2460
|
+
name: string,
|
|
2461
|
+
opts?: {
|
|
2462
|
+
apiKey?: string;
|
|
2463
|
+
credentials?: Record<string, string>;
|
|
2464
|
+
/** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
|
|
2465
|
+
stateContext?: Record<string, unknown>;
|
|
2466
|
+
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
2467
|
+
scopes?: string[];
|
|
2468
|
+
/**
|
|
2469
|
+
* Opt out of proxy routing when the ref's source registry has
|
|
2470
|
+
* `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
|
|
2471
|
+
*/
|
|
2472
|
+
preferLocal?: boolean;
|
|
2473
|
+
},
|
|
2474
|
+
): Promise<AuthStartResult> {
|
|
1993
2475
|
const config = await readConfig();
|
|
1994
2476
|
const entry = findRef(config.refs ?? [], name);
|
|
1995
2477
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
@@ -1998,14 +2480,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1998
2480
|
// agent. The registry owns the client_id/secret and returns an authorize
|
|
1999
2481
|
// URL pointing at the registry's OAuth callback domain, so the user
|
|
2000
2482
|
// completes the flow against the registry instead of localhost.
|
|
2001
|
-
const proxy = await resolveProxyForRef(entry, {
|
|
2483
|
+
const proxy = await resolveProxyForRef(entry, {
|
|
2484
|
+
preferLocal: opts?.preferLocal,
|
|
2485
|
+
});
|
|
2002
2486
|
if (proxy) {
|
|
2003
2487
|
const params: Record<string, unknown> = { name };
|
|
2004
2488
|
if (opts?.apiKey !== undefined) params.apiKey = opts.apiKey;
|
|
2005
2489
|
if (opts?.credentials) params.credentials = opts.credentials;
|
|
2006
2490
|
if (opts?.scopes) params.scopes = opts.scopes;
|
|
2007
2491
|
if (opts?.stateContext) params.stateContext = opts.stateContext;
|
|
2008
|
-
return forwardRefOpToProxy<AuthStartResult>(
|
|
2492
|
+
return forwardRefOpToProxy<AuthStartResult>(
|
|
2493
|
+
proxy.reg,
|
|
2494
|
+
proxy.agent,
|
|
2495
|
+
"auth",
|
|
2496
|
+
params,
|
|
2497
|
+
);
|
|
2009
2498
|
}
|
|
2010
2499
|
|
|
2011
2500
|
const status = await ref.authStatus(name);
|
|
@@ -2018,20 +2507,31 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2018
2507
|
|
|
2019
2508
|
if (security.type === "apiKey") {
|
|
2020
2509
|
const apiKeySec = security as {
|
|
2021
|
-
name?: string;
|
|
2510
|
+
name?: string;
|
|
2511
|
+
prefix?: string;
|
|
2022
2512
|
headers?: Record<string, { description?: string }>;
|
|
2023
2513
|
};
|
|
2024
2514
|
|
|
2025
2515
|
const toStorageKey = (headerName: string) =>
|
|
2026
|
-
headerName
|
|
2516
|
+
headerName
|
|
2517
|
+
.toLowerCase()
|
|
2518
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
2519
|
+
.replace(/^_|_$/g, "");
|
|
2027
2520
|
|
|
2028
2521
|
// Check existing config.headers
|
|
2029
|
-
const existingHeaders = (
|
|
2030
|
-
Record<string,
|
|
2522
|
+
const existingHeaders = (
|
|
2523
|
+
(entry.config ?? {}) as Record<string, unknown>
|
|
2524
|
+
).headers as Record<string, string> | undefined;
|
|
2031
2525
|
|
|
2032
2526
|
// Collect declared headers: from security.headers or security.name
|
|
2033
|
-
const declaredHeaders: Array<{
|
|
2034
|
-
|
|
2527
|
+
const declaredHeaders: Array<{
|
|
2528
|
+
headerName: string;
|
|
2529
|
+
description?: string;
|
|
2530
|
+
}> = apiKeySec.headers
|
|
2531
|
+
? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
|
|
2532
|
+
headerName: h,
|
|
2533
|
+
description: meta.description,
|
|
2534
|
+
}))
|
|
2035
2535
|
: apiKeySec.name
|
|
2036
2536
|
? [{ headerName: apiKeySec.name }]
|
|
2037
2537
|
: [];
|
|
@@ -2043,12 +2543,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2043
2543
|
for (const { headerName, description } of declaredHeaders) {
|
|
2044
2544
|
const storageKey = toStorageKey(headerName);
|
|
2045
2545
|
// Check: credentials param → existing config.headers → legacy config key → resolve callback
|
|
2046
|
-
const value =
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2546
|
+
const value =
|
|
2547
|
+
opts?.credentials?.[storageKey] ??
|
|
2548
|
+
opts?.credentials?.[headerName] ??
|
|
2549
|
+
(existingHeaders &&
|
|
2550
|
+
Object.entries(existingHeaders).find(
|
|
2551
|
+
([k]) => k.toLowerCase() === headerName.toLowerCase(),
|
|
2552
|
+
)?.[1]) ??
|
|
2553
|
+
opts?.apiKey ??
|
|
2554
|
+
(await readRefSecret(name, storageKey)) ??
|
|
2555
|
+
(await tryResolve(storageKey));
|
|
2052
2556
|
|
|
2053
2557
|
if (value) {
|
|
2054
2558
|
resolvedHeaders[headerName] = value;
|
|
@@ -2070,23 +2574,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2070
2574
|
const encKey = options.encryptionKey;
|
|
2071
2575
|
const headersToStore: Record<string, string> = {};
|
|
2072
2576
|
for (const [h, v] of Object.entries(resolvedHeaders)) {
|
|
2073
|
-
headersToStore[h] = encKey
|
|
2577
|
+
headersToStore[h] = encKey
|
|
2578
|
+
? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}`
|
|
2579
|
+
: v;
|
|
2074
2580
|
}
|
|
2075
2581
|
await ref.update(name, { config: { headers: headersToStore } });
|
|
2076
2582
|
return { type: "apiKey", complete: true };
|
|
2077
2583
|
}
|
|
2078
2584
|
|
|
2079
2585
|
// Fallback: no headers declared → generic api_key
|
|
2080
|
-
const key =
|
|
2586
|
+
const key =
|
|
2587
|
+
opts?.credentials?.["api_key"] ??
|
|
2588
|
+
opts?.apiKey ??
|
|
2589
|
+
(await tryResolve("api_key"));
|
|
2081
2590
|
if (!key) {
|
|
2082
2591
|
return {
|
|
2083
2592
|
type: "apiKey",
|
|
2084
2593
|
complete: false,
|
|
2085
|
-
fields: [
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2594
|
+
fields: [
|
|
2595
|
+
{
|
|
2596
|
+
name: "api_key",
|
|
2597
|
+
label: "API Key",
|
|
2598
|
+
secret: true,
|
|
2599
|
+
},
|
|
2600
|
+
],
|
|
2090
2601
|
};
|
|
2091
2602
|
}
|
|
2092
2603
|
await storeRefSecret(name, "api_key", key);
|
|
@@ -2098,12 +2609,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2098
2609
|
const isBasic = httpSec.scheme === "basic";
|
|
2099
2610
|
|
|
2100
2611
|
if (isBasic) {
|
|
2101
|
-
const username =
|
|
2102
|
-
|
|
2612
|
+
const username =
|
|
2613
|
+
opts?.credentials?.["username"] ?? (await tryResolve("username"));
|
|
2614
|
+
const password =
|
|
2615
|
+
opts?.credentials?.["password"] ?? (await tryResolve("password"));
|
|
2103
2616
|
if (!username || !password) {
|
|
2104
2617
|
const missingFields: AuthChallengeField[] = [];
|
|
2105
|
-
if (!username)
|
|
2106
|
-
|
|
2618
|
+
if (!username)
|
|
2619
|
+
missingFields.push({
|
|
2620
|
+
name: "username",
|
|
2621
|
+
label: "Username",
|
|
2622
|
+
secret: false,
|
|
2623
|
+
});
|
|
2624
|
+
if (!password)
|
|
2625
|
+
missingFields.push({
|
|
2626
|
+
name: "password",
|
|
2627
|
+
label: "Password",
|
|
2628
|
+
secret: true,
|
|
2629
|
+
});
|
|
2107
2630
|
return { type: "http", complete: false, fields: missingFields };
|
|
2108
2631
|
}
|
|
2109
2632
|
// Store as base64 encoded basic auth token
|
|
@@ -2113,7 +2636,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2113
2636
|
}
|
|
2114
2637
|
|
|
2115
2638
|
// Bearer token
|
|
2116
|
-
const token =
|
|
2639
|
+
const token =
|
|
2640
|
+
opts?.credentials?.["token"] ??
|
|
2641
|
+
opts?.apiKey ??
|
|
2642
|
+
(await tryResolve("token"));
|
|
2117
2643
|
if (!token) {
|
|
2118
2644
|
return {
|
|
2119
2645
|
type: "http",
|
|
@@ -2126,7 +2652,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2126
2652
|
}
|
|
2127
2653
|
|
|
2128
2654
|
if (security.type === "oauth2") {
|
|
2129
|
-
const flows = (
|
|
2655
|
+
const flows = (
|
|
2656
|
+
security as {
|
|
2657
|
+
flows?: {
|
|
2658
|
+
authorizationCode?: {
|
|
2659
|
+
authorizationUrl?: string;
|
|
2660
|
+
tokenUrl?: string;
|
|
2661
|
+
};
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
).flows;
|
|
2130
2665
|
const authCodeFlow = flows?.authorizationCode;
|
|
2131
2666
|
if (!authCodeFlow?.authorizationUrl) {
|
|
2132
2667
|
return {
|
|
@@ -2147,7 +2682,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2147
2682
|
}
|
|
2148
2683
|
// Fallback: construct metadata from the security scheme's explicit URLs
|
|
2149
2684
|
if (!metadata && authCodeFlow.tokenUrl) {
|
|
2150
|
-
const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
|
|
2685
|
+
const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
|
|
2686
|
+
| Record<string, string>
|
|
2687
|
+
| undefined;
|
|
2151
2688
|
metadata = {
|
|
2152
2689
|
issuer: new URL(authUrl).origin,
|
|
2153
2690
|
authorization_endpoint: authUrl,
|
|
@@ -2172,18 +2709,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2172
2709
|
let clientSecret: string | undefined = fromHelper?.clientSecret;
|
|
2173
2710
|
|
|
2174
2711
|
if (!clientId && metadata.registration_endpoint) {
|
|
2175
|
-
const supportedAuthMethods =
|
|
2712
|
+
const supportedAuthMethods =
|
|
2713
|
+
metadata.token_endpoint_auth_methods_supported ?? ["none"];
|
|
2176
2714
|
const preferredMethod = supportedAuthMethods.includes("none")
|
|
2177
2715
|
? "none"
|
|
2178
|
-
: supportedAuthMethods[0] ?? "client_secret_post";
|
|
2179
|
-
|
|
2180
|
-
const securityClientName = (security as { clientName?: string })
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2716
|
+
: (supportedAuthMethods[0] ?? "client_secret_post");
|
|
2717
|
+
|
|
2718
|
+
const securityClientName = (security as { clientName?: string })
|
|
2719
|
+
.clientName;
|
|
2720
|
+
const reg = await dynamicClientRegistration(
|
|
2721
|
+
metadata.registration_endpoint,
|
|
2722
|
+
{
|
|
2723
|
+
clientName:
|
|
2724
|
+
securityClientName ?? options.oauthClientName ?? "adk",
|
|
2725
|
+
redirectUris: [redirectUri],
|
|
2726
|
+
grantTypes: ["authorization_code"],
|
|
2727
|
+
tokenEndpointAuthMethod: preferredMethod,
|
|
2728
|
+
},
|
|
2729
|
+
);
|
|
2187
2730
|
clientId = reg.clientId;
|
|
2188
2731
|
clientSecret = reg.clientSecret;
|
|
2189
2732
|
await storeRefSecret(name, "client_id", clientId);
|
|
@@ -2196,10 +2739,18 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2196
2739
|
// Return fields telling the caller what OAuth credentials to provide
|
|
2197
2740
|
const missingFields: AuthChallengeField[] = [];
|
|
2198
2741
|
if (!clientId) {
|
|
2199
|
-
missingFields.push({
|
|
2742
|
+
missingFields.push({
|
|
2743
|
+
name: "client_id",
|
|
2744
|
+
label: "Client ID",
|
|
2745
|
+
secret: false,
|
|
2746
|
+
});
|
|
2200
2747
|
}
|
|
2201
2748
|
// Always ask for client_secret alongside client_id — most providers need it
|
|
2202
|
-
missingFields.push({
|
|
2749
|
+
missingFields.push({
|
|
2750
|
+
name: "client_secret",
|
|
2751
|
+
label: "Client Secret",
|
|
2752
|
+
secret: true,
|
|
2753
|
+
});
|
|
2203
2754
|
return { type: "oauth2", complete: false, fields: missingFields };
|
|
2204
2755
|
}
|
|
2205
2756
|
|
|
@@ -2213,32 +2764,42 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2213
2764
|
};
|
|
2214
2765
|
const state = btoa(JSON.stringify(statePayload));
|
|
2215
2766
|
|
|
2216
|
-
const securityExt2 = security as {
|
|
2217
|
-
|
|
2767
|
+
const securityExt2 = security as {
|
|
2768
|
+
requiredScopes?: string[];
|
|
2769
|
+
optionalScopes?: string[];
|
|
2770
|
+
authorizationParams?: Record<string, string>;
|
|
2771
|
+
};
|
|
2772
|
+
const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
|
|
2773
|
+
| Record<string, string>
|
|
2774
|
+
| undefined;
|
|
2218
2775
|
const agentScopes = [
|
|
2219
2776
|
...(securityExt2.requiredScopes ?? []),
|
|
2220
2777
|
...(flowScopes ? Object.keys(flowScopes) : []),
|
|
2221
2778
|
...(opts?.scopes ?? []),
|
|
2222
2779
|
].filter((v, i, a) => a.indexOf(v) === i);
|
|
2223
|
-
const scopes =
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2780
|
+
const scopes =
|
|
2781
|
+
agentScopes.length > 0
|
|
2782
|
+
? [
|
|
2783
|
+
...agentScopes,
|
|
2784
|
+
...(metadata.scopes_supported?.includes("openid")
|
|
2785
|
+
? ["openid"]
|
|
2786
|
+
: []),
|
|
2787
|
+
]
|
|
2788
|
+
: metadata.scopes_supported;
|
|
2229
2789
|
|
|
2230
2790
|
// Read provider-specific authorization params from the agent's security section
|
|
2231
2791
|
// (e.g., { access_type: 'offline', prompt: 'consent' } for Google)
|
|
2232
2792
|
const authorizationParams = securityExt2.authorizationParams;
|
|
2233
2793
|
|
|
2234
|
-
const { url: authorizeUrl, codeVerifier } =
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2794
|
+
const { url: authorizeUrl, codeVerifier } =
|
|
2795
|
+
await buildOAuthAuthorizeUrl({
|
|
2796
|
+
authorizationEndpoint: metadata.authorization_endpoint,
|
|
2797
|
+
clientId,
|
|
2798
|
+
redirectUri,
|
|
2799
|
+
scopes,
|
|
2800
|
+
state,
|
|
2801
|
+
extraParams: authorizationParams,
|
|
2802
|
+
});
|
|
2242
2803
|
|
|
2243
2804
|
// Persist pending state so handleCallback works across processes
|
|
2244
2805
|
await storePendingOAuth(state, {
|
|
@@ -2257,10 +2818,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2257
2818
|
return { type: security.type, complete: false };
|
|
2258
2819
|
},
|
|
2259
2820
|
|
|
2260
|
-
async authLocal(
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2821
|
+
async authLocal(
|
|
2822
|
+
name: string,
|
|
2823
|
+
opts?: {
|
|
2824
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
2825
|
+
timeoutMs?: number;
|
|
2826
|
+
},
|
|
2827
|
+
): Promise<{ complete: boolean }> {
|
|
2264
2828
|
// `ref.auth` is already proxy-aware — for proxied refs it returns
|
|
2265
2829
|
// the authorizeUrl that the registry minted against its own
|
|
2266
2830
|
// callback domain. Everything below is identical for local and
|
|
@@ -2283,7 +2847,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2283
2847
|
// owns the credential store, so the user needs to submit via
|
|
2284
2848
|
// whatever UI the registry exposes. Supporting this through the
|
|
2285
2849
|
// proxy would need a remote form endpoint — out of scope here.
|
|
2286
|
-
if (
|
|
2850
|
+
if (
|
|
2851
|
+
result.fields &&
|
|
2852
|
+
result.fields.length > 0 &&
|
|
2853
|
+
result.type !== "oauth2"
|
|
2854
|
+
) {
|
|
2287
2855
|
if (proxy) {
|
|
2288
2856
|
throw new Error(
|
|
2289
2857
|
`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`,
|
|
@@ -2320,19 +2888,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2320
2888
|
resolve({ complete: true });
|
|
2321
2889
|
} else {
|
|
2322
2890
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2323
|
-
res.end(
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2891
|
+
res.end(
|
|
2892
|
+
renderCredentialForm(
|
|
2893
|
+
name,
|
|
2894
|
+
authResult.fields ?? result.fields!,
|
|
2895
|
+
"Some credentials were missing or invalid.",
|
|
2896
|
+
),
|
|
2897
|
+
);
|
|
2328
2898
|
}
|
|
2329
2899
|
} catch (err) {
|
|
2330
2900
|
res.writeHead(500, { "Content-Type": "text/html" });
|
|
2331
|
-
res.end(
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2901
|
+
res.end(
|
|
2902
|
+
renderCredentialForm(
|
|
2903
|
+
name,
|
|
2904
|
+
result.fields!,
|
|
2905
|
+
err instanceof Error ? err.message : String(err),
|
|
2906
|
+
),
|
|
2907
|
+
);
|
|
2336
2908
|
}
|
|
2337
2909
|
return;
|
|
2338
2910
|
}
|
|
@@ -2379,7 +2951,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2379
2951
|
const state = reqUrl.searchParams.get("state");
|
|
2380
2952
|
|
|
2381
2953
|
if (!code || !state) {
|
|
2382
|
-
const error =
|
|
2954
|
+
const error =
|
|
2955
|
+
reqUrl.searchParams.get("error") ?? "missing code/state";
|
|
2383
2956
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2384
2957
|
res.end(`<h1>Error</h1><p>${error}</p>`);
|
|
2385
2958
|
server.close();
|
|
@@ -2395,7 +2968,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2395
2968
|
resolve({ complete: cbResult.complete });
|
|
2396
2969
|
} catch (err) {
|
|
2397
2970
|
res.writeHead(500, { "Content-Type": "text/html" });
|
|
2398
|
-
res.end(
|
|
2971
|
+
res.end(
|
|
2972
|
+
`<h1>Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`,
|
|
2973
|
+
);
|
|
2399
2974
|
server.close();
|
|
2400
2975
|
reject(err);
|
|
2401
2976
|
}
|
|
@@ -2440,9 +3015,17 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2440
3015
|
|
|
2441
3016
|
const status = await ref.authStatus(name);
|
|
2442
3017
|
const security = status.security;
|
|
2443
|
-
const flows =
|
|
2444
|
-
|
|
2445
|
-
|
|
3018
|
+
const flows =
|
|
3019
|
+
security && "flows" in security
|
|
3020
|
+
? (
|
|
3021
|
+
security as {
|
|
3022
|
+
flows?: Record<
|
|
3023
|
+
string,
|
|
3024
|
+
{ tokenUrl?: string; refreshUrl?: string }
|
|
3025
|
+
>;
|
|
3026
|
+
}
|
|
3027
|
+
).flows
|
|
3028
|
+
: undefined;
|
|
2446
3029
|
const authCodeFlow = flows?.authorizationCode;
|
|
2447
3030
|
const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
|
|
2448
3031
|
if (!tokenUrl) return null;
|
|
@@ -2469,7 +3052,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2469
3052
|
|
|
2470
3053
|
if (!res.ok) return null;
|
|
2471
3054
|
|
|
2472
|
-
const data = await res.json() as Record<string, unknown>;
|
|
3055
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
2473
3056
|
const newAccessToken = data.access_token as string | undefined;
|
|
2474
3057
|
if (!newAccessToken) return null;
|
|
2475
3058
|
|
|
@@ -2487,7 +3070,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2487
3070
|
// Top-level callback handler
|
|
2488
3071
|
// ==========================================
|
|
2489
3072
|
|
|
2490
|
-
async function handleCallback(params: {
|
|
3073
|
+
async function handleCallback(params: {
|
|
3074
|
+
code: string;
|
|
3075
|
+
state: string;
|
|
3076
|
+
}): Promise<{
|
|
3077
|
+
refName: string;
|
|
3078
|
+
complete: boolean;
|
|
3079
|
+
stateContext?: Record<string, unknown>;
|
|
3080
|
+
}> {
|
|
2491
3081
|
const pending = await consumePendingOAuth(params.state);
|
|
2492
3082
|
if (!pending) {
|
|
2493
3083
|
throw new Error(`No pending OAuth flow for state "${params.state}".`);
|
|
@@ -2503,13 +3093,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2503
3093
|
|
|
2504
3094
|
await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
|
|
2505
3095
|
if (tokens.refreshToken) {
|
|
2506
|
-
await storeRefSecret(
|
|
3096
|
+
await storeRefSecret(
|
|
3097
|
+
pending.refName,
|
|
3098
|
+
"refresh_token",
|
|
3099
|
+
tokens.refreshToken,
|
|
3100
|
+
);
|
|
2507
3101
|
}
|
|
2508
3102
|
|
|
2509
3103
|
let stateContext: Record<string, unknown> | undefined;
|
|
2510
3104
|
try {
|
|
2511
3105
|
stateContext = JSON.parse(atob(params.state));
|
|
2512
|
-
} catch {
|
|
3106
|
+
} catch {
|
|
3107
|
+
/* state wasn't base64 JSON — legacy format */
|
|
3108
|
+
}
|
|
2513
3109
|
|
|
2514
3110
|
return { refName: pending.refName, complete: true, stateContext };
|
|
2515
3111
|
}
|