@agentproto/auth 0.2.0 → 1.0.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/README.md +2 -1
- package/dist/index.d.ts +370 -31
- package/dist/index.mjs +277 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/skill/auth/SKILL.md +0 -131
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ 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
9
|
import { dirname, resolve, join } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
@@ -380,6 +380,21 @@ async function writeKeychainToken(service, account, token) {
|
|
|
380
380
|
"agentproto auth token"
|
|
381
381
|
]);
|
|
382
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
|
+
}
|
|
383
398
|
|
|
384
399
|
// src/store/keychain-store.ts
|
|
385
400
|
var envelopeSchema = z.object({
|
|
@@ -415,6 +430,10 @@ var KeychainStore = class {
|
|
|
415
430
|
};
|
|
416
431
|
await writeKeychainToken(ref.path, account, JSON.stringify(envelope));
|
|
417
432
|
}
|
|
433
|
+
async delete(ref) {
|
|
434
|
+
const account = ref.account ?? ref.path;
|
|
435
|
+
await deleteKeychainToken(ref.path, account);
|
|
436
|
+
}
|
|
418
437
|
};
|
|
419
438
|
|
|
420
439
|
// src/store/resolve-ref.ts
|
|
@@ -1165,13 +1184,30 @@ var CredentialBroker = class {
|
|
|
1165
1184
|
}
|
|
1166
1185
|
};
|
|
1167
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
|
+
});
|
|
1168
1196
|
var authProfileSchema = z.object({
|
|
1169
1197
|
id: z.string(),
|
|
1170
1198
|
vendor: z.string(),
|
|
1171
1199
|
method: authMethodSchema,
|
|
1172
|
-
credentialRef: z.string(),
|
|
1173
|
-
|
|
1174
|
-
|
|
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 }));
|
|
1175
1211
|
var authProfilesFileSchema = z.object({
|
|
1176
1212
|
version: z.literal(1),
|
|
1177
1213
|
profiles: z.record(z.string(), authProfileSchema)
|
|
@@ -1193,15 +1229,24 @@ async function loadAuthProfiles() {
|
|
|
1193
1229
|
async function writeAuthProfiles(file) {
|
|
1194
1230
|
const dir = join(homedir(), ".agentproto");
|
|
1195
1231
|
await mkdir(dir, { recursive: true });
|
|
1196
|
-
|
|
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", {
|
|
1197
1242
|
encoding: "utf8",
|
|
1198
1243
|
mode: 384
|
|
1199
1244
|
});
|
|
1200
1245
|
}
|
|
1201
|
-
async function listAuthProfiles(
|
|
1246
|
+
async function listAuthProfiles(endpoint) {
|
|
1202
1247
|
const file = await loadAuthProfiles();
|
|
1203
1248
|
const all = Object.values(file.profiles);
|
|
1204
|
-
return
|
|
1249
|
+
return endpoint === void 0 ? all : all.filter((p) => p.endpoint === endpoint);
|
|
1205
1250
|
}
|
|
1206
1251
|
async function getAuthProfile(id) {
|
|
1207
1252
|
const file = await loadAuthProfiles();
|
|
@@ -1219,30 +1264,248 @@ async function removeAuthProfile(id) {
|
|
|
1219
1264
|
await writeAuthProfiles(file);
|
|
1220
1265
|
return true;
|
|
1221
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
|
+
async function refreshAuthProfileModels(id, currentIds, deps) {
|
|
1462
|
+
const trimmed = (id ?? "").trim();
|
|
1463
|
+
if (!trimmed) throw new AuthProfileValidationError("id is required");
|
|
1464
|
+
const profile = await deps.getProfile(trimmed);
|
|
1465
|
+
if (!profile) {
|
|
1466
|
+
throw new AuthProfileValidationError(`no profile with id "${trimmed}"`);
|
|
1467
|
+
}
|
|
1468
|
+
if (!profile.models || profile.models.mode !== "allow") {
|
|
1469
|
+
throw new AuthProfileValidationError(
|
|
1470
|
+
`profile "${trimmed}" has no mode:"allow" curation to refresh \u2014 mode:"all" (or absent models) already tracks the live catalog on every read`
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
const nextIds = [...new Set(currentIds.map((s) => s.trim()).filter(Boolean))];
|
|
1474
|
+
const prevIds = profile.models.ids;
|
|
1475
|
+
const prevSet = new Set(prevIds);
|
|
1476
|
+
const nextSet = new Set(nextIds);
|
|
1477
|
+
const added = nextIds.filter((i) => !prevSet.has(i));
|
|
1478
|
+
const removed = prevIds.filter((i) => !nextSet.has(i));
|
|
1479
|
+
const updated = { ...profile, models: { mode: "allow", ids: nextIds } };
|
|
1480
|
+
await deps.addProfile(updated);
|
|
1481
|
+
return { profile: updated, added, removed };
|
|
1482
|
+
}
|
|
1222
1483
|
|
|
1223
1484
|
// src/eligibility.ts
|
|
1224
1485
|
function resolveEndpoint(manifest, route) {
|
|
1225
|
-
const
|
|
1226
|
-
if (
|
|
1486
|
+
const endpoint = manifest.endpointByRoute[route];
|
|
1487
|
+
if (endpoint === void 0) {
|
|
1227
1488
|
throw new Error(
|
|
1228
|
-
`resolveEndpoint: adapter "${manifest.id}" declares no
|
|
1489
|
+
`resolveEndpoint: adapter "${manifest.id}" declares no endpoint for route "${route}"`
|
|
1229
1490
|
);
|
|
1230
1491
|
}
|
|
1231
|
-
return {
|
|
1492
|
+
return { endpoint };
|
|
1232
1493
|
}
|
|
1233
1494
|
function methodsPresentable(manifest, route) {
|
|
1234
1495
|
return [...manifest.methodsByRoute[route] ?? []];
|
|
1235
1496
|
}
|
|
1236
1497
|
function eligibleProfiles(profiles, manifest, route) {
|
|
1237
|
-
const {
|
|
1498
|
+
const { endpoint } = resolveEndpoint(manifest, route);
|
|
1238
1499
|
const methods = methodsPresentable(manifest, route);
|
|
1239
|
-
return profiles.filter(
|
|
1500
|
+
return profiles.filter(
|
|
1501
|
+
(p) => !p.disabled && p.endpoint === endpoint && methods.includes(p.method)
|
|
1502
|
+
);
|
|
1240
1503
|
}
|
|
1241
1504
|
|
|
1242
1505
|
// src/index.ts
|
|
1243
1506
|
var SPEC_NAME = "agentauth";
|
|
1244
1507
|
var SPEC_VERSION = "v1";
|
|
1245
1508
|
|
|
1246
|
-
export { BUILTIN_AUTH_PROVIDERS, CeremonyRequiredError, CredentialBroker, DiscoveryError, FLOW_ENGINES, FileStore, KeychainStore, MemoryStore, SPEC_NAME, SPEC_VERSION, addAuthProfile, authConfigSchema, authProfilesPath, authProviderFrontmatterSchema, defineAuthProvider, discoverEndpoints, eligibleProfiles, getAuthProfile, getAuthProvider, guildeAuthProvider, installConfigSchema, listAuthProfiles, listAuthProviderIds, listAuthProviders, loadAuthProfiles, methodsPresentable, parseAuthProviderManifest, parseAuthProviderManifestRaw, readKeychainToken, registerAuthProvider, removeAuthProfile, resolveAccount, resolveEndpoint, resolveStoreRef, runAuthFlow, tokenStoreSpecSchema, writeKeychainToken };
|
|
1509
|
+
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, refreshAuthProfileModels, registerAuthProvider, removeAuthProfile, resolveAccount, resolveEndpoint, resolveStoreRef, runAuthFlow, setAuthProfileEnabled, setAuthProfileModels, tokenStoreSpecSchema, validateCreateInput, writeKeychainToken };
|
|
1247
1510
|
//# sourceMappingURL=index.mjs.map
|
|
1248
1511
|
//# sourceMappingURL=index.mjs.map
|