autho 1.0.0 → 2.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 +120 -0
- package/dist/autho.js +1873 -0
- package/package.json +27 -14
- package/app.js +0 -39
- package/bin.js +0 -117
- package/utils.js +0 -15
- package/wizards/createSecret.js +0 -124
- package/wizards/getEncryptionKey.js +0 -30
- package/wizards/getSecret.js +0 -31
package/dist/autho.js
ADDED
|
@@ -0,0 +1,1873 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// apps/cli/src/index.ts
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { existsSync as existsSync4 } from "fs";
|
|
7
|
+
import { createInterface } from "readline/promises";
|
|
8
|
+
import { resolve as resolve3 } from "path";
|
|
9
|
+
|
|
10
|
+
// packages/core/src/daemon.ts
|
|
11
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
12
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
|
|
13
|
+
|
|
14
|
+
// packages/storage/src/index.ts
|
|
15
|
+
import { Database } from "bun:sqlite";
|
|
16
|
+
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
17
|
+
import { dirname } from "path";
|
|
18
|
+
function parseJson(value) {
|
|
19
|
+
return JSON.parse(value);
|
|
20
|
+
}
|
|
21
|
+
function tryChmod(path, mode) {
|
|
22
|
+
if (process.platform === "win32") {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
chmodSync(path, mode);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class AuthoDatabase {
|
|
31
|
+
vaultPath;
|
|
32
|
+
db;
|
|
33
|
+
constructor(vaultPath) {
|
|
34
|
+
this.vaultPath = vaultPath;
|
|
35
|
+
if (vaultPath !== ":memory:") {
|
|
36
|
+
mkdirSync(dirname(vaultPath), { mode: 448, recursive: true });
|
|
37
|
+
tryChmod(dirname(vaultPath), 448);
|
|
38
|
+
}
|
|
39
|
+
this.db = new Database(vaultPath, { create: true, strict: true });
|
|
40
|
+
this.migrate();
|
|
41
|
+
this.hardenStorageFiles();
|
|
42
|
+
}
|
|
43
|
+
close() {
|
|
44
|
+
this.hardenStorageFiles();
|
|
45
|
+
this.db.close();
|
|
46
|
+
}
|
|
47
|
+
hardenStorageFiles() {
|
|
48
|
+
if (this.vaultPath === ":memory:") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const path of [this.vaultPath, `${this.vaultPath}-shm`, `${this.vaultPath}-wal`]) {
|
|
52
|
+
if (existsSync(path)) {
|
|
53
|
+
tryChmod(path, 384);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
migrate() {
|
|
58
|
+
this.db.exec(`
|
|
59
|
+
PRAGMA journal_mode = WAL;
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
62
|
+
key TEXT PRIMARY KEY,
|
|
63
|
+
value TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS secrets (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
name TEXT NOT NULL UNIQUE,
|
|
69
|
+
type TEXT NOT NULL,
|
|
70
|
+
payload TEXT NOT NULL,
|
|
71
|
+
wrapped_key TEXT NOT NULL,
|
|
72
|
+
created_at TEXT NOT NULL,
|
|
73
|
+
updated_at TEXT NOT NULL
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS leases (
|
|
77
|
+
id TEXT PRIMARY KEY,
|
|
78
|
+
name TEXT NOT NULL,
|
|
79
|
+
secret_refs TEXT NOT NULL,
|
|
80
|
+
expires_at TEXT NOT NULL,
|
|
81
|
+
revoked_at TEXT,
|
|
82
|
+
created_at TEXT NOT NULL
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
86
|
+
id TEXT PRIMARY KEY,
|
|
87
|
+
event_type TEXT NOT NULL,
|
|
88
|
+
subject_type TEXT NOT NULL,
|
|
89
|
+
subject_ref TEXT,
|
|
90
|
+
message TEXT NOT NULL,
|
|
91
|
+
metadata TEXT NOT NULL,
|
|
92
|
+
created_at TEXT NOT NULL
|
|
93
|
+
);
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
getVaultConfig() {
|
|
97
|
+
const row = this.db.query("SELECT value FROM meta WHERE key = ?1").get("vault.config");
|
|
98
|
+
return row ? parseJson(row.value) : null;
|
|
99
|
+
}
|
|
100
|
+
setVaultConfig(config) {
|
|
101
|
+
this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)").run("vault.config", JSON.stringify(config));
|
|
102
|
+
}
|
|
103
|
+
countSecrets() {
|
|
104
|
+
const row = this.db.query("SELECT COUNT(*) AS count FROM secrets").get();
|
|
105
|
+
return row.count;
|
|
106
|
+
}
|
|
107
|
+
countActiveLeases(nowIso) {
|
|
108
|
+
const row = this.db.query(`SELECT COUNT(*) AS count
|
|
109
|
+
FROM leases
|
|
110
|
+
WHERE revoked_at IS NULL AND expires_at > ?1`).get(nowIso);
|
|
111
|
+
return row.count;
|
|
112
|
+
}
|
|
113
|
+
countAuditEvents() {
|
|
114
|
+
const row = this.db.query("SELECT COUNT(*) AS count FROM audit_events").get();
|
|
115
|
+
return row.count;
|
|
116
|
+
}
|
|
117
|
+
insertSecret(secret) {
|
|
118
|
+
this.db.query(`INSERT INTO secrets (id, name, type, payload, wrapped_key, created_at, updated_at)
|
|
119
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`).run(secret.id, secret.name, secret.type, secret.payload, secret.wrappedKey, secret.createdAt, secret.updatedAt);
|
|
120
|
+
}
|
|
121
|
+
listSecrets() {
|
|
122
|
+
return this.db.query(`SELECT
|
|
123
|
+
id,
|
|
124
|
+
name,
|
|
125
|
+
type,
|
|
126
|
+
payload,
|
|
127
|
+
wrapped_key AS wrappedKey,
|
|
128
|
+
created_at AS createdAt,
|
|
129
|
+
updated_at AS updatedAt
|
|
130
|
+
FROM secrets
|
|
131
|
+
ORDER BY created_at ASC`).all();
|
|
132
|
+
}
|
|
133
|
+
findSecret(ref) {
|
|
134
|
+
const row = this.db.query(`SELECT
|
|
135
|
+
id,
|
|
136
|
+
name,
|
|
137
|
+
type,
|
|
138
|
+
payload,
|
|
139
|
+
wrapped_key AS wrappedKey,
|
|
140
|
+
created_at AS createdAt,
|
|
141
|
+
updated_at AS updatedAt
|
|
142
|
+
FROM secrets
|
|
143
|
+
WHERE id = ?1 OR name = ?1
|
|
144
|
+
LIMIT 1`).get(ref);
|
|
145
|
+
return row ?? null;
|
|
146
|
+
}
|
|
147
|
+
deleteSecret(id) {
|
|
148
|
+
this.db.query("DELETE FROM secrets WHERE id = ?1").run(id);
|
|
149
|
+
}
|
|
150
|
+
insertLease(lease) {
|
|
151
|
+
this.db.query(`INSERT INTO leases (id, name, secret_refs, expires_at, revoked_at, created_at)
|
|
152
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6)`).run(lease.id, lease.name, lease.secretRefs, lease.expiresAt, lease.revokedAt, lease.createdAt);
|
|
153
|
+
}
|
|
154
|
+
findLease(id) {
|
|
155
|
+
const row = this.db.query(`SELECT
|
|
156
|
+
id,
|
|
157
|
+
name,
|
|
158
|
+
secret_refs AS secretRefs,
|
|
159
|
+
expires_at AS expiresAt,
|
|
160
|
+
revoked_at AS revokedAt,
|
|
161
|
+
created_at AS createdAt
|
|
162
|
+
FROM leases
|
|
163
|
+
WHERE id = ?1
|
|
164
|
+
LIMIT 1`).get(id);
|
|
165
|
+
return row ?? null;
|
|
166
|
+
}
|
|
167
|
+
revokeLease(id, revokedAt) {
|
|
168
|
+
this.db.query("UPDATE leases SET revoked_at = ?2 WHERE id = ?1").run(id, revokedAt);
|
|
169
|
+
}
|
|
170
|
+
insertAudit(event) {
|
|
171
|
+
this.db.query(`INSERT INTO audit_events (id, event_type, subject_type, subject_ref, message, metadata, created_at)
|
|
172
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`).run(event.id, event.eventType, event.subjectType, event.subjectRef, event.message, event.metadata, event.createdAt);
|
|
173
|
+
}
|
|
174
|
+
listAudit(limit) {
|
|
175
|
+
return this.db.query(`SELECT
|
|
176
|
+
id,
|
|
177
|
+
event_type AS eventType,
|
|
178
|
+
subject_type AS subjectType,
|
|
179
|
+
subject_ref AS subjectRef,
|
|
180
|
+
message,
|
|
181
|
+
metadata,
|
|
182
|
+
created_at AS createdAt
|
|
183
|
+
FROM audit_events
|
|
184
|
+
ORDER BY created_at DESC
|
|
185
|
+
LIMIT ?1`).all(limit);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// packages/crypto/src/index.ts
|
|
190
|
+
import {
|
|
191
|
+
createCipheriv,
|
|
192
|
+
createDecipheriv,
|
|
193
|
+
randomBytes,
|
|
194
|
+
scryptSync
|
|
195
|
+
} from "crypto";
|
|
196
|
+
var DEFAULT_KDF = {
|
|
197
|
+
keyLength: 32,
|
|
198
|
+
name: "scrypt",
|
|
199
|
+
salt: "",
|
|
200
|
+
N: 1 << 17,
|
|
201
|
+
p: 1,
|
|
202
|
+
r: 8
|
|
203
|
+
};
|
|
204
|
+
function toBuffer(value) {
|
|
205
|
+
return Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
|
|
206
|
+
}
|
|
207
|
+
function randomId(size = 16) {
|
|
208
|
+
return randomBytes(size).toString("hex");
|
|
209
|
+
}
|
|
210
|
+
function deriveKeyFromPassword(password, config) {
|
|
211
|
+
return scryptSync(password, Buffer.from(config.salt, "base64"), config.keyLength, {
|
|
212
|
+
maxmem: 256 * 1024 * 1024,
|
|
213
|
+
N: config.N,
|
|
214
|
+
p: config.p,
|
|
215
|
+
r: config.r
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function encryptWithKey(value, key, aad) {
|
|
219
|
+
const iv = randomBytes(12);
|
|
220
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
221
|
+
cipher.setAAD(Buffer.from(aad, "utf8"));
|
|
222
|
+
const ciphertext = Buffer.concat([cipher.update(toBuffer(value)), cipher.final()]);
|
|
223
|
+
return {
|
|
224
|
+
algorithm: "aes-256-gcm",
|
|
225
|
+
ciphertext: ciphertext.toString("base64"),
|
|
226
|
+
iv: iv.toString("base64"),
|
|
227
|
+
tag: cipher.getAuthTag().toString("base64")
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function decryptWithKey(blob, key, aad) {
|
|
231
|
+
const decipher = createDecipheriv(blob.algorithm, key, Buffer.from(blob.iv, "base64"));
|
|
232
|
+
decipher.setAAD(Buffer.from(aad, "utf8"));
|
|
233
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "base64"));
|
|
234
|
+
return Buffer.concat([
|
|
235
|
+
decipher.update(Buffer.from(blob.ciphertext, "base64")),
|
|
236
|
+
decipher.final()
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
function createVaultConfig(password) {
|
|
240
|
+
const rootKey = randomBytes(32);
|
|
241
|
+
const kdf = {
|
|
242
|
+
...DEFAULT_KDF,
|
|
243
|
+
salt: randomBytes(16).toString("base64")
|
|
244
|
+
};
|
|
245
|
+
const key = deriveKeyFromPassword(password, kdf);
|
|
246
|
+
return {
|
|
247
|
+
config: {
|
|
248
|
+
createdAt: new Date().toISOString(),
|
|
249
|
+
kdf,
|
|
250
|
+
version: 1,
|
|
251
|
+
wrappedRootKey: encryptWithKey(rootKey, key, "autho:vault-root")
|
|
252
|
+
},
|
|
253
|
+
rootKey
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function unlockRootKey(password, config) {
|
|
257
|
+
const key = deriveKeyFromPassword(password, config.kdf);
|
|
258
|
+
return decryptWithKey(config.wrappedRootKey, key, "autho:vault-root");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// packages/core/src/index.ts
|
|
262
|
+
import { spawnSync } from "child_process";
|
|
263
|
+
import { createHmac, randomBytes as randomBytes3 } from "crypto";
|
|
264
|
+
import {
|
|
265
|
+
existsSync as existsSync2,
|
|
266
|
+
readFileSync as readFileSync2
|
|
267
|
+
} from "fs";
|
|
268
|
+
import { basename as basename2 } from "path";
|
|
269
|
+
|
|
270
|
+
// packages/core/src/artifacts.ts
|
|
271
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
272
|
+
import {
|
|
273
|
+
readdirSync,
|
|
274
|
+
readFileSync,
|
|
275
|
+
statSync
|
|
276
|
+
} from "fs";
|
|
277
|
+
import { basename, join as join2, relative, resolve as resolve2, sep } from "path";
|
|
278
|
+
|
|
279
|
+
// packages/core/src/paths.ts
|
|
280
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
281
|
+
import { homedir } from "os";
|
|
282
|
+
import { dirname as dirname2, join, resolve } from "path";
|
|
283
|
+
function normalizePath(path) {
|
|
284
|
+
return resolve(path).replace(/\\/g, "/");
|
|
285
|
+
}
|
|
286
|
+
function tryChmod2(path, mode) {
|
|
287
|
+
if (process.platform === "win32") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
chmodSync2(path, mode);
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
function authoHomeDir() {
|
|
295
|
+
return normalizePath(process.env.AUTHO_HOME ?? join(homedir(), ".autho"));
|
|
296
|
+
}
|
|
297
|
+
function defaultVaultPath() {
|
|
298
|
+
return normalizePath(join(authoHomeDir(), "vault.db"));
|
|
299
|
+
}
|
|
300
|
+
function defaultProjectFilePath() {
|
|
301
|
+
return normalizePath(join(authoHomeDir(), "project.json"));
|
|
302
|
+
}
|
|
303
|
+
function defaultDaemonStatePath() {
|
|
304
|
+
return normalizePath(join(authoHomeDir(), "daemon.json"));
|
|
305
|
+
}
|
|
306
|
+
function ensurePrivateDir(path) {
|
|
307
|
+
mkdirSync2(path, { mode: 448, recursive: true });
|
|
308
|
+
tryChmod2(path, 448);
|
|
309
|
+
}
|
|
310
|
+
function ensurePrivateParent(path) {
|
|
311
|
+
ensurePrivateDir(dirname2(path));
|
|
312
|
+
}
|
|
313
|
+
function hardenFilePermissions(path) {
|
|
314
|
+
tryChmod2(path, 384);
|
|
315
|
+
}
|
|
316
|
+
function writeTextFileSecure(path, content) {
|
|
317
|
+
ensurePrivateParent(path);
|
|
318
|
+
writeFileSync(path, content, { encoding: "utf8", mode: 384 });
|
|
319
|
+
hardenFilePermissions(path);
|
|
320
|
+
}
|
|
321
|
+
function writeBinaryFileSecure(path, content) {
|
|
322
|
+
ensurePrivateParent(path);
|
|
323
|
+
writeFileSync(path, content, { mode: 384 });
|
|
324
|
+
hardenFilePermissions(path);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// packages/core/src/artifacts.ts
|
|
328
|
+
function normalizeRelativePath(input) {
|
|
329
|
+
return input.replace(/\\/g, "/");
|
|
330
|
+
}
|
|
331
|
+
function walkFiles(rootPath) {
|
|
332
|
+
const entries = readdirSync(rootPath, { withFileTypes: true });
|
|
333
|
+
const files = [];
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
const entryPath = join2(rootPath, entry.name);
|
|
336
|
+
if (entry.isDirectory()) {
|
|
337
|
+
files.push(...walkFiles(entryPath));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (entry.isFile()) {
|
|
341
|
+
files.push(entryPath);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return files;
|
|
345
|
+
}
|
|
346
|
+
function defaultEncryptedFilePath(inputPath) {
|
|
347
|
+
return `${inputPath}.autho`;
|
|
348
|
+
}
|
|
349
|
+
function defaultDecryptedFilePath(inputPath) {
|
|
350
|
+
return inputPath.endsWith(".autho") ? inputPath.slice(0, -".autho".length) : `${inputPath}.decrypted`;
|
|
351
|
+
}
|
|
352
|
+
function defaultEncryptedFolderPath(inputPath) {
|
|
353
|
+
return `${inputPath}.autho-folder`;
|
|
354
|
+
}
|
|
355
|
+
function defaultDecryptedFolderPath(inputPath) {
|
|
356
|
+
return inputPath.endsWith(".autho-folder") ? inputPath.slice(0, -".autho-folder".length) : `${inputPath}.folder`;
|
|
357
|
+
}
|
|
358
|
+
function encryptFileArtifact(inputPath, outputPath, rootKey) {
|
|
359
|
+
const fileKey = randomBytes2(32);
|
|
360
|
+
const payload = encryptWithKey(readFileSync(inputPath), fileKey, `autho:file:${basename(inputPath)}`);
|
|
361
|
+
const envelope = {
|
|
362
|
+
kind: "file",
|
|
363
|
+
originalName: basename(inputPath),
|
|
364
|
+
payload,
|
|
365
|
+
version: 1,
|
|
366
|
+
wrappedKey: encryptWithKey(fileKey, rootKey, "autho:file:dek")
|
|
367
|
+
};
|
|
368
|
+
writeTextFileSecure(outputPath, JSON.stringify(envelope, null, 2));
|
|
369
|
+
return { outputPath };
|
|
370
|
+
}
|
|
371
|
+
function decryptFileArtifact(inputPath, outputPath, rootKey) {
|
|
372
|
+
const envelope = JSON.parse(readFileSync(inputPath, "utf8"));
|
|
373
|
+
if (envelope.kind !== "file" || envelope.version !== 1) {
|
|
374
|
+
throw new Error(`Unsupported file artifact: ${inputPath}`);
|
|
375
|
+
}
|
|
376
|
+
const fileKey = decryptWithKey(envelope.wrappedKey, rootKey, "autho:file:dek");
|
|
377
|
+
const content = decryptWithKey(envelope.payload, fileKey, `autho:file:${envelope.originalName}`);
|
|
378
|
+
writeBinaryFileSecure(outputPath, content);
|
|
379
|
+
return { outputPath };
|
|
380
|
+
}
|
|
381
|
+
function encryptFolderArtifact(inputPath, outputPath, rootKey) {
|
|
382
|
+
const folderKey = randomBytes2(32);
|
|
383
|
+
const files = walkFiles(inputPath);
|
|
384
|
+
const rootName = basename(inputPath);
|
|
385
|
+
const envelope = {
|
|
386
|
+
entries: files.map((filePath) => {
|
|
387
|
+
const relativePath = normalizeRelativePath(relative(inputPath, filePath));
|
|
388
|
+
return {
|
|
389
|
+
path: relativePath,
|
|
390
|
+
payload: encryptWithKey(readFileSync(filePath), folderKey, `autho:folder:${relativePath}`)
|
|
391
|
+
};
|
|
392
|
+
}),
|
|
393
|
+
kind: "folder",
|
|
394
|
+
rootName,
|
|
395
|
+
version: 1,
|
|
396
|
+
wrappedKey: encryptWithKey(folderKey, rootKey, `autho:folder:dek:${rootName}`)
|
|
397
|
+
};
|
|
398
|
+
writeTextFileSecure(outputPath, JSON.stringify(envelope, null, 2));
|
|
399
|
+
return {
|
|
400
|
+
fileCount: envelope.entries.length,
|
|
401
|
+
outputPath
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function decryptFolderArtifact(inputPath, outputPath, rootKey) {
|
|
405
|
+
const envelope = JSON.parse(readFileSync(inputPath, "utf8"));
|
|
406
|
+
if (envelope.kind !== "folder" || envelope.version !== 1) {
|
|
407
|
+
throw new Error(`Unsupported folder artifact: ${inputPath}`);
|
|
408
|
+
}
|
|
409
|
+
const folderKey = decryptWithKey(envelope.wrappedKey, rootKey, `autho:folder:dek:${envelope.rootName}`);
|
|
410
|
+
const resolvedOutput = resolve2(outputPath);
|
|
411
|
+
ensurePrivateDir(outputPath);
|
|
412
|
+
for (const entry of envelope.entries) {
|
|
413
|
+
const destination = join2(outputPath, entry.path);
|
|
414
|
+
const resolvedDest = resolve2(destination);
|
|
415
|
+
if (!resolvedDest.startsWith(resolvedOutput + sep) && resolvedDest !== resolvedOutput) {
|
|
416
|
+
throw new Error(`Path traversal detected in folder artifact: ${entry.path}`);
|
|
417
|
+
}
|
|
418
|
+
ensurePrivateParent(destination);
|
|
419
|
+
const content = decryptWithKey(entry.payload, folderKey, `autho:folder:${entry.path}`);
|
|
420
|
+
writeBinaryFileSecure(destination, content);
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
fileCount: envelope.entries.length,
|
|
424
|
+
outputPath
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function assertPathIsDirectory(path) {
|
|
428
|
+
if (!statSync(path).isDirectory()) {
|
|
429
|
+
throw new Error(`Expected directory: ${path}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function assertPathIsFile(path) {
|
|
433
|
+
if (!statSync(path).isFile()) {
|
|
434
|
+
throw new Error(`Expected file: ${path}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// packages/core/src/index.ts
|
|
439
|
+
function requireValue(value, label) {
|
|
440
|
+
if (!value) {
|
|
441
|
+
throw new Error(`Missing required option: ${label}`);
|
|
442
|
+
}
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
function decodeBase32(input) {
|
|
446
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
447
|
+
const normalized = input.toUpperCase().replace(/=+$/g, "").replace(/\s+/g, "");
|
|
448
|
+
let bits = 0;
|
|
449
|
+
let value = 0;
|
|
450
|
+
const output = [];
|
|
451
|
+
for (const char of normalized) {
|
|
452
|
+
const index = alphabet.indexOf(char);
|
|
453
|
+
if (index === -1) {
|
|
454
|
+
throw new Error("OTP secret must be valid base32");
|
|
455
|
+
}
|
|
456
|
+
value = value << 5 | index;
|
|
457
|
+
bits += 5;
|
|
458
|
+
if (bits >= 8) {
|
|
459
|
+
output.push(value >>> bits - 8 & 255);
|
|
460
|
+
bits -= 8;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return Uint8Array.from(output);
|
|
464
|
+
}
|
|
465
|
+
function generateTotp(secret, options, now = Date.now()) {
|
|
466
|
+
const algorithm = (options?.algorithm ?? "sha1").toLowerCase();
|
|
467
|
+
const digits = options?.digits ?? 6;
|
|
468
|
+
const key = decodeBase32(secret);
|
|
469
|
+
const counter = Math.floor(now / 30000);
|
|
470
|
+
const message = Buffer.alloc(8);
|
|
471
|
+
let cursor = counter;
|
|
472
|
+
for (let index = 7;index >= 0; index -= 1) {
|
|
473
|
+
message[index] = cursor & 255;
|
|
474
|
+
cursor >>= 8;
|
|
475
|
+
}
|
|
476
|
+
const hash = createHmac(algorithm, Buffer.from(key)).update(message).digest();
|
|
477
|
+
const offset = hash[hash.length - 1] & 15;
|
|
478
|
+
const binary = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
|
|
479
|
+
const mod = 10 ** digits;
|
|
480
|
+
const code = String(binary % mod).padStart(digits, "0");
|
|
481
|
+
const expiresAt = new Date((counter + 1) * 30000).toISOString();
|
|
482
|
+
return { code, expiresAt };
|
|
483
|
+
}
|
|
484
|
+
function normalizeSecretType(type) {
|
|
485
|
+
if (type === "password" || type === "note" || type === "otp") {
|
|
486
|
+
return type;
|
|
487
|
+
}
|
|
488
|
+
throw new Error(`Unsupported secret type: ${type}`);
|
|
489
|
+
}
|
|
490
|
+
function parseProjectMappings(projectFile) {
|
|
491
|
+
const raw = JSON.parse(readFileSync2(projectFile, "utf8"));
|
|
492
|
+
return Object.entries(raw.env ?? {}).map(([envName, secretRef]) => ({
|
|
493
|
+
envName,
|
|
494
|
+
secretRef
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
function parseLease(row) {
|
|
498
|
+
return {
|
|
499
|
+
createdAt: row.createdAt,
|
|
500
|
+
expiresAt: row.expiresAt,
|
|
501
|
+
id: row.id,
|
|
502
|
+
name: row.name,
|
|
503
|
+
revokedAt: row.revokedAt,
|
|
504
|
+
secretRefs: JSON.parse(row.secretRefs)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function toAuditEvent(row) {
|
|
508
|
+
return {
|
|
509
|
+
createdAt: row.createdAt,
|
|
510
|
+
eventType: row.eventType,
|
|
511
|
+
id: row.id,
|
|
512
|
+
message: row.message,
|
|
513
|
+
metadata: JSON.parse(row.metadata),
|
|
514
|
+
subjectRef: row.subjectRef,
|
|
515
|
+
subjectType: row.subjectType
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function normalizeMetadata(input) {
|
|
519
|
+
return Object.fromEntries(Object.entries(input ?? {}).filter(([, value]) => value !== undefined && value !== null && value !== ""));
|
|
520
|
+
}
|
|
521
|
+
function parseLegacySecret(secret) {
|
|
522
|
+
const type = normalizeSecretType(requireValue(secret.type, "legacy.type"));
|
|
523
|
+
const name = requireValue(secret.name, "legacy.name");
|
|
524
|
+
const value = requireValue(secret.secret ?? secret.value, "legacy.secret");
|
|
525
|
+
if (type === "password") {
|
|
526
|
+
return {
|
|
527
|
+
metadata: normalizeMetadata({
|
|
528
|
+
description: secret.description,
|
|
529
|
+
url: secret.url
|
|
530
|
+
}),
|
|
531
|
+
name,
|
|
532
|
+
type,
|
|
533
|
+
username: secret.username,
|
|
534
|
+
value
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (type === "otp") {
|
|
538
|
+
return {
|
|
539
|
+
metadata: normalizeMetadata({
|
|
540
|
+
algorithm: secret.algorithm ?? "SHA1",
|
|
541
|
+
description: secret.description,
|
|
542
|
+
digits: secret.digits ?? 6
|
|
543
|
+
}),
|
|
544
|
+
name,
|
|
545
|
+
type,
|
|
546
|
+
username: secret.username,
|
|
547
|
+
value
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
metadata: normalizeMetadata({
|
|
552
|
+
description: secret.description
|
|
553
|
+
}),
|
|
554
|
+
name,
|
|
555
|
+
type,
|
|
556
|
+
value
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function quoteEnvValue(value) {
|
|
560
|
+
return JSON.stringify(value);
|
|
561
|
+
}
|
|
562
|
+
function summarizeCommand(cmd) {
|
|
563
|
+
return {
|
|
564
|
+
argCount: Math.max(0, cmd.length - 1),
|
|
565
|
+
executable: basename2(cmd[0] ?? "unknown")
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function projectMappingsForStatus(projectFile) {
|
|
569
|
+
if (!projectFile || !existsSync2(projectFile)) {
|
|
570
|
+
return {
|
|
571
|
+
mappings: [],
|
|
572
|
+
path: projectFile ?? null
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
mappings: parseProjectMappings(projectFile).map((mapping) => mapping.envName),
|
|
577
|
+
path: projectFile
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function resolveMappings(options) {
|
|
581
|
+
const fromMaps = (options.maps ?? []).map((mapping) => {
|
|
582
|
+
const splitIndex = mapping.indexOf("=");
|
|
583
|
+
if (splitIndex === -1) {
|
|
584
|
+
throw new Error(`Invalid env mapping: ${mapping}`);
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
envName: mapping.slice(0, splitIndex),
|
|
588
|
+
secretRef: mapping.slice(splitIndex + 1)
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
if (options.projectFile) {
|
|
592
|
+
if (!existsSync2(options.projectFile)) {
|
|
593
|
+
throw new Error(`Project mapping file not found: ${options.projectFile}`);
|
|
594
|
+
}
|
|
595
|
+
return [...parseProjectMappings(options.projectFile), ...fromMaps];
|
|
596
|
+
}
|
|
597
|
+
return fromMaps;
|
|
598
|
+
}
|
|
599
|
+
function writeProjectConfig(input) {
|
|
600
|
+
if (input.mappings.length === 0) {
|
|
601
|
+
throw new Error("Provide at least one env mapping");
|
|
602
|
+
}
|
|
603
|
+
if (!input.force && existsSync2(input.outputPath)) {
|
|
604
|
+
throw new Error(`Project config already exists: ${input.outputPath}`);
|
|
605
|
+
}
|
|
606
|
+
const env = Object.fromEntries(input.mappings.map((mapping) => [mapping.envName, mapping.secretRef]));
|
|
607
|
+
writeTextFileSecure(input.outputPath, JSON.stringify({
|
|
608
|
+
env,
|
|
609
|
+
generatedAt: new Date().toISOString(),
|
|
610
|
+
version: 1
|
|
611
|
+
}, null, 2) + `
|
|
612
|
+
`);
|
|
613
|
+
return {
|
|
614
|
+
mappingCount: input.mappings.length,
|
|
615
|
+
outputPath: input.outputPath
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
class VaultService {
|
|
620
|
+
static initialize(vaultPath, password) {
|
|
621
|
+
const db = new AuthoDatabase(vaultPath);
|
|
622
|
+
try {
|
|
623
|
+
if (db.getVaultConfig()) {
|
|
624
|
+
throw new Error(`Vault already initialized at ${vaultPath}`);
|
|
625
|
+
}
|
|
626
|
+
const { config } = createVaultConfig(password);
|
|
627
|
+
db.setVaultConfig(config);
|
|
628
|
+
db.insertAudit({
|
|
629
|
+
createdAt: new Date().toISOString(),
|
|
630
|
+
eventType: "vault.initialized",
|
|
631
|
+
id: randomId(),
|
|
632
|
+
message: "Vault initialized",
|
|
633
|
+
metadata: JSON.stringify({ version: config.version }),
|
|
634
|
+
subjectRef: null,
|
|
635
|
+
subjectType: "vault"
|
|
636
|
+
});
|
|
637
|
+
return { vaultPath };
|
|
638
|
+
} finally {
|
|
639
|
+
db.close();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
static status(vaultPath, options) {
|
|
643
|
+
const db = new AuthoDatabase(vaultPath);
|
|
644
|
+
try {
|
|
645
|
+
const config = db.getVaultConfig();
|
|
646
|
+
const project = projectMappingsForStatus(options?.projectFile);
|
|
647
|
+
if (!config) {
|
|
648
|
+
return {
|
|
649
|
+
activeLeaseCount: 0,
|
|
650
|
+
auditEventCount: 0,
|
|
651
|
+
initialized: false,
|
|
652
|
+
projectFile: project.path,
|
|
653
|
+
projectMappings: project.mappings,
|
|
654
|
+
secretCount: 0,
|
|
655
|
+
unlocked: false,
|
|
656
|
+
vaultPath
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (!options?.password) {
|
|
660
|
+
return {
|
|
661
|
+
activeLeaseCount: 0,
|
|
662
|
+
auditEventCount: 0,
|
|
663
|
+
initialized: true,
|
|
664
|
+
projectFile: project.path,
|
|
665
|
+
projectMappings: project.mappings,
|
|
666
|
+
secretCount: 0,
|
|
667
|
+
unlocked: false,
|
|
668
|
+
vaultPath
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const rootKey = unlockRootKey(options.password, config);
|
|
672
|
+
const session = new VaultSession(db, rootKey);
|
|
673
|
+
return session.status(vaultPath, project.path, project.mappings);
|
|
674
|
+
} finally {
|
|
675
|
+
db.close();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
static unlock(vaultPath, password) {
|
|
679
|
+
const db = new AuthoDatabase(vaultPath);
|
|
680
|
+
const config = db.getVaultConfig();
|
|
681
|
+
if (!config) {
|
|
682
|
+
db.close();
|
|
683
|
+
throw new Error(`Vault is not initialized at ${vaultPath}`);
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const rootKey = unlockRootKey(password, config);
|
|
687
|
+
return new VaultSession(db, rootKey);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
db.close();
|
|
690
|
+
throw new Error("Invalid vault password", { cause: error });
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
class VaultSession {
|
|
696
|
+
db;
|
|
697
|
+
rootKey;
|
|
698
|
+
constructor(db, rootKey) {
|
|
699
|
+
this.db = db;
|
|
700
|
+
this.rootKey = rootKey;
|
|
701
|
+
}
|
|
702
|
+
close() {
|
|
703
|
+
this.db.close();
|
|
704
|
+
}
|
|
705
|
+
status(vaultPath, projectFile, projectMappings = []) {
|
|
706
|
+
return {
|
|
707
|
+
activeLeaseCount: this.db.countActiveLeases(new Date().toISOString()),
|
|
708
|
+
auditEventCount: this.db.countAuditEvents(),
|
|
709
|
+
initialized: true,
|
|
710
|
+
projectFile: projectFile ?? null,
|
|
711
|
+
projectMappings,
|
|
712
|
+
secretCount: this.db.countSecrets(),
|
|
713
|
+
unlocked: true,
|
|
714
|
+
vaultPath
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
audit(eventType, subjectType, subjectRef, message, metadata) {
|
|
718
|
+
this.db.insertAudit({
|
|
719
|
+
createdAt: new Date().toISOString(),
|
|
720
|
+
eventType,
|
|
721
|
+
id: randomId(),
|
|
722
|
+
message,
|
|
723
|
+
metadata: JSON.stringify(metadata),
|
|
724
|
+
subjectRef,
|
|
725
|
+
subjectType
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
unwrapSecret(row) {
|
|
729
|
+
const wrappedKey = JSON.parse(row.wrappedKey);
|
|
730
|
+
const payload = JSON.parse(row.payload);
|
|
731
|
+
const dek = decryptWithKey(wrappedKey, this.rootKey, `autho:secret:${row.id}:dek`);
|
|
732
|
+
const secret = JSON.parse(decryptWithKey(payload, dek, `autho:secret:${row.id}:payload`).toString("utf8"));
|
|
733
|
+
return {
|
|
734
|
+
createdAt: row.createdAt,
|
|
735
|
+
id: row.id,
|
|
736
|
+
metadata: normalizeMetadata(secret.metadata),
|
|
737
|
+
name: row.name,
|
|
738
|
+
type: normalizeSecretType(row.type),
|
|
739
|
+
updatedAt: row.updatedAt,
|
|
740
|
+
username: secret.username,
|
|
741
|
+
value: secret.value
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
getSecretOrThrow(ref) {
|
|
745
|
+
const row = this.db.findSecret(ref);
|
|
746
|
+
if (!row) {
|
|
747
|
+
throw new Error(`Secret not found: ${ref}`);
|
|
748
|
+
}
|
|
749
|
+
return this.unwrapSecret(row);
|
|
750
|
+
}
|
|
751
|
+
getLeaseOrThrow(id) {
|
|
752
|
+
const row = this.db.findLease(id);
|
|
753
|
+
if (!row) {
|
|
754
|
+
throw new Error(`Lease not found: ${id}`);
|
|
755
|
+
}
|
|
756
|
+
return parseLease(row);
|
|
757
|
+
}
|
|
758
|
+
assertLeaseAllows(leaseId, secretRef) {
|
|
759
|
+
if (!leaseId) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const lease = this.getLeaseOrThrow(leaseId);
|
|
763
|
+
if (lease.revokedAt) {
|
|
764
|
+
throw new Error(`Lease revoked: ${lease.id}`);
|
|
765
|
+
}
|
|
766
|
+
if (Date.parse(lease.expiresAt) <= Date.now()) {
|
|
767
|
+
throw new Error(`Lease expired: ${lease.id}`);
|
|
768
|
+
}
|
|
769
|
+
const secret = this.getSecretOrThrow(secretRef);
|
|
770
|
+
if (!lease.secretRefs.includes(secret.id) && !lease.secretRefs.includes(secret.name)) {
|
|
771
|
+
throw new Error(`Lease ${lease.id} does not allow secret ${secretRef}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
addSecret(input) {
|
|
775
|
+
requireValue(input.name, "--name");
|
|
776
|
+
requireValue(input.value, "--value");
|
|
777
|
+
const type = normalizeSecretType(input.type);
|
|
778
|
+
if (this.db.findSecret(input.name)) {
|
|
779
|
+
throw new Error(`Secret already exists: ${input.name}`);
|
|
780
|
+
}
|
|
781
|
+
const now = new Date().toISOString();
|
|
782
|
+
const id = randomId();
|
|
783
|
+
const dek = randomBytes3(32);
|
|
784
|
+
const payload = encryptWithKey(JSON.stringify({
|
|
785
|
+
metadata: normalizeMetadata(input.metadata),
|
|
786
|
+
username: input.username ?? null,
|
|
787
|
+
value: input.value
|
|
788
|
+
}), dek, `autho:secret:${id}:payload`);
|
|
789
|
+
const wrappedKey = encryptWithKey(dek, this.rootKey, `autho:secret:${id}:dek`);
|
|
790
|
+
this.db.insertSecret({
|
|
791
|
+
createdAt: now,
|
|
792
|
+
id,
|
|
793
|
+
name: input.name,
|
|
794
|
+
payload: JSON.stringify(payload),
|
|
795
|
+
type,
|
|
796
|
+
updatedAt: now,
|
|
797
|
+
wrappedKey: JSON.stringify(wrappedKey)
|
|
798
|
+
});
|
|
799
|
+
this.audit("secret.created", "secret", id, "Secret created", {
|
|
800
|
+
metadataKeyCount: Object.keys(normalizeMetadata(input.metadata)).length,
|
|
801
|
+
type
|
|
802
|
+
});
|
|
803
|
+
return {
|
|
804
|
+
createdAt: now,
|
|
805
|
+
id,
|
|
806
|
+
name: input.name,
|
|
807
|
+
type,
|
|
808
|
+
updatedAt: now
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
importLegacyFile(filePath, options) {
|
|
812
|
+
const raw = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
813
|
+
let imported = 0;
|
|
814
|
+
let skipped = 0;
|
|
815
|
+
for (const entry of raw) {
|
|
816
|
+
if (!entry) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const parsed = parseLegacySecret(entry);
|
|
820
|
+
if (this.db.findSecret(parsed.name)) {
|
|
821
|
+
if (options?.skipExisting ?? true) {
|
|
822
|
+
skipped += 1;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
throw new Error(`Secret already exists: ${parsed.name}`);
|
|
826
|
+
}
|
|
827
|
+
this.addSecret(parsed);
|
|
828
|
+
imported += 1;
|
|
829
|
+
}
|
|
830
|
+
this.audit("import.legacy", "vault", null, "Legacy backup imported", {
|
|
831
|
+
imported,
|
|
832
|
+
skipped
|
|
833
|
+
});
|
|
834
|
+
return { imported, skipped };
|
|
835
|
+
}
|
|
836
|
+
listSecrets() {
|
|
837
|
+
return this.db.listSecrets().map((row) => ({
|
|
838
|
+
createdAt: row.createdAt,
|
|
839
|
+
id: row.id,
|
|
840
|
+
name: row.name,
|
|
841
|
+
type: normalizeSecretType(row.type),
|
|
842
|
+
updatedAt: row.updatedAt
|
|
843
|
+
}));
|
|
844
|
+
}
|
|
845
|
+
getSecret(ref) {
|
|
846
|
+
const secret = this.getSecretOrThrow(ref);
|
|
847
|
+
this.audit("secret.read", "secret", secret.id, "Secret read", {
|
|
848
|
+
metadataKeyCount: Object.keys(secret.metadata).length,
|
|
849
|
+
type: secret.type
|
|
850
|
+
});
|
|
851
|
+
return secret;
|
|
852
|
+
}
|
|
853
|
+
removeSecret(ref) {
|
|
854
|
+
const secret = this.getSecretOrThrow(ref);
|
|
855
|
+
this.db.deleteSecret(secret.id);
|
|
856
|
+
this.audit("secret.deleted", "secret", secret.id, "Secret deleted", {
|
|
857
|
+
type: secret.type
|
|
858
|
+
});
|
|
859
|
+
return { id: secret.id, name: secret.name };
|
|
860
|
+
}
|
|
861
|
+
generateOtp(ref) {
|
|
862
|
+
const secret = this.getSecretOrThrow(ref);
|
|
863
|
+
if (secret.type !== "otp") {
|
|
864
|
+
throw new Error(`Secret is not an OTP secret: ${ref}`);
|
|
865
|
+
}
|
|
866
|
+
const result = generateTotp(secret.value, {
|
|
867
|
+
algorithm: secret.metadata.algorithm,
|
|
868
|
+
digits: secret.metadata.digits
|
|
869
|
+
});
|
|
870
|
+
this.audit("otp.generated", "secret", secret.id, "OTP code generated", {
|
|
871
|
+
expiresAt: result.expiresAt
|
|
872
|
+
});
|
|
873
|
+
return {
|
|
874
|
+
code: result.code,
|
|
875
|
+
expiresAt: result.expiresAt,
|
|
876
|
+
secret: secret.name
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
createLease(input) {
|
|
880
|
+
if (input.secretRefs.length === 0) {
|
|
881
|
+
throw new Error("Lease requires at least one --secret");
|
|
882
|
+
}
|
|
883
|
+
if (input.ttlSeconds <= 0) {
|
|
884
|
+
throw new Error("Lease ttl must be greater than zero");
|
|
885
|
+
}
|
|
886
|
+
const resolved = input.secretRefs.map((ref) => this.getSecretOrThrow(ref));
|
|
887
|
+
const now = new Date().toISOString();
|
|
888
|
+
const expiresAt = new Date(Date.now() + input.ttlSeconds * 1000).toISOString();
|
|
889
|
+
const lease = {
|
|
890
|
+
createdAt: now,
|
|
891
|
+
expiresAt,
|
|
892
|
+
id: randomId(),
|
|
893
|
+
name: input.name || "session",
|
|
894
|
+
revokedAt: null,
|
|
895
|
+
secretRefs: resolved.map((secret) => secret.id)
|
|
896
|
+
};
|
|
897
|
+
this.db.insertLease({
|
|
898
|
+
createdAt: lease.createdAt,
|
|
899
|
+
expiresAt: lease.expiresAt,
|
|
900
|
+
id: lease.id,
|
|
901
|
+
name: lease.name,
|
|
902
|
+
revokedAt: lease.revokedAt,
|
|
903
|
+
secretRefs: JSON.stringify(lease.secretRefs)
|
|
904
|
+
});
|
|
905
|
+
this.audit("lease.created", "lease", lease.id, "Lease created", {
|
|
906
|
+
expiresAt,
|
|
907
|
+
secretCount: lease.secretRefs.length
|
|
908
|
+
});
|
|
909
|
+
return lease;
|
|
910
|
+
}
|
|
911
|
+
revokeLease(id) {
|
|
912
|
+
const lease = this.getLeaseOrThrow(id);
|
|
913
|
+
const revokedAt = new Date().toISOString();
|
|
914
|
+
this.db.revokeLease(id, revokedAt);
|
|
915
|
+
this.audit("lease.revoked", "lease", id, "Lease revoked", {
|
|
916
|
+
id
|
|
917
|
+
});
|
|
918
|
+
return {
|
|
919
|
+
...lease,
|
|
920
|
+
revokedAt
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
buildEnv(mappings, leaseId) {
|
|
924
|
+
if (mappings.length === 0) {
|
|
925
|
+
throw new Error("Provide at least one env mapping");
|
|
926
|
+
}
|
|
927
|
+
const output = {};
|
|
928
|
+
for (const mapping of mappings) {
|
|
929
|
+
this.assertLeaseAllows(leaseId, mapping.secretRef);
|
|
930
|
+
const secret = this.getSecretOrThrow(mapping.secretRef);
|
|
931
|
+
output[mapping.envName] = secret.value;
|
|
932
|
+
}
|
|
933
|
+
return output;
|
|
934
|
+
}
|
|
935
|
+
renderEnv(mappings, leaseId) {
|
|
936
|
+
const env = this.buildEnv(mappings, leaseId);
|
|
937
|
+
this.audit("env.rendered", "lease", leaseId ?? null, "Environment rendered", {
|
|
938
|
+
leaseId: leaseId ?? null,
|
|
939
|
+
varCount: Object.keys(env).length
|
|
940
|
+
});
|
|
941
|
+
return env;
|
|
942
|
+
}
|
|
943
|
+
syncEnvFile(input) {
|
|
944
|
+
const env = this.buildEnv(input.mappings, input.leaseId);
|
|
945
|
+
if (!input.force && existsSync2(input.outputPath)) {
|
|
946
|
+
throw new Error(`Env file already exists: ${input.outputPath}`);
|
|
947
|
+
}
|
|
948
|
+
const createdAt = new Date().toISOString();
|
|
949
|
+
const expiresAt = input.ttlSeconds ? new Date(Date.now() + input.ttlSeconds * 1000).toISOString() : null;
|
|
950
|
+
const lines = [
|
|
951
|
+
"# autho-generated=true",
|
|
952
|
+
`# autho-created-at=${createdAt}`,
|
|
953
|
+
`# autho-expires-at=${expiresAt ?? ""}`,
|
|
954
|
+
...Object.entries(env).map(([key, value]) => `${key}=${quoteEnvValue(value)}`),
|
|
955
|
+
""
|
|
956
|
+
];
|
|
957
|
+
writeTextFileSecure(input.outputPath, lines.join(`
|
|
958
|
+
`));
|
|
959
|
+
this.audit("env.synced", "lease", input.leaseId ?? null, "Environment file written", {
|
|
960
|
+
expiresAt,
|
|
961
|
+
leaseId: input.leaseId ?? null,
|
|
962
|
+
varCount: Object.keys(env).length
|
|
963
|
+
});
|
|
964
|
+
return {
|
|
965
|
+
expiresAt,
|
|
966
|
+
outputPath: input.outputPath,
|
|
967
|
+
varCount: Object.keys(env).length
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
runExec(input) {
|
|
971
|
+
if (input.cmd.length === 0) {
|
|
972
|
+
throw new Error("Missing command after --");
|
|
973
|
+
}
|
|
974
|
+
const injectedEnv = this.buildEnv(input.mappings, input.leaseId);
|
|
975
|
+
const result = spawnSync(input.cmd[0], input.cmd.slice(1), {
|
|
976
|
+
env: {
|
|
977
|
+
...process.env,
|
|
978
|
+
...injectedEnv
|
|
979
|
+
},
|
|
980
|
+
stdio: "pipe"
|
|
981
|
+
});
|
|
982
|
+
this.audit("exec.run", "lease", input.leaseId ?? null, "Injected command executed", {
|
|
983
|
+
...summarizeCommand(input.cmd),
|
|
984
|
+
envCount: Object.keys(injectedEnv).length,
|
|
985
|
+
exitCode: result.status ?? 1,
|
|
986
|
+
leaseId: input.leaseId ?? null
|
|
987
|
+
});
|
|
988
|
+
return {
|
|
989
|
+
exitCode: result.status ?? 1,
|
|
990
|
+
stderr: (result.stderr ?? Buffer.from("")).toString("utf8"),
|
|
991
|
+
stdout: (result.stdout ?? Buffer.from("")).toString("utf8")
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
encryptFile(inputPath, outputPath, options) {
|
|
995
|
+
assertPathIsFile(inputPath);
|
|
996
|
+
const resolvedOutput = outputPath ?? defaultEncryptedFilePath(inputPath);
|
|
997
|
+
if (!options?.force && existsSync2(resolvedOutput)) {
|
|
998
|
+
throw new Error(`Output file already exists: ${resolvedOutput}`);
|
|
999
|
+
}
|
|
1000
|
+
const result = encryptFileArtifact(inputPath, resolvedOutput, this.rootKey);
|
|
1001
|
+
this.audit("file.encrypted", "artifact", null, "File encrypted", {
|
|
1002
|
+
kind: "file"
|
|
1003
|
+
});
|
|
1004
|
+
return result;
|
|
1005
|
+
}
|
|
1006
|
+
decryptFile(inputPath, outputPath, options) {
|
|
1007
|
+
assertPathIsFile(inputPath);
|
|
1008
|
+
const resolvedOutput = outputPath ?? defaultDecryptedFilePath(inputPath);
|
|
1009
|
+
if (!options?.force && existsSync2(resolvedOutput)) {
|
|
1010
|
+
throw new Error(`Output file already exists: ${resolvedOutput}`);
|
|
1011
|
+
}
|
|
1012
|
+
const result = decryptFileArtifact(inputPath, resolvedOutput, this.rootKey);
|
|
1013
|
+
this.audit("file.decrypted", "artifact", null, "File decrypted", {
|
|
1014
|
+
kind: "file"
|
|
1015
|
+
});
|
|
1016
|
+
return result;
|
|
1017
|
+
}
|
|
1018
|
+
encryptFolder(inputPath, outputPath, options) {
|
|
1019
|
+
assertPathIsDirectory(inputPath);
|
|
1020
|
+
const resolvedOutput = outputPath ?? defaultEncryptedFolderPath(inputPath);
|
|
1021
|
+
if (!options?.force && existsSync2(resolvedOutput)) {
|
|
1022
|
+
throw new Error(`Output file already exists: ${resolvedOutput}`);
|
|
1023
|
+
}
|
|
1024
|
+
const result = encryptFolderArtifact(inputPath, resolvedOutput, this.rootKey);
|
|
1025
|
+
this.audit("folder.encrypted", "artifact", null, "Folder encrypted", {
|
|
1026
|
+
fileCount: result.fileCount,
|
|
1027
|
+
kind: "folder"
|
|
1028
|
+
});
|
|
1029
|
+
return result;
|
|
1030
|
+
}
|
|
1031
|
+
decryptFolder(inputPath, outputPath, options) {
|
|
1032
|
+
assertPathIsFile(inputPath);
|
|
1033
|
+
const resolvedOutput = outputPath ?? defaultDecryptedFolderPath(inputPath);
|
|
1034
|
+
if (!options?.force && existsSync2(resolvedOutput)) {
|
|
1035
|
+
throw new Error(`Output path already exists: ${resolvedOutput}`);
|
|
1036
|
+
}
|
|
1037
|
+
const result = decryptFolderArtifact(inputPath, resolvedOutput, this.rootKey);
|
|
1038
|
+
this.audit("folder.decrypted", "artifact", null, "Folder decrypted", {
|
|
1039
|
+
fileCount: result.fileCount,
|
|
1040
|
+
kind: "folder"
|
|
1041
|
+
});
|
|
1042
|
+
return result;
|
|
1043
|
+
}
|
|
1044
|
+
listAudit(limit = 50) {
|
|
1045
|
+
return this.db.listAudit(limit).map(toAuditEvent);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// packages/core/src/daemon.ts
|
|
1050
|
+
var DAEMON_TOKEN_SERVICE = "autho.daemon";
|
|
1051
|
+
function daemonTokenName(statePath) {
|
|
1052
|
+
return createHash("sha256").update(statePath).digest("hex");
|
|
1053
|
+
}
|
|
1054
|
+
async function storeDaemonToken(statePath, token) {
|
|
1055
|
+
const tokenName = daemonTokenName(statePath);
|
|
1056
|
+
if (process.env.AUTHO_DISABLE_OS_SECRETS !== "1") {
|
|
1057
|
+
try {
|
|
1058
|
+
await Bun.secrets.set({
|
|
1059
|
+
name: tokenName,
|
|
1060
|
+
service: DAEMON_TOKEN_SERVICE,
|
|
1061
|
+
value: token
|
|
1062
|
+
});
|
|
1063
|
+
return {
|
|
1064
|
+
token: null,
|
|
1065
|
+
tokenName,
|
|
1066
|
+
tokenStorage: "os"
|
|
1067
|
+
};
|
|
1068
|
+
} catch {}
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
token,
|
|
1072
|
+
tokenName: null,
|
|
1073
|
+
tokenStorage: "file"
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
async function resolveDaemonToken(state) {
|
|
1077
|
+
if (state.tokenStorage === "os") {
|
|
1078
|
+
if (!state.tokenName) {
|
|
1079
|
+
throw new Error("Daemon state is missing tokenName for OS secret storage");
|
|
1080
|
+
}
|
|
1081
|
+
const token = await Bun.secrets.get({
|
|
1082
|
+
name: state.tokenName,
|
|
1083
|
+
service: DAEMON_TOKEN_SERVICE
|
|
1084
|
+
});
|
|
1085
|
+
if (!token) {
|
|
1086
|
+
throw new Error("Daemon token not found in OS secret storage");
|
|
1087
|
+
}
|
|
1088
|
+
return token;
|
|
1089
|
+
}
|
|
1090
|
+
if (!state.token) {
|
|
1091
|
+
throw new Error("Daemon state is missing token");
|
|
1092
|
+
}
|
|
1093
|
+
return state.token;
|
|
1094
|
+
}
|
|
1095
|
+
async function deleteStoredDaemonToken(state) {
|
|
1096
|
+
if (!state || state.tokenStorage !== "os" || !state.tokenName) {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
try {
|
|
1100
|
+
await Bun.secrets.delete({
|
|
1101
|
+
name: state.tokenName,
|
|
1102
|
+
service: DAEMON_TOKEN_SERVICE
|
|
1103
|
+
});
|
|
1104
|
+
} catch {}
|
|
1105
|
+
}
|
|
1106
|
+
function readDaemonState(statePath) {
|
|
1107
|
+
if (!existsSync3(statePath)) {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
const stored = JSON.parse(readFileSync3(statePath, "utf8"));
|
|
1111
|
+
return {
|
|
1112
|
+
pid: stored.pid,
|
|
1113
|
+
port: stored.port,
|
|
1114
|
+
startedAt: stored.startedAt,
|
|
1115
|
+
token: stored.token ?? null,
|
|
1116
|
+
tokenName: stored.tokenName ?? null,
|
|
1117
|
+
tokenStorage: stored.tokenStorage ?? (stored.token ? "file" : "os"),
|
|
1118
|
+
vaultPath: stored.vaultPath,
|
|
1119
|
+
version: stored.version
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
async function writeDaemonState(statePath, state) {
|
|
1123
|
+
const tokenState = await storeDaemonToken(statePath, state.token);
|
|
1124
|
+
writeTextFileSecure(statePath, JSON.stringify({
|
|
1125
|
+
pid: state.pid,
|
|
1126
|
+
port: state.port,
|
|
1127
|
+
startedAt: state.startedAt,
|
|
1128
|
+
token: tokenState.token ?? undefined,
|
|
1129
|
+
tokenName: tokenState.tokenName ?? undefined,
|
|
1130
|
+
tokenStorage: tokenState.tokenStorage,
|
|
1131
|
+
vaultPath: state.vaultPath,
|
|
1132
|
+
version: state.version
|
|
1133
|
+
}, null, 2) + `
|
|
1134
|
+
`);
|
|
1135
|
+
}
|
|
1136
|
+
async function deleteDaemonState(statePath) {
|
|
1137
|
+
const state = readDaemonState(statePath);
|
|
1138
|
+
await deleteStoredDaemonToken(state);
|
|
1139
|
+
if (existsSync3(statePath)) {
|
|
1140
|
+
rmSync(statePath, { force: true });
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function openSessionFromRootKey(rootKey, vaultPath) {
|
|
1144
|
+
return new VaultSession(new AuthoDatabase(vaultPath), rootKey);
|
|
1145
|
+
}
|
|
1146
|
+
function unlockVaultRootKey(vaultPath, password) {
|
|
1147
|
+
const db = new AuthoDatabase(vaultPath);
|
|
1148
|
+
try {
|
|
1149
|
+
const config = db.getVaultConfig();
|
|
1150
|
+
if (!config) {
|
|
1151
|
+
throw new Error(`Vault is not initialized at ${vaultPath}`);
|
|
1152
|
+
}
|
|
1153
|
+
return unlockRootKey(password, config);
|
|
1154
|
+
} finally {
|
|
1155
|
+
db.close();
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
async function readJson(request) {
|
|
1159
|
+
return await request.json();
|
|
1160
|
+
}
|
|
1161
|
+
function json(data, status = 200) {
|
|
1162
|
+
return new Response(JSON.stringify(data, null, 2), {
|
|
1163
|
+
headers: {
|
|
1164
|
+
"content-type": "application/json; charset=utf-8"
|
|
1165
|
+
},
|
|
1166
|
+
status
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
function unauthorized() {
|
|
1170
|
+
return json({ error: "Unauthorized daemon request" }, 401);
|
|
1171
|
+
}
|
|
1172
|
+
function getBearerToken(request) {
|
|
1173
|
+
const header = request.headers.get("authorization");
|
|
1174
|
+
if (!header?.startsWith("Bearer ")) {
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
return header.slice("Bearer ".length);
|
|
1178
|
+
}
|
|
1179
|
+
async function startDaemonServer(options) {
|
|
1180
|
+
const sessions = new Map;
|
|
1181
|
+
const startedAt = new Date().toISOString();
|
|
1182
|
+
const token = randomId(24);
|
|
1183
|
+
const cleanupExpiredSessions = () => {
|
|
1184
|
+
const now = Date.now();
|
|
1185
|
+
for (const [id, session] of sessions.entries()) {
|
|
1186
|
+
if (Date.parse(session.expiresAt) <= now) {
|
|
1187
|
+
sessions.delete(id);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
let server = null;
|
|
1192
|
+
const shutdown = () => {
|
|
1193
|
+
deleteDaemonState(options.statePath).then(() => {
|
|
1194
|
+
server?.stop(true);
|
|
1195
|
+
process.exit(0);
|
|
1196
|
+
});
|
|
1197
|
+
};
|
|
1198
|
+
const auth = (request) => {
|
|
1199
|
+
const provided = getBearerToken(request);
|
|
1200
|
+
if (!provided || provided.length !== token.length || !timingSafeEqual(Buffer.from(provided), Buffer.from(token))) {
|
|
1201
|
+
return unauthorized();
|
|
1202
|
+
}
|
|
1203
|
+
return null;
|
|
1204
|
+
};
|
|
1205
|
+
server = Bun.serve({
|
|
1206
|
+
async fetch(request) {
|
|
1207
|
+
cleanupExpiredSessions();
|
|
1208
|
+
const url = new URL(request.url);
|
|
1209
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
1210
|
+
return json({
|
|
1211
|
+
ok: true,
|
|
1212
|
+
running: true,
|
|
1213
|
+
startedAt
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
const authResponse = auth(request);
|
|
1217
|
+
if (authResponse) {
|
|
1218
|
+
return authResponse;
|
|
1219
|
+
}
|
|
1220
|
+
try {
|
|
1221
|
+
if (request.method === "POST" && url.pathname === "/status") {
|
|
1222
|
+
const db = new AuthoDatabase(options.vaultPath);
|
|
1223
|
+
try {
|
|
1224
|
+
const config = db.getVaultConfig();
|
|
1225
|
+
const status = {
|
|
1226
|
+
activeLeaseCount: db.countActiveLeases(new Date().toISOString()),
|
|
1227
|
+
auditEventCount: db.countAuditEvents(),
|
|
1228
|
+
initialized: config !== null,
|
|
1229
|
+
projectFile: null,
|
|
1230
|
+
projectMappings: [],
|
|
1231
|
+
secretCount: db.countSecrets(),
|
|
1232
|
+
unlocked: sessions.size > 0,
|
|
1233
|
+
vaultPath: options.vaultPath
|
|
1234
|
+
};
|
|
1235
|
+
return json({
|
|
1236
|
+
activeSessions: sessions.size,
|
|
1237
|
+
daemonStartedAt: startedAt,
|
|
1238
|
+
status
|
|
1239
|
+
});
|
|
1240
|
+
} finally {
|
|
1241
|
+
db.close();
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (request.method === "POST" && url.pathname === "/unlock") {
|
|
1245
|
+
const body = await readJson(request);
|
|
1246
|
+
const ttlSeconds = body.ttlSeconds ?? 900;
|
|
1247
|
+
if (ttlSeconds <= 0 || ttlSeconds > 86400) {
|
|
1248
|
+
return json({ error: "Unlock ttl must be between 1 and 86400 seconds" }, 400);
|
|
1249
|
+
}
|
|
1250
|
+
const rootKey = unlockVaultRootKey(options.vaultPath, body.password);
|
|
1251
|
+
const sessionId = randomId();
|
|
1252
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
1253
|
+
sessions.set(sessionId, {
|
|
1254
|
+
expiresAt,
|
|
1255
|
+
id: sessionId,
|
|
1256
|
+
rootKey,
|
|
1257
|
+
vaultPath: options.vaultPath
|
|
1258
|
+
});
|
|
1259
|
+
return json({
|
|
1260
|
+
expiresAt,
|
|
1261
|
+
sessionId
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
if (request.method === "POST" && url.pathname === "/lock") {
|
|
1265
|
+
const body = await readJson(request);
|
|
1266
|
+
sessions.delete(body.sessionId);
|
|
1267
|
+
return json({ locked: true, sessionId: body.sessionId });
|
|
1268
|
+
}
|
|
1269
|
+
if (request.method === "POST" && url.pathname === "/env/render") {
|
|
1270
|
+
const body = await readJson(request);
|
|
1271
|
+
const session = sessions.get(body.sessionId);
|
|
1272
|
+
if (!session) {
|
|
1273
|
+
return json({ error: `Unknown daemon session: ${body.sessionId}` }, 404);
|
|
1274
|
+
}
|
|
1275
|
+
const vaultSession = openSessionFromRootKey(session.rootKey, session.vaultPath);
|
|
1276
|
+
try {
|
|
1277
|
+
return json(vaultSession.renderEnv(body.mappings, body.leaseId));
|
|
1278
|
+
} finally {
|
|
1279
|
+
vaultSession.close();
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (request.method === "POST" && url.pathname === "/exec") {
|
|
1283
|
+
const body = await readJson(request);
|
|
1284
|
+
const session = sessions.get(body.sessionId);
|
|
1285
|
+
if (!session) {
|
|
1286
|
+
return json({ error: `Unknown daemon session: ${body.sessionId}` }, 404);
|
|
1287
|
+
}
|
|
1288
|
+
const vaultSession = openSessionFromRootKey(session.rootKey, session.vaultPath);
|
|
1289
|
+
try {
|
|
1290
|
+
return json(vaultSession.runExec(body));
|
|
1291
|
+
} finally {
|
|
1292
|
+
vaultSession.close();
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (request.method === "POST" && url.pathname === "/shutdown") {
|
|
1296
|
+
shutdown();
|
|
1297
|
+
return json({ ok: true, stopped: true });
|
|
1298
|
+
}
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1301
|
+
return json({ error: message }, 400);
|
|
1302
|
+
}
|
|
1303
|
+
return json({ error: "Not found" }, 404);
|
|
1304
|
+
},
|
|
1305
|
+
hostname: options.host,
|
|
1306
|
+
port: options.port
|
|
1307
|
+
});
|
|
1308
|
+
await writeDaemonState(options.statePath, {
|
|
1309
|
+
pid: process.pid,
|
|
1310
|
+
port: server.port,
|
|
1311
|
+
startedAt,
|
|
1312
|
+
token,
|
|
1313
|
+
vaultPath: options.vaultPath,
|
|
1314
|
+
version: 1
|
|
1315
|
+
});
|
|
1316
|
+
const onSignal = () => {
|
|
1317
|
+
shutdown();
|
|
1318
|
+
process.exit(0);
|
|
1319
|
+
};
|
|
1320
|
+
process.on("SIGINT", onSignal);
|
|
1321
|
+
process.on("SIGTERM", onSignal);
|
|
1322
|
+
await new Promise(() => {});
|
|
1323
|
+
}
|
|
1324
|
+
async function waitForDaemonStateDeletion(statePath) {
|
|
1325
|
+
for (let attempt = 0;attempt < 20; attempt += 1) {
|
|
1326
|
+
if (!existsSync3(statePath)) {
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
await Bun.sleep(25);
|
|
1330
|
+
}
|
|
1331
|
+
return !existsSync3(statePath);
|
|
1332
|
+
}
|
|
1333
|
+
async function daemonRequest(state, path, body) {
|
|
1334
|
+
const token = await resolveDaemonToken(state);
|
|
1335
|
+
const response = await fetch(`http://127.0.0.1:${state.port}${path}`, {
|
|
1336
|
+
body: JSON.stringify(body ?? {}),
|
|
1337
|
+
headers: {
|
|
1338
|
+
authorization: `Bearer ${token}`,
|
|
1339
|
+
"content-type": "application/json"
|
|
1340
|
+
},
|
|
1341
|
+
method: "POST"
|
|
1342
|
+
});
|
|
1343
|
+
const data = await response.json();
|
|
1344
|
+
if (!response.ok) {
|
|
1345
|
+
throw new Error(data.error ?? `Daemon request failed: ${response.status}`);
|
|
1346
|
+
}
|
|
1347
|
+
return data;
|
|
1348
|
+
}
|
|
1349
|
+
async function daemonStatus(options) {
|
|
1350
|
+
const state = readDaemonState(options.statePath);
|
|
1351
|
+
if (!state) {
|
|
1352
|
+
throw new Error(`Daemon state not found: ${options.statePath}`);
|
|
1353
|
+
}
|
|
1354
|
+
return daemonRequest(state, "/status", {});
|
|
1355
|
+
}
|
|
1356
|
+
async function daemonUnlock(options) {
|
|
1357
|
+
const state = readDaemonState(options.statePath);
|
|
1358
|
+
if (!state) {
|
|
1359
|
+
throw new Error(`Daemon state not found: ${options.statePath}`);
|
|
1360
|
+
}
|
|
1361
|
+
return daemonRequest(state, "/unlock", {
|
|
1362
|
+
password: options.password,
|
|
1363
|
+
ttlSeconds: options.ttlSeconds
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
async function daemonLock(options) {
|
|
1367
|
+
const state = readDaemonState(options.statePath);
|
|
1368
|
+
if (!state) {
|
|
1369
|
+
throw new Error(`Daemon state not found: ${options.statePath}`);
|
|
1370
|
+
}
|
|
1371
|
+
return daemonRequest(state, "/lock", { sessionId: options.sessionId });
|
|
1372
|
+
}
|
|
1373
|
+
async function daemonRenderEnv(options) {
|
|
1374
|
+
const state = readDaemonState(options.statePath);
|
|
1375
|
+
if (!state) {
|
|
1376
|
+
throw new Error(`Daemon state not found: ${options.statePath}`);
|
|
1377
|
+
}
|
|
1378
|
+
return daemonRequest(state, "/env/render", options);
|
|
1379
|
+
}
|
|
1380
|
+
async function daemonExec(options) {
|
|
1381
|
+
const state = readDaemonState(options.statePath);
|
|
1382
|
+
if (!state) {
|
|
1383
|
+
throw new Error(`Daemon state not found: ${options.statePath}`);
|
|
1384
|
+
}
|
|
1385
|
+
return daemonRequest(state, "/exec", options);
|
|
1386
|
+
}
|
|
1387
|
+
async function daemonStop(options) {
|
|
1388
|
+
const state = readDaemonState(options.statePath);
|
|
1389
|
+
if (!state) {
|
|
1390
|
+
throw new Error(`Daemon state not found: ${options.statePath}`);
|
|
1391
|
+
}
|
|
1392
|
+
try {
|
|
1393
|
+
const result = await daemonRequest(state, "/shutdown", {});
|
|
1394
|
+
await waitForDaemonStateDeletion(options.statePath);
|
|
1395
|
+
return result;
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
if (await waitForDaemonStateDeletion(options.statePath)) {
|
|
1398
|
+
return { ok: true, stopped: true };
|
|
1399
|
+
}
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// apps/cli/src/index.ts
|
|
1405
|
+
function parseArgs(argv) {
|
|
1406
|
+
const dashDashIndex = argv.indexOf("--");
|
|
1407
|
+
const main = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
|
|
1408
|
+
const passthrough = dashDashIndex === -1 ? [] : argv.slice(dashDashIndex + 1);
|
|
1409
|
+
const positionals = [];
|
|
1410
|
+
const options = {};
|
|
1411
|
+
for (let index = 0;index < main.length; index += 1) {
|
|
1412
|
+
const token = main[index];
|
|
1413
|
+
if (!token.startsWith("--")) {
|
|
1414
|
+
positionals.push(token);
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
const key = token.slice(2);
|
|
1418
|
+
const next = main[index + 1];
|
|
1419
|
+
if (!next || next.startsWith("--")) {
|
|
1420
|
+
options[key] = true;
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
const current = options[key];
|
|
1424
|
+
if (current === undefined) {
|
|
1425
|
+
options[key] = next;
|
|
1426
|
+
} else if (Array.isArray(current)) {
|
|
1427
|
+
current.push(next);
|
|
1428
|
+
} else {
|
|
1429
|
+
options[key] = [current, next];
|
|
1430
|
+
}
|
|
1431
|
+
index += 1;
|
|
1432
|
+
}
|
|
1433
|
+
return { options, passthrough, positionals };
|
|
1434
|
+
}
|
|
1435
|
+
function getString(args, key) {
|
|
1436
|
+
const value = args.options[key];
|
|
1437
|
+
return typeof value === "string" ? value : undefined;
|
|
1438
|
+
}
|
|
1439
|
+
function getStrings(args, key) {
|
|
1440
|
+
const value = args.options[key];
|
|
1441
|
+
if (Array.isArray(value)) {
|
|
1442
|
+
return value.filter((entry) => typeof entry === "string");
|
|
1443
|
+
}
|
|
1444
|
+
if (typeof value === "string") {
|
|
1445
|
+
return [value];
|
|
1446
|
+
}
|
|
1447
|
+
return [];
|
|
1448
|
+
}
|
|
1449
|
+
function getBoolean(args, key) {
|
|
1450
|
+
return args.options[key] === true;
|
|
1451
|
+
}
|
|
1452
|
+
function required(value, label) {
|
|
1453
|
+
if (!value) {
|
|
1454
|
+
throw new Error(`Missing required option: ${label}`);
|
|
1455
|
+
}
|
|
1456
|
+
return value;
|
|
1457
|
+
}
|
|
1458
|
+
function requirePositiveInt(value, label) {
|
|
1459
|
+
const num = Number(value);
|
|
1460
|
+
if (!Number.isFinite(num) || num <= 0 || Math.floor(num) !== num) {
|
|
1461
|
+
throw new Error(`${label} must be a positive integer`);
|
|
1462
|
+
}
|
|
1463
|
+
return num;
|
|
1464
|
+
}
|
|
1465
|
+
function output(value, jsonMode = false) {
|
|
1466
|
+
if (jsonMode) {
|
|
1467
|
+
console.log(JSON.stringify(value, null, 2));
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (Array.isArray(value)) {
|
|
1471
|
+
console.table(value);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (typeof value === "object" && value !== null) {
|
|
1475
|
+
console.log(JSON.stringify(value, null, 2));
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
console.log(value);
|
|
1479
|
+
}
|
|
1480
|
+
function absolutePath(path) {
|
|
1481
|
+
return resolve3(path);
|
|
1482
|
+
}
|
|
1483
|
+
function buildSecretMetadata(args) {
|
|
1484
|
+
return Object.fromEntries(Object.entries({
|
|
1485
|
+
algorithm: getString(args, "algorithm"),
|
|
1486
|
+
description: getString(args, "description"),
|
|
1487
|
+
digits: getString(args, "digits") ? Number(getString(args, "digits")) : undefined,
|
|
1488
|
+
url: getString(args, "url")
|
|
1489
|
+
}).filter(([, value]) => value !== undefined));
|
|
1490
|
+
}
|
|
1491
|
+
async function readBufferedStdin() {
|
|
1492
|
+
process.stdin.setEncoding("utf8");
|
|
1493
|
+
let input = "";
|
|
1494
|
+
for await (const chunk of process.stdin) {
|
|
1495
|
+
input += chunk;
|
|
1496
|
+
}
|
|
1497
|
+
return input.split(/\r?\n/);
|
|
1498
|
+
}
|
|
1499
|
+
async function createPromptAdapter() {
|
|
1500
|
+
if (process.stdin.isTTY) {
|
|
1501
|
+
const rl = createInterface({
|
|
1502
|
+
input: process.stdin,
|
|
1503
|
+
output: process.stdout
|
|
1504
|
+
});
|
|
1505
|
+
return {
|
|
1506
|
+
ask: async (prompt) => (await rl.question(prompt)).trim(),
|
|
1507
|
+
close: () => rl.close()
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
const answers = await readBufferedStdin();
|
|
1511
|
+
let index = 0;
|
|
1512
|
+
return {
|
|
1513
|
+
ask: async (prompt) => {
|
|
1514
|
+
process.stdout.write(prompt);
|
|
1515
|
+
const value = answers[index] ?? "";
|
|
1516
|
+
index += 1;
|
|
1517
|
+
return value.trim();
|
|
1518
|
+
},
|
|
1519
|
+
close: () => {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
async function askPassword(prompt, initial) {
|
|
1525
|
+
if (initial) {
|
|
1526
|
+
return initial;
|
|
1527
|
+
}
|
|
1528
|
+
return prompt.ask("Master password: ");
|
|
1529
|
+
}
|
|
1530
|
+
async function runPromptMode(vaultPath, initialPassword) {
|
|
1531
|
+
const prompt = await createPromptAdapter();
|
|
1532
|
+
try {
|
|
1533
|
+
const password = await askPassword(prompt, initialPassword);
|
|
1534
|
+
const session = VaultService.unlock(vaultPath, password);
|
|
1535
|
+
try {
|
|
1536
|
+
const action = (await prompt.ask("Action [create/read/list/delete/otp/exit]: ")).toLowerCase();
|
|
1537
|
+
if (action === "exit") {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
if (action === "list") {
|
|
1541
|
+
output(session.listSecrets());
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (action === "read") {
|
|
1545
|
+
const ref = await prompt.ask("Secret ref: ");
|
|
1546
|
+
output(session.getSecret(ref));
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (action === "delete") {
|
|
1550
|
+
const ref = await prompt.ask("Secret ref: ");
|
|
1551
|
+
output(session.removeSecret(ref));
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
if (action === "otp") {
|
|
1555
|
+
const ref = await prompt.ask("OTP ref: ");
|
|
1556
|
+
output(session.generateOtp(ref));
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (action === "create") {
|
|
1560
|
+
const name = await prompt.ask("Name: ");
|
|
1561
|
+
const type = (await prompt.ask("Type [password/note/otp]: ")).toLowerCase();
|
|
1562
|
+
const value = await prompt.ask("Value: ");
|
|
1563
|
+
const username = type !== "note" ? await prompt.ask("Username (optional): ") : "";
|
|
1564
|
+
const url = type === "password" ? await prompt.ask("URL (optional): ") : "";
|
|
1565
|
+
const description = await prompt.ask("Description (optional): ");
|
|
1566
|
+
const digits = type === "otp" ? await prompt.ask("Digits [6]: ") : "";
|
|
1567
|
+
const algorithm = type === "otp" ? await prompt.ask("Algorithm [SHA1]: ") : "";
|
|
1568
|
+
output(session.addSecret({
|
|
1569
|
+
metadata: Object.fromEntries(Object.entries({
|
|
1570
|
+
algorithm: algorithm || undefined,
|
|
1571
|
+
description: description || undefined,
|
|
1572
|
+
digits: digits ? Number(digits) : undefined,
|
|
1573
|
+
url: url || undefined
|
|
1574
|
+
}).filter(([, entry]) => entry !== undefined)),
|
|
1575
|
+
name,
|
|
1576
|
+
type,
|
|
1577
|
+
username: username || undefined,
|
|
1578
|
+
value
|
|
1579
|
+
}));
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
throw new Error(`Unknown prompt action: ${action}`);
|
|
1583
|
+
} finally {
|
|
1584
|
+
session.close();
|
|
1585
|
+
}
|
|
1586
|
+
} finally {
|
|
1587
|
+
prompt.close();
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
async function runWebServer(vaultPath, args) {
|
|
1591
|
+
const commandArgs = [
|
|
1592
|
+
"run",
|
|
1593
|
+
absolutePath("./apps/web/src/index.ts"),
|
|
1594
|
+
"serve",
|
|
1595
|
+
"--vault",
|
|
1596
|
+
absolutePath(vaultPath)
|
|
1597
|
+
];
|
|
1598
|
+
const host = getString(args, "host");
|
|
1599
|
+
const port = getString(args, "port");
|
|
1600
|
+
if (host) {
|
|
1601
|
+
commandArgs.push("--host", host);
|
|
1602
|
+
}
|
|
1603
|
+
if (port) {
|
|
1604
|
+
commandArgs.push("--port", port);
|
|
1605
|
+
}
|
|
1606
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
1607
|
+
const child = spawn(process.execPath, commandArgs, {
|
|
1608
|
+
cwd: process.cwd(),
|
|
1609
|
+
stdio: "inherit"
|
|
1610
|
+
});
|
|
1611
|
+
child.on("exit", (code) => {
|
|
1612
|
+
if (code === 0) {
|
|
1613
|
+
resolvePromise();
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
rejectPromise(new Error(`Web server exited with code ${code ?? 1}`));
|
|
1617
|
+
});
|
|
1618
|
+
child.on("error", rejectPromise);
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
function help() {
|
|
1622
|
+
return [
|
|
1623
|
+
"Autho Bun CLI",
|
|
1624
|
+
"",
|
|
1625
|
+
"Commands:",
|
|
1626
|
+
" prompt [--password <value>] [--vault <path>]",
|
|
1627
|
+
" init --password <value> [--vault <path>]",
|
|
1628
|
+
" status [--password <value>] [--vault <path>] [--project-file <path>] [--json]",
|
|
1629
|
+
" project init --map <ENV_NAME=secretRef> [--map <ENV_NAME=secretRef>] [--output <path>] [--force] [--json]",
|
|
1630
|
+
" web serve [--vault <path>] [--host <value>] [--port <value>]",
|
|
1631
|
+
" daemon serve [--vault <path>] [--state-file <path>] [--host <value>] [--port <value>]",
|
|
1632
|
+
" daemon status [--state-file <path>] [--json]",
|
|
1633
|
+
" daemon unlock --password <value> [--ttl <seconds>] [--state-file <path>] [--json]",
|
|
1634
|
+
" daemon lock --session <id> [--state-file <path>] [--json]",
|
|
1635
|
+
" daemon stop [--state-file <path>] [--json]",
|
|
1636
|
+
" daemon env render --session <id> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--state-file <path>] [--json]",
|
|
1637
|
+
" daemon exec --session <id> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--state-file <path>] -- <command>",
|
|
1638
|
+
" import legacy --password <value> --file <path> [--skip-existing] [--vault <path>] [--json]",
|
|
1639
|
+
" secrets add --password <value> --name <name> --type <password|note|otp> --value <value> [--username <value>] [--url <value>] [--description <value>] [--digits <value>] [--algorithm <value>] [--vault <path>]",
|
|
1640
|
+
" secrets list --password <value> [--vault <path>] [--json]",
|
|
1641
|
+
" secrets get --password <value> --ref <name-or-id> [--vault <path>] [--json]",
|
|
1642
|
+
" secrets rm --password <value> --ref <name-or-id> [--vault <path>] [--json]",
|
|
1643
|
+
" otp code --password <value> --ref <name-or-id> [--vault <path>] [--json]",
|
|
1644
|
+
" lease create --password <value> --secret <name-or-id> [--secret <name-or-id>] --ttl <seconds> [--name <value>] [--vault <path>] [--json]",
|
|
1645
|
+
" lease revoke --password <value> --lease <lease-id> [--vault <path>] [--json]",
|
|
1646
|
+
" env render --password <value> --map <ENV_NAME=secretRef> [--map <ENV_NAME=secretRef>] [--project-file <path>] [--lease <lease-id>] [--vault <path>] [--json]",
|
|
1647
|
+
" env sync --password <value> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--ttl <seconds>] [--output <path>] [--force] [--vault <path>] [--json]",
|
|
1648
|
+
" exec --password <value> --map <ENV_NAME=secretRef> [--project-file <path>] [--lease <lease-id>] [--vault <path>] -- <command>",
|
|
1649
|
+
" file encrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
|
|
1650
|
+
" file decrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
|
|
1651
|
+
" files encrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
|
|
1652
|
+
" files decrypt --password <value> --input <path> [--output <path>] [--force] [--vault <path>] [--json]",
|
|
1653
|
+
" audit list --password <value> [--limit <number>] [--vault <path>] [--json]",
|
|
1654
|
+
"",
|
|
1655
|
+
"Notes:",
|
|
1656
|
+
" Running `autho` with no command enters interactive prompt mode.",
|
|
1657
|
+
" The default vault path is ~/.autho/vault.db (or AUTHO_HOME/vault.db).",
|
|
1658
|
+
" The default project file is ~/.autho/project.json when it exists (or AUTHO_HOME/project.json).",
|
|
1659
|
+
" The default daemon state file is ~/.autho/daemon.json (or AUTHO_HOME/daemon.json).",
|
|
1660
|
+
" AUTHO_MASTER_PASSWORD can be used instead of --password."
|
|
1661
|
+
].join(`
|
|
1662
|
+
`);
|
|
1663
|
+
}
|
|
1664
|
+
async function main() {
|
|
1665
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1666
|
+
const [scope, action, subaction] = args.positionals;
|
|
1667
|
+
const jsonMode = getBoolean(args, "json");
|
|
1668
|
+
const vaultPath = getString(args, "vault") ?? defaultVaultPath();
|
|
1669
|
+
const statePath = absolutePath(getString(args, "state-file") ?? defaultDaemonStatePath());
|
|
1670
|
+
const explicitProjectFile = getString(args, "project-file");
|
|
1671
|
+
const fallbackProjectFile = defaultProjectFilePath();
|
|
1672
|
+
const projectFile = explicitProjectFile ?? (existsSync4(fallbackProjectFile) ? fallbackProjectFile : undefined);
|
|
1673
|
+
const password = getString(args, "password") ?? process.env.AUTHO_MASTER_PASSWORD;
|
|
1674
|
+
if (!scope) {
|
|
1675
|
+
await runPromptMode(vaultPath, password);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
if (scope === "help" || scope === "--help") {
|
|
1679
|
+
console.log(help());
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (scope === "prompt") {
|
|
1683
|
+
await runPromptMode(vaultPath, password);
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
if (scope === "init") {
|
|
1687
|
+
output(VaultService.initialize(vaultPath, required(password, "--password")), jsonMode);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
if (scope === "status") {
|
|
1691
|
+
output(VaultService.status(vaultPath, {
|
|
1692
|
+
password,
|
|
1693
|
+
projectFile
|
|
1694
|
+
}), jsonMode);
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
if (scope === "project" && action === "init") {
|
|
1698
|
+
output(writeProjectConfig({
|
|
1699
|
+
force: getBoolean(args, "force"),
|
|
1700
|
+
mappings: resolveMappings({ maps: getStrings(args, "map") }),
|
|
1701
|
+
outputPath: absolutePath(getString(args, "output") ?? projectFile ?? defaultProjectFilePath())
|
|
1702
|
+
}), jsonMode);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (scope === "web" && action === "serve") {
|
|
1706
|
+
await runWebServer(vaultPath, args);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
if (scope === "daemon" && action === "serve") {
|
|
1710
|
+
await startDaemonServer({
|
|
1711
|
+
host: getString(args, "host") ?? "127.0.0.1",
|
|
1712
|
+
port: Number(getString(args, "port") ?? "0"),
|
|
1713
|
+
statePath,
|
|
1714
|
+
vaultPath: absolutePath(vaultPath)
|
|
1715
|
+
});
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (scope === "daemon" && action === "status") {
|
|
1719
|
+
output(await daemonStatus({ statePath }), jsonMode);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (scope === "daemon" && action === "unlock") {
|
|
1723
|
+
output(await daemonUnlock({
|
|
1724
|
+
password: required(password, "--password"),
|
|
1725
|
+
statePath,
|
|
1726
|
+
ttlSeconds: getString(args, "ttl") ? Number(getString(args, "ttl")) : undefined
|
|
1727
|
+
}), jsonMode);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
if (scope === "daemon" && action === "lock") {
|
|
1731
|
+
output(await daemonLock({
|
|
1732
|
+
sessionId: required(getString(args, "session"), "--session"),
|
|
1733
|
+
statePath
|
|
1734
|
+
}), jsonMode);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
if (scope === "daemon" && action === "stop") {
|
|
1738
|
+
output(await daemonStop({ statePath }), jsonMode);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (scope === "daemon" && action === "env" && subaction === "render") {
|
|
1742
|
+
output(await daemonRenderEnv({
|
|
1743
|
+
leaseId: getString(args, "lease"),
|
|
1744
|
+
mappings: resolveMappings({ maps: getStrings(args, "map"), projectFile }),
|
|
1745
|
+
sessionId: required(getString(args, "session"), "--session"),
|
|
1746
|
+
statePath
|
|
1747
|
+
}), jsonMode);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if (scope === "daemon" && action === "exec") {
|
|
1751
|
+
const result = await daemonExec({
|
|
1752
|
+
cmd: args.passthrough,
|
|
1753
|
+
leaseId: getString(args, "lease"),
|
|
1754
|
+
mappings: resolveMappings({ maps: getStrings(args, "map"), projectFile }),
|
|
1755
|
+
sessionId: required(getString(args, "session"), "--session"),
|
|
1756
|
+
statePath
|
|
1757
|
+
});
|
|
1758
|
+
process.stdout.write(result.stdout);
|
|
1759
|
+
process.stderr.write(result.stderr);
|
|
1760
|
+
process.exit(result.exitCode);
|
|
1761
|
+
}
|
|
1762
|
+
const session = VaultService.unlock(vaultPath, required(password, "--password"));
|
|
1763
|
+
try {
|
|
1764
|
+
if (scope === "import" && action === "legacy") {
|
|
1765
|
+
output(session.importLegacyFile(absolutePath(required(getString(args, "file"), "--file")), {
|
|
1766
|
+
skipExisting: !getBoolean(args, "no-skip-existing")
|
|
1767
|
+
}), jsonMode);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
if (scope === "secrets" && action === "add") {
|
|
1771
|
+
output(session.addSecret({
|
|
1772
|
+
metadata: buildSecretMetadata(args),
|
|
1773
|
+
name: required(getString(args, "name"), "--name"),
|
|
1774
|
+
type: required(getString(args, "type"), "--type"),
|
|
1775
|
+
username: getString(args, "username"),
|
|
1776
|
+
value: required(getString(args, "value"), "--value")
|
|
1777
|
+
}), jsonMode);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (scope === "secrets" && action === "list") {
|
|
1781
|
+
output(session.listSecrets(), jsonMode);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (scope === "secrets" && action === "get") {
|
|
1785
|
+
const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
|
|
1786
|
+
output(session.getSecret(required(ref, "--ref")), jsonMode);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (scope === "secrets" && action === "rm") {
|
|
1790
|
+
const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
|
|
1791
|
+
output(session.removeSecret(required(ref, "--ref")), jsonMode);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (scope === "otp" && action === "code") {
|
|
1795
|
+
const ref = getString(args, "ref") ?? getString(args, "name") ?? getString(args, "id");
|
|
1796
|
+
output(session.generateOtp(required(ref, "--ref")), jsonMode);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (scope === "lease" && action === "create") {
|
|
1800
|
+
output(session.createLease({
|
|
1801
|
+
name: getString(args, "name") ?? "session",
|
|
1802
|
+
secretRefs: getStrings(args, "secret"),
|
|
1803
|
+
ttlSeconds: requirePositiveInt(required(getString(args, "ttl"), "--ttl"), "--ttl")
|
|
1804
|
+
}), jsonMode);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (scope === "lease" && action === "revoke") {
|
|
1808
|
+
output(session.revokeLease(required(getString(args, "lease"), "--lease")), jsonMode);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (scope === "env" && action === "render") {
|
|
1812
|
+
output(session.renderEnv(resolveMappings({
|
|
1813
|
+
maps: getStrings(args, "map"),
|
|
1814
|
+
projectFile
|
|
1815
|
+
}), getString(args, "lease")), jsonMode);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (scope === "env" && action === "sync") {
|
|
1819
|
+
output(session.syncEnvFile({
|
|
1820
|
+
force: getBoolean(args, "force"),
|
|
1821
|
+
leaseId: getString(args, "lease"),
|
|
1822
|
+
mappings: resolveMappings({
|
|
1823
|
+
maps: getStrings(args, "map"),
|
|
1824
|
+
projectFile
|
|
1825
|
+
}),
|
|
1826
|
+
outputPath: absolutePath(getString(args, "output") ?? ".env.autho"),
|
|
1827
|
+
ttlSeconds: getString(args, "ttl") ? requirePositiveInt(getString(args, "ttl"), "--ttl") : undefined
|
|
1828
|
+
}), jsonMode);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (scope === "exec") {
|
|
1832
|
+
const result = session.runExec({
|
|
1833
|
+
cmd: args.passthrough,
|
|
1834
|
+
leaseId: getString(args, "lease"),
|
|
1835
|
+
mappings: resolveMappings({
|
|
1836
|
+
maps: getStrings(args, "map"),
|
|
1837
|
+
projectFile
|
|
1838
|
+
})
|
|
1839
|
+
});
|
|
1840
|
+
process.stdout.write(result.stdout);
|
|
1841
|
+
process.stderr.write(result.stderr);
|
|
1842
|
+
process.exit(result.exitCode);
|
|
1843
|
+
}
|
|
1844
|
+
if (scope === "file" && action === "encrypt") {
|
|
1845
|
+
output(session.encryptFile(absolutePath(required(getString(args, "input"), "--input")), getString(args, "output") ? absolutePath(getString(args, "output")) : undefined, { force: getBoolean(args, "force") }), jsonMode);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
if (scope === "file" && action === "decrypt") {
|
|
1849
|
+
output(session.decryptFile(absolutePath(required(getString(args, "input"), "--input")), getString(args, "output") ? absolutePath(getString(args, "output")) : undefined, { force: getBoolean(args, "force") }), jsonMode);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (scope === "files" && action === "encrypt") {
|
|
1853
|
+
output(session.encryptFolder(absolutePath(required(getString(args, "input"), "--input")), getString(args, "output") ? absolutePath(getString(args, "output")) : undefined, { force: getBoolean(args, "force") }), jsonMode);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (scope === "files" && action === "decrypt") {
|
|
1857
|
+
output(session.decryptFolder(absolutePath(required(getString(args, "input"), "--input")), getString(args, "output") ? absolutePath(getString(args, "output")) : undefined, { force: getBoolean(args, "force") }), jsonMode);
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
if (scope === "audit" && action === "list") {
|
|
1861
|
+
output(session.listAudit(Number(getString(args, "limit") ?? "50")), jsonMode);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
throw new Error(`Unknown command: ${[scope, action, subaction].filter(Boolean).join(" ")}`);
|
|
1865
|
+
} finally {
|
|
1866
|
+
session.close();
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
main().catch((error) => {
|
|
1870
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1871
|
+
console.error(message);
|
|
1872
|
+
process.exit(1);
|
|
1873
|
+
});
|