@hasna/skills 0.2.0 → 0.3.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 +100 -36
- package/bin/index.js +2151 -650
- package/bin/mcp.js +1787 -625
- package/bin/migrate.js +5 -0
- package/bin/server.js +365 -198
- package/bin/worker.js +346 -179
- package/dist/cli/cli.test-utils.d.ts +14 -0
- package/dist/cli/commands/publish.d.ts +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1100 -152
- package/dist/lib/api-url.d.ts +32 -23
- package/dist/lib/auth-store.d.ts +111 -42
- package/dist/lib/config.d.ts +19 -21
- package/dist/lib/feedback.d.ts +8 -3
- package/dist/lib/fleet-credentials.d.ts +246 -0
- package/dist/lib/portable-skills-files.d.ts +9 -0
- package/dist/lib/portable-skills.d.ts +2 -2
- package/dist/lib/remote-client.d.ts +20 -10
- package/dist/lib/remote-registry.d.ts +31 -11
- package/dist/lib/run-routing.d.ts +6 -4
- package/dist/lib/vendor-host-policy.d.ts +31 -0
- package/dist/sdk/index.d.ts +6 -0
- package/dist/sdk/index.js +1738 -572
- package/dist/server/artifact-storage.d.ts +11 -0
- package/dist/server/skills-api.d.ts +13 -2
- package/dist/storage.js +9 -14
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -57,7 +57,12 @@ import { join as join2, dirname } from "path";
|
|
|
57
57
|
// src/lib/retired-settings.ts
|
|
58
58
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
59
59
|
var RETIRED_CONFIG_KEYS = {
|
|
60
|
-
mode: "
|
|
60
|
+
mode: "a configured API origin",
|
|
61
|
+
apiUrl: "skills setup --api-url <origin>"
|
|
62
|
+
};
|
|
63
|
+
var RETIRED_CONFIG_KEY_REASONS = {
|
|
64
|
+
mode: "Deployment modes were removed: a Skills client either resolves a credential " + "or it does not, and that is the whole of it.",
|
|
65
|
+
apiUrl: "The service address is no longer kept in this app's config file: it is read from " + "HASNA_SKILLS_API_URL, then the macOS Keychain item hasna.credentials.skills.api-url, " + "then ~/.hasna/skills/config/credentials, then the fleet gateway."
|
|
61
66
|
};
|
|
62
67
|
|
|
63
68
|
class RetiredSettingError extends Error {
|
|
@@ -96,7 +101,7 @@ function assertNoRetiredConfigKeys(config, source) {
|
|
|
96
101
|
for (const [key, replacement] of Object.entries(RETIRED_CONFIG_KEYS)) {
|
|
97
102
|
if (!(key in config))
|
|
98
103
|
continue;
|
|
99
|
-
throw new RetiredSettingError(key, `${source}: "${key}" is no longer a configuration key. ` +
|
|
104
|
+
throw new RetiredSettingError(key, `${source}: "${key}" is no longer a configuration key. ` + `${RETIRED_CONFIG_KEY_REASONS[key] ?? ""} ` + `Use ${replacement} instead, and remove the old key with: skills config unset ${key}. ` + "Refused rather than ignored, because silently dropping it would leave an " + "operator believing they had pointed this install at a server.");
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
|
|
@@ -224,7 +229,7 @@ var ENUM_KEYS = {
|
|
|
224
229
|
defaultScope: ["global", "project"],
|
|
225
230
|
format: ["compact", "json", "csv"]
|
|
226
231
|
};
|
|
227
|
-
var STRING_KEYS = ["
|
|
232
|
+
var STRING_KEYS = ["extensionsDir"];
|
|
228
233
|
function validKeys() {
|
|
229
234
|
return [...Object.keys(ENUM_KEYS), ...STRING_KEYS];
|
|
230
235
|
}
|
|
@@ -255,16 +260,6 @@ function normalizeConfigValue(key, value) {
|
|
|
255
260
|
const allowed = allowedValues(key);
|
|
256
261
|
if (allowed)
|
|
257
262
|
return allowed.includes(value) ? value : undefined;
|
|
258
|
-
if (key === "apiUrl") {
|
|
259
|
-
try {
|
|
260
|
-
const url = new URL(value);
|
|
261
|
-
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
262
|
-
return;
|
|
263
|
-
return value.replace(/\/+$/, "");
|
|
264
|
-
} catch {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
263
|
if (key === "extensionsDir")
|
|
269
264
|
return value.trim() ? value : undefined;
|
|
270
265
|
return;
|
|
@@ -334,7 +329,7 @@ function saveConfig(key, value, scope = "project") {
|
|
|
334
329
|
const normalized = normalizeConfigValue(key, value);
|
|
335
330
|
if (normalized === undefined) {
|
|
336
331
|
const allowed = allowedValues(key);
|
|
337
|
-
throw new Error(allowed ? `Invalid value '${value}' for ${key}. Allowed: ${allowed.join(", ")}` : `Invalid value '${value}' for ${key}. Expected
|
|
332
|
+
throw new Error(allowed ? `Invalid value '${value}' for ${key}. Allowed: ${allowed.join(", ")}` : `Invalid value '${value}' for ${key}. Expected a non-empty path`);
|
|
338
333
|
}
|
|
339
334
|
const filePath = getConfigPath(scope);
|
|
340
335
|
let existing = {};
|
|
@@ -1842,7 +1837,7 @@ function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)
|
|
|
1842
1837
|
const pkg = existsSync6(pkgPath) ? readJsonObject(pkgPath) : undefined;
|
|
1843
1838
|
const name = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
|
|
1844
1839
|
const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
|
|
1845
|
-
const version =
|
|
1840
|
+
const version = readDeclaredSkillVersion(skillPath) ?? PORTABLE_SKILL_DEFAULT_VERSION;
|
|
1846
1841
|
const kind = parseSkillKind(stringField(jsonManifest, "kind") ?? frontmatter?.kind);
|
|
1847
1842
|
const commands = parseManifestCommands(jsonManifest) ?? (kind === "instruction" ? [] : inferPackageCommands(pkg, name)) ?? [];
|
|
1848
1843
|
return {
|
|
@@ -1866,6 +1861,15 @@ function parseSkillKind(value) {
|
|
|
1866
1861
|
return value;
|
|
1867
1862
|
return;
|
|
1868
1863
|
}
|
|
1864
|
+
function readDeclaredSkillVersion(skillPath) {
|
|
1865
|
+
const skillJsonPath = join6(skillPath, "skill.json");
|
|
1866
|
+
const skillMdPath = join6(skillPath, "SKILL.md");
|
|
1867
|
+
const pkgPath = join6(skillPath, "package.json");
|
|
1868
|
+
const jsonManifest = existsSync6(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
|
|
1869
|
+
const frontmatter = existsSync6(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
|
|
1870
|
+
const pkg = existsSync6(pkgPath) ? readJsonObject(pkgPath) : undefined;
|
|
1871
|
+
return stringField(jsonManifest, "version") ?? frontmatter?.version ?? stringValue(pkg?.version);
|
|
1872
|
+
}
|
|
1869
1873
|
function createInstructionManifest(name, options) {
|
|
1870
1874
|
return {
|
|
1871
1875
|
$schema: PORTABLE_SKILL_SCHEMA,
|
|
@@ -4181,7 +4185,8 @@ function getSkillRequirements(name) {
|
|
|
4181
4185
|
}
|
|
4182
4186
|
}
|
|
4183
4187
|
envVars.delete("SKILL_API_KEY");
|
|
4184
|
-
envVars.
|
|
4188
|
+
envVars.delete("SKILLS_API_KEY");
|
|
4189
|
+
envVars.add("HASNA_SKILLS_API_KEY");
|
|
4185
4190
|
}
|
|
4186
4191
|
const systemDeps = new Set;
|
|
4187
4192
|
const depPatterns = [
|
|
@@ -8400,65 +8405,815 @@ var coerce = {
|
|
|
8400
8405
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
8401
8406
|
};
|
|
8402
8407
|
var NEVER = INVALID;
|
|
8403
|
-
//
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8408
|
+
// ../contracts/dist/client/transport.js
|
|
8409
|
+
import { isIP } from "net";
|
|
8410
|
+
import { spawnSync } from "child_process";
|
|
8411
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync12 } from "fs";
|
|
8412
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
8413
|
+
import { createRequire } from "module";
|
|
8414
|
+
import { hostname as osHostname } from "os";
|
|
8415
|
+
import { isAbsolute as isAbsolute3, join as join14 } from "path";
|
|
8416
|
+
function envToken(name) {
|
|
8417
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
8418
|
+
}
|
|
8419
|
+
function clientTransportEnvKeys(name) {
|
|
8420
|
+
const envSegment = envToken(name);
|
|
8421
|
+
return {
|
|
8422
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
8423
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
8424
|
+
};
|
|
8425
|
+
}
|
|
8426
|
+
function credentialOverrideEnvKey(name) {
|
|
8427
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
8428
|
+
}
|
|
8429
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
8430
|
+
function credentialPointerEnvKey(name) {
|
|
8431
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
8432
|
+
}
|
|
8407
8433
|
|
|
8408
|
-
class
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8434
|
+
class CredentialResolutionError extends Error {
|
|
8435
|
+
appName;
|
|
8436
|
+
attempted;
|
|
8437
|
+
constructor(appName, message, attempted) {
|
|
8438
|
+
super(message);
|
|
8439
|
+
this.name = "CredentialResolutionError";
|
|
8440
|
+
this.appName = appName;
|
|
8441
|
+
this.attempted = attempted;
|
|
8413
8442
|
}
|
|
8414
8443
|
}
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8444
|
+
|
|
8445
|
+
class CredentialFileUnsafeError extends Error {
|
|
8446
|
+
path;
|
|
8447
|
+
constructor(path, reason) {
|
|
8448
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
8449
|
+
this.name = "CredentialFileUnsafeError";
|
|
8450
|
+
this.path = path;
|
|
8451
|
+
}
|
|
8419
8452
|
}
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8453
|
+
var HASNA_HOME_ENV_KEY = "HASNA_HOME";
|
|
8454
|
+
var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
|
|
8455
|
+
var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
|
|
8456
|
+
var HASNA_HOME_DIR = ".hasna";
|
|
8457
|
+
var CONFIG_SUBDIR = "config";
|
|
8458
|
+
var CREDENTIALS_FILE = "credentials";
|
|
8459
|
+
var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
|
|
8460
|
+
var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
|
|
8461
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
|
|
8462
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
|
|
8463
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
8464
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
8465
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
8466
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
8467
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
8468
|
+
function homeDir(env) {
|
|
8469
|
+
const home = env.HOME?.trim();
|
|
8470
|
+
return home ? home : null;
|
|
8471
|
+
}
|
|
8472
|
+
function absoluteOverride(env, key) {
|
|
8473
|
+
const value = env[key]?.trim();
|
|
8474
|
+
return value && isAbsolute3(value) ? value : null;
|
|
8475
|
+
}
|
|
8476
|
+
function hasnaHomeDir(env) {
|
|
8477
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
8478
|
+
if (override)
|
|
8479
|
+
return override;
|
|
8480
|
+
const home = homeDir(env);
|
|
8481
|
+
return home ? join14(home, HASNA_HOME_DIR) : null;
|
|
8482
|
+
}
|
|
8483
|
+
function appConfigDir(name, env) {
|
|
8484
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
8485
|
+
if (configRoot)
|
|
8486
|
+
return join14(configRoot, name);
|
|
8487
|
+
const root = hasnaHomeDir(env);
|
|
8488
|
+
return root ? join14(root, name, CONFIG_SUBDIR) : null;
|
|
8489
|
+
}
|
|
8490
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
8491
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
8492
|
+
return [];
|
|
8493
|
+
const directory = appConfigDir(name, env);
|
|
8494
|
+
if (!directory)
|
|
8495
|
+
return [];
|
|
8496
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
8497
|
+
return [{ path: join14(directory, file), tier: "disk" }];
|
|
8425
8498
|
}
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8499
|
+
function credentialDiskSources(name, env) {
|
|
8500
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
8501
|
+
}
|
|
8502
|
+
function profileDiskSources(name, env, profile) {
|
|
8503
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
8504
|
+
}
|
|
8505
|
+
function parseEnvFile(text) {
|
|
8506
|
+
const values = new Map;
|
|
8507
|
+
const unusable = new Set;
|
|
8508
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
8509
|
+
const line = rawLine.trim();
|
|
8510
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
8511
|
+
continue;
|
|
8512
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
8513
|
+
const equals = withoutExport.indexOf("=");
|
|
8514
|
+
if (equals <= 0)
|
|
8515
|
+
continue;
|
|
8516
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
8517
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
8518
|
+
continue;
|
|
8519
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
8520
|
+
const quote = value[0];
|
|
8521
|
+
if (quote === '"' || quote === "'") {
|
|
8522
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
8523
|
+
unusable.add(key);
|
|
8524
|
+
continue;
|
|
8525
|
+
}
|
|
8526
|
+
value = value.slice(1, -1);
|
|
8527
|
+
}
|
|
8528
|
+
if (value.trim().length === 0) {
|
|
8529
|
+
unusable.add(key);
|
|
8530
|
+
continue;
|
|
8531
|
+
}
|
|
8532
|
+
if (values.has(key) && values.get(key) !== value)
|
|
8533
|
+
unusable.add(key);
|
|
8534
|
+
values.set(key, value);
|
|
8535
|
+
}
|
|
8536
|
+
return { values, unusable };
|
|
8433
8537
|
}
|
|
8434
|
-
function
|
|
8435
|
-
|
|
8538
|
+
function configFileModeAllowed(mode) {
|
|
8539
|
+
const permissions = mode & 4095;
|
|
8540
|
+
return permissions === 256 || permissions === 384;
|
|
8436
8541
|
}
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8542
|
+
function configFileReadsCoherent(before, after) {
|
|
8543
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
8544
|
+
}
|
|
8545
|
+
function readAppConfigFile(path) {
|
|
8546
|
+
const unsafe = (reason) => {
|
|
8547
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
8548
|
+
};
|
|
8549
|
+
let fd = -1;
|
|
8441
8550
|
try {
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
const
|
|
8445
|
-
if (
|
|
8446
|
-
cachedConfig = null;
|
|
8551
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
8552
|
+
} catch (error) {
|
|
8553
|
+
const code = error.code;
|
|
8554
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
8447
8555
|
return null;
|
|
8556
|
+
if (code === "ELOOP")
|
|
8557
|
+
unsafe("the path is a symlink");
|
|
8558
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
8559
|
+
}
|
|
8560
|
+
try {
|
|
8561
|
+
const before = fstatSync(fd);
|
|
8562
|
+
if (!before.isFile())
|
|
8563
|
+
unsafe("the path is not a regular file");
|
|
8564
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
8565
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
8566
|
+
}
|
|
8567
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
8568
|
+
if (uid !== undefined && before.uid !== uid)
|
|
8569
|
+
unsafe("the file is not owned by the current user");
|
|
8570
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
8571
|
+
unsafe("the file exceeds the size limit");
|
|
8572
|
+
const bytes = readFileSync12(fd);
|
|
8573
|
+
const after = fstatSync(fd);
|
|
8574
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
8575
|
+
unsafe("the file changed while being read");
|
|
8576
|
+
}
|
|
8577
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
8578
|
+
} finally {
|
|
8579
|
+
if (fd !== -1)
|
|
8580
|
+
closeSync(fd);
|
|
8581
|
+
}
|
|
8582
|
+
}
|
|
8583
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
8584
|
+
const parsed = readAppConfigFile(path);
|
|
8585
|
+
if (!parsed)
|
|
8586
|
+
return null;
|
|
8587
|
+
for (const key of apiKeyKeys) {
|
|
8588
|
+
if (parsed.unusable.has(key)) {
|
|
8589
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
8448
8590
|
}
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8591
|
+
}
|
|
8592
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
8593
|
+
if (new Set(values).size > 1) {
|
|
8594
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
8595
|
+
}
|
|
8596
|
+
return values[0] ?? null;
|
|
8597
|
+
}
|
|
8598
|
+
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
8599
|
+
function appConfigDiskValue(name, env, keys) {
|
|
8600
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
8601
|
+
if (wanted.length === 0)
|
|
8602
|
+
return null;
|
|
8603
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
8604
|
+
const parsed = readAppConfigFile(path);
|
|
8605
|
+
if (!parsed)
|
|
8606
|
+
continue;
|
|
8607
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
8608
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
8609
|
+
}
|
|
8610
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
8611
|
+
if (new Set(values).size > 1)
|
|
8612
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
8613
|
+
for (const key of wanted) {
|
|
8614
|
+
if (parsed.unusable.has(key))
|
|
8615
|
+
return { key, value: "", path, unusable: true };
|
|
8616
|
+
const value = parsed.values.get(key)?.trim();
|
|
8617
|
+
if (value)
|
|
8618
|
+
return { key, value, path };
|
|
8619
|
+
}
|
|
8620
|
+
}
|
|
8621
|
+
return null;
|
|
8622
|
+
}
|
|
8623
|
+
function assertUsableCredential(appName, source, value) {
|
|
8624
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
8625
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
8626
|
+
}
|
|
8627
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
8628
|
+
return;
|
|
8629
|
+
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]);
|
|
8630
|
+
}
|
|
8631
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
8632
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
8633
|
+
function sealCredential(fields) {
|
|
8634
|
+
const { apiKey } = fields;
|
|
8635
|
+
const visible = {
|
|
8636
|
+
tier: fields.tier,
|
|
8637
|
+
source: fields.source,
|
|
8638
|
+
deliberate: fields.deliberate,
|
|
8639
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
8640
|
+
warning: fields.warning
|
|
8641
|
+
};
|
|
8642
|
+
const sealed = { ...visible };
|
|
8643
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
8644
|
+
value: apiKey,
|
|
8645
|
+
enumerable: false,
|
|
8646
|
+
writable: false,
|
|
8647
|
+
configurable: false
|
|
8648
|
+
});
|
|
8649
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
8650
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
8651
|
+
value: fields.pointerVaultKey,
|
|
8652
|
+
enumerable: false,
|
|
8653
|
+
writable: false,
|
|
8654
|
+
configurable: false
|
|
8655
|
+
});
|
|
8656
|
+
}
|
|
8657
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
8658
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
8659
|
+
enumerable: false,
|
|
8660
|
+
writable: false,
|
|
8661
|
+
configurable: false
|
|
8662
|
+
});
|
|
8663
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
8664
|
+
value: true,
|
|
8665
|
+
enumerable: false,
|
|
8666
|
+
writable: false,
|
|
8667
|
+
configurable: false
|
|
8668
|
+
});
|
|
8669
|
+
return Object.freeze(sealed);
|
|
8670
|
+
}
|
|
8671
|
+
function firstEnvValue(env, keys) {
|
|
8672
|
+
for (const key of keys) {
|
|
8673
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
8674
|
+
continue;
|
|
8675
|
+
const value = env[key]?.trim();
|
|
8676
|
+
if (value)
|
|
8677
|
+
return { key, value };
|
|
8678
|
+
}
|
|
8679
|
+
return null;
|
|
8680
|
+
}
|
|
8681
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
8682
|
+
function isAmbientEnvironment(env) {
|
|
8683
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
8684
|
+
}
|
|
8685
|
+
function defaultKeychainRunner(argv) {
|
|
8686
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
8687
|
+
encoding: "utf8",
|
|
8688
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
8689
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
8690
|
+
});
|
|
8691
|
+
return {
|
|
8692
|
+
status: result.status,
|
|
8693
|
+
stdout: result.stdout ?? "",
|
|
8694
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
8695
|
+
};
|
|
8696
|
+
}
|
|
8697
|
+
function keychainTierEnabled(env, options) {
|
|
8698
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
8699
|
+
return false;
|
|
8700
|
+
if (options.enabled !== undefined)
|
|
8701
|
+
return options.enabled;
|
|
8702
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
8703
|
+
}
|
|
8704
|
+
function keychainAccount(env, options) {
|
|
8705
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
8706
|
+
if (station)
|
|
8707
|
+
return station;
|
|
8708
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
8709
|
+
if (host)
|
|
8710
|
+
return host;
|
|
8711
|
+
const user = env.USER?.trim();
|
|
8712
|
+
return user || null;
|
|
8713
|
+
}
|
|
8714
|
+
function keychainFailureHint(text) {
|
|
8715
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
8716
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
8717
|
+
return clean ? `: ${clean}` : "";
|
|
8718
|
+
}
|
|
8719
|
+
function readKeychainItem(name, env, kind, options) {
|
|
8720
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
8721
|
+
return null;
|
|
8722
|
+
const account = keychainAccount(env, options);
|
|
8723
|
+
if (!account)
|
|
8724
|
+
return null;
|
|
8725
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
8726
|
+
const source = `keychain:${service}@${account}`;
|
|
8727
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
8728
|
+
let result;
|
|
8729
|
+
try {
|
|
8730
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
8731
|
+
} catch (error) {
|
|
8732
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
8733
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
|
|
8734
|
+
}
|
|
8735
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
8453
8736
|
return null;
|
|
8737
|
+
if (result.status !== 0) {
|
|
8738
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
|
|
8739
|
+
}
|
|
8740
|
+
const value = result.stdout.trim();
|
|
8741
|
+
if (!value) {
|
|
8742
|
+
throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
|
|
8743
|
+
}
|
|
8744
|
+
return { value, source };
|
|
8745
|
+
}
|
|
8746
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
8747
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
8748
|
+
}
|
|
8749
|
+
function snapshotClientEnvironment(name, env) {
|
|
8750
|
+
const keys = clientTransportEnvKeys(name);
|
|
8751
|
+
const ambient = isAmbientEnvironment(env);
|
|
8752
|
+
const snapshot = Object.create(null);
|
|
8753
|
+
for (const key of [
|
|
8754
|
+
...keys.apiUrlKeys,
|
|
8755
|
+
...keys.apiKeyKeys,
|
|
8756
|
+
credentialOverrideEnvKey(name),
|
|
8757
|
+
credentialPointerEnvKey(name),
|
|
8758
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
8759
|
+
"HOME",
|
|
8760
|
+
HASNA_HOME_ENV_KEY,
|
|
8761
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
8762
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
8763
|
+
"USER"
|
|
8764
|
+
]) {
|
|
8765
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
8766
|
+
if (!descriptor)
|
|
8767
|
+
continue;
|
|
8768
|
+
if (!("value" in descriptor)) {
|
|
8769
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
8770
|
+
}
|
|
8771
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
8772
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
8773
|
+
}
|
|
8774
|
+
snapshot[key] = descriptor.value;
|
|
8775
|
+
}
|
|
8776
|
+
if (ambient) {
|
|
8777
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
8778
|
+
value: true,
|
|
8779
|
+
enumerable: false,
|
|
8780
|
+
writable: false,
|
|
8781
|
+
configurable: false
|
|
8782
|
+
});
|
|
8783
|
+
}
|
|
8784
|
+
return Object.freeze(snapshot);
|
|
8785
|
+
}
|
|
8786
|
+
function resolveCredential(name, env, options = {}) {
|
|
8787
|
+
env = snapshotClientEnvironment(name, env);
|
|
8788
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
8789
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
8790
|
+
if (options.apiKey !== undefined) {
|
|
8791
|
+
const explicitKey = options.apiKey.trim();
|
|
8792
|
+
if (!explicitKey) {
|
|
8793
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
8794
|
+
}
|
|
8795
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
8796
|
+
return sealCredential({
|
|
8797
|
+
apiKey: explicitKey,
|
|
8798
|
+
tier: "argument",
|
|
8799
|
+
source: "explicit apiKey argument",
|
|
8800
|
+
deliberate: true,
|
|
8801
|
+
diskCandidates: diskPaths,
|
|
8802
|
+
warning: null
|
|
8803
|
+
});
|
|
8804
|
+
}
|
|
8805
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
8806
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
8807
|
+
if (overrideRaw !== undefined) {
|
|
8808
|
+
const override = overrideRaw.trim();
|
|
8809
|
+
if (!override) {
|
|
8810
|
+
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]);
|
|
8811
|
+
}
|
|
8812
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
8813
|
+
return sealCredential({
|
|
8814
|
+
apiKey: override,
|
|
8815
|
+
tier: "override",
|
|
8816
|
+
source: overrideKeyName,
|
|
8817
|
+
deliberate: true,
|
|
8818
|
+
diskCandidates: diskPaths,
|
|
8819
|
+
warning: null
|
|
8820
|
+
});
|
|
8821
|
+
}
|
|
8822
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
8823
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
8824
|
+
if (pointerRaw !== undefined) {
|
|
8825
|
+
const pointer = pointerRaw.trim();
|
|
8826
|
+
if (!pointer) {
|
|
8827
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
8828
|
+
}
|
|
8829
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
8830
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
8831
|
+
}
|
|
8832
|
+
return sealCredential({
|
|
8833
|
+
apiKey: "",
|
|
8834
|
+
pointerVaultKey: pointer,
|
|
8835
|
+
tier: "pointer",
|
|
8836
|
+
source: pointerKeyName,
|
|
8837
|
+
deliberate: true,
|
|
8838
|
+
diskCandidates: diskPaths,
|
|
8839
|
+
warning: null
|
|
8840
|
+
});
|
|
8841
|
+
}
|
|
8842
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
8843
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
8844
|
+
}
|
|
8845
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
8846
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
8847
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
8848
|
+
}
|
|
8849
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
8850
|
+
if (profile) {
|
|
8851
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
8852
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
8853
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
8854
|
+
}
|
|
8855
|
+
const paths = profileDiskSources(name, env, profile);
|
|
8856
|
+
for (const path of paths) {
|
|
8857
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
8858
|
+
if (value) {
|
|
8859
|
+
assertUsableCredential(name, path, value);
|
|
8860
|
+
return sealCredential({
|
|
8861
|
+
apiKey: value,
|
|
8862
|
+
tier: "profile",
|
|
8863
|
+
source: path,
|
|
8864
|
+
deliberate: true,
|
|
8865
|
+
diskCandidates: paths,
|
|
8866
|
+
warning: null
|
|
8867
|
+
});
|
|
8868
|
+
}
|
|
8869
|
+
}
|
|
8870
|
+
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);
|
|
8871
|
+
}
|
|
8872
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
8873
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
8874
|
+
if (blankEnv) {
|
|
8875
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
8876
|
+
}
|
|
8877
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
8878
|
+
throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
|
|
8879
|
+
}
|
|
8880
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
8881
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
8882
|
+
if (keychainHit) {
|
|
8883
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
8884
|
+
const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain 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;
|
|
8885
|
+
return sealCredential({
|
|
8886
|
+
apiKey: keychainHit.value,
|
|
8887
|
+
tier: "keychain",
|
|
8888
|
+
source: keychainHit.source,
|
|
8889
|
+
deliberate: false,
|
|
8890
|
+
diskCandidates: diskPaths,
|
|
8891
|
+
warning
|
|
8892
|
+
});
|
|
8893
|
+
}
|
|
8894
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
8895
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
8896
|
+
if (diskHits.length > 0) {
|
|
8897
|
+
const winner = diskHits[0];
|
|
8898
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
8899
|
+
const divergentSources = [
|
|
8900
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
8901
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
8902
|
+
];
|
|
8903
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.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;
|
|
8904
|
+
return sealCredential({
|
|
8905
|
+
apiKey: winner.value,
|
|
8906
|
+
tier: winner.src.tier,
|
|
8907
|
+
source: winner.src.path,
|
|
8908
|
+
deliberate: false,
|
|
8909
|
+
diskCandidates: diskPaths,
|
|
8910
|
+
warning
|
|
8911
|
+
});
|
|
8912
|
+
}
|
|
8913
|
+
if (envHit) {
|
|
8914
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
8915
|
+
return sealCredential({
|
|
8916
|
+
apiKey: envHit.value,
|
|
8917
|
+
tier: "env",
|
|
8918
|
+
source: envHit.key,
|
|
8919
|
+
deliberate: false,
|
|
8920
|
+
diskCandidates: diskPaths,
|
|
8921
|
+
warning: null
|
|
8922
|
+
});
|
|
8923
|
+
}
|
|
8924
|
+
return null;
|
|
8925
|
+
}
|
|
8926
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
8927
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
8928
|
+
async function completePointerCredential(name, pointerResolution, env = process.env) {
|
|
8929
|
+
const vaultKey = pointerResolution.pointerVaultKey;
|
|
8930
|
+
const pointerEnvKey = pointerResolution.source;
|
|
8931
|
+
if (!vaultKey) {
|
|
8932
|
+
throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
|
|
8933
|
+
}
|
|
8934
|
+
let secretsSdk;
|
|
8935
|
+
try {
|
|
8936
|
+
secretsSdk = requireSecretsSdk(SECRETS_PACKAGE_SPECIFIER);
|
|
8937
|
+
} catch {
|
|
8938
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
|
|
8939
|
+
}
|
|
8940
|
+
let client;
|
|
8941
|
+
try {
|
|
8942
|
+
client = secretsSdk.createSecretsClientFromEnv(env);
|
|
8943
|
+
} catch {
|
|
8944
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
8945
|
+
}
|
|
8946
|
+
let secret;
|
|
8947
|
+
try {
|
|
8948
|
+
secret = await client.getSecret({ key: vaultKey });
|
|
8949
|
+
} catch {
|
|
8950
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
8951
|
+
}
|
|
8952
|
+
const value = secret.value;
|
|
8953
|
+
if (!value) {
|
|
8954
|
+
throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
|
|
8955
|
+
}
|
|
8956
|
+
assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
|
|
8957
|
+
return sealCredential({
|
|
8958
|
+
apiKey: value,
|
|
8959
|
+
tier: "pointer",
|
|
8960
|
+
source: `${pointerEnvKey} -> vault:${vaultKey}`,
|
|
8961
|
+
deliberate: true,
|
|
8962
|
+
diskCandidates: pointerResolution.diskCandidates,
|
|
8963
|
+
warning: null
|
|
8964
|
+
});
|
|
8965
|
+
}
|
|
8966
|
+
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
8967
|
+
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
8968
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
8969
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
8970
|
+
}
|
|
8971
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
8972
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
8973
|
+
function isValidDnsDomain(value) {
|
|
8974
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
8975
|
+
return false;
|
|
8454
8976
|
}
|
|
8977
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
8455
8978
|
}
|
|
8456
|
-
function
|
|
8457
|
-
if (
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8979
|
+
function validateAppSlug(name) {
|
|
8980
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
8981
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
8982
|
+
}
|
|
8983
|
+
return name;
|
|
8984
|
+
}
|
|
8985
|
+
function rawAuthority(value) {
|
|
8986
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
8987
|
+
if (!match)
|
|
8988
|
+
throw new Error("API URL must be absolute.");
|
|
8989
|
+
const afterScheme = value.slice(match[0].length);
|
|
8990
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
8991
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
8992
|
+
if (!authority)
|
|
8993
|
+
throw new Error("API URL must include a hostname.");
|
|
8994
|
+
return authority;
|
|
8995
|
+
}
|
|
8996
|
+
function assertCanonicalPort(port) {
|
|
8997
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
8998
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
8999
|
+
}
|
|
9000
|
+
const numericPort = Number(port);
|
|
9001
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
9002
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
9003
|
+
}
|
|
9004
|
+
}
|
|
9005
|
+
function canonicalAuthorityHostname(authority) {
|
|
9006
|
+
let rawHostname;
|
|
9007
|
+
if (authority.startsWith("[")) {
|
|
9008
|
+
const closingBracket = authority.indexOf("]");
|
|
9009
|
+
if (closingBracket === -1) {
|
|
9010
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
9011
|
+
}
|
|
9012
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
9013
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
9014
|
+
if (portSuffix) {
|
|
9015
|
+
if (!portSuffix.startsWith(":")) {
|
|
9016
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
9017
|
+
}
|
|
9018
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
9019
|
+
}
|
|
9020
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
9021
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
9022
|
+
}
|
|
9023
|
+
} else {
|
|
9024
|
+
const firstColon = authority.indexOf(":");
|
|
9025
|
+
const lastColon = authority.lastIndexOf(":");
|
|
9026
|
+
if (firstColon !== lastColon) {
|
|
9027
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
9028
|
+
}
|
|
9029
|
+
if (lastColon !== -1) {
|
|
9030
|
+
const port = authority.slice(lastColon + 1);
|
|
9031
|
+
assertCanonicalPort(port);
|
|
9032
|
+
rawHostname = authority.slice(0, lastColon);
|
|
9033
|
+
} else {
|
|
9034
|
+
rawHostname = authority;
|
|
9035
|
+
}
|
|
9036
|
+
const ipVersion = isIP(rawHostname);
|
|
9037
|
+
const numericAddressParts = rawHostname.split(".");
|
|
9038
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
9039
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
9040
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
9041
|
+
}
|
|
9042
|
+
}
|
|
9043
|
+
return rawHostname.toLowerCase();
|
|
9044
|
+
}
|
|
9045
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
9046
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
9047
|
+
}
|
|
9048
|
+
function toV1BaseUrl(apiUrl) {
|
|
9049
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
9050
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
9051
|
+
}
|
|
9052
|
+
const input = apiUrl.trim();
|
|
9053
|
+
const authority = rawAuthority(input);
|
|
9054
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
9055
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
9056
|
+
}
|
|
9057
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
9058
|
+
const url = new URL(input);
|
|
9059
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
9060
|
+
throw new Error("API URL must use http or https.");
|
|
9061
|
+
}
|
|
9062
|
+
if (url.username || url.password) {
|
|
9063
|
+
throw new Error("API URL must not include credentials.");
|
|
9064
|
+
}
|
|
9065
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
9066
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
9067
|
+
}
|
|
9068
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
9069
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
9070
|
+
}
|
|
9071
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
9072
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
9073
|
+
}
|
|
9074
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
9075
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
9076
|
+
}
|
|
9077
|
+
if (url.search || url.hash) {
|
|
9078
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
9079
|
+
}
|
|
9080
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
9081
|
+
if (path.endsWith("/v1"))
|
|
9082
|
+
path = path.slice(0, -"/v1".length);
|
|
9083
|
+
url.pathname = `${path}/v1`;
|
|
9084
|
+
return url.toString().replace(/\/+$/, "");
|
|
9085
|
+
}
|
|
9086
|
+
class ClientTransportConfigurationError extends Error {
|
|
9087
|
+
appName;
|
|
9088
|
+
sources;
|
|
9089
|
+
constructor(appName, message, sources = []) {
|
|
9090
|
+
super(message);
|
|
9091
|
+
this.name = "ClientTransportConfigurationError";
|
|
9092
|
+
this.appName = appName;
|
|
9093
|
+
this.sources = Object.freeze([...sources]);
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9096
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
9097
|
+
env = snapshotClientEnvironment(name, env);
|
|
9098
|
+
const keys = clientTransportEnvKeys(name);
|
|
9099
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
9100
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
9101
|
+
if (blankUrl) {
|
|
9102
|
+
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
9103
|
+
}
|
|
9104
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
9105
|
+
if (controlledUrl) {
|
|
9106
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
9107
|
+
}
|
|
9108
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
9109
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
9110
|
+
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
9111
|
+
}
|
|
9112
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
9113
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
9114
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
9115
|
+
if (diskConfigUrlHit?.unusable) {
|
|
9116
|
+
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
9117
|
+
}
|
|
9118
|
+
const urlCandidates = [
|
|
9119
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
9120
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
9121
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
9122
|
+
];
|
|
9123
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
9124
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
9125
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
9126
|
+
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
9127
|
+
}
|
|
9128
|
+
const warnings = [];
|
|
9129
|
+
if (configuredUrl && !envUrlHit) {
|
|
9130
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
9131
|
+
}
|
|
9132
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
9133
|
+
if (!credential) {
|
|
9134
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
9135
|
+
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
9136
|
+
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
9137
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
9138
|
+
}
|
|
9139
|
+
if (credential.warning)
|
|
9140
|
+
warnings.push(credential.warning);
|
|
9141
|
+
let urlHit;
|
|
9142
|
+
if (configuredUrl) {
|
|
9143
|
+
urlHit = configuredUrl;
|
|
9144
|
+
} else {
|
|
9145
|
+
try {
|
|
9146
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
9147
|
+
} catch (error) {
|
|
9148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9149
|
+
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
9150
|
+
}
|
|
9151
|
+
}
|
|
9152
|
+
const apiUrlSource = urlHit.key;
|
|
9153
|
+
let baseUrl;
|
|
9154
|
+
try {
|
|
9155
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
9156
|
+
} catch (error) {
|
|
9157
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9158
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
9159
|
+
}
|
|
9160
|
+
return {
|
|
9161
|
+
resolution: {
|
|
9162
|
+
transport: "http",
|
|
9163
|
+
transportSource: urlHit.key,
|
|
9164
|
+
baseUrl,
|
|
9165
|
+
apiUrlSource,
|
|
9166
|
+
apiKeyPresent: true,
|
|
9167
|
+
apiKeySource: credential.source,
|
|
9168
|
+
apiKeyTier: credential.tier,
|
|
9169
|
+
misconfigured: false,
|
|
9170
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
9171
|
+
},
|
|
9172
|
+
credential
|
|
9173
|
+
};
|
|
9174
|
+
}
|
|
9175
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
9176
|
+
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
9177
|
+
}
|
|
9178
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
9179
|
+
const paths = credentialDiskSources(name, env);
|
|
9180
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
9181
|
+
}
|
|
9182
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
9183
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
9184
|
+
"host",
|
|
9185
|
+
":authority",
|
|
9186
|
+
"forwarded",
|
|
9187
|
+
"x-forwarded-host",
|
|
9188
|
+
"x-original-host"
|
|
9189
|
+
]);
|
|
9190
|
+
|
|
9191
|
+
// src/lib/fleet-credentials.ts
|
|
9192
|
+
var SKILLS_APP = "skills";
|
|
9193
|
+
var ENV_KEYS = clientTransportEnvKeys(SKILLS_APP);
|
|
9194
|
+
var SKILLS_API_URL_ENV_KEYS = ENV_KEYS.apiUrlKeys;
|
|
9195
|
+
var SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
|
|
9196
|
+
var SKILLS_API_URL_ENV = SKILLS_API_URL_ENV_KEYS[0];
|
|
9197
|
+
var SKILLS_API_KEY_ENV = SKILLS_API_KEY_ENV_KEYS[0];
|
|
9198
|
+
|
|
9199
|
+
class SkillsFleetCredentialError extends Error {
|
|
9200
|
+
code;
|
|
9201
|
+
constructor(message, code = "MISSING_API_CREDENTIAL") {
|
|
9202
|
+
super(message);
|
|
9203
|
+
this.name = "SkillsFleetCredentialError";
|
|
9204
|
+
this.code = code;
|
|
9205
|
+
}
|
|
9206
|
+
}
|
|
9207
|
+
function isClientTransportConfigurationError(error) {
|
|
9208
|
+
return error instanceof ClientTransportConfigurationError || typeof error === "object" && error !== null && error.name === "ClientTransportConfigurationError";
|
|
9209
|
+
}
|
|
9210
|
+
function isCredentialResolutionError(error) {
|
|
9211
|
+
return error instanceof CredentialResolutionError || typeof error === "object" && error !== null && error.name === "CredentialResolutionError";
|
|
9212
|
+
}
|
|
9213
|
+
function asSkillsFleetCredentialError(error) {
|
|
9214
|
+
if (!isCredentialResolutionError(error))
|
|
9215
|
+
return null;
|
|
9216
|
+
return new SkillsFleetCredentialError(error.message, "MISSING_API_CREDENTIAL");
|
|
8462
9217
|
}
|
|
8463
9218
|
function normalizeSkillsApiOrigin(apiUrl) {
|
|
8464
9219
|
const url = new URL(apiUrl);
|
|
@@ -8472,8 +9227,172 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
8472
9227
|
}
|
|
8473
9228
|
return url.toString().replace(/\/+$/, "");
|
|
8474
9229
|
}
|
|
8475
|
-
function
|
|
8476
|
-
|
|
9230
|
+
function configuredSkillsApiUrl(env = process.env, keychain) {
|
|
9231
|
+
for (const key of SKILLS_API_URL_ENV_KEYS) {
|
|
9232
|
+
const value = env[key]?.trim();
|
|
9233
|
+
if (value)
|
|
9234
|
+
return { value, source: key };
|
|
9235
|
+
}
|
|
9236
|
+
const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
|
|
9237
|
+
if (fromKeychain)
|
|
9238
|
+
return { value: fromKeychain.value.trim(), source: fromKeychain.source };
|
|
9239
|
+
const fromDisk = appConfigDiskValue(SKILLS_APP, env, SKILLS_API_URL_ENV_KEYS);
|
|
9240
|
+
if (fromDisk?.unusable) {
|
|
9241
|
+
throw new SkillsFleetCredentialError(`${fromDisk.key} in ${fromDisk.path} is declared but blank or malformed; ` + `a Skills authority must be a valid https URL (or an exact loopback http URL).`, "INVALID_API_URL");
|
|
9242
|
+
}
|
|
9243
|
+
if (fromDisk)
|
|
9244
|
+
return { value: fromDisk.value.trim(), source: fromDisk.path };
|
|
9245
|
+
return null;
|
|
9246
|
+
}
|
|
9247
|
+
function skillsCredentialFiles(env = process.env) {
|
|
9248
|
+
return credentialDiskSources(SKILLS_APP, env);
|
|
9249
|
+
}
|
|
9250
|
+
function skillsCredentialFilePath(env = process.env) {
|
|
9251
|
+
const paths = skillsCredentialFiles(env);
|
|
9252
|
+
const path = paths[0];
|
|
9253
|
+
if (!path) {
|
|
9254
|
+
throw new Error("No home directory is set (HOME or HASNA_HOME), so there is nowhere to store a Skills credential.");
|
|
9255
|
+
}
|
|
9256
|
+
return path;
|
|
9257
|
+
}
|
|
9258
|
+
var localNoticePrinted = false;
|
|
9259
|
+
function noticeLocalSkillsMode(write = (line) => console.error(line)) {
|
|
9260
|
+
if (localNoticePrinted)
|
|
9261
|
+
return;
|
|
9262
|
+
localNoticePrinted = true;
|
|
9263
|
+
write(`skills: local mode \u2014 no ${SKILLS_API_KEY_ENV} and no ${SKILLS_API_URL_ENV} resolved, ` + `so this runs on this machine against the bundled corpus. ` + `Sign in with: skills auth login`);
|
|
9264
|
+
}
|
|
9265
|
+
function resolveSkillsFleet(env = process.env, options = {}) {
|
|
9266
|
+
try {
|
|
9267
|
+
return resolveSkillsFleetOrThrow(env, options);
|
|
9268
|
+
} catch (error) {
|
|
9269
|
+
const translated = asSkillsFleetCredentialError(error);
|
|
9270
|
+
if (translated)
|
|
9271
|
+
throw translated;
|
|
9272
|
+
throw error;
|
|
9273
|
+
}
|
|
9274
|
+
}
|
|
9275
|
+
function resolveSkillsFleetOrThrow(env, options) {
|
|
9276
|
+
let resolution;
|
|
9277
|
+
try {
|
|
9278
|
+
resolution = resolveClientTransport(SKILLS_APP, env, { credentials: options.credentials });
|
|
9279
|
+
} catch (error) {
|
|
9280
|
+
if (!isClientTransportConfigurationError(error))
|
|
9281
|
+
throw error;
|
|
9282
|
+
const configured2 = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
9283
|
+
const credential2 = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
9284
|
+
if (!configured2 && !credential2) {
|
|
9285
|
+
if (env === process.env)
|
|
9286
|
+
noticeLocalSkillsMode();
|
|
9287
|
+
return { mode: "local", apiOrigin: null, apiKey: null };
|
|
9288
|
+
}
|
|
9289
|
+
if (configured2 && !credential2) {
|
|
9290
|
+
throw new SkillsFleetCredentialError(`${configured2.source} points this CLI at a Skills service but no API key resolved \u2014 ` + `refusing to run locally instead. Looked in the Keychain item ` + `hasna.credentials.${SKILLS_APP}.api-key, then ${skillsCredentialFiles(env).join(" or ") || "no credentials file (no HOME)"}, ` + `then ${SKILLS_API_KEY_ENV}. Sign in with: skills auth login`);
|
|
9291
|
+
}
|
|
9292
|
+
throw error;
|
|
9293
|
+
}
|
|
9294
|
+
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
9295
|
+
const apiOrigin = configured ? normalizeSkillsApiOrigin(configured.value) : stripV1(resolution.baseUrl);
|
|
9296
|
+
const credential = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
9297
|
+
if (!credential) {
|
|
9298
|
+
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
|
|
9299
|
+
}
|
|
9300
|
+
const base = {
|
|
9301
|
+
mode: "hosted",
|
|
9302
|
+
apiOrigin,
|
|
9303
|
+
apiUrlSource: resolution.apiUrlSource ?? (configured?.source ?? "default"),
|
|
9304
|
+
apiKeySource: resolution.apiKeySource ?? credential.source,
|
|
9305
|
+
apiKeyTier: resolution.apiKeyTier,
|
|
9306
|
+
warning: resolution.warning
|
|
9307
|
+
};
|
|
9308
|
+
if (credential.tier === "pointer") {
|
|
9309
|
+
return { ...base, apiKey: null, apiKeyPointer: credential };
|
|
9310
|
+
}
|
|
9311
|
+
if (!credential.apiKey.trim()) {
|
|
9312
|
+
throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills auth login`);
|
|
9313
|
+
}
|
|
9314
|
+
return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
|
|
9315
|
+
}
|
|
9316
|
+
async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
9317
|
+
const fleet = resolveSkillsFleet(env, options);
|
|
9318
|
+
if (fleet.mode !== "hosted")
|
|
9319
|
+
return null;
|
|
9320
|
+
if (fleet.apiKey)
|
|
9321
|
+
return fleet.apiKey;
|
|
9322
|
+
const pointer = fleet.apiKeyPointer;
|
|
9323
|
+
if (!pointer) {
|
|
9324
|
+
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
|
|
9325
|
+
}
|
|
9326
|
+
let completed;
|
|
9327
|
+
try {
|
|
9328
|
+
completed = await completePointerCredential(SKILLS_APP, pointer, env);
|
|
9329
|
+
} catch (error) {
|
|
9330
|
+
const translated = asSkillsFleetCredentialError(error);
|
|
9331
|
+
if (translated)
|
|
9332
|
+
throw translated;
|
|
9333
|
+
throw error;
|
|
9334
|
+
}
|
|
9335
|
+
if (!completed.apiKey?.trim()) {
|
|
9336
|
+
throw new SkillsFleetCredentialError(`${credentialPointerEnvKey(SKILLS_APP)} names a vault item that produced an empty Skills API key \u2014 ` + `refusing to send an unauthenticated request.`);
|
|
9337
|
+
}
|
|
9338
|
+
return completed.apiKey;
|
|
9339
|
+
}
|
|
9340
|
+
async function requireSkillsApiKey(action = "This command", env = process.env, options = {}) {
|
|
9341
|
+
const apiKey = await resolveSkillsApiKey(env, options);
|
|
9342
|
+
if (!apiKey)
|
|
9343
|
+
throw new MissingSkillsFleetError(action);
|
|
9344
|
+
return apiKey;
|
|
9345
|
+
}
|
|
9346
|
+
function stripV1(baseUrl) {
|
|
9347
|
+
return baseUrl.replace(/\/v1$/, "").replace(/\/+$/, "");
|
|
9348
|
+
}
|
|
9349
|
+
async function skillsCredentialOrReason(env = process.env, options = {}) {
|
|
9350
|
+
try {
|
|
9351
|
+
const apiKey = await resolveSkillsApiKey(env, options);
|
|
9352
|
+
return apiKey ? { apiKey, reason: null } : { apiKey: null, reason: null };
|
|
9353
|
+
} catch (error) {
|
|
9354
|
+
if (error instanceof SkillsFleetCredentialError || error?.name === "SkillsFleetCredentialError") {
|
|
9355
|
+
return { apiKey: null, reason: error.message };
|
|
9356
|
+
}
|
|
9357
|
+
throw error;
|
|
9358
|
+
}
|
|
9359
|
+
}
|
|
9360
|
+
function resolveSkillsApiOrigin(env = process.env, options = {}) {
|
|
9361
|
+
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
9362
|
+
if (configured) {
|
|
9363
|
+
toV1BaseUrl(configured.value);
|
|
9364
|
+
return { origin: normalizeSkillsApiOrigin(configured.value), source: configured.source };
|
|
9365
|
+
}
|
|
9366
|
+
const fleet = resolveSkillsFleet(env, options);
|
|
9367
|
+
return fleet.mode === "hosted" ? { origin: fleet.apiOrigin, source: fleet.apiUrlSource } : null;
|
|
9368
|
+
}
|
|
9369
|
+
function requireSkillsApiOrigin(action = "This command", env = process.env, options = {}) {
|
|
9370
|
+
const resolved = resolveSkillsApiOrigin(env, options);
|
|
9371
|
+
if (!resolved)
|
|
9372
|
+
throw new MissingSkillsFleetError(action);
|
|
9373
|
+
return resolved.origin;
|
|
9374
|
+
}
|
|
9375
|
+
function requireSkillsFleet(action = "This command", env = process.env, options = {}) {
|
|
9376
|
+
const fleet = resolveSkillsFleet(env, options);
|
|
9377
|
+
if (fleet.mode === "hosted")
|
|
9378
|
+
return fleet;
|
|
9379
|
+
throw new MissingSkillsFleetError(action);
|
|
9380
|
+
}
|
|
9381
|
+
|
|
9382
|
+
class MissingSkillsFleetError extends Error {
|
|
9383
|
+
code = "MISSING_API_URL";
|
|
9384
|
+
constructor(action = "This command") {
|
|
9385
|
+
super(`${action} requires a Skills API credential and none is configured \u2014 ` + `run: skills auth login, or set ${SKILLS_API_KEY_ENV} ` + `(add the Keychain item hasna.credentials.${SKILLS_APP}.api-key, or write ~/.hasna/skills/config/credentials). ` + `Point at your own instance with ${SKILLS_API_URL_ENV}, or run: skills setup --api-url <your Skills instance origin>`);
|
|
9386
|
+
this.name = "MissingSkillsFleetError";
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9389
|
+
|
|
9390
|
+
// src/lib/api-url.ts
|
|
9391
|
+
var API_URL_ENV_VAR = SKILLS_API_URL_ENV;
|
|
9392
|
+
var MISSING_API_URL_HINT = `run: skills auth login, or set ${API_URL_ENV_VAR}=<your Skills instance origin>, ` + `or run: skills setup --api-url <your Skills instance origin>`;
|
|
9393
|
+
function resolveApiUrl(env = process.env, options = {}) {
|
|
9394
|
+
const fleet = resolveSkillsFleet(env, options);
|
|
9395
|
+
return fleet.mode === "hosted" ? fleet.apiOrigin : undefined;
|
|
8477
9396
|
}
|
|
8478
9397
|
|
|
8479
9398
|
// src/lib/discovery.ts
|
|
@@ -8601,26 +9520,19 @@ var remoteRegistrySchema = exports_external.union([
|
|
|
8601
9520
|
exports_external.object({ skills: exports_external.array(remoteSkillSchema) }),
|
|
8602
9521
|
exports_external.object({ data: exports_external.array(remoteSkillSchema) })
|
|
8603
9522
|
]);
|
|
8604
|
-
function getConfiguredApiUrl(
|
|
8605
|
-
return resolveApiUrl(
|
|
9523
|
+
function getConfiguredApiUrl(env = process.env) {
|
|
9524
|
+
return resolveApiUrl(env);
|
|
8606
9525
|
}
|
|
8607
9526
|
function buildSkillsApiUrl(apiUrl, endpoint = "/skills") {
|
|
8608
9527
|
const url = new URL(apiUrl);
|
|
8609
9528
|
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
8610
9529
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
} else {
|
|
8615
|
-
url.pathname = `${pathname.slice(0, -"/skills".length)}${cleanEndpoint}` || cleanEndpoint;
|
|
8616
|
-
}
|
|
8617
|
-
return url.toString();
|
|
8618
|
-
}
|
|
8619
|
-
if (pathname.endsWith("/api") || pathname.endsWith("/api/v1")) {
|
|
8620
|
-
url.pathname = `${pathname}${cleanEndpoint}`;
|
|
9530
|
+
const apiBase = /\/api(?:\/v1)?\/skills$/.test(pathname) ? pathname.slice(0, -"/skills".length) : pathname;
|
|
9531
|
+
if (/\/api(?:\/v1)?$/.test(apiBase)) {
|
|
9532
|
+
url.pathname = `${apiBase}${cleanEndpoint}`;
|
|
8621
9533
|
return url.toString();
|
|
8622
9534
|
}
|
|
8623
|
-
url.pathname = `${
|
|
9535
|
+
url.pathname = `${apiBase}/api/v1${cleanEndpoint}`.replace(/\/{2,}/g, "/");
|
|
8624
9536
|
return url.toString();
|
|
8625
9537
|
}
|
|
8626
9538
|
function titleize(name) {
|
|
@@ -8681,9 +9593,9 @@ function parseRemoteContract(schema, payload, message) {
|
|
|
8681
9593
|
throw error;
|
|
8682
9594
|
}
|
|
8683
9595
|
}
|
|
8684
|
-
function remoteRequestHeaders(options) {
|
|
9596
|
+
async function remoteRequestHeaders(options) {
|
|
8685
9597
|
const headers = new Headers({ Accept: "application/json" });
|
|
8686
|
-
const token = options.authToken !== undefined ? options.authToken :
|
|
9598
|
+
const token = options.authToken !== undefined ? options.authToken : await resolveSkillsApiKey();
|
|
8687
9599
|
const trimmed = token?.trim();
|
|
8688
9600
|
if (trimmed)
|
|
8689
9601
|
headers.set("Authorization", `Bearer ${trimmed}`);
|
|
@@ -8691,11 +9603,12 @@ function remoteRequestHeaders(options) {
|
|
|
8691
9603
|
}
|
|
8692
9604
|
async function fetchRemoteJson(url, options) {
|
|
8693
9605
|
const fetchImpl = options.fetchImpl || fetch;
|
|
9606
|
+
const headers = await remoteRequestHeaders(options);
|
|
8694
9607
|
const controller = new AbortController;
|
|
8695
9608
|
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 1e4);
|
|
8696
9609
|
try {
|
|
8697
9610
|
const response = await fetchImpl(url, {
|
|
8698
|
-
headers
|
|
9611
|
+
headers,
|
|
8699
9612
|
signal: controller.signal
|
|
8700
9613
|
});
|
|
8701
9614
|
if (!response.ok) {
|
|
@@ -8709,7 +9622,7 @@ async function fetchRemoteJson(url, options) {
|
|
|
8709
9622
|
async function loadRemoteRegistry(options = {}) {
|
|
8710
9623
|
const apiUrl = options.apiUrl || getConfiguredApiUrl();
|
|
8711
9624
|
if (!apiUrl) {
|
|
8712
|
-
throw new Error(
|
|
9625
|
+
throw new Error(`Remote registry requires a Skills credential (${SKILLS_API_KEY_ENV}, the Keychain item, or ~/.hasna/skills/config/credentials) and, for your own instance, ${SKILLS_API_URL_ENV}`);
|
|
8713
9626
|
}
|
|
8714
9627
|
const url = buildSkillsApiUrl(apiUrl, options.endpoint);
|
|
8715
9628
|
return parseRemoteRegistryPayload(await fetchRemoteJson(url, options));
|
|
@@ -8717,7 +9630,7 @@ async function loadRemoteRegistry(options = {}) {
|
|
|
8717
9630
|
async function loadRemoteSkill(name, options = {}) {
|
|
8718
9631
|
const apiUrl = options.apiUrl || getConfiguredApiUrl();
|
|
8719
9632
|
if (!apiUrl) {
|
|
8720
|
-
throw new Error(
|
|
9633
|
+
throw new Error(`Remote registry requires a Skills credential (${SKILLS_API_KEY_ENV}, the Keychain item, or ~/.hasna/skills/config/credentials) and, for your own instance, ${SKILLS_API_URL_ENV}`);
|
|
8721
9634
|
}
|
|
8722
9635
|
const slug = encodeURIComponent(name);
|
|
8723
9636
|
const url = buildSkillsApiUrl(apiUrl, options.endpoint ?? `/skills/${slug}`);
|
|
@@ -8857,7 +9770,7 @@ var TOOL_PRIMITIVES = [
|
|
|
8857
9770
|
cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
|
|
8858
9771
|
mcpTools: ["get_skill_tool_dependencies", "run_skill"],
|
|
8859
9772
|
apiSurfaces: ["runSkill", "SkillRunRecord", "RemoteSkillRunContract"],
|
|
8860
|
-
envVars: ["
|
|
9773
|
+
envVars: ["HASNA_SKILLS_API_KEY"],
|
|
8861
9774
|
outputTypes: ["text", "json", "markdown", "artifact"],
|
|
8862
9775
|
capabilities: ["completion", "reasoning", "tool-calling", "vision-input", "structured-output"]
|
|
8863
9776
|
},
|
|
@@ -8913,7 +9826,7 @@ var TOOL_PRIMITIVES = [
|
|
|
8913
9826
|
cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
|
|
8914
9827
|
mcpTools: ["get_skill_tool_dependencies", "run_skill"],
|
|
8915
9828
|
apiSurfaces: ["runSkill", "RemoteSkillRunContract"],
|
|
8916
|
-
envVars: ["
|
|
9829
|
+
envVars: ["HASNA_SKILLS_API_KEY"],
|
|
8917
9830
|
outputTypes: ["png", "jpeg", "webp", "svg", "zip"],
|
|
8918
9831
|
capabilities: ["image-generation", "image-analysis", "image-editing", "asset-packaging"]
|
|
8919
9832
|
},
|
|
@@ -8927,7 +9840,7 @@ var TOOL_PRIMITIVES = [
|
|
|
8927
9840
|
cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
|
|
8928
9841
|
mcpTools: ["get_skill_tool_dependencies", "run_skill"],
|
|
8929
9842
|
apiSurfaces: ["runSkill", "RemoteSkillRunContract"],
|
|
8930
|
-
envVars: ["
|
|
9843
|
+
envVars: ["HASNA_SKILLS_API_KEY"],
|
|
8931
9844
|
outputTypes: ["mp3", "wav", "txt", "srt", "json", "zip"],
|
|
8932
9845
|
capabilities: ["transcription", "audio-generation", "voiceover", "audio-cleanup"]
|
|
8933
9846
|
},
|
|
@@ -8941,7 +9854,7 @@ var TOOL_PRIMITIVES = [
|
|
|
8941
9854
|
cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
|
|
8942
9855
|
mcpTools: ["get_skill_tool_dependencies", "run_skill"],
|
|
8943
9856
|
apiSurfaces: ["runSkill", "RemoteSkillRunContract"],
|
|
8944
|
-
envVars: ["
|
|
9857
|
+
envVars: ["HASNA_SKILLS_API_KEY"],
|
|
8945
9858
|
outputTypes: ["mp4", "mov", "srt", "png", "json", "zip"],
|
|
8946
9859
|
capabilities: ["video-generation", "video-analysis", "captioning", "highlight-extraction"]
|
|
8947
9860
|
},
|
|
@@ -8969,7 +9882,7 @@ var TOOL_PRIMITIVES = [
|
|
|
8969
9882
|
cliCommands: ["skills tools deps <skill>", "skills run <skill>"],
|
|
8970
9883
|
mcpTools: ["get_skill_tool_dependencies", "run_skill"],
|
|
8971
9884
|
apiSurfaces: ["SkillRunContext", "RemoteSkillRunContract"],
|
|
8972
|
-
envVars: ["
|
|
9885
|
+
envVars: ["HASNA_SKILLS_API_KEY"],
|
|
8973
9886
|
outputTypes: ["json", "artifact"],
|
|
8974
9887
|
capabilities: ["approval", "external-api", "account-scoped-execution"]
|
|
8975
9888
|
},
|
|
@@ -9011,7 +9924,7 @@ var TOOL_PRIMITIVES = [
|
|
|
9011
9924
|
cliCommands: ["skills auth login", "skills run <skill>"],
|
|
9012
9925
|
mcpTools: ["run_skill"],
|
|
9013
9926
|
apiSurfaces: ["RemoteSkillsClient", "RemoteSkillRunContract"],
|
|
9014
|
-
envVars: ["
|
|
9927
|
+
envVars: ["HASNA_SKILLS_API_KEY"],
|
|
9015
9928
|
outputTypes: ["json"],
|
|
9016
9929
|
capabilities: ["account-auth", "remote-run-submit"]
|
|
9017
9930
|
}
|
|
@@ -9219,6 +10132,11 @@ function primitiveHaystack(primitive) {
|
|
|
9219
10132
|
function clone(value) {
|
|
9220
10133
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
9221
10134
|
}
|
|
10135
|
+
// src/lib/auth-store.ts
|
|
10136
|
+
function getApiUrl(action, env = process.env, options = {}) {
|
|
10137
|
+
return requireSkillsApiOrigin(action, env, options);
|
|
10138
|
+
}
|
|
10139
|
+
|
|
9222
10140
|
// src/lib/remote-run-contract.ts
|
|
9223
10141
|
var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
|
|
9224
10142
|
function normalizeRemoteSkillRunContract(payload, fallbackSkill) {
|
|
@@ -9551,21 +10469,25 @@ function normalizeUpdatedSincePage(payload) {
|
|
|
9551
10469
|
}
|
|
9552
10470
|
return { skills, nextCursor };
|
|
9553
10471
|
}
|
|
9554
|
-
function createRemoteSkillsClient() {
|
|
9555
|
-
const
|
|
9556
|
-
if (
|
|
10472
|
+
async function createRemoteSkillsClient(env = process.env) {
|
|
10473
|
+
const fleet = resolveSkillsFleet(env);
|
|
10474
|
+
if (fleet.mode !== "hosted")
|
|
9557
10475
|
return null;
|
|
9558
|
-
|
|
10476
|
+
const apiKey = await resolveSkillsApiKey(env);
|
|
10477
|
+
if (!apiKey) {
|
|
10478
|
+
throw new Error("A Skills authority resolved but no API key did. Sign in with: skills auth login");
|
|
10479
|
+
}
|
|
10480
|
+
return new RemoteSkillsClient(apiKey, fleet.apiOrigin);
|
|
9559
10481
|
}
|
|
9560
10482
|
// src/lib/scheduler.ts
|
|
9561
|
-
import { existsSync as
|
|
10483
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
9562
10484
|
import { join as join15 } from "path";
|
|
9563
10485
|
function getSchedulesPath(targetDir = process.cwd()) {
|
|
9564
10486
|
return join15(targetDir, ".skills", "schedules.json");
|
|
9565
10487
|
}
|
|
9566
10488
|
function loadSchedules(targetDir = process.cwd()) {
|
|
9567
10489
|
const path = getSchedulesPath(targetDir);
|
|
9568
|
-
if (
|
|
10490
|
+
if (existsSync14(path)) {
|
|
9569
10491
|
try {
|
|
9570
10492
|
return JSON.parse(readFileSync13(path, "utf-8"));
|
|
9571
10493
|
} catch {}
|
|
@@ -9575,9 +10497,9 @@ function loadSchedules(targetDir = process.cwd()) {
|
|
|
9575
10497
|
function saveSchedules(data, targetDir = process.cwd()) {
|
|
9576
10498
|
const path = getSchedulesPath(targetDir);
|
|
9577
10499
|
const dir = join15(targetDir, ".skills");
|
|
9578
|
-
if (!
|
|
9579
|
-
|
|
9580
|
-
|
|
10500
|
+
if (!existsSync14(dir))
|
|
10501
|
+
mkdirSync7(dir, { recursive: true });
|
|
10502
|
+
writeFileSync7(path, JSON.stringify(data, null, 2));
|
|
9581
10503
|
}
|
|
9582
10504
|
function validateCronField(expr, min, max, label) {
|
|
9583
10505
|
for (const part of expr.split(",")) {
|
|
@@ -9762,8 +10684,8 @@ function recordScheduleRun(id, status, targetDir) {
|
|
|
9762
10684
|
saveSchedules(data, targetDir);
|
|
9763
10685
|
}
|
|
9764
10686
|
// src/lib/pull.ts
|
|
9765
|
-
import { existsSync as
|
|
9766
|
-
import { dirname as
|
|
10687
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readFileSync as readFileSync15, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
10688
|
+
import { dirname as dirname6, join as join17 } from "path";
|
|
9767
10689
|
|
|
9768
10690
|
// src/lib/revision.ts
|
|
9769
10691
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -10160,9 +11082,9 @@ class PullSkillError extends Error {
|
|
|
10160
11082
|
}
|
|
10161
11083
|
}
|
|
10162
11084
|
async function pullSkills(options = {}) {
|
|
10163
|
-
const client = options.client !== undefined ? options.client : createRemoteSkillsClient();
|
|
11085
|
+
const client = options.client !== undefined ? options.client : await createRemoteSkillsClient();
|
|
10164
11086
|
if (!client) {
|
|
10165
|
-
throw new PullSkillError("No API key configured, so there is no instance to pull from.", ["Run `skills login`, or set
|
|
11087
|
+
throw new PullSkillError("No API key configured, so there is no instance to pull from.", ["Run `skills auth login`, or set HASNA_SKILLS_API_KEY (and HASNA_SKILLS_API_URL for your own instance)."]);
|
|
10166
11088
|
}
|
|
10167
11089
|
const signingKey = options.signingKey ?? resolveSigningKey() ?? undefined;
|
|
10168
11090
|
const targets = await resolveTargetSlugs(client, options);
|
|
@@ -10215,7 +11137,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
|
|
|
10215
11137
|
}
|
|
10216
11138
|
if (bundleResponse.status === 404) {
|
|
10217
11139
|
const marker = readPullMarker(join17(getPortableSkillsRoot(corpusOptions), slug));
|
|
10218
|
-
if (marker
|
|
11140
|
+
if (isPublishedInstallMarker(marker)) {
|
|
10219
11141
|
return { name: slug, success: true, purged: true, removed: false };
|
|
10220
11142
|
}
|
|
10221
11143
|
}
|
|
@@ -10254,7 +11176,7 @@ async function pullOne(client, rawName, corpusOptions, verify) {
|
|
|
10254
11176
|
}
|
|
10255
11177
|
if (!meta?.revisionId) {
|
|
10256
11178
|
const marker = readPullMarker(join17(getPortableSkillsRoot(corpusOptions), slug));
|
|
10257
|
-
if (marker
|
|
11179
|
+
if (isPublishedInstallMarker(marker)) {
|
|
10258
11180
|
return { name: slug, success: true, purged: true, removed: false };
|
|
10259
11181
|
}
|
|
10260
11182
|
}
|
|
@@ -10311,7 +11233,7 @@ function provenRevision(meta, slug, bundle) {
|
|
|
10311
11233
|
}
|
|
10312
11234
|
function reconcileTombstone(slug, corpusOptions) {
|
|
10313
11235
|
const target = join17(getPortableSkillsRoot(corpusOptions), slug);
|
|
10314
|
-
if (!
|
|
11236
|
+
if (!existsSync15(join17(target, PULL_MARKER_FILE))) {
|
|
10315
11237
|
return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
|
|
10316
11238
|
}
|
|
10317
11239
|
rmSync4(target, { recursive: true, force: true });
|
|
@@ -10324,6 +11246,13 @@ function readPullMarker(dir) {
|
|
|
10324
11246
|
return null;
|
|
10325
11247
|
}
|
|
10326
11248
|
}
|
|
11249
|
+
function isPublishedInstallMarker(marker) {
|
|
11250
|
+
if (!marker)
|
|
11251
|
+
return false;
|
|
11252
|
+
const revisionId = typeof marker.revisionId === "string" ? marker.revisionId : "";
|
|
11253
|
+
const contentHash = typeof marker.contentHash === "string" ? marker.contentHash : "";
|
|
11254
|
+
return revisionId.length > 0 || contentHash.length > 0;
|
|
11255
|
+
}
|
|
10327
11256
|
function installVerifiedBundle(slug, response, meta, corpusOptions, verify, exact) {
|
|
10328
11257
|
return response.arrayBuffer().then((buffer) => {
|
|
10329
11258
|
const verified = verifyBundleResponseBytes(buffer, response, verify);
|
|
@@ -10446,17 +11375,17 @@ function verifyBundleResponseBytes(buffer, response, verify = {}) {
|
|
|
10446
11375
|
}
|
|
10447
11376
|
function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
10448
11377
|
const root = getPortableSkillsRoot(options);
|
|
10449
|
-
|
|
11378
|
+
mkdirSync8(root, { recursive: true });
|
|
10450
11379
|
const target = join17(root, name);
|
|
10451
|
-
const created = !
|
|
11380
|
+
const created = !existsSync15(target);
|
|
10452
11381
|
const staging = mkdtempSync3(join17(root, `.pull-${name}-`));
|
|
10453
11382
|
let moved = false;
|
|
10454
11383
|
let backup = null;
|
|
10455
11384
|
try {
|
|
10456
11385
|
for (const entry of entries) {
|
|
10457
11386
|
const destination = join17(staging, entry.path);
|
|
10458
|
-
|
|
10459
|
-
|
|
11387
|
+
mkdirSync8(dirname6(destination), { recursive: true });
|
|
11388
|
+
writeFileSync8(destination, entry.bytes, { mode: entry.mode });
|
|
10460
11389
|
}
|
|
10461
11390
|
writePullMarker(staging, {
|
|
10462
11391
|
skill: name,
|
|
@@ -10466,7 +11395,7 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
|
10466
11395
|
...marker.signature ? { signature: marker.signature } : {},
|
|
10467
11396
|
...marker.revisionId ? { revisionId: marker.revisionId } : {}
|
|
10468
11397
|
});
|
|
10469
|
-
if (
|
|
11398
|
+
if (existsSync15(target)) {
|
|
10470
11399
|
backup = mkdtempSync3(join17(root, `.pull-backup-${name}-`));
|
|
10471
11400
|
renameSync3(target, join17(backup, name));
|
|
10472
11401
|
moved = true;
|
|
@@ -10476,7 +11405,7 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
|
10476
11405
|
rmSync4(backup, { recursive: true, force: true });
|
|
10477
11406
|
} catch (error) {
|
|
10478
11407
|
rmSync4(staging, { recursive: true, force: true });
|
|
10479
|
-
if (moved && backup &&
|
|
11408
|
+
if (moved && backup && existsSync15(join17(backup, name))) {
|
|
10480
11409
|
try {
|
|
10481
11410
|
renameSync3(join17(backup, name), target);
|
|
10482
11411
|
} catch {}
|
|
@@ -10497,7 +11426,7 @@ function writePullMarker(dir, record) {
|
|
|
10497
11426
|
...record.revisionId ? { revisionId: record.revisionId } : {},
|
|
10498
11427
|
syncedAt: new Date().toISOString()
|
|
10499
11428
|
};
|
|
10500
|
-
|
|
11429
|
+
writeFileSync8(join17(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
|
|
10501
11430
|
`);
|
|
10502
11431
|
}
|
|
10503
11432
|
async function safeMeta(client, slug) {
|
|
@@ -10652,17 +11581,18 @@ function findSkillsParityForMcpTool(tool) {
|
|
|
10652
11581
|
return SKILLS_CLI_MCP_PARITY.find((entry) => entry.mcpTools.includes(tool));
|
|
10653
11582
|
}
|
|
10654
11583
|
// src/lib/registry-sync.ts
|
|
10655
|
-
import { mkdirSync as
|
|
10656
|
-
import { dirname as
|
|
11584
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
11585
|
+
import { dirname as dirname7, relative as relative4 } from "path";
|
|
10657
11586
|
// package.json
|
|
10658
11587
|
var package_default = {
|
|
10659
11588
|
name: "@hasna/skills",
|
|
10660
|
-
version: "0.
|
|
11589
|
+
version: "0.3.0",
|
|
10661
11590
|
description: "Skills library for AI coding agents",
|
|
10662
11591
|
type: "module",
|
|
10663
11592
|
bin: {
|
|
10664
11593
|
skills: "bin/index.js",
|
|
10665
11594
|
"skills-mcp": "bin/mcp.js",
|
|
11595
|
+
"skills-serve": "bin/server.js",
|
|
10666
11596
|
"skills-server": "bin/server.js",
|
|
10667
11597
|
"skills-worker": "bin/worker.js",
|
|
10668
11598
|
"skills-migrate": "bin/migrate.js"
|
|
@@ -10748,6 +11678,7 @@ var package_default = {
|
|
|
10748
11678
|
dependencies: {
|
|
10749
11679
|
"@aws-sdk/client-ecs": "^3.1079.0",
|
|
10750
11680
|
"@aws-sdk/client-s3": "^3.1079.0",
|
|
11681
|
+
"@hasna/contracts": "1.0.1",
|
|
10751
11682
|
"@hasna/events": "0.1.16",
|
|
10752
11683
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
10753
11684
|
chalk: "^5.3.0",
|
|
@@ -10833,8 +11764,8 @@ function createRegistrySyncArtifact(options = {}) {
|
|
|
10833
11764
|
};
|
|
10834
11765
|
}
|
|
10835
11766
|
function writeRegistrySyncArtifact(path, artifact) {
|
|
10836
|
-
|
|
10837
|
-
|
|
11767
|
+
mkdirSync9(dirname7(path), { recursive: true });
|
|
11768
|
+
writeFileSync9(path, `${JSON.stringify(artifact, null, 2)}
|
|
10838
11769
|
`);
|
|
10839
11770
|
}
|
|
10840
11771
|
function buildDocs(name) {
|
|
@@ -11673,17 +12604,17 @@ function clone2(value) {
|
|
|
11673
12604
|
return JSON.parse(JSON.stringify(value));
|
|
11674
12605
|
}
|
|
11675
12606
|
// src/lib/feedback.ts
|
|
11676
|
-
import { appendFileSync, existsSync as
|
|
11677
|
-
import { dirname as
|
|
12607
|
+
import { appendFileSync, existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
|
|
12608
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
11678
12609
|
import { Database } from "bun:sqlite";
|
|
11679
12610
|
function getFeedbackDbPath() {
|
|
11680
12611
|
return join18(getDataDir(), "skills.db");
|
|
11681
12612
|
}
|
|
11682
12613
|
function getFeedbackDb() {
|
|
11683
12614
|
const dbPath = getFeedbackDbPath();
|
|
11684
|
-
const dir =
|
|
11685
|
-
if (!
|
|
11686
|
-
|
|
12615
|
+
const dir = dirname8(dbPath);
|
|
12616
|
+
if (!existsSync16(dir))
|
|
12617
|
+
mkdirSync10(dir, { recursive: true });
|
|
11687
12618
|
const db = new Database(dbPath);
|
|
11688
12619
|
db.exec("PRAGMA journal_mode = WAL");
|
|
11689
12620
|
db.exec([
|
|
@@ -11710,9 +12641,9 @@ function saveFeedback(input) {
|
|
|
11710
12641
|
const category = input.category ?? "general";
|
|
11711
12642
|
if (isApiMode()) {
|
|
11712
12643
|
const path = join18(getDataDir(), "feedback.jsonl");
|
|
11713
|
-
const dir =
|
|
11714
|
-
if (!
|
|
11715
|
-
|
|
12644
|
+
const dir = dirname8(path);
|
|
12645
|
+
if (!existsSync16(dir))
|
|
12646
|
+
mkdirSync10(dir, { recursive: true });
|
|
11716
12647
|
appendFileSync(path, JSON.stringify({ message, category, email: input.email ?? null, agent: input.agent ?? null, version: input.version ?? null, createdAt: new Date().toISOString() }) + `
|
|
11717
12648
|
`);
|
|
11718
12649
|
return { saved: true, category, path };
|
|
@@ -11726,25 +12657,23 @@ function saveFeedback(input) {
|
|
|
11726
12657
|
return { saved: true, category, path: getFeedbackDbPath() };
|
|
11727
12658
|
}
|
|
11728
12659
|
function isApiMode(env = process.env) {
|
|
11729
|
-
if (env.HASNA_SKILLS_API_URL?.trim())
|
|
11730
|
-
return true;
|
|
11731
12660
|
try {
|
|
11732
|
-
return Boolean(resolveApiUrl(
|
|
12661
|
+
return Boolean(resolveApiUrl(env));
|
|
11733
12662
|
} catch {
|
|
11734
|
-
return
|
|
12663
|
+
return true;
|
|
11735
12664
|
}
|
|
11736
12665
|
}
|
|
11737
12666
|
// src/lib/native-storage.ts
|
|
11738
12667
|
import { createHash as createHash5, createHmac as createHmac2 } from "crypto";
|
|
11739
12668
|
import {
|
|
11740
|
-
existsSync as
|
|
11741
|
-
mkdirSync as
|
|
12669
|
+
existsSync as existsSync17,
|
|
12670
|
+
mkdirSync as mkdirSync11,
|
|
11742
12671
|
readFileSync as readFileSync16,
|
|
11743
12672
|
readdirSync as readdirSync10,
|
|
11744
12673
|
statSync as statSync10,
|
|
11745
|
-
writeFileSync as
|
|
12674
|
+
writeFileSync as writeFileSync10
|
|
11746
12675
|
} from "fs";
|
|
11747
|
-
import { dirname as
|
|
12676
|
+
import { dirname as dirname9, join as join19, normalize as normalize3, relative as relative5, sep as sep2 } from "path";
|
|
11748
12677
|
var SKILLS_STORAGE_TABLES = [
|
|
11749
12678
|
"skills_sync_records",
|
|
11750
12679
|
"skills_sync_cursors"
|
|
@@ -11904,7 +12833,7 @@ function getStorageStatus(options = {}) {
|
|
|
11904
12833
|
function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
11905
12834
|
const projectStateDir = getProjectStateDir(targetDir);
|
|
11906
12835
|
const files = [];
|
|
11907
|
-
if (
|
|
12836
|
+
if (existsSync17(projectStateDir)) {
|
|
11908
12837
|
for (const filePath of walkFiles2(projectStateDir)) {
|
|
11909
12838
|
const bytes = readFileSync16(filePath);
|
|
11910
12839
|
const relativePath = toPosix(relative5(targetDir, filePath));
|
|
@@ -11931,7 +12860,7 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
|
|
|
11931
12860
|
continue;
|
|
11932
12861
|
}
|
|
11933
12862
|
const absolutePath = resolveSnapshotPath(targetDir, file.path);
|
|
11934
|
-
if (
|
|
12863
|
+
if (existsSync17(absolutePath) && !options.overwrite) {
|
|
11935
12864
|
skipped += 1;
|
|
11936
12865
|
continue;
|
|
11937
12866
|
}
|
|
@@ -11940,8 +12869,8 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
|
|
|
11940
12869
|
if (hash !== file.sha256) {
|
|
11941
12870
|
throw new Error(`Snapshot file checksum mismatch: ${file.path}`);
|
|
11942
12871
|
}
|
|
11943
|
-
|
|
11944
|
-
|
|
12872
|
+
mkdirSync11(dirname9(absolutePath), { recursive: true });
|
|
12873
|
+
writeFileSync10(absolutePath, bytes);
|
|
11945
12874
|
written += 1;
|
|
11946
12875
|
}
|
|
11947
12876
|
return { written, skipped };
|
|
@@ -12319,16 +13248,16 @@ function toArrayBuffer(bytes) {
|
|
|
12319
13248
|
import { createHash as createHash6 } from "crypto";
|
|
12320
13249
|
import {
|
|
12321
13250
|
copyFileSync as copyFileSync2,
|
|
12322
|
-
mkdirSync as
|
|
13251
|
+
mkdirSync as mkdirSync12,
|
|
12323
13252
|
readFileSync as readFileSync17,
|
|
12324
13253
|
statSync as statSync12,
|
|
12325
|
-
writeFileSync as
|
|
13254
|
+
writeFileSync as writeFileSync11
|
|
12326
13255
|
} from "fs";
|
|
12327
|
-
import { dirname as
|
|
13256
|
+
import { dirname as dirname10, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve2, sep as sep4 } from "path";
|
|
12328
13257
|
|
|
12329
13258
|
// src/lib/portable-snapshot-filter.ts
|
|
12330
13259
|
import { readdirSync as readdirSync11, statSync as statSync11 } from "fs";
|
|
12331
|
-
import { homedir as
|
|
13260
|
+
import { homedir as homedir4 } from "os";
|
|
12332
13261
|
import { join as join20, sep as sep3 } from "path";
|
|
12333
13262
|
var SYNC_HOMES = [
|
|
12334
13263
|
{ name: "skills", subClass: "skills", agent: null },
|
|
@@ -12420,7 +13349,7 @@ function isPortableWithinSkill(relativeParts) {
|
|
|
12420
13349
|
return PORTABLE_SUBDIRS.has(second);
|
|
12421
13350
|
}
|
|
12422
13351
|
function homePathFor(definition, homesRoot) {
|
|
12423
|
-
const home = homesRoot ??
|
|
13352
|
+
const home = homesRoot ?? homedir4();
|
|
12424
13353
|
if (definition.subClass === "skills" || definition.subClass === "custom") {
|
|
12425
13354
|
return join20(skillsDataRootForHome(home), definition.name);
|
|
12426
13355
|
}
|
|
@@ -12590,7 +13519,7 @@ function writeStationSnapshot(options) {
|
|
|
12590
13519
|
for (const plan of plans) {
|
|
12591
13520
|
const destination = resolve2(repoRoot, plan.destination);
|
|
12592
13521
|
const destinationRelative = relative6(repoRoot, destination);
|
|
12593
|
-
if (destinationRelative.startsWith("..") || destinationRelative.startsWith(sep4) ||
|
|
13522
|
+
if (destinationRelative.startsWith("..") || destinationRelative.startsWith(sep4) || isAbsolute4(destinationRelative)) {
|
|
12594
13523
|
throw new StationSnapshotError("DESTINATION_ESCAPE", `destination escapes repo root: ${plan.destination}`);
|
|
12595
13524
|
}
|
|
12596
13525
|
let existingDigest = null;
|
|
@@ -12612,7 +13541,7 @@ function writeStationSnapshot(options) {
|
|
|
12612
13541
|
let written = 0;
|
|
12613
13542
|
for (const plan of untouched) {
|
|
12614
13543
|
const destination = resolve2(repoRoot, plan.destination);
|
|
12615
|
-
|
|
13544
|
+
mkdirSync12(dirname10(destination), { recursive: true });
|
|
12616
13545
|
copyFileSync2(plan.source.fullPath, destination);
|
|
12617
13546
|
written += 1;
|
|
12618
13547
|
}
|
|
@@ -12631,8 +13560,8 @@ function writeStationSnapshot(options) {
|
|
|
12631
13560
|
files: manifestFiles
|
|
12632
13561
|
};
|
|
12633
13562
|
const manifestPath = resolve2(repoRoot, "resources", options.stationId, "skills", "sync-manifest.json");
|
|
12634
|
-
|
|
12635
|
-
|
|
13563
|
+
mkdirSync12(dirname10(manifestPath), { recursive: true });
|
|
13564
|
+
writeFileSync11(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
12636
13565
|
`);
|
|
12637
13566
|
return {
|
|
12638
13567
|
...base,
|
|
@@ -12645,13 +13574,13 @@ function writeStationSnapshot(options) {
|
|
|
12645
13574
|
import { createHash as createHash7 } from "crypto";
|
|
12646
13575
|
import {
|
|
12647
13576
|
copyFileSync as copyFileSync3,
|
|
12648
|
-
mkdirSync as
|
|
13577
|
+
mkdirSync as mkdirSync13,
|
|
12649
13578
|
readdirSync as readdirSync12,
|
|
12650
13579
|
readFileSync as readFileSync18,
|
|
12651
13580
|
statSync as statSync13,
|
|
12652
|
-
writeFileSync as
|
|
13581
|
+
writeFileSync as writeFileSync12
|
|
12653
13582
|
} from "fs";
|
|
12654
|
-
import { dirname as
|
|
13583
|
+
import { dirname as dirname11, join as join21, resolve as resolve3, sep as sep5 } from "path";
|
|
12655
13584
|
var STATION_HYDRATION_MANIFEST_SCHEMA = "hasna.fleet-resources.skills-hydration-manifest/v1";
|
|
12656
13585
|
var STATION_HYDRATION_PRODUCER = { name: "@hasna/skills", version: package_default.version };
|
|
12657
13586
|
var MANIFEST_HASH_KEY_SEP = String.fromCharCode(0);
|
|
@@ -12907,7 +13836,7 @@ function writeStationHydration(options) {
|
|
|
12907
13836
|
}
|
|
12908
13837
|
let written = 0;
|
|
12909
13838
|
for (const entry of toWrite) {
|
|
12910
|
-
|
|
13839
|
+
mkdirSync13(dirname11(entry.destination), { recursive: true });
|
|
12911
13840
|
copyFileSync3(entry.fullPath, entry.destination);
|
|
12912
13841
|
written += 1;
|
|
12913
13842
|
}
|
|
@@ -12928,9 +13857,9 @@ function writeStationHydration(options) {
|
|
|
12928
13857
|
},
|
|
12929
13858
|
skills: resultSkills
|
|
12930
13859
|
};
|
|
12931
|
-
const hydrationManifestPath = join21(
|
|
12932
|
-
|
|
12933
|
-
|
|
13860
|
+
const hydrationManifestPath = join21(dirname11(cacheRoot), `hydration-${options.stationId}.json`);
|
|
13861
|
+
mkdirSync13(dirname11(hydrationManifestPath), { recursive: true });
|
|
13862
|
+
writeFileSync12(hydrationManifestPath, `${JSON.stringify(hydration, null, 2)}
|
|
12934
13863
|
`);
|
|
12935
13864
|
return {
|
|
12936
13865
|
...base,
|
|
@@ -12967,6 +13896,9 @@ export {
|
|
|
12967
13896
|
summarizeMcpToolContract,
|
|
12968
13897
|
storageCapabilities,
|
|
12969
13898
|
skillsPostgresSyncSchemaSql,
|
|
13899
|
+
skillsCredentialOrReason,
|
|
13900
|
+
skillsCredentialFiles,
|
|
13901
|
+
skillsCredentialFilePath,
|
|
12970
13902
|
skillExists,
|
|
12971
13903
|
signSkillsAwsV4Request,
|
|
12972
13904
|
sha256File,
|
|
@@ -12983,7 +13915,13 @@ export {
|
|
|
12983
13915
|
resolveSyncAgents,
|
|
12984
13916
|
resolveStorageConfig,
|
|
12985
13917
|
resolveSkillsNativeStorageConfig,
|
|
13918
|
+
resolveSkillsFleet,
|
|
13919
|
+
resolveSkillsApiOrigin,
|
|
13920
|
+
resolveSkillsApiKey,
|
|
12986
13921
|
resolveSkillAlias,
|
|
13922
|
+
requireSkillsFleet,
|
|
13923
|
+
requireSkillsApiOrigin,
|
|
13924
|
+
requireSkillsApiKey,
|
|
12987
13925
|
removeSkillForAgent,
|
|
12988
13926
|
removeSkill,
|
|
12989
13927
|
removeSchedule,
|
|
@@ -13005,6 +13943,8 @@ export {
|
|
|
13005
13943
|
parseSkillFrontmatter,
|
|
13006
13944
|
parseRemoteSkillPayload,
|
|
13007
13945
|
parseRemoteRegistryPayload,
|
|
13946
|
+
noticeLocalSkillsMode,
|
|
13947
|
+
normalizeSkillsApiOrigin,
|
|
13008
13948
|
normalizeSkillSlug,
|
|
13009
13949
|
normalizeRemoteSkillRunContract,
|
|
13010
13950
|
normalizePortableSkillName,
|
|
@@ -13097,6 +14037,7 @@ export {
|
|
|
13097
14037
|
createRegistrySyncArtifact,
|
|
13098
14038
|
createMcpContractManifest,
|
|
13099
14039
|
createLocalSkillManifest,
|
|
14040
|
+
configuredSkillsApiUrl,
|
|
13100
14041
|
computeContentHash,
|
|
13101
14042
|
completeSkillRun,
|
|
13102
14043
|
clearRegistryCache,
|
|
@@ -13112,6 +14053,7 @@ export {
|
|
|
13112
14053
|
StationSnapshotError,
|
|
13113
14054
|
SkillsS3ObjectStore,
|
|
13114
14055
|
SkillsPostgresSyncStore,
|
|
14056
|
+
SkillsFleetCredentialError,
|
|
13115
14057
|
SYNC_MARKER_MANAGED_BY,
|
|
13116
14058
|
SYNC_MARKER_FILE,
|
|
13117
14059
|
SYNC_HOMES,
|
|
@@ -13130,6 +14072,11 @@ export {
|
|
|
13130
14072
|
SKILLS_NATIVE_STORAGE_FALLBACK_ENV,
|
|
13131
14073
|
SKILLS_NATIVE_STORAGE_ENV,
|
|
13132
14074
|
SKILLS_CLI_MCP_PARITY,
|
|
14075
|
+
SKILLS_APP,
|
|
14076
|
+
SKILLS_API_URL_ENV_KEYS,
|
|
14077
|
+
SKILLS_API_URL_ENV,
|
|
14078
|
+
SKILLS_API_KEY_ENV_KEYS,
|
|
14079
|
+
SKILLS_API_KEY_ENV,
|
|
13133
14080
|
SKILLS,
|
|
13134
14081
|
RemoteSkillsClient,
|
|
13135
14082
|
RemoteRouteUnsupportedError,
|
|
@@ -13141,6 +14088,7 @@ export {
|
|
|
13141
14088
|
PORTABLE_SKILL_STANDARD,
|
|
13142
14089
|
PORTABLE_SKILL_SCHEMA,
|
|
13143
14090
|
PORTABLE_SKILL_DEFAULT_VERSION,
|
|
14091
|
+
MissingSkillsFleetError,
|
|
13144
14092
|
MCP_CONTRACT_SCHEMA_VERSION,
|
|
13145
14093
|
DEFAULT_EXPORT_DIR,
|
|
13146
14094
|
CATEGORIES,
|