ai-remote 0.4.13 → 0.4.15

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.
@@ -0,0 +1,412 @@
1
+ import {
2
+ SshReader,
3
+ SshWriter,
4
+ concatBytes,
5
+ fromBase64,
6
+ padStart,
7
+ toBase64Url
8
+ } from "./cli-chunk-XT2FISR5.mjs";
9
+
10
+ // src/cli/identities.ts
11
+ import { connect } from "node:net";
12
+ import { createPrivateKey } from "node:crypto";
13
+ import { readFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ // src/protocols/ssh/identity.ts
18
+ var subtle = globalThis.crypto?.subtle;
19
+ var CURVES = {
20
+ "nistp256": { jwk: "P-256", hash: "SHA-256", size: 32 },
21
+ "nistp384": { jwk: "P-384", hash: "SHA-384", size: 48 },
22
+ "nistp521": { jwk: "P-521", hash: "SHA-512", size: 66 }
23
+ };
24
+ var RSA_HASHES = {
25
+ "rsa-sha2-512": "SHA-512",
26
+ "rsa-sha2-256": "SHA-256",
27
+ "ssh-rsa": "SHA-1"
28
+ };
29
+ var OPENSSH_MAGIC = "openssh-key-v1\0";
30
+ var b64u = (bytes) => toBase64Url(bytes);
31
+ function pemBody(text, label) {
32
+ const match = new RegExp(
33
+ `-----BEGIN ${label}-----([\\s\\S]*?)-----END ${label}-----`
34
+ ).exec(text);
35
+ if (!match) return null;
36
+ return fromBase64(match[1].replace(/\s+/g, ""));
37
+ }
38
+ async function parsePrivateKey(text, comment = "") {
39
+ const body = pemBody(text, "OPENSSH PRIVATE KEY");
40
+ if (body) return parseOpenSshKey(body, comment);
41
+ const pkcs8 = pemBody(text, "PRIVATE KEY");
42
+ if (pkcs8) return fromPkcs8(pkcs8, comment);
43
+ if (/BEGIN (RSA|EC|DSA) PRIVATE KEY/.test(text)) {
44
+ throw new Error(
45
+ "this is a legacy PEM key; convert it with `ssh-keygen -p -m RFC4716 -f <file>`"
46
+ );
47
+ }
48
+ if (/ENCRYPTED PRIVATE KEY/.test(text)) {
49
+ throw new Error("this key is encrypted; add it to ssh-agent with `ssh-add`");
50
+ }
51
+ throw new Error("this does not look like a private key");
52
+ }
53
+ async function parseOpenSshKey(body, comment) {
54
+ const magic = new TextDecoder().decode(body.subarray(0, OPENSSH_MAGIC.length));
55
+ if (magic !== OPENSSH_MAGIC) throw new Error("this is not an OpenSSH private key");
56
+ const reader = new SshReader(body, OPENSSH_MAGIC.length);
57
+ const cipher = reader.string();
58
+ reader.string();
59
+ reader.stringBytes();
60
+ const count = reader.u32();
61
+ if (count !== 1) throw new Error(`this file holds ${count} keys; one was expected`);
62
+ const keyBlob = reader.stringBytes();
63
+ const secret = reader.stringBytes();
64
+ if (cipher !== "none") {
65
+ throw new Error("this key has a passphrase; add it to ssh-agent with `ssh-add`");
66
+ }
67
+ const inner = new SshReader(secret, 0);
68
+ const check1 = inner.u32();
69
+ const check2 = inner.u32();
70
+ if (check1 !== check2) throw new Error("this key file is damaged");
71
+ const keyType = inner.string();
72
+ const identity = await fromSecret(keyType, inner, keyBlob, comment);
73
+ return identity.comment ? identity : { ...identity, comment: readComment(inner) };
74
+ }
75
+ function readComment(reader) {
76
+ try {
77
+ return reader.string();
78
+ } catch {
79
+ return "";
80
+ }
81
+ }
82
+ async function fromSecret(keyType, reader, keyBlob, comment) {
83
+ if (keyType === "ssh-ed25519") {
84
+ const publicKey = reader.stringBytes();
85
+ const secret = reader.stringBytes();
86
+ const seed = secret.subarray(0, 32);
87
+ return ed25519Identity(keyBlob, publicKey, seed, comment);
88
+ }
89
+ if (keyType === "ssh-rsa") {
90
+ const n = reader.mpint();
91
+ const e = reader.mpint();
92
+ const d = reader.mpint();
93
+ const iqmp = reader.mpint();
94
+ const p = reader.mpint();
95
+ const q = reader.mpint();
96
+ return rsaIdentity(keyBlob, { n, e, d, p, q, iqmp }, comment);
97
+ }
98
+ if (keyType.startsWith("ecdsa-sha2-")) {
99
+ const curve = reader.string();
100
+ const point = reader.stringBytes();
101
+ const d = reader.mpint();
102
+ return ecdsaIdentity(keyBlob, curve, point, d, comment);
103
+ }
104
+ throw new Error(`${keyType} keys are not supported`);
105
+ }
106
+ async function ed25519Identity(keyBlob, publicKey, seed, comment) {
107
+ const key = await importJwk(
108
+ { kty: "OKP", crv: "Ed25519", x: b64u(publicKey), d: b64u(seed) },
109
+ { name: "Ed25519" }
110
+ );
111
+ return {
112
+ keyType: "ssh-ed25519",
113
+ keyBlob: keyBlob.length ? keyBlob : blobFor("ssh-ed25519", publicKey),
114
+ comment,
115
+ algorithms: ["ssh-ed25519"],
116
+ async sign(data, algorithm) {
117
+ const signature = new Uint8Array(await subtle.sign({ name: "Ed25519" }, key, signable(data)));
118
+ return signatureBlob(algorithm, signature);
119
+ }
120
+ };
121
+ }
122
+ async function rsaIdentity(keyBlob, parts, comment) {
123
+ const { n, e, d, p, q, iqmp } = parts;
124
+ const jwk = {
125
+ kty: "RSA",
126
+ n: b64u(n),
127
+ e: b64u(e),
128
+ d: b64u(d),
129
+ p: b64u(p),
130
+ q: b64u(q),
131
+ // OpenSSH stores neither exponent, because a signer can derive both. A JSON
132
+ // Web Key wants them, so they are derived here rather than left out -- a
133
+ // key missing them imports as a slower non-CRT key, or not at all.
134
+ dp: b64u(modulo(d, minusOne(p))),
135
+ dq: b64u(modulo(d, minusOne(q))),
136
+ qi: b64u(iqmp)
137
+ };
138
+ const keys = /* @__PURE__ */ new Map();
139
+ const keyFor = (hash) => {
140
+ let pending = keys.get(hash);
141
+ if (!pending) {
142
+ pending = importJwk(jwk, { name: "RSASSA-PKCS1-v1_5", hash });
143
+ keys.set(hash, pending);
144
+ }
145
+ return pending;
146
+ };
147
+ return {
148
+ keyType: "ssh-rsa",
149
+ keyBlob: keyBlob.length ? keyBlob : concatBytes(
150
+ new SshWriter(32).string("ssh-rsa").take(),
151
+ new SshWriter(512).mpint(e).mpint(n).take()
152
+ ),
153
+ comment,
154
+ // SHA-1 last: a host new enough to refuse it will have taken one of the
155
+ // others, and one old enough to want it takes nothing else.
156
+ algorithms: ["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"],
157
+ async sign(data, algorithm) {
158
+ const hash = RSA_HASHES[algorithm] ?? "SHA-256";
159
+ const key = await keyFor(hash);
160
+ const signature = new Uint8Array(
161
+ await subtle.sign({ name: "RSASSA-PKCS1-v1_5" }, key, signable(data))
162
+ );
163
+ return signatureBlob(algorithm, signature);
164
+ }
165
+ };
166
+ }
167
+ async function ecdsaIdentity(keyBlob, curve, point, d, comment) {
168
+ const spec = CURVES[curve];
169
+ if (!spec) throw new Error(`the curve ${curve} is not supported`);
170
+ if (point[0] !== 4) throw new Error("the public point is in a form this cannot read");
171
+ const x = point.subarray(1, 1 + spec.size);
172
+ const y = point.subarray(1 + spec.size, 1 + spec.size * 2);
173
+ const key = await importJwk(
174
+ { kty: "EC", crv: spec.jwk, x: b64u(x), y: b64u(y), d: b64u(padStart(d, spec.size)) },
175
+ { name: "ECDSA", namedCurve: spec.jwk }
176
+ );
177
+ const algorithm = `ecdsa-sha2-${curve}`;
178
+ return {
179
+ keyType: algorithm,
180
+ keyBlob: keyBlob.length ? keyBlob : concatBytes(
181
+ new SshWriter(64).string(algorithm).string(curve).string(point).take()
182
+ ),
183
+ comment,
184
+ algorithms: [algorithm],
185
+ async sign(data) {
186
+ const raw = new Uint8Array(await subtle.sign(
187
+ { name: "ECDSA", hash: spec.hash },
188
+ key,
189
+ signable(data)
190
+ ));
191
+ const half = raw.length / 2;
192
+ const pair = new SshWriter(raw.length + 16).mpint(raw.subarray(0, half)).mpint(raw.subarray(half)).take();
193
+ return signatureBlob(algorithm, pair);
194
+ }
195
+ };
196
+ }
197
+ async function fromPkcs8(der, comment) {
198
+ const candidates = [
199
+ { name: "Ed25519" },
200
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
201
+ { name: "ECDSA", namedCurve: "P-256" },
202
+ { name: "ECDSA", namedCurve: "P-384" },
203
+ { name: "ECDSA", namedCurve: "P-521" }
204
+ ];
205
+ for (const algorithm of candidates) {
206
+ let jwk;
207
+ try {
208
+ const key = await subtle.importKey("pkcs8", signable(der), algorithm, true, ["sign"]);
209
+ jwk = await subtle.exportKey("jwk", key);
210
+ } catch {
211
+ continue;
212
+ }
213
+ const from = (value) => fromBase64Url(value ?? "");
214
+ if (jwk.kty === "OKP") {
215
+ const x = from(jwk.x);
216
+ return ed25519Identity(blobFor("ssh-ed25519", x), x, from(jwk.d), comment);
217
+ }
218
+ if (jwk.kty === "RSA") {
219
+ return rsaIdentity(new Uint8Array(0), {
220
+ n: from(jwk.n),
221
+ e: from(jwk.e),
222
+ d: from(jwk.d),
223
+ p: from(jwk.p),
224
+ q: from(jwk.q),
225
+ iqmp: from(jwk.qi)
226
+ }, comment);
227
+ }
228
+ if (jwk.kty === "EC") {
229
+ const curve = Object.entries(CURVES).find(([, spec2]) => spec2.jwk === jwk.crv);
230
+ if (!curve) continue;
231
+ const [name, spec] = curve;
232
+ const point = concatBytes(
233
+ new Uint8Array([4]),
234
+ padStart(from(jwk.x), spec.size),
235
+ padStart(from(jwk.y), spec.size)
236
+ );
237
+ return ecdsaIdentity(new Uint8Array(0), name, point, from(jwk.d), comment);
238
+ }
239
+ }
240
+ throw new Error("this key is of a type that cannot be used for SSH here");
241
+ }
242
+ function blobFor(type, publicKey) {
243
+ return new SshWriter(64).string(type).string(publicKey).take();
244
+ }
245
+ function signable(bytes) {
246
+ const copy = new Uint8Array(new ArrayBuffer(bytes.length));
247
+ copy.set(bytes);
248
+ return copy;
249
+ }
250
+ function signatureBlob(algorithm, signature) {
251
+ return new SshWriter(signature.length + 64).string(algorithm).string(signature).take();
252
+ }
253
+ function importJwk(jwk, algorithm) {
254
+ if (!subtle) throw new Error("this runtime has no Web Crypto, so it cannot sign with a key");
255
+ return subtle.importKey("jwk", jwk, algorithm, false, ["sign"]);
256
+ }
257
+ function fromBase64Url(text) {
258
+ const padded = text.replaceAll("-", "+").replaceAll("_", "/");
259
+ return fromBase64(padded + "=".repeat((4 - padded.length % 4) % 4));
260
+ }
261
+ var toBigInt = (bytes) => bytes.reduce((value, byte) => value << 8n | BigInt(byte), 0n);
262
+ function toBytes(value) {
263
+ let hex = value.toString(16);
264
+ if (hex.length % 2) hex = `0${hex}`;
265
+ const out = new Uint8Array(hex.length / 2);
266
+ for (let index = 0; index < out.length; index++) {
267
+ out[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
268
+ }
269
+ return out;
270
+ }
271
+ var minusOne = (bytes) => toBytes(toBigInt(bytes) - 1n);
272
+ var modulo = (a, b) => toBytes(toBigInt(a) % toBigInt(b));
273
+
274
+ // src/cli/identities.ts
275
+ var DEFAULT_KEYS = ["id_ed25519", "id_ecdsa", "id_rsa"];
276
+ var AGENT = {
277
+ REQUEST_IDENTITIES: 11,
278
+ IDENTITIES_ANSWER: 12,
279
+ SIGN_REQUEST: 13,
280
+ SIGN_RESPONSE: 14
281
+ };
282
+ var RSA_FLAGS = { "rsa-sha2-256": 2, "rsa-sha2-512": 4 };
283
+ async function loadIdentities(paths = []) {
284
+ const identities = [];
285
+ const skipped = [];
286
+ if (paths.length) {
287
+ for (const path of paths) await readKey(path, identities, skipped, true);
288
+ return { identities, skipped };
289
+ }
290
+ identities.push(...await agentIdentities().catch(() => []));
291
+ const held = new Set(identities.map((identity) => fingerprintOf(identity.keyBlob)));
292
+ for (const name of DEFAULT_KEYS) {
293
+ const path = join(homedir(), ".ssh", name);
294
+ const before = identities.length;
295
+ await readKey(path, identities, skipped, false);
296
+ if (identities.length > before && held.has(fingerprintOf(identities.at(-1).keyBlob))) {
297
+ identities.pop();
298
+ }
299
+ }
300
+ return { identities, skipped };
301
+ }
302
+ async function readKey(path, into, skipped, wanted) {
303
+ let text;
304
+ try {
305
+ text = readFileSync(path, "utf8");
306
+ } catch (error) {
307
+ const missing = error?.code === "ENOENT";
308
+ if (wanted) {
309
+ skipped.push(`${path}: ${missing ? "no such file" : error instanceof Error ? error.message : String(error)}`);
310
+ }
311
+ return;
312
+ }
313
+ try {
314
+ into.push(await parsePrivateKey(text, path));
315
+ return;
316
+ } catch (error) {
317
+ const message = error instanceof Error ? error.message : String(error);
318
+ const modern = /legacy PEM/.test(message) ? toPkcs8(text) : null;
319
+ if (!modern) {
320
+ skipped.push(`${path}: ${message}`);
321
+ return;
322
+ }
323
+ try {
324
+ into.push(await parsePrivateKey(modern, path));
325
+ } catch (second) {
326
+ skipped.push(`${path}: ${second instanceof Error ? second.message : String(second)}`);
327
+ }
328
+ }
329
+ }
330
+ function toPkcs8(text) {
331
+ try {
332
+ const der = createPrivateKey(text).export({ type: "pkcs8", format: "der" });
333
+ const base64 = Buffer.from(der).toString("base64").replace(/(.{64})/g, "$1\n");
334
+ return `-----BEGIN PRIVATE KEY-----
335
+ ${base64}
336
+ -----END PRIVATE KEY-----
337
+ `;
338
+ } catch {
339
+ return null;
340
+ }
341
+ }
342
+ function fingerprintOf(keyBlob) {
343
+ let hash = 2166136261;
344
+ for (const byte of keyBlob) {
345
+ hash ^= byte;
346
+ hash = Math.imul(hash, 16777619);
347
+ }
348
+ return `${keyBlob.length}:${hash >>> 0}`;
349
+ }
350
+ async function agentIdentities() {
351
+ const socketPath = process.env.SSH_AUTH_SOCK;
352
+ if (!socketPath) return [];
353
+ const answer = await agentRequest(socketPath, new SshWriter(8).u8(AGENT.REQUEST_IDENTITIES).take());
354
+ const reader = new SshReader(answer, 0);
355
+ if (reader.u8() !== AGENT.IDENTITIES_ANSWER) return [];
356
+ const count = reader.u32();
357
+ const identities = [];
358
+ for (let index = 0; index < count; index++) {
359
+ const keyBlob = reader.stringBytes().slice();
360
+ const comment = reader.string();
361
+ const keyType = new SshReader(keyBlob, 0).string();
362
+ identities.push({
363
+ keyType,
364
+ keyBlob,
365
+ comment: comment || `${keyType} (agent)`,
366
+ algorithms: keyType === "ssh-rsa" ? ["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"] : [keyType],
367
+ async sign(data, algorithm) {
368
+ const request = new SshWriter(keyBlob.length + data.length + 32).u8(AGENT.SIGN_REQUEST).string(keyBlob).string(data).u32(RSA_FLAGS[algorithm] ?? 0).take();
369
+ const response = new SshReader(await agentRequest(socketPath, request), 0);
370
+ if (response.u8() !== AGENT.SIGN_RESPONSE) {
371
+ throw new Error(`the agent would not sign with ${comment || keyType}`);
372
+ }
373
+ return response.stringBytes().slice();
374
+ }
375
+ });
376
+ }
377
+ return identities;
378
+ }
379
+ function agentRequest(socketPath, payload, timeoutMs = 5e3) {
380
+ return new Promise((resolve, reject) => {
381
+ const socket = connect(socketPath);
382
+ const chunks = [];
383
+ let length = -1;
384
+ const fail = (error) => {
385
+ socket.destroy();
386
+ reject(error);
387
+ };
388
+ socket.setTimeout(timeoutMs, () => fail(new Error("ssh-agent did not answer")));
389
+ socket.on("error", fail);
390
+ socket.on("connect", () => {
391
+ const header = Buffer.alloc(4);
392
+ header.writeUInt32BE(payload.length, 0);
393
+ socket.write(Buffer.concat([header, Buffer.from(payload)]));
394
+ });
395
+ socket.on("data", (chunk) => {
396
+ chunks.push(chunk);
397
+ const buffer = Buffer.concat(chunks);
398
+ if (length < 0 && buffer.length >= 4) length = buffer.readUInt32BE(0);
399
+ if (length >= 0 && buffer.length >= length + 4) {
400
+ socket.end();
401
+ resolve(new Uint8Array(buffer.subarray(4, 4 + length)));
402
+ }
403
+ });
404
+ socket.on("close", () => {
405
+ if (length < 0) reject(new Error("ssh-agent closed the connection"));
406
+ });
407
+ });
408
+ }
409
+ export {
410
+ agentIdentities,
411
+ loadIdentities
412
+ };
@@ -0,0 +1,218 @@
1
+ // src/cli/secrets.ts
2
+ import { spawn } from "node:child_process";
3
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { homedir, platform } from "node:os";
6
+ import { join } from "node:path";
7
+ var SERVICE = "ai-remote";
8
+ function configDir() {
9
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
10
+ return join(base, "ai-remote");
11
+ }
12
+ var legacyDir = () => join(homedir(), ".ai-remote");
13
+ var indexPath = (directory) => join(directory, "credentials.json");
14
+ var keyPath = (directory) => join(directory, "credentials.key");
15
+ function secretKey(host, port, username) {
16
+ return `${username || "anonymous"}@${host}:${port}`;
17
+ }
18
+ function readIndex(directory) {
19
+ try {
20
+ const parsed = JSON.parse(readFileSync(indexPath(directory), "utf8"));
21
+ return { entries: Array.isArray(parsed.entries) ? parsed.entries : [] };
22
+ } catch {
23
+ return { entries: [] };
24
+ }
25
+ }
26
+ function findEntry(key) {
27
+ for (const directory of [configDir(), legacyDir()]) {
28
+ const entry = readIndex(directory).entries.find((candidate) => candidate.key === key);
29
+ if (entry) return { entry, directory };
30
+ }
31
+ return null;
32
+ }
33
+ function writeIndex(directory, index) {
34
+ mkdirSync(directory, { recursive: true, mode: 448 });
35
+ writeFileSync(indexPath(directory), `${JSON.stringify(index, null, 2)}
36
+ `, { mode: 384 });
37
+ chmodSync(indexPath(directory), 384);
38
+ }
39
+ function run(command, args, input) {
40
+ return new Promise((resolve) => {
41
+ const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], detached: true });
42
+ let out = "";
43
+ child.stdout.on("data", (chunk) => {
44
+ out += chunk;
45
+ });
46
+ child.stderr.on("data", () => {
47
+ });
48
+ child.on("error", () => resolve({ code: -1, out: "" }));
49
+ child.on("close", (code) => resolve({ code: code ?? -1, out }));
50
+ if (input !== void 0) child.stdin.end(input);
51
+ else child.stdin.end();
52
+ });
53
+ }
54
+ var has = async (command) => (await run("command", ["-v", command])).code === 0 || (await run(command, ["--version"])).code >= 0;
55
+ async function chooseBackend(secret) {
56
+ const asked = process.env.AI_REMOTE_SECRET_BACKEND;
57
+ if (asked === "file" || asked === "keychain" || asked === "secret-service") return asked;
58
+ if (secret.includes("\n")) return "file";
59
+ if (platform() === "darwin") return "keychain";
60
+ if (platform() === "linux" && await has("secret-tool")) return "secret-service";
61
+ return "file";
62
+ }
63
+ async function writeToStore(backend, key, secret) {
64
+ if (backend === "keychain") {
65
+ const { code } = await run(
66
+ "security",
67
+ ["add-generic-password", "-U", "-s", SERVICE, "-a", key, "-D", "ai-remote password", "-w"],
68
+ `${secret}
69
+ ${secret}
70
+ `
71
+ );
72
+ return code === 0;
73
+ }
74
+ if (backend === "secret-service") {
75
+ const { code } = await run(
76
+ "secret-tool",
77
+ ["store", "--label", `ai-remote ${key}`, "service", SERVICE, "account", key],
78
+ secret
79
+ );
80
+ return code === 0;
81
+ }
82
+ return false;
83
+ }
84
+ async function readFromStore(backend, key) {
85
+ if (backend === "keychain") {
86
+ const { code, out } = await run(
87
+ "security",
88
+ ["find-generic-password", "-s", SERVICE, "-a", key, "-w"]
89
+ );
90
+ return code === 0 ? out.replace(/\n$/, "") : "";
91
+ }
92
+ if (backend === "secret-service") {
93
+ const { code, out } = await run("secret-tool", ["lookup", "service", SERVICE, "account", key]);
94
+ return code === 0 ? out : "";
95
+ }
96
+ return "";
97
+ }
98
+ async function deleteFromStore(backend, key) {
99
+ if (backend === "keychain") {
100
+ await run("security", ["delete-generic-password", "-s", SERVICE, "-a", key]);
101
+ }
102
+ if (backend === "secret-service") {
103
+ await run("secret-tool", ["clear", "service", SERVICE, "account", key]);
104
+ }
105
+ }
106
+ function fileKey(directory) {
107
+ const path = keyPath(directory);
108
+ if (existsSync(path)) {
109
+ const key2 = readFileSync(path);
110
+ if (key2.length === 32) return new Uint8Array(key2);
111
+ }
112
+ mkdirSync(directory, { recursive: true, mode: 448 });
113
+ const key = randomBytes(32);
114
+ writeFileSync(path, key, { mode: 384 });
115
+ chmodSync(path, 384);
116
+ return new Uint8Array(key);
117
+ }
118
+ function encrypt(directory, secret) {
119
+ const iv = randomBytes(12);
120
+ const cipher = createCipheriv("aes-256-gcm", fileKey(directory), iv);
121
+ const data = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
122
+ return {
123
+ iv: iv.toString("base64"),
124
+ tag: cipher.getAuthTag().toString("base64"),
125
+ data: data.toString("base64")
126
+ };
127
+ }
128
+ function decrypt(directory, stored) {
129
+ const decipher = createDecipheriv(
130
+ "aes-256-gcm",
131
+ fileKey(directory),
132
+ Buffer.from(stored.iv, "base64")
133
+ );
134
+ decipher.setAuthTag(Buffer.from(stored.tag, "base64"));
135
+ return Buffer.concat([
136
+ decipher.update(Buffer.from(stored.data, "base64")),
137
+ decipher.final()
138
+ ]).toString("utf8");
139
+ }
140
+ async function saveSecret(key, secret) {
141
+ const directory = configDir();
142
+ let backend = await chooseBackend(secret);
143
+ if (backend !== "file" && !await writeToStore(backend, key, secret)) {
144
+ backend = "file";
145
+ }
146
+ const index = readIndex(directory);
147
+ index.entries = index.entries.filter((entry) => entry.key !== key);
148
+ index.entries.push({
149
+ key,
150
+ backend,
151
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
152
+ secret: backend === "file" ? encrypt(directory, secret) : void 0
153
+ });
154
+ writeIndex(directory, index);
155
+ return { backend, where: describe(backend, directory) };
156
+ }
157
+ function describe(backend, directory = configDir()) {
158
+ if (backend === "keychain") return "your login keychain";
159
+ if (backend === "secret-service") return "your desktop keyring";
160
+ return `${indexPath(directory)}, encrypted under a key in the same directory`;
161
+ }
162
+ function backendName(backend) {
163
+ if (backend === "keychain") return "login keychain";
164
+ if (backend === "secret-service") return "desktop keyring";
165
+ return "encrypted file";
166
+ }
167
+ async function loadSecret(key) {
168
+ const found = findEntry(key);
169
+ if (!found) return "";
170
+ const { entry, directory } = found;
171
+ if (entry.backend === "file") {
172
+ if (!entry.secret) return "";
173
+ try {
174
+ return decrypt(directory, entry.secret);
175
+ } catch {
176
+ return "";
177
+ }
178
+ }
179
+ return readFromStore(entry.backend, key);
180
+ }
181
+ async function forgetSecret(key) {
182
+ let forgotten = false;
183
+ for (const directory of [configDir(), legacyDir()]) {
184
+ const index = readIndex(directory);
185
+ const entry = index.entries.find((candidate) => candidate.key === key);
186
+ if (!entry) continue;
187
+ if (entry.backend !== "file") await deleteFromStore(entry.backend, key);
188
+ index.entries = index.entries.filter((candidate) => candidate.key !== key);
189
+ writeIndex(directory, index);
190
+ forgotten = true;
191
+ }
192
+ return forgotten;
193
+ }
194
+ function savedSecrets() {
195
+ const seen = /* @__PURE__ */ new Map();
196
+ for (const directory of [configDir(), legacyDir()]) {
197
+ for (const entry of readIndex(directory).entries) {
198
+ if (!seen.has(entry.key)) seen.set(entry.key, entry);
199
+ }
200
+ }
201
+ return [...seen.values()].map(({ key, backend, savedAt }) => ({ key, backend, savedAt })).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
202
+ }
203
+ async function forgetAll() {
204
+ const keys = savedSecrets().map((entry) => entry.key);
205
+ for (const key of keys) await forgetSecret(key);
206
+ return keys.length;
207
+ }
208
+ export {
209
+ backendName,
210
+ configDir,
211
+ describe,
212
+ forgetAll,
213
+ forgetSecret,
214
+ loadSecret,
215
+ saveSecret,
216
+ savedSecrets,
217
+ secretKey
218
+ };
@@ -0,0 +1,10 @@
1
+ import {
2
+ Shell,
3
+ bareUsername
4
+ } from "./cli-chunk-RBCU3NX4.mjs";
5
+ import "./cli-chunk-EYQCDSPT.mjs";
6
+ import "./cli-chunk-XT2FISR5.mjs";
7
+ export {
8
+ Shell,
9
+ bareUsername
10
+ };
@@ -0,0 +1,15 @@
1
+ import {
2
+ detectCurrentDisplaySize,
3
+ nativeWindowAvailable,
4
+ openWindow,
5
+ readCurrentDisplaySize,
6
+ runWindow
7
+ } from "./cli-chunk-7UHHVT7Q.mjs";
8
+ import "./cli-chunk-K2DYJXC5.mjs";
9
+ export {
10
+ detectCurrentDisplaySize,
11
+ nativeWindowAvailable,
12
+ openWindow,
13
+ readCurrentDisplaySize,
14
+ runWindow
15
+ };