@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
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The signed-in session on disk: one owner-only file, keyed by gateway URL so a staging
|
|
3
|
+
* session is never used against production.
|
|
4
|
+
*
|
|
5
|
+
* Metadata (who, which workspace, when) goes through `load`/`save`. Secrets go through a
|
|
6
|
+
* `SecretStore`; `fileSecretStore` keeps them in this same file, in the flat shape
|
|
7
|
+
* earlier releases read. Since both write one file, every write merges into the entry.
|
|
5
8
|
*/
|
|
6
|
-
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
10
|
import { dirname, join } from "node:path";
|
|
11
|
+
import { windowsFileAcl } from "./fileAcl.js";
|
|
12
|
+
import { withLock } from "./lock.js";
|
|
8
13
|
const DIR_MODE = 0o700;
|
|
9
14
|
const FILE_MODE = 0o600;
|
|
15
|
+
const platformAcl = process.platform === "win32" ? windowsFileAcl() : undefined;
|
|
16
|
+
function aclFor(ctx) {
|
|
17
|
+
return ctx.acl ?? platformAcl;
|
|
18
|
+
}
|
|
19
|
+
/** One command reads the file several times; one warning per problem is enough. */
|
|
20
|
+
const warned = new WeakMap();
|
|
21
|
+
function warnOnce(ctx, message) {
|
|
22
|
+
if (ctx.warn === undefined)
|
|
23
|
+
return;
|
|
24
|
+
const seen = warned.get(ctx.warn) ?? new Set();
|
|
25
|
+
warned.set(ctx.warn, seen);
|
|
26
|
+
if (seen.has(message))
|
|
27
|
+
return;
|
|
28
|
+
seen.add(message);
|
|
29
|
+
ctx.warn(message);
|
|
30
|
+
}
|
|
10
31
|
export function credentialsPath(ctx) {
|
|
11
32
|
return join(ctx.home, ".config", "ai21-gateway", "credentials.json");
|
|
12
33
|
}
|
|
@@ -21,37 +42,60 @@ function readFile(path) {
|
|
|
21
42
|
return undefined;
|
|
22
43
|
}
|
|
23
44
|
}
|
|
45
|
+
/** Checks metadata only; the caller asks the `SecretStore` whether the secrets exist. */
|
|
24
46
|
function isComplete(value) {
|
|
25
47
|
if (typeof value !== "object" || value === null)
|
|
26
48
|
return false;
|
|
27
49
|
const c = value;
|
|
28
|
-
return (typeof c["
|
|
29
|
-
typeof c["firebaseApiKey"] === "string" &&
|
|
50
|
+
return (typeof c["firebaseApiKey"] === "string" &&
|
|
30
51
|
typeof c["projectId"] === "string" &&
|
|
31
52
|
typeof c["userId"] === "string" &&
|
|
32
53
|
typeof c["userEmail"] === "string");
|
|
33
54
|
}
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
/** The owner-only check, shared by every read of the file. */
|
|
56
|
+
function unreadableReason(path, ctx) {
|
|
36
57
|
let mode;
|
|
37
58
|
try {
|
|
38
59
|
mode = statSync(path).mode & 0o777;
|
|
39
60
|
}
|
|
40
61
|
catch {
|
|
41
|
-
return
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
// Checked on read too: the file may predate this version or have been copied in, and a
|
|
65
|
+
// readable refresh token is another local user's session.
|
|
66
|
+
const acl = aclFor(ctx);
|
|
67
|
+
if (acl !== undefined)
|
|
68
|
+
return aclReason(path, acl, ctx);
|
|
69
|
+
if ((mode & 0o077) !== 0) {
|
|
70
|
+
return `${path} is readable by other users (mode ${mode.toString(8)}). Run \`chmod 600 ${path}\`, or delete it and sign in again.`;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/** A check that could not run is warned about, not enforced: refusing then would lock out every Windows user whose `icacls` misbehaves. */
|
|
75
|
+
function aclReason(path, acl, ctx) {
|
|
76
|
+
const inspection = acl.inspect(path);
|
|
77
|
+
switch (inspection.kind) {
|
|
78
|
+
case "owner-only":
|
|
79
|
+
return undefined;
|
|
80
|
+
case "unknown":
|
|
81
|
+
warnOnce(ctx, `Could not check who can read ${path}: ${inspection.reason}`);
|
|
82
|
+
return undefined;
|
|
83
|
+
case "broader":
|
|
84
|
+
return (`${path} is readable by other accounts (${inspection.others.join(", ")}). ` +
|
|
85
|
+
`Run \`icacls "${path}"\` to see who, or delete it and sign in again.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function load(gateway, ctx) {
|
|
89
|
+
const path = credentialsPath(ctx);
|
|
90
|
+
try {
|
|
91
|
+
statSync(path);
|
|
42
92
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
* Skipped on Windows, which has no POSIX mode: Node reports 0o666 for an ordinary
|
|
47
|
-
* writable file, so this would refuse every session and advise a `chmod` that does not
|
|
48
|
-
* exist there. ACLs are the equivalent, and checking them is not this function's job. */
|
|
49
|
-
if (process.platform !== "win32" && (mode & 0o077) !== 0) {
|
|
50
|
-
return {
|
|
51
|
-
kind: "unusable",
|
|
52
|
-
reason: `${path} is readable by other users (mode ${mode.toString(8)}). Run \`chmod 600 ${path}\`, or delete it and sign in again.`,
|
|
53
|
-
};
|
|
93
|
+
catch {
|
|
94
|
+
return { kind: "none" };
|
|
54
95
|
}
|
|
96
|
+
const refused = unreadableReason(path, ctx);
|
|
97
|
+
if (refused !== undefined)
|
|
98
|
+
return { kind: "unusable", reason: refused };
|
|
55
99
|
const file = readFile(path);
|
|
56
100
|
if (file === undefined) {
|
|
57
101
|
return { kind: "unusable", reason: `${path} is not readable as JSON. Delete it and sign in again.` };
|
|
@@ -64,35 +108,99 @@ export function load(gateway, ctx) {
|
|
|
64
108
|
: { kind: "unusable", reason: `the stored session for ${gateway} is incomplete. Sign in again.` };
|
|
65
109
|
}
|
|
66
110
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
111
|
+
* Restricted while still empty, so the secret is never on disk under a broader ACL;
|
|
112
|
+
* the truncating write and the rename both keep the DACL. A leftover temp is removed
|
|
113
|
+
* first, since it would carry its old ACL through that write.
|
|
69
114
|
*/
|
|
70
|
-
|
|
71
|
-
const path = credentialsPath(ctx);
|
|
72
|
-
mkdirSync(dirname(path), { recursive: true, mode: DIR_MODE });
|
|
73
|
-
const existing = readFile(path) ?? {};
|
|
74
|
-
const merged = { ...existing, [gateway]: credential };
|
|
75
|
-
// Temp then rename, so an interrupted write cannot replace a whole session with half of one.
|
|
115
|
+
function writeOwnerOnly(path, content, ctx) {
|
|
76
116
|
const temp = `${path}.tmp`;
|
|
77
|
-
|
|
117
|
+
rmSync(temp, { force: true });
|
|
118
|
+
writeFileSync(temp, "", { mode: FILE_MODE });
|
|
78
119
|
chmodSync(temp, FILE_MODE);
|
|
120
|
+
// Warn, not fail: the read-time check refuses the file if it really is shared.
|
|
121
|
+
const failed = aclFor(ctx)?.restrictToOwner(temp);
|
|
122
|
+
if (failed !== undefined)
|
|
123
|
+
warnOnce(ctx, `Could not restrict ${path} to your account: ${failed}`);
|
|
124
|
+
writeFileSync(temp, `${JSON.stringify(content, null, 2)}\n`);
|
|
79
125
|
renameSync(temp, path);
|
|
80
126
|
}
|
|
81
|
-
/**
|
|
127
|
+
/** Sits beside the credentials file, so one lock covers every gateway's entry. */
|
|
128
|
+
export function lockPath(ctx) {
|
|
129
|
+
return `${credentialsPath(ctx)}.lock`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Merges one gateway's entry, leaving the others alone. Created at 0600 rather than
|
|
133
|
+
* chmodded after: the gap between the two is openable by another local user. Read and
|
|
134
|
+
* write happen under one lock, or concurrent processes drop each other's entries.
|
|
135
|
+
*/
|
|
136
|
+
function mutate(gateway, ctx, change) {
|
|
137
|
+
const path = credentialsPath(ctx);
|
|
138
|
+
mkdirSync(dirname(path), { recursive: true, mode: DIR_MODE });
|
|
139
|
+
return withLock(lockPath(ctx), () => {
|
|
140
|
+
const existing = readFile(path) ?? {};
|
|
141
|
+
const next = change(existing[gateway] ?? {});
|
|
142
|
+
if (next === undefined)
|
|
143
|
+
return false;
|
|
144
|
+
const merged = { ...existing, [gateway]: next };
|
|
145
|
+
// Temp then rename, so an interrupted write cannot replace a whole session with half of one.
|
|
146
|
+
writeOwnerOnly(path, merged, ctx);
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/** The keys the file tier owns as a secret store, and the only ones `save` carries over. */
|
|
151
|
+
const SECRET_NAMES = ["refreshToken", "ai21Key"];
|
|
152
|
+
/**
|
|
153
|
+
* Replaces the metadata and keeps only the secret fields. Not a blanket merge: an
|
|
154
|
+
* optional field such as `workspaceId` would otherwise survive from the previous user.
|
|
155
|
+
*/
|
|
156
|
+
export function save(gateway, metadata, ctx) {
|
|
157
|
+
mutate(gateway, ctx, (entry) => ({
|
|
158
|
+
...Object.fromEntries(SECRET_NAMES.filter((name) => name in entry).map((name) => [name, entry[name]])),
|
|
159
|
+
...metadata,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
/** Keeps secrets in the credentials file itself; the fallback where no keychain is reachable. */
|
|
163
|
+
export function fileSecretStore(ctx) {
|
|
164
|
+
return {
|
|
165
|
+
tier: "file",
|
|
166
|
+
read: (gateway, name) => Promise.resolve(readSecret(gateway, name, ctx)),
|
|
167
|
+
write: (gateway, name, value) => {
|
|
168
|
+
mutate(gateway, ctx, (entry) => ({ ...entry, [name]: value }));
|
|
169
|
+
return Promise.resolve();
|
|
170
|
+
},
|
|
171
|
+
forget: (gateway, name) => Promise.resolve(mutate(gateway, ctx, (entry) => (name in entry ? omit(entry, name) : undefined))),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function readSecret(gateway, name, ctx) {
|
|
175
|
+
const path = credentialsPath(ctx);
|
|
176
|
+
// A file other users can read is not a source of secrets, the same as on load.
|
|
177
|
+
if (unreadableReason(path, ctx) !== undefined)
|
|
178
|
+
return undefined;
|
|
179
|
+
const value = readFile(path)?.[gateway]?.[name];
|
|
180
|
+
return typeof value === "string" ? value : undefined;
|
|
181
|
+
}
|
|
182
|
+
function omit(entry, name) {
|
|
183
|
+
return Object.fromEntries(Object.entries(entry).filter(([key]) => key !== name));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Removes one gateway's entry from this file. Returns false when there was none.
|
|
187
|
+
* Secrets held by another tier are not touched; forget them through the store.
|
|
188
|
+
*/
|
|
82
189
|
export function clear(gateway, ctx) {
|
|
83
190
|
const path = credentialsPath(ctx);
|
|
84
|
-
|
|
85
|
-
if (existing === undefined || existing[gateway] === undefined)
|
|
191
|
+
if (!existsSync(path))
|
|
86
192
|
return false;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
193
|
+
return withLock(lockPath(ctx), () => {
|
|
194
|
+
const existing = readFile(path);
|
|
195
|
+
if (existing === undefined || existing[gateway] === undefined)
|
|
196
|
+
return false;
|
|
197
|
+
const rest = Object.fromEntries(Object.entries(existing).filter(([key]) => key !== gateway));
|
|
198
|
+
if (Object.keys(rest).length === 0) {
|
|
199
|
+
rmSync(path, { force: true });
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
writeOwnerOnly(path, rest, ctx);
|
|
90
203
|
return true;
|
|
91
|
-
}
|
|
92
|
-
const temp = `${path}.tmp`;
|
|
93
|
-
writeFileSync(temp, `${JSON.stringify(rest, null, 2)}\n`, { mode: FILE_MODE });
|
|
94
|
-
chmodSync(temp, FILE_MODE);
|
|
95
|
-
renameSync(temp, path);
|
|
96
|
-
return true;
|
|
204
|
+
});
|
|
97
205
|
}
|
|
98
206
|
//# sourceMappingURL=credentials.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/login/credentials.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/login/credentials.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type SyncCommandRunner } from "./keychain/runner.js";
|
|
2
|
+
export type AclInspection = {
|
|
3
|
+
readonly kind: "owner-only";
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: "broader";
|
|
6
|
+
readonly others: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** The check itself could not run; callers must not treat this as a violation. */
|
|
9
|
+
| {
|
|
10
|
+
readonly kind: "unknown";
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
};
|
|
13
|
+
export interface FileAcl {
|
|
14
|
+
/** Returns why it failed, or undefined on success. Never throws: a write must not fail on this. */
|
|
15
|
+
restrictToOwner(path: string): string | undefined;
|
|
16
|
+
inspect(path: string): AclInspection;
|
|
17
|
+
}
|
|
18
|
+
/** Reads the SID from `whoami /user`, whose CSV form is `"host\user","S-1-5-21-..."`. */
|
|
19
|
+
export declare function parseUserSid(stdout: string): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Which SIDs the DACL grants anything to, other than `userSid` and the trusted set.
|
|
22
|
+
* Deny and inherit-only entries grant nothing to this file and are skipped.
|
|
23
|
+
* Undefined when the text holds no DACL at all.
|
|
24
|
+
*
|
|
25
|
+
* A conditional or callback ACE (`X*`, `Z*`) counts as broader whoever it names: its
|
|
26
|
+
* nested parentheses end the parse, so the entries after it go unread.
|
|
27
|
+
*/
|
|
28
|
+
export declare function grantsBeyond(sddl: string, userSid: string): readonly string[] | undefined;
|
|
29
|
+
export declare function windowsFileAcl(run?: SyncCommandRunner): FileAcl;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Windows arm of "owner-only". NTFS has no POSIX mode, so a `0600` write
|
|
3
|
+
* protects nothing there; the equivalent is a DACL naming only the current user.
|
|
4
|
+
*
|
|
5
|
+
* Synchronous because it runs inside the credentials-file lock, which is.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, win32 } from "node:path";
|
|
10
|
+
import { failureReason, runCredentialCommandSync } from "./keychain/runner.js";
|
|
11
|
+
/** A local call; long only because Windows Defender can stall a first spawn. */
|
|
12
|
+
const ACL_TIMEOUT_MS = 10_000;
|
|
13
|
+
/** Absolute, because Windows searches the current directory before PATH. */
|
|
14
|
+
function system32(tool) {
|
|
15
|
+
return win32.join(process.env["SystemRoot"] ?? "C:\\Windows", "System32", tool);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Principals that may hold access besides the user, as root can read a `0600` file.
|
|
19
|
+
* Both forms, since SDDL abbreviates well-known SIDs.
|
|
20
|
+
*/
|
|
21
|
+
const TRUSTED = new Set(["SY", "S-1-5-18", "BA", "S-1-5-32-544"]);
|
|
22
|
+
/** Reads the SID from `whoami /user`, whose CSV form is `"host\user","S-1-5-21-..."`. */
|
|
23
|
+
export function parseUserSid(stdout) {
|
|
24
|
+
return /"(S-1-[0-9-]+)"\s*$/m.exec(stdout)?.[1];
|
|
25
|
+
}
|
|
26
|
+
/** SDDL writes the machine's built-in Administrator and Guest accounts as aliases, not SIDs. */
|
|
27
|
+
const ACCOUNT_ALIASES = { LA: "-500", LG: "-501" };
|
|
28
|
+
function isUser(sid, userSid) {
|
|
29
|
+
const rid = ACCOUNT_ALIASES[sid];
|
|
30
|
+
return sid === userSid || (rid !== undefined && userSid.endsWith(rid));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Which SIDs the DACL grants anything to, other than `userSid` and the trusted set.
|
|
34
|
+
* Deny and inherit-only entries grant nothing to this file and are skipped.
|
|
35
|
+
* Undefined when the text holds no DACL at all.
|
|
36
|
+
*
|
|
37
|
+
* A conditional or callback ACE (`X*`, `Z*`) counts as broader whoever it names: its
|
|
38
|
+
* nested parentheses end the parse, so the entries after it go unread.
|
|
39
|
+
*/
|
|
40
|
+
export function grantsBeyond(sddl, userSid) {
|
|
41
|
+
const dacl = /D:([A-Z_]*)((?:\([^)]*\))*)/.exec(sddl);
|
|
42
|
+
if (dacl === null)
|
|
43
|
+
return undefined;
|
|
44
|
+
// A null DACL: no access control at all, so everyone has full access.
|
|
45
|
+
if (dacl[1].includes("NO_ACCESS_CONTROL"))
|
|
46
|
+
return ["WD"];
|
|
47
|
+
const others = new Set();
|
|
48
|
+
for (const [, ace] of dacl[2].matchAll(/\(([^)]*)\)/g)) {
|
|
49
|
+
const [type = "", flags = "", , , , sid = ""] = ace.split(";");
|
|
50
|
+
const conditional = /^[XZ]/.test(type);
|
|
51
|
+
if (!conditional && (type.includes("D") || flags.includes("IO")))
|
|
52
|
+
continue;
|
|
53
|
+
if (conditional || (!isUser(sid, userSid) && !TRUSTED.has(sid)))
|
|
54
|
+
others.add(sid);
|
|
55
|
+
}
|
|
56
|
+
return [...others];
|
|
57
|
+
}
|
|
58
|
+
export function windowsFileAcl(run = runCredentialCommandSync) {
|
|
59
|
+
let userSid;
|
|
60
|
+
// One command reads the file several times, at two processes per inspection.
|
|
61
|
+
const inspected = new Map();
|
|
62
|
+
const currentUser = () => {
|
|
63
|
+
if (userSid !== undefined)
|
|
64
|
+
return userSid;
|
|
65
|
+
const outcome = run(system32("whoami.exe"), ["/user", "/fo", "csv", "/nh"], { timeoutMs: ACL_TIMEOUT_MS });
|
|
66
|
+
const sid = outcome.code === 0 ? parseUserSid(outcome.stdout) : undefined;
|
|
67
|
+
if (sid === undefined)
|
|
68
|
+
throw new Error(`could not identify the current user (whoami: ${failureReason(outcome)})`);
|
|
69
|
+
userSid = sid;
|
|
70
|
+
return sid;
|
|
71
|
+
};
|
|
72
|
+
const icacls = (args) => {
|
|
73
|
+
const outcome = run(system32("icacls.exe"), args, { timeoutMs: ACL_TIMEOUT_MS });
|
|
74
|
+
if (outcome.code !== 0)
|
|
75
|
+
throw new Error(`icacls: ${failureReason(outcome)}`);
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
restrictToOwner(path) {
|
|
79
|
+
try {
|
|
80
|
+
// `*SID` rather than a name: names are localised and may not round-trip through a console codepage.
|
|
81
|
+
icacls([path, "/inheritance:r", "/grant:r", `*${currentUser()}:F`]);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return err.message;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
inspect(path) {
|
|
89
|
+
// A rewrite renames in a new file, and an ACL edit moves ctime, so either invalidates this.
|
|
90
|
+
let version;
|
|
91
|
+
try {
|
|
92
|
+
const { ino, ctimeMs } = statSync(path);
|
|
93
|
+
version = `${ino}:${ctimeMs}`;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Unreadable here is left to icacls to report.
|
|
97
|
+
}
|
|
98
|
+
const cached = inspected.get(path);
|
|
99
|
+
if (version !== undefined && cached?.version === version)
|
|
100
|
+
return cached.result;
|
|
101
|
+
const result = inspectNow(path);
|
|
102
|
+
if (version !== undefined && result.kind !== "unknown")
|
|
103
|
+
inspected.set(path, { version, result });
|
|
104
|
+
return result;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
function inspectNow(path) {
|
|
108
|
+
// `/save` is the only icacls output with raw SIDs, and it writes only to a file.
|
|
109
|
+
let scratch;
|
|
110
|
+
try {
|
|
111
|
+
const sid = currentUser();
|
|
112
|
+
scratch = mkdtempSync(join(tmpdir(), "ai21-gateway-acl-"));
|
|
113
|
+
const saved = join(scratch, "acl");
|
|
114
|
+
icacls([path, "/save", saved]);
|
|
115
|
+
const others = grantsBeyond(readFileSync(saved, "utf16le"), sid);
|
|
116
|
+
if (others === undefined)
|
|
117
|
+
return { kind: "unknown", reason: "icacls /save wrote no DACL" };
|
|
118
|
+
return others.length === 0 ? { kind: "owner-only" } : { kind: "broader", others };
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return { kind: "unknown", reason: err.message };
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (scratch !== undefined)
|
|
125
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=fileAcl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileAcl.js","sourceRoot":"","sources":["../../src/login/fileAcl.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Linux tier: the Secret Service, through `secret-tool` from libsecret.
|
|
3
|
+
*
|
|
4
|
+
* Usually absent: `libsecret-tools` is not installed by default, and servers, containers
|
|
5
|
+
* and SSH sessions have no Secret Service on the bus. So the file tier is the normal
|
|
6
|
+
* outcome on Linux. Availability is presence on PATH, never `secret-tool --version`,
|
|
7
|
+
* which exits 2. WSL is excluded: it rarely has a session bus.
|
|
8
|
+
*
|
|
9
|
+
* The secret goes in on stdin and comes out on stdout unchanged, so unlike macOS it needs
|
|
10
|
+
* no encoding.
|
|
11
|
+
*/
|
|
12
|
+
import { type PathLookup } from "../../verify.js";
|
|
13
|
+
import type { SecretStore } from "../secretStore.js";
|
|
14
|
+
import { type CommandRunner } from "./runner.js";
|
|
15
|
+
export declare const SECRET_TOOL = "secret-tool";
|
|
16
|
+
export interface LinuxKeychainDeps {
|
|
17
|
+
readonly run: CommandRunner;
|
|
18
|
+
readonly lookup: PathLookup;
|
|
19
|
+
/** `/proc/sys/kernel/osrelease`, or undefined where it cannot be read. */
|
|
20
|
+
readonly osRelease: () => string | undefined;
|
|
21
|
+
/** Overridden only by tests, so they cannot collide with real entries. */
|
|
22
|
+
readonly service?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** "microsoft" on WSL2, "Microsoft" on WSL1. */
|
|
25
|
+
export declare function isWsl(osRelease: string | undefined): boolean;
|
|
26
|
+
/** The path to `secret-tool` when this machine can use it, otherwise undefined. */
|
|
27
|
+
export declare function findSecretTool(deps: LinuxKeychainDeps): string | undefined;
|
|
28
|
+
export declare function linuxKeychainStore(deps: LinuxKeychainDeps, executable: string): SecretStore;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Linux tier: the Secret Service, through `secret-tool` from libsecret.
|
|
3
|
+
*
|
|
4
|
+
* Usually absent: `libsecret-tools` is not installed by default, and servers, containers
|
|
5
|
+
* and SSH sessions have no Secret Service on the bus. So the file tier is the normal
|
|
6
|
+
* outcome on Linux. Availability is presence on PATH, never `secret-tool --version`,
|
|
7
|
+
* which exits 2. WSL is excluded: it rarely has a session bus.
|
|
8
|
+
*
|
|
9
|
+
* The secret goes in on stdin and comes out on stdout unchanged, so unlike macOS it needs
|
|
10
|
+
* no encoding.
|
|
11
|
+
*/
|
|
12
|
+
import { findOnPath } from "../../verify.js";
|
|
13
|
+
import { KEYCHAIN_SERVICE, KEYCHAIN_TIMEOUT_MS, accountFor, failureReason } from "./runner.js";
|
|
14
|
+
export const SECRET_TOOL = "secret-tool";
|
|
15
|
+
/** How the entry is labelled in Seahorse or KWalletManager, for a human deleting it. */
|
|
16
|
+
const LABEL = "AI21 Gateway CLI";
|
|
17
|
+
/** "microsoft" on WSL2, "Microsoft" on WSL1. */
|
|
18
|
+
export function isWsl(osRelease) {
|
|
19
|
+
return (osRelease ?? "").toLowerCase().includes("microsoft");
|
|
20
|
+
}
|
|
21
|
+
/** The path to `secret-tool` when this machine can use it, otherwise undefined. */
|
|
22
|
+
export function findSecretTool(deps) {
|
|
23
|
+
if (deps.lookup.platform !== "linux" || isWsl(deps.osRelease()))
|
|
24
|
+
return undefined;
|
|
25
|
+
return findOnPath(SECRET_TOOL, deps.lookup);
|
|
26
|
+
}
|
|
27
|
+
export function linuxKeychainStore(deps, executable) {
|
|
28
|
+
const service = deps.service ?? KEYCHAIN_SERVICE;
|
|
29
|
+
const attributes = (gateway, name) => ["service", service, "account", accountFor(gateway, name)];
|
|
30
|
+
const run = (args, input) => deps.run(executable, args, { input, timeoutMs: KEYCHAIN_TIMEOUT_MS });
|
|
31
|
+
const read = async (gateway, name) => {
|
|
32
|
+
const outcome = await run(["lookup", ...attributes(gateway, name)]);
|
|
33
|
+
// A timeout throws so the caller stops asking; any other miss reads as "not stored".
|
|
34
|
+
if (outcome.timedOut)
|
|
35
|
+
throw new Error("the secret service did not answer in time");
|
|
36
|
+
if (outcome.code !== 0 || outcome.stdout === "")
|
|
37
|
+
return undefined;
|
|
38
|
+
// As-is: `secret-tool` adds a newline only when stdout is a terminal, never on a pipe.
|
|
39
|
+
return outcome.stdout;
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
tier: "keychain",
|
|
43
|
+
read,
|
|
44
|
+
async write(gateway, name, value) {
|
|
45
|
+
const outcome = await run(["store", "--label", LABEL, ...attributes(gateway, name)], value);
|
|
46
|
+
if (outcome.code !== 0)
|
|
47
|
+
throw new Error(`the secret service rejected the write: ${failureReason(outcome)}`);
|
|
48
|
+
},
|
|
49
|
+
async forget(gateway, name) {
|
|
50
|
+
// `clear` exits 0 whether or not anything matched, so a lookup answers "was it there".
|
|
51
|
+
const existing = await read(gateway, name);
|
|
52
|
+
await run(["clear", ...attributes(gateway, name)]);
|
|
53
|
+
return existing !== undefined;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=linux.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linux.js","sourceRoot":"","sources":["../../../src/login/keychain/linux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAmB,MAAM,iBAAiB,CAAC;AAE9D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,UAAU,EAAE,aAAa,EAAsB,MAAM,aAAa,CAAC;AAEnH,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAC;AAEzC,wFAAwF;AACxF,MAAM,KAAK,GAAG,kBAAkB,CAAC;AAWjC,gDAAgD;AAChD,MAAM,UAAU,KAAK,CAAC,SAA6B;IACjD,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/D,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAAC,IAAuB;IACpD,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IAElF,OAAO,UAAU,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAuB,EAAE,UAAkB;IAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC;IACjD,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,IAAgB,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;IACrH,MAAM,GAAG,GAAG,CAAC,IAAuB,EAAE,KAAc,EAAE,EAAE,CACtD,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,CAAC,CAAC;IAExE,MAAM,IAAI,GAAG,KAAK,EAAE,OAAe,EAAE,IAAgB,EAA+B,EAAE;QACpF,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAEpE,qFAAqF;QACrF,IAAI,OAAO,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;QAEnF,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE;YAAE,OAAO,SAAS,CAAC;QAElE,uFAAuF;QACvF,OAAO,OAAO,CAAC,MAAM,CAAC;IACxB,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,UAAU;QAEhB,IAAI;QAEJ,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK;YAC9B,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YAE5F,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC9G,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI;YACxB,uFAAuF;YACvF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC3C,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;YAEnD,OAAO,QAAQ,KAAK,SAAS,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The macOS tier: generic passwords in the login keychain, via `/usr/bin/security`.
|
|
3
|
+
*
|
|
4
|
+
* One entry per secret, so the refresh token and the AI21 key keep the independent
|
|
5
|
+
* lifetimes the store promises. The account carries the gateway, so a staging secret is
|
|
6
|
+
* never returned for production:
|
|
7
|
+
*
|
|
8
|
+
* service ai21-gateway
|
|
9
|
+
* account https://api.ai21.com/gateway#ai21Key
|
|
10
|
+
*
|
|
11
|
+
* Writes go through `security -i`, which reads the command from stdin: the one-shot form
|
|
12
|
+
* takes the secret as `-w <value>`, visible in `ps`. Values are base64 so they need no
|
|
13
|
+
* quoting there, and so `security` does not hex-encode non-ASCII on the way out.
|
|
14
|
+
*
|
|
15
|
+
* No `-T` list, so any process running as this user can read the entries through
|
|
16
|
+
* `security`, as it can the file. What the keychain adds is encryption at rest and no
|
|
17
|
+
* plaintext in backups, not protection from the user's own processes.
|
|
18
|
+
*/
|
|
19
|
+
import type { SecretStore } from "../secretStore.js";
|
|
20
|
+
import { type CommandRunner } from "./runner.js";
|
|
21
|
+
export declare const SECURITY_BIN = "/usr/bin/security";
|
|
22
|
+
export interface MacosKeychainDeps {
|
|
23
|
+
readonly run: CommandRunner;
|
|
24
|
+
readonly platform: NodeJS.Platform;
|
|
25
|
+
/** Overridden only by the live CI test, so it cannot collide with real entries. */
|
|
26
|
+
readonly service?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** `security` ships with every macOS. Whether the keychain answers is found out at runtime. */
|
|
29
|
+
export declare function macosKeychainAvailable(deps: MacosKeychainDeps): boolean;
|
|
30
|
+
export declare function macosKeychainStore(deps: MacosKeychainDeps): SecretStore;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { KEYCHAIN_SERVICE, KEYCHAIN_TIMEOUT_MS, accountFor, failureReason } from "./runner.js";
|
|
2
|
+
export const SECURITY_BIN = "/usr/bin/security";
|
|
3
|
+
/** Marks a value this tool wrote; anything else under the service is not ours to decode. */
|
|
4
|
+
const ENCODING_PREFIX = "ai21-b64:";
|
|
5
|
+
/** `security` ships with every macOS. Whether the keychain answers is found out at runtime. */
|
|
6
|
+
export function macosKeychainAvailable(deps) {
|
|
7
|
+
return deps.platform === "darwin";
|
|
8
|
+
}
|
|
9
|
+
/** Single-quoted for `security -i`, which is not a POSIX shell: it backslash-escapes inside quotes. */
|
|
10
|
+
function quote(value) {
|
|
11
|
+
return `'${value.replace(/[\\']/g, (c) => `\\${c}`)}'`;
|
|
12
|
+
}
|
|
13
|
+
export function macosKeychainStore(deps) {
|
|
14
|
+
const service = deps.service ?? KEYCHAIN_SERVICE;
|
|
15
|
+
const run = (args, input) => deps.run(SECURITY_BIN, args, { input, timeoutMs: KEYCHAIN_TIMEOUT_MS });
|
|
16
|
+
return {
|
|
17
|
+
tier: "keychain",
|
|
18
|
+
async read(gateway, name) {
|
|
19
|
+
const account = accountFor(gateway, name);
|
|
20
|
+
const outcome = await run(["find-generic-password", "-s", service, "-a", account, "-w"]);
|
|
21
|
+
// A timeout throws so the caller stops asking; any other miss reads as "not stored".
|
|
22
|
+
if (outcome.timedOut)
|
|
23
|
+
throw new Error("the macOS keychain did not answer in time");
|
|
24
|
+
if (outcome.code !== 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
return decode(outcome.stdout.trim());
|
|
27
|
+
},
|
|
28
|
+
async write(gateway, name, value) {
|
|
29
|
+
const account = accountFor(gateway, name);
|
|
30
|
+
// -U updates in place; without it a second sign-in errors on the existing entry.
|
|
31
|
+
const command = ["add-generic-password", "-U", "-s", quote(service), "-a", quote(account), "-w", quote(encode(value))].join(" ") + "\n";
|
|
32
|
+
const outcome = await run(["-i"], command);
|
|
33
|
+
if (outcome.code !== 0) {
|
|
34
|
+
throw new Error(`the macOS keychain rejected the write: ${failureReason(outcome)}`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
async forget(gateway, name) {
|
|
38
|
+
const account = accountFor(gateway, name);
|
|
39
|
+
const outcome = await run(["delete-generic-password", "-s", service, "-a", account]);
|
|
40
|
+
return outcome.code === 0;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function encode(value) {
|
|
45
|
+
return `${ENCODING_PREFIX}${Buffer.from(value, "utf8").toString("base64")}`;
|
|
46
|
+
}
|
|
47
|
+
function decode(stored) {
|
|
48
|
+
if (!stored.startsWith(ENCODING_PREFIX))
|
|
49
|
+
return undefined;
|
|
50
|
+
return Buffer.from(stored.slice(ENCODING_PREFIX.length), "base64").toString("utf8");
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=macos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"macos.js","sourceRoot":"","sources":["../../../src/login/keychain/macos.ts"],"names":[],"mappings":"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"}
|