@usearete/sdk 0.32.0 → 0.34.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.cjs CHANGED
@@ -1043,6 +1043,639 @@ const AUTH_ERROR_CODES_BY_WIRE = {
1043
1043
  'stack-version-unknown': 'STACK_VERSION_UNKNOWN',
1044
1044
  };
1045
1045
 
1046
+ /**
1047
+ * The key from the active `a4` CLI login, for servers and local scripts.
1048
+ *
1049
+ * The `a4` CLI stores the keys it logs in with in a TOML credentials file
1050
+ * (`~/.arete/credentials.toml`, or `ARETE_CREDENTIALS_PATH`), keyed by profile
1051
+ * and API URL. This module mirrors the CLI's lookup (the Rust SDK's
1052
+ * `credentials` module is the reference):
1053
+ *
1054
+ * - Profile: `.arete/auth.toml` in the working directory (may pin only the
1055
+ * `agent` profile), then `ARETE_PROFILE`, then the single profile holding a
1056
+ * key for the API URL. More than one candidate means no key.
1057
+ * - Only the key stored for the default Arete API is used, because that is
1058
+ * where the SDK's default token endpoint sends it. Keys the CLI stored for
1059
+ * another API URL are never sent anywhere else.
1060
+ * - Any problem (no file, unreadable, malformed, ambiguous) means no key.
1061
+ *
1062
+ * Browser safety: nothing here is imported statically from Node. Node builtins
1063
+ * are reached at call time through `process.getBuiltinModule` (Node 20.16+,
1064
+ * 22.3+, Bun), Deno through its own permission-checked APIs, so bundlers see no
1065
+ * `fs` import. Callers must not call this in a browser; `resolveAuthConfig`
1066
+ * returns before reaching it there.
1067
+ */
1068
+ /** API URL whose key the SDK may use: its default token endpoint's origin. */
1069
+ const DEFAULT_A4_API_URL = 'https://api.arete.run';
1070
+ /**
1071
+ * True when `url` is on the API the `a4` login key was stored for. A key
1072
+ * found in the login is only ever sent there, never to an endpoint a stack
1073
+ * names on another host.
1074
+ */
1075
+ function isA4LoginKeyDestination(url) {
1076
+ try {
1077
+ const parsed = new URL(url);
1078
+ const host = parsed.hostname.toLowerCase().replace(/\.+$/, '');
1079
+ return parsed.protocol === 'https:'
1080
+ && host === new URL(DEFAULT_A4_API_URL).hostname
1081
+ && (parsed.port === '' || parsed.port === '443')
1082
+ && parsed.username === ''
1083
+ && parsed.password === '';
1084
+ }
1085
+ catch {
1086
+ return false;
1087
+ }
1088
+ }
1089
+ const PROFILE_ENV = 'ARETE_PROFILE';
1090
+ const CREDENTIALS_PATH_ENV = 'ARETE_CREDENTIALS_PATH';
1091
+ const AGENT_PROFILE = 'agent';
1092
+ const HUMAN_PROFILE = 'human';
1093
+ // ── Minimal TOML reader ─────────────────────────────────────────────────────
1094
+ //
1095
+ // Covers what the CLI writes (`[a.b."c"]` tables and `key = "string"`
1096
+ // pairs) plus one-line scalars. Anything else (arrays, inline tables,
1097
+ // multi-line strings) makes the whole document unreadable, which means
1098
+ // "no key" rather than a guess.
1099
+ /** A one-line non-string value (number, boolean, date); its text is unused. */
1100
+ class TomlScalar {
1101
+ constructor(text) {
1102
+ this.text = text;
1103
+ }
1104
+ }
1105
+ class TomlUnsupported extends Error {
1106
+ }
1107
+ function isTable(value) {
1108
+ return typeof value === 'object' && !(value instanceof TomlScalar);
1109
+ }
1110
+ function readBasicString(line, start) {
1111
+ let out = '';
1112
+ let index = start + 1;
1113
+ while (index < line.length) {
1114
+ const char = line[index];
1115
+ if (char === '"')
1116
+ return [out, index + 1];
1117
+ if (char === '\\') {
1118
+ const next = line[index + 1];
1119
+ const simple = {
1120
+ b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\',
1121
+ };
1122
+ if (next !== undefined && simple[next] !== undefined) {
1123
+ out += simple[next];
1124
+ index += 2;
1125
+ continue;
1126
+ }
1127
+ if (next === 'u' || next === 'U') {
1128
+ const length = next === 'u' ? 4 : 8;
1129
+ const hex = line.slice(index + 2, index + 2 + length);
1130
+ if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== length)
1131
+ throw new TomlUnsupported();
1132
+ out += String.fromCodePoint(parseInt(hex, 16));
1133
+ index += 2 + length;
1134
+ continue;
1135
+ }
1136
+ throw new TomlUnsupported();
1137
+ }
1138
+ out += char;
1139
+ index += 1;
1140
+ }
1141
+ throw new TomlUnsupported();
1142
+ }
1143
+ function readLiteralString(line, start) {
1144
+ const end = line.indexOf("'", start + 1);
1145
+ if (end < 0)
1146
+ throw new TomlUnsupported();
1147
+ return [line.slice(start + 1, end), end + 1];
1148
+ }
1149
+ function skipSpaces(line, index) {
1150
+ while (index < line.length && (line[index] === ' ' || line[index] === '\t'))
1151
+ index += 1;
1152
+ return index;
1153
+ }
1154
+ /** Read a dotted key (`a."b".c`) ending at `terminator`. */
1155
+ function readKey(line, start, terminator) {
1156
+ const parts = [];
1157
+ let index = skipSpaces(line, start);
1158
+ for (;;) {
1159
+ let part;
1160
+ if (line[index] === '"') {
1161
+ [part, index] = readBasicString(line, index);
1162
+ }
1163
+ else if (line[index] === "'") {
1164
+ [part, index] = readLiteralString(line, index);
1165
+ }
1166
+ else {
1167
+ const match = /^[A-Za-z0-9_-]+/.exec(line.slice(index));
1168
+ if (!match)
1169
+ throw new TomlUnsupported();
1170
+ part = match[0];
1171
+ index += part.length;
1172
+ }
1173
+ parts.push(part);
1174
+ index = skipSpaces(line, index);
1175
+ if (line[index] === '.') {
1176
+ index = skipSpaces(line, index + 1);
1177
+ continue;
1178
+ }
1179
+ if (line.startsWith(terminator, index))
1180
+ return [parts, index + terminator.length];
1181
+ throw new TomlUnsupported();
1182
+ }
1183
+ }
1184
+ function assertRestIsComment(line, index) {
1185
+ const rest = line.slice(index).trim();
1186
+ if (rest !== '' && !rest.startsWith('#'))
1187
+ throw new TomlUnsupported();
1188
+ }
1189
+ function tableAt(root, path) {
1190
+ let table = root;
1191
+ for (const part of path) {
1192
+ const existing = table[part];
1193
+ if (existing === undefined) {
1194
+ const created = Object.create(null);
1195
+ table[part] = created;
1196
+ table = created;
1197
+ }
1198
+ else if (isTable(existing)) {
1199
+ table = existing;
1200
+ }
1201
+ else {
1202
+ throw new TomlUnsupported();
1203
+ }
1204
+ }
1205
+ return table;
1206
+ }
1207
+ /** @internal Parse the subset of TOML described above; throws when unsupported. */
1208
+ function parseSimpleToml(content) {
1209
+ const root = Object.create(null);
1210
+ let current = root;
1211
+ for (const rawLine of content.replace(/^/, '').split(/\r?\n/)) {
1212
+ const line = rawLine.trim();
1213
+ if (line === '' || line.startsWith('#'))
1214
+ continue;
1215
+ if (line.startsWith('[['))
1216
+ throw new TomlUnsupported();
1217
+ if (line.startsWith('[')) {
1218
+ const [path, end] = readKey(line, 1, ']');
1219
+ assertRestIsComment(line, end);
1220
+ current = tableAt(root, path);
1221
+ continue;
1222
+ }
1223
+ const [path, afterEquals] = readKey(line, 0, '=');
1224
+ const valueStart = skipSpaces(line, afterEquals);
1225
+ let value;
1226
+ let end;
1227
+ if (line.startsWith('"""', valueStart) || line.startsWith("'''", valueStart)) {
1228
+ throw new TomlUnsupported();
1229
+ }
1230
+ else if (line[valueStart] === '"') {
1231
+ [value, end] = readBasicString(line, valueStart);
1232
+ }
1233
+ else if (line[valueStart] === "'") {
1234
+ [value, end] = readLiteralString(line, valueStart);
1235
+ }
1236
+ else {
1237
+ const match = /^[A-Za-z0-9_:.+-]+(?:[ T][0-9:.+Z-]+)?/.exec(line.slice(valueStart));
1238
+ if (!match)
1239
+ throw new TomlUnsupported();
1240
+ value = new TomlScalar(match[0]);
1241
+ end = valueStart + match[0].length;
1242
+ }
1243
+ assertRestIsComment(line, end);
1244
+ const key = path[path.length - 1];
1245
+ const table = tableAt(current, path.slice(0, -1));
1246
+ if (Object.prototype.hasOwnProperty.call(table, key))
1247
+ throw new TomlUnsupported();
1248
+ table[key] = value;
1249
+ }
1250
+ return root;
1251
+ }
1252
+ // ── Lookup (mirror of the Rust `lookup_credentials`) ─────────────────────────
1253
+ /** Mirror of the CLI's API URL normalisation for credential lookup. */
1254
+ function normalizeA4ApiUrl(url) {
1255
+ const trimmed = url.trim().replace(/\/+$/, '');
1256
+ const schemeAt = trimmed.indexOf('://');
1257
+ const scheme = schemeAt >= 0 ? trimmed.slice(0, schemeAt).toLowerCase() : '';
1258
+ const rest = schemeAt >= 0 ? trimmed.slice(schemeAt + 3) : trimmed;
1259
+ const authorityEnd = rest.search(/[/?#]/);
1260
+ const authority = authorityEnd >= 0 ? rest.slice(0, authorityEnd) : rest;
1261
+ const tail = authorityEnd >= 0 ? rest.slice(authorityEnd) : '';
1262
+ const hostStart = authority.lastIndexOf('@') + 1;
1263
+ const hostPort = authority.slice(hostStart);
1264
+ let host;
1265
+ let port;
1266
+ if (hostPort.startsWith('[')) {
1267
+ const close = hostPort.indexOf(']');
1268
+ host = close >= 0 ? hostPort.slice(0, close + 1) : hostPort;
1269
+ port = close >= 0 ? hostPort.slice(close + 1) : '';
1270
+ }
1271
+ else {
1272
+ const colon = hostPort.lastIndexOf(':');
1273
+ host = colon >= 0 ? hostPort.slice(0, colon) : hostPort;
1274
+ port = colon >= 0 ? hostPort.slice(colon) : '';
1275
+ }
1276
+ host = host.replace(/\.+$/, '').toLowerCase();
1277
+ return `${scheme ? `${scheme}://` : ''}${authority.slice(0, hostStart)}${host}${port}${tail}`;
1278
+ }
1279
+ function stringMap(value) {
1280
+ if (value === undefined)
1281
+ return undefined;
1282
+ if (!isTable(value))
1283
+ throw new TomlUnsupported();
1284
+ const out = new Map();
1285
+ for (const [key, entry] of Object.entries(value)) {
1286
+ if (typeof entry !== 'string')
1287
+ throw new TomlUnsupported();
1288
+ out.set(key, entry);
1289
+ }
1290
+ return out;
1291
+ }
1292
+ function findUrlKey(keys, apiUrl) {
1293
+ if (!keys)
1294
+ return undefined;
1295
+ const wanted = normalizeA4ApiUrl(apiUrl);
1296
+ const exact = (keys.get(apiUrl) ?? keys.get(wanted))?.trim();
1297
+ if (exact)
1298
+ return exact;
1299
+ return [...keys.entries()]
1300
+ .filter(([url]) => normalizeA4ApiUrl(url) === wanted)
1301
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
1302
+ .map(([, key]) => key.trim())
1303
+ .find((key) => key !== '');
1304
+ }
1305
+ function isValidProfileName(profile) {
1306
+ return profile.length > 0 && profile.length <= 64 && /^[A-Za-z0-9_-]+$/.test(profile);
1307
+ }
1308
+ function keyFitsProfile(profile, key) {
1309
+ const agentKey = key.trim().startsWith('a4_ak_');
1310
+ if (profile === AGENT_PROFILE)
1311
+ return agentKey;
1312
+ if (profile === HUMAN_PROFILE)
1313
+ return !agentKey;
1314
+ return true;
1315
+ }
1316
+ /** @internal Mirror of the Rust `lookup_credentials`. */
1317
+ function lookupA4Credentials(content, apiUrl, requestedProfile) {
1318
+ let profiles;
1319
+ let legacyKeys;
1320
+ let legacyApiKey;
1321
+ try {
1322
+ const parsed = parseSimpleToml(content);
1323
+ const profilesValue = parsed['profiles'];
1324
+ if (profilesValue !== undefined && !isTable(profilesValue))
1325
+ return { kind: 'invalid' };
1326
+ profiles = Object.entries(profilesValue ?? {}).map(([name, profile]) => {
1327
+ if (!isTable(profile))
1328
+ throw new TomlUnsupported();
1329
+ return [name, stringMap(profile['keys'])];
1330
+ });
1331
+ legacyKeys = stringMap(parsed['keys']);
1332
+ const apiKey = parsed['api_key'];
1333
+ if (apiKey !== undefined && typeof apiKey !== 'string')
1334
+ return { kind: 'invalid' };
1335
+ legacyApiKey = apiKey?.trim() || undefined;
1336
+ }
1337
+ catch {
1338
+ return { kind: 'invalid' };
1339
+ }
1340
+ const legacy = findUrlKey(legacyKeys, apiUrl) ?? legacyApiKey;
1341
+ if (requestedProfile !== undefined) {
1342
+ if (!isValidProfileName(requestedProfile))
1343
+ return { kind: 'invalid' };
1344
+ const entry = profiles.find(([name]) => name === requestedProfile);
1345
+ const key = findUrlKey(entry?.[1], apiUrl);
1346
+ if (key !== undefined) {
1347
+ return keyFitsProfile(requestedProfile, key) ? { kind: 'key', key } : { kind: 'invalid' };
1348
+ }
1349
+ if (legacy !== undefined) {
1350
+ const compatible = requestedProfile === AGENT_PROFILE
1351
+ ? legacy.startsWith('a4_ak_')
1352
+ : requestedProfile === HUMAN_PROFILE && !legacy.startsWith('a4_ak_');
1353
+ if (compatible)
1354
+ return { kind: 'key', key: legacy };
1355
+ }
1356
+ return { kind: 'none' };
1357
+ }
1358
+ const matches = profiles
1359
+ .map(([name, keys]) => [name, findUrlKey(keys, apiUrl)])
1360
+ .filter((entry) => entry[1] !== undefined);
1361
+ if (matches.length > 1)
1362
+ return { kind: 'ambiguous' };
1363
+ if (matches.length === 1) {
1364
+ const [name, key] = matches[0];
1365
+ return keyFitsProfile(name, key) ? { kind: 'key', key } : { kind: 'invalid' };
1366
+ }
1367
+ return legacy !== undefined ? { kind: 'key', key: legacy } : { kind: 'none' };
1368
+ }
1369
+ /** Selected profile: `undefined` for none, `null` when selection is invalid. */
1370
+ function selectProfile(host) {
1371
+ // The project file can pin the agent profile; if it cannot be checked
1372
+ // (unknown working directory, denied or failed read), choose nothing rather
1373
+ // than risk a profile the project excludes.
1374
+ const cwd = host.cwd();
1375
+ if (cwd === undefined)
1376
+ return null;
1377
+ let project;
1378
+ try {
1379
+ project = host.readTextFile(host.joinPath(cwd, '.arete', 'auth.toml'));
1380
+ }
1381
+ catch {
1382
+ return null;
1383
+ }
1384
+ if (project !== null) {
1385
+ try {
1386
+ const profile = parseSimpleToml(project)['default_profile'];
1387
+ return profile === AGENT_PROFILE ? AGENT_PROFILE : null;
1388
+ }
1389
+ catch {
1390
+ return null;
1391
+ }
1392
+ }
1393
+ const profile = host.readEnv(PROFILE_ENV)?.trim();
1394
+ if (profile === undefined)
1395
+ return undefined;
1396
+ return isValidProfileName(profile) ? profile : null;
1397
+ }
1398
+ /**
1399
+ * @internal Key of the active `a4` login for the default Arete API, or
1400
+ * `undefined`. Never throws. The caller checks the key class.
1401
+ */
1402
+ function readA4ProfileKey(host = systemProfileHost()) {
1403
+ if (!host)
1404
+ return {};
1405
+ try {
1406
+ const profile = selectProfile(host);
1407
+ if (profile === null)
1408
+ return {};
1409
+ const override = host.readEnv(CREDENTIALS_PATH_ENV);
1410
+ let path;
1411
+ if (override) {
1412
+ path = override;
1413
+ }
1414
+ else {
1415
+ const home = host.homeDir();
1416
+ path = home ? host.joinPath(home, '.arete', 'credentials.toml') : undefined;
1417
+ }
1418
+ if (!path)
1419
+ return {};
1420
+ const content = host.readTextFile(path);
1421
+ if (content === null)
1422
+ return {};
1423
+ const lookup = lookupA4Credentials(content, DEFAULT_A4_API_URL, profile);
1424
+ if (lookup.kind === 'key')
1425
+ return { key: lookup.key };
1426
+ if (lookup.kind === 'ambiguous')
1427
+ return { ambiguous: true };
1428
+ return {};
1429
+ }
1430
+ catch {
1431
+ return {};
1432
+ }
1433
+ }
1434
+ function denoHost(deno) {
1435
+ const granted = (descriptor) => {
1436
+ try {
1437
+ return deno.permissions?.querySync?.(descriptor)?.state === 'granted';
1438
+ }
1439
+ catch {
1440
+ return false;
1441
+ }
1442
+ };
1443
+ const readEnv = (name) => granted({ name: 'env', variable: name }) ? deno.env?.get?.(name) : undefined;
1444
+ const separator = deno.build?.os === 'windows' ? '\\' : '/';
1445
+ return {
1446
+ readEnv,
1447
+ readTextFile(path) {
1448
+ // Without --allow-read Deno would prompt or throw; ask first. Denied
1449
+ // access is not a missing file: callers must not treat it as absent.
1450
+ if (!deno.readTextFileSync)
1451
+ throw new Error('file reads unavailable');
1452
+ if (!granted({ name: 'read', path }))
1453
+ throw new Error('read permission not granted');
1454
+ try {
1455
+ return deno.readTextFileSync(path);
1456
+ }
1457
+ catch (error) {
1458
+ const notFound = deno.errors?.NotFound;
1459
+ if (notFound && error instanceof notFound)
1460
+ return null;
1461
+ throw error;
1462
+ }
1463
+ },
1464
+ cwd() {
1465
+ if (!granted({ name: 'read' }))
1466
+ return undefined;
1467
+ try {
1468
+ return deno.cwd?.();
1469
+ }
1470
+ catch {
1471
+ return undefined;
1472
+ }
1473
+ },
1474
+ homeDir: () => readEnv('HOME') ?? readEnv('USERPROFILE'),
1475
+ joinPath: (...parts) => parts.join(separator),
1476
+ };
1477
+ }
1478
+ function nodeHost(process) {
1479
+ const load = process.getBuiltinModule;
1480
+ if (typeof load !== 'function')
1481
+ return undefined;
1482
+ const fs = load.call(process, 'fs');
1483
+ const os = load.call(process, 'os');
1484
+ const path = load.call(process, 'path');
1485
+ if (!fs || !os || !path)
1486
+ return undefined;
1487
+ return {
1488
+ readEnv: (name) => process.env?.[name],
1489
+ readTextFile(file) {
1490
+ try {
1491
+ return fs.readFileSync(file, 'utf8');
1492
+ }
1493
+ catch (error) {
1494
+ if (error.code === 'ENOENT')
1495
+ return null;
1496
+ throw error;
1497
+ }
1498
+ },
1499
+ cwd: () => process.cwd?.(),
1500
+ homeDir: () => os.homedir(),
1501
+ joinPath: (...parts) => path.join(...parts),
1502
+ };
1503
+ }
1504
+ /** Host for the current server runtime, or `undefined` where files are unreachable. */
1505
+ function systemProfileHost() {
1506
+ try {
1507
+ const scope = globalThis;
1508
+ if (scope.Deno)
1509
+ return denoHost(scope.Deno);
1510
+ if (scope.process)
1511
+ return nodeHost(scope.process);
1512
+ }
1513
+ catch {
1514
+ // Fall through: no profile support in this runtime.
1515
+ }
1516
+ return undefined;
1517
+ }
1518
+
1519
+ /** Environment variable read for a secret-class key outside browsers. */
1520
+ const ARETE_API_KEY_ENV = 'ARETE_API_KEY';
1521
+ const CREATE_PUBLISHABLE_HINT = 'create one with `a4 auth keys create-publishable --origin <scheme://host[:port]>`';
1522
+ const PUBLISHABLE_PREFIXES = ['a4_pk_', 'hspk_'];
1523
+ const SECRET_PREFIXES = ['a4_sk_', 'a4_ak_', 'hsk_'];
1524
+ function classifyApiKey(key) {
1525
+ const trimmed = key.trim();
1526
+ if (PUBLISHABLE_PREFIXES.some((prefix) => trimmed.startsWith(prefix)))
1527
+ return 'publishable';
1528
+ if (SECRET_PREFIXES.some((prefix) => trimmed.startsWith(prefix)))
1529
+ return 'secret';
1530
+ return 'unknown';
1531
+ }
1532
+ /**
1533
+ * True in a browser page: both `window` and `document` exist. Web workers,
1534
+ * service workers, edge runtimes, SSR (Node/Bun) and Deno have no `document`
1535
+ * and count as server-side.
1536
+ */
1537
+ function isBrowserEnvironment() {
1538
+ const scope = globalThis;
1539
+ return typeof scope.window === 'object'
1540
+ && scope.window !== null
1541
+ && typeof scope.document === 'object'
1542
+ && scope.document !== null;
1543
+ }
1544
+ /**
1545
+ * Read an environment variable in Node, Bun or Deno without prompting for or
1546
+ * failing on missing permissions. Browsers have no environment.
1547
+ */
1548
+ function readEnvironmentVariable(name) {
1549
+ try {
1550
+ const deno = globalThis.Deno;
1551
+ if (deno) {
1552
+ // Reading without --allow-env throws or prompts; ask first.
1553
+ const state = deno.permissions?.querySync?.({ name: 'env', variable: name })?.state;
1554
+ if (state !== 'granted')
1555
+ return undefined;
1556
+ const value = deno.env?.get?.(name);
1557
+ return typeof value === 'string' ? value : undefined;
1558
+ }
1559
+ const value = globalThis
1560
+ .process?.env?.[name];
1561
+ return typeof value === 'string' ? value : undefined;
1562
+ }
1563
+ catch {
1564
+ return undefined;
1565
+ }
1566
+ }
1567
+ const warned$1 = new Set();
1568
+ /**
1569
+ * Marks a resolved config whose `secretKey` came from the `a4` login. It holds
1570
+ * that key, so the mark is carried through object spreads (binding paths copy
1571
+ * the resolved config) and only applies while `secretKey` is still that key:
1572
+ * a copy given another `secretKey` is no longer restricted. Per config, never
1573
+ * process-wide, so the same key passed explicitly elsewhere is unaffected.
1574
+ */
1575
+ const A4_LOGIN_SECRET_KEY = Symbol('arete.a4LoginSecretKey');
1576
+ /** True when `auth.secretKey` was supplied by the `a4` login fallback. */
1577
+ function secretKeyFromA4Login(auth) {
1578
+ const marked = auth;
1579
+ return marked?.secretKey !== undefined && marked[A4_LOGIN_SECRET_KEY] === marked.secretKey;
1580
+ }
1581
+ function warnOnce$1(id, message) {
1582
+ if (warned$1.has(id))
1583
+ return;
1584
+ warned$1.add(id);
1585
+ console.warn(`[arete] ${message}`);
1586
+ }
1587
+ function hasExplicitAuth(auth) {
1588
+ return auth !== undefined && (auth.token !== undefined
1589
+ || auth.getToken !== undefined
1590
+ || auth.tokenEndpoint !== undefined
1591
+ || auth.publishableKey !== undefined
1592
+ || auth.secretKey !== undefined);
1593
+ }
1594
+ /**
1595
+ * Validate the configured API keys and apply the server-side credential chain.
1596
+ *
1597
+ * - `secretKey` is refused in browsers and refuses publishable keys.
1598
+ * - A secret-class key in `publishableKey` is refused in browsers and warned
1599
+ * about elsewhere (it still works server-side, as it always has).
1600
+ * - Outside browsers, when no auth option is set at all, `ARETE_API_KEY`
1601
+ * supplies `secretKey`; without it, the agent or secret key from the active
1602
+ * `a4` CLI login does. Browsers never read either.
1603
+ *
1604
+ * Error and warning text never includes key material.
1605
+ */
1606
+ function resolveAuthConfig(auth, readProfileKey = readA4ProfileKey) {
1607
+ const browser = isBrowserEnvironment();
1608
+ if (auth?.secretKey !== undefined) {
1609
+ if (browser) {
1610
+ throw new AreteError('auth.secretKey cannot be used in a browser: anything shipped to a browser is public. '
1611
+ + 'Use auth.publishableKey with a publishable key (a4_pk_...) instead; '
1612
+ + `${CREATE_PUBLISHABLE_HINT}. Keep secret and agent keys on servers and in local scripts.`, 'INVALID_CONFIG');
1613
+ }
1614
+ if (auth.secretKey.trim() === '') {
1615
+ throw new AreteError('auth.secretKey is empty', 'INVALID_CONFIG');
1616
+ }
1617
+ if (classifyApiKey(auth.secretKey) === 'publishable') {
1618
+ throw new AreteError('auth.secretKey was given a publishable key (a4_pk_...). Pass it as auth.publishableKey '
1619
+ + 'instead; auth.secretKey takes an agent key (a4_ak_...) or secret key (a4_sk_...).', 'INVALID_CONFIG');
1620
+ }
1621
+ if (auth.publishableKey !== undefined) {
1622
+ throw new AreteError('Set either auth.secretKey (servers and scripts) or auth.publishableKey (browsers), not both.', 'INVALID_CONFIG');
1623
+ }
1624
+ }
1625
+ if (auth?.publishableKey !== undefined && classifyApiKey(auth.publishableKey) === 'secret') {
1626
+ if (browser) {
1627
+ throw new AreteError('auth.publishableKey was given a secret-class key (a4_sk_... or a4_ak_...). Never ship a '
1628
+ + 'secret or agent key to a browser. Use a publishable key (a4_pk_...) instead; '
1629
+ + `${CREATE_PUBLISHABLE_HINT}.`, 'INVALID_CONFIG');
1630
+ }
1631
+ warnOnce$1('secret-in-publishable', 'auth.publishableKey was given a secret-class key (a4_sk_... or a4_ak_...). '
1632
+ + 'Pass it as auth.secretKey instead (servers and scripts only), or set ARETE_API_KEY.');
1633
+ }
1634
+ if (browser || hasExplicitAuth(auth))
1635
+ return auth;
1636
+ const environmentKey = readEnvironmentVariable(ARETE_API_KEY_ENV)?.trim();
1637
+ if (environmentKey) {
1638
+ if (classifyApiKey(environmentKey) !== 'publishable') {
1639
+ return { ...auth, secretKey: environmentKey };
1640
+ }
1641
+ warnOnce$1('publishable-in-env', `${ARETE_API_KEY_ENV} holds a publishable key (a4_pk_...) and was ignored. Set it to an agent `
1642
+ + 'key (a4_ak_...) or secret key (a4_sk_...), or pass the publishable key as auth.publishableKey.');
1643
+ }
1644
+ const profile = readProfileKey();
1645
+ if (profile.ambiguous) {
1646
+ warnOnce$1('ambiguous-a4-profile', 'More than one a4 login profile holds a key; not choosing one. Set ARETE_PROFILE '
1647
+ + `(for example \`agent\`) or ${ARETE_API_KEY_ENV}.`);
1648
+ }
1649
+ if (profile.key && classifyApiKey(profile.key) === 'secret') {
1650
+ const resolved = {
1651
+ ...auth,
1652
+ secretKey: profile.key,
1653
+ [A4_LOGIN_SECRET_KEY]: profile.key,
1654
+ };
1655
+ return resolved;
1656
+ }
1657
+ return auth;
1658
+ }
1659
+ /**
1660
+ * Appended to a 401 for a request that carried no API key. It names the
1661
+ * commands and options that supply one, never where credentials are stored.
1662
+ */
1663
+ const NO_API_KEY_HINT = 'No Arete API key found. Run `a4 auth login` (or `a4 auth signup` for an agent), '
1664
+ + 'or set ARETE_API_KEY, or pass auth.secretKey.';
1665
+ /**
1666
+ * The key sent as the bearer credential to `endpoint`, if any. A key taken
1667
+ * from the `a4` login is only sent to the Arete API it was stored for.
1668
+ */
1669
+ function tokenEndpointApiKey(auth, endpoint) {
1670
+ if (secretKeyFromA4Login(auth) && !isA4LoginKeyDestination(endpoint))
1671
+ return undefined;
1672
+ return auth?.secretKey ?? auth?.publishableKey;
1673
+ }
1674
+ /** True when `headers` carries its own `Authorization` header. */
1675
+ function hasAuthorizationHeader(headers) {
1676
+ return Object.keys(headers ?? {}).some((name) => name.toLowerCase() === 'authorization');
1677
+ }
1678
+
1046
1679
  const GZIP_MAGIC_0 = 0x1f;
1047
1680
  const GZIP_MAGIC_1 = 0x8b;
1048
1681
  const FRAME_MODES = new Set(['state', 'append', 'list']);
@@ -1850,7 +2483,7 @@ class ConnectionManager {
1850
2483
  this.reconnectIntervals = config.reconnectIntervals ?? DEFAULT_CONFIG.reconnectIntervals;
1851
2484
  this.maxReconnectAttempts =
1852
2485
  config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts;
1853
- this.authConfig = config.auth;
2486
+ this.authConfig = resolveAuthConfig(config.auth);
1854
2487
  this.release = normalizeStackRelease(config.release);
1855
2488
  this.authFetch = config.fetch ?? ((input, init) => {
1856
2489
  if (typeof globalThis.fetch !== 'function') {
@@ -2102,12 +2735,11 @@ class ConnectionManager {
2102
2735
  };
2103
2736
  }
2104
2737
  async fetchTokenFromEndpoint(tokenEndpoint, request) {
2738
+ const apiKey = tokenEndpointApiKey(this.authConfig, tokenEndpoint);
2105
2739
  const response = await this.authFetch(tokenEndpoint, {
2106
2740
  method: 'POST',
2107
2741
  headers: {
2108
- ...(this.authConfig?.publishableKey
2109
- ? { Authorization: `Bearer ${this.authConfig.publishableKey}` }
2110
- : {}),
2742
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
2111
2743
  ...(this.authConfig?.tokenEndpointHeaders ?? {}),
2112
2744
  'Content-Type': 'application/json',
2113
2745
  },
@@ -2134,9 +2766,17 @@ class ConnectionManager {
2134
2766
  : response.status === 429
2135
2767
  ? 'QUOTA_EXCEEDED'
2136
2768
  : 'AUTH_REQUIRED';
2137
- const errorMessage = typeof parsedError?.error === 'string' && parsedError.error.length > 0
2769
+ const responseMessage = typeof parsedError?.error === 'string' && parsedError.error.length > 0
2138
2770
  ? parsedError.error
2139
2771
  : rawError || response.statusText || 'Authentication request failed';
2772
+ // A server-side 401 for a request that carried no credential at all:
2773
+ // say how to supply one. Custom Authorization headers have their own
2774
+ // advice to give.
2775
+ const sentCredential = Boolean(apiKey)
2776
+ || hasAuthorizationHeader(this.authConfig?.tokenEndpointHeaders);
2777
+ const errorMessage = response.status === 401 && !sentCredential && !isBrowserEnvironment()
2778
+ ? `${responseMessage}. ${NO_API_KEY_HINT}`
2779
+ : responseMessage;
2140
2780
  const retryAfterHeader = response.headers.get('Retry-After');
2141
2781
  const retryAfterSeconds = retryAfterHeader && /^\d+$/.test(retryAfterHeader)
2142
2782
  ? Number(retryAfterHeader)
@@ -5453,7 +6093,10 @@ function hasRuntimeAuthStrategy(auth) {
5453
6093
  || auth?.getToken !== undefined
5454
6094
  || auth?.tokenEndpoint !== undefined;
5455
6095
  }
5456
- function bindingAuthConfig(binding, runtimeAuth) {
6096
+ function bindingAuthConfig(binding, configuredAuth) {
6097
+ // Resolve ARETE_API_KEY before the binding endpoint is filled in, so an
6098
+ // otherwise unconfigured client still authenticates.
6099
+ const runtimeAuth = resolveAuthConfig(configuredAuth);
5457
6100
  if (hasRuntimeAuthStrategy(runtimeAuth))
5458
6101
  return runtimeAuth;
5459
6102
  if (!binding.auth.required)
@@ -7500,7 +8143,10 @@ function parseProgramReadResponse(response, path, body) {
7500
8143
  throw new AreteError(`Program read '${path}' returned invalid JSON`, 'INVALID_RESPONSE', error);
7501
8144
  }
7502
8145
  }
7503
- function hostedAuthConfig(binding, runtimeAuth) {
8146
+ function hostedAuthConfig(binding, configuredAuth) {
8147
+ // Resolve ARETE_API_KEY before the binding endpoint is filled in, so an
8148
+ // otherwise unconfigured client still authenticates.
8149
+ const runtimeAuth = resolveAuthConfig(configuredAuth);
7504
8150
  const metadata = binding.auth;
7505
8151
  const runtimeStrategyConfigured = runtimeAuth?.token !== undefined
7506
8152
  || runtimeAuth?.getToken !== undefined
@@ -8975,6 +9621,7 @@ function stringifyBigints(value) {
8975
9621
  const Arete = Object.assign(Arete$1, { session: createSession });
8976
9622
 
8977
9623
  exports.API_PROBLEM_SCHEMA_VERSION = API_PROBLEM_SCHEMA_VERSION;
9624
+ exports.ARETE_API_KEY_ENV = ARETE_API_KEY_ENV;
8978
9625
  exports.ASSOCIATED_TOKEN_PROGRAM_ADDRESS = ASSOCIATED_TOKEN_PROGRAM_ADDRESS;
8979
9626
  exports.Arete = Arete;
8980
9627
  exports.AreteError = AreteError;
@@ -9019,6 +9666,7 @@ exports.buildInstruction = buildInstruction;
9019
9666
  exports.bytes = bytes;
9020
9667
  exports.canonicalQueryKey = canonicalQueryKey;
9021
9668
  exports.chainAccountLoader = chainAccountLoader;
9669
+ exports.classifyApiKey = classifyApiKey;
9022
9670
  exports.compareProgramIdentity = compareProgramIdentity;
9023
9671
  exports.createChainClient = createChainClient;
9024
9672
  exports.createEntityStream = createEntityStream;