@ai21/gateway 0.6.0 → 1.0.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 +42 -12
- package/dist/args.d.ts +4 -2
- package/dist/args.js +15 -6
- package/dist/args.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +45 -5
- package/dist/cli.js.map +1 -1
- package/dist/login/apiKeys.d.ts +20 -0
- package/dist/login/apiKeys.js +75 -0
- package/dist/login/apiKeys.js.map +1 -0
- package/dist/login/credentials.d.ts +21 -8
- package/dist/login/credentials.js +149 -41
- package/dist/login/credentials.js.map +1 -1
- package/dist/login/fileAcl.d.ts +29 -0
- package/dist/login/fileAcl.js +129 -0
- package/dist/login/fileAcl.js.map +1 -0
- package/dist/login/keychain/linux.d.ts +28 -0
- package/dist/login/keychain/linux.js +57 -0
- package/dist/login/keychain/linux.js.map +1 -0
- package/dist/login/keychain/macos.d.ts +30 -0
- package/dist/login/keychain/macos.js +52 -0
- package/dist/login/keychain/macos.js.map +1 -0
- package/dist/login/keychain/runner.d.ts +30 -0
- package/dist/login/keychain/runner.js +88 -0
- package/dist/login/keychain/runner.js.map +1 -0
- package/dist/login/keychain/select.d.ts +14 -0
- package/dist/login/keychain/select.js +74 -0
- package/dist/login/keychain/select.js.map +1 -0
- package/dist/login/lock.d.ts +16 -0
- package/dist/login/lock.js +139 -0
- package/dist/login/lock.js.map +1 -0
- package/dist/login/secretStore.d.ts +21 -0
- package/dist/login/secretStore.js +12 -0
- package/dist/login/secretStore.js.map +1 -0
- package/dist/login/session.d.ts +3 -0
- package/dist/login/session.js.map +1 -1
- package/dist/login/workspaces.d.ts +18 -1
- package/dist/login/workspaces.js +46 -2
- package/dist/login/workspaces.js.map +1 -1
- package/dist/precheck.d.ts +46 -0
- package/dist/precheck.js +86 -0
- package/dist/precheck.js.map +1 -0
- package/dist/prompt.d.ts +4 -19
- package/dist/prompt.js +1 -71
- package/dist/prompt.js.map +1 -1
- package/dist/report.d.ts +18 -9
- package/dist/report.js +86 -13
- package/dist/report.js.map +1 -1
- package/dist/result.d.ts +2 -0
- package/dist/result.js +2 -0
- package/dist/result.js.map +1 -1
- package/dist/router.d.ts +2 -0
- package/dist/router.js +240 -69
- package/dist/router.js.map +1 -1
- package/dist/verify.d.ts +1 -0
- package/dist/verify.js +1 -1
- package/dist/verify.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SecretName } from "../secretStore.js";
|
|
2
|
+
/** The service every keychain entry is filed under, on both backends. */
|
|
3
|
+
export declare const KEYCHAIN_SERVICE = "ai21-gateway";
|
|
4
|
+
/** Generous for a local call, short enough that a hung prompt does not hold the CLI. */
|
|
5
|
+
export declare const KEYCHAIN_TIMEOUT_MS = 10000;
|
|
6
|
+
/** One entry per secret per gateway, so staging is never production and each secret has its own lifetime. */
|
|
7
|
+
export declare function accountFor(gateway: string, name: SecretName): string;
|
|
8
|
+
export interface CommandOutcome {
|
|
9
|
+
/** `null` when the process was killed or never started. */
|
|
10
|
+
readonly code: number | null;
|
|
11
|
+
readonly stdout: string;
|
|
12
|
+
readonly stderr: string;
|
|
13
|
+
readonly timedOut: boolean;
|
|
14
|
+
/** The executable is missing or not runnable — the tier is unavailable, not failing. */
|
|
15
|
+
readonly spawnFailed: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Injected so tests drive the backends without a real keychain. */
|
|
18
|
+
export type CommandRunner = (executable: string, args: readonly string[], options: {
|
|
19
|
+
readonly input?: string | undefined;
|
|
20
|
+
readonly timeoutMs: number;
|
|
21
|
+
}) => Promise<CommandOutcome>;
|
|
22
|
+
/** Why a credential command failed, for an error message. Never includes stdout, which may hold a secret. */
|
|
23
|
+
export declare function failureReason(outcome: CommandOutcome): string;
|
|
24
|
+
export declare const runCredentialCommand: CommandRunner;
|
|
25
|
+
/** For callers inside the credentials-file lock, which is synchronous. */
|
|
26
|
+
export type SyncCommandRunner = (executable: string, args: readonly string[], options: {
|
|
27
|
+
readonly timeoutMs: number;
|
|
28
|
+
}) => CommandOutcome;
|
|
29
|
+
/** No stdin: nothing synchronous needs to send a secret. */
|
|
30
|
+
export declare const runCredentialCommandSync: SyncCommandRunner;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a credential tool as a child process with a hard bound. A child process, not a
|
|
3
|
+
* native binding: a blocked child can be killed, a blocked native call cannot, and on
|
|
4
|
+
* macOS the keychain grant stays with the Apple-signed `/usr/bin/security` across node
|
|
5
|
+
* upgrades. Secrets go in on stdin, never in `args`, which `ps` shows to every user.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
+
/** The service every keychain entry is filed under, on both backends. */
|
|
9
|
+
export const KEYCHAIN_SERVICE = "ai21-gateway";
|
|
10
|
+
/** Generous for a local call, short enough that a hung prompt does not hold the CLI. */
|
|
11
|
+
export const KEYCHAIN_TIMEOUT_MS = 10_000;
|
|
12
|
+
/** One entry per secret per gateway, so staging is never production and each secret has its own lifetime. */
|
|
13
|
+
export function accountFor(gateway, name) {
|
|
14
|
+
return `${gateway}#${name}`;
|
|
15
|
+
}
|
|
16
|
+
/** Enough for a key or a token; a credential tool answering with more is malfunctioning. */
|
|
17
|
+
const MAX_OUTPUT_CHARS = 64 * 1024;
|
|
18
|
+
/** Why a credential command failed, for an error message. Never includes stdout, which may hold a secret. */
|
|
19
|
+
export function failureReason(outcome) {
|
|
20
|
+
if (outcome.spawnFailed)
|
|
21
|
+
return "the tool could not be run";
|
|
22
|
+
if (outcome.timedOut)
|
|
23
|
+
return "timed out";
|
|
24
|
+
const text = outcome.stderr.trim();
|
|
25
|
+
return text === "" ? `exit code ${outcome.code}` : text.split("\n")[0];
|
|
26
|
+
}
|
|
27
|
+
export const runCredentialCommand = (executable, args, options) => new Promise((resolve) => {
|
|
28
|
+
let child;
|
|
29
|
+
try {
|
|
30
|
+
child = spawn(executable, [...args], { stdio: ["pipe", "pipe", "pipe"] });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return resolve({ code: null, stdout: "", stderr: "", timedOut: false, spawnFailed: true });
|
|
34
|
+
}
|
|
35
|
+
let stdout = "";
|
|
36
|
+
let stderr = "";
|
|
37
|
+
const capture = (append) => (chunk) => {
|
|
38
|
+
append(chunk.toString("utf8"));
|
|
39
|
+
};
|
|
40
|
+
child.stdout.on("data", capture((t) => (stdout += stdout.length < MAX_OUTPUT_CHARS ? t : "")));
|
|
41
|
+
child.stderr.on("data", capture((t) => (stderr += stderr.length < MAX_OUTPUT_CHARS ? t : "")));
|
|
42
|
+
let done = false;
|
|
43
|
+
const settle = (outcome) => {
|
|
44
|
+
if (done)
|
|
45
|
+
return;
|
|
46
|
+
done = true;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
resolve(outcome);
|
|
49
|
+
};
|
|
50
|
+
// SIGKILL: a process waiting on a keychain dialog may not act on a signal it can catch.
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
child.kill("SIGKILL");
|
|
53
|
+
settle({ code: null, stdout, stderr, timedOut: true, spawnFailed: false });
|
|
54
|
+
}, options.timeoutMs);
|
|
55
|
+
timer.unref();
|
|
56
|
+
// ENOENT for a tool that is not installed — `secret-tool` on most servers.
|
|
57
|
+
child.on("error", () => settle({ code: null, stdout: "", stderr: "", timedOut: false, spawnFailed: true }));
|
|
58
|
+
child.on("close", (code) => settle({ code, stdout, stderr, timedOut: false, spawnFailed: false }));
|
|
59
|
+
// EPIPE when the child has already gone; the close handler reports that.
|
|
60
|
+
child.stdin.on("error", () => undefined);
|
|
61
|
+
if (options.input !== undefined)
|
|
62
|
+
child.stdin.end(options.input);
|
|
63
|
+
else
|
|
64
|
+
child.stdin.end();
|
|
65
|
+
});
|
|
66
|
+
/** No stdin: nothing synchronous needs to send a secret. */
|
|
67
|
+
export const runCredentialCommandSync = (executable, args, options) => {
|
|
68
|
+
const result = spawnSync(executable, [...args], {
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
71
|
+
timeout: options.timeoutMs,
|
|
72
|
+
killSignal: "SIGKILL",
|
|
73
|
+
maxBuffer: MAX_OUTPUT_CHARS,
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
});
|
|
76
|
+
// pid 0: the process never started, e.g. ENOENT.
|
|
77
|
+
if (result.pid === 0)
|
|
78
|
+
return { code: null, stdout: "", stderr: "", timedOut: false, spawnFailed: true };
|
|
79
|
+
const timedOut = result.error?.code === "ETIMEDOUT";
|
|
80
|
+
return {
|
|
81
|
+
code: result.status,
|
|
82
|
+
stdout: result.stdout ?? "",
|
|
83
|
+
stderr: result.stderr ?? "",
|
|
84
|
+
timedOut,
|
|
85
|
+
spawnFailed: false,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/login/keychain/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picking a tier, and falling back to the file when the keychain will not answer. The
|
|
3
|
+
* fallback is silent: storage is meant to be invisible, and once an agent is configured
|
|
4
|
+
* the key sits in plaintext in its own config anyway.
|
|
5
|
+
*/
|
|
6
|
+
import { type CredentialPaths } from "../credentials.js";
|
|
7
|
+
import type { SecretStore } from "../secretStore.js";
|
|
8
|
+
import { type LinuxKeychainDeps } from "./linux.js";
|
|
9
|
+
import { type MacosKeychainDeps } from "./macos.js";
|
|
10
|
+
export interface TierDeps extends MacosKeychainDeps, LinuxKeychainDeps {
|
|
11
|
+
readonly paths: CredentialPaths;
|
|
12
|
+
}
|
|
13
|
+
/** The platform's keychain where one is reachable, otherwise the file. */
|
|
14
|
+
export declare function selectSecretStore(deps: TierDeps): SecretStore;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picking a tier, and falling back to the file when the keychain will not answer. The
|
|
3
|
+
* fallback is silent: storage is meant to be invisible, and once an agent is configured
|
|
4
|
+
* the key sits in plaintext in its own config anyway.
|
|
5
|
+
*/
|
|
6
|
+
import { fileSecretStore } from "../credentials.js";
|
|
7
|
+
import { findSecretTool, linuxKeychainStore } from "./linux.js";
|
|
8
|
+
import { macosKeychainAvailable, macosKeychainStore } from "./macos.js";
|
|
9
|
+
/** The platform's keychain where one is reachable, otherwise the file. */
|
|
10
|
+
export function selectSecretStore(deps) {
|
|
11
|
+
const file = fileSecretStore(deps.paths);
|
|
12
|
+
const keychain = chooseKeychain(deps);
|
|
13
|
+
return keychain === undefined ? file : withFileFallback(keychain, file);
|
|
14
|
+
}
|
|
15
|
+
function chooseKeychain(deps) {
|
|
16
|
+
if (macosKeychainAvailable(deps))
|
|
17
|
+
return macosKeychainStore(deps);
|
|
18
|
+
const secretTool = findSecretTool(deps);
|
|
19
|
+
return secretTool === undefined ? undefined : linuxKeychainStore(deps, secretTool);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The file holds a secret only when it is the newest copy: written before the keychain
|
|
23
|
+
* tier existed, or written there because the keychain refused. A successful keychain
|
|
24
|
+
* write drops the file copy. So reads try the file first, and a keychain entry left
|
|
25
|
+
* behind by a failed write can never shadow the value that replaced it.
|
|
26
|
+
*
|
|
27
|
+
* A keychain that throws is not asked again for the rest of the process, so a locked
|
|
28
|
+
* keychain costs one timeout, not one per call.
|
|
29
|
+
*/
|
|
30
|
+
function withFileFallback(keychain, file) {
|
|
31
|
+
let degraded = false;
|
|
32
|
+
const degrade = () => {
|
|
33
|
+
degraded = true;
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
get tier() {
|
|
37
|
+
return degraded ? file.tier : keychain.tier;
|
|
38
|
+
},
|
|
39
|
+
async read(gateway, name) {
|
|
40
|
+
const fromFile = await file.read(gateway, name);
|
|
41
|
+
if (fromFile !== undefined || degraded)
|
|
42
|
+
return fromFile;
|
|
43
|
+
try {
|
|
44
|
+
return await keychain.read(gateway, name);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
degrade();
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async write(gateway, name, value) {
|
|
52
|
+
if (!degraded) {
|
|
53
|
+
try {
|
|
54
|
+
await keychain.write(gateway, name, value);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
degrade();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (degraded) {
|
|
61
|
+
await file.write(gateway, name, value);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await file.forget(gateway, name);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async forget(gateway, name) {
|
|
68
|
+
const fromKeychain = degraded ? false : await keychain.forget(gateway, name);
|
|
69
|
+
const fromFile = await file.forget(gateway, name);
|
|
70
|
+
return fromKeychain || fromFile;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.js","sourceRoot":"","sources":["../../../src/login/keychain/select.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,eAAe,EAAwB,MAAM,mBAAmB,CAAC;AAE1E,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAA0B,MAAM,YAAY,CAAC;AACxF,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAA0B,MAAM,YAAY,CAAC;AAMhG,0EAA0E;AAC1E,MAAM,UAAU,iBAAiB,CAAC,IAAc;IAC9C,MAAM,IAAI,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAEtC,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,cAAc,CAAC,IAAc;IACpC,IAAI,sBAAsB,CAAC,IAAI,CAAC;QAAE,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAElE,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAExC,OAAO,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,gBAAgB,CAAC,QAAqB,EAAE,IAAiB;IAChE,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,QAAQ,GAAG,IAAI,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,IAAI;YACN,OAAO,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC9C,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI;YACtB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAChD,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ;gBAAE,OAAO,QAAQ,CAAC;YAExD,IAAI,CAAC;gBACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC5C,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,CAAC;gBAEV,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAED,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK;YAC9B,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,IAAI,CAAC;oBACH,MAAM,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;gBAC7C,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,EAAE,CAAC;gBACZ,CAAC;YACH,CAAC;YAED,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACzC,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI;YACxB,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC7E,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAElD,OAAO,YAAY,IAAI,QAAQ,CAAC;QAClC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes a lock judged stale without removing a live one. Several waiters can judge the
|
|
3
|
+
* same lock stale; renaming it to a name only this process uses lets exactly one take it.
|
|
4
|
+
* If what it took is not the lock it judged, another waiter already replaced that one,
|
|
5
|
+
* so it goes back. `link` rather than `rename` for that, so it cannot overwrite a third
|
|
6
|
+
* process's lock; in that case the lock stays lost, which takes three processes racing
|
|
7
|
+
* within microseconds of a crash.
|
|
8
|
+
*/
|
|
9
|
+
export declare function breakStale(lockPath: string, judgedIno: number): void;
|
|
10
|
+
/**
|
|
11
|
+
* Runs `change` with the lock held, and releases it however that turns out. The lock's
|
|
12
|
+
* directory must exist. Throws if the lock is not free within the budget.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withLock<T>(lockPath: string, change: () => T, options?: {
|
|
15
|
+
waitBudgetMs?: number;
|
|
16
|
+
}): T;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cross-process lock around the credentials file. Every write there is
|
|
3
|
+
* read-modify-write, so without it two concurrent `ai21-gateway` processes can drop each
|
|
4
|
+
* other's gateway entries. Synchronous, to match the file writes it guards.
|
|
5
|
+
*
|
|
6
|
+
* Known limit: liveness is `kill(pid, 0)`, which only means something on this host. A
|
|
7
|
+
* home directory shared across hosts or containers can make a live holder look dead.
|
|
8
|
+
*/
|
|
9
|
+
import { closeSync, fstatSync, linkSync, openSync, readFileSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
|
|
10
|
+
/** Long enough that an ordinary write never hits it, short enough to not feel hung. */
|
|
11
|
+
const WAIT_BUDGET_MS = 10_000;
|
|
12
|
+
/**
|
|
13
|
+
* Backstop for a lock whose pid is unreadable or was recycled. Normally staleness is
|
|
14
|
+
* decided by whether the holder is still running, so a slow holder is never robbed.
|
|
15
|
+
* A write holds the lock for milliseconds, so this only has to outlast the wait budget.
|
|
16
|
+
*/
|
|
17
|
+
const STALE_AFTER_MS = 15_000;
|
|
18
|
+
const RETRY_MS = 20;
|
|
19
|
+
/** Blocks this thread without a busy loop. A CLI has nothing else to do meanwhile. */
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
22
|
+
}
|
|
23
|
+
/** One consistent look at the lock file, or undefined when it cannot be read (usually: it is gone). */
|
|
24
|
+
function inspect(lockPath) {
|
|
25
|
+
let fd;
|
|
26
|
+
try {
|
|
27
|
+
fd = openSync(lockPath, "r");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const { ino, mtimeMs } = fstatSync(fd);
|
|
34
|
+
return { ino, ageMs: Date.now() - mtimeMs, running: isRunning(readFileSync(fd, "utf8")) };
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
closeSync(fd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `kill(pid, 0)` only asks whether the pid exists; `EPERM` means it does, under another
|
|
42
|
+
* user. A missing or malformed pid counts as running — the holder may not have written it
|
|
43
|
+
* yet — and is left to the age backstop.
|
|
44
|
+
*/
|
|
45
|
+
function isRunning(content) {
|
|
46
|
+
const pid = Number.parseInt(content.trim(), 10);
|
|
47
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
48
|
+
return true;
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, 0);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return err.code === "EPERM";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Removes a lock judged stale without removing a live one. Several waiters can judge the
|
|
59
|
+
* same lock stale; renaming it to a name only this process uses lets exactly one take it.
|
|
60
|
+
* If what it took is not the lock it judged, another waiter already replaced that one,
|
|
61
|
+
* so it goes back. `link` rather than `rename` for that, so it cannot overwrite a third
|
|
62
|
+
* process's lock; in that case the lock stays lost, which takes three processes racing
|
|
63
|
+
* within microseconds of a crash.
|
|
64
|
+
*/
|
|
65
|
+
export function breakStale(lockPath, judgedIno) {
|
|
66
|
+
const claimed = `${lockPath}.${process.pid}`;
|
|
67
|
+
try {
|
|
68
|
+
renameSync(lockPath, claimed);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (err.code === "ENOENT")
|
|
72
|
+
return;
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
if (statSync(claimed).ino !== judgedIno)
|
|
77
|
+
linkSync(claimed, lockPath);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err.code !== "EEXIST")
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
rmSync(claimed, { force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Only if it is still ours: if our lock was broken as stale, what sits there now is someone else's. */
|
|
88
|
+
function release(lockPath, ownIno) {
|
|
89
|
+
try {
|
|
90
|
+
if (statSync(lockPath).ino === ownIno)
|
|
91
|
+
rmSync(lockPath, { force: true });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.code !== "ENOENT")
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Runs `change` with the lock held, and releases it however that turns out. The lock's
|
|
100
|
+
* directory must exist. Throws if the lock is not free within the budget.
|
|
101
|
+
*/
|
|
102
|
+
export function withLock(lockPath, change, options = {}) {
|
|
103
|
+
// Overridable so the test for the refusal does not spend the real budget waiting.
|
|
104
|
+
const deadline = Date.now() + (options.waitBudgetMs ?? WAIT_BUDGET_MS);
|
|
105
|
+
let fd;
|
|
106
|
+
for (;;) {
|
|
107
|
+
try {
|
|
108
|
+
// "wx" is O_CREAT|O_EXCL: the create either wins or fails, with no window between
|
|
109
|
+
// the check and the claim.
|
|
110
|
+
fd = openSync(lockPath, "wx", 0o600);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (err.code !== "EEXIST")
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
const holder = inspect(lockPath);
|
|
118
|
+
if (holder !== undefined && (!holder.running || holder.ageMs > STALE_AFTER_MS)) {
|
|
119
|
+
breakStale(lockPath, holder.ino);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (Date.now() >= deadline) {
|
|
123
|
+
throw new Error(`another ai21-gateway command is writing credentials (${lockPath} held for ${holder?.ageMs ?? "?"}ms). ` +
|
|
124
|
+
"Wait for it to finish, or delete that file if nothing is running.");
|
|
125
|
+
}
|
|
126
|
+
sleep(RETRY_MS);
|
|
127
|
+
}
|
|
128
|
+
const ownIno = fstatSync(fd).ino;
|
|
129
|
+
try {
|
|
130
|
+
// Waiters test this pid to decide whether the lock is stale.
|
|
131
|
+
writeSync(fd, `${process.pid}\n`);
|
|
132
|
+
return change();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
closeSync(fd);
|
|
136
|
+
release(lockPath, ownIno);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/login/lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where secrets live between runs.
|
|
3
|
+
*
|
|
4
|
+
* `login` holds two secrets with independent lifetimes: a Firebase refresh token, which
|
|
5
|
+
* acts as the user, and the AI21 key minted for it. Either can be revoked or expire
|
|
6
|
+
* without the other, so each is read, replaced and forgotten on its own. Both are keyed
|
|
7
|
+
* by gateway URL, so a staging secret is never sent to production.
|
|
8
|
+
*
|
|
9
|
+
* Async although the file tier is synchronous: the keychain tiers shell out.
|
|
10
|
+
*/
|
|
11
|
+
/** Matches the field names the file tier already uses, so the on-disk shape is unchanged. */
|
|
12
|
+
export type SecretName = "refreshToken" | "ai21Key";
|
|
13
|
+
/** Which backend holds the secrets, so the CLI can tell the user where their key is. */
|
|
14
|
+
export type SecretTier = "file" | "keychain";
|
|
15
|
+
export interface SecretStore {
|
|
16
|
+
readonly tier: SecretTier;
|
|
17
|
+
read(gateway: string, name: SecretName): Promise<string | undefined>;
|
|
18
|
+
write(gateway: string, name: SecretName, value: string): Promise<void>;
|
|
19
|
+
/** Resolves false when there was nothing to forget. */
|
|
20
|
+
forget(gateway: string, name: SecretName): Promise<boolean>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where secrets live between runs.
|
|
3
|
+
*
|
|
4
|
+
* `login` holds two secrets with independent lifetimes: a Firebase refresh token, which
|
|
5
|
+
* acts as the user, and the AI21 key minted for it. Either can be revoked or expire
|
|
6
|
+
* without the other, so each is read, replaced and forgotten on its own. Both are keyed
|
|
7
|
+
* by gateway URL, so a staging secret is never sent to production.
|
|
8
|
+
*
|
|
9
|
+
* Async although the file tier is synchronous: the keychain tiers shell out.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=secretStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretStore.js","sourceRoot":"","sources":["../../src/login/secretStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
package/dist/login/session.d.ts
CHANGED
|
@@ -2,11 +2,14 @@ import { type ResolvedUrls } from "../args.js";
|
|
|
2
2
|
import type { FetchLike } from "./tokens.js";
|
|
3
3
|
import { type CliCredential } from "./contract.js";
|
|
4
4
|
import { type Listener, type LoopbackOptions } from "./loopback.js";
|
|
5
|
+
import type { SecretStore } from "./secretStore.js";
|
|
5
6
|
export interface LoginDeps {
|
|
6
7
|
readonly startListener: (options: LoopbackOptions) => Promise<Listener>;
|
|
7
8
|
readonly openBrowser: (url: string) => boolean;
|
|
8
9
|
/** For the token exchange and workspace listing; injected so tests make no calls. */
|
|
9
10
|
readonly fetchImpl: FetchLike;
|
|
11
|
+
/** Where the refresh token and the minted key are kept; injected so tests use neither. */
|
|
12
|
+
readonly secrets: SecretStore;
|
|
10
13
|
}
|
|
11
14
|
export type LoginOutcome = {
|
|
12
15
|
readonly kind: "credential";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/login/session.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAIxC,OAAO,EACL,SAAS,EACT,YAAY,EACZ,cAAc,EAEd,UAAU,EACV,aAAa,EACb,cAAc,EACd,WAAW,EACX,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAA6D,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/login/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The caller's AI21 workspaces,
|
|
2
|
+
* The caller's AI21 workspaces, and which one to act in.
|
|
3
3
|
*
|
|
4
4
|
* Identity shares the gateway's origin (`api.<domain>/identity`), so the URL is derived
|
|
5
5
|
* from it. Authenticated with the Firebase ID token in a `firebase-token` header —
|
|
@@ -9,9 +9,26 @@ import type { FetchLike } from "./tokens.js";
|
|
|
9
9
|
export interface Workspace {
|
|
10
10
|
readonly id: string;
|
|
11
11
|
readonly name: string;
|
|
12
|
+
/** `"public"`, `"private"`, or `"personal"` — identity sends `workspace_type`. */
|
|
13
|
+
readonly workspaceType: string;
|
|
12
14
|
}
|
|
13
15
|
/** `https://api.example.com/gateway` → `https://api.example.com`. */
|
|
14
16
|
export declare function identityBase(gatewayUrl: string): string | undefined;
|
|
15
17
|
/** Empty rather than throwing on anything unexpected: a completed login must not be lost
|
|
16
18
|
* to a failed listing. */
|
|
17
19
|
export declare function fetchWorkspaces(gatewayUrl: string, idToken: string, fetchImpl?: FetchLike): Promise<readonly Workspace[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Which workspace the key is minted into. `overrideId` is `--workspace`, and returns
|
|
22
|
+
* undefined when it names nothing in the listing rather than falling back — silently
|
|
23
|
+
* minting somewhere else is exactly what the flag exists to prevent.
|
|
24
|
+
*
|
|
25
|
+
* Identity returns public + personal + private, but neither group is ordered within
|
|
26
|
+
* itself, so taking its first is not reproducible. Public first, then by id, is.
|
|
27
|
+
*/
|
|
28
|
+
export declare function chooseWorkspace(available: readonly Workspace[], overrideId?: string): Workspace | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The workspace holding `agentId`, found by listing each workspace's agents. A key only
|
|
31
|
+
* sees its own workspace's agents, so minting anywhere else makes `install` refuse the
|
|
32
|
+
* agent it was asked to configure. Undefined when none holds it or none answered.
|
|
33
|
+
*/
|
|
34
|
+
export declare function workspaceOwningAgent(agentsUrl: string, idToken: string, workspaces: readonly Workspace[], agentId: string, fetchImpl?: FetchLike): Promise<Workspace | undefined>;
|
package/dist/login/workspaces.js
CHANGED
|
@@ -53,13 +53,57 @@ function parse(payload) {
|
|
|
53
53
|
const name = row["name"];
|
|
54
54
|
if (typeof id !== "string" || id === "")
|
|
55
55
|
continue;
|
|
56
|
-
|
|
56
|
+
// identity sends `workspace_type`; the legacy `type` is tolerated alongside it.
|
|
57
|
+
const rawType = typeof row["workspace_type"] === "string" ? row["workspace_type"] : row["type"];
|
|
58
|
+
const workspaceType = typeof rawType === "string" ? rawType.toLowerCase() : "";
|
|
59
|
+
if (workspaceType === LEGACY_PERSONAL_TYPE)
|
|
57
60
|
continue;
|
|
58
61
|
// Stripped for the same reason as the callback's strings: this gets printed.
|
|
59
62
|
// eslint-disable-next-line no-control-regex
|
|
60
63
|
const label = typeof name === "string" ? name.replace(/[\u0000-\u001F\u007F]/g, "") : "";
|
|
61
|
-
found.push({ id, name: label === "" ? id : label });
|
|
64
|
+
found.push({ id, name: label === "" ? id : label, workspaceType });
|
|
62
65
|
}
|
|
63
66
|
return found;
|
|
64
67
|
}
|
|
68
|
+
/** Identity's own notion of primary: an org's public workspace, not a private one. */
|
|
69
|
+
const PUBLIC_TYPE = "public";
|
|
70
|
+
/**
|
|
71
|
+
* Which workspace the key is minted into. `overrideId` is `--workspace`, and returns
|
|
72
|
+
* undefined when it names nothing in the listing rather than falling back — silently
|
|
73
|
+
* minting somewhere else is exactly what the flag exists to prevent.
|
|
74
|
+
*
|
|
75
|
+
* Identity returns public + personal + private, but neither group is ordered within
|
|
76
|
+
* itself, so taking its first is not reproducible. Public first, then by id, is.
|
|
77
|
+
*/
|
|
78
|
+
export function chooseWorkspace(available, overrideId) {
|
|
79
|
+
if (overrideId !== undefined)
|
|
80
|
+
return available.find((workspace) => workspace.id === overrideId);
|
|
81
|
+
const rank = (workspace) => (workspace.workspaceType === PUBLIC_TYPE ? 0 : 1);
|
|
82
|
+
return [...available].sort((a, b) => rank(a) - rank(b) || a.id.localeCompare(b.id))[0];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The workspace holding `agentId`, found by listing each workspace's agents. A key only
|
|
86
|
+
* sees its own workspace's agents, so minting anywhere else makes `install` refuse the
|
|
87
|
+
* agent it was asked to configure. Undefined when none holds it or none answered.
|
|
88
|
+
*/
|
|
89
|
+
export async function workspaceOwningAgent(agentsUrl, idToken, workspaces, agentId, fetchImpl = fetch) {
|
|
90
|
+
const holds = async (workspace) => {
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetchImpl(agentsUrl, {
|
|
93
|
+
method: "GET",
|
|
94
|
+
headers: { Authorization: `Bearer ${idToken}`, "X-Workspace-Id": workspace.id },
|
|
95
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok)
|
|
98
|
+
return false;
|
|
99
|
+
const agents = JSON.parse(await response.text());
|
|
100
|
+
return Array.isArray(agents) && agents.some((agent) => agent?.agent_id === agentId);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const answers = await Promise.all(workspaces.map(holds));
|
|
107
|
+
return workspaces.find((_, n) => answers[n]);
|
|
108
|
+
}
|
|
65
109
|
//# sourceMappingURL=workspaces.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaces.js","sourceRoot":"","sources":["../../src/login/workspaces.ts"],"names":[],"mappings":"AASA,MAAM,eAAe,GAAG,sBAAsB,CAAC;AAC/C,MAAM,UAAU,GAAG,MAAM,CAAC;AAE1B;uCACuC;AACvC,MAAM,oBAAoB,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"workspaces.js","sourceRoot":"","sources":["../../src/login/workspaces.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks a pasted AI21 key before anything is written, with a GET on the agents
|
|
3
|
+
* endpoint. Only a positively identified rejection stops a write: there is no flag to
|
|
4
|
+
* bypass this, so anything we cannot read has to fall through and let the write happen.
|
|
5
|
+
*/
|
|
6
|
+
/** Just enough of `fetch` to GET with. `text` is optional; absent reads as unparseable. */
|
|
7
|
+
export type FetchLike = (url: string, init: {
|
|
8
|
+
method: string;
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
}) => Promise<{
|
|
12
|
+
ok: boolean;
|
|
13
|
+
status: number;
|
|
14
|
+
text?: () => Promise<string>;
|
|
15
|
+
}>;
|
|
16
|
+
export interface PrecheckDeps {
|
|
17
|
+
readonly fetchImpl: FetchLike;
|
|
18
|
+
}
|
|
19
|
+
export interface PrecheckContext {
|
|
20
|
+
/** The resolved agents endpoint — `urls.agents`, never rebuilt here. */
|
|
21
|
+
readonly agentsUrl: string;
|
|
22
|
+
/** The agent the config will name; checked for existence under this key. */
|
|
23
|
+
readonly agentId: string;
|
|
24
|
+
readonly ai21Key: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const PRECHECK_TIMEOUT_MS = 10000;
|
|
27
|
+
/** `rejected` and `unknown-agent` are separate because their remedies differ. */
|
|
28
|
+
export type PrecheckOutcome = {
|
|
29
|
+
readonly kind: "accepted";
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: "rejected";
|
|
32
|
+
readonly reason: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: "unknown-agent";
|
|
35
|
+
readonly known: readonly string[];
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: "inconclusive";
|
|
38
|
+
readonly reason: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly kind: "skipped";
|
|
41
|
+
readonly reason: string;
|
|
42
|
+
};
|
|
43
|
+
/** Reasons carry no key and end without a full stop — `report.ts` punctuates. */
|
|
44
|
+
export declare function precheckKey(ctx: PrecheckContext, deps: PrecheckDeps): Promise<PrecheckOutcome>;
|
|
45
|
+
/** The real-world adapter, kept here so `cli.ts` stays a thin edge. */
|
|
46
|
+
export declare function nodePrecheckDeps(): PrecheckDeps;
|