@hasna/skills 0.9.20 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -22
- package/bin/index.js +5081 -3944
- package/bin/mcp.js +175 -33
- package/bin/migrate.js +1 -1
- package/bin/server.js +5 -3
- package/bin/worker.js +1 -1
- package/dist/cli/commands/auth.d.ts +2 -0
- package/dist/cli/components/AccountView.d.ts +16 -0
- package/dist/cli/components/App.d.ts +18 -1
- package/dist/cli/components/SearchView.d.ts +3 -1
- package/dist/cli/profile-selection.d.ts +3 -0
- package/dist/index.js +76 -31
- package/dist/lib/auth-store.d.ts +48 -4
- package/dist/lib/cli-credential-profile.d.ts +2 -0
- package/dist/lib/fleet-credentials.d.ts +87 -3
- package/dist/lib/instance-credentials.d.ts +6 -0
- package/dist/lib/product-default.d.ts +13 -0
- package/dist/lib/sign-in.d.ts +213 -0
- package/dist/lib/vendor-host-policy.d.ts +26 -0
- package/dist/lib/vendor-host-url.d.ts +1 -1
- package/dist/sdk/index.js +75 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9611,7 +9611,7 @@ import { isAbsolute as isAbsolute11, join as join23, resolve as resolve11 } from
|
|
|
9611
9611
|
// package.json
|
|
9612
9612
|
var package_default = {
|
|
9613
9613
|
name: "@hasna/skills",
|
|
9614
|
-
version: "0.
|
|
9614
|
+
version: "0.10.1",
|
|
9615
9615
|
description: "Skills library for AI coding agents",
|
|
9616
9616
|
type: "module",
|
|
9617
9617
|
bin: {
|
|
@@ -10569,9 +10569,17 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
|
10569
10569
|
|
|
10570
10570
|
// src/lib/instance-credentials.ts
|
|
10571
10571
|
import { closeSync as closeSync5, constants as constants2, fstatSync as fstatSync5, lstatSync as lstatSync8, openSync as openSync5, readSync as readSync2 } from "fs";
|
|
10572
|
+
|
|
10573
|
+
// src/lib/cli-credential-profile.ts
|
|
10574
|
+
var cliCredentialProfile;
|
|
10575
|
+
function selectedCliSkillsCredentialProfile() {
|
|
10576
|
+
return cliCredentialProfile;
|
|
10577
|
+
}
|
|
10578
|
+
|
|
10579
|
+
// src/lib/instance-credentials.ts
|
|
10572
10580
|
var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
|
|
10573
10581
|
function selectedSkillsProfile(env, explicit) {
|
|
10574
|
-
const selected = explicit ?? env.HASNA_PROFILE;
|
|
10582
|
+
const selected = explicit ?? selectedCliSkillsCredentialProfile() ?? env.HASNA_PROFILE;
|
|
10575
10583
|
if (selected === undefined)
|
|
10576
10584
|
return null;
|
|
10577
10585
|
const profile = selected.trim();
|
|
@@ -10684,13 +10692,15 @@ var SKILLS_API_URL_ENV_KEYS = ENV_KEYS.apiUrlKeys;
|
|
|
10684
10692
|
var SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
|
|
10685
10693
|
var SKILLS_API_URL_ENV = SKILLS_API_URL_ENV_KEYS[0];
|
|
10686
10694
|
var SKILLS_API_KEY_ENV = SKILLS_API_KEY_ENV_KEYS[0];
|
|
10687
|
-
|
|
10688
10695
|
class SkillsFleetCredentialError extends Error {
|
|
10689
10696
|
code;
|
|
10690
|
-
|
|
10697
|
+
next;
|
|
10698
|
+
constructor(message, code = "MISSING_API_CREDENTIAL", next) {
|
|
10691
10699
|
super(message);
|
|
10692
10700
|
this.name = "SkillsFleetCredentialError";
|
|
10693
10701
|
this.code = code;
|
|
10702
|
+
if (next)
|
|
10703
|
+
this.next = next;
|
|
10694
10704
|
}
|
|
10695
10705
|
}
|
|
10696
10706
|
function isCredentialResolutionError(error) {
|
|
@@ -10748,7 +10758,7 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
|
10748
10758
|
if (!entry.value.trim() || /[\x00-\x1f\x7f]/.test(entry.value))
|
|
10749
10759
|
throw new SkillsFleetCredentialError(`${entry.key} is blank or contains control characters`, "INVALID_API_URL");
|
|
10750
10760
|
}
|
|
10751
|
-
const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key }));
|
|
10761
|
+
const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key, trust: "environment" }));
|
|
10752
10762
|
if (new Set(normalized.map((entry) => entry.value)).size > 1)
|
|
10753
10763
|
throw new SkillsFleetCredentialError("Skills API URL aliases disagree", "INVALID_API_URL");
|
|
10754
10764
|
if (normalized[0])
|
|
@@ -10757,19 +10767,19 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
|
10757
10767
|
for (const file of skillsProfileCredentialFiles(env, profile)) {
|
|
10758
10768
|
const metadata = readSkillsInstanceMetadata(file);
|
|
10759
10769
|
if (metadata.apiUrl || metadata.binding)
|
|
10760
|
-
return { value: metadata.apiUrl ?? metadata.binding, source: file };
|
|
10770
|
+
return { value: metadata.apiUrl ?? metadata.binding, source: file, trust: "file" };
|
|
10761
10771
|
}
|
|
10762
|
-
return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default" };
|
|
10772
|
+
return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default", trust: "default" };
|
|
10763
10773
|
}
|
|
10764
10774
|
const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
|
|
10765
10775
|
if (fromKeychain)
|
|
10766
|
-
return { value: fromKeychain.value.trim(), source: fromKeychain.source };
|
|
10776
|
+
return { value: fromKeychain.value.trim(), source: fromKeychain.source, trust: "keychain" };
|
|
10767
10777
|
const fromDisk = appConfigDiskValue(SKILLS_APP, env, SKILLS_API_URL_ENV_KEYS);
|
|
10768
10778
|
if (fromDisk?.unusable) {
|
|
10769
10779
|
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");
|
|
10770
10780
|
}
|
|
10771
10781
|
if (fromDisk)
|
|
10772
|
-
return { value: fromDisk.value.trim(), source: fromDisk.path };
|
|
10782
|
+
return { value: fromDisk.value.trim(), source: fromDisk.path, trust: "file" };
|
|
10773
10783
|
return null;
|
|
10774
10784
|
}
|
|
10775
10785
|
function skillsCredentialFiles(env = process.env) {
|
|
@@ -10817,11 +10827,13 @@ function snapshotSkillsEnvironment(env) {
|
|
|
10817
10827
|
return Object.freeze(snapshot);
|
|
10818
10828
|
}
|
|
10819
10829
|
function snapshotSkillsOptions(env, options) {
|
|
10830
|
+
const profile = options.credentials?.profile ?? selectedCliSkillsCredentialProfile();
|
|
10831
|
+
const credentials = profile === undefined ? options.credentials : { ...options.credentials, profile };
|
|
10820
10832
|
if (env !== process.env)
|
|
10821
|
-
return options;
|
|
10822
|
-
return { ...options, credentials: { ...
|
|
10823
|
-
...
|
|
10824
|
-
enabled:
|
|
10833
|
+
return credentials === options.credentials ? options : { ...options, credentials };
|
|
10834
|
+
return { ...options, credentials: { ...credentials, keychain: {
|
|
10835
|
+
...credentials?.keychain,
|
|
10836
|
+
enabled: credentials?.keychain?.enabled ?? true
|
|
10825
10837
|
} } };
|
|
10826
10838
|
}
|
|
10827
10839
|
function resolveSkillsFleetOrThrow(env, options) {
|
|
@@ -10832,13 +10844,13 @@ function resolveSkillsFleetOrThrow(env, options) {
|
|
|
10832
10844
|
const credential = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
10833
10845
|
if (!credential) {
|
|
10834
10846
|
if (!configured) {
|
|
10835
|
-
throw new SkillsFleetCredentialError(`No API key resolved and no Skills API URL is configured \u2014 failing closed ` + `(local mode is opt-in only: set ${SKILLS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 to run on this machine). ` + `Looked in ${credentialLocations(env)}. Sign in with: skills
|
|
10847
|
+
throw new SkillsFleetCredentialError(`No API key resolved and no Skills API URL is configured \u2014 failing closed ` + `(local mode is opt-in only: set ${SKILLS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 to run on this machine). ` + `Looked in ${credentialLocations(env)}. Sign in with: skills login`);
|
|
10836
10848
|
}
|
|
10837
|
-
throw new SkillsFleetCredentialError(`${configured.source} points this CLI at a Skills service but no API key resolved \u2014 refusing to run locally instead. ` + `Looked in ${credentialLocations(env)}. Sign in with: skills
|
|
10849
|
+
throw new SkillsFleetCredentialError(`${configured.source} points this CLI at a Skills service but no API key resolved \u2014 refusing to run locally instead. ` + `Looked in ${credentialLocations(env)}. Sign in with: skills login`);
|
|
10838
10850
|
}
|
|
10839
10851
|
const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
|
|
10840
10852
|
toV1BaseUrl(apiOrigin);
|
|
10841
|
-
|
|
10853
|
+
assertCredentialInstancePairing(credential, configured, apiOrigin, env, options);
|
|
10842
10854
|
assertFilesUnchanged();
|
|
10843
10855
|
const base = {
|
|
10844
10856
|
mode: "hosted",
|
|
@@ -10852,7 +10864,7 @@ function resolveSkillsFleetOrThrow(env, options) {
|
|
|
10852
10864
|
return { ...base, apiKey: null, apiKeyPointer: credential };
|
|
10853
10865
|
}
|
|
10854
10866
|
if (!credential.apiKey.trim()) {
|
|
10855
|
-
throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills
|
|
10867
|
+
throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills login`);
|
|
10856
10868
|
}
|
|
10857
10869
|
return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
|
|
10858
10870
|
}
|
|
@@ -10860,25 +10872,58 @@ function credentialLocations(env) {
|
|
|
10860
10872
|
const files = skillsCredentialFiles(env).join(" or ") || "no credentials file (HOME is unset)";
|
|
10861
10873
|
return `hasna.credentials.${SKILLS_APP}.api-key (macOS Keychain, account HASNA_STATION or the host name), ${files}, and ${SKILLS_API_KEY_ENV}`;
|
|
10862
10874
|
}
|
|
10863
|
-
function
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10875
|
+
function credentialPairing(credential, env, options = {}) {
|
|
10876
|
+
const fileBinding = (file) => {
|
|
10877
|
+
const metadata = readSkillsInstanceMetadata(file);
|
|
10878
|
+
return { kind: "bound", instance: normalizeSkillsApiOrigin(metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP)) };
|
|
10879
|
+
};
|
|
10880
|
+
const tier = credential.tier;
|
|
10881
|
+
switch (tier) {
|
|
10882
|
+
case "disk":
|
|
10883
|
+
case "profile":
|
|
10884
|
+
return fileBinding(credential.source);
|
|
10885
|
+
case "pointer":
|
|
10886
|
+
return credential.diskCandidates.includes(credential.source) ? fileBinding(credential.source) : { kind: "unbound" };
|
|
10887
|
+
case "keychain":
|
|
10888
|
+
return {
|
|
10889
|
+
kind: "bound",
|
|
10890
|
+
instance: normalizeSkillsApiOrigin(keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP))
|
|
10891
|
+
};
|
|
10892
|
+
case "env":
|
|
10893
|
+
case "override":
|
|
10894
|
+
case "argument":
|
|
10895
|
+
return { kind: "unbound" };
|
|
10896
|
+
default: {
|
|
10897
|
+
const unclassified = tier;
|
|
10898
|
+
throw new SkillsFleetCredentialError(`The Skills credential from ${credential.source} has an unclassified tier (${String(unclassified)}); refusing to send it.`, "INSTANCE_CREDENTIAL_MISMATCH");
|
|
10899
|
+
}
|
|
10870
10900
|
}
|
|
10871
|
-
|
|
10901
|
+
}
|
|
10902
|
+
function assertCredentialInstancePairing(credential, configured, apiOrigin, env, options) {
|
|
10903
|
+
const pairing = credentialPairing(credential, env, options);
|
|
10904
|
+
if (pairing.kind === "bound") {
|
|
10905
|
+
if (pairing.instance === apiOrigin)
|
|
10906
|
+
return;
|
|
10872
10907
|
throw new SkillsFleetCredentialError("The selected Skills API does not match this credential's instance. Select its profile or sign in to the new instance; no credential was sent.", "INSTANCE_CREDENTIAL_MISMATCH");
|
|
10873
10908
|
}
|
|
10909
|
+
if (apiOrigin === normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP)))
|
|
10910
|
+
return;
|
|
10911
|
+
if (configured?.trust === "environment")
|
|
10912
|
+
return;
|
|
10913
|
+
const next = [
|
|
10914
|
+
`set ${SKILLS_API_URL_ENV} alongside ${credential.source}`,
|
|
10915
|
+
`unset ${credential.source}, then run: skills login`
|
|
10916
|
+
];
|
|
10917
|
+
throw new SkillsFleetCredentialError(`${credential.source} records no Skills server of its own, so it is only sent to the internal gateway or to a URL ` + `from the environment (${SKILLS_API_URL_ENV}); the Skills URL here comes from ${configured?.source ?? "nowhere"}. ` + `No credential was sent. To use your own server, ${next[0]}; otherwise ${next[1]}.`, "INSTANCE_CREDENTIAL_MISMATCH", next);
|
|
10874
10918
|
}
|
|
10875
10919
|
async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
10876
10920
|
return (await resolveSkillsConnection(env, options))?.apiKey ?? null;
|
|
10877
10921
|
}
|
|
10878
10922
|
async function resolveSkillsConnection(env = process.env, options = {}) {
|
|
10879
10923
|
const snapshotEnv = snapshotSkillsEnvironment(env);
|
|
10880
|
-
const
|
|
10881
|
-
const
|
|
10924
|
+
const resolvedOptions = snapshotSkillsOptions(env, options);
|
|
10925
|
+
const assertFilesUnchanged = selectsSkillsLocalMode(snapshotEnv) ? () => {} : captureSkillsCredentialFiles(skillsProfileCredentialFiles(snapshotEnv, resolvedOptions.credentials?.profile));
|
|
10926
|
+
const fleet = resolveSkillsFleet(snapshotEnv, resolvedOptions);
|
|
10882
10927
|
if (fleet.mode === "local" && env === process.env)
|
|
10883
10928
|
noticeLocalSkillsMode();
|
|
10884
10929
|
if (fleet.mode !== "hosted")
|
|
@@ -10887,7 +10932,7 @@ async function resolveSkillsConnection(env = process.env, options = {}) {
|
|
|
10887
10932
|
return { ...fleet, apiKey: fleet.apiKey };
|
|
10888
10933
|
const pointer = fleet.apiKeyPointer;
|
|
10889
10934
|
if (!pointer) {
|
|
10890
|
-
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills
|
|
10935
|
+
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills login`);
|
|
10891
10936
|
}
|
|
10892
10937
|
let completed;
|
|
10893
10938
|
try {
|
|
@@ -10954,7 +10999,7 @@ function requireSkillsFleet(action = "This command", env = process.env, options
|
|
|
10954
10999
|
class MissingSkillsFleetError extends Error {
|
|
10955
11000
|
code = "MISSING_API_URL";
|
|
10956
11001
|
constructor(action = "This command") {
|
|
10957
|
-
super(`${action} requires a Skills API credential and none is configured \u2014 ` + `run: skills
|
|
11002
|
+
super(`${action} requires a Skills API credential and none is configured \u2014 ` + `run: skills 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>`);
|
|
10958
11003
|
this.name = "MissingSkillsFleetError";
|
|
10959
11004
|
}
|
|
10960
11005
|
}
|
|
@@ -36498,7 +36543,7 @@ function date4(params) {
|
|
|
36498
36543
|
}
|
|
36499
36544
|
// src/lib/api-url.ts
|
|
36500
36545
|
var API_URL_ENV_VAR = SKILLS_API_URL_ENV;
|
|
36501
|
-
var MISSING_API_URL_HINT = `run: skills
|
|
36546
|
+
var MISSING_API_URL_HINT = `run: skills login, or set ${API_URL_ENV_VAR}=<your Skills instance origin>, ` + `or run: skills setup --api-url <your Skills instance origin>`;
|
|
36502
36547
|
function resolveApiUrl(env = process.env, options = {}) {
|
|
36503
36548
|
const fleet = resolveSkillsFleet(env, options);
|
|
36504
36549
|
return fleet.mode === "hosted" ? fleet.apiOrigin : undefined;
|
|
@@ -40659,7 +40704,7 @@ class RemoteSkillsAuthClient {
|
|
|
40659
40704
|
return (await this.sessionClient(email4, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
|
|
40660
40705
|
}
|
|
40661
40706
|
request(path, options) {
|
|
40662
|
-
if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path))
|
|
40707
|
+
if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami", "/api/auth/logout"].includes(path))
|
|
40663
40708
|
throw new Error("Unsupported authentication operation");
|
|
40664
40709
|
return requestAuthApi(this.apiOrigin, path, options);
|
|
40665
40710
|
}
|
package/dist/lib/auth-store.d.ts
CHANGED
|
@@ -82,12 +82,56 @@ export declare function getAuthConfig(env?: Env, options?: SkillsFleetOptions):
|
|
|
82
82
|
/** Alias kept for the read-only callers; resolution never writes. */
|
|
83
83
|
export declare function getAuthConfigReadOnly(env?: Env, options?: SkillsFleetOptions): AuthConfig | null;
|
|
84
84
|
/**
|
|
85
|
-
*
|
|
85
|
+
* How the stored key was obtained. `sign-in` keys were minted for this CLI by a
|
|
86
|
+
* browser/device or email sign-in, so `skills logout` may revoke them on the
|
|
87
|
+
* server; an `api-key` the user pasted is theirs to manage and is only
|
|
88
|
+
* forgotten locally. Recorded in identity.json (never a secret).
|
|
89
|
+
*/
|
|
90
|
+
export type StoredKeyIssuer = "sign-in" | "api-key";
|
|
91
|
+
export declare function saveAuthConfig(config: StoredAuthConfig, env?: Env, authenticatedOrigin?: string, issuedBy?: StoredKeyIssuer): string;
|
|
92
|
+
/**
|
|
93
|
+
* Who put the active profile's stored credential there — the question
|
|
94
|
+
* `skills logout` has to answer before it touches it (Instructions rule
|
|
95
|
+
* global-cli-logout-semantics, points 1 and 2).
|
|
96
|
+
*
|
|
97
|
+
* sign-in — minted for this CLI by `skills login` (browser, device code,
|
|
98
|
+
* email code) or workspace enrollment. Revoked on logout by default.
|
|
99
|
+
* api-key — brought by the user with `skills login --api-key`. Deleted
|
|
100
|
+
* locally on logout; revoked only with an explicit `--revoke`.
|
|
101
|
+
* legacy — stored by an older `skills auth login` that did not record how
|
|
102
|
+
* the key was issued, so it cannot be revoked automatically.
|
|
103
|
+
* external — no sign-in record at all: a provisioned key, a vault pointer, or
|
|
104
|
+
* one written by another tool. Logout leaves it alone.
|
|
105
|
+
*/
|
|
106
|
+
export type StoredCredentialOrigin = StoredKeyIssuer | "legacy" | "external";
|
|
107
|
+
/** The active profile's stored credential, as `skills logout` sees it. The key is never printed. */
|
|
108
|
+
export interface StoredCredential {
|
|
109
|
+
/** The credentials file. A path: safe to print. */
|
|
110
|
+
file: string;
|
|
111
|
+
/** The stored key, or null when the file holds only a vault pointer. Never printed or logged. */
|
|
112
|
+
apiKey: string | null;
|
|
113
|
+
/** The instance the credential belongs to: its recorded binding, else the file URL, else the internal gateway. */
|
|
114
|
+
origin: string;
|
|
115
|
+
storedBy: StoredCredentialOrigin;
|
|
116
|
+
/** True when the file's URL line is there only because `skills login` wrote it; logout then removes it too. */
|
|
117
|
+
urlWrittenByLogin: boolean;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read the active profile's stored credential WITHOUT the resolution ladder:
|
|
121
|
+
* logout must act on exactly the credential login stored for this profile,
|
|
122
|
+
* never on one the environment or the Keychain would supply instead.
|
|
123
|
+
*/
|
|
124
|
+
export declare function readStoredCredential(env?: Env): StoredCredential | null;
|
|
125
|
+
/**
|
|
126
|
+
* Delete the stored credential for the active profile, and throw when it cannot
|
|
127
|
+
* be deleted: the key, its binding, any vault pointer and the display identity.
|
|
86
128
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
129
|
+
* `removeUrl` also deletes the URL line, for a URL that `skills login` wrote
|
|
130
|
+
* (the product default or a `--url` given only to login). Leaving it would let
|
|
131
|
+
* a later environment-only key pair with a server nobody configured. A URL the
|
|
132
|
+
* user configured (`skills setup --api-url`) stays.
|
|
89
133
|
*/
|
|
90
|
-
export declare function
|
|
134
|
+
export declare function deleteStoredCredential(env?: Env, removeUrl?: boolean): string;
|
|
91
135
|
/** Store (or clear, with null) the API URL beside the credential. */
|
|
92
136
|
export declare function saveApiUrl(apiUrl: string | null, env?: Env): string;
|
|
93
137
|
/** The API URL recorded in the credentials file, or null. */
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
*
|
|
27
27
|
* URL: `HASNA_SKILLS_API_URL` → the Keychain `api-url` item → the credentials
|
|
28
28
|
* file → the fleet gateway `https://api.hasna.com/skills`. The gateway default
|
|
29
|
-
* applies ONLY once a credential has resolved, so
|
|
30
|
-
*
|
|
29
|
+
* applies ONLY once a credential has resolved, so a DATA request from an install
|
|
30
|
+
* with no credential names no host at all (the R1 boundary in
|
|
31
|
+
* vendor-host-policy.ts). Signing in is the one exception: with no URL and no
|
|
32
|
+
* credential, `resolveSkillsSignInOrigin` returns the product default
|
|
33
|
+
* (product-default.ts; owner rulings 2026-09-23).
|
|
31
34
|
*
|
|
32
35
|
* The unprefixed `SKILLS_API_URL` / `SKILLS_API_KEY` spellings are still
|
|
33
36
|
* accepted, silently, because the shared seam accepts `<APP>_API_URL` /
|
|
@@ -81,6 +84,13 @@ export interface SkillsFleetOptions {
|
|
|
81
84
|
/** Tier-1 credential inputs and the Keychain-tier controls (a fake runner in tests). */
|
|
82
85
|
credentials?: CredentialChainOptions;
|
|
83
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Every tier the shared resolver can return, in its precedence order. The
|
|
89
|
+
* pairing invariant (assertCredentialInstancePairing) classifies each one, and
|
|
90
|
+
* the pairing matrix test derives its rows from this list; the type below fails
|
|
91
|
+
* to compile if the resolver gains a tier this list does not name.
|
|
92
|
+
*/
|
|
93
|
+
export declare const SKILLS_CREDENTIAL_TIERS: readonly ["argument", "override", "pointer", "profile", "keychain", "disk", "env"];
|
|
84
94
|
/** A hosted resolution: an authority to call and a credential to call it with. */
|
|
85
95
|
export interface HostedSkillsFleet {
|
|
86
96
|
mode: "hosted";
|
|
@@ -137,7 +147,9 @@ export type SkillsFleetErrorCode = "MISSING_API_CREDENTIAL" | "INVALID_API_URL"
|
|
|
137
147
|
*/
|
|
138
148
|
export declare class SkillsFleetCredentialError extends Error {
|
|
139
149
|
readonly code: SkillsFleetErrorCode;
|
|
140
|
-
|
|
150
|
+
/** Machine-readable next steps, the same ones the message names. Never a value. */
|
|
151
|
+
readonly next?: readonly string[];
|
|
152
|
+
constructor(message: string, code?: SkillsFleetErrorCode, next?: readonly string[]);
|
|
141
153
|
}
|
|
142
154
|
/**
|
|
143
155
|
* True for this package's own refusal, across bundle boundaries.
|
|
@@ -183,6 +195,12 @@ export interface ConfiguredSkillsApiUrl {
|
|
|
183
195
|
value: string;
|
|
184
196
|
/** An env key NAME, a `keychain:<service>@<account>` reference, or an absolute path. */
|
|
185
197
|
source: string;
|
|
198
|
+
/**
|
|
199
|
+
* Which trust source configured it: the process environment, the Keychain,
|
|
200
|
+
* a credentials file (default or profile), or nothing (the gateway fallback
|
|
201
|
+
* of a selected profile). The pairing invariant reads this, never the name.
|
|
202
|
+
*/
|
|
203
|
+
trust: "environment" | "keychain" | "file" | "default";
|
|
186
204
|
}
|
|
187
205
|
/**
|
|
188
206
|
* The authority an operator configured, in the shared seam's precedence order —
|
|
@@ -222,6 +240,35 @@ export declare function resetLocalSkillsModeNotice(): void;
|
|
|
222
240
|
* configured authority to local mode.
|
|
223
241
|
*/
|
|
224
242
|
export declare function resolveSkillsFleet(env?: Env, options?: SkillsFleetOptions): SkillsFleet;
|
|
243
|
+
/**
|
|
244
|
+
* Where a resolved credential may be sent: THE pairing invariant, classified
|
|
245
|
+
* once for every tier in {@link SKILLS_CREDENTIAL_TIERS} (the switch below is
|
|
246
|
+
* exhaustive, and an unknown tier is refused rather than guessed).
|
|
247
|
+
*
|
|
248
|
+
* bound — a stored credential that records its instance. The credentials
|
|
249
|
+
* file key (`disk`), a profile file key (`profile`), a vault pointer
|
|
250
|
+
* stored in a credentials file (`pointer` from a file) and the
|
|
251
|
+
* Keychain key (`keychain`, bound to the Keychain `api-url` beside
|
|
252
|
+
* it). It is sent only to that instance. A stored credential that
|
|
253
|
+
* recorded none is a legacy internal key: the internal gateway.
|
|
254
|
+
* unbound — a credential that records no instance: `HASNA_SKILLS_API_KEY` /
|
|
255
|
+
* `SKILLS_API_KEY` (`env`), `HASNA_SKILLS_API_KEY_OVERRIDE`
|
|
256
|
+
* (`override`), an environment `HASNA_SKILLS_API_KEY_REF` (`pointer`
|
|
257
|
+
* from the environment) and an explicit argument (`argument`). It is
|
|
258
|
+
* sent only to the internal gateway or to a URL from the environment
|
|
259
|
+
* — never to a URL from a credentials file or the Keychain, which may
|
|
260
|
+
* have been written for a different credential (a `skills login`, a
|
|
261
|
+
* `skills setup`, a station provisioner).
|
|
262
|
+
*
|
|
263
|
+
* So a URL that `skills login` wrote binds only the key login stored beside it.
|
|
264
|
+
*/
|
|
265
|
+
export type CredentialPairing = {
|
|
266
|
+
kind: "bound";
|
|
267
|
+
instance: string;
|
|
268
|
+
} | {
|
|
269
|
+
kind: "unbound";
|
|
270
|
+
};
|
|
271
|
+
export declare function credentialPairing(credential: ResolvedCredential, env: Env, options?: SkillsFleetOptions): CredentialPairing;
|
|
225
272
|
/**
|
|
226
273
|
* The usable API key for this process, completing a vault pointer if that is
|
|
227
274
|
* the tier that won.
|
|
@@ -282,6 +329,43 @@ export declare function resolveSkillsApiOrigin(env?: Env, options?: SkillsFleetO
|
|
|
282
329
|
} | null;
|
|
283
330
|
/** The authority for an auth flow, or throw naming what is missing. */
|
|
284
331
|
export declare function requireSkillsApiOrigin(action?: string, env?: Env, options?: SkillsFleetOptions): string;
|
|
332
|
+
/** Where a sign-in goes, and what decided it. */
|
|
333
|
+
export interface SignInTarget {
|
|
334
|
+
origin: string;
|
|
335
|
+
/** "--url", a configured URL's source, or "default". */
|
|
336
|
+
source: string;
|
|
337
|
+
/** Set when an already-resolving credential decided the origin: its source NAME, never a value. */
|
|
338
|
+
credentialSource?: string;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The refusal for a sign-in that would target the internal gateway, which has
|
|
342
|
+
* no sign-in service — or null. It names what to unset or remove, and never
|
|
343
|
+
* suggests pointing this machine at the product server while an internal
|
|
344
|
+
* credential is still configured. Nothing is sent either way.
|
|
345
|
+
*/
|
|
346
|
+
export declare function gatewaySignInRefusal(target: SignInTarget): SkillsFleetCredentialError | null;
|
|
347
|
+
/**
|
|
348
|
+
* The authority a SIGN-IN talks to: `skills login`, `skills auth login` /
|
|
349
|
+
* `signup`, and the TUI `/login`. Resolved on its own because signing in is how
|
|
350
|
+
* a credential is obtained, so it cannot require one.
|
|
351
|
+
*
|
|
352
|
+
* In order:
|
|
353
|
+
*
|
|
354
|
+
* 1. `--url <origin>` — an explicit choice for this sign-in. It is refused when
|
|
355
|
+
* an outranking URL (the environment or the Keychain) names a different
|
|
356
|
+
* instance, because the key it mints would be shadowed the moment it is
|
|
357
|
+
* saved. A URL in the credentials file is what a sign-in replaces.
|
|
358
|
+
* 2. a configured URL — environment, profile or credentials file, Keychain.
|
|
359
|
+
* 3. the instance an already-stored credential belongs to. A legacy internal
|
|
360
|
+
* key with no recorded URL belongs to the fleet gateway, so a machine that
|
|
361
|
+
* holds one keeps signing in there: its key is never sent to the product
|
|
362
|
+
* default, and the product default is never chosen for it silently.
|
|
363
|
+
* 4. the product default ({@link SKILLS_PRODUCT_DEFAULT_ORIGIN}) — only when
|
|
364
|
+
* no URL and no credential resolve anywhere (owner rulings 2026-09-23).
|
|
365
|
+
*
|
|
366
|
+
* Nothing here sends a request.
|
|
367
|
+
*/
|
|
368
|
+
export declare function resolveSkillsSignInOrigin(env?: Env, options?: SkillsFleetOptions, explicitUrl?: string): SignInTarget;
|
|
285
369
|
/**
|
|
286
370
|
* The hosted resolution, or throw. Use on every auth and write path.
|
|
287
371
|
*
|
|
@@ -10,4 +10,10 @@ export declare function readSkillsInstanceMetadata(file: string): {
|
|
|
10
10
|
apiUrl?: string;
|
|
11
11
|
binding?: string;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Whether a credentials file names a key or a vault pointer. Reads key NAMES
|
|
15
|
+
* only, never a value; a missing file has none. Used to tell a brand-new named
|
|
16
|
+
* profile (nothing stored yet, so it may sign in) from a broken selection.
|
|
17
|
+
*/
|
|
18
|
+
export declare function skillsFileHasCredential(file: string): boolean;
|
|
13
19
|
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where `skills login` signs in when no URL and no credential are configured.
|
|
3
|
+
*
|
|
4
|
+
* It is not a data default: with no credential every data surface still fails
|
|
5
|
+
* closed and sends nothing. It is not a fallback for a credential that already
|
|
6
|
+
* resolves either: a legacy internal key with no bound URL keeps its internal
|
|
7
|
+
* gateway, for data and for signing in, and is never sent here. A completed
|
|
8
|
+
* sign-in records this origin beside the key it minted. Select another server
|
|
9
|
+
* with `skills login --url <origin>` or HASNA_SKILLS_API_URL.
|
|
10
|
+
*
|
|
11
|
+
* Typed as `string` so the emitted declaration carries no literal host.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SKILLS_PRODUCT_DEFAULT_ORIGIN: string;
|