@usearete/sdk 0.33.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/README.md +8 -3
- package/dist/index.cjs +535 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +535 -12
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1273,7 +1273,7 @@ interface AuthConfig {
|
|
|
1273
1273
|
* Agent key (`a4_ak_...`) or secret key (`a4_sk_...`) for servers, agents
|
|
1274
1274
|
* and local scripts. Server-only: setting it in a browser throws. When no
|
|
1275
1275
|
* auth option is set, outside browsers it defaults to the `ARETE_API_KEY`
|
|
1276
|
-
* environment variable.
|
|
1276
|
+
* environment variable, then to the key from the active `a4` CLI login.
|
|
1277
1277
|
*/
|
|
1278
1278
|
secretKey?: string;
|
|
1279
1279
|
/** Pre-minted static token (for server-side use) */
|
package/dist/index.esm.js
CHANGED
|
@@ -1041,6 +1041,479 @@ const AUTH_ERROR_CODES_BY_WIRE = {
|
|
|
1041
1041
|
'stack-version-unknown': 'STACK_VERSION_UNKNOWN',
|
|
1042
1042
|
};
|
|
1043
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* The key from the active `a4` CLI login, for servers and local scripts.
|
|
1046
|
+
*
|
|
1047
|
+
* The `a4` CLI stores the keys it logs in with in a TOML credentials file
|
|
1048
|
+
* (`~/.arete/credentials.toml`, or `ARETE_CREDENTIALS_PATH`), keyed by profile
|
|
1049
|
+
* and API URL. This module mirrors the CLI's lookup (the Rust SDK's
|
|
1050
|
+
* `credentials` module is the reference):
|
|
1051
|
+
*
|
|
1052
|
+
* - Profile: `.arete/auth.toml` in the working directory (may pin only the
|
|
1053
|
+
* `agent` profile), then `ARETE_PROFILE`, then the single profile holding a
|
|
1054
|
+
* key for the API URL. More than one candidate means no key.
|
|
1055
|
+
* - Only the key stored for the default Arete API is used, because that is
|
|
1056
|
+
* where the SDK's default token endpoint sends it. Keys the CLI stored for
|
|
1057
|
+
* another API URL are never sent anywhere else.
|
|
1058
|
+
* - Any problem (no file, unreadable, malformed, ambiguous) means no key.
|
|
1059
|
+
*
|
|
1060
|
+
* Browser safety: nothing here is imported statically from Node. Node builtins
|
|
1061
|
+
* are reached at call time through `process.getBuiltinModule` (Node 20.16+,
|
|
1062
|
+
* 22.3+, Bun), Deno through its own permission-checked APIs, so bundlers see no
|
|
1063
|
+
* `fs` import. Callers must not call this in a browser; `resolveAuthConfig`
|
|
1064
|
+
* returns before reaching it there.
|
|
1065
|
+
*/
|
|
1066
|
+
/** API URL whose key the SDK may use: its default token endpoint's origin. */
|
|
1067
|
+
const DEFAULT_A4_API_URL = 'https://api.arete.run';
|
|
1068
|
+
/**
|
|
1069
|
+
* True when `url` is on the API the `a4` login key was stored for. A key
|
|
1070
|
+
* found in the login is only ever sent there, never to an endpoint a stack
|
|
1071
|
+
* names on another host.
|
|
1072
|
+
*/
|
|
1073
|
+
function isA4LoginKeyDestination(url) {
|
|
1074
|
+
try {
|
|
1075
|
+
const parsed = new URL(url);
|
|
1076
|
+
const host = parsed.hostname.toLowerCase().replace(/\.+$/, '');
|
|
1077
|
+
return parsed.protocol === 'https:'
|
|
1078
|
+
&& host === new URL(DEFAULT_A4_API_URL).hostname
|
|
1079
|
+
&& (parsed.port === '' || parsed.port === '443')
|
|
1080
|
+
&& parsed.username === ''
|
|
1081
|
+
&& parsed.password === '';
|
|
1082
|
+
}
|
|
1083
|
+
catch {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const PROFILE_ENV = 'ARETE_PROFILE';
|
|
1088
|
+
const CREDENTIALS_PATH_ENV = 'ARETE_CREDENTIALS_PATH';
|
|
1089
|
+
const AGENT_PROFILE = 'agent';
|
|
1090
|
+
const HUMAN_PROFILE = 'human';
|
|
1091
|
+
// ── Minimal TOML reader ─────────────────────────────────────────────────────
|
|
1092
|
+
//
|
|
1093
|
+
// Covers what the CLI writes (`[a.b."c"]` tables and `key = "string"`
|
|
1094
|
+
// pairs) plus one-line scalars. Anything else (arrays, inline tables,
|
|
1095
|
+
// multi-line strings) makes the whole document unreadable, which means
|
|
1096
|
+
// "no key" rather than a guess.
|
|
1097
|
+
/** A one-line non-string value (number, boolean, date); its text is unused. */
|
|
1098
|
+
class TomlScalar {
|
|
1099
|
+
constructor(text) {
|
|
1100
|
+
this.text = text;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
class TomlUnsupported extends Error {
|
|
1104
|
+
}
|
|
1105
|
+
function isTable(value) {
|
|
1106
|
+
return typeof value === 'object' && !(value instanceof TomlScalar);
|
|
1107
|
+
}
|
|
1108
|
+
function readBasicString(line, start) {
|
|
1109
|
+
let out = '';
|
|
1110
|
+
let index = start + 1;
|
|
1111
|
+
while (index < line.length) {
|
|
1112
|
+
const char = line[index];
|
|
1113
|
+
if (char === '"')
|
|
1114
|
+
return [out, index + 1];
|
|
1115
|
+
if (char === '\\') {
|
|
1116
|
+
const next = line[index + 1];
|
|
1117
|
+
const simple = {
|
|
1118
|
+
b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\',
|
|
1119
|
+
};
|
|
1120
|
+
if (next !== undefined && simple[next] !== undefined) {
|
|
1121
|
+
out += simple[next];
|
|
1122
|
+
index += 2;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (next === 'u' || next === 'U') {
|
|
1126
|
+
const length = next === 'u' ? 4 : 8;
|
|
1127
|
+
const hex = line.slice(index + 2, index + 2 + length);
|
|
1128
|
+
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== length)
|
|
1129
|
+
throw new TomlUnsupported();
|
|
1130
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
1131
|
+
index += 2 + length;
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
throw new TomlUnsupported();
|
|
1135
|
+
}
|
|
1136
|
+
out += char;
|
|
1137
|
+
index += 1;
|
|
1138
|
+
}
|
|
1139
|
+
throw new TomlUnsupported();
|
|
1140
|
+
}
|
|
1141
|
+
function readLiteralString(line, start) {
|
|
1142
|
+
const end = line.indexOf("'", start + 1);
|
|
1143
|
+
if (end < 0)
|
|
1144
|
+
throw new TomlUnsupported();
|
|
1145
|
+
return [line.slice(start + 1, end), end + 1];
|
|
1146
|
+
}
|
|
1147
|
+
function skipSpaces(line, index) {
|
|
1148
|
+
while (index < line.length && (line[index] === ' ' || line[index] === '\t'))
|
|
1149
|
+
index += 1;
|
|
1150
|
+
return index;
|
|
1151
|
+
}
|
|
1152
|
+
/** Read a dotted key (`a."b".c`) ending at `terminator`. */
|
|
1153
|
+
function readKey(line, start, terminator) {
|
|
1154
|
+
const parts = [];
|
|
1155
|
+
let index = skipSpaces(line, start);
|
|
1156
|
+
for (;;) {
|
|
1157
|
+
let part;
|
|
1158
|
+
if (line[index] === '"') {
|
|
1159
|
+
[part, index] = readBasicString(line, index);
|
|
1160
|
+
}
|
|
1161
|
+
else if (line[index] === "'") {
|
|
1162
|
+
[part, index] = readLiteralString(line, index);
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
const match = /^[A-Za-z0-9_-]+/.exec(line.slice(index));
|
|
1166
|
+
if (!match)
|
|
1167
|
+
throw new TomlUnsupported();
|
|
1168
|
+
part = match[0];
|
|
1169
|
+
index += part.length;
|
|
1170
|
+
}
|
|
1171
|
+
parts.push(part);
|
|
1172
|
+
index = skipSpaces(line, index);
|
|
1173
|
+
if (line[index] === '.') {
|
|
1174
|
+
index = skipSpaces(line, index + 1);
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (line.startsWith(terminator, index))
|
|
1178
|
+
return [parts, index + terminator.length];
|
|
1179
|
+
throw new TomlUnsupported();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function assertRestIsComment(line, index) {
|
|
1183
|
+
const rest = line.slice(index).trim();
|
|
1184
|
+
if (rest !== '' && !rest.startsWith('#'))
|
|
1185
|
+
throw new TomlUnsupported();
|
|
1186
|
+
}
|
|
1187
|
+
function tableAt(root, path) {
|
|
1188
|
+
let table = root;
|
|
1189
|
+
for (const part of path) {
|
|
1190
|
+
const existing = table[part];
|
|
1191
|
+
if (existing === undefined) {
|
|
1192
|
+
const created = Object.create(null);
|
|
1193
|
+
table[part] = created;
|
|
1194
|
+
table = created;
|
|
1195
|
+
}
|
|
1196
|
+
else if (isTable(existing)) {
|
|
1197
|
+
table = existing;
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
throw new TomlUnsupported();
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return table;
|
|
1204
|
+
}
|
|
1205
|
+
/** @internal Parse the subset of TOML described above; throws when unsupported. */
|
|
1206
|
+
function parseSimpleToml(content) {
|
|
1207
|
+
const root = Object.create(null);
|
|
1208
|
+
let current = root;
|
|
1209
|
+
for (const rawLine of content.replace(/^/, '').split(/\r?\n/)) {
|
|
1210
|
+
const line = rawLine.trim();
|
|
1211
|
+
if (line === '' || line.startsWith('#'))
|
|
1212
|
+
continue;
|
|
1213
|
+
if (line.startsWith('[['))
|
|
1214
|
+
throw new TomlUnsupported();
|
|
1215
|
+
if (line.startsWith('[')) {
|
|
1216
|
+
const [path, end] = readKey(line, 1, ']');
|
|
1217
|
+
assertRestIsComment(line, end);
|
|
1218
|
+
current = tableAt(root, path);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
const [path, afterEquals] = readKey(line, 0, '=');
|
|
1222
|
+
const valueStart = skipSpaces(line, afterEquals);
|
|
1223
|
+
let value;
|
|
1224
|
+
let end;
|
|
1225
|
+
if (line.startsWith('"""', valueStart) || line.startsWith("'''", valueStart)) {
|
|
1226
|
+
throw new TomlUnsupported();
|
|
1227
|
+
}
|
|
1228
|
+
else if (line[valueStart] === '"') {
|
|
1229
|
+
[value, end] = readBasicString(line, valueStart);
|
|
1230
|
+
}
|
|
1231
|
+
else if (line[valueStart] === "'") {
|
|
1232
|
+
[value, end] = readLiteralString(line, valueStart);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
const match = /^[A-Za-z0-9_:.+-]+(?:[ T][0-9:.+Z-]+)?/.exec(line.slice(valueStart));
|
|
1236
|
+
if (!match)
|
|
1237
|
+
throw new TomlUnsupported();
|
|
1238
|
+
value = new TomlScalar(match[0]);
|
|
1239
|
+
end = valueStart + match[0].length;
|
|
1240
|
+
}
|
|
1241
|
+
assertRestIsComment(line, end);
|
|
1242
|
+
const key = path[path.length - 1];
|
|
1243
|
+
const table = tableAt(current, path.slice(0, -1));
|
|
1244
|
+
if (Object.prototype.hasOwnProperty.call(table, key))
|
|
1245
|
+
throw new TomlUnsupported();
|
|
1246
|
+
table[key] = value;
|
|
1247
|
+
}
|
|
1248
|
+
return root;
|
|
1249
|
+
}
|
|
1250
|
+
// ── Lookup (mirror of the Rust `lookup_credentials`) ─────────────────────────
|
|
1251
|
+
/** Mirror of the CLI's API URL normalisation for credential lookup. */
|
|
1252
|
+
function normalizeA4ApiUrl(url) {
|
|
1253
|
+
const trimmed = url.trim().replace(/\/+$/, '');
|
|
1254
|
+
const schemeAt = trimmed.indexOf('://');
|
|
1255
|
+
const scheme = schemeAt >= 0 ? trimmed.slice(0, schemeAt).toLowerCase() : '';
|
|
1256
|
+
const rest = schemeAt >= 0 ? trimmed.slice(schemeAt + 3) : trimmed;
|
|
1257
|
+
const authorityEnd = rest.search(/[/?#]/);
|
|
1258
|
+
const authority = authorityEnd >= 0 ? rest.slice(0, authorityEnd) : rest;
|
|
1259
|
+
const tail = authorityEnd >= 0 ? rest.slice(authorityEnd) : '';
|
|
1260
|
+
const hostStart = authority.lastIndexOf('@') + 1;
|
|
1261
|
+
const hostPort = authority.slice(hostStart);
|
|
1262
|
+
let host;
|
|
1263
|
+
let port;
|
|
1264
|
+
if (hostPort.startsWith('[')) {
|
|
1265
|
+
const close = hostPort.indexOf(']');
|
|
1266
|
+
host = close >= 0 ? hostPort.slice(0, close + 1) : hostPort;
|
|
1267
|
+
port = close >= 0 ? hostPort.slice(close + 1) : '';
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
const colon = hostPort.lastIndexOf(':');
|
|
1271
|
+
host = colon >= 0 ? hostPort.slice(0, colon) : hostPort;
|
|
1272
|
+
port = colon >= 0 ? hostPort.slice(colon) : '';
|
|
1273
|
+
}
|
|
1274
|
+
host = host.replace(/\.+$/, '').toLowerCase();
|
|
1275
|
+
return `${scheme ? `${scheme}://` : ''}${authority.slice(0, hostStart)}${host}${port}${tail}`;
|
|
1276
|
+
}
|
|
1277
|
+
function stringMap(value) {
|
|
1278
|
+
if (value === undefined)
|
|
1279
|
+
return undefined;
|
|
1280
|
+
if (!isTable(value))
|
|
1281
|
+
throw new TomlUnsupported();
|
|
1282
|
+
const out = new Map();
|
|
1283
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1284
|
+
if (typeof entry !== 'string')
|
|
1285
|
+
throw new TomlUnsupported();
|
|
1286
|
+
out.set(key, entry);
|
|
1287
|
+
}
|
|
1288
|
+
return out;
|
|
1289
|
+
}
|
|
1290
|
+
function findUrlKey(keys, apiUrl) {
|
|
1291
|
+
if (!keys)
|
|
1292
|
+
return undefined;
|
|
1293
|
+
const wanted = normalizeA4ApiUrl(apiUrl);
|
|
1294
|
+
const exact = (keys.get(apiUrl) ?? keys.get(wanted))?.trim();
|
|
1295
|
+
if (exact)
|
|
1296
|
+
return exact;
|
|
1297
|
+
return [...keys.entries()]
|
|
1298
|
+
.filter(([url]) => normalizeA4ApiUrl(url) === wanted)
|
|
1299
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
1300
|
+
.map(([, key]) => key.trim())
|
|
1301
|
+
.find((key) => key !== '');
|
|
1302
|
+
}
|
|
1303
|
+
function isValidProfileName(profile) {
|
|
1304
|
+
return profile.length > 0 && profile.length <= 64 && /^[A-Za-z0-9_-]+$/.test(profile);
|
|
1305
|
+
}
|
|
1306
|
+
function keyFitsProfile(profile, key) {
|
|
1307
|
+
const agentKey = key.trim().startsWith('a4_ak_');
|
|
1308
|
+
if (profile === AGENT_PROFILE)
|
|
1309
|
+
return agentKey;
|
|
1310
|
+
if (profile === HUMAN_PROFILE)
|
|
1311
|
+
return !agentKey;
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
/** @internal Mirror of the Rust `lookup_credentials`. */
|
|
1315
|
+
function lookupA4Credentials(content, apiUrl, requestedProfile) {
|
|
1316
|
+
let profiles;
|
|
1317
|
+
let legacyKeys;
|
|
1318
|
+
let legacyApiKey;
|
|
1319
|
+
try {
|
|
1320
|
+
const parsed = parseSimpleToml(content);
|
|
1321
|
+
const profilesValue = parsed['profiles'];
|
|
1322
|
+
if (profilesValue !== undefined && !isTable(profilesValue))
|
|
1323
|
+
return { kind: 'invalid' };
|
|
1324
|
+
profiles = Object.entries(profilesValue ?? {}).map(([name, profile]) => {
|
|
1325
|
+
if (!isTable(profile))
|
|
1326
|
+
throw new TomlUnsupported();
|
|
1327
|
+
return [name, stringMap(profile['keys'])];
|
|
1328
|
+
});
|
|
1329
|
+
legacyKeys = stringMap(parsed['keys']);
|
|
1330
|
+
const apiKey = parsed['api_key'];
|
|
1331
|
+
if (apiKey !== undefined && typeof apiKey !== 'string')
|
|
1332
|
+
return { kind: 'invalid' };
|
|
1333
|
+
legacyApiKey = apiKey?.trim() || undefined;
|
|
1334
|
+
}
|
|
1335
|
+
catch {
|
|
1336
|
+
return { kind: 'invalid' };
|
|
1337
|
+
}
|
|
1338
|
+
const legacy = findUrlKey(legacyKeys, apiUrl) ?? legacyApiKey;
|
|
1339
|
+
if (requestedProfile !== undefined) {
|
|
1340
|
+
if (!isValidProfileName(requestedProfile))
|
|
1341
|
+
return { kind: 'invalid' };
|
|
1342
|
+
const entry = profiles.find(([name]) => name === requestedProfile);
|
|
1343
|
+
const key = findUrlKey(entry?.[1], apiUrl);
|
|
1344
|
+
if (key !== undefined) {
|
|
1345
|
+
return keyFitsProfile(requestedProfile, key) ? { kind: 'key', key } : { kind: 'invalid' };
|
|
1346
|
+
}
|
|
1347
|
+
if (legacy !== undefined) {
|
|
1348
|
+
const compatible = requestedProfile === AGENT_PROFILE
|
|
1349
|
+
? legacy.startsWith('a4_ak_')
|
|
1350
|
+
: requestedProfile === HUMAN_PROFILE && !legacy.startsWith('a4_ak_');
|
|
1351
|
+
if (compatible)
|
|
1352
|
+
return { kind: 'key', key: legacy };
|
|
1353
|
+
}
|
|
1354
|
+
return { kind: 'none' };
|
|
1355
|
+
}
|
|
1356
|
+
const matches = profiles
|
|
1357
|
+
.map(([name, keys]) => [name, findUrlKey(keys, apiUrl)])
|
|
1358
|
+
.filter((entry) => entry[1] !== undefined);
|
|
1359
|
+
if (matches.length > 1)
|
|
1360
|
+
return { kind: 'ambiguous' };
|
|
1361
|
+
if (matches.length === 1) {
|
|
1362
|
+
const [name, key] = matches[0];
|
|
1363
|
+
return keyFitsProfile(name, key) ? { kind: 'key', key } : { kind: 'invalid' };
|
|
1364
|
+
}
|
|
1365
|
+
return legacy !== undefined ? { kind: 'key', key: legacy } : { kind: 'none' };
|
|
1366
|
+
}
|
|
1367
|
+
/** Selected profile: `undefined` for none, `null` when selection is invalid. */
|
|
1368
|
+
function selectProfile(host) {
|
|
1369
|
+
// The project file can pin the agent profile; if it cannot be checked
|
|
1370
|
+
// (unknown working directory, denied or failed read), choose nothing rather
|
|
1371
|
+
// than risk a profile the project excludes.
|
|
1372
|
+
const cwd = host.cwd();
|
|
1373
|
+
if (cwd === undefined)
|
|
1374
|
+
return null;
|
|
1375
|
+
let project;
|
|
1376
|
+
try {
|
|
1377
|
+
project = host.readTextFile(host.joinPath(cwd, '.arete', 'auth.toml'));
|
|
1378
|
+
}
|
|
1379
|
+
catch {
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
if (project !== null) {
|
|
1383
|
+
try {
|
|
1384
|
+
const profile = parseSimpleToml(project)['default_profile'];
|
|
1385
|
+
return profile === AGENT_PROFILE ? AGENT_PROFILE : null;
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const profile = host.readEnv(PROFILE_ENV)?.trim();
|
|
1392
|
+
if (profile === undefined)
|
|
1393
|
+
return undefined;
|
|
1394
|
+
return isValidProfileName(profile) ? profile : null;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* @internal Key of the active `a4` login for the default Arete API, or
|
|
1398
|
+
* `undefined`. Never throws. The caller checks the key class.
|
|
1399
|
+
*/
|
|
1400
|
+
function readA4ProfileKey(host = systemProfileHost()) {
|
|
1401
|
+
if (!host)
|
|
1402
|
+
return {};
|
|
1403
|
+
try {
|
|
1404
|
+
const profile = selectProfile(host);
|
|
1405
|
+
if (profile === null)
|
|
1406
|
+
return {};
|
|
1407
|
+
const override = host.readEnv(CREDENTIALS_PATH_ENV);
|
|
1408
|
+
let path;
|
|
1409
|
+
if (override) {
|
|
1410
|
+
path = override;
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
const home = host.homeDir();
|
|
1414
|
+
path = home ? host.joinPath(home, '.arete', 'credentials.toml') : undefined;
|
|
1415
|
+
}
|
|
1416
|
+
if (!path)
|
|
1417
|
+
return {};
|
|
1418
|
+
const content = host.readTextFile(path);
|
|
1419
|
+
if (content === null)
|
|
1420
|
+
return {};
|
|
1421
|
+
const lookup = lookupA4Credentials(content, DEFAULT_A4_API_URL, profile);
|
|
1422
|
+
if (lookup.kind === 'key')
|
|
1423
|
+
return { key: lookup.key };
|
|
1424
|
+
if (lookup.kind === 'ambiguous')
|
|
1425
|
+
return { ambiguous: true };
|
|
1426
|
+
return {};
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
return {};
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
function denoHost(deno) {
|
|
1433
|
+
const granted = (descriptor) => {
|
|
1434
|
+
try {
|
|
1435
|
+
return deno.permissions?.querySync?.(descriptor)?.state === 'granted';
|
|
1436
|
+
}
|
|
1437
|
+
catch {
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
const readEnv = (name) => granted({ name: 'env', variable: name }) ? deno.env?.get?.(name) : undefined;
|
|
1442
|
+
const separator = deno.build?.os === 'windows' ? '\\' : '/';
|
|
1443
|
+
return {
|
|
1444
|
+
readEnv,
|
|
1445
|
+
readTextFile(path) {
|
|
1446
|
+
// Without --allow-read Deno would prompt or throw; ask first. Denied
|
|
1447
|
+
// access is not a missing file: callers must not treat it as absent.
|
|
1448
|
+
if (!deno.readTextFileSync)
|
|
1449
|
+
throw new Error('file reads unavailable');
|
|
1450
|
+
if (!granted({ name: 'read', path }))
|
|
1451
|
+
throw new Error('read permission not granted');
|
|
1452
|
+
try {
|
|
1453
|
+
return deno.readTextFileSync(path);
|
|
1454
|
+
}
|
|
1455
|
+
catch (error) {
|
|
1456
|
+
const notFound = deno.errors?.NotFound;
|
|
1457
|
+
if (notFound && error instanceof notFound)
|
|
1458
|
+
return null;
|
|
1459
|
+
throw error;
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
cwd() {
|
|
1463
|
+
if (!granted({ name: 'read' }))
|
|
1464
|
+
return undefined;
|
|
1465
|
+
try {
|
|
1466
|
+
return deno.cwd?.();
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
return undefined;
|
|
1470
|
+
}
|
|
1471
|
+
},
|
|
1472
|
+
homeDir: () => readEnv('HOME') ?? readEnv('USERPROFILE'),
|
|
1473
|
+
joinPath: (...parts) => parts.join(separator),
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
function nodeHost(process) {
|
|
1477
|
+
const load = process.getBuiltinModule;
|
|
1478
|
+
if (typeof load !== 'function')
|
|
1479
|
+
return undefined;
|
|
1480
|
+
const fs = load.call(process, 'fs');
|
|
1481
|
+
const os = load.call(process, 'os');
|
|
1482
|
+
const path = load.call(process, 'path');
|
|
1483
|
+
if (!fs || !os || !path)
|
|
1484
|
+
return undefined;
|
|
1485
|
+
return {
|
|
1486
|
+
readEnv: (name) => process.env?.[name],
|
|
1487
|
+
readTextFile(file) {
|
|
1488
|
+
try {
|
|
1489
|
+
return fs.readFileSync(file, 'utf8');
|
|
1490
|
+
}
|
|
1491
|
+
catch (error) {
|
|
1492
|
+
if (error.code === 'ENOENT')
|
|
1493
|
+
return null;
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
cwd: () => process.cwd?.(),
|
|
1498
|
+
homeDir: () => os.homedir(),
|
|
1499
|
+
joinPath: (...parts) => path.join(...parts),
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
/** Host for the current server runtime, or `undefined` where files are unreachable. */
|
|
1503
|
+
function systemProfileHost() {
|
|
1504
|
+
try {
|
|
1505
|
+
const scope = globalThis;
|
|
1506
|
+
if (scope.Deno)
|
|
1507
|
+
return denoHost(scope.Deno);
|
|
1508
|
+
if (scope.process)
|
|
1509
|
+
return nodeHost(scope.process);
|
|
1510
|
+
}
|
|
1511
|
+
catch {
|
|
1512
|
+
// Fall through: no profile support in this runtime.
|
|
1513
|
+
}
|
|
1514
|
+
return undefined;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1044
1517
|
/** Environment variable read for a secret-class key outside browsers. */
|
|
1045
1518
|
const ARETE_API_KEY_ENV = 'ARETE_API_KEY';
|
|
1046
1519
|
const CREATE_PUBLISHABLE_HINT = 'create one with `a4 auth keys create-publishable --origin <scheme://host[:port]>`';
|
|
@@ -1090,6 +1563,19 @@ function readEnvironmentVariable(name) {
|
|
|
1090
1563
|
}
|
|
1091
1564
|
}
|
|
1092
1565
|
const warned$1 = new Set();
|
|
1566
|
+
/**
|
|
1567
|
+
* Marks a resolved config whose `secretKey` came from the `a4` login. It holds
|
|
1568
|
+
* that key, so the mark is carried through object spreads (binding paths copy
|
|
1569
|
+
* the resolved config) and only applies while `secretKey` is still that key:
|
|
1570
|
+
* a copy given another `secretKey` is no longer restricted. Per config, never
|
|
1571
|
+
* process-wide, so the same key passed explicitly elsewhere is unaffected.
|
|
1572
|
+
*/
|
|
1573
|
+
const A4_LOGIN_SECRET_KEY = Symbol('arete.a4LoginSecretKey');
|
|
1574
|
+
/** True when `auth.secretKey` was supplied by the `a4` login fallback. */
|
|
1575
|
+
function secretKeyFromA4Login(auth) {
|
|
1576
|
+
const marked = auth;
|
|
1577
|
+
return marked?.secretKey !== undefined && marked[A4_LOGIN_SECRET_KEY] === marked.secretKey;
|
|
1578
|
+
}
|
|
1093
1579
|
function warnOnce$1(id, message) {
|
|
1094
1580
|
if (warned$1.has(id))
|
|
1095
1581
|
return;
|
|
@@ -1104,17 +1590,18 @@ function hasExplicitAuth(auth) {
|
|
|
1104
1590
|
|| auth.secretKey !== undefined);
|
|
1105
1591
|
}
|
|
1106
1592
|
/**
|
|
1107
|
-
* Validate the configured API keys and apply the
|
|
1593
|
+
* Validate the configured API keys and apply the server-side credential chain.
|
|
1108
1594
|
*
|
|
1109
1595
|
* - `secretKey` is refused in browsers and refuses publishable keys.
|
|
1110
1596
|
* - A secret-class key in `publishableKey` is refused in browsers and warned
|
|
1111
1597
|
* about elsewhere (it still works server-side, as it always has).
|
|
1112
1598
|
* - Outside browsers, when no auth option is set at all, `ARETE_API_KEY`
|
|
1113
|
-
* supplies `secretKey
|
|
1599
|
+
* supplies `secretKey`; without it, the agent or secret key from the active
|
|
1600
|
+
* `a4` CLI login does. Browsers never read either.
|
|
1114
1601
|
*
|
|
1115
1602
|
* Error and warning text never includes key material.
|
|
1116
1603
|
*/
|
|
1117
|
-
function resolveAuthConfig(auth) {
|
|
1604
|
+
function resolveAuthConfig(auth, readProfileKey = readA4ProfileKey) {
|
|
1118
1605
|
const browser = isBrowserEnvironment();
|
|
1119
1606
|
if (auth?.secretKey !== undefined) {
|
|
1120
1607
|
if (browser) {
|
|
@@ -1145,19 +1632,47 @@ function resolveAuthConfig(auth) {
|
|
|
1145
1632
|
if (browser || hasExplicitAuth(auth))
|
|
1146
1633
|
return auth;
|
|
1147
1634
|
const environmentKey = readEnvironmentVariable(ARETE_API_KEY_ENV)?.trim();
|
|
1148
|
-
if (
|
|
1149
|
-
|
|
1150
|
-
|
|
1635
|
+
if (environmentKey) {
|
|
1636
|
+
if (classifyApiKey(environmentKey) !== 'publishable') {
|
|
1637
|
+
return { ...auth, secretKey: environmentKey };
|
|
1638
|
+
}
|
|
1151
1639
|
warnOnce$1('publishable-in-env', `${ARETE_API_KEY_ENV} holds a publishable key (a4_pk_...) and was ignored. Set it to an agent `
|
|
1152
1640
|
+ 'key (a4_ak_...) or secret key (a4_sk_...), or pass the publishable key as auth.publishableKey.');
|
|
1153
|
-
return auth;
|
|
1154
1641
|
}
|
|
1155
|
-
|
|
1642
|
+
const profile = readProfileKey();
|
|
1643
|
+
if (profile.ambiguous) {
|
|
1644
|
+
warnOnce$1('ambiguous-a4-profile', 'More than one a4 login profile holds a key; not choosing one. Set ARETE_PROFILE '
|
|
1645
|
+
+ `(for example \`agent\`) or ${ARETE_API_KEY_ENV}.`);
|
|
1646
|
+
}
|
|
1647
|
+
if (profile.key && classifyApiKey(profile.key) === 'secret') {
|
|
1648
|
+
const resolved = {
|
|
1649
|
+
...auth,
|
|
1650
|
+
secretKey: profile.key,
|
|
1651
|
+
[A4_LOGIN_SECRET_KEY]: profile.key,
|
|
1652
|
+
};
|
|
1653
|
+
return resolved;
|
|
1654
|
+
}
|
|
1655
|
+
return auth;
|
|
1156
1656
|
}
|
|
1157
|
-
/**
|
|
1158
|
-
|
|
1657
|
+
/**
|
|
1658
|
+
* Appended to a 401 for a request that carried no API key. It names the
|
|
1659
|
+
* commands and options that supply one, never where credentials are stored.
|
|
1660
|
+
*/
|
|
1661
|
+
const NO_API_KEY_HINT = 'No Arete API key found. Run `a4 auth login` (or `a4 auth signup` for an agent), '
|
|
1662
|
+
+ 'or set ARETE_API_KEY, or pass auth.secretKey.';
|
|
1663
|
+
/**
|
|
1664
|
+
* The key sent as the bearer credential to `endpoint`, if any. A key taken
|
|
1665
|
+
* from the `a4` login is only sent to the Arete API it was stored for.
|
|
1666
|
+
*/
|
|
1667
|
+
function tokenEndpointApiKey(auth, endpoint) {
|
|
1668
|
+
if (secretKeyFromA4Login(auth) && !isA4LoginKeyDestination(endpoint))
|
|
1669
|
+
return undefined;
|
|
1159
1670
|
return auth?.secretKey ?? auth?.publishableKey;
|
|
1160
1671
|
}
|
|
1672
|
+
/** True when `headers` carries its own `Authorization` header. */
|
|
1673
|
+
function hasAuthorizationHeader(headers) {
|
|
1674
|
+
return Object.keys(headers ?? {}).some((name) => name.toLowerCase() === 'authorization');
|
|
1675
|
+
}
|
|
1161
1676
|
|
|
1162
1677
|
const GZIP_MAGIC_0 = 0x1f;
|
|
1163
1678
|
const GZIP_MAGIC_1 = 0x8b;
|
|
@@ -2218,7 +2733,7 @@ class ConnectionManager {
|
|
|
2218
2733
|
};
|
|
2219
2734
|
}
|
|
2220
2735
|
async fetchTokenFromEndpoint(tokenEndpoint, request) {
|
|
2221
|
-
const apiKey = tokenEndpointApiKey(this.authConfig);
|
|
2736
|
+
const apiKey = tokenEndpointApiKey(this.authConfig, tokenEndpoint);
|
|
2222
2737
|
const response = await this.authFetch(tokenEndpoint, {
|
|
2223
2738
|
method: 'POST',
|
|
2224
2739
|
headers: {
|
|
@@ -2249,9 +2764,17 @@ class ConnectionManager {
|
|
|
2249
2764
|
: response.status === 429
|
|
2250
2765
|
? 'QUOTA_EXCEEDED'
|
|
2251
2766
|
: 'AUTH_REQUIRED';
|
|
2252
|
-
const
|
|
2767
|
+
const responseMessage = typeof parsedError?.error === 'string' && parsedError.error.length > 0
|
|
2253
2768
|
? parsedError.error
|
|
2254
2769
|
: rawError || response.statusText || 'Authentication request failed';
|
|
2770
|
+
// A server-side 401 for a request that carried no credential at all:
|
|
2771
|
+
// say how to supply one. Custom Authorization headers have their own
|
|
2772
|
+
// advice to give.
|
|
2773
|
+
const sentCredential = Boolean(apiKey)
|
|
2774
|
+
|| hasAuthorizationHeader(this.authConfig?.tokenEndpointHeaders);
|
|
2775
|
+
const errorMessage = response.status === 401 && !sentCredential && !isBrowserEnvironment()
|
|
2776
|
+
? `${responseMessage}. ${NO_API_KEY_HINT}`
|
|
2777
|
+
: responseMessage;
|
|
2255
2778
|
const retryAfterHeader = response.headers.get('Retry-After');
|
|
2256
2779
|
const retryAfterSeconds = retryAfterHeader && /^\d+$/.test(retryAfterHeader)
|
|
2257
2780
|
? Number(retryAfterHeader)
|