@agentproto/auth 0.1.1 → 1.0.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/index.mjs CHANGED
@@ -4,9 +4,10 @@ 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';
7
+ import { createHash, createDecipheriv, randomBytes, createCipheriv } from 'crypto';
8
8
  import { readFile, mkdir, writeFile } from 'fs/promises';
9
- import { dirname } from 'path';
9
+ import { dirname, resolve, join } from 'path';
10
+ import { homedir } from 'os';
10
11
 
11
12
  /**
12
13
  * @agentproto/auth v0.1.0-alpha
@@ -379,6 +380,21 @@ async function writeKeychainToken(service, account, token) {
379
380
  "agentproto auth token"
380
381
  ]);
381
382
  }
383
+ async function deleteKeychainToken(service, account) {
384
+ assertKeychainSupported();
385
+ try {
386
+ await exec("security", [
387
+ "delete-generic-password",
388
+ "-s",
389
+ service,
390
+ "-a",
391
+ account
392
+ ]);
393
+ return true;
394
+ } catch {
395
+ return false;
396
+ }
397
+ }
382
398
 
383
399
  // src/store/keychain-store.ts
384
400
  var envelopeSchema = z.object({
@@ -414,6 +430,10 @@ var KeychainStore = class {
414
430
  };
415
431
  await writeKeychainToken(ref.path, account, JSON.stringify(envelope));
416
432
  }
433
+ async delete(ref) {
434
+ const account = ref.account ?? ref.path;
435
+ await deleteKeychainToken(ref.path, account);
436
+ }
417
437
  };
418
438
 
419
439
  // src/store/resolve-ref.ts
@@ -443,16 +463,16 @@ async function promptToken(label) {
443
463
  const input = process.stdin;
444
464
  const out = process.stderr;
445
465
  if (!input.isTTY) {
446
- return new Promise((resolve) => {
466
+ return new Promise((resolve2) => {
447
467
  const rl = createInterface({ input, output: out });
448
468
  out.write(`${label}: `);
449
469
  rl.question("", (answer) => {
450
470
  rl.close();
451
- resolve(answer.trim());
471
+ resolve2(answer.trim());
452
472
  });
453
473
  });
454
474
  }
455
- return new Promise((resolve, reject) => {
475
+ return new Promise((resolve2, reject) => {
456
476
  out.write(`${label}: `);
457
477
  const chars = [];
458
478
  input.setRawMode(true);
@@ -470,7 +490,7 @@ async function promptToken(label) {
470
490
  case "\n":
471
491
  out.write("\n");
472
492
  cleanup();
473
- resolve(chars.join("").trim());
493
+ resolve2(chars.join("").trim());
474
494
  return;
475
495
  case "":
476
496
  out.write("\n");
@@ -480,7 +500,7 @@ async function promptToken(label) {
480
500
  case "":
481
501
  out.write("\n");
482
502
  cleanup();
483
- resolve(chars.join("").trim());
503
+ resolve2(chars.join("").trim());
484
504
  return;
485
505
  case "\x7F":
486
506
  // Backspace / Delete
@@ -1163,11 +1183,307 @@ var CredentialBroker = class {
1163
1183
  return headers;
1164
1184
  }
1165
1185
  };
1186
+ var authMethodSchema = z.enum(["oauth-bearer", "api-key"]);
1187
+ var modelCurationSchema = z.object({
1188
+ mode: z.enum(["all", "allow"]),
1189
+ ids: z.array(z.string())
1190
+ });
1191
+ var costBudgetSchema = z.object({
1192
+ maxCostUsd: z.number(),
1193
+ window: z.string(),
1194
+ scope: z.enum(["session", "profile"])
1195
+ });
1196
+ var authProfileSchema = z.object({
1197
+ id: z.string(),
1198
+ vendor: z.string(),
1199
+ method: authMethodSchema,
1200
+ credentialRef: z.string().optional(),
1201
+ source: z.string().optional(),
1202
+ label: z.string().optional(),
1203
+ // Additive, back-compat fields — an entry that predates them parses
1204
+ // unchanged (both optional), and `endpoint` still shadows the on-disk
1205
+ // `vendor` alias below.
1206
+ disabled: z.boolean().optional(),
1207
+ models: modelCurationSchema.optional(),
1208
+ costBudget: costBudgetSchema.optional(),
1209
+ origin: z.string().optional()
1210
+ }).transform(({ vendor, ...profile }) => ({ ...profile, endpoint: vendor }));
1211
+ var authProfilesFileSchema = z.object({
1212
+ version: z.literal(1),
1213
+ profiles: z.record(z.string(), authProfileSchema)
1214
+ });
1215
+ function emptyFile() {
1216
+ return { version: 1, profiles: {} };
1217
+ }
1218
+ function authProfilesPath() {
1219
+ return resolve(homedir(), ".agentproto", "auth-profiles.json");
1220
+ }
1221
+ async function loadAuthProfiles() {
1222
+ try {
1223
+ const raw = await readFile(authProfilesPath(), "utf8");
1224
+ return authProfilesFileSchema.parse(JSON.parse(raw));
1225
+ } catch {
1226
+ return emptyFile();
1227
+ }
1228
+ }
1229
+ async function writeAuthProfiles(file) {
1230
+ const dir = join(homedir(), ".agentproto");
1231
+ await mkdir(dir, { recursive: true });
1232
+ const diskFile = {
1233
+ version: file.version,
1234
+ profiles: Object.fromEntries(
1235
+ Object.entries(file.profiles).map(([id, { endpoint, ...profile }]) => [
1236
+ id,
1237
+ { ...profile, vendor: endpoint }
1238
+ ])
1239
+ )
1240
+ };
1241
+ await writeFile(authProfilesPath(), JSON.stringify(diskFile, null, 2) + "\n", {
1242
+ encoding: "utf8",
1243
+ mode: 384
1244
+ });
1245
+ }
1246
+ async function listAuthProfiles(endpoint) {
1247
+ const file = await loadAuthProfiles();
1248
+ const all = Object.values(file.profiles);
1249
+ return endpoint === void 0 ? all : all.filter((p) => p.endpoint === endpoint);
1250
+ }
1251
+ async function getAuthProfile(id) {
1252
+ const file = await loadAuthProfiles();
1253
+ return file.profiles[id];
1254
+ }
1255
+ async function addAuthProfile(profile) {
1256
+ const file = await loadAuthProfiles();
1257
+ file.profiles[profile.id] = profile;
1258
+ await writeAuthProfiles(file);
1259
+ }
1260
+ async function removeAuthProfile(id) {
1261
+ const file = await loadAuthProfiles();
1262
+ if (!(id in file.profiles)) return false;
1263
+ delete file.profiles[id];
1264
+ await writeAuthProfiles(file);
1265
+ return true;
1266
+ }
1267
+ var AuthProfileValidationError = class extends Error {
1268
+ constructor(message) {
1269
+ super(message);
1270
+ this.name = "AuthProfileValidationError";
1271
+ }
1272
+ };
1273
+ var AUTH_METHODS = ["oauth-bearer", "api-key"];
1274
+ var SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1275
+ function validateCreateInput(input) {
1276
+ const id = (input.id ?? "").trim();
1277
+ if (!id) throw new AuthProfileValidationError("id is required");
1278
+ if (!SLUG_RE.test(id)) {
1279
+ throw new AuthProfileValidationError(
1280
+ `id "${id}" is invalid \u2014 use letters, digits, ".", "_" or "-", starting with a letter or digit`
1281
+ );
1282
+ }
1283
+ const endpoint = (input.endpoint ?? "").trim();
1284
+ if (!endpoint) throw new AuthProfileValidationError("endpoint is required");
1285
+ if (!SLUG_RE.test(endpoint)) {
1286
+ throw new AuthProfileValidationError(
1287
+ `endpoint "${endpoint}" is invalid \u2014 use letters, digits, ".", "_" or "-"`
1288
+ );
1289
+ }
1290
+ const method = input.method;
1291
+ if (!AUTH_METHODS.includes(method)) {
1292
+ throw new AuthProfileValidationError(
1293
+ `method must be one of ${AUTH_METHODS.join(" | ")} (got "${String(method)}")`
1294
+ );
1295
+ }
1296
+ const credential = input.credential !== void 0 ? input.credential.trim() : void 0;
1297
+ const source = input.source?.trim();
1298
+ if (method === "api-key") {
1299
+ if (source) {
1300
+ throw new AuthProfileValidationError(
1301
+ "source is only supported for oauth-bearer profiles \u2014 api-key profiles require a credential"
1302
+ );
1303
+ }
1304
+ if (!credential) throw new AuthProfileValidationError("credential is required");
1305
+ } else {
1306
+ if (credential && source) {
1307
+ throw new AuthProfileValidationError(
1308
+ "give either credential or source, not both \u2014 a source-backed profile stores no secret"
1309
+ );
1310
+ }
1311
+ if (!credential && !source) {
1312
+ throw new AuthProfileValidationError("credential or source is required");
1313
+ }
1314
+ }
1315
+ const label = input.label?.trim();
1316
+ const credentialRef = input.credentialRef?.trim();
1317
+ if (credentialRef !== void 0 && credentialRef !== "" && !SLUG_RE.test(credentialRef)) {
1318
+ throw new AuthProfileValidationError(
1319
+ `credentialRef "${credentialRef}" is invalid \u2014 use letters, digits, ".", "_" or "-"`
1320
+ );
1321
+ }
1322
+ const origin = input.origin?.trim();
1323
+ return {
1324
+ id,
1325
+ endpoint,
1326
+ method,
1327
+ ...credential ? { credential } : {},
1328
+ ...source ? { source } : {},
1329
+ ...label ? { label } : {},
1330
+ ...credentialRef ? { credentialRef } : {},
1331
+ ...origin ? { origin } : {}
1332
+ };
1333
+ }
1334
+ function deriveCredentialRef(input) {
1335
+ const parts = ["agentproto", "auth", input.endpoint];
1336
+ if (input.method === "oauth-bearer") parts.push("sub");
1337
+ if (input.qualifier) parts.push(input.qualifier);
1338
+ return parts.join(".");
1339
+ }
1340
+ function fingerprintCredential(secret) {
1341
+ return createHash("sha256").update(secret, "utf8").digest("hex").slice(0, 12);
1342
+ }
1343
+ var MIN_LEN_FOR_LAST4 = 8;
1344
+ function credentialIdentity(secret) {
1345
+ return {
1346
+ fingerprint: fingerprintCredential(secret),
1347
+ ...secret.length >= MIN_LEN_FOR_LAST4 ? { last4: secret.slice(-4) } : {}
1348
+ };
1349
+ }
1350
+ function credentialKind(method) {
1351
+ return method === "oauth-bearer" ? "oat" : "pat";
1352
+ }
1353
+ async function createAuthProfile(input, deps) {
1354
+ const v = validateCreateInput(input);
1355
+ if (await deps.getProfile(v.id)) {
1356
+ throw new AuthProfileValidationError(
1357
+ `a profile with id "${v.id}" already exists \u2014 delete it first or pick another id`
1358
+ );
1359
+ }
1360
+ if (v.source !== void 0) {
1361
+ const profile2 = {
1362
+ id: v.id,
1363
+ endpoint: v.endpoint,
1364
+ method: v.method,
1365
+ source: v.source,
1366
+ ...v.label ? { label: v.label } : {},
1367
+ ...v.origin ? { origin: v.origin } : {}
1368
+ };
1369
+ await deps.addProfile(profile2);
1370
+ return {
1371
+ id: profile2.id,
1372
+ endpoint: profile2.endpoint,
1373
+ method: profile2.method,
1374
+ source: v.source,
1375
+ ...profile2.label ? { label: profile2.label } : {},
1376
+ ...profile2.origin ? { origin: profile2.origin } : {}
1377
+ };
1378
+ }
1379
+ const credential = v.credential;
1380
+ const existing = await deps.listProfiles();
1381
+ const takenRefs = new Set(existing.map((p) => p.credentialRef));
1382
+ let credentialRef = v.credentialRef ?? deriveCredentialRef(v);
1383
+ if (!v.credentialRef && takenRefs.has(credentialRef)) {
1384
+ credentialRef = deriveCredentialRef({ ...v, qualifier: v.id });
1385
+ }
1386
+ await deps.store.write(
1387
+ { path: credentialRef },
1388
+ { value: credential, kind: credentialKind(v.method) }
1389
+ );
1390
+ const profile = {
1391
+ id: v.id,
1392
+ endpoint: v.endpoint,
1393
+ method: v.method,
1394
+ credentialRef,
1395
+ ...v.label ? { label: v.label } : {},
1396
+ ...v.origin ? { origin: v.origin } : {}
1397
+ };
1398
+ await deps.addProfile(profile);
1399
+ return {
1400
+ id: profile.id,
1401
+ endpoint: profile.endpoint,
1402
+ method: profile.method,
1403
+ credentialRef,
1404
+ ...profile.label ? { label: profile.label } : {},
1405
+ ...profile.origin ? { origin: profile.origin } : {},
1406
+ fingerprint: fingerprintCredential(credential)
1407
+ };
1408
+ }
1409
+ async function deleteAuthProfile(id, deps) {
1410
+ const trimmed = (id ?? "").trim();
1411
+ if (!trimmed) throw new AuthProfileValidationError("id is required");
1412
+ const profile = await deps.getProfile(trimmed);
1413
+ if (!profile) return { deleted: false, id: trimmed };
1414
+ await deps.removeProfile(trimmed);
1415
+ if (profile.credentialRef === void 0) {
1416
+ return { deleted: true, id: trimmed };
1417
+ }
1418
+ const stillReferenced = (await deps.listProfiles()).some(
1419
+ (p) => p.credentialRef === profile.credentialRef
1420
+ );
1421
+ if (!stillReferenced && deps.store.delete) {
1422
+ await deps.store.delete({ path: profile.credentialRef });
1423
+ }
1424
+ return { deleted: true, id: trimmed, credentialRef: profile.credentialRef };
1425
+ }
1426
+ async function setAuthProfileEnabled(id, enabled, deps) {
1427
+ const trimmed = (id ?? "").trim();
1428
+ if (!trimmed) throw new AuthProfileValidationError("id is required");
1429
+ const profile = await deps.getProfile(trimmed);
1430
+ if (!profile) {
1431
+ throw new AuthProfileValidationError(`no profile with id "${trimmed}"`);
1432
+ }
1433
+ const { disabled: _drop, ...rest } = profile;
1434
+ const updated = enabled ? { ...rest } : { ...rest, disabled: true };
1435
+ await deps.addProfile(updated);
1436
+ return updated;
1437
+ }
1438
+ async function setAuthProfileModels(id, curation, deps) {
1439
+ const trimmed = (id ?? "").trim();
1440
+ if (!trimmed) throw new AuthProfileValidationError("id is required");
1441
+ if (curation.mode !== "all" && curation.mode !== "allow") {
1442
+ throw new AuthProfileValidationError(
1443
+ `models.mode must be "all" or "allow" (got "${String(curation.mode)}")`
1444
+ );
1445
+ }
1446
+ const profile = await deps.getProfile(trimmed);
1447
+ if (!profile) {
1448
+ throw new AuthProfileValidationError(`no profile with id "${trimmed}"`);
1449
+ }
1450
+ const { models: _drop, ...rest } = profile;
1451
+ if (curation.mode === "all") {
1452
+ const updated2 = { ...rest };
1453
+ await deps.addProfile(updated2);
1454
+ return updated2;
1455
+ }
1456
+ const ids = [...new Set((curation.ids ?? []).map((s) => s.trim()).filter(Boolean))];
1457
+ const updated = { ...rest, models: { mode: "allow", ids } };
1458
+ await deps.addProfile(updated);
1459
+ return updated;
1460
+ }
1461
+
1462
+ // src/eligibility.ts
1463
+ function resolveEndpoint(manifest, route) {
1464
+ const endpoint = manifest.endpointByRoute[route];
1465
+ if (endpoint === void 0) {
1466
+ throw new Error(
1467
+ `resolveEndpoint: adapter "${manifest.id}" declares no endpoint for route "${route}"`
1468
+ );
1469
+ }
1470
+ return { endpoint };
1471
+ }
1472
+ function methodsPresentable(manifest, route) {
1473
+ return [...manifest.methodsByRoute[route] ?? []];
1474
+ }
1475
+ function eligibleProfiles(profiles, manifest, route) {
1476
+ const { endpoint } = resolveEndpoint(manifest, route);
1477
+ const methods = methodsPresentable(manifest, route);
1478
+ return profiles.filter(
1479
+ (p) => !p.disabled && p.endpoint === endpoint && methods.includes(p.method)
1480
+ );
1481
+ }
1166
1482
 
1167
1483
  // src/index.ts
1168
1484
  var SPEC_NAME = "agentauth";
1169
1485
  var SPEC_VERSION = "v1";
1170
1486
 
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 };
1487
+ export { AuthProfileValidationError, BUILTIN_AUTH_PROVIDERS, CeremonyRequiredError, CredentialBroker, DiscoveryError, FLOW_ENGINES, FileStore, KeychainStore, MemoryStore, SPEC_NAME, SPEC_VERSION, addAuthProfile, authConfigSchema, authProfilesPath, authProviderFrontmatterSchema, createAuthProfile, credentialIdentity, defineAuthProvider, deleteAuthProfile, deleteKeychainToken, deriveCredentialRef, discoverEndpoints, eligibleProfiles, fingerprintCredential, getAuthProfile, getAuthProvider, guildeAuthProvider, installConfigSchema, listAuthProfiles, listAuthProviderIds, listAuthProviders, loadAuthProfiles, methodsPresentable, parseAuthProviderManifest, parseAuthProviderManifestRaw, readKeychainToken, registerAuthProvider, removeAuthProfile, resolveAccount, resolveEndpoint, resolveStoreRef, runAuthFlow, setAuthProfileEnabled, setAuthProfileModels, tokenStoreSpecSchema, validateCreateInput, writeKeychainToken };
1172
1488
  //# sourceMappingURL=index.mjs.map
1173
1489
  //# sourceMappingURL=index.mjs.map