@agentproto/auth 0.1.0-alpha.0 → 0.1.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.
package/dist/index.mjs CHANGED
@@ -4,6 +4,9 @@ import matter from 'gray-matter';
4
4
  import { createInterface } from 'readline';
5
5
  import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
+ import { createDecipheriv, randomBytes, createCipheriv } from 'crypto';
8
+ import { readFile, mkdir, writeFile } from 'fs/promises';
9
+ import { dirname } from 'path';
7
10
 
8
11
  /**
9
12
  * @agentproto/auth v0.1.0-alpha
@@ -12,6 +15,7 @@ import { promisify } from 'util';
12
15
 
13
16
  var tokenStoreSpecSchema = z.object({
14
17
  keychain: z.string().min(1),
18
+ path: z.string().min(1).optional(),
15
19
  account: z.string().min(1).optional()
16
20
  }).strict();
17
21
  var patAuthConfigSchema = z.object({
@@ -24,9 +28,17 @@ var serviceAuthConfigSchema = z.object({
24
28
  loginHint: z.string().min(1).optional(),
25
29
  tokenStore: tokenStoreSpecSchema
26
30
  }).strict();
31
+ var deviceCodeAuthConfigSchema = z.object({
32
+ flow: z.literal("device-code"),
33
+ clientId: z.string().min(1).optional(),
34
+ scope: z.string().min(1).optional(),
35
+ deviceLabel: z.string().min(1).optional(),
36
+ tokenStore: tokenStoreSpecSchema
37
+ }).strict();
27
38
  var authConfigSchema = z.discriminatedUnion("flow", [
28
39
  patAuthConfigSchema,
29
- serviceAuthConfigSchema
40
+ serviceAuthConfigSchema,
41
+ deviceCodeAuthConfigSchema
30
42
  ]);
31
43
  var installConfigSchema = z.object({
32
44
  sealKey: z.string().min(1),
@@ -37,7 +49,9 @@ var authProviderFrontmatterSchema = z.object({
37
49
  description: z.string().min(1).max(2e3),
38
50
  apiBase: z.string().url(),
39
51
  auth: authConfigSchema,
40
- install: installConfigSchema.optional()
52
+ install: installConfigSchema.optional(),
53
+ // Recommended values "tunnel" | "api" | "mcp" — free-form allowed, not an enum.
54
+ audience: z.string().min(1).optional()
41
55
  }).strict().describe(
42
56
  "AIP-50 auth-provider manifest: how a CLI tool authenticates to an API server and (optionally) where to call the AIP-19 provision endpoints."
43
57
  );
@@ -85,6 +99,7 @@ var guildeAuthProvider = defineAuthProvider({
85
99
  id: "guilde",
86
100
  description: "Guilde AI company platform. Authenticate via a browser-approve flow \u2014 no key to paste.",
87
101
  apiBase: "https://api.guilde.work",
102
+ audience: "api",
88
103
  auth: {
89
104
  flow: "service-auth",
90
105
  clientId: "agentproto-cli",
@@ -184,6 +199,7 @@ var asMetaSchema = z.object({
184
199
  issuer: z.string().optional(),
185
200
  token_endpoint: z.string().optional(),
186
201
  revocation_endpoint: z.string().optional(),
202
+ device_authorization_endpoint: z.string().optional(),
187
203
  grant_types_supported: z.array(z.string()).optional(),
188
204
  agent_auth: z.object({
189
205
  skill: z.string().optional(),
@@ -194,12 +210,12 @@ var asMetaSchema = z.object({
194
210
  identity_assertion: z.object({ assertion_types_supported: z.array(z.string()).optional() }).loose().optional()
195
211
  }).loose().optional()
196
212
  }).loose();
197
- async function discoverEndpoints(apiBase, opts = {}) {
198
- const base = apiBase.replace(/\/$/, "");
199
- const fetchOpts = {
200
- signal: opts.signal,
201
- redirect: "manual"
202
- };
213
+ var hostJsonSchema = z.object({
214
+ device_authorization_endpoint: z.string().optional(),
215
+ token_endpoint: z.string().optional(),
216
+ revocation_endpoint: z.string().optional()
217
+ }).loose();
218
+ async function discoverViaPRM(base, fetchOpts, opts) {
203
219
  const prmUrl = `${base}/.well-known/oauth-protected-resource`;
204
220
  let prm;
205
221
  try {
@@ -208,19 +224,19 @@ async function discoverEndpoints(apiBase, opts = {}) {
208
224
  if (!res.ok) {
209
225
  throw new DiscoveryError(
210
226
  `PRM returned ${res.status} ${res.statusText}`,
211
- apiBase
227
+ base
212
228
  );
213
229
  }
214
230
  prm = prmSchema.parse(await res.json());
215
231
  } catch (err) {
216
232
  if (err instanceof DiscoveryError) throw err;
217
- throw new DiscoveryError(`PRM fetch failed: ${err}`, apiBase);
233
+ throw new DiscoveryError(`PRM fetch failed: ${err}`, base);
218
234
  }
219
235
  const authServerBase = prm.authorization_servers?.[0]?.replace(/\/$/, "");
220
236
  if (!authServerBase) {
221
237
  throw new DiscoveryError(
222
238
  "PRM missing authorization_servers[0]",
223
- apiBase
239
+ base
224
240
  );
225
241
  }
226
242
  const asUrl = `${authServerBase}/.well-known/oauth-authorization-server`;
@@ -231,23 +247,23 @@ async function discoverEndpoints(apiBase, opts = {}) {
231
247
  if (!res.ok) {
232
248
  throw new DiscoveryError(
233
249
  `AS metadata returned ${res.status} ${res.statusText}`,
234
- apiBase
250
+ base
235
251
  );
236
252
  }
237
253
  as = asMetaSchema.parse(await res.json());
238
254
  } catch (err) {
239
255
  if (err instanceof DiscoveryError) throw err;
240
- throw new DiscoveryError(`AS metadata fetch failed: ${err}`, apiBase);
256
+ throw new DiscoveryError(`AS metadata fetch failed: ${err}`, base);
241
257
  }
242
258
  const tokenEndpoint = as.token_endpoint;
243
259
  const identityEndpoint = as.agent_auth?.identity_endpoint;
244
260
  if (!tokenEndpoint) {
245
- throw new DiscoveryError("AS metadata missing token_endpoint", apiBase);
261
+ throw new DiscoveryError("AS metadata missing token_endpoint", base);
246
262
  }
247
263
  if (!identityEndpoint) {
248
264
  throw new DiscoveryError(
249
265
  "AS metadata missing agent_auth.identity_endpoint",
250
- apiBase
266
+ base
251
267
  );
252
268
  }
253
269
  return {
@@ -258,10 +274,67 @@ async function discoverEndpoints(apiBase, opts = {}) {
258
274
  revocationEndpoint: as.revocation_endpoint,
259
275
  identityEndpoint,
260
276
  claimEndpoint: as.agent_auth?.claim_endpoint,
277
+ deviceAuthorizationEndpoint: as.device_authorization_endpoint,
261
278
  identityTypesSupported: as.agent_auth?.identity_types_supported ?? [],
262
279
  grantTypesSupported: as.grant_types_supported ?? []
263
280
  };
264
281
  }
282
+ async function discoverViaHostJson(base, fetchOpts, opts) {
283
+ const url = `${base}/.well-known/agentproto-host.json`;
284
+ let doc;
285
+ try {
286
+ assertSecureUrl(url);
287
+ const res = await fetchWithDeadline(url, fetchOpts, opts.timeoutMs);
288
+ if (!res.ok) {
289
+ throw new DiscoveryError(
290
+ `agentproto-host.json returned ${res.status} ${res.statusText}`,
291
+ base
292
+ );
293
+ }
294
+ doc = hostJsonSchema.parse(await res.json());
295
+ } catch (err) {
296
+ if (err instanceof DiscoveryError) throw err;
297
+ throw new DiscoveryError(`agentproto-host.json fetch failed: ${err}`, base);
298
+ }
299
+ if (!doc.token_endpoint) {
300
+ throw new DiscoveryError(
301
+ "agentproto-host.json missing token_endpoint",
302
+ base
303
+ );
304
+ }
305
+ if (!doc.device_authorization_endpoint) {
306
+ throw new DiscoveryError(
307
+ "agentproto-host.json missing device_authorization_endpoint",
308
+ base
309
+ );
310
+ }
311
+ return {
312
+ resource: base,
313
+ authServerBase: base,
314
+ tokenEndpoint: doc.token_endpoint,
315
+ revocationEndpoint: doc.revocation_endpoint,
316
+ deviceAuthorizationEndpoint: doc.device_authorization_endpoint,
317
+ identityTypesSupported: [],
318
+ grantTypesSupported: []
319
+ };
320
+ }
321
+ async function discoverEndpoints(apiBase, opts = {}) {
322
+ const base = apiBase.replace(/\/$/, "");
323
+ const fetchOpts = {
324
+ signal: opts.signal,
325
+ redirect: "manual"
326
+ };
327
+ try {
328
+ return await discoverViaPRM(base, fetchOpts, opts);
329
+ } catch (prmErr) {
330
+ if (opts.signal?.aborted) throw prmErr;
331
+ try {
332
+ return await discoverViaHostJson(base, fetchOpts, opts);
333
+ } catch {
334
+ throw prmErr;
335
+ }
336
+ }
337
+ }
265
338
  var exec = promisify(execFile);
266
339
  function assertKeychainSupported() {
267
340
  if (process.platform !== "darwin") {
@@ -307,6 +380,64 @@ async function writeKeychainToken(service, account, token) {
307
380
  ]);
308
381
  }
309
382
 
383
+ // src/store/keychain-store.ts
384
+ var envelopeSchema = z.object({
385
+ v: z.string(),
386
+ k: z.enum(["pat", "assertion", "oat", "daemon"]),
387
+ e: z.string().optional(),
388
+ m: z.record(z.string(), z.unknown()).optional()
389
+ });
390
+ var KeychainStore = class {
391
+ async read(ref) {
392
+ const account = ref.account ?? ref.path;
393
+ const raw = await readKeychainToken(ref.path, account);
394
+ if (raw === void 0) return void 0;
395
+ try {
396
+ const envelope = envelopeSchema.parse(JSON.parse(raw));
397
+ return {
398
+ value: envelope.v,
399
+ kind: envelope.k,
400
+ ...envelope.e !== void 0 ? { expiresAt: envelope.e } : {},
401
+ ...envelope.m !== void 0 ? { metadata: envelope.m } : {}
402
+ };
403
+ } catch {
404
+ return { value: raw, kind: "pat" };
405
+ }
406
+ }
407
+ async write(ref, cred) {
408
+ const account = ref.account ?? ref.path;
409
+ const envelope = {
410
+ v: cred.value,
411
+ k: cred.kind,
412
+ ...cred.expiresAt !== void 0 ? { e: cred.expiresAt } : {},
413
+ ...cred.metadata !== void 0 ? { m: cred.metadata } : {}
414
+ };
415
+ await writeKeychainToken(ref.path, account, JSON.stringify(envelope));
416
+ }
417
+ };
418
+
419
+ // src/store/resolve-ref.ts
420
+ function resolveStoreRef(spec, server, audience) {
421
+ const path = spec.path ?? spec.keychain;
422
+ return {
423
+ path: audience ? `${audience}:${path}` : path,
424
+ account: resolveAccount(spec.account, server)
425
+ };
426
+ }
427
+ function resolveStoreRefs(spec, server, audience) {
428
+ const ref = resolveStoreRef(spec, server, audience);
429
+ const legacyRef = audience ? resolveStoreRef(spec, server) : ref;
430
+ return { ref, legacyRef };
431
+ }
432
+ async function readStoreRefWithFallback(store, ref, legacyRef) {
433
+ const stored = await store.read(ref);
434
+ if (stored) return stored;
435
+ if (ref.path === legacyRef.path && ref.account === legacyRef.account) {
436
+ return void 0;
437
+ }
438
+ return store.read(legacyRef);
439
+ }
440
+
310
441
  // src/flow-engines/pat.ts
311
442
  async function promptToken(label) {
312
443
  const input = process.stdin;
@@ -377,39 +508,37 @@ var patFlowEngine = {
377
508
  if (auth.flow !== "pat") {
378
509
  throw new Error(`patFlowEngine: invoked with flow="${auth.flow}"`);
379
510
  }
380
- const account = resolveAccount(auth.tokenStore.account, opts.server);
511
+ const store = opts.store ?? new KeychainStore();
512
+ const { ref, legacyRef } = resolveStoreRefs(
513
+ auth.tokenStore,
514
+ opts.server,
515
+ provider.audience
516
+ );
381
517
  if (!opts.force) {
382
- const existing = await readKeychainToken(auth.tokenStore.keychain, account);
518
+ const existing = await readStoreRefWithFallback(store, ref, legacyRef);
383
519
  if (existing) {
384
- return { accessToken: existing, tokenKind: "pat" };
520
+ return { accessToken: existing.value, tokenKind: "pat" };
385
521
  }
386
522
  }
387
523
  const token = await promptToken(`${provider.id} personal access token`);
388
524
  if (!token) throw new Error("no token provided");
525
+ await store.write(ref, { value: token, kind: "pat" });
389
526
  return { accessToken: token, tokenKind: "pat" };
390
527
  }
391
528
  };
392
529
  var execAsync = promisify(execFile);
393
- var CLAIM_GRANT_TYPE = "urn:workos:agent-auth:grant-type:claim";
394
- var JWT_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
395
- var DEFAULT_POLL_INTERVAL_S = 5;
396
- var identityResponseSchema = z.object({
397
- registration_id: z.string(),
398
- claim_token: z.string(),
399
- claim: z.object({
400
- user_code: z.string(),
401
- verification_uri: z.string(),
402
- expires_in: z.number(),
403
- interval: z.number().optional()
404
- }).loose()
405
- }).loose();
406
530
  var tokenSuccessSchema = z.object({
407
531
  access_token: z.string(),
408
532
  token_type: z.string().optional(),
409
533
  refresh_token: z.string().optional(),
410
534
  identity_assertion: z.string().optional(),
411
535
  assertion_expires_in: z.number().optional(),
412
- assertion_expires: z.string().optional()
536
+ assertion_expires: z.string().optional(),
537
+ // device-code flow fields (ignored by service-auth today; see WP-2).
538
+ expires_in: z.number().optional(),
539
+ scope: z.string().optional(),
540
+ subject: z.string().optional(),
541
+ revocation_id: z.string().optional()
413
542
  }).loose();
414
543
  var tokenErrorSchema = z.object({
415
544
  error: z.string(),
@@ -423,20 +552,7 @@ async function openBrowser(url) {
423
552
  } catch {
424
553
  }
425
554
  }
426
- async function postJson(url, body, schema, signal) {
427
- const res = await fetchWithDeadline(url, {
428
- method: "POST",
429
- headers: { "Content-Type": "application/json" },
430
- body: JSON.stringify(body),
431
- signal
432
- });
433
- if (!res.ok) {
434
- const text = await res.text();
435
- throw new Error(`POST ${url} \u2192 ${res.status}: ${text}`);
436
- }
437
- return schema.parse(await res.json());
438
- }
439
- async function postForm(url, params, schema, signal) {
555
+ async function postFormOAuth(url, params, schema, signal) {
440
556
  const res = await fetchWithDeadline(url, {
441
557
  method: "POST",
442
558
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
@@ -445,21 +561,17 @@ async function postForm(url, params, schema, signal) {
445
561
  });
446
562
  return schema.parse(await res.json());
447
563
  }
448
- async function pollForToken(tokenEndpoint, claimToken, clientId, intervalS, expiresIn, signal) {
449
- const deadline = Date.now() + expiresIn * 1e3;
450
- let pollMs = intervalS * 1e3;
564
+ async function pollDeviceGrant(o) {
565
+ const deadline = Date.now() + o.expiresIn * 1e3;
566
+ let pollMs = o.intervalS * 1e3;
451
567
  while (Date.now() < deadline) {
452
- if (signal?.aborted) throw new Error("auth cancelled");
568
+ if (o.signal?.aborted) throw new Error("auth cancelled");
453
569
  await new Promise((r) => setTimeout(r, pollMs));
454
- const data = await postForm(
455
- tokenEndpoint,
456
- {
457
- grant_type: CLAIM_GRANT_TYPE,
458
- claim_token: claimToken,
459
- client_id: clientId
460
- },
570
+ const data = await postFormOAuth(
571
+ o.tokenEndpoint,
572
+ { grant_type: o.grantType, ...o.params },
461
573
  tokenResponseSchema,
462
- signal
574
+ o.signal
463
575
  );
464
576
  if (!("error" in data)) return data;
465
577
  const pollErr = data.error;
@@ -479,9 +591,37 @@ async function pollForToken(tokenEndpoint, claimToken, clientId, intervalS, expi
479
591
  }
480
592
  throw new Error("auth timeout \u2014 approval window closed");
481
593
  }
594
+
595
+ // src/flow-engines/service-auth.ts
596
+ var CLAIM_GRANT_TYPE = "urn:workos:agent-auth:grant-type:claim";
597
+ var JWT_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
598
+ var DEFAULT_POLL_INTERVAL_S = 5;
599
+ var identityResponseSchema = z.object({
600
+ registration_id: z.string(),
601
+ claim_token: z.string(),
602
+ claim: z.object({
603
+ user_code: z.string(),
604
+ verification_uri: z.string(),
605
+ expires_in: z.number(),
606
+ interval: z.number().optional()
607
+ }).loose()
608
+ }).loose();
609
+ async function postJson(url, body, schema, signal) {
610
+ const res = await fetchWithDeadline(url, {
611
+ method: "POST",
612
+ headers: { "Content-Type": "application/json" },
613
+ body: JSON.stringify(body),
614
+ signal
615
+ });
616
+ if (!res.ok) {
617
+ const text = await res.text();
618
+ throw new Error(`POST ${url} \u2192 ${res.status}: ${text}`);
619
+ }
620
+ return schema.parse(await res.json());
621
+ }
482
622
  async function exchangeAssertion(tokenEndpoint, assertion, clientId, signal) {
483
623
  try {
484
- const data = await postForm(
624
+ const data = await postFormOAuth(
485
625
  tokenEndpoint,
486
626
  {
487
627
  grant_type: JWT_BEARER_GRANT_TYPE,
@@ -511,24 +651,27 @@ var serviceAuthFlowEngine = {
511
651
  const tokenEndpoint = discovered?.tokenEndpoint ?? `${server}/oauth/token`;
512
652
  assertSecureUrl(identityEndpoint);
513
653
  assertSecureUrl(tokenEndpoint);
514
- const primarySlot = config.tokenStore.keychain;
515
- const account = resolveAccount(config.tokenStore.account, server);
654
+ const store = opts.store ?? new KeychainStore();
655
+ const { ref, legacyRef } = resolveStoreRefs(
656
+ config.tokenStore,
657
+ server,
658
+ provider.audience
659
+ );
516
660
  if (!opts.force) {
517
- const storedAssertion = await readKeychainToken(primarySlot, account);
518
- if (storedAssertion) {
661
+ const stored = await readStoreRefWithFallback(store, ref, legacyRef);
662
+ if (stored) {
519
663
  const exchanged = await exchangeAssertion(
520
664
  tokenEndpoint,
521
- storedAssertion,
665
+ stored.value,
522
666
  clientId,
523
667
  opts.signal
524
668
  );
525
669
  if (exchanged) {
526
670
  if (exchanged.identity_assertion) {
527
- await writeKeychainToken(
528
- primarySlot,
529
- account,
530
- exchanged.identity_assertion
531
- );
671
+ await store.write(ref, {
672
+ value: exchanged.identity_assertion,
673
+ kind: "assertion"
674
+ });
532
675
  }
533
676
  return {
534
677
  accessToken: exchanged.access_token,
@@ -561,23 +704,22 @@ var serviceAuthFlowEngine = {
561
704
  process.stderr.write(` URL: ${claim.verification_uri}
562
705
 
563
706
  `);
564
- await openBrowser(claim.verification_uri);
707
+ if (opts.openBrowser !== false) {
708
+ await openBrowser(claim.verification_uri);
709
+ }
565
710
  process.stderr.write(` Waiting for approval (${windowMin} min)\u2026
566
711
 
567
712
  `);
568
- const tokenResult = await pollForToken(
713
+ const tokenResult = await pollDeviceGrant({
569
714
  tokenEndpoint,
570
- claim_token,
571
- clientId,
715
+ grantType: CLAIM_GRANT_TYPE,
716
+ params: { claim_token, client_id: clientId },
572
717
  intervalS,
573
- claim.expires_in,
574
- opts.signal
575
- );
718
+ expiresIn: claim.expires_in,
719
+ signal: opts.signal
720
+ });
576
721
  const accessToken = tokenResult.access_token;
577
722
  const assertion = tokenResult.identity_assertion;
578
- if (assertion) {
579
- await writeKeychainToken(primarySlot, account, assertion);
580
- }
581
723
  let assertionExpires;
582
724
  if (tokenResult.assertion_expires) {
583
725
  assertionExpires = tokenResult.assertion_expires;
@@ -586,6 +728,13 @@ var serviceAuthFlowEngine = {
586
728
  Date.now() + tokenResult.assertion_expires_in * 1e3
587
729
  ).toISOString();
588
730
  }
731
+ if (assertion) {
732
+ await store.write(ref, {
733
+ value: assertion,
734
+ kind: "assertion",
735
+ ...assertionExpires ? { expiresAt: assertionExpires } : {}
736
+ });
737
+ }
589
738
  return {
590
739
  accessToken,
591
740
  identityAssertion: assertion,
@@ -594,11 +743,217 @@ var serviceAuthFlowEngine = {
594
743
  };
595
744
  }
596
745
  };
746
+ var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
747
+ var REFRESH_GRANT_TYPE = "refresh_token";
748
+ var DEFAULT_POLL_INTERVAL_S2 = 5;
749
+ var CeremonyRequiredError = class extends Error {
750
+ constructor(message) {
751
+ super(message);
752
+ this.name = "CeremonyRequiredError";
753
+ }
754
+ };
755
+ var deviceAuthorizationResponseSchema = z.object({
756
+ device_code: z.string(),
757
+ user_code: z.string(),
758
+ verification_uri: z.string(),
759
+ verification_uri_complete: z.string().optional(),
760
+ expires_in: z.number(),
761
+ interval: z.number().optional()
762
+ }).loose();
763
+ async function postDeviceAuthorization(url, params, signal) {
764
+ const res = await fetchWithDeadline(url, {
765
+ method: "POST",
766
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
767
+ body: new URLSearchParams(params).toString(),
768
+ signal
769
+ });
770
+ if (!res.ok) {
771
+ const text = await res.text();
772
+ throw new Error(`POST ${url} \u2192 ${res.status}: ${text}`);
773
+ }
774
+ return deviceAuthorizationResponseSchema.parse(await res.json());
775
+ }
776
+ function metaString(metadata, key) {
777
+ const v = metadata?.[key];
778
+ return typeof v === "string" ? v : void 0;
779
+ }
780
+ function isExpired(cred) {
781
+ if (!cred.expiresAt) return false;
782
+ const t = Date.parse(cred.expiresAt);
783
+ if (Number.isNaN(t)) return true;
784
+ return t <= Date.now();
785
+ }
786
+ function resultFromStored(cred) {
787
+ const refreshToken = metaString(cred.metadata, "refreshToken");
788
+ const scope = metaString(cred.metadata, "scope");
789
+ const subject = metaString(cred.metadata, "subject");
790
+ const revocationId = metaString(cred.metadata, "revocationId");
791
+ return {
792
+ accessToken: cred.value,
793
+ tokenKind: "daemon",
794
+ ...refreshToken ? { refreshToken } : {},
795
+ ...scope ? { scope } : {},
796
+ ...subject ? { subject } : {},
797
+ ...revocationId ? { revocationId } : {}
798
+ };
799
+ }
800
+ async function refreshAccessToken(tokenEndpoint, refreshToken, clientId, signal) {
801
+ try {
802
+ const data = await postFormOAuth(
803
+ tokenEndpoint,
804
+ {
805
+ grant_type: REFRESH_GRANT_TYPE,
806
+ refresh_token: refreshToken,
807
+ client_id: clientId
808
+ },
809
+ tokenResponseSchema,
810
+ signal
811
+ );
812
+ if ("error" in data) return null;
813
+ return data;
814
+ } catch {
815
+ return null;
816
+ }
817
+ }
818
+ async function persistAndResult(store, ref, o) {
819
+ const expiresAt = o.expiresIn ? new Date(Date.now() + o.expiresIn * 1e3).toISOString() : void 0;
820
+ const metadata = {
821
+ obtainedAt: (/* @__PURE__ */ new Date()).toISOString(),
822
+ ...o.refreshToken ? { refreshToken: o.refreshToken } : {},
823
+ ...o.scope ? { scope: o.scope } : {},
824
+ ...o.subject ? { subject: o.subject } : {},
825
+ ...o.deviceLabel ? { deviceLabel: o.deviceLabel } : {},
826
+ ...o.revocationId ? { revocationId: o.revocationId } : {}
827
+ };
828
+ await store.write(ref, {
829
+ value: o.accessToken,
830
+ kind: "daemon",
831
+ ...expiresAt ? { expiresAt } : {},
832
+ metadata
833
+ });
834
+ return {
835
+ accessToken: o.accessToken,
836
+ tokenKind: "daemon",
837
+ ...o.refreshToken ? { refreshToken: o.refreshToken } : {},
838
+ ...o.scope ? { scope: o.scope } : {},
839
+ ...o.subject ? { subject: o.subject } : {},
840
+ ...o.revocationId ? { revocationId: o.revocationId } : {}
841
+ };
842
+ }
843
+ var deviceCodeFlowEngine = {
844
+ id: "device-code",
845
+ async run(provider, discovered, opts) {
846
+ const { auth } = provider;
847
+ if (auth.flow !== "device-code") {
848
+ throw new Error(`deviceCodeFlowEngine: invoked with flow="${auth.flow}"`);
849
+ }
850
+ const config = auth;
851
+ const clientId = config.clientId ?? "agentproto-cli";
852
+ const server = opts.server.replace(/\/$/, "");
853
+ const deviceAuthorizationEndpoint = discovered?.deviceAuthorizationEndpoint ?? `${server}/oauth/device`;
854
+ const tokenEndpoint = discovered?.tokenEndpoint ?? `${server}/oauth/token`;
855
+ assertSecureUrl(deviceAuthorizationEndpoint);
856
+ assertSecureUrl(tokenEndpoint);
857
+ const store = opts.store ?? new KeychainStore();
858
+ const { ref, legacyRef } = resolveStoreRefs(
859
+ config.tokenStore,
860
+ server,
861
+ provider.audience
862
+ );
863
+ async function tryRefresh(stored) {
864
+ const refreshToken = metaString(stored.metadata, "refreshToken");
865
+ if (!refreshToken) return null;
866
+ const refreshed = await refreshAccessToken(
867
+ tokenEndpoint,
868
+ refreshToken,
869
+ clientId,
870
+ opts.signal
871
+ );
872
+ if (!refreshed) return null;
873
+ return persistAndResult(store, ref, {
874
+ accessToken: refreshed.access_token,
875
+ expiresIn: refreshed.expires_in,
876
+ refreshToken: refreshed.refresh_token ?? refreshToken,
877
+ scope: refreshed.scope ?? metaString(stored.metadata, "scope"),
878
+ subject: refreshed.subject ?? metaString(stored.metadata, "subject"),
879
+ deviceLabel: config.deviceLabel ?? metaString(stored.metadata, "deviceLabel"),
880
+ revocationId: refreshed.revocation_id ?? metaString(stored.metadata, "revocationId")
881
+ });
882
+ }
883
+ if (opts.refreshOnly) {
884
+ const stored = await readStoreRefWithFallback(store, ref, legacyRef);
885
+ if (stored) {
886
+ if (!isExpired(stored)) {
887
+ return resultFromStored(stored);
888
+ }
889
+ const refreshed = await tryRefresh(stored);
890
+ if (refreshed) return refreshed;
891
+ }
892
+ throw new CeremonyRequiredError(
893
+ `${provider.id}: no fresh or refreshable device-code credential \u2014 an interactive ceremony would be required, but refreshOnly was set`
894
+ );
895
+ }
896
+ if (!opts.force) {
897
+ const stored = await readStoreRefWithFallback(store, ref, legacyRef);
898
+ if (stored) {
899
+ if (!isExpired(stored)) {
900
+ return resultFromStored(stored);
901
+ }
902
+ const refreshed = await tryRefresh(stored);
903
+ if (refreshed) return refreshed;
904
+ }
905
+ }
906
+ const params = { client_id: clientId };
907
+ if (config.scope) params.scope = config.scope;
908
+ if (config.deviceLabel) params.device_label = config.deviceLabel;
909
+ const authz = await postDeviceAuthorization(
910
+ deviceAuthorizationEndpoint,
911
+ params,
912
+ opts.signal
913
+ );
914
+ const intervalS = authz.interval ?? DEFAULT_POLL_INTERVAL_S2;
915
+ const windowMin = Math.round(authz.expires_in / 60);
916
+ process.stderr.write(`
917
+ `);
918
+ process.stderr.write(` Approve ${provider.id} access in your browser
919
+
920
+ `);
921
+ process.stderr.write(` Code: ${authz.user_code}
922
+ `);
923
+ process.stderr.write(` URL: ${authz.verification_uri}
924
+
925
+ `);
926
+ if (opts.openBrowser !== false) {
927
+ await openBrowser(authz.verification_uri_complete ?? authz.verification_uri);
928
+ }
929
+ process.stderr.write(` Waiting for approval (${windowMin} min)\u2026
930
+
931
+ `);
932
+ const tokenResult = await pollDeviceGrant({
933
+ tokenEndpoint,
934
+ grantType: DEVICE_CODE_GRANT_TYPE,
935
+ params: { device_code: authz.device_code, client_id: clientId },
936
+ intervalS,
937
+ expiresIn: authz.expires_in,
938
+ signal: opts.signal
939
+ });
940
+ return persistAndResult(store, ref, {
941
+ accessToken: tokenResult.access_token,
942
+ expiresIn: tokenResult.expires_in,
943
+ refreshToken: tokenResult.refresh_token,
944
+ scope: tokenResult.scope,
945
+ subject: tokenResult.subject,
946
+ deviceLabel: config.deviceLabel,
947
+ revocationId: tokenResult.revocation_id
948
+ });
949
+ }
950
+ };
597
951
 
598
952
  // src/flow-engines/index.ts
599
953
  var FLOW_ENGINES = {
600
954
  [patFlowEngine.id]: patFlowEngine,
601
- [serviceAuthFlowEngine.id]: serviceAuthFlowEngine
955
+ [serviceAuthFlowEngine.id]: serviceAuthFlowEngine,
956
+ [deviceCodeFlowEngine.id]: deviceCodeFlowEngine
602
957
  };
603
958
 
604
959
  // src/run-flow.ts
@@ -611,7 +966,7 @@ async function runAuthFlow(provider, opts) {
611
966
  );
612
967
  }
613
968
  let discovered = opts.discovered !== void 0 ? opts.discovered : null;
614
- if (discovered === null && flowId === "service-auth") {
969
+ if (discovered === null && (flowId === "service-auth" || flowId === "device-code")) {
615
970
  try {
616
971
  discovered = await discoverEndpoints(opts.server, { signal: opts.signal });
617
972
  } catch (err) {
@@ -625,10 +980,194 @@ async function runAuthFlow(provider, opts) {
625
980
  return engine.run(provider, discovered, opts);
626
981
  }
627
982
 
983
+ // src/store/memory-store.ts
984
+ function keyOf(ref) {
985
+ return `${ref.path}\0${ref.account ?? ref.path}`;
986
+ }
987
+ var MemoryStore = class {
988
+ entries = /* @__PURE__ */ new Map();
989
+ async read(ref) {
990
+ const entry = this.entries.get(keyOf(ref));
991
+ return entry ? { ...entry } : void 0;
992
+ }
993
+ async write(ref, cred) {
994
+ this.entries.set(keyOf(ref), { ...cred });
995
+ }
996
+ async delete(ref) {
997
+ this.entries.delete(keyOf(ref));
998
+ }
999
+ };
1000
+ var ALGORITHM = "aes-256-gcm";
1001
+ var KEY_ENV_VAR = "AGENTPROTO_STORE_KEY";
1002
+ var IV_BYTES = 12;
1003
+ var sealedEntrySchema = z.object({
1004
+ iv: z.string(),
1005
+ tag: z.string(),
1006
+ ciphertext: z.string()
1007
+ });
1008
+ var sealedFileSchema = z.record(z.string(), sealedEntrySchema);
1009
+ var storedCredentialSchema = z.object({
1010
+ value: z.string(),
1011
+ kind: z.enum(["pat", "assertion", "oat", "daemon"]),
1012
+ expiresAt: z.string().optional(),
1013
+ metadata: z.record(z.string(), z.unknown()).optional()
1014
+ });
1015
+ function keyOf2(ref) {
1016
+ return `${ref.path} ${ref.account ?? ref.path}`;
1017
+ }
1018
+ function isEnoent(err) {
1019
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
1020
+ }
1021
+ function loadKey() {
1022
+ const raw = process.env[KEY_ENV_VAR];
1023
+ if (!raw) {
1024
+ throw new Error(
1025
+ `FileStore: missing ${KEY_ENV_VAR} \u2014 set a 32-byte key (64 hex chars, or base64) to use the encrypted file store. Refusing to store credentials in plaintext.`
1026
+ );
1027
+ }
1028
+ const key = /^[0-9a-fA-F]{64}$/.test(raw) ? Buffer.from(raw, "hex") : Buffer.from(raw, "base64");
1029
+ if (key.length !== 32) {
1030
+ throw new Error(
1031
+ `FileStore: ${KEY_ENV_VAR} must decode to exactly 32 bytes (got ${key.length}). Provide a 64-char hex string or base64 encoding 32 bytes.`
1032
+ );
1033
+ }
1034
+ return key;
1035
+ }
1036
+ function seal(key, plaintext) {
1037
+ const iv = randomBytes(IV_BYTES);
1038
+ const cipher = createCipheriv(ALGORITHM, key, iv);
1039
+ const ciphertext = Buffer.concat([
1040
+ cipher.update(plaintext, "utf8"),
1041
+ cipher.final()
1042
+ ]);
1043
+ return {
1044
+ iv: iv.toString("base64"),
1045
+ tag: cipher.getAuthTag().toString("base64"),
1046
+ ciphertext: ciphertext.toString("base64")
1047
+ };
1048
+ }
1049
+ function unseal(key, entry) {
1050
+ const decipher = createDecipheriv(
1051
+ ALGORITHM,
1052
+ key,
1053
+ Buffer.from(entry.iv, "base64")
1054
+ );
1055
+ decipher.setAuthTag(Buffer.from(entry.tag, "base64"));
1056
+ const plaintext = Buffer.concat([
1057
+ decipher.update(Buffer.from(entry.ciphertext, "base64")),
1058
+ decipher.final()
1059
+ ]);
1060
+ return plaintext.toString("utf8");
1061
+ }
1062
+ var FileStore = class {
1063
+ constructor(filePath) {
1064
+ this.filePath = filePath;
1065
+ }
1066
+ filePath;
1067
+ async read(ref) {
1068
+ const key = loadKey();
1069
+ const file = await this.readFile();
1070
+ const sealed = file[keyOf2(ref)];
1071
+ if (!sealed) return void 0;
1072
+ return storedCredentialSchema.parse(JSON.parse(unseal(key, sealed)));
1073
+ }
1074
+ async write(ref, cred) {
1075
+ const key = loadKey();
1076
+ const file = await this.readFile();
1077
+ file[keyOf2(ref)] = seal(key, JSON.stringify(cred));
1078
+ await this.writeFile(file);
1079
+ }
1080
+ async delete(ref) {
1081
+ const file = await this.readFile();
1082
+ delete file[keyOf2(ref)];
1083
+ await this.writeFile(file);
1084
+ }
1085
+ async readFile() {
1086
+ try {
1087
+ const raw = await readFile(this.filePath, "utf8");
1088
+ return sealedFileSchema.parse(JSON.parse(raw));
1089
+ } catch (err) {
1090
+ if (isEnoent(err)) return {};
1091
+ throw err;
1092
+ }
1093
+ }
1094
+ async writeFile(file) {
1095
+ await mkdir(dirname(this.filePath), { recursive: true });
1096
+ await writeFile(this.filePath, JSON.stringify(file, null, 2), "utf8");
1097
+ }
1098
+ };
1099
+
1100
+ // src/broker.ts
1101
+ var EXPIRY_SKEW_MS = 6e4;
1102
+ function parsePath(path) {
1103
+ const slash = path.indexOf("/");
1104
+ if (slash === -1) return { providerId: path };
1105
+ return { providerId: path.slice(0, slash), account: path.slice(slash + 1) };
1106
+ }
1107
+ function isFresh(cred) {
1108
+ if (!cred.expiresAt) return true;
1109
+ const expiresAtMs = Date.parse(cred.expiresAt);
1110
+ if (Number.isNaN(expiresAtMs)) return false;
1111
+ return expiresAtMs - EXPIRY_SKEW_MS > Date.now();
1112
+ }
1113
+ function bearerHeaders(value, kind) {
1114
+ if (kind === "pat" || kind === "oat" || kind === "daemon") {
1115
+ return { Authorization: `Bearer ${value}` };
1116
+ }
1117
+ return void 0;
1118
+ }
1119
+ var CredentialBroker = class {
1120
+ store;
1121
+ getProvider;
1122
+ constructor(opts) {
1123
+ this.store = opts.store;
1124
+ this.getProvider = opts.getProvider;
1125
+ }
1126
+ async resolveHeaders(o) {
1127
+ const { providerId, account } = parsePath(o.path);
1128
+ const provider = this.getProvider(providerId);
1129
+ if (!provider) {
1130
+ throw new Error(`CredentialBroker: unknown auth provider "${providerId}"`);
1131
+ }
1132
+ if (o.audience !== void 0 && provider.audience !== void 0 && o.audience !== provider.audience) {
1133
+ throw new Error(
1134
+ `CredentialBroker: provider "${providerId}" is scoped to audience "${provider.audience}", requested "${o.audience}"`
1135
+ );
1136
+ }
1137
+ const server = o.server ?? provider.apiBase;
1138
+ const { ref, legacyRef } = resolveStoreRefs(
1139
+ provider.auth.tokenStore,
1140
+ server,
1141
+ provider.audience
1142
+ );
1143
+ if (account !== void 0) {
1144
+ ref.account = account;
1145
+ legacyRef.account = account;
1146
+ }
1147
+ const stored = await readStoreRefWithFallback(this.store, ref, legacyRef);
1148
+ if (stored && isFresh(stored)) {
1149
+ const headers2 = bearerHeaders(stored.value, stored.kind);
1150
+ if (headers2) return headers2;
1151
+ }
1152
+ const result = await runAuthFlow(provider, {
1153
+ server,
1154
+ store: this.store,
1155
+ signal: o.signal
1156
+ });
1157
+ const headers = result.accessToken !== void 0 ? bearerHeaders(result.accessToken, result.tokenKind) : void 0;
1158
+ if (!headers) {
1159
+ throw new Error(
1160
+ `CredentialBroker: auth flow for "${providerId}" produced no usable access token`
1161
+ );
1162
+ }
1163
+ return headers;
1164
+ }
1165
+ };
1166
+
628
1167
  // src/index.ts
629
1168
  var SPEC_NAME = "agentauth";
630
1169
  var SPEC_VERSION = "v1";
631
1170
 
632
- export { BUILTIN_AUTH_PROVIDERS, DiscoveryError, FLOW_ENGINES, SPEC_NAME, SPEC_VERSION, authConfigSchema, authProviderFrontmatterSchema, defineAuthProvider, discoverEndpoints, getAuthProvider, guildeAuthProvider, installConfigSchema, listAuthProviderIds, listAuthProviders, parseAuthProviderManifest, parseAuthProviderManifestRaw, readKeychainToken, registerAuthProvider, resolveAccount, runAuthFlow, tokenStoreSpecSchema, writeKeychainToken };
1171
+ export { BUILTIN_AUTH_PROVIDERS, CeremonyRequiredError, CredentialBroker, DiscoveryError, FLOW_ENGINES, FileStore, KeychainStore, MemoryStore, SPEC_NAME, SPEC_VERSION, authConfigSchema, authProviderFrontmatterSchema, defineAuthProvider, discoverEndpoints, getAuthProvider, guildeAuthProvider, installConfigSchema, listAuthProviderIds, listAuthProviders, parseAuthProviderManifest, parseAuthProviderManifestRaw, readKeychainToken, registerAuthProvider, resolveAccount, resolveStoreRef, runAuthFlow, tokenStoreSpecSchema, writeKeychainToken };
633
1172
  //# sourceMappingURL=index.mjs.map
634
1173
  //# sourceMappingURL=index.mjs.map