@hasna/skills 0.9.19 → 0.10.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 +80 -22
- package/bin/index.js +5063 -3937
- package/bin/mcp.js +159 -26
- 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 +58 -24
- package/dist/lib/auth-store.d.ts +48 -4
- 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 +57 -23
- package/package.json +1 -1
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;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signing in and out, shared by `skills login` / `skills logout` / `skills
|
|
3
|
+
* whoami` and the interactive TUI's `/login`, `/logout` and `/whoami`.
|
|
4
|
+
*
|
|
5
|
+
* The flow follows the Codex CLI model the owner chose (2026-09-23): a browser
|
|
6
|
+
* sign-in by default, a printed device code for headless machines, an API key
|
|
7
|
+
* read from stdin, and a logout that revokes what the sign-in minted.
|
|
8
|
+
*
|
|
9
|
+
* Device authorization follows RFC 8628: poll no faster than the server's
|
|
10
|
+
* `interval`, add five seconds on every `slow_down`, and stop on
|
|
11
|
+
* `expired_token`, `invalid_device_code` or `access_denied`. A pending device
|
|
12
|
+
* session is saved beside the credentials file, owner-only, so a headless
|
|
13
|
+
* `skills login --device` can be finished later with `skills login --poll`
|
|
14
|
+
* instead of starting a new session and discarding the code the user approved.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here decides WHERE to sign in: callers pass the origin from
|
|
17
|
+
* `resolveSkillsSignInOrigin()`, which never sends a legacy internal key to the
|
|
18
|
+
* product default.
|
|
19
|
+
*/
|
|
20
|
+
import { type StoredCredentialOrigin, type StoredKeyIssuer } from "./auth-store.js";
|
|
21
|
+
import { type SkillsFleetOptions } from "./fleet-credentials.js";
|
|
22
|
+
type Env = Record<string, string | undefined>;
|
|
23
|
+
/** The CLI's device-authorization client label, recorded by the server. */
|
|
24
|
+
export declare const SKILLS_CLI_DEVICE_CLIENT = "skills-cli";
|
|
25
|
+
/** RFC 8628 §3.5: every slow_down adds five seconds to the polling interval. */
|
|
26
|
+
export declare const SLOW_DOWN_INCREMENT_MS = 5000;
|
|
27
|
+
export interface DeviceAuthorizationStart {
|
|
28
|
+
/** Bearer capability for the pending key. Never printed; stored owner-only. */
|
|
29
|
+
deviceCode: string;
|
|
30
|
+
userCode: string;
|
|
31
|
+
verificationUri: string;
|
|
32
|
+
verificationUriComplete?: string;
|
|
33
|
+
expiresIn: number;
|
|
34
|
+
interval: number;
|
|
35
|
+
}
|
|
36
|
+
/** What a successful sign-in returns: the key (or a session to mint one) and who signed in. */
|
|
37
|
+
export interface SignInResult {
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
token?: string;
|
|
40
|
+
user?: {
|
|
41
|
+
id?: string;
|
|
42
|
+
email?: string;
|
|
43
|
+
role?: string;
|
|
44
|
+
};
|
|
45
|
+
organization?: {
|
|
46
|
+
id?: string;
|
|
47
|
+
slug?: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
};
|
|
50
|
+
firstLogin?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export type DevicePollOutcome = {
|
|
53
|
+
status: "authorized";
|
|
54
|
+
result: SignInResult;
|
|
55
|
+
} | {
|
|
56
|
+
status: "expired";
|
|
57
|
+
} | {
|
|
58
|
+
status: "invalid";
|
|
59
|
+
} | {
|
|
60
|
+
status: "denied";
|
|
61
|
+
} | {
|
|
62
|
+
status: "timeout";
|
|
63
|
+
} | {
|
|
64
|
+
status: "cancelled";
|
|
65
|
+
};
|
|
66
|
+
/** Validate the server's device-start answer; a malformed one is refused, never guessed at. */
|
|
67
|
+
export declare function parseDeviceAuthorizationStart(body: unknown): DeviceAuthorizationStart;
|
|
68
|
+
/** Ask the instance for a device code. Sends no credential. */
|
|
69
|
+
export declare function startDeviceAuthorization(origin: string, client?: string): Promise<DeviceAuthorizationStart>;
|
|
70
|
+
/** One poll of the token endpoint. Sends only the device code. */
|
|
71
|
+
export declare function pollDeviceToken(origin: string, deviceCode: string): Promise<unknown>;
|
|
72
|
+
export interface PollDeviceAuthorizationInput {
|
|
73
|
+
/** One token-endpoint request. Resolves with the body or throws HostedApiError. */
|
|
74
|
+
poll: () => Promise<unknown>;
|
|
75
|
+
intervalSeconds: number;
|
|
76
|
+
/** Epoch milliseconds after which polling stops with `timeout`. */
|
|
77
|
+
deadline: number;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
/** Called with the new interval each time the server asks to slow down. */
|
|
80
|
+
onSlowDown?: (intervalMs: number) => void;
|
|
81
|
+
sleep?: (ms: number) => Promise<void>;
|
|
82
|
+
now?: () => number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Poll until the user approves, the code expires, or the deadline passes.
|
|
86
|
+
*
|
|
87
|
+
* Transport failures and unexpected server errors are thrown, never read as
|
|
88
|
+
* "still pending": a sign-in that cannot reach its server must say so.
|
|
89
|
+
*/
|
|
90
|
+
export declare function pollDeviceAuthorization(input: PollDeviceAuthorizationInput): Promise<DevicePollOutcome>;
|
|
91
|
+
export interface PendingDeviceSignIn {
|
|
92
|
+
origin: string;
|
|
93
|
+
deviceCode: string;
|
|
94
|
+
userCode: string;
|
|
95
|
+
verificationUri: string;
|
|
96
|
+
verificationUriComplete?: string;
|
|
97
|
+
interval: number;
|
|
98
|
+
/** Epoch milliseconds. */
|
|
99
|
+
expiresAt: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Beside the credentials file, one per profile AND instance:
|
|
103
|
+
* `device-login[-<profile>]-<origin hash>.json`. Keyed by origin so a sign-in
|
|
104
|
+
* started on one server never overwrites a pending session for another.
|
|
105
|
+
*/
|
|
106
|
+
export declare function pendingDeviceSignInPath(origin: string, env?: Env): string;
|
|
107
|
+
export declare function savePendingDeviceSignIn(origin: string, start: DeviceAuthorizationStart, env?: Env, now?: number): string;
|
|
108
|
+
/**
|
|
109
|
+
* The saved session for exactly this instance, if it has not expired. A session
|
|
110
|
+
* saved for a different instance is never returned, so its device code is only
|
|
111
|
+
* ever sent back to the server that issued it.
|
|
112
|
+
*/
|
|
113
|
+
export declare function loadPendingDeviceSignIn(origin: string, env?: Env, now?: number): PendingDeviceSignIn | null;
|
|
114
|
+
/** The instances with an unexpired pending sign-in for this profile. */
|
|
115
|
+
export declare function pendingDeviceSignInOrigins(env?: Env, now?: number): string[];
|
|
116
|
+
/** Forget the pending session for one instance, or every pending session of this profile. */
|
|
117
|
+
export declare function clearPendingDeviceSignIn(env?: Env, origin?: string): void;
|
|
118
|
+
/**
|
|
119
|
+
* Store the key a sign-in produced, bound to the instance that issued it.
|
|
120
|
+
* A session-only answer (a token, no key) mints a CLI key first.
|
|
121
|
+
*/
|
|
122
|
+
export declare function persistSignIn(result: SignInResult, origin: string, env?: Env, issuedBy?: StoredKeyIssuer): Promise<string>;
|
|
123
|
+
/**
|
|
124
|
+
* What happened to server-side revocation.
|
|
125
|
+
*
|
|
126
|
+
* revoked — the server confirmed it revoked the key.
|
|
127
|
+
* already_ended — the server no longer accepts the key (HTTP 401/403).
|
|
128
|
+
* not_requested — a user-brought key deleted without --revoke (still works).
|
|
129
|
+
* skipped — --no-revoke on a login-minted key (may still be live).
|
|
130
|
+
* unsupported — cannot be revoked: the server has no revoke route
|
|
131
|
+
* (404/405/501), the internal gateway has no login service,
|
|
132
|
+
* or an older sign-in left no record of how it was issued.
|
|
133
|
+
* not_revoked — the server answered without confirming revocation.
|
|
134
|
+
* failed — the request did not complete (network, 5xx, bad answer).
|
|
135
|
+
* none — no credential stored by `skills login` to revoke.
|
|
136
|
+
*/
|
|
137
|
+
export type RevocationOutcome = "revoked" | "already_ended" | "not_requested" | "skipped" | "unsupported" | "not_revoked" | "failed" | "none";
|
|
138
|
+
/** A stable reason code plus one plain sentence. Never contains a credential. */
|
|
139
|
+
export interface SignOutReason {
|
|
140
|
+
code: "revocation_failed" | "revocation_not_confirmed" | "revocation_unsupported" | "revocation_skipped" | "credential_not_from_login" | "credential_still_active" | "local_delete_failed";
|
|
141
|
+
message: string;
|
|
142
|
+
}
|
|
143
|
+
export interface SignOutResult {
|
|
144
|
+
/** Exit 0 when true: every reason list is empty. */
|
|
145
|
+
signedOut: boolean;
|
|
146
|
+
/** What happened to the active profile's stored credential. */
|
|
147
|
+
stored: "deleted" | "none" | "left_alone" | "delete_failed";
|
|
148
|
+
storedBy?: StoredCredentialOrigin;
|
|
149
|
+
/** The credentials file, when one holds a credential. */
|
|
150
|
+
file?: string;
|
|
151
|
+
/** The instance the stored credential belongs to. */
|
|
152
|
+
origin?: string;
|
|
153
|
+
revocation: RevocationOutcome;
|
|
154
|
+
/** Sentences for a human: what was done, in order. */
|
|
155
|
+
notes: string[];
|
|
156
|
+
/** Why the command must exit non-zero. Empty when signed out. */
|
|
157
|
+
reasons: SignOutReason[];
|
|
158
|
+
}
|
|
159
|
+
export interface SignOutOptions {
|
|
160
|
+
/**
|
|
161
|
+
* true: also revoke a user-brought or legacy key (`--revoke`).
|
|
162
|
+
* false: revoke nothing (`--no-revoke`); a login-minted key left live exits non-zero.
|
|
163
|
+
* undefined: the default — revoke only what login minted.
|
|
164
|
+
*/
|
|
165
|
+
revoke?: boolean;
|
|
166
|
+
env?: Env;
|
|
167
|
+
/** Resolution controls (a fake Keychain in tests). */
|
|
168
|
+
fleet?: SkillsFleetOptions;
|
|
169
|
+
/**
|
|
170
|
+
* A profile the ENVIRONMENT selects that differs from the active one, e.g.
|
|
171
|
+
* `HASNA_PROFILE=b skills --profile a logout`. If it still holds a credential,
|
|
172
|
+
* the next plain command uses it, so logout names it and exits non-zero.
|
|
173
|
+
*/
|
|
174
|
+
environmentProfile?: string;
|
|
175
|
+
}
|
|
176
|
+
/** Where a person revokes a key by hand. Never a composed URL. */
|
|
177
|
+
export declare function manualRevocationHint(origin: string): string;
|
|
178
|
+
export declare function signOut(options?: SignOutOptions): Promise<SignOutResult>;
|
|
179
|
+
export type SignedInAccount = {
|
|
180
|
+
signedIn: false;
|
|
181
|
+
reason: string;
|
|
182
|
+
} | {
|
|
183
|
+
signedIn: true;
|
|
184
|
+
apiOrigin: string;
|
|
185
|
+
source: string;
|
|
186
|
+
email?: string;
|
|
187
|
+
organization?: string;
|
|
188
|
+
error?: string;
|
|
189
|
+
};
|
|
190
|
+
/** The account in effect, for a compact display (the TUI's `/whoami`). */
|
|
191
|
+
export declare function readSignedInAccount(env?: Env): Promise<SignedInAccount>;
|
|
192
|
+
/**
|
|
193
|
+
* The verification page to open, or null when it must only be printed.
|
|
194
|
+
*
|
|
195
|
+
* The URL comes from the server's answer, so it is opened only when it is an
|
|
196
|
+
* https URL (http only on loopback) with no userinfo, on the sign-in origin
|
|
197
|
+
* itself or on `auth.<sign-in host>` — where rule global-product-auth-url-layout
|
|
198
|
+
* will move the identity endpoints. Anything else is printed for the user to
|
|
199
|
+
* judge and never handed to the operating system.
|
|
200
|
+
*/
|
|
201
|
+
export declare function verificationUrlToOpen(raw: string, signInOrigin: string): string | null;
|
|
202
|
+
/**
|
|
203
|
+
* The opener argv for a platform. Never a shell: on Windows `cmd /c start`
|
|
204
|
+
* would re-parse `&`, `|` and `^` in a server-provided URL, so the URL goes to
|
|
205
|
+
* the URL protocol handler as one argument instead.
|
|
206
|
+
*/
|
|
207
|
+
export declare function browserCommand(url: string, platform?: NodeJS.Platform): string[];
|
|
208
|
+
/**
|
|
209
|
+
* Best effort: the URL and code are always printed too, so a refusal or a
|
|
210
|
+
* failure here costs nothing. Returns whether a browser was asked to open it.
|
|
211
|
+
*/
|
|
212
|
+
export declare function openVerificationPage(url: string, signInOrigin: string): boolean;
|
|
213
|
+
export {};
|
|
@@ -69,6 +69,32 @@ export declare const VENDOR_HOST_URL_EXCEPTIONS: readonly {
|
|
|
69
69
|
url: string;
|
|
70
70
|
reason: string;
|
|
71
71
|
}[];
|
|
72
|
+
/**
|
|
73
|
+
* The product default: the one vendor URL an UNCONFIGURED install may name.
|
|
74
|
+
*
|
|
75
|
+
* Owner rulings 2026-09-23 (Todos PLA8-00366) made the product the default place
|
|
76
|
+
* to SIGN IN, superseding R1 for that one path: with no URL and no credential,
|
|
77
|
+
* `skills login` targets this origin. Every data surface still fails closed with
|
|
78
|
+
* no URL at all, and a credential that already resolves keeps its own instance
|
|
79
|
+
* (see unconfigured-client-boundary.test.ts and skills-md-default.test.ts).
|
|
80
|
+
*
|
|
81
|
+
* Unlike {@link VENDOR_HOST_URL_EXCEPTIONS}, this exception is scoped to FILES as
|
|
82
|
+
* well as to the exact URL. The origin is declared once, in `src/lib/product-default.ts`;
|
|
83
|
+
* it may appear in the two bundles built from the CLI and MCP entry points that
|
|
84
|
+
* import it, and in the README that documents it. The same URL anywhere else —
|
|
85
|
+
* a server default, a skill, a split or folded literal — is still a finding, so
|
|
86
|
+
* a second copy cannot quietly become another default. The URL is not repeated
|
|
87
|
+
* in this file: it is the imported constant.
|
|
88
|
+
*/
|
|
89
|
+
export declare const PRODUCT_DEFAULT_URL_EXCEPTION: {
|
|
90
|
+
url: string;
|
|
91
|
+
files: readonly string[];
|
|
92
|
+
reason: string;
|
|
93
|
+
};
|
|
94
|
+
/** Is this exact URL excepted in this exact file? Global exceptions, then the scoped product default. */
|
|
95
|
+
export declare function isExceptedVendorUrl(url: string, file: string): boolean;
|
|
96
|
+
/** Every excepted URL for one file, for the value-independent token scan to strip. */
|
|
97
|
+
export declare function exceptedVendorUrlsFor(file: string | undefined): string[];
|
|
72
98
|
/** Reduce a hostname to its registrable domain (approximate eTLD+1). */
|
|
73
99
|
export declare function registrableDomain(host: string): string;
|
|
74
100
|
export declare function isLoopbackHost(host: string): boolean;
|
|
@@ -65,7 +65,7 @@ export interface VendorHostFinding {
|
|
|
65
65
|
/**
|
|
66
66
|
* Find known vendor domains even when they do not appear in a complete URL.
|
|
67
67
|
*/
|
|
68
|
-
export declare function findVendorDomainTokens(text: string): {
|
|
68
|
+
export declare function findVendorDomainTokens(text: string, file?: string): {
|
|
69
69
|
domain: string;
|
|
70
70
|
index: number;
|
|
71
71
|
}[];
|
package/dist/sdk/index.js
CHANGED
|
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
|
|
|
32373
32373
|
// package.json
|
|
32374
32374
|
var package_default = {
|
|
32375
32375
|
name: "@hasna/skills",
|
|
32376
|
-
version: "0.
|
|
32376
|
+
version: "0.10.0",
|
|
32377
32377
|
description: "Skills library for AI coding agents",
|
|
32378
32378
|
type: "module",
|
|
32379
32379
|
bin: {
|
|
@@ -33505,13 +33505,15 @@ var SKILLS_API_URL_ENV_KEYS = ENV_KEYS.apiUrlKeys;
|
|
|
33505
33505
|
var SKILLS_API_KEY_ENV_KEYS = ENV_KEYS.apiKeyKeys;
|
|
33506
33506
|
var SKILLS_API_URL_ENV = SKILLS_API_URL_ENV_KEYS[0];
|
|
33507
33507
|
var SKILLS_API_KEY_ENV = SKILLS_API_KEY_ENV_KEYS[0];
|
|
33508
|
-
|
|
33509
33508
|
class SkillsFleetCredentialError extends Error {
|
|
33510
33509
|
code;
|
|
33511
|
-
|
|
33510
|
+
next;
|
|
33511
|
+
constructor(message, code = "MISSING_API_CREDENTIAL", next) {
|
|
33512
33512
|
super(message);
|
|
33513
33513
|
this.name = "SkillsFleetCredentialError";
|
|
33514
33514
|
this.code = code;
|
|
33515
|
+
if (next)
|
|
33516
|
+
this.next = next;
|
|
33515
33517
|
}
|
|
33516
33518
|
}
|
|
33517
33519
|
function isCredentialResolutionError(error) {
|
|
@@ -33569,7 +33571,7 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
|
33569
33571
|
if (!entry.value.trim() || /[\x00-\x1f\x7f]/.test(entry.value))
|
|
33570
33572
|
throw new SkillsFleetCredentialError(`${entry.key} is blank or contains control characters`, "INVALID_API_URL");
|
|
33571
33573
|
}
|
|
33572
|
-
const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key }));
|
|
33574
|
+
const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key, trust: "environment" }));
|
|
33573
33575
|
if (new Set(normalized.map((entry) => entry.value)).size > 1)
|
|
33574
33576
|
throw new SkillsFleetCredentialError("Skills API URL aliases disagree", "INVALID_API_URL");
|
|
33575
33577
|
if (normalized[0])
|
|
@@ -33578,19 +33580,19 @@ function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
|
33578
33580
|
for (const file of skillsProfileCredentialFiles(env, profile)) {
|
|
33579
33581
|
const metadata = readSkillsInstanceMetadata(file);
|
|
33580
33582
|
if (metadata.apiUrl || metadata.binding)
|
|
33581
|
-
return { value: metadata.apiUrl ?? metadata.binding, source: file };
|
|
33583
|
+
return { value: metadata.apiUrl ?? metadata.binding, source: file, trust: "file" };
|
|
33582
33584
|
}
|
|
33583
|
-
return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default" };
|
|
33585
|
+
return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default", trust: "default" };
|
|
33584
33586
|
}
|
|
33585
33587
|
const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
|
|
33586
33588
|
if (fromKeychain)
|
|
33587
|
-
return { value: fromKeychain.value.trim(), source: fromKeychain.source };
|
|
33589
|
+
return { value: fromKeychain.value.trim(), source: fromKeychain.source, trust: "keychain" };
|
|
33588
33590
|
const fromDisk = appConfigDiskValue(SKILLS_APP, env, SKILLS_API_URL_ENV_KEYS);
|
|
33589
33591
|
if (fromDisk?.unusable) {
|
|
33590
33592
|
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");
|
|
33591
33593
|
}
|
|
33592
33594
|
if (fromDisk)
|
|
33593
|
-
return { value: fromDisk.value.trim(), source: fromDisk.path };
|
|
33595
|
+
return { value: fromDisk.value.trim(), source: fromDisk.path, trust: "file" };
|
|
33594
33596
|
return null;
|
|
33595
33597
|
}
|
|
33596
33598
|
function skillsCredentialFiles(env = process.env) {
|
|
@@ -33653,13 +33655,13 @@ function resolveSkillsFleetOrThrow(env, options) {
|
|
|
33653
33655
|
const credential = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
33654
33656
|
if (!credential) {
|
|
33655
33657
|
if (!configured) {
|
|
33656
|
-
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
|
|
33658
|
+
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`);
|
|
33657
33659
|
}
|
|
33658
|
-
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
|
|
33660
|
+
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`);
|
|
33659
33661
|
}
|
|
33660
33662
|
const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
|
|
33661
33663
|
toV1BaseUrl(apiOrigin);
|
|
33662
|
-
|
|
33664
|
+
assertCredentialInstancePairing(credential, configured, apiOrigin, env, options);
|
|
33663
33665
|
assertFilesUnchanged();
|
|
33664
33666
|
const base = {
|
|
33665
33667
|
mode: "hosted",
|
|
@@ -33673,7 +33675,7 @@ function resolveSkillsFleetOrThrow(env, options) {
|
|
|
33673
33675
|
return { ...base, apiKey: null, apiKeyPointer: credential };
|
|
33674
33676
|
}
|
|
33675
33677
|
if (!credential.apiKey.trim()) {
|
|
33676
|
-
throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills
|
|
33678
|
+
throw new SkillsFleetCredentialError(`The Skills API key from ${credential.source} is empty \u2014 refusing to send an unauthenticated request. ` + `Sign in with: skills login`);
|
|
33677
33679
|
}
|
|
33678
33680
|
return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
|
|
33679
33681
|
}
|
|
@@ -33681,17 +33683,49 @@ function credentialLocations(env) {
|
|
|
33681
33683
|
const files = skillsCredentialFiles(env).join(" or ") || "no credentials file (HOME is unset)";
|
|
33682
33684
|
return `hasna.credentials.${SKILLS_APP}.api-key (macOS Keychain, account HASNA_STATION or the host name), ${files}, and ${SKILLS_API_KEY_ENV}`;
|
|
33683
33685
|
}
|
|
33684
|
-
function
|
|
33685
|
-
|
|
33686
|
-
|
|
33687
|
-
|
|
33688
|
-
|
|
33689
|
-
|
|
33690
|
-
|
|
33686
|
+
function credentialPairing(credential, env, options = {}) {
|
|
33687
|
+
const fileBinding = (file) => {
|
|
33688
|
+
const metadata = readSkillsInstanceMetadata(file);
|
|
33689
|
+
return { kind: "bound", instance: normalizeSkillsApiOrigin(metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP)) };
|
|
33690
|
+
};
|
|
33691
|
+
const tier = credential.tier;
|
|
33692
|
+
switch (tier) {
|
|
33693
|
+
case "disk":
|
|
33694
|
+
case "profile":
|
|
33695
|
+
return fileBinding(credential.source);
|
|
33696
|
+
case "pointer":
|
|
33697
|
+
return credential.diskCandidates.includes(credential.source) ? fileBinding(credential.source) : { kind: "unbound" };
|
|
33698
|
+
case "keychain":
|
|
33699
|
+
return {
|
|
33700
|
+
kind: "bound",
|
|
33701
|
+
instance: normalizeSkillsApiOrigin(keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP))
|
|
33702
|
+
};
|
|
33703
|
+
case "env":
|
|
33704
|
+
case "override":
|
|
33705
|
+
case "argument":
|
|
33706
|
+
return { kind: "unbound" };
|
|
33707
|
+
default: {
|
|
33708
|
+
const unclassified = tier;
|
|
33709
|
+
throw new SkillsFleetCredentialError(`The Skills credential from ${credential.source} has an unclassified tier (${String(unclassified)}); refusing to send it.`, "INSTANCE_CREDENTIAL_MISMATCH");
|
|
33710
|
+
}
|
|
33691
33711
|
}
|
|
33692
|
-
|
|
33712
|
+
}
|
|
33713
|
+
function assertCredentialInstancePairing(credential, configured, apiOrigin, env, options) {
|
|
33714
|
+
const pairing = credentialPairing(credential, env, options);
|
|
33715
|
+
if (pairing.kind === "bound") {
|
|
33716
|
+
if (pairing.instance === apiOrigin)
|
|
33717
|
+
return;
|
|
33693
33718
|
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");
|
|
33694
33719
|
}
|
|
33720
|
+
if (apiOrigin === normalizeSkillsApiOrigin(defaultFleetGatewayBaseUrl(SKILLS_APP)))
|
|
33721
|
+
return;
|
|
33722
|
+
if (configured?.trust === "environment")
|
|
33723
|
+
return;
|
|
33724
|
+
const next = [
|
|
33725
|
+
`set ${SKILLS_API_URL_ENV} alongside ${credential.source}`,
|
|
33726
|
+
`unset ${credential.source}, then run: skills login`
|
|
33727
|
+
];
|
|
33728
|
+
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);
|
|
33695
33729
|
}
|
|
33696
33730
|
async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
33697
33731
|
return (await resolveSkillsConnection(env, options))?.apiKey ?? null;
|
|
@@ -33708,7 +33742,7 @@ async function resolveSkillsConnection(env = process.env, options = {}) {
|
|
|
33708
33742
|
return { ...fleet, apiKey: fleet.apiKey };
|
|
33709
33743
|
const pointer = fleet.apiKeyPointer;
|
|
33710
33744
|
if (!pointer) {
|
|
33711
|
-
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills
|
|
33745
|
+
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills login`);
|
|
33712
33746
|
}
|
|
33713
33747
|
let completed;
|
|
33714
33748
|
try {
|
|
@@ -33775,7 +33809,7 @@ function requireSkillsFleet(action = "This command", env = process.env, options
|
|
|
33775
33809
|
class MissingSkillsFleetError extends Error {
|
|
33776
33810
|
code = "MISSING_API_URL";
|
|
33777
33811
|
constructor(action = "This command") {
|
|
33778
|
-
super(`${action} requires a Skills API credential and none is configured \u2014 ` + `run: skills
|
|
33812
|
+
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>`);
|
|
33779
33813
|
this.name = "MissingSkillsFleetError";
|
|
33780
33814
|
}
|
|
33781
33815
|
}
|
|
@@ -90557,7 +90591,7 @@ class RemoteSkillsAuthClient {
|
|
|
90557
90591
|
return (await this.sessionClient(email4, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
|
|
90558
90592
|
}
|
|
90559
90593
|
request(path2, options) {
|
|
90560
|
-
if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path2))
|
|
90594
|
+
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(path2))
|
|
90561
90595
|
throw new Error("Unsupported authentication operation");
|
|
90562
90596
|
return requestAuthApi(this.apiOrigin, path2, options);
|
|
90563
90597
|
}
|