@hasna/recordings 0.3.8 → 0.3.9

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/storage.js CHANGED
@@ -1022,7 +1022,7 @@ function setAgentFocus(idOrName, projectId, db) {
1022
1022
  // package.json
1023
1023
  var package_default = {
1024
1024
  name: "@hasna/recordings",
1025
- version: "0.3.8",
1025
+ version: "0.3.9",
1026
1026
  type: "module",
1027
1027
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
1028
1028
  repository: {
@@ -1106,7 +1106,7 @@ var package_default = {
1106
1106
  "LICENSE"
1107
1107
  ],
1108
1108
  dependencies: {
1109
- "@hasna/contracts": "0.13.3",
1109
+ "@hasna/contracts": "0.13.4",
1110
1110
  "@hasna/events": "0.1.11",
1111
1111
  "@modelcontextprotocol/sdk": "^1.12.1",
1112
1112
  chalk: "^5.4.1",
@@ -1138,12 +1138,1688 @@ function saveFeedback(input) {
1138
1138
  db.query("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(input.message, input.email ?? null, input.category ?? "general", input.version ?? VERSION);
1139
1139
  }
1140
1140
 
1141
+ // ../contracts/dist/client/transport.js
1142
+ import { isIP } from "net";
1143
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
1144
+ import { join as join3 } from "path";
1145
+ function envToken(name) {
1146
+ return name.toUpperCase().replace(/-/g, "_");
1147
+ }
1148
+ function clientTransportEnvKeys(name) {
1149
+ const envSegment = envToken(name);
1150
+ return {
1151
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
1152
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
1153
+ };
1154
+ }
1155
+ function credentialOverrideEnvKey(name) {
1156
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
1157
+ }
1158
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
1159
+
1160
+ class CredentialResolutionError extends Error {
1161
+ appName;
1162
+ attempted;
1163
+ constructor(appName, message, attempted) {
1164
+ super(message);
1165
+ this.name = "CredentialResolutionError";
1166
+ this.appName = appName;
1167
+ this.attempted = attempted;
1168
+ }
1169
+ }
1170
+ var HASNA_STATE_DIR = ".hasna";
1171
+ var FLEET_CREDENTIAL_DIR = "cloud";
1172
+ var CONFIG_DIR = ".config";
1173
+ var CONFIG_NAMESPACE = "hasna";
1174
+ var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
1175
+ var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
1176
+ var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
1177
+ var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
1178
+ function homeDir(env) {
1179
+ const home = env.HOME?.trim();
1180
+ return home ? home : null;
1181
+ }
1182
+ function credentialDiskSources(name, env) {
1183
+ return profileDiskSources(name, env, null);
1184
+ }
1185
+ function profileDiskSources(name, env, profile) {
1186
+ const home = homeDir(env);
1187
+ if (!home || !SAFE_APP_SLUG.test(name))
1188
+ return [];
1189
+ const stem = profile ? `${name}.${profile}` : name;
1190
+ const configStem = profile ? `${name}-${profile}` : name;
1191
+ return [
1192
+ join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
1193
+ join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
1194
+ ];
1195
+ }
1196
+ function parseEnvFile(text) {
1197
+ const values = new Map;
1198
+ for (const rawLine of text.split(/\r?\n/)) {
1199
+ const line = rawLine.trim();
1200
+ if (line.length === 0 || line.startsWith("#"))
1201
+ continue;
1202
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
1203
+ const equals = withoutExport.indexOf("=");
1204
+ if (equals <= 0)
1205
+ continue;
1206
+ const key = withoutExport.slice(0, equals).trim();
1207
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
1208
+ continue;
1209
+ let value = withoutExport.slice(equals + 1).trim();
1210
+ const quote = value[0];
1211
+ if (quote === '"' || quote === "'") {
1212
+ if (value.length < 2 || !value.endsWith(quote))
1213
+ continue;
1214
+ value = value.slice(1, -1);
1215
+ }
1216
+ if (value.length === 0)
1217
+ continue;
1218
+ values.set(key, value);
1219
+ }
1220
+ return values;
1221
+ }
1222
+ function readAppConfigFile(path) {
1223
+ let text;
1224
+ try {
1225
+ const stats = statSync2(path);
1226
+ if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
1227
+ return null;
1228
+ text = readFileSync2(path, "utf8");
1229
+ } catch {
1230
+ return null;
1231
+ }
1232
+ return parseEnvFile(text);
1233
+ }
1234
+ function readCredentialFile(path, apiKeyKeys) {
1235
+ const values = readAppConfigFile(path);
1236
+ if (!values)
1237
+ return null;
1238
+ for (const key of apiKeyKeys) {
1239
+ const value = values.get(key)?.trim();
1240
+ if (value)
1241
+ return value;
1242
+ }
1243
+ return null;
1244
+ }
1245
+ var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
1246
+ function appConfigDiskValue(name, env, keys) {
1247
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
1248
+ if (wanted.length === 0)
1249
+ return null;
1250
+ for (const path of credentialDiskSources(name, env)) {
1251
+ const values = readAppConfigFile(path);
1252
+ if (!values)
1253
+ continue;
1254
+ for (const key of wanted) {
1255
+ const value = values.get(key)?.trim();
1256
+ if (value)
1257
+ return { key, value, path };
1258
+ }
1259
+ }
1260
+ return null;
1261
+ }
1262
+ function assertUsableCredential(appName, source, value) {
1263
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
1264
+ return;
1265
+ throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
1266
+ }
1267
+ var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
1268
+ var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
1269
+ var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
1270
+ function sealCredential(fields) {
1271
+ const { apiKey } = fields;
1272
+ const visible = {
1273
+ tier: fields.tier,
1274
+ source: fields.source,
1275
+ deliberate: fields.deliberate,
1276
+ deprecated: fields.deprecated,
1277
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
1278
+ warning: fields.warning
1279
+ };
1280
+ const sealed = { ...visible };
1281
+ Object.defineProperty(sealed, "apiKey", {
1282
+ value: apiKey,
1283
+ enumerable: false,
1284
+ writable: false,
1285
+ configurable: false
1286
+ });
1287
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
1288
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
1289
+ enumerable: false,
1290
+ writable: false,
1291
+ configurable: false
1292
+ });
1293
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
1294
+ value: true,
1295
+ enumerable: false,
1296
+ writable: false,
1297
+ configurable: false
1298
+ });
1299
+ return Object.freeze(sealed);
1300
+ }
1301
+ function isSealedCredential(credential) {
1302
+ return credential[CREDENTIAL_SEAL] === true;
1303
+ }
1304
+ function explicitCredential(appName, apiKey) {
1305
+ const source = "explicit apiKey option";
1306
+ assertUsableCredential(appName, source, apiKey);
1307
+ return sealCredential({
1308
+ apiKey,
1309
+ tier: "argument",
1310
+ source,
1311
+ deliberate: true,
1312
+ deprecated: false,
1313
+ diskCandidates: [],
1314
+ warning: null
1315
+ });
1316
+ }
1317
+ function validateAndSealResolvedCredential(appName, credential) {
1318
+ const apiKey = credential.apiKey;
1319
+ assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
1320
+ if (!isSealedCredential(credential)) {
1321
+ return sealCredential({
1322
+ apiKey,
1323
+ tier: "argument",
1324
+ source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
1325
+ deliberate: true,
1326
+ deprecated: false,
1327
+ diskCandidates: [],
1328
+ warning: null
1329
+ });
1330
+ }
1331
+ return sealCredential({
1332
+ apiKey,
1333
+ tier: credential.tier,
1334
+ source: credential.source,
1335
+ deliberate: credential.deliberate,
1336
+ deprecated: credential.deprecated,
1337
+ diskCandidates: credential.diskCandidates,
1338
+ warning: credential.warning
1339
+ });
1340
+ }
1341
+ function firstEnvValue(env, keys) {
1342
+ for (const key of keys) {
1343
+ const value = env[key]?.trim();
1344
+ if (value)
1345
+ return { key, value };
1346
+ }
1347
+ return null;
1348
+ }
1349
+ var DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
1350
+ function deprecationNotified() {
1351
+ const host = globalThis;
1352
+ const existing = host[DEPRECATION_REGISTRY];
1353
+ if (existing instanceof Set)
1354
+ return existing;
1355
+ const created = new Set;
1356
+ host[DEPRECATION_REGISTRY] = created;
1357
+ return created;
1358
+ }
1359
+ function defaultDeprecationSink(message) {
1360
+ if (typeof process !== "undefined" && process.stderr) {
1361
+ process.stderr.write(`${message}
1362
+ `);
1363
+ }
1364
+ }
1365
+ function resolveCredential(name, env, options = {}) {
1366
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
1367
+ const diskPaths = credentialDiskSources(name, env);
1368
+ const explicitKey = options.apiKey?.trim();
1369
+ if (explicitKey) {
1370
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
1371
+ return sealCredential({
1372
+ apiKey: explicitKey,
1373
+ tier: "argument",
1374
+ source: "explicit apiKey argument",
1375
+ deliberate: true,
1376
+ deprecated: false,
1377
+ diskCandidates: diskPaths,
1378
+ warning: null
1379
+ });
1380
+ }
1381
+ const overrideKeyName = credentialOverrideEnvKey(name);
1382
+ const overrideRaw = env[overrideKeyName];
1383
+ if (overrideRaw !== undefined) {
1384
+ const override = overrideRaw.trim();
1385
+ if (!override) {
1386
+ throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
1387
+ }
1388
+ assertUsableCredential(name, overrideKeyName, override);
1389
+ return sealCredential({
1390
+ apiKey: override,
1391
+ tier: "override",
1392
+ source: overrideKeyName,
1393
+ deliberate: true,
1394
+ deprecated: false,
1395
+ diskCandidates: diskPaths,
1396
+ warning: null
1397
+ });
1398
+ }
1399
+ const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
1400
+ if (profile) {
1401
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
1402
+ if (!SAFE_PROFILE.test(profile)) {
1403
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
1404
+ }
1405
+ const paths = profileDiskSources(name, env, profile);
1406
+ for (const path of paths) {
1407
+ const value = readCredentialFile(path, apiKeyKeys);
1408
+ if (value) {
1409
+ assertUsableCredential(name, path, value);
1410
+ return sealCredential({
1411
+ apiKey: value,
1412
+ tier: "profile",
1413
+ source: path,
1414
+ deliberate: true,
1415
+ deprecated: false,
1416
+ diskCandidates: paths,
1417
+ warning: null
1418
+ });
1419
+ }
1420
+ }
1421
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
1422
+ }
1423
+ const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
1424
+ if (diskHits.length > 0) {
1425
+ const winner = diskHits[0];
1426
+ assertUsableCredential(name, winner.path, winner.value);
1427
+ const divergentSources = [
1428
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
1429
+ ...(() => {
1430
+ const legacyHit = firstEnvValue(env, apiKeyKeys);
1431
+ return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
1432
+ })()
1433
+ ];
1434
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
1435
+ return sealCredential({
1436
+ apiKey: winner.value,
1437
+ tier: "disk",
1438
+ source: winner.path,
1439
+ deliberate: false,
1440
+ deprecated: false,
1441
+ diskCandidates: diskPaths,
1442
+ warning
1443
+ });
1444
+ }
1445
+ const legacy = firstEnvValue(env, apiKeyKeys);
1446
+ if (legacy) {
1447
+ assertUsableCredential(name, legacy.key, legacy.value);
1448
+ const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
1449
+ const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
1450
+ const sink = options.onDeprecation ?? defaultDeprecationSink;
1451
+ const notified = deprecationNotified();
1452
+ if (!notified.has(name)) {
1453
+ notified.add(name);
1454
+ sink(message);
1455
+ }
1456
+ return sealCredential({
1457
+ apiKey: legacy.value,
1458
+ tier: "legacy-env",
1459
+ source: legacy.key,
1460
+ deliberate: false,
1461
+ deprecated: true,
1462
+ diskCandidates: diskPaths,
1463
+ warning: message
1464
+ });
1465
+ }
1466
+ return null;
1467
+ }
1468
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
1469
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
1470
+ function isValidDnsDomain(value) {
1471
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
1472
+ return false;
1473
+ }
1474
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
1475
+ }
1476
+ function firstEnv(env, keys, options = {}) {
1477
+ for (const key of keys) {
1478
+ const raw = env[key];
1479
+ const value = raw?.trim();
1480
+ if (value)
1481
+ return { key, value: options.preserveRaw ? raw : value };
1482
+ }
1483
+ return null;
1484
+ }
1485
+ function firstEnvDefinedKey(env, keys) {
1486
+ for (const key of keys) {
1487
+ if (env[key] !== undefined)
1488
+ return key;
1489
+ }
1490
+ return null;
1491
+ }
1492
+ function rawAuthority(value) {
1493
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
1494
+ if (!match)
1495
+ throw new Error("API URL must be absolute.");
1496
+ const afterScheme = value.slice(match[0].length);
1497
+ const boundary = afterScheme.search(/[/?#]/);
1498
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
1499
+ if (!authority)
1500
+ throw new Error("API URL must include a hostname.");
1501
+ return authority;
1502
+ }
1503
+ function assertCanonicalPort(port) {
1504
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
1505
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
1506
+ }
1507
+ const numericPort = Number(port);
1508
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
1509
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
1510
+ }
1511
+ }
1512
+ function canonicalAuthorityHostname(authority) {
1513
+ let rawHostname;
1514
+ if (authority.startsWith("[")) {
1515
+ const closingBracket = authority.indexOf("]");
1516
+ if (closingBracket === -1) {
1517
+ throw new Error("API URL authority must contain a canonical hostname.");
1518
+ }
1519
+ rawHostname = authority.slice(0, closingBracket + 1);
1520
+ const portSuffix = authority.slice(closingBracket + 1);
1521
+ if (portSuffix) {
1522
+ if (!portSuffix.startsWith(":")) {
1523
+ throw new Error("API URL authority must contain a canonical hostname and port.");
1524
+ }
1525
+ assertCanonicalPort(portSuffix.slice(1));
1526
+ }
1527
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
1528
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
1529
+ }
1530
+ } else {
1531
+ const firstColon = authority.indexOf(":");
1532
+ const lastColon = authority.lastIndexOf(":");
1533
+ if (firstColon !== lastColon) {
1534
+ throw new Error("IPv6 API URL authorities must use brackets.");
1535
+ }
1536
+ if (lastColon !== -1) {
1537
+ const port = authority.slice(lastColon + 1);
1538
+ assertCanonicalPort(port);
1539
+ rawHostname = authority.slice(0, lastColon);
1540
+ } else {
1541
+ rawHostname = authority;
1542
+ }
1543
+ const ipVersion = isIP(rawHostname);
1544
+ const numericAddressParts = rawHostname.split(".");
1545
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
1546
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
1547
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
1548
+ }
1549
+ }
1550
+ return rawHostname.toLowerCase();
1551
+ }
1552
+ function isDeliberateLoopbackHttpAuthority(authority) {
1553
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
1554
+ }
1555
+ function toV1BaseUrl(apiUrl) {
1556
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
1557
+ throw new Error("API URL must not contain ASCII control characters.");
1558
+ }
1559
+ const input = apiUrl.trim();
1560
+ const authority = rawAuthority(input);
1561
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
1562
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
1563
+ }
1564
+ const canonicalHostname = canonicalAuthorityHostname(authority);
1565
+ const url = new URL(input);
1566
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1567
+ throw new Error("API URL must use http or https.");
1568
+ }
1569
+ if (url.username || url.password) {
1570
+ throw new Error("API URL must not include credentials.");
1571
+ }
1572
+ if (!url.hostname || url.hostname.endsWith(".")) {
1573
+ throw new Error("API URL must include a canonical hostname.");
1574
+ }
1575
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
1576
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
1577
+ }
1578
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
1579
+ throw new Error("API URL must not use IDN or punycode hostnames.");
1580
+ }
1581
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
1582
+ throw new Error("API URL may use http only for an exact loopback authority.");
1583
+ }
1584
+ if (url.search || url.hash) {
1585
+ throw new Error("API URL must not include a query string or fragment.");
1586
+ }
1587
+ let path = url.pathname.replace(/\/+$/, "");
1588
+ if (path.endsWith("/v1"))
1589
+ path = path.slice(0, -"/v1".length);
1590
+ url.pathname = `${path}/v1`;
1591
+ return url.toString().replace(/\/+$/, "");
1592
+ }
1593
+ function resolveClientTransport(name, env = process.env, options = {}) {
1594
+ const keys = clientTransportEnvKeys(name);
1595
+ const envUrlHit = firstEnv(env, keys.apiUrlKeys, { preserveRaw: true });
1596
+ const explicitLocalKey = envUrlHit ? null : firstEnvDefinedKey(env, keys.apiUrlKeys);
1597
+ const diskUrlHit = envUrlHit || explicitLocalKey ? null : appConfigDiskValue(name, env, keys.apiUrlKeys);
1598
+ const urlHit = envUrlHit ?? (diskUrlHit ? { key: diskUrlHit.path, value: diskUrlHit.value } : null);
1599
+ const keyHit = firstEnv(env, keys.apiKeyKeys);
1600
+ const warnings = [];
1601
+ if (!urlHit) {
1602
+ if (explicitLocalKey) {
1603
+ const overriddenPointer = appConfigDiskValue(name, env, keys.apiUrlKeys);
1604
+ if (overriddenPointer) {
1605
+ warnings.push(`${explicitLocalKey} is defined but blank, which selects the local store. ` + `The server URL in ${overriddenPointer.path} was NOT selected: an explicit blank wins over a disk pointer.`);
1606
+ }
1607
+ return {
1608
+ transport: "sqlite",
1609
+ transportSource: explicitLocalKey,
1610
+ baseUrl: null,
1611
+ apiUrlSource: null,
1612
+ apiKeyPresent: Boolean(keyHit),
1613
+ apiKeySource: keyHit ? keyHit.key : null,
1614
+ apiKeyTier: null,
1615
+ misconfigured: false,
1616
+ warning: warnings.length > 0 ? warnings.join(" ") : null
1617
+ };
1618
+ }
1619
+ return {
1620
+ transport: "sqlite",
1621
+ transportSource: "default",
1622
+ baseUrl: null,
1623
+ apiUrlSource: null,
1624
+ apiKeyPresent: Boolean(keyHit),
1625
+ apiKeySource: keyHit ? keyHit.key : null,
1626
+ apiKeyTier: null,
1627
+ misconfigured: false,
1628
+ warning: null
1629
+ };
1630
+ }
1631
+ if (diskUrlHit) {
1632
+ warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${diskUrlHit.path} was used, so this client connects to the server. ` + `Unset the pointer or remove the file to stay on the local store.`);
1633
+ }
1634
+ const credential = resolveCredential(name, env, options.credentials);
1635
+ if (!credential) {
1636
+ const diskHint = credentialDiskSourcesForMessage(name, env);
1637
+ warnings.push(`${urlHit.key} selects the HTTP server for '${name}', but no API key could be resolved; ` + `refusing to route and leaving the local sqlite store selected. ` + `Looked for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
1638
+ return {
1639
+ transport: "sqlite",
1640
+ transportSource: urlHit.key,
1641
+ baseUrl: null,
1642
+ apiUrlSource: urlHit.key,
1643
+ apiKeyPresent: false,
1644
+ apiKeySource: null,
1645
+ apiKeyTier: null,
1646
+ misconfigured: true,
1647
+ warning: warnings.join(" ")
1648
+ };
1649
+ }
1650
+ if (credential.warning)
1651
+ warnings.push(credential.warning);
1652
+ const apiUrlSource = urlHit.key;
1653
+ let baseUrl;
1654
+ try {
1655
+ baseUrl = toV1BaseUrl(urlHit.value);
1656
+ } catch (error) {
1657
+ const message = error instanceof Error ? error.message : String(error);
1658
+ warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
1659
+ return {
1660
+ transport: "sqlite",
1661
+ transportSource: urlHit.key,
1662
+ baseUrl: null,
1663
+ apiUrlSource: urlHit.key,
1664
+ apiKeyPresent: true,
1665
+ apiKeySource: credential.source,
1666
+ apiKeyTier: credential.tier,
1667
+ misconfigured: true,
1668
+ warning: warnings.join(" ")
1669
+ };
1670
+ }
1671
+ return {
1672
+ transport: "http",
1673
+ transportSource: urlHit.key,
1674
+ baseUrl,
1675
+ apiUrlSource,
1676
+ apiKeyPresent: true,
1677
+ apiKeySource: credential.source,
1678
+ apiKeyTier: credential.tier,
1679
+ misconfigured: false,
1680
+ warning: warnings.length > 0 ? warnings.join(" ") : null
1681
+ };
1682
+ }
1683
+ function credentialDiskSourcesForMessage(name, env) {
1684
+ const paths = credentialDiskSources(name, env);
1685
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME set in this environment, so no credential file was consulted>";
1686
+ }
1687
+
1688
+ class HasnaHttpError extends Error {
1689
+ status;
1690
+ method;
1691
+ path;
1692
+ body;
1693
+ credentialSource;
1694
+ credentialTier;
1695
+ constructor(method, path, status, body, credential) {
1696
+ const guidance = credential ? `. ${credential.guidance}` : "";
1697
+ super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
1698
+ this.name = "HasnaHttpError";
1699
+ this.status = status;
1700
+ this.method = method;
1701
+ this.path = path;
1702
+ this.body = body;
1703
+ this.credentialSource = credential?.source ?? null;
1704
+ this.credentialTier = credential?.tier ?? null;
1705
+ }
1706
+ }
1707
+ function currentCredential(name, apiKey) {
1708
+ if (typeof apiKey === "function") {
1709
+ return validateAndSealResolvedCredential(name, apiKey());
1710
+ }
1711
+ return explicitCredential(name, apiKey);
1712
+ }
1713
+ function authFailureGuidance(credential) {
1714
+ const origin = `The API key for this request came from ${credential.source}`;
1715
+ if (credential.deliberate) {
1716
+ const remedy = credential.source === CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE ? `Fix that provider so it returns the current key, or replace it with resolveCredential() ` + `so diagnostics can name the original source.` : `Rotate that key, or unset the override to use the credential on disk.`;
1717
+ return `${origin} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: ` + `falling back here would authenticate as a different principal than the one you named, which is ` + `exactly the failure an override exists to prevent. ${remedy}`;
1718
+ }
1719
+ if (credential.deprecated) {
1720
+ const target = credential.diskCandidates[0];
1721
+ const remedy = target ? `Write the CURRENT key to ${target} \u2014 that file is re-read on every call, so rotations take ` + `effect immediately and in every shell. Do not simply unset ${credential.source}: nothing was ` + `found on disk, so that would leave this client with no credential at all.` : `This environment has no HOME, so no credential file could be consulted; the disk tier is ` + `unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.`;
1722
+ return `${origin}, a variable in this process's environment \u2014 which is a snapshot taken when the process ` + `started. A STALE SHELL is the most common cause of this error: this shell exported the key before ` + `it was rotated, and will keep sending the old one until it exits. ${remedy}`;
1723
+ }
1724
+ return `${origin}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. ` + `The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution ` + `so this machine gets the current key.`;
1725
+ }
1726
+ var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
1727
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
1728
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
1729
+ "host",
1730
+ ":authority",
1731
+ "forwarded",
1732
+ "x-forwarded-host",
1733
+ "x-original-host"
1734
+ ]);
1735
+ function assertNoAuthorityOverrideHeaders(headers, source) {
1736
+ if (!headers)
1737
+ return;
1738
+ const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS.has(name.trim().toLowerCase()));
1739
+ if (forbidden) {
1740
+ throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
1741
+ }
1742
+ }
1743
+ function appendQuery(path, query) {
1744
+ if (!query)
1745
+ return path;
1746
+ const params = query instanceof URLSearchParams ? query : new URLSearchParams;
1747
+ if (!(query instanceof URLSearchParams)) {
1748
+ for (const [key, value] of Object.entries(query)) {
1749
+ if (value === null || value === undefined)
1750
+ continue;
1751
+ if (Array.isArray(value)) {
1752
+ for (const v of value)
1753
+ params.append(key, String(v));
1754
+ } else {
1755
+ params.append(key, String(value));
1756
+ }
1757
+ }
1758
+ }
1759
+ const qs = params.toString();
1760
+ if (!qs)
1761
+ return path;
1762
+ return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
1763
+ }
1764
+ var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
1765
+ function createHasnaHttpTransport(options) {
1766
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
1767
+ const base = toV1BaseUrl(options.baseUrl);
1768
+ const timeoutMs = options.timeoutMs ?? 30000;
1769
+ const sleep = options.sleepImpl ?? defaultSleep;
1770
+ const defaultRetry = options.retry;
1771
+ function resolveRetry(callRetry) {
1772
+ const chosen = callRetry !== undefined ? callRetry : defaultRetry;
1773
+ if (chosen === false)
1774
+ return null;
1775
+ const r = chosen ?? {};
1776
+ return {
1777
+ retries: r.retries ?? 2,
1778
+ baseDelayMs: r.baseDelayMs ?? 200,
1779
+ maxDelayMs: r.maxDelayMs ?? 2000,
1780
+ retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
1781
+ };
1782
+ }
1783
+ async function once(method, rel, url, body, opts, credential) {
1784
+ assertNoAuthorityOverrideHeaders(options.headers, "transport");
1785
+ assertNoAuthorityOverrideHeaders(opts.headers, "request");
1786
+ const headers = {
1787
+ "x-api-key": credential.apiKey,
1788
+ Authorization: `Bearer ${credential.apiKey}`,
1789
+ Accept: "application/json",
1790
+ ...options.headers ?? {},
1791
+ ...opts.headers ?? {}
1792
+ };
1793
+ if (opts.idempotencyKey)
1794
+ headers["Idempotency-Key"] = opts.idempotencyKey;
1795
+ const init = {
1796
+ method,
1797
+ headers,
1798
+ redirect: "manual"
1799
+ };
1800
+ if (body !== undefined) {
1801
+ headers["Content-Type"] = "application/json";
1802
+ init.body = JSON.stringify(body);
1803
+ }
1804
+ const controller = new AbortController;
1805
+ const onAbort = () => controller.abort();
1806
+ if (opts.signal) {
1807
+ if (opts.signal.aborted)
1808
+ controller.abort();
1809
+ else
1810
+ opts.signal.addEventListener("abort", onAbort, { once: true });
1811
+ }
1812
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
1813
+ init.signal = controller.signal;
1814
+ let response;
1815
+ try {
1816
+ response = await fetchImpl(url, init);
1817
+ } catch (error) {
1818
+ const err = error instanceof Error ? error : new Error(String(error));
1819
+ if (opts.signal?.aborted)
1820
+ return { ok: false, retryable: false, error: err };
1821
+ return { ok: false, retryable: true, error: err };
1822
+ } finally {
1823
+ clearTimeout(timer);
1824
+ if (opts.signal)
1825
+ opts.signal.removeEventListener("abort", onAbort);
1826
+ }
1827
+ const text = await response.text();
1828
+ let parsed = undefined;
1829
+ if (text.length > 0) {
1830
+ try {
1831
+ parsed = JSON.parse(text);
1832
+ } catch {
1833
+ parsed = text;
1834
+ }
1835
+ }
1836
+ if (!response.ok) {
1837
+ if (response.status >= 300 && response.status < 400) {
1838
+ return {
1839
+ ok: false,
1840
+ retryable: false,
1841
+ error: new HasnaHttpError(method, rel, response.status, parsed)
1842
+ };
1843
+ }
1844
+ if (response.status === 401 || response.status === 403) {
1845
+ return {
1846
+ ok: false,
1847
+ retryable: false,
1848
+ error: new HasnaHttpError(method, rel, response.status, parsed, {
1849
+ source: credential.source,
1850
+ tier: credential.tier,
1851
+ guidance: authFailureGuidance(credential)
1852
+ })
1853
+ };
1854
+ }
1855
+ const retry = resolveRetry(opts.retry);
1856
+ const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
1857
+ return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
1858
+ }
1859
+ return { ok: true, value: parsed };
1860
+ }
1861
+ async function request(method, path, body, opts = {}) {
1862
+ const upper = method.toUpperCase();
1863
+ const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
1864
+ const url = `${base}${rel}`;
1865
+ const retry = resolveRetry(opts.retry);
1866
+ const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
1867
+ const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
1868
+ const credential = currentCredential(options.name, options.apiKey);
1869
+ let last = null;
1870
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
1871
+ const result = await once(upper, rel, url, body, opts, credential);
1872
+ if (result.ok)
1873
+ return result.value;
1874
+ last = result;
1875
+ const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
1876
+ if (!canRetry)
1877
+ break;
1878
+ const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
1879
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
1880
+ await sleep(backoff + jitter);
1881
+ }
1882
+ throw last.error;
1883
+ }
1884
+ return {
1885
+ baseUrl: base,
1886
+ request,
1887
+ get: (path, opts) => request("GET", path, undefined, opts),
1888
+ post: (path, body, opts) => request("POST", path, body, opts),
1889
+ put: (path, body, opts) => request("PUT", path, body, opts),
1890
+ patch: (path, body, opts) => request("PATCH", path, body, opts),
1891
+ del: (path, body, opts) => request("DELETE", path, body, opts)
1892
+ };
1893
+ }
1894
+ function createClientTransport(name, env = process.env, overrides) {
1895
+ const credentialOptions = overrides?.credentials;
1896
+ const resolution = resolveClientTransport(name, env, { ...credentialOptions ? { credentials: credentialOptions } : {} });
1897
+ if (resolution.misconfigured) {
1898
+ throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the API client.`);
1899
+ }
1900
+ if (resolution.transport === "sqlite" || !resolution.baseUrl) {
1901
+ return { transport: "sqlite", client: null, resolution };
1902
+ }
1903
+ const credentialProvider = () => {
1904
+ const resolved = resolveCredential(name, env, credentialOptions);
1905
+ if (!resolved) {
1906
+ throw new Error(`Client for '${name}' resolved to the http transport but no API key is available any more. ` + `Looked at ${credentialDiskSourcesForMessage(name, env)}, then the environment. ` + `A credential file that was removed after this client was built is the usual cause.`);
1907
+ }
1908
+ return resolved;
1909
+ };
1910
+ return {
1911
+ transport: "http",
1912
+ client: createHasnaHttpTransport({
1913
+ name,
1914
+ baseUrl: resolution.baseUrl,
1915
+ apiKey: credentialProvider,
1916
+ ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
1917
+ ...overrides?.headers ? { headers: overrides.headers } : {},
1918
+ ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
1919
+ ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
1920
+ ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
1921
+ }),
1922
+ resolution
1923
+ };
1924
+ }
1925
+
1926
+ // ../contracts/dist/client/storage.js
1927
+ import { isIP as isIP2 } from "net";
1928
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1929
+ import { join as join4 } from "path";
1930
+ function envToken2(name) {
1931
+ return name.toUpperCase().replace(/-/g, "_");
1932
+ }
1933
+ function clientTransportEnvKeys2(name) {
1934
+ const envSegment = envToken2(name);
1935
+ return {
1936
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
1937
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
1938
+ };
1939
+ }
1940
+ function credentialOverrideEnvKey2(name) {
1941
+ return `HASNA_${envToken2(name)}_API_KEY_OVERRIDE`;
1942
+ }
1943
+ var CREDENTIAL_PROFILE_ENV_KEY2 = "HASNA_PROFILE";
1944
+
1945
+ class CredentialResolutionError2 extends Error {
1946
+ appName;
1947
+ attempted;
1948
+ constructor(appName, message, attempted) {
1949
+ super(message);
1950
+ this.name = "CredentialResolutionError";
1951
+ this.appName = appName;
1952
+ this.attempted = attempted;
1953
+ }
1954
+ }
1955
+ var HASNA_STATE_DIR2 = ".hasna";
1956
+ var FLEET_CREDENTIAL_DIR2 = "cloud";
1957
+ var CONFIG_DIR2 = ".config";
1958
+ var CONFIG_NAMESPACE2 = "hasna";
1959
+ var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
1960
+ var SAFE_APP_SLUG2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
1961
+ var SAFE_PROFILE2 = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
1962
+ var ILLEGAL_IN_HEADER_VALUE2 = /[^\t\x20-\x7e]/;
1963
+ function homeDir2(env) {
1964
+ const home = env.HOME?.trim();
1965
+ return home ? home : null;
1966
+ }
1967
+ function credentialDiskSources2(name, env) {
1968
+ return profileDiskSources2(name, env, null);
1969
+ }
1970
+ function profileDiskSources2(name, env, profile) {
1971
+ const home = homeDir2(env);
1972
+ if (!home || !SAFE_APP_SLUG2.test(name))
1973
+ return [];
1974
+ const stem = profile ? `${name}.${profile}` : name;
1975
+ const configStem = profile ? `${name}-${profile}` : name;
1976
+ return [
1977
+ join4(home, HASNA_STATE_DIR2, FLEET_CREDENTIAL_DIR2, `${stem}.env`),
1978
+ join4(home, CONFIG_DIR2, CONFIG_NAMESPACE2, `${configStem}-cloud.env`)
1979
+ ];
1980
+ }
1981
+ function parseEnvFile2(text) {
1982
+ const values = new Map;
1983
+ for (const rawLine of text.split(/\r?\n/)) {
1984
+ const line = rawLine.trim();
1985
+ if (line.length === 0 || line.startsWith("#"))
1986
+ continue;
1987
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
1988
+ const equals = withoutExport.indexOf("=");
1989
+ if (equals <= 0)
1990
+ continue;
1991
+ const key = withoutExport.slice(0, equals).trim();
1992
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
1993
+ continue;
1994
+ let value = withoutExport.slice(equals + 1).trim();
1995
+ const quote = value[0];
1996
+ if (quote === '"' || quote === "'") {
1997
+ if (value.length < 2 || !value.endsWith(quote))
1998
+ continue;
1999
+ value = value.slice(1, -1);
2000
+ }
2001
+ if (value.length === 0)
2002
+ continue;
2003
+ values.set(key, value);
2004
+ }
2005
+ return values;
2006
+ }
2007
+ function readAppConfigFile2(path) {
2008
+ let text;
2009
+ try {
2010
+ const stats = statSync3(path);
2011
+ if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES2)
2012
+ return null;
2013
+ text = readFileSync3(path, "utf8");
2014
+ } catch {
2015
+ return null;
2016
+ }
2017
+ return parseEnvFile2(text);
2018
+ }
2019
+ function readCredentialFile2(path, apiKeyKeys) {
2020
+ const values = readAppConfigFile2(path);
2021
+ if (!values)
2022
+ return null;
2023
+ for (const key of apiKeyKeys) {
2024
+ const value = values.get(key)?.trim();
2025
+ if (value)
2026
+ return value;
2027
+ }
2028
+ return null;
2029
+ }
2030
+ var CREDENTIAL_SHAPED_KEY2 = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
2031
+ function appConfigDiskValue2(name, env, keys) {
2032
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY2.test(key));
2033
+ if (wanted.length === 0)
2034
+ return null;
2035
+ for (const path of credentialDiskSources2(name, env)) {
2036
+ const values = readAppConfigFile2(path);
2037
+ if (!values)
2038
+ continue;
2039
+ for (const key of wanted) {
2040
+ const value = values.get(key)?.trim();
2041
+ if (value)
2042
+ return { key, value, path };
2043
+ }
2044
+ }
2045
+ return null;
2046
+ }
2047
+ function assertUsableCredential2(appName, source, value) {
2048
+ if (!ILLEGAL_IN_HEADER_VALUE2.test(value))
2049
+ return;
2050
+ throw new CredentialResolutionError2(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
2051
+ }
2052
+ var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
2053
+ var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
2054
+ var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE2 = "caller-supplied CredentialProvider";
2055
+ function sealCredential2(fields) {
2056
+ const { apiKey } = fields;
2057
+ const visible = {
2058
+ tier: fields.tier,
2059
+ source: fields.source,
2060
+ deliberate: fields.deliberate,
2061
+ deprecated: fields.deprecated,
2062
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
2063
+ warning: fields.warning
2064
+ };
2065
+ const sealed = { ...visible };
2066
+ Object.defineProperty(sealed, "apiKey", {
2067
+ value: apiKey,
2068
+ enumerable: false,
2069
+ writable: false,
2070
+ configurable: false
2071
+ });
2072
+ Object.defineProperty(sealed, INSPECT_CUSTOM2, {
2073
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
2074
+ enumerable: false,
2075
+ writable: false,
2076
+ configurable: false
2077
+ });
2078
+ Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
2079
+ value: true,
2080
+ enumerable: false,
2081
+ writable: false,
2082
+ configurable: false
2083
+ });
2084
+ return Object.freeze(sealed);
2085
+ }
2086
+ function isSealedCredential2(credential) {
2087
+ return credential[CREDENTIAL_SEAL2] === true;
2088
+ }
2089
+ function explicitCredential2(appName, apiKey) {
2090
+ const source = "explicit apiKey option";
2091
+ assertUsableCredential2(appName, source, apiKey);
2092
+ return sealCredential2({
2093
+ apiKey,
2094
+ tier: "argument",
2095
+ source,
2096
+ deliberate: true,
2097
+ deprecated: false,
2098
+ diskCandidates: [],
2099
+ warning: null
2100
+ });
2101
+ }
2102
+ function validateAndSealResolvedCredential2(appName, credential) {
2103
+ const apiKey = credential.apiKey;
2104
+ assertUsableCredential2(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE2, apiKey);
2105
+ if (!isSealedCredential2(credential)) {
2106
+ return sealCredential2({
2107
+ apiKey,
2108
+ tier: "argument",
2109
+ source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE2,
2110
+ deliberate: true,
2111
+ deprecated: false,
2112
+ diskCandidates: [],
2113
+ warning: null
2114
+ });
2115
+ }
2116
+ return sealCredential2({
2117
+ apiKey,
2118
+ tier: credential.tier,
2119
+ source: credential.source,
2120
+ deliberate: credential.deliberate,
2121
+ deprecated: credential.deprecated,
2122
+ diskCandidates: credential.diskCandidates,
2123
+ warning: credential.warning
2124
+ });
2125
+ }
2126
+ function firstEnvValue2(env, keys) {
2127
+ for (const key of keys) {
2128
+ const value = env[key]?.trim();
2129
+ if (value)
2130
+ return { key, value };
2131
+ }
2132
+ return null;
2133
+ }
2134
+ var DEPRECATION_REGISTRY2 = Symbol.for("hasna:contracts:credentialDeprecationNotices");
2135
+ function deprecationNotified2() {
2136
+ const host = globalThis;
2137
+ const existing = host[DEPRECATION_REGISTRY2];
2138
+ if (existing instanceof Set)
2139
+ return existing;
2140
+ const created = new Set;
2141
+ host[DEPRECATION_REGISTRY2] = created;
2142
+ return created;
2143
+ }
2144
+ function defaultDeprecationSink2(message) {
2145
+ if (typeof process !== "undefined" && process.stderr) {
2146
+ process.stderr.write(`${message}
2147
+ `);
2148
+ }
2149
+ }
2150
+ function resolveCredential2(name, env, options = {}) {
2151
+ const { apiKeyKeys } = clientTransportEnvKeys2(name);
2152
+ const diskPaths = credentialDiskSources2(name, env);
2153
+ const explicitKey = options.apiKey?.trim();
2154
+ if (explicitKey) {
2155
+ assertUsableCredential2(name, "the explicit apiKey argument", explicitKey);
2156
+ return sealCredential2({
2157
+ apiKey: explicitKey,
2158
+ tier: "argument",
2159
+ source: "explicit apiKey argument",
2160
+ deliberate: true,
2161
+ deprecated: false,
2162
+ diskCandidates: diskPaths,
2163
+ warning: null
2164
+ });
2165
+ }
2166
+ const overrideKeyName = credentialOverrideEnvKey2(name);
2167
+ const overrideRaw = env[overrideKeyName];
2168
+ if (overrideRaw !== undefined) {
2169
+ const override = overrideRaw.trim();
2170
+ if (!override) {
2171
+ throw new CredentialResolutionError2(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
2172
+ }
2173
+ assertUsableCredential2(name, overrideKeyName, override);
2174
+ return sealCredential2({
2175
+ apiKey: override,
2176
+ tier: "override",
2177
+ source: overrideKeyName,
2178
+ deliberate: true,
2179
+ deprecated: false,
2180
+ diskCandidates: diskPaths,
2181
+ warning: null
2182
+ });
2183
+ }
2184
+ const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY2]?.trim();
2185
+ if (profile) {
2186
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY2;
2187
+ if (!SAFE_PROFILE2.test(profile)) {
2188
+ throw new CredentialResolutionError2(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
2189
+ }
2190
+ const paths = profileDiskSources2(name, env, profile);
2191
+ for (const path of paths) {
2192
+ const value = readCredentialFile2(path, apiKeyKeys);
2193
+ if (value) {
2194
+ assertUsableCredential2(name, path, value);
2195
+ return sealCredential2({
2196
+ apiKey: value,
2197
+ tier: "profile",
2198
+ source: path,
2199
+ deliberate: true,
2200
+ deprecated: false,
2201
+ diskCandidates: paths,
2202
+ warning: null
2203
+ });
2204
+ }
2205
+ }
2206
+ throw new CredentialResolutionError2(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY2}.`, paths);
2207
+ }
2208
+ const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile2(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
2209
+ if (diskHits.length > 0) {
2210
+ const winner = diskHits[0];
2211
+ assertUsableCredential2(name, winner.path, winner.value);
2212
+ const divergentSources = [
2213
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
2214
+ ...(() => {
2215
+ const legacyHit = firstEnvValue2(env, apiKeyKeys);
2216
+ return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
2217
+ })()
2218
+ ];
2219
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
2220
+ return sealCredential2({
2221
+ apiKey: winner.value,
2222
+ tier: "disk",
2223
+ source: winner.path,
2224
+ deliberate: false,
2225
+ deprecated: false,
2226
+ diskCandidates: diskPaths,
2227
+ warning
2228
+ });
2229
+ }
2230
+ const legacy = firstEnvValue2(env, apiKeyKeys);
2231
+ if (legacy) {
2232
+ assertUsableCredential2(name, legacy.key, legacy.value);
2233
+ const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
2234
+ const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
2235
+ const sink = options.onDeprecation ?? defaultDeprecationSink2;
2236
+ const notified = deprecationNotified2();
2237
+ if (!notified.has(name)) {
2238
+ notified.add(name);
2239
+ sink(message);
2240
+ }
2241
+ return sealCredential2({
2242
+ apiKey: legacy.value,
2243
+ tier: "legacy-env",
2244
+ source: legacy.key,
2245
+ deliberate: false,
2246
+ deprecated: true,
2247
+ diskCandidates: diskPaths,
2248
+ warning: message
2249
+ });
2250
+ }
2251
+ return null;
2252
+ }
2253
+ var ASCII_CONTROL_PATTERN2 = /[\u0000-\u001f\u007f]/;
2254
+ var DNS_LABEL_PATTERN2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
2255
+ function isValidDnsDomain2(value) {
2256
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN2.test(value) || /[^\x00-\x7f]/.test(value)) {
2257
+ return false;
2258
+ }
2259
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN2.test(label));
2260
+ }
2261
+ function firstEnv2(env, keys, options = {}) {
2262
+ for (const key of keys) {
2263
+ const raw = env[key];
2264
+ const value = raw?.trim();
2265
+ if (value)
2266
+ return { key, value: options.preserveRaw ? raw : value };
2267
+ }
2268
+ return null;
2269
+ }
2270
+ function firstEnvDefinedKey2(env, keys) {
2271
+ for (const key of keys) {
2272
+ if (env[key] !== undefined)
2273
+ return key;
2274
+ }
2275
+ return null;
2276
+ }
2277
+ function rawAuthority2(value) {
2278
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
2279
+ if (!match)
2280
+ throw new Error("API URL must be absolute.");
2281
+ const afterScheme = value.slice(match[0].length);
2282
+ const boundary = afterScheme.search(/[/?#]/);
2283
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
2284
+ if (!authority)
2285
+ throw new Error("API URL must include a hostname.");
2286
+ return authority;
2287
+ }
2288
+ function assertCanonicalPort2(port) {
2289
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
2290
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2291
+ }
2292
+ const numericPort = Number(port);
2293
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
2294
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2295
+ }
2296
+ }
2297
+ function canonicalAuthorityHostname2(authority) {
2298
+ let rawHostname;
2299
+ if (authority.startsWith("[")) {
2300
+ const closingBracket = authority.indexOf("]");
2301
+ if (closingBracket === -1) {
2302
+ throw new Error("API URL authority must contain a canonical hostname.");
2303
+ }
2304
+ rawHostname = authority.slice(0, closingBracket + 1);
2305
+ const portSuffix = authority.slice(closingBracket + 1);
2306
+ if (portSuffix) {
2307
+ if (!portSuffix.startsWith(":")) {
2308
+ throw new Error("API URL authority must contain a canonical hostname and port.");
2309
+ }
2310
+ assertCanonicalPort2(portSuffix.slice(1));
2311
+ }
2312
+ if (isIP2(rawHostname.slice(1, -1)) !== 6) {
2313
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
2314
+ }
2315
+ } else {
2316
+ const firstColon = authority.indexOf(":");
2317
+ const lastColon = authority.lastIndexOf(":");
2318
+ if (firstColon !== lastColon) {
2319
+ throw new Error("IPv6 API URL authorities must use brackets.");
2320
+ }
2321
+ if (lastColon !== -1) {
2322
+ const port = authority.slice(lastColon + 1);
2323
+ assertCanonicalPort2(port);
2324
+ rawHostname = authority.slice(0, lastColon);
2325
+ } else {
2326
+ rawHostname = authority;
2327
+ }
2328
+ const ipVersion = isIP2(rawHostname);
2329
+ const numericAddressParts = rawHostname.split(".");
2330
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
2331
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain2(rawHostname.toLowerCase())) {
2332
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
2333
+ }
2334
+ }
2335
+ return rawHostname.toLowerCase();
2336
+ }
2337
+ function isDeliberateLoopbackHttpAuthority2(authority) {
2338
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
2339
+ }
2340
+ function toV1BaseUrl2(apiUrl) {
2341
+ if (ASCII_CONTROL_PATTERN2.test(apiUrl)) {
2342
+ throw new Error("API URL must not contain ASCII control characters.");
2343
+ }
2344
+ const input = apiUrl.trim();
2345
+ const authority = rawAuthority2(input);
2346
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
2347
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
2348
+ }
2349
+ const canonicalHostname = canonicalAuthorityHostname2(authority);
2350
+ const url = new URL(input);
2351
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2352
+ throw new Error("API URL must use http or https.");
2353
+ }
2354
+ if (url.username || url.password) {
2355
+ throw new Error("API URL must not include credentials.");
2356
+ }
2357
+ if (!url.hostname || url.hostname.endsWith(".")) {
2358
+ throw new Error("API URL must include a canonical hostname.");
2359
+ }
2360
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
2361
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
2362
+ }
2363
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
2364
+ throw new Error("API URL must not use IDN or punycode hostnames.");
2365
+ }
2366
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority2(authority)) {
2367
+ throw new Error("API URL may use http only for an exact loopback authority.");
2368
+ }
2369
+ if (url.search || url.hash) {
2370
+ throw new Error("API URL must not include a query string or fragment.");
2371
+ }
2372
+ let path = url.pathname.replace(/\/+$/, "");
2373
+ if (path.endsWith("/v1"))
2374
+ path = path.slice(0, -"/v1".length);
2375
+ url.pathname = `${path}/v1`;
2376
+ return url.toString().replace(/\/+$/, "");
2377
+ }
2378
+ function resolveClientTransport2(name, env = process.env, options = {}) {
2379
+ const keys = clientTransportEnvKeys2(name);
2380
+ const envUrlHit = firstEnv2(env, keys.apiUrlKeys, { preserveRaw: true });
2381
+ const explicitLocalKey = envUrlHit ? null : firstEnvDefinedKey2(env, keys.apiUrlKeys);
2382
+ const diskUrlHit = envUrlHit || explicitLocalKey ? null : appConfigDiskValue2(name, env, keys.apiUrlKeys);
2383
+ const urlHit = envUrlHit ?? (diskUrlHit ? { key: diskUrlHit.path, value: diskUrlHit.value } : null);
2384
+ const keyHit = firstEnv2(env, keys.apiKeyKeys);
2385
+ const warnings = [];
2386
+ if (!urlHit) {
2387
+ if (explicitLocalKey) {
2388
+ const overriddenPointer = appConfigDiskValue2(name, env, keys.apiUrlKeys);
2389
+ if (overriddenPointer) {
2390
+ warnings.push(`${explicitLocalKey} is defined but blank, which selects the local store. ` + `The server URL in ${overriddenPointer.path} was NOT selected: an explicit blank wins over a disk pointer.`);
2391
+ }
2392
+ return {
2393
+ transport: "sqlite",
2394
+ transportSource: explicitLocalKey,
2395
+ baseUrl: null,
2396
+ apiUrlSource: null,
2397
+ apiKeyPresent: Boolean(keyHit),
2398
+ apiKeySource: keyHit ? keyHit.key : null,
2399
+ apiKeyTier: null,
2400
+ misconfigured: false,
2401
+ warning: warnings.length > 0 ? warnings.join(" ") : null
2402
+ };
2403
+ }
2404
+ return {
2405
+ transport: "sqlite",
2406
+ transportSource: "default",
2407
+ baseUrl: null,
2408
+ apiUrlSource: null,
2409
+ apiKeyPresent: Boolean(keyHit),
2410
+ apiKeySource: keyHit ? keyHit.key : null,
2411
+ apiKeyTier: null,
2412
+ misconfigured: false,
2413
+ warning: null
2414
+ };
2415
+ }
2416
+ if (diskUrlHit) {
2417
+ warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${diskUrlHit.path} was used, so this client connects to the server. ` + `Unset the pointer or remove the file to stay on the local store.`);
2418
+ }
2419
+ const credential = resolveCredential2(name, env, options.credentials);
2420
+ if (!credential) {
2421
+ const diskHint = credentialDiskSourcesForMessage2(name, env);
2422
+ warnings.push(`${urlHit.key} selects the HTTP server for '${name}', but no API key could be resolved; ` + `refusing to route and leaving the local sqlite store selected. ` + `Looked for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
2423
+ return {
2424
+ transport: "sqlite",
2425
+ transportSource: urlHit.key,
2426
+ baseUrl: null,
2427
+ apiUrlSource: urlHit.key,
2428
+ apiKeyPresent: false,
2429
+ apiKeySource: null,
2430
+ apiKeyTier: null,
2431
+ misconfigured: true,
2432
+ warning: warnings.join(" ")
2433
+ };
2434
+ }
2435
+ if (credential.warning)
2436
+ warnings.push(credential.warning);
2437
+ const apiUrlSource = urlHit.key;
2438
+ let baseUrl;
2439
+ try {
2440
+ baseUrl = toV1BaseUrl2(urlHit.value);
2441
+ } catch (error) {
2442
+ const message = error instanceof Error ? error.message : String(error);
2443
+ warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
2444
+ return {
2445
+ transport: "sqlite",
2446
+ transportSource: urlHit.key,
2447
+ baseUrl: null,
2448
+ apiUrlSource: urlHit.key,
2449
+ apiKeyPresent: true,
2450
+ apiKeySource: credential.source,
2451
+ apiKeyTier: credential.tier,
2452
+ misconfigured: true,
2453
+ warning: warnings.join(" ")
2454
+ };
2455
+ }
2456
+ return {
2457
+ transport: "http",
2458
+ transportSource: urlHit.key,
2459
+ baseUrl,
2460
+ apiUrlSource,
2461
+ apiKeyPresent: true,
2462
+ apiKeySource: credential.source,
2463
+ apiKeyTier: credential.tier,
2464
+ misconfigured: false,
2465
+ warning: warnings.length > 0 ? warnings.join(" ") : null
2466
+ };
2467
+ }
2468
+ function credentialDiskSourcesForMessage2(name, env) {
2469
+ const paths = credentialDiskSources2(name, env);
2470
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME set in this environment, so no credential file was consulted>";
2471
+ }
2472
+
2473
+ class HasnaHttpError2 extends Error {
2474
+ status;
2475
+ method;
2476
+ path;
2477
+ body;
2478
+ credentialSource;
2479
+ credentialTier;
2480
+ constructor(method, path, status, body, credential) {
2481
+ const guidance = credential ? `. ${credential.guidance}` : "";
2482
+ super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
2483
+ this.name = "HasnaHttpError";
2484
+ this.status = status;
2485
+ this.method = method;
2486
+ this.path = path;
2487
+ this.body = body;
2488
+ this.credentialSource = credential?.source ?? null;
2489
+ this.credentialTier = credential?.tier ?? null;
2490
+ }
2491
+ }
2492
+ function currentCredential2(name, apiKey) {
2493
+ if (typeof apiKey === "function") {
2494
+ return validateAndSealResolvedCredential2(name, apiKey());
2495
+ }
2496
+ return explicitCredential2(name, apiKey);
2497
+ }
2498
+ function authFailureGuidance2(credential) {
2499
+ const origin = `The API key for this request came from ${credential.source}`;
2500
+ if (credential.deliberate) {
2501
+ const remedy = credential.source === CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE2 ? `Fix that provider so it returns the current key, or replace it with resolveCredential() ` + `so diagnostics can name the original source.` : `Rotate that key, or unset the override to use the credential on disk.`;
2502
+ return `${origin} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: ` + `falling back here would authenticate as a different principal than the one you named, which is ` + `exactly the failure an override exists to prevent. ${remedy}`;
2503
+ }
2504
+ if (credential.deprecated) {
2505
+ const target = credential.diskCandidates[0];
2506
+ const remedy = target ? `Write the CURRENT key to ${target} \u2014 that file is re-read on every call, so rotations take ` + `effect immediately and in every shell. Do not simply unset ${credential.source}: nothing was ` + `found on disk, so that would leave this client with no credential at all.` : `This environment has no HOME, so no credential file could be consulted; the disk tier is ` + `unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.`;
2507
+ return `${origin}, a variable in this process's environment \u2014 which is a snapshot taken when the process ` + `started. A STALE SHELL is the most common cause of this error: this shell exported the key before ` + `it was rotated, and will keep sending the old one until it exits. ${remedy}`;
2508
+ }
2509
+ return `${origin}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. ` + `The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution ` + `so this machine gets the current key.`;
2510
+ }
2511
+ var DEFAULT_RETRY_STATUSES2 = [408, 425, 429, 500, 502, 503, 504];
2512
+ var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
2513
+ var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
2514
+ "host",
2515
+ ":authority",
2516
+ "forwarded",
2517
+ "x-forwarded-host",
2518
+ "x-original-host"
2519
+ ]);
2520
+ function assertNoAuthorityOverrideHeaders2(headers, source) {
2521
+ if (!headers)
2522
+ return;
2523
+ const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS2.has(name.trim().toLowerCase()));
2524
+ if (forbidden) {
2525
+ throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
2526
+ }
2527
+ }
2528
+ function appendQuery2(path, query) {
2529
+ if (!query)
2530
+ return path;
2531
+ const params = query instanceof URLSearchParams ? query : new URLSearchParams;
2532
+ if (!(query instanceof URLSearchParams)) {
2533
+ for (const [key, value] of Object.entries(query)) {
2534
+ if (value === null || value === undefined)
2535
+ continue;
2536
+ if (Array.isArray(value)) {
2537
+ for (const v of value)
2538
+ params.append(key, String(v));
2539
+ } else {
2540
+ params.append(key, String(value));
2541
+ }
2542
+ }
2543
+ }
2544
+ const qs = params.toString();
2545
+ if (!qs)
2546
+ return path;
2547
+ return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
2548
+ }
2549
+ var defaultSleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
2550
+ function createHasnaHttpTransport2(options) {
2551
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
2552
+ const base = toV1BaseUrl2(options.baseUrl);
2553
+ const timeoutMs = options.timeoutMs ?? 30000;
2554
+ const sleep = options.sleepImpl ?? defaultSleep2;
2555
+ const defaultRetry = options.retry;
2556
+ function resolveRetry(callRetry) {
2557
+ const chosen = callRetry !== undefined ? callRetry : defaultRetry;
2558
+ if (chosen === false)
2559
+ return null;
2560
+ const r = chosen ?? {};
2561
+ return {
2562
+ retries: r.retries ?? 2,
2563
+ baseDelayMs: r.baseDelayMs ?? 200,
2564
+ maxDelayMs: r.maxDelayMs ?? 2000,
2565
+ retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES2]
2566
+ };
2567
+ }
2568
+ async function once(method, rel, url, body, opts, credential) {
2569
+ assertNoAuthorityOverrideHeaders2(options.headers, "transport");
2570
+ assertNoAuthorityOverrideHeaders2(opts.headers, "request");
2571
+ const headers = {
2572
+ "x-api-key": credential.apiKey,
2573
+ Authorization: `Bearer ${credential.apiKey}`,
2574
+ Accept: "application/json",
2575
+ ...options.headers ?? {},
2576
+ ...opts.headers ?? {}
2577
+ };
2578
+ if (opts.idempotencyKey)
2579
+ headers["Idempotency-Key"] = opts.idempotencyKey;
2580
+ const init = {
2581
+ method,
2582
+ headers,
2583
+ redirect: "manual"
2584
+ };
2585
+ if (body !== undefined) {
2586
+ headers["Content-Type"] = "application/json";
2587
+ init.body = JSON.stringify(body);
2588
+ }
2589
+ const controller = new AbortController;
2590
+ const onAbort = () => controller.abort();
2591
+ if (opts.signal) {
2592
+ if (opts.signal.aborted)
2593
+ controller.abort();
2594
+ else
2595
+ opts.signal.addEventListener("abort", onAbort, { once: true });
2596
+ }
2597
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
2598
+ init.signal = controller.signal;
2599
+ let response;
2600
+ try {
2601
+ response = await fetchImpl(url, init);
2602
+ } catch (error) {
2603
+ const err = error instanceof Error ? error : new Error(String(error));
2604
+ if (opts.signal?.aborted)
2605
+ return { ok: false, retryable: false, error: err };
2606
+ return { ok: false, retryable: true, error: err };
2607
+ } finally {
2608
+ clearTimeout(timer);
2609
+ if (opts.signal)
2610
+ opts.signal.removeEventListener("abort", onAbort);
2611
+ }
2612
+ const text = await response.text();
2613
+ let parsed = undefined;
2614
+ if (text.length > 0) {
2615
+ try {
2616
+ parsed = JSON.parse(text);
2617
+ } catch {
2618
+ parsed = text;
2619
+ }
2620
+ }
2621
+ if (!response.ok) {
2622
+ if (response.status >= 300 && response.status < 400) {
2623
+ return {
2624
+ ok: false,
2625
+ retryable: false,
2626
+ error: new HasnaHttpError2(method, rel, response.status, parsed)
2627
+ };
2628
+ }
2629
+ if (response.status === 401 || response.status === 403) {
2630
+ return {
2631
+ ok: false,
2632
+ retryable: false,
2633
+ error: new HasnaHttpError2(method, rel, response.status, parsed, {
2634
+ source: credential.source,
2635
+ tier: credential.tier,
2636
+ guidance: authFailureGuidance2(credential)
2637
+ })
2638
+ };
2639
+ }
2640
+ const retry = resolveRetry(opts.retry);
2641
+ const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
2642
+ return { ok: false, retryable, error: new HasnaHttpError2(method, rel, response.status, parsed) };
2643
+ }
2644
+ return { ok: true, value: parsed };
2645
+ }
2646
+ async function request(method, path, body, opts = {}) {
2647
+ const upper = method.toUpperCase();
2648
+ const rel = appendQuery2(path.startsWith("/") ? path : `/${path}`, opts.query);
2649
+ const url = `${base}${rel}`;
2650
+ const retry = resolveRetry(opts.retry);
2651
+ const methodRetryable = IDEMPOTENT_METHODS2.has(upper) || Boolean(opts.idempotencyKey);
2652
+ const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
2653
+ const credential = currentCredential2(options.name, options.apiKey);
2654
+ let last = null;
2655
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2656
+ const result = await once(upper, rel, url, body, opts, credential);
2657
+ if (result.ok)
2658
+ return result.value;
2659
+ last = result;
2660
+ const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
2661
+ if (!canRetry)
2662
+ break;
2663
+ const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
2664
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
2665
+ await sleep(backoff + jitter);
2666
+ }
2667
+ throw last.error;
2668
+ }
2669
+ return {
2670
+ baseUrl: base,
2671
+ request,
2672
+ get: (path, opts) => request("GET", path, undefined, opts),
2673
+ post: (path, body, opts) => request("POST", path, body, opts),
2674
+ put: (path, body, opts) => request("PUT", path, body, opts),
2675
+ patch: (path, body, opts) => request("PATCH", path, body, opts),
2676
+ del: (path, body, opts) => request("DELETE", path, body, opts)
2677
+ };
2678
+ }
2679
+ function createClientTransport2(name, env = process.env, overrides) {
2680
+ const credentialOptions = overrides?.credentials;
2681
+ const resolution = resolveClientTransport2(name, env, { ...credentialOptions ? { credentials: credentialOptions } : {} });
2682
+ if (resolution.misconfigured) {
2683
+ throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the API client.`);
2684
+ }
2685
+ if (resolution.transport === "sqlite" || !resolution.baseUrl) {
2686
+ return { transport: "sqlite", client: null, resolution };
2687
+ }
2688
+ const credentialProvider = () => {
2689
+ const resolved = resolveCredential2(name, env, credentialOptions);
2690
+ if (!resolved) {
2691
+ throw new Error(`Client for '${name}' resolved to the http transport but no API key is available any more. ` + `Looked at ${credentialDiskSourcesForMessage2(name, env)}, then the environment. ` + `A credential file that was removed after this client was built is the usual cause.`);
2692
+ }
2693
+ return resolved;
2694
+ };
2695
+ return {
2696
+ transport: "http",
2697
+ client: createHasnaHttpTransport2({
2698
+ name,
2699
+ baseUrl: resolution.baseUrl,
2700
+ apiKey: credentialProvider,
2701
+ ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
2702
+ ...overrides?.headers ? { headers: overrides.headers } : {},
2703
+ ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
2704
+ ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
2705
+ ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
2706
+ }),
2707
+ resolution
2708
+ };
2709
+ }
2710
+ function resourcePath(resource) {
2711
+ const trimmed = resource.replace(/^\/+|\/+$/g, "");
2712
+ if (!trimmed)
2713
+ throw new Error("resource must be a non-empty path segment");
2714
+ return `/${trimmed}`;
2715
+ }
2716
+ function entityPath(resource, id) {
2717
+ if (id === undefined || id === null || `${id}`.length === 0) {
2718
+ throw new Error("id must be a non-empty string");
2719
+ }
2720
+ return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
2721
+ }
2722
+ function newIdempotencyKey() {
2723
+ const g = globalThis;
2724
+ if (g.crypto?.randomUUID)
2725
+ return g.crypto.randomUUID();
2726
+ return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
2727
+ }
2728
+ function extractItems(raw) {
2729
+ if (Array.isArray(raw))
2730
+ return raw;
2731
+ if (raw && typeof raw === "object") {
2732
+ const obj = raw;
2733
+ for (const key of ["items", "data", "results", "rows", "records"]) {
2734
+ if (Array.isArray(obj[key]))
2735
+ return obj[key];
2736
+ }
2737
+ }
2738
+ return [];
2739
+ }
2740
+ function extractTotal(raw) {
2741
+ if (raw && typeof raw === "object") {
2742
+ const obj = raw;
2743
+ for (const key of ["total", "count", "totalCount", "total_count"]) {
2744
+ if (typeof obj[key] === "number")
2745
+ return obj[key];
2746
+ }
2747
+ }
2748
+ return null;
2749
+ }
2750
+ function extractCursor(raw) {
2751
+ if (raw && typeof raw === "object") {
2752
+ const obj = raw;
2753
+ for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
2754
+ if (typeof obj[key] === "string")
2755
+ return obj[key];
2756
+ }
2757
+ }
2758
+ return null;
2759
+ }
2760
+ function isNotFoundHttpError(error) {
2761
+ return typeof error === "object" && error !== null && error.name === "HasnaHttpError" && error.status === 404;
2762
+ }
2763
+ function createHasnaStorageClient(name, transport) {
2764
+ return {
2765
+ name,
2766
+ baseUrl: transport.baseUrl,
2767
+ transport,
2768
+ async list(resource, options = {}) {
2769
+ const raw = await transport.get(resourcePath(resource), options);
2770
+ return {
2771
+ items: extractItems(raw),
2772
+ total: extractTotal(raw),
2773
+ cursor: extractCursor(raw),
2774
+ raw
2775
+ };
2776
+ },
2777
+ async get(resource, id, options = {}) {
2778
+ try {
2779
+ return await transport.get(entityPath(resource, id), options);
2780
+ } catch (error) {
2781
+ if (isNotFoundHttpError(error))
2782
+ return null;
2783
+ throw error;
2784
+ }
2785
+ },
2786
+ async create(resource, body, options = {}) {
2787
+ const { idempotencyKey, ...rest } = options;
2788
+ return transport.post(resourcePath(resource), body, {
2789
+ ...rest,
2790
+ idempotencyKey: idempotencyKey ?? newIdempotencyKey()
2791
+ });
2792
+ },
2793
+ async update(resource, id, patch, options = {}) {
2794
+ const { method = "PATCH", idempotencyKey, ...rest } = options;
2795
+ const call = method === "PUT" ? transport.put : transport.patch;
2796
+ return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
2797
+ },
2798
+ async delete(resource, id, options = {}) {
2799
+ try {
2800
+ await transport.del(entityPath(resource, id), undefined, options);
2801
+ } catch (error) {
2802
+ if (isNotFoundHttpError(error))
2803
+ return;
2804
+ throw error;
2805
+ }
2806
+ }
2807
+ };
2808
+ }
2809
+ function resolveStorageClient(name, env = process.env, overrides) {
2810
+ const wired = createClientTransport2(name, env, overrides);
2811
+ if (wired.transport === "http") {
2812
+ return { transport: "http", client: createHasnaStorageClient(name, wired.client) };
2813
+ }
2814
+ return { transport: "sqlite", client: null };
2815
+ }
2816
+
1141
2817
  // src/http/client.ts
1142
- function envToken(name) {
2818
+ function envToken3(name) {
1143
2819
  return name.toUpperCase().replace(/-/g, "_");
1144
2820
  }
1145
2821
  function envKeys(name) {
1146
- const token = envToken(name);
2822
+ const token = envToken3(name);
1147
2823
  return {
1148
2824
  storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
1149
2825
  apiUrlKeys: [`HASNA_${token}_API_URL`],
@@ -1158,7 +2834,7 @@ function normalizeClientStore(value) {
1158
2834
  return "http";
1159
2835
  throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
1160
2836
  }
1161
- function firstEnv(env, keys) {
2837
+ function firstEnv3(env, keys) {
1162
2838
  for (const key of keys) {
1163
2839
  const value = env[key]?.trim();
1164
2840
  if (value)
@@ -1166,7 +2842,7 @@ function firstEnv(env, keys) {
1166
2842
  }
1167
2843
  return null;
1168
2844
  }
1169
- function toV1BaseUrl(apiUrl) {
2845
+ function toV1BaseUrl3(apiUrl) {
1170
2846
  const url = new URL(apiUrl);
1171
2847
  if (url.protocol !== "http:" && url.protocol !== "https:") {
1172
2848
  throw new Error("API URL must use http or https.");
@@ -1181,9 +2857,9 @@ function toV1BaseUrl(apiUrl) {
1181
2857
  }
1182
2858
  function resolveTransport(name, env = process.env) {
1183
2859
  const keys = envKeys(name);
1184
- const storeHit = firstEnv(env, keys.storeKeys);
1185
- const urlHit = firstEnv(env, keys.apiUrlKeys);
1186
- const keyHit = firstEnv(env, keys.apiKeyKeys);
2860
+ const storeHit = firstEnv3(env, keys.storeKeys);
2861
+ const urlHit = firstEnv3(env, keys.apiUrlKeys);
2862
+ const keyHit = firstEnv3(env, keys.apiKeyKeys);
1187
2863
  let requested = "sqlite";
1188
2864
  let modeSource = "default";
1189
2865
  if (storeHit) {
@@ -1233,7 +2909,7 @@ function resolveTransport(name, env = process.env) {
1233
2909
  const rawUrl = urlHit.value;
1234
2910
  let baseUrl;
1235
2911
  try {
1236
- baseUrl = toV1BaseUrl(rawUrl);
2912
+ baseUrl = toV1BaseUrl3(rawUrl);
1237
2913
  } catch (error) {
1238
2914
  const message = error instanceof Error ? error.message : String(error);
1239
2915
  return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
@@ -1241,7 +2917,7 @@ function resolveTransport(name, env = process.env) {
1241
2917
  return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
1242
2918
  }
1243
2919
 
1244
- class HasnaHttpError extends Error {
2920
+ class HasnaHttpError3 extends Error {
1245
2921
  status;
1246
2922
  method;
1247
2923
  path;
@@ -1255,7 +2931,7 @@ class HasnaHttpError extends Error {
1255
2931
  this.body = body;
1256
2932
  }
1257
2933
  }
1258
- function appendQuery(path, query) {
2934
+ function appendQuery3(path, query) {
1259
2935
  if (!query)
1260
2936
  return path;
1261
2937
  const params = new URLSearchParams;
@@ -1273,12 +2949,12 @@ function appendQuery(path, query) {
1273
2949
  }
1274
2950
  var RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
1275
2951
  var IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
1276
- var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
2952
+ var defaultSleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1277
2953
  function createHttpTransport(options) {
1278
2954
  const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
1279
2955
  const base = options.baseUrl.replace(/\/+$/, "");
1280
2956
  const timeoutMs = options.timeoutMs ?? 30000;
1281
- const sleep = options.sleepImpl ?? defaultSleep;
2957
+ const sleep = options.sleepImpl ?? defaultSleep3;
1282
2958
  async function once(method, rel, url, body, opts) {
1283
2959
  const headers = {
1284
2960
  "x-api-key": options.apiKey,
@@ -1326,13 +3002,13 @@ function createHttpTransport(options) {
1326
3002
  }
1327
3003
  }
1328
3004
  if (!response.ok) {
1329
- return { ok: false, retryable: RETRY_STATUSES.has(response.status), error: new HasnaHttpError(method, rel, response.status, parsed) };
3005
+ return { ok: false, retryable: RETRY_STATUSES.has(response.status), error: new HasnaHttpError3(method, rel, response.status, parsed) };
1330
3006
  }
1331
3007
  return { ok: true, value: parsed };
1332
3008
  }
1333
3009
  async function request(method, path, body, opts = {}) {
1334
3010
  const upper = method.toUpperCase();
1335
- const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
3011
+ const rel = appendQuery3(path.startsWith("/") ? path : `/${path}`, opts.query);
1336
3012
  const url = `${base}${rel}`;
1337
3013
  const methodRetryable = IDEMPOTENT.has(upper) || Boolean(opts.idempotencyKey);
1338
3014
  const maxRetries = opts.retries ?? 2;
@@ -1364,13 +3040,13 @@ function createHttpTransport(options) {
1364
3040
  del: (path, body, opts) => request("DELETE", path, body, opts)
1365
3041
  };
1366
3042
  }
1367
- function newIdempotencyKey() {
3043
+ function newIdempotencyKey2() {
1368
3044
  const g = globalThis;
1369
3045
  if (g.crypto?.randomUUID)
1370
3046
  return g.crypto.randomUUID();
1371
3047
  return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
1372
3048
  }
1373
- function extractItems(raw, extraKeys = []) {
3049
+ function extractItems2(raw, extraKeys = []) {
1374
3050
  if (Array.isArray(raw))
1375
3051
  return raw;
1376
3052
  if (raw && typeof raw === "object") {
@@ -1391,19 +3067,19 @@ function createStorageClient(name, transport) {
1391
3067
  transport,
1392
3068
  async list(resource, query) {
1393
3069
  const raw = await transport.get(rp(resource), { query });
1394
- return { items: extractItems(raw, [resource]), raw };
3070
+ return { items: extractItems2(raw, [resource]), raw };
1395
3071
  },
1396
3072
  async get(resource, id) {
1397
3073
  try {
1398
3074
  return await transport.get(ep(resource, id));
1399
3075
  } catch (error) {
1400
- if (error instanceof HasnaHttpError && error.status === 404)
3076
+ if (error instanceof HasnaHttpError3 && error.status === 404)
1401
3077
  return null;
1402
3078
  throw error;
1403
3079
  }
1404
3080
  },
1405
3081
  async create(resource, body, idempotencyKey) {
1406
- return transport.post(rp(resource), body, { idempotencyKey: idempotencyKey ?? newIdempotencyKey() });
3082
+ return transport.post(rp(resource), body, { idempotencyKey: idempotencyKey ?? newIdempotencyKey2() });
1407
3083
  },
1408
3084
  async update(resource, id, patch, method = "PATCH") {
1409
3085
  const call = method === "PUT" ? transport.put : transport.patch;
@@ -1413,27 +3089,42 @@ function createStorageClient(name, transport) {
1413
3089
  try {
1414
3090
  await transport.del(ep(resource, id));
1415
3091
  } catch (error) {
1416
- if (error instanceof HasnaHttpError && error.status === 404)
3092
+ if (error instanceof HasnaHttpError3 && error.status === 404)
1417
3093
  return;
1418
3094
  throw error;
1419
3095
  }
1420
3096
  }
1421
3097
  };
1422
3098
  }
1423
- function resolveStorageClient(name, env = process.env, fetchImpl) {
3099
+ function resolveStoreClient(name, env = process.env) {
1424
3100
  const resolution = resolveTransport(name, env);
1425
3101
  if (resolution.misconfigured) {
3102
+ const wired2 = createClientTransport(name, env);
3103
+ if (wired2.transport === "http") {
3104
+ return {
3105
+ transport: "http",
3106
+ client: createHasnaStorageClient(name, wired2.client),
3107
+ resolution: {
3108
+ transport: "http",
3109
+ requested: "http",
3110
+ modeSource: resolution.modeSource === "default" ? "auto:api-url+seam-credential" : resolution.modeSource,
3111
+ baseUrl: wired2.resolution.baseUrl,
3112
+ apiKeyPresent: true,
3113
+ misconfigured: false,
3114
+ warning: null
3115
+ }
3116
+ };
3117
+ }
1426
3118
  throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
1427
3119
  }
1428
3120
  if (resolution.transport === "sqlite" || !resolution.baseUrl) {
1429
3121
  return { transport: "sqlite", client: null, resolution };
1430
3122
  }
1431
- const keys = envKeys(name);
1432
- const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
1433
- if (!apiKey)
3123
+ const wired = createClientTransport(name, env);
3124
+ if (wired.transport !== "http") {
1434
3125
  throw new Error(`Client for '${name}' resolved to the /v1 API without an API key.`);
1435
- const transport = createHttpTransport({ name, baseUrl: resolution.baseUrl, apiKey, ...fetchImpl ? { fetchImpl } : {} });
1436
- return { transport: "http", client: createStorageClient(name, transport), resolution };
3126
+ }
3127
+ return { transport: "http", client: createHasnaStorageClient(name, wired.client), resolution };
1437
3128
  }
1438
3129
 
1439
3130
  // src/store.ts
@@ -1516,6 +3207,22 @@ var localStore = {
1516
3207
  await withLocalStoreReaderLease(() => saveFeedback(input));
1517
3208
  }
1518
3209
  };
3210
+ async function listResource(client, resource, query) {
3211
+ const raw = await client.transport.get(`/${resource}`, query ? { query } : undefined);
3212
+ return { items: extractEnvelopeItems(raw, resource), raw };
3213
+ }
3214
+ function extractEnvelopeItems(raw, resource) {
3215
+ if (Array.isArray(raw))
3216
+ return raw;
3217
+ if (raw && typeof raw === "object") {
3218
+ const obj = raw;
3219
+ for (const key of [resource, "items", "data", "results", "rows", "records"]) {
3220
+ if (Array.isArray(obj[key]))
3221
+ return obj[key];
3222
+ }
3223
+ }
3224
+ return [];
3225
+ }
1519
3226
  function apiStore(client) {
1520
3227
  return {
1521
3228
  mode: "http",
@@ -1523,7 +3230,7 @@ function apiStore(client) {
1523
3230
  async createRecording(input, idempotencyKey) {
1524
3231
  const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID2() : idempotencyKey;
1525
3232
  const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
1526
- const res = await client.create("recordings", identity.input, identity.idempotencyKey);
3233
+ const res = await client.create("recordings", identity.input, { idempotencyKey: identity.idempotencyKey });
1527
3234
  return unwrap(res, "recording");
1528
3235
  },
1529
3236
  async getRecording(id) {
@@ -1531,7 +3238,7 @@ function apiStore(client) {
1531
3238
  return res ? unwrap(res, "recording") : null;
1532
3239
  },
1533
3240
  async listRecordings(filter) {
1534
- const { items } = await client.list("recordings", listQuery(filter));
3241
+ const { items } = await listResource(client, "recordings", listQuery(filter));
1535
3242
  return items;
1536
3243
  },
1537
3244
  async countRecordings(filter) {
@@ -1542,7 +3249,7 @@ function apiStore(client) {
1542
3249
  const seenPageKeys = new Set;
1543
3250
  while (pageRequests < maxPageRequests) {
1544
3251
  pageRequests += 1;
1545
- const { items, raw } = await client.list("recordings", {
3252
+ const { items, raw } = await listResource(client, "recordings", {
1546
3253
  ...listQuery(filter),
1547
3254
  limit: pageLimit,
1548
3255
  offset
@@ -1565,7 +3272,7 @@ function apiStore(client) {
1565
3272
  throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
1566
3273
  },
1567
3274
  async searchRecordings(query, filter) {
1568
- const { items } = await client.list("recordings", listQuery({ ...filter ?? {}, search: query }));
3275
+ const { items } = await listResource(client, "recordings", listQuery({ ...filter ?? {}, search: query }));
1569
3276
  return items;
1570
3277
  },
1571
3278
  async deleteRecording(id) {
@@ -1597,7 +3304,7 @@ function apiStore(client) {
1597
3304
  return res ? unwrap(res, "agent") : null;
1598
3305
  },
1599
3306
  async listAgents() {
1600
- const { items } = await client.list("agents");
3307
+ const { items } = await listResource(client, "agents");
1601
3308
  return items;
1602
3309
  },
1603
3310
  async heartbeatAgent(idOrName) {
@@ -1637,7 +3344,7 @@ function apiStore(client) {
1637
3344
  return res ? unwrap(res, "project") : null;
1638
3345
  },
1639
3346
  async listProjects() {
1640
- const { items } = await client.list("projects");
3347
+ const { items } = await listResource(client, "projects");
1641
3348
  return items;
1642
3349
  },
1643
3350
  async saveFeedback(input) {
@@ -1660,7 +3367,7 @@ var cached = null;
1660
3367
  function getStore(env = process.env) {
1661
3368
  if (env === process.env && cached)
1662
3369
  return cached;
1663
- const resolved = resolveStorageClient(APP, env);
3370
+ const resolved = resolveStoreClient(APP, env);
1664
3371
  const store = resolved.transport === "http" ? apiStore(resolved.client) : localStore;
1665
3372
  if (env === process.env)
1666
3373
  cached = store;
@@ -1698,13 +3405,13 @@ class EnhancementError extends Error {
1698
3405
  }
1699
3406
  }
1700
3407
  export {
1701
- toV1BaseUrl,
3408
+ toV1BaseUrl3 as toV1BaseUrl,
1702
3409
  resolveTransport,
1703
3410
  resolveStorageClient,
1704
3411
  getStore,
1705
3412
  createStorageClient,
1706
3413
  createHttpTransport,
1707
3414
  __resetStore,
1708
- HasnaHttpError,
3415
+ HasnaHttpError3 as HasnaHttpError,
1709
3416
  APP
1710
3417
  };