@oh-hai/cli 0.1.0-beta.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 +154 -0
- package/dist/auth/file-backend.d.ts +16 -0
- package/dist/auth/file-backend.js +98 -0
- package/dist/auth/file-backend.js.map +1 -0
- package/dist/auth/keychain.d.ts +54 -0
- package/dist/auth/keychain.js +232 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/resolve-token.d.ts +34 -0
- package/dist/auth/resolve-token.js +91 -0
- package/dist/auth/resolve-token.js.map +1 -0
- package/dist/auth/secure-write.d.ts +2 -0
- package/dist/auth/secure-write.js +30 -0
- package/dist/auth/secure-write.js.map +1 -0
- package/dist/auth/token-store.d.ts +104 -0
- package/dist/auth/token-store.js +208 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +238 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +370 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/ask.d.ts +2 -0
- package/dist/commands/ask.js +246 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/context.d.ts +72 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +237 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/flags.d.ts +25 -0
- package/dist/commands/flags.js +100 -0
- package/dist/commands/flags.js.map +1 -0
- package/dist/commands/handlers.d.ts +2 -0
- package/dist/commands/handlers.js +26 -0
- package/dist/commands/handlers.js.map +1 -0
- package/dist/commands/http.d.ts +8 -0
- package/dist/commands/http.js +19 -0
- package/dist/commands/http.js.map +1 -0
- package/dist/commands/inbox.d.ts +2 -0
- package/dist/commands/inbox.js +111 -0
- package/dist/commands/inbox.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +272 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +35 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/messaging/await.d.ts +43 -0
- package/dist/commands/messaging/await.js +125 -0
- package/dist/commands/messaging/await.js.map +1 -0
- package/dist/commands/messaging/build.d.ts +46 -0
- package/dist/commands/messaging/build.js +66 -0
- package/dist/commands/messaging/build.js.map +1 -0
- package/dist/commands/messaging/http.d.ts +22 -0
- package/dist/commands/messaging/http.js +270 -0
- package/dist/commands/messaging/http.js.map +1 -0
- package/dist/commands/messaging/identity.d.ts +29 -0
- package/dist/commands/messaging/identity.js +63 -0
- package/dist/commands/messaging/identity.js.map +1 -0
- package/dist/commands/messaging/shared.d.ts +53 -0
- package/dist/commands/messaging/shared.js +135 -0
- package/dist/commands/messaging/shared.js.map +1 -0
- package/dist/commands/messaging/state.d.ts +26 -0
- package/dist/commands/messaging/state.js +82 -0
- package/dist/commands/messaging/state.js.map +1 -0
- package/dist/commands/messaging/validate.d.ts +40 -0
- package/dist/commands/messaging/validate.js +193 -0
- package/dist/commands/messaging/validate.js.map +1 -0
- package/dist/commands/messaging/wire.d.ts +133 -0
- package/dist/commands/messaging/wire.js +16 -0
- package/dist/commands/messaging/wire.js.map +1 -0
- package/dist/commands/notify.d.ts +2 -0
- package/dist/commands/notify.js +68 -0
- package/dist/commands/notify.js.map +1 -0
- package/dist/commands/registry.d.ts +14 -0
- package/dist/commands/registry.js +144 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/stub.d.ts +1 -0
- package/dist/commands/stub.js +9 -0
- package/dist/commands/stub.js.map +1 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +223 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.js +90 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config-file.d.ts +38 -0
- package/dist/config-file.js +233 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +97 -0
- package/dist/config.js.map +1 -0
- package/dist/envelope.d.ts +25 -0
- package/dist/envelope.js +41 -0
- package/dist/envelope.js.map +1 -0
- package/dist/exit-codes.d.ts +51 -0
- package/dist/exit-codes.js +57 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/help.d.ts +1 -0
- package/dist/help.js +17 -0
- package/dist/help.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.js +18 -0
- package/dist/terminal.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Token resolution (docs/specs/cli.md §6) — the layer that composes the pure config precedence
|
|
2
|
+
// (config.ts, which knows only the `MA2H_AGENT_TOKEN` env token) with the local credential
|
|
3
|
+
// stores. Kept OUT of `resolveConfig` so that function stays sync + pure + unit-testable; the
|
|
4
|
+
// keychain/file READ (I/O) lives here behind the injectable `CredentialStore`.
|
|
5
|
+
//
|
|
6
|
+
// token := MA2H_AGENT_TOKEN (env; CI path — overrides local stores, §5.3)
|
|
7
|
+
// || store[<origin>|<agent id>] (the active backend: keychain, else 0600 file)
|
|
8
|
+
// || none
|
|
9
|
+
//
|
|
10
|
+
// The token is NEVER read from a config-file value (§5.4). A local lookup requires a resolved
|
|
11
|
+
// agent id (the store is keyed by `<origin>|<agent id>`); without one there is nothing to look
|
|
12
|
+
// up, so the source is `none`.
|
|
13
|
+
//
|
|
14
|
+
// The store layers the keychain over the `0600` file (token-store.ts), so `getWithSource`
|
|
15
|
+
// realizes §6's `keychain || file` as a real dual-read and reports which backend held the
|
|
16
|
+
// token. The store is optional here so `whoami` can skip opening it (and probing the OS
|
|
17
|
+
// keychain) when the CI env token is present — resolution short-circuits before it's needed.
|
|
18
|
+
/** Derive the Hub origin (scheme+host+port) from a resolved base URL, for Hub-scoped keying
|
|
19
|
+
* (§5.1). Falls back to the raw string if the URL can't be parsed (resolveConfig guarantees a
|
|
20
|
+
* valid default, so this is belt-and-suspenders). */
|
|
21
|
+
export function originOf(baseUrl) {
|
|
22
|
+
try {
|
|
23
|
+
return new URL(baseUrl).origin;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return baseUrl;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Resolve the effective bearer token and where it came from, per §6. `store` may be omitted
|
|
30
|
+
* when the caller already knows resolution won't touch it (an env token, or no account). */
|
|
31
|
+
export async function resolveToken(config, store) {
|
|
32
|
+
// The env token (CI path, §5.3) wins over local stores — resolveConfig already captured it.
|
|
33
|
+
if (config.tokenSource === "env") {
|
|
34
|
+
return { token: config.token, source: "env" };
|
|
35
|
+
}
|
|
36
|
+
// A local lookup needs an agent id (the stores are keyed by `<origin>|<agent id>`) and a store.
|
|
37
|
+
if (config.account === undefined || store === undefined) {
|
|
38
|
+
return { token: undefined, source: "none" };
|
|
39
|
+
}
|
|
40
|
+
const found = await store.getWithSource(originOf(config.baseUrl), config.account);
|
|
41
|
+
return found !== undefined ? { token: found.token, source: found.source } : { token: undefined, source: "none" };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the effective identity (§6): the account plus its bearer. Extends `resolveToken` with a
|
|
45
|
+
* zero-config fallback for the device-code login path, where the bearer is stored under a
|
|
46
|
+
* Hub-chosen agent id but no `--account`/`MA2H_AGENT_ID`/config account is set. When no account is
|
|
47
|
+
* configured and the token isn't the CI env credential, fall back to the SOLE stored identity for
|
|
48
|
+
* the current Hub origin — so a fresh `oh-hai login` then `oh-hai whoami` resolves without a manual
|
|
49
|
+
* `--account`. Ambiguity is never guessed: with zero or more-than-one stored identity for the
|
|
50
|
+
* origin, the account stays undefined and the user selects with `--account` (§6). The env path is
|
|
51
|
+
* never discovered — the env credential is authoritative and its agent id may differ from a stored
|
|
52
|
+
* login. Discovery keys on the origin (derived from the flag/env/user-config `base_url`, never a
|
|
53
|
+
* repo-controlled value), so it preserves §5.1 isolation and can't leak a bearer to a chosen Hub.
|
|
54
|
+
*/
|
|
55
|
+
export async function resolveIdentity(config, store) {
|
|
56
|
+
// The env token (CI path, §5.3) is authoritative — carry the configured account (possibly
|
|
57
|
+
// undefined) and never consult the store, so `whoami` can keep skipping it for an env token.
|
|
58
|
+
if (config.tokenSource === "env") {
|
|
59
|
+
return { account: config.account, token: config.token, source: "env" };
|
|
60
|
+
}
|
|
61
|
+
// An explicitly configured account wins — resolve its token directly (no discovery).
|
|
62
|
+
if (config.account !== undefined) {
|
|
63
|
+
const { token, source } = await resolveToken(config, store);
|
|
64
|
+
return { account: config.account, token, source };
|
|
65
|
+
}
|
|
66
|
+
// No account and no env token: discover the sole stored identity for this Hub origin.
|
|
67
|
+
if (store === undefined) {
|
|
68
|
+
return { account: undefined, token: undefined, source: "none" };
|
|
69
|
+
}
|
|
70
|
+
const origin = originOf(config.baseUrl);
|
|
71
|
+
const listed = (await store.list()).filter((identity) => identity.origin === origin);
|
|
72
|
+
// Resolve each listed identity to its bearer and keep only the ones that actually resolve.
|
|
73
|
+
// `list()` reads a keys-only index (redaction-safe: identities, never tokens), which can drift
|
|
74
|
+
// from the backend — a key manually removed from the keychain, or left behind by a failed delete.
|
|
75
|
+
// A listed key is a REAL identity only if its token still resolves, so we count RESOLVABLE
|
|
76
|
+
// identities, not listed keys: otherwise a stale index entry would make a sole real identity look
|
|
77
|
+
// ambiguous and break the zero-config path. (The per-identity read is required, not a missed
|
|
78
|
+
// optimization, precisely because `list()` withholds tokens.)
|
|
79
|
+
const resolved = [];
|
|
80
|
+
for (const identity of listed) {
|
|
81
|
+
const found = await store.getWithSource(origin, identity.agentId);
|
|
82
|
+
if (found !== undefined) {
|
|
83
|
+
resolved.push({ account: identity.agentId, token: found.token, source: found.source });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Exactly one resolvable identity is unambiguous; zero → nothing to resolve, many → ambiguous and
|
|
87
|
+
// requires --account (§6). Either non-unique case resolves to none.
|
|
88
|
+
const only = resolved.length === 1 ? resolved[0] : undefined;
|
|
89
|
+
return only ?? { account: undefined, token: undefined, source: "none" };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=resolve-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-token.js","sourceRoot":"","sources":["../../src/auth/resolve-token.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Atomic, permission-tight file writes for the credential store (docs/specs/cli.md §5.2).
|
|
2
|
+
// Both the file backend (the credentials map) and the keychain backend (the keys index) write
|
|
3
|
+
// secret-adjacent data to disk; this is their single source of truth for doing it safely.
|
|
4
|
+
//
|
|
5
|
+
// Write to a sibling temp file created `0600` from the first byte, then `rename()` over the
|
|
6
|
+
// target. Two properties fall out: (1) there is no window where the secret sits at looser
|
|
7
|
+
// permissions (the old write-then-chmod left a sub-millisecond TOCTOU gap on a pre-existing
|
|
8
|
+
// world-readable file), and (2) a crash mid-write never leaves a truncated/partial file — the
|
|
9
|
+
// rename is atomic, so a reader sees either the whole old file or the whole new one.
|
|
10
|
+
//
|
|
11
|
+
// This makes each individual write atomic and crash-safe. It does NOT lock against a *separate
|
|
12
|
+
// process* doing its own read-modify-write concurrently — two `oh-hai` invocations racing the
|
|
13
|
+
// same credentials file can still last-writer-win. The file store is single-user by design
|
|
14
|
+
// (the keychain is the real multi-writer store); a lockfile would be the fix if that changes.
|
|
15
|
+
import { chmodSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname } from "node:path";
|
|
17
|
+
// Per-process counter so two writes from the same process never collide on the temp name.
|
|
18
|
+
let tmpSeq = 0;
|
|
19
|
+
/** Atomically write `content` to `path` at mode `0600`, creating the parent dir at `0700`. */
|
|
20
|
+
export function writeSecureFile(path, content) {
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
22
|
+
const tmp = `${path}.tmp.${process.pid}.${tmpSeq++}`;
|
|
23
|
+
// `mode` on writeFileSync applies on create (tmp is always new); chmod re-asserts exactly
|
|
24
|
+
// 0600 in case a restrictive umask narrowed it further — the token must not be group/world
|
|
25
|
+
// readable, and the temp file must be tight before the rename exposes it at `path`.
|
|
26
|
+
writeFileSync(tmp, content, { mode: 0o600 });
|
|
27
|
+
chmodSync(tmp, 0o600);
|
|
28
|
+
renameSync(tmp, path);
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=secure-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-write.js","sourceRoot":"","sources":["../../src/auth/secure-write.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,8FAA8F;AAC9F,0FAA0F;AAC1F,EAAE;AACF,4FAA4F;AAC5F,0FAA0F;AAC1F,4FAA4F;AAC5F,8FAA8F;AAC9F,qFAAqF;AACrF,EAAE;AACF,+FAA+F;AAC/F,8FAA8F;AAC9F,2FAA2F;AAC3F,8FAA8F;AAE9F,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,0FAA0F;AAC1F,IAAI,MAAM,GAAG,CAAC,CAAC;AAEf,8FAA8F;AAC9F,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,OAAe;IAC3D,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3D,MAAM,GAAG,GAAG,GAAG,IAAI,QAAQ,OAAO,CAAC,GAAG,IAAI,MAAM,EAAE,EAAE,CAAC;IACrD,0FAA0F;IAC1F,2FAA2F;IAC3F,oFAAoF;IACpF,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7C,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtB,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type KeychainRunner } from "./keychain.js";
|
|
2
|
+
/** A pluggable secret backend. Both the keychain and file backends implement this; the store
|
|
3
|
+
* is backend-agnostic. Ops are keyed by the already-built `<origin>|<agent id>` string. */
|
|
4
|
+
export interface SecretBackend {
|
|
5
|
+
readonly name: "keychain" | "file";
|
|
6
|
+
get(key: string): Promise<string | undefined>;
|
|
7
|
+
set(key: string, token: string): Promise<void>;
|
|
8
|
+
/** Returns true when an entry was removed, false when the key was already absent. */
|
|
9
|
+
delete(key: string): Promise<boolean>;
|
|
10
|
+
/** Enumerate stored keys (`<origin>|<agent id>`) — never token values. */
|
|
11
|
+
listKeys(): Promise<string[]>;
|
|
12
|
+
/** Remove every entry; returns how many were removed. */
|
|
13
|
+
clearAll(): Promise<number>;
|
|
14
|
+
}
|
|
15
|
+
/** A stored identity, as returned by `list()`. `source` is the active backend. */
|
|
16
|
+
export interface StoredIdentity {
|
|
17
|
+
origin: string;
|
|
18
|
+
agentId: string;
|
|
19
|
+
source: "keychain" | "file";
|
|
20
|
+
}
|
|
21
|
+
/** The loud one-time warning shown when the store falls back to the on-disk file (§5.2). Goes
|
|
22
|
+
* to stderr only, so it never pollutes the `--json` stdout envelope (§8). */
|
|
23
|
+
export declare const FILE_FALLBACK_WARNING: string;
|
|
24
|
+
/** Build the Hub-scoped storage key from an origin and agent id (§5.1). */
|
|
25
|
+
export declare function keyOf(origin: string, agentId: string): string;
|
|
26
|
+
/** Split a storage key back into `{ origin, agentId }`. Origins (scheme+host+port) never
|
|
27
|
+
* contain `|`, so splitting on the FIRST `|` is unambiguous even if an agent id contained one. */
|
|
28
|
+
export declare function parseKey(key: string): {
|
|
29
|
+
origin: string;
|
|
30
|
+
agentId: string;
|
|
31
|
+
};
|
|
32
|
+
/** The keys-only index file the keychain backend uses for enumeration — a sibling of the
|
|
33
|
+
* credentials file (`~/.config/oh-hai/keys.json`), honoring the same XDG resolution. */
|
|
34
|
+
export declare function keychainIndexPath(env?: NodeJS.ProcessEnv): string;
|
|
35
|
+
interface StoreOptions {
|
|
36
|
+
fellBack?: boolean;
|
|
37
|
+
warn?: (msg: string) => void;
|
|
38
|
+
}
|
|
39
|
+
/** A token plus which backend held it (§6 source reporting). */
|
|
40
|
+
export interface FoundToken {
|
|
41
|
+
token: string;
|
|
42
|
+
source: "keychain" | "file";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A layered credential store over one or more backends. Writes go to the PRIMARY backend
|
|
46
|
+
* (keychain when available, else the file fallback); reads, deletes, list, and clearAll span
|
|
47
|
+
* EVERY backend. That dual-read/dual-scrub matters for §6's `keychain || file` chain and for
|
|
48
|
+
* security: a token written to the `0600` file while the keychain was down must still be
|
|
49
|
+
* resolved, and — critically — must still be *removed* by `logout`/`logout --all` once the
|
|
50
|
+
* keychain is back (a single-backend store would leave the unencrypted copy behind). A given
|
|
51
|
+
* key only ever lives in ONE backend (set writes to the primary), so scanning all backends
|
|
52
|
+
* never double-counts or duplicates.
|
|
53
|
+
*/
|
|
54
|
+
export declare class CredentialStore {
|
|
55
|
+
/** The PRIMARY (write) backend's name — surfaced so `login` can report `storage`. */
|
|
56
|
+
readonly backendName: "keychain" | "file";
|
|
57
|
+
private readonly backends;
|
|
58
|
+
private readonly primary;
|
|
59
|
+
private readonly fellBack;
|
|
60
|
+
private readonly warn;
|
|
61
|
+
private warned;
|
|
62
|
+
constructor(backend: SecretBackend | SecretBackend[], options?: StoreOptions);
|
|
63
|
+
/** Emit the on-disk warning exactly once per store, only when we fell back to the file. */
|
|
64
|
+
private maybeWarn;
|
|
65
|
+
get(origin: string, agentId: string): Promise<string | undefined>;
|
|
66
|
+
/** Read from the first backend (primary before fallback) that holds the token, reporting
|
|
67
|
+
* which one it was — so `whoami`/resolution can show the real source (§6). */
|
|
68
|
+
getWithSource(origin: string, agentId: string): Promise<FoundToken | undefined>;
|
|
69
|
+
set(origin: string, agentId: string, token: string): Promise<void>;
|
|
70
|
+
/** Delete from EVERY backend so a file-fallback copy is scrubbed even when the keychain is
|
|
71
|
+
* primary. A backend error doesn't stop the others (the file must always be scrubbed); the
|
|
72
|
+
* first error is surfaced after all backends have been attempted. */
|
|
73
|
+
delete(origin: string, agentId: string): Promise<boolean>;
|
|
74
|
+
/** Union of identities across all backends (a key lives in exactly one, so no dedup needed —
|
|
75
|
+
* the guard is belt-and-suspenders). */
|
|
76
|
+
list(): Promise<StoredIdentity[]>;
|
|
77
|
+
/** Clear EVERY backend (so `logout --all` scrubs the file fallback too). As with delete, a
|
|
78
|
+
* backend error doesn't skip the others; the first error is surfaced afterward. */
|
|
79
|
+
clearAll(): Promise<number>;
|
|
80
|
+
}
|
|
81
|
+
export interface OpenStoreOptions {
|
|
82
|
+
/** Override the credentials file path (tests). */
|
|
83
|
+
filePath?: string;
|
|
84
|
+
/** Override the keychain key-index path (tests). */
|
|
85
|
+
indexPath?: string;
|
|
86
|
+
/** Inject the keychain runner (tests use a fake so no real keychain is touched). */
|
|
87
|
+
keychainRunner?: KeychainRunner;
|
|
88
|
+
/** Force a specific backend, bypassing detection (tests). */
|
|
89
|
+
backend?: SecretBackend;
|
|
90
|
+
/** Sink for the one-time fallback warning (tests capture it). */
|
|
91
|
+
warn?: (msg: string) => void;
|
|
92
|
+
/** Bound on the keychain availability probe (defaults to KEYCHAIN_PROBE_TIMEOUT_MS). */
|
|
93
|
+
probeTimeoutMs?: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Open a credential store. The keychain (when available) is the PRIMARY backend with the file
|
|
97
|
+
* fallback layered underneath — so a token written to the file during a past keychain outage is
|
|
98
|
+
* still read and, on `logout`, scrubbed (§6). When the keychain is unavailable/locked, the file
|
|
99
|
+
* is the sole backend and the one-time on-disk warning is armed. Backend selection is the only
|
|
100
|
+
* async step: a side-effect-free `get` of a sentinel key, bounded by a timeout so a hung/locked
|
|
101
|
+
* keychain can't hang the CLI — the abandoned probe child is cancelled on timeout.
|
|
102
|
+
*/
|
|
103
|
+
export declare function openStore(options?: OpenStoreOptions): Promise<CredentialStore>;
|
|
104
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// The Hub-scoped credential store (docs/specs/cli.md §5). One façade over two backends — the
|
|
2
|
+
// OS keychain (keychain.ts) and the `0600` file fallback (file-backend.ts) — selected at
|
|
3
|
+
// runtime: keychain when the platform tool is available, else the file with a LOUD ONE-TIME
|
|
4
|
+
// warning that the token is on disk unencrypted (§5.2).
|
|
5
|
+
//
|
|
6
|
+
// Every credential is keyed Hub-scoped by `<origin>|<agent id>` (§5.1) so the same agent id on
|
|
7
|
+
// two Hubs (local dev + production) never collides or cross-sends its bearer. Callers pass
|
|
8
|
+
// `(origin, agentId)`; the store builds the key. Enumeration (`list`) is redaction-safe — it
|
|
9
|
+
// returns identities, never tokens.
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { FileBackend, credentialsPath } from "./file-backend.js";
|
|
12
|
+
import { KeychainBackend, systemKeychainRunner } from "./keychain.js";
|
|
13
|
+
/** The loud one-time warning shown when the store falls back to the on-disk file (§5.2). Goes
|
|
14
|
+
* to stderr only, so it never pollutes the `--json` stdout envelope (§8). */
|
|
15
|
+
export const FILE_FALLBACK_WARNING = "oh-hai: the OS keychain is unavailable — the agent token is being stored UNENCRYPTED in " +
|
|
16
|
+
"~/.config/oh-hai/credentials (0600). Anyone able to read your home directory can read it.";
|
|
17
|
+
/** Build the Hub-scoped storage key from an origin and agent id (§5.1). */
|
|
18
|
+
export function keyOf(origin, agentId) {
|
|
19
|
+
return `${origin}|${agentId}`;
|
|
20
|
+
}
|
|
21
|
+
/** Split a storage key back into `{ origin, agentId }`. Origins (scheme+host+port) never
|
|
22
|
+
* contain `|`, so splitting on the FIRST `|` is unambiguous even if an agent id contained one. */
|
|
23
|
+
export function parseKey(key) {
|
|
24
|
+
const idx = key.indexOf("|");
|
|
25
|
+
return idx < 0 ? { origin: "", agentId: key } : { origin: key.slice(0, idx), agentId: key.slice(idx + 1) };
|
|
26
|
+
}
|
|
27
|
+
/** The keys-only index file the keychain backend uses for enumeration — a sibling of the
|
|
28
|
+
* credentials file (`~/.config/oh-hai/keys.json`), honoring the same XDG resolution. */
|
|
29
|
+
export function keychainIndexPath(env = process.env) {
|
|
30
|
+
return join(dirname(credentialsPath(env)), "keys.json");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A layered credential store over one or more backends. Writes go to the PRIMARY backend
|
|
34
|
+
* (keychain when available, else the file fallback); reads, deletes, list, and clearAll span
|
|
35
|
+
* EVERY backend. That dual-read/dual-scrub matters for §6's `keychain || file` chain and for
|
|
36
|
+
* security: a token written to the `0600` file while the keychain was down must still be
|
|
37
|
+
* resolved, and — critically — must still be *removed* by `logout`/`logout --all` once the
|
|
38
|
+
* keychain is back (a single-backend store would leave the unencrypted copy behind). A given
|
|
39
|
+
* key only ever lives in ONE backend (set writes to the primary), so scanning all backends
|
|
40
|
+
* never double-counts or duplicates.
|
|
41
|
+
*/
|
|
42
|
+
export class CredentialStore {
|
|
43
|
+
/** The PRIMARY (write) backend's name — surfaced so `login` can report `storage`. */
|
|
44
|
+
backendName;
|
|
45
|
+
backends;
|
|
46
|
+
primary;
|
|
47
|
+
fellBack;
|
|
48
|
+
warn;
|
|
49
|
+
warned = false;
|
|
50
|
+
constructor(backend, options = {}) {
|
|
51
|
+
const backends = Array.isArray(backend) ? backend : [backend];
|
|
52
|
+
const primary = backends[0];
|
|
53
|
+
if (primary === undefined) {
|
|
54
|
+
throw new Error("CredentialStore requires at least one backend");
|
|
55
|
+
}
|
|
56
|
+
this.backends = backends;
|
|
57
|
+
this.primary = primary;
|
|
58
|
+
this.backendName = primary.name;
|
|
59
|
+
this.fellBack = options.fellBack ?? false;
|
|
60
|
+
this.warn = options.warn ?? ((msg) => process.stderr.write(`${msg}\n`));
|
|
61
|
+
}
|
|
62
|
+
/** Emit the on-disk warning exactly once per store, only when we fell back to the file. */
|
|
63
|
+
maybeWarn() {
|
|
64
|
+
if (this.fellBack && !this.warned) {
|
|
65
|
+
this.warned = true;
|
|
66
|
+
this.warn(FILE_FALLBACK_WARNING);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async get(origin, agentId) {
|
|
70
|
+
return (await this.getWithSource(origin, agentId))?.token;
|
|
71
|
+
}
|
|
72
|
+
/** Read from the first backend (primary before fallback) that holds the token, reporting
|
|
73
|
+
* which one it was — so `whoami`/resolution can show the real source (§6). */
|
|
74
|
+
async getWithSource(origin, agentId) {
|
|
75
|
+
const key = keyOf(origin, agentId);
|
|
76
|
+
for (const backend of this.backends) {
|
|
77
|
+
const token = await backend.get(key);
|
|
78
|
+
if (token !== undefined) {
|
|
79
|
+
return { token, source: backend.name };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
async set(origin, agentId, token) {
|
|
85
|
+
this.maybeWarn();
|
|
86
|
+
const key = keyOf(origin, agentId);
|
|
87
|
+
await this.primary.set(key, token);
|
|
88
|
+
// Scrub any stale copy of this key from the OTHER backends — e.g. an old plaintext file
|
|
89
|
+
// token written during a past keychain outage. Without this, an overwrite would leave a
|
|
90
|
+
// divergent bearer on disk that resolution could fall back to. Best-effort: the primary
|
|
91
|
+
// write already succeeded, so a failure to scrub a secondary must not fail the login.
|
|
92
|
+
for (const backend of this.backends) {
|
|
93
|
+
if (backend === this.primary)
|
|
94
|
+
continue;
|
|
95
|
+
try {
|
|
96
|
+
await backend.delete(key);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ignore — the authoritative copy is in the primary
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Delete from EVERY backend so a file-fallback copy is scrubbed even when the keychain is
|
|
104
|
+
* primary. A backend error doesn't stop the others (the file must always be scrubbed); the
|
|
105
|
+
* first error is surfaced after all backends have been attempted. */
|
|
106
|
+
async delete(origin, agentId) {
|
|
107
|
+
const key = keyOf(origin, agentId);
|
|
108
|
+
let removed = false;
|
|
109
|
+
let firstError;
|
|
110
|
+
for (const backend of this.backends) {
|
|
111
|
+
try {
|
|
112
|
+
if (await backend.delete(key))
|
|
113
|
+
removed = true;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
firstError ??= error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (firstError !== undefined)
|
|
120
|
+
throw firstError;
|
|
121
|
+
return removed;
|
|
122
|
+
}
|
|
123
|
+
/** Union of identities across all backends (a key lives in exactly one, so no dedup needed —
|
|
124
|
+
* the guard is belt-and-suspenders). */
|
|
125
|
+
async list() {
|
|
126
|
+
const seen = new Map();
|
|
127
|
+
for (const backend of this.backends) {
|
|
128
|
+
for (const key of await backend.listKeys()) {
|
|
129
|
+
if (!seen.has(key))
|
|
130
|
+
seen.set(key, { ...parseKey(key), source: backend.name });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return [...seen.values()];
|
|
134
|
+
}
|
|
135
|
+
/** Clear EVERY backend (so `logout --all` scrubs the file fallback too). As with delete, a
|
|
136
|
+
* backend error doesn't skip the others; the first error is surfaced afterward. */
|
|
137
|
+
async clearAll() {
|
|
138
|
+
let total = 0;
|
|
139
|
+
let firstError;
|
|
140
|
+
for (const backend of this.backends) {
|
|
141
|
+
try {
|
|
142
|
+
total += await backend.clearAll();
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
firstError ??= error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (firstError !== undefined)
|
|
149
|
+
throw firstError;
|
|
150
|
+
return total;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** How long the keychain availability probe may run before we give up and use the file
|
|
154
|
+
* fallback. A locked macOS login keychain can make `security` block on a GUI unlock prompt,
|
|
155
|
+
* which in a headless/unattended context never answers — without a bound, every command would
|
|
156
|
+
* hang forever. Falling back to the `0600` file on timeout is the §5.2 locked-keychain path. */
|
|
157
|
+
const KEYCHAIN_PROBE_TIMEOUT_MS = 5000;
|
|
158
|
+
/** Resolve `promise`, or `fallback` if it doesn't settle within `ms`. The abandoned probe
|
|
159
|
+
* process (if any) is left to be reaped when the CLI exits; the timer is unref'd so it never
|
|
160
|
+
* keeps the event loop alive on its own. */
|
|
161
|
+
function withTimeout(promise, ms, fallback) {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
let settled = false;
|
|
164
|
+
const timer = setTimeout(() => {
|
|
165
|
+
if (!settled) {
|
|
166
|
+
settled = true;
|
|
167
|
+
resolve(fallback);
|
|
168
|
+
}
|
|
169
|
+
}, ms);
|
|
170
|
+
timer.unref?.();
|
|
171
|
+
const done = (value) => {
|
|
172
|
+
if (!settled) {
|
|
173
|
+
settled = true;
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
resolve(value);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
promise.then(done, () => done(fallback));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Open a credential store. The keychain (when available) is the PRIMARY backend with the file
|
|
183
|
+
* fallback layered underneath — so a token written to the file during a past keychain outage is
|
|
184
|
+
* still read and, on `logout`, scrubbed (§6). When the keychain is unavailable/locked, the file
|
|
185
|
+
* is the sole backend and the one-time on-disk warning is armed. Backend selection is the only
|
|
186
|
+
* async step: a side-effect-free `get` of a sentinel key, bounded by a timeout so a hung/locked
|
|
187
|
+
* keychain can't hang the CLI — the abandoned probe child is cancelled on timeout.
|
|
188
|
+
*/
|
|
189
|
+
export async function openStore(options = {}) {
|
|
190
|
+
const warn = options.warn;
|
|
191
|
+
if (options.backend !== undefined) {
|
|
192
|
+
return new CredentialStore(options.backend, warn !== undefined ? { warn } : {});
|
|
193
|
+
}
|
|
194
|
+
const file = new FileBackend(options.filePath ?? credentialsPath());
|
|
195
|
+
const indexPath = options.indexPath ?? keychainIndexPath();
|
|
196
|
+
const runner = options.keychainRunner ?? systemKeychainRunner();
|
|
197
|
+
const keychain = new KeychainBackend(runner, indexPath);
|
|
198
|
+
// Bound the probe and cancel it on timeout: a locked keychain that blocks on a GUI unlock
|
|
199
|
+
// would otherwise leave the `security`/`secret-tool` child running and keep Node alive.
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const available = await withTimeout(keychain.available(controller.signal), options.probeTimeoutMs ?? KEYCHAIN_PROBE_TIMEOUT_MS, false);
|
|
202
|
+
if (!available) {
|
|
203
|
+
controller.abort();
|
|
204
|
+
return new CredentialStore(file, warn !== undefined ? { fellBack: true, warn } : { fellBack: true });
|
|
205
|
+
}
|
|
206
|
+
return new CredentialStore([keychain, file], warn !== undefined ? { warn } : {});
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/auth/token-store.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { Io, RuntimeDeps } from "./commands/context.js";
|
|
3
|
+
export type { Io } from "./commands/context.js";
|
|
4
|
+
export { flagOn } from "./commands/flags.js";
|
|
5
|
+
/**
|
|
6
|
+
* True when `moduleUrl`'s file is the process entrypoint. Compares REAL paths (resolving
|
|
7
|
+
* symlinks) so a bin installed as a symlink by npm/Homebrew — where `process.argv[1]` is the
|
|
8
|
+
* symlink but `import.meta.url` is the resolved target — is still recognized as the entry.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isMainModule(invokedPath: string | undefined, moduleUrl: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Run the CLI for the given argv (excluding node + script path) and return the process
|
|
13
|
+
* exit code. Never calls process.exit — the caller sets `process.exitCode`. `runtime` is
|
|
14
|
+
* injectable so command tests stay hermetic.
|
|
15
|
+
*/
|
|
16
|
+
export declare function run(argv: string[], io?: Io, runtime?: RuntimeDeps): Promise<number>;
|