@seekrit/cli 0.0.1

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.
Files changed (2) hide show
  1. package/dist/index.js +921 -0
  2. package/package.json +33 -0
package/dist/index.js ADDED
@@ -0,0 +1,921 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { spawn } from "child_process";
5
+
6
+ // ../../packages/crypto/src/encoding.ts
7
+ var CHUNK = 32768;
8
+ function toBase64Url(bytes) {
9
+ let binary = "";
10
+ for (let i = 0; i < bytes.length; i += CHUNK) {
11
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
12
+ }
13
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
14
+ }
15
+ function fromBase64Url(text) {
16
+ const base64 = text.replaceAll("-", "+").replaceAll("_", "/");
17
+ const binary = atob(base64);
18
+ const bytes = new Uint8Array(binary.length);
19
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
20
+ return bytes;
21
+ }
22
+ function utf8Encode(text) {
23
+ return new TextEncoder().encode(text);
24
+ }
25
+ function utf8Decode(bytes) {
26
+ return new TextDecoder().decode(bytes);
27
+ }
28
+
29
+ // ../../packages/crypto/src/errors.ts
30
+ var SeekritCryptoError = class extends Error {
31
+ code;
32
+ constructor(code, message) {
33
+ super(message);
34
+ this.name = "SeekritCryptoError";
35
+ this.code = code;
36
+ }
37
+ };
38
+ function splitBlob(blob, prefix, segments) {
39
+ const parts = blob.split(".");
40
+ if (parts[0] !== prefix) {
41
+ throw new SeekritCryptoError(
42
+ "UNSUPPORTED_VERSION",
43
+ `expected a "${prefix}" blob, got "${parts[0] ?? ""}"`
44
+ );
45
+ }
46
+ if (parts.length !== segments + 1 || parts.some((p) => p.length === 0)) {
47
+ throw new SeekritCryptoError("MALFORMED_BLOB", `malformed "${prefix}" blob`);
48
+ }
49
+ return parts.slice(1);
50
+ }
51
+
52
+ // ../../packages/crypto/src/aes.ts
53
+ var SECRET_PREFIX = "sc1";
54
+ var IV_LENGTH = 12;
55
+ function generateDek() {
56
+ return crypto.getRandomValues(new Uint8Array(32));
57
+ }
58
+ async function importDek(dek, usage) {
59
+ return crypto.subtle.importKey("raw", dek, { name: "AES-GCM" }, false, [usage]);
60
+ }
61
+ async function encryptSecret(dek, plaintext, aad) {
62
+ const key = await importDek(dek, "encrypt");
63
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
64
+ const ciphertext = await crypto.subtle.encrypt(
65
+ { name: "AES-GCM", iv, additionalData: utf8Encode(aad) },
66
+ key,
67
+ utf8Encode(plaintext)
68
+ );
69
+ return `${SECRET_PREFIX}.${toBase64Url(iv)}.${toBase64Url(new Uint8Array(ciphertext))}`;
70
+ }
71
+ async function decryptSecret(dek, blob, aad) {
72
+ const [ivB64, ctB64] = splitBlob(blob, SECRET_PREFIX, 2);
73
+ const key = await importDek(dek, "decrypt");
74
+ try {
75
+ const plaintext = await crypto.subtle.decrypt(
76
+ {
77
+ name: "AES-GCM",
78
+ iv: fromBase64Url(ivB64),
79
+ additionalData: utf8Encode(aad)
80
+ },
81
+ key,
82
+ fromBase64Url(ctB64)
83
+ );
84
+ return utf8Decode(new Uint8Array(plaintext));
85
+ } catch {
86
+ throw new SeekritCryptoError(
87
+ "DECRYPT_FAILED",
88
+ "secret decryption failed: wrong key, tampered data, or mismatched context"
89
+ );
90
+ }
91
+ }
92
+ function secretAad(environmentId, secretName) {
93
+ return `${environmentId}/${secretName}`;
94
+ }
95
+
96
+ // ../../packages/crypto/src/keys.ts
97
+ async function generateKeyPair() {
98
+ const pair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, [
99
+ "deriveBits"
100
+ ]);
101
+ const [publicJwk, privateJwk] = await Promise.all([
102
+ crypto.subtle.exportKey("jwk", pair.publicKey),
103
+ crypto.subtle.exportKey("jwk", pair.privateKey)
104
+ ]);
105
+ return {
106
+ publicKeyJwk: JSON.stringify(publicJwk),
107
+ privateKeyJwk: JSON.stringify(privateJwk)
108
+ };
109
+ }
110
+ async function importPublicKey(publicKeyJwk) {
111
+ return crypto.subtle.importKey(
112
+ "jwk",
113
+ JSON.parse(publicKeyJwk),
114
+ { name: "ECDH", namedCurve: "P-256" },
115
+ true,
116
+ []
117
+ );
118
+ }
119
+ async function importPrivateKey(privateKeyJwk) {
120
+ return crypto.subtle.importKey(
121
+ "jwk",
122
+ JSON.parse(privateKeyJwk),
123
+ { name: "ECDH", namedCurve: "P-256" },
124
+ true,
125
+ ["deriveBits"]
126
+ );
127
+ }
128
+ async function exportPrivateKeyPkcs8(key) {
129
+ return new Uint8Array(await crypto.subtle.exportKey("pkcs8", key));
130
+ }
131
+ async function importPrivateKeyPkcs8(pkcs8) {
132
+ return crypto.subtle.importKey(
133
+ "pkcs8",
134
+ pkcs8,
135
+ { name: "ECDH", namedCurve: "P-256" },
136
+ true,
137
+ ["deriveBits"]
138
+ );
139
+ }
140
+
141
+ // ../../packages/crypto/src/passphrase.ts
142
+ var PK_PREFIX = "pk1";
143
+ var PBKDF2_ITERATIONS = 6e5;
144
+ async function deriveKek(passphrase, salt, iterations, usage) {
145
+ const material = await crypto.subtle.importKey(
146
+ "raw",
147
+ utf8Encode(passphrase),
148
+ "PBKDF2",
149
+ false,
150
+ ["deriveKey"]
151
+ );
152
+ return crypto.subtle.deriveKey(
153
+ { name: "PBKDF2", hash: "SHA-256", salt, iterations },
154
+ material,
155
+ { name: "AES-GCM", length: 256 },
156
+ false,
157
+ [usage]
158
+ );
159
+ }
160
+ async function encryptPrivateKey(passphrase, privateKeyJwk) {
161
+ const salt = crypto.getRandomValues(new Uint8Array(16));
162
+ const iv = crypto.getRandomValues(new Uint8Array(12));
163
+ const kek = await deriveKek(passphrase, salt, PBKDF2_ITERATIONS, "encrypt");
164
+ const ciphertext = await crypto.subtle.encrypt(
165
+ { name: "AES-GCM", iv },
166
+ kek,
167
+ utf8Encode(privateKeyJwk)
168
+ );
169
+ return [
170
+ PK_PREFIX,
171
+ String(PBKDF2_ITERATIONS),
172
+ toBase64Url(salt),
173
+ toBase64Url(iv),
174
+ toBase64Url(new Uint8Array(ciphertext))
175
+ ].join(".");
176
+ }
177
+ async function decryptPrivateKey(passphrase, blob) {
178
+ const [iterStr, saltB64, ivB64, ctB64] = splitBlob(blob, PK_PREFIX, 4);
179
+ const iterations = Number.parseInt(iterStr, 10);
180
+ if (!Number.isFinite(iterations) || iterations < 1) {
181
+ throw new SeekritCryptoError("MALFORMED_BLOB", "invalid PBKDF2 iteration count");
182
+ }
183
+ const kek = await deriveKek(passphrase, fromBase64Url(saltB64), iterations, "decrypt");
184
+ try {
185
+ const plaintext = await crypto.subtle.decrypt(
186
+ { name: "AES-GCM", iv: fromBase64Url(ivB64) },
187
+ kek,
188
+ fromBase64Url(ctB64)
189
+ );
190
+ return utf8Decode(new Uint8Array(plaintext));
191
+ } catch {
192
+ throw new SeekritCryptoError("DECRYPT_FAILED", "wrong passphrase or corrupted key blob");
193
+ }
194
+ }
195
+
196
+ // ../../packages/crypto/src/token.ts
197
+ var TOKEN_PREFIX = "skt";
198
+ var TOKEN_ID_LENGTH = 22;
199
+ var ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
200
+ function randomTokenId() {
201
+ let out = "";
202
+ while (out.length < TOKEN_ID_LENGTH) {
203
+ const bytes = crypto.getRandomValues(new Uint8Array(TOKEN_ID_LENGTH - out.length));
204
+ for (const byte of bytes) {
205
+ if (byte < 248) out += ID_ALPHABET[byte % 62];
206
+ if (out.length === TOKEN_ID_LENGTH) break;
207
+ }
208
+ }
209
+ return `${TOKEN_PREFIX}_${out}`;
210
+ }
211
+ async function hashToken(token2) {
212
+ const digest = await crypto.subtle.digest("SHA-256", utf8Encode(token2));
213
+ return toBase64Url(new Uint8Array(digest));
214
+ }
215
+ async function createServiceToken() {
216
+ const { publicKeyJwk, privateKeyJwk } = await generateKeyPair();
217
+ const privateKey = await importPrivateKey(privateKeyJwk);
218
+ const pkcs8 = await exportPrivateKeyPkcs8(privateKey);
219
+ const tokenId = randomTokenId();
220
+ const token2 = `${tokenId}_${toBase64Url(pkcs8)}`;
221
+ return { token: token2, tokenId, tokenHash: await hashToken(token2), publicKeyJwk };
222
+ }
223
+ async function parseServiceToken(token2) {
224
+ const match = /^(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)$/.exec(token2);
225
+ if (!match) {
226
+ throw new SeekritCryptoError("MALFORMED_TOKEN", "not a valid seekrit service token");
227
+ }
228
+ const [, tokenId, keyB64] = match;
229
+ try {
230
+ const privateKey = await importPrivateKeyPkcs8(fromBase64Url(keyB64));
231
+ return { tokenId, privateKey };
232
+ } catch {
233
+ throw new SeekritCryptoError("MALFORMED_TOKEN", "service token private key is corrupted");
234
+ }
235
+ }
236
+ function isServiceToken(value) {
237
+ return value.startsWith(`${TOKEN_PREFIX}_`);
238
+ }
239
+
240
+ // ../../packages/crypto/src/wrap.ts
241
+ var WRAP_PREFIX = "wd1";
242
+ var HKDF_INFO = "seekrit/wrap-dek/v1";
243
+ async function deriveWrappingKey(ownPrivateKey, peerPublicKey, salt, usage) {
244
+ const ecdh = { name: "ECDH", public: peerPublicKey };
245
+ const sharedBits = await crypto.subtle.deriveBits(ecdh, ownPrivateKey, 256);
246
+ const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
247
+ return crypto.subtle.deriveKey(
248
+ {
249
+ name: "HKDF",
250
+ hash: "SHA-256",
251
+ salt,
252
+ info: utf8Encode(HKDF_INFO)
253
+ },
254
+ hkdfKey,
255
+ { name: "AES-GCM", length: 256 },
256
+ false,
257
+ [usage]
258
+ );
259
+ }
260
+ async function wrapDek(dek, recipientPublicKeyJwk) {
261
+ const recipientKey = await importPublicKey(recipientPublicKeyJwk);
262
+ const ephemeral = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, [
263
+ "deriveBits"
264
+ ]);
265
+ const salt = crypto.getRandomValues(new Uint8Array(16));
266
+ const wrappingKey = await deriveWrappingKey(ephemeral.privateKey, recipientKey, salt, "encrypt");
267
+ const iv = crypto.getRandomValues(new Uint8Array(12));
268
+ const ciphertext = await crypto.subtle.encrypt(
269
+ { name: "AES-GCM", iv },
270
+ wrappingKey,
271
+ dek
272
+ );
273
+ const ephemeralRaw = new Uint8Array(
274
+ await crypto.subtle.exportKey("raw", ephemeral.publicKey)
275
+ );
276
+ return [
277
+ WRAP_PREFIX,
278
+ toBase64Url(ephemeralRaw),
279
+ toBase64Url(salt),
280
+ toBase64Url(iv),
281
+ toBase64Url(new Uint8Array(ciphertext))
282
+ ].join(".");
283
+ }
284
+ async function unwrapDek(wrapped, privateKey) {
285
+ const [ephB64, saltB64, ivB64, ctB64] = splitBlob(wrapped, WRAP_PREFIX, 4);
286
+ const ephemeralKey = await crypto.subtle.importKey(
287
+ "raw",
288
+ fromBase64Url(ephB64),
289
+ { name: "ECDH", namedCurve: "P-256" },
290
+ false,
291
+ []
292
+ );
293
+ const wrappingKey = await deriveWrappingKey(
294
+ privateKey,
295
+ ephemeralKey,
296
+ fromBase64Url(saltB64),
297
+ "decrypt"
298
+ );
299
+ try {
300
+ const dek = await crypto.subtle.decrypt(
301
+ { name: "AES-GCM", iv: fromBase64Url(ivB64) },
302
+ wrappingKey,
303
+ fromBase64Url(ctB64)
304
+ );
305
+ return new Uint8Array(dek);
306
+ } catch {
307
+ throw new SeekritCryptoError(
308
+ "DECRYPT_FAILED",
309
+ "DEK unwrap failed: wrong private key or tampered grant"
310
+ );
311
+ }
312
+ }
313
+
314
+ // src/index.ts
315
+ import { Command } from "commander";
316
+
317
+ // src/config.ts
318
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
319
+ import { homedir } from "os";
320
+ import { dirname, join, parse } from "path";
321
+ var DEFAULT_API_URL = "http://localhost:8787";
322
+ var PROJECT_FILE = "seekrit.json";
323
+ function globalConfigPath() {
324
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
325
+ }
326
+ function readGlobalConfig() {
327
+ const path = globalConfigPath();
328
+ if (!existsSync(path)) return {};
329
+ return JSON.parse(readFileSync(path, "utf8"));
330
+ }
331
+ function writeGlobalConfig(update) {
332
+ const path = globalConfigPath();
333
+ const merged = { ...readGlobalConfig(), ...update };
334
+ mkdirSync(dirname(path), { recursive: true });
335
+ writeFileSync(path, `${JSON.stringify(merged, null, 2)}
336
+ `, { mode: 384 });
337
+ }
338
+ function findProjectConfig(startDir = process.cwd()) {
339
+ let dir = startDir;
340
+ const { root } = parse(dir);
341
+ while (true) {
342
+ const candidate = join(dir, PROJECT_FILE);
343
+ if (existsSync(candidate)) {
344
+ return JSON.parse(readFileSync(candidate, "utf8"));
345
+ }
346
+ if (dir === root) return null;
347
+ dir = dirname(dir);
348
+ }
349
+ }
350
+ function writeProjectConfig(config, dir = process.cwd()) {
351
+ const path = join(dir, PROJECT_FILE);
352
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
353
+ `);
354
+ return path;
355
+ }
356
+
357
+ // ../../packages/api-client/src/index.ts
358
+ var SeekritApiError = class extends Error {
359
+ status;
360
+ code;
361
+ constructor(status, code, message) {
362
+ super(message);
363
+ this.name = "SeekritApiError";
364
+ this.status = status;
365
+ this.code = code;
366
+ }
367
+ };
368
+ var SeekritClient = class {
369
+ baseUrl;
370
+ auth;
371
+ fetchImpl;
372
+ constructor(options) {
373
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
374
+ this.auth = options.auth;
375
+ this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
376
+ }
377
+ async request(method, path, body) {
378
+ const headers = { accept: "application/json" };
379
+ if (this.auth.type === "bearer") {
380
+ headers.authorization = `Bearer ${this.auth.token}`;
381
+ } else if (this.auth.type === "dynamic") {
382
+ const token2 = await this.auth.getToken();
383
+ if (!token2) throw new SeekritApiError(401, "unauthorized", "session expired");
384
+ headers.authorization = `Bearer ${token2}`;
385
+ } else {
386
+ headers["x-seekrit-dev-user"] = this.auth.email;
387
+ }
388
+ if (body !== void 0) headers["content-type"] = "application/json";
389
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
390
+ method,
391
+ headers,
392
+ body: body === void 0 ? void 0 : JSON.stringify(body)
393
+ });
394
+ if (!res.ok) {
395
+ const fallback = { error: { code: "internal", message: `HTTP ${res.status}` } };
396
+ const payload = await res.json().catch(() => fallback);
397
+ throw new SeekritApiError(
398
+ res.status,
399
+ payload.error?.code ?? "internal",
400
+ payload.error?.message ?? `HTTP ${res.status}`
401
+ );
402
+ }
403
+ return await res.json();
404
+ }
405
+ // ── identity ────────────────────────────────────────────────────────────
406
+ me() {
407
+ return this.request("GET", "/v1/me");
408
+ }
409
+ getMyKeys() {
410
+ return this.request("GET", "/v1/me/keys");
411
+ }
412
+ setMyKeys(input) {
413
+ return this.request("PUT", "/v1/me/keys", input);
414
+ }
415
+ // ── organizations ───────────────────────────────────────────────────────
416
+ listOrgs() {
417
+ return this.request("GET", "/v1/orgs");
418
+ }
419
+ createOrg(input) {
420
+ return this.request("POST", "/v1/orgs", input);
421
+ }
422
+ getOrg(orgId) {
423
+ return this.request("GET", `/v1/orgs/${orgId}`);
424
+ }
425
+ listMembers(orgId) {
426
+ return this.request("GET", `/v1/orgs/${orgId}/members`);
427
+ }
428
+ // ── applications ────────────────────────────────────────────────────────
429
+ listApps(orgId) {
430
+ return this.request("GET", `/v1/orgs/${orgId}/apps`);
431
+ }
432
+ createApp(orgId, input) {
433
+ return this.request("POST", `/v1/orgs/${orgId}/apps`, input);
434
+ }
435
+ getApp(orgId, appId) {
436
+ return this.request("GET", `/v1/orgs/${orgId}/apps/${appId}`);
437
+ }
438
+ deleteApp(orgId, appId) {
439
+ return this.request("DELETE", `/v1/orgs/${orgId}/apps/${appId}`);
440
+ }
441
+ // ── environments ────────────────────────────────────────────────────────
442
+ listEnvs(orgId, appId) {
443
+ return this.request("GET", `/v1/orgs/${orgId}/apps/${appId}/envs`);
444
+ }
445
+ createEnv(orgId, appId, input) {
446
+ return this.request("POST", `/v1/orgs/${orgId}/apps/${appId}/envs`, input);
447
+ }
448
+ getEnv(orgId, envId) {
449
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}`);
450
+ }
451
+ deleteEnv(orgId, envId) {
452
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}`);
453
+ }
454
+ // ── secrets (ciphertext only — encrypt/decrypt happens in the caller) ───
455
+ listSecrets(orgId, envId) {
456
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/secrets`);
457
+ }
458
+ setSecret(orgId, envId, name, ciphertext) {
459
+ return this.request("PUT", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`, { ciphertext });
460
+ }
461
+ deleteSecret(orgId, envId, name) {
462
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`);
463
+ }
464
+ // ── environment key grants ──────────────────────────────────────────────
465
+ /** The calling principal's wrapped DEK for this environment. */
466
+ getMyEnvKey(orgId, envId) {
467
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key`);
468
+ }
469
+ listEnvKeys(orgId, envId) {
470
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/keys`);
471
+ }
472
+ grantEnvKey(orgId, envId, input) {
473
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/keys`, input);
474
+ }
475
+ revokeEnvKey(orgId, envId, grantId) {
476
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/keys/${grantId}`);
477
+ }
478
+ // ── service tokens ──────────────────────────────────────────────────────
479
+ listTokens(orgId) {
480
+ return this.request("GET", `/v1/orgs/${orgId}/tokens`);
481
+ }
482
+ createToken(orgId, input) {
483
+ return this.request("POST", `/v1/orgs/${orgId}/tokens`, input);
484
+ }
485
+ revokeToken(orgId, tokenId) {
486
+ return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}`);
487
+ }
488
+ // ── audit ───────────────────────────────────────────────────────────────
489
+ listAudit(orgId, query = {}) {
490
+ const params = new URLSearchParams();
491
+ if (query.cursor) params.set("cursor", query.cursor);
492
+ if (query.limit) params.set("limit", String(query.limit));
493
+ if (query.action) params.set("action", query.action);
494
+ if (query.resourceType) params.set("resourceType", query.resourceType);
495
+ const qs = params.size > 0 ? `?${params}` : "";
496
+ return this.request("GET", `/v1/orgs/${orgId}/audit${qs}`);
497
+ }
498
+ };
499
+
500
+ // src/io.ts
501
+ import { createInterface } from "readline";
502
+ import { Writable } from "stream";
503
+ function fail(message) {
504
+ console.error(`error: ${message}`);
505
+ process.exit(1);
506
+ }
507
+ function promptHidden(question) {
508
+ const muted = new Writable({
509
+ write(_chunk, _encoding, callback) {
510
+ callback();
511
+ }
512
+ });
513
+ process.stderr.write(question);
514
+ const rl = createInterface({ input: process.stdin, output: muted, terminal: true });
515
+ return new Promise((resolve) => {
516
+ rl.question("", (answer) => {
517
+ rl.close();
518
+ process.stderr.write("\n");
519
+ resolve(answer);
520
+ });
521
+ });
522
+ }
523
+ async function readStdin() {
524
+ const chunks = [];
525
+ for await (const chunk of process.stdin) chunks.push(chunk);
526
+ return Buffer.concat(chunks).toString("utf8");
527
+ }
528
+
529
+ // src/context.ts
530
+ function buildContext() {
531
+ const config = readGlobalConfig();
532
+ const apiUrl = process.env.SEEKRIT_API_URL ?? config.apiUrl ?? DEFAULT_API_URL;
533
+ const token2 = process.env.SEEKRIT_TOKEN ?? config.token;
534
+ const devUser = process.env.SEEKRIT_DEV_USER ?? config.devUser;
535
+ let auth;
536
+ if (token2) auth = { type: "bearer", token: token2 };
537
+ else if (devUser) auth = { type: "dev", email: devUser };
538
+ else {
539
+ fail(
540
+ "no credentials found \u2014 run `seekrit login --token skt_\u2026`, or set SEEKRIT_TOKEN / SEEKRIT_DEV_USER"
541
+ );
542
+ }
543
+ return { client: new SeekritClient({ baseUrl: apiUrl, auth }), auth };
544
+ }
545
+ function requireProject() {
546
+ const project = findProjectConfig();
547
+ if (!project) {
548
+ fail("no seekrit.json found \u2014 run `seekrit init` in your project directory");
549
+ }
550
+ return project;
551
+ }
552
+ async function getDek(ctx, orgId, envId) {
553
+ const { wrappedDek } = await ctx.client.getMyEnvKey(orgId, envId);
554
+ if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
555
+ const { privateKey } = await parseServiceToken(ctx.auth.token);
556
+ return unwrapDek(wrappedDek, privateKey);
557
+ }
558
+ const { encryptedPrivateKey } = await ctx.client.getMyKeys();
559
+ const passphrase = process.env.SEEKRIT_PASSPHRASE ?? await promptHidden("Passphrase: ");
560
+ const privateKeyJwk = await decryptPrivateKey(passphrase, encryptedPrivateKey);
561
+ return unwrapDek(wrappedDek, await importPrivateKey(privateKeyJwk));
562
+ }
563
+
564
+ // src/format.ts
565
+ function needsQuoting(value) {
566
+ return /[\s"'`$\\#]/.test(value) || value === "";
567
+ }
568
+ function dotenvQuote(value) {
569
+ if (!needsQuoting(value)) return value;
570
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n")}"`;
571
+ }
572
+ function shellQuote(value) {
573
+ return `'${value.replaceAll("'", `'\\''`)}'`;
574
+ }
575
+ function formatSecrets(values, format) {
576
+ const names = Object.keys(values).sort();
577
+ switch (format) {
578
+ case "json":
579
+ return JSON.stringify(values, names, 2);
580
+ case "shell":
581
+ return names.map((name) => `export ${name}=${shellQuote(values[name] ?? "")}`).join("\n");
582
+ case "dotenv":
583
+ return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
584
+ }
585
+ }
586
+
587
+ // src/secrets.ts
588
+ async function fetchDecryptedSecrets(ctx, orgId, envId) {
589
+ const [dek, { secrets: secrets2 }] = await Promise.all([
590
+ getDek(ctx, orgId, envId),
591
+ ctx.client.listSecrets(orgId, envId)
592
+ ]);
593
+ const entries = await Promise.all(
594
+ secrets2.map(
595
+ async (secret) => [
596
+ secret.name,
597
+ await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))
598
+ ]
599
+ )
600
+ );
601
+ return Object.fromEntries(entries);
602
+ }
603
+ async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
604
+ const dek = await getDek(ctx, orgId, envId);
605
+ const ciphertext = await encryptSecret(dek, value, secretAad(envId, name));
606
+ await ctx.client.setSecret(orgId, envId, name, ciphertext);
607
+ }
608
+
609
+ // package.json
610
+ var version = "0.0.1";
611
+
612
+ // src/index.ts
613
+ var program = new Command("seekrit").description("End-to-end encrypted secrets manager").version(version).enablePositionalOptions();
614
+ program.command("login").description("store credentials for the API").option("--token <token>", "service token (skt_\u2026)").option(
615
+ "--dev-user <email>",
616
+ "dev-mode identity (local API with AUTH_MODE=dev)"
617
+ ).option("--api-url <url>", "API base URL").action((options) => {
618
+ if (options.token && !isServiceToken(options.token))
619
+ fail("token must start with skt_");
620
+ writeGlobalConfig({
621
+ ...options.token ? { token: options.token } : {},
622
+ ...options.devUser ? { devUser: options.devUser } : {},
623
+ ...options.apiUrl ? { apiUrl: options.apiUrl } : {}
624
+ });
625
+ console.error("credentials saved");
626
+ });
627
+ program.command("whoami").description("show the authenticated identity").action(async () => {
628
+ const ctx = buildContext();
629
+ if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
630
+ const { tokenId } = await parseServiceToken(ctx.auth.token);
631
+ const { orgs: orgs2 } = await ctx.client.listOrgs();
632
+ console.log(
633
+ `service token ${tokenId} (org: ${orgs2[0]?.slug ?? "unknown"})`
634
+ );
635
+ return;
636
+ }
637
+ const { user, orgs } = await ctx.client.me();
638
+ console.log(
639
+ `${user.email}${user.hasKeys ? "" : " (key setup pending \u2014 run `seekrit keys setup`)"}`
640
+ );
641
+ for (const org2 of orgs) console.log(` ${org2.slug} (${org2.role})`);
642
+ });
643
+ var keys = program.command("keys").description("manage your encryption keys");
644
+ keys.command("setup").description("generate your keypair and protect it with a passphrase").action(async () => {
645
+ const ctx = buildContext();
646
+ const passphrase = process.env.SEEKRIT_PASSPHRASE ?? await promptHidden("New passphrase: ");
647
+ if (!process.env.SEEKRIT_PASSPHRASE) {
648
+ const confirm = await promptHidden("Confirm passphrase: ");
649
+ if (confirm !== passphrase) fail("passphrases do not match");
650
+ }
651
+ if (passphrase.length < 8) fail("passphrase must be at least 8 characters");
652
+ const pair = await generateKeyPair();
653
+ const encryptedPrivateKey = await encryptPrivateKey(
654
+ passphrase,
655
+ pair.privateKeyJwk
656
+ );
657
+ await ctx.client.setMyKeys({
658
+ publicKeyJwk: pair.publicKeyJwk,
659
+ encryptedPrivateKey
660
+ });
661
+ console.error(
662
+ "keys generated and uploaded \u2014 your passphrase never leaves this machine"
663
+ );
664
+ });
665
+ program.command("init").description("link this directory to an org/app/environment").requiredOption("--org <slug>", "organization slug").requiredOption("--app <slug>", "application slug").requiredOption("--env <slug>", "environment slug").action(async (options) => {
666
+ const ctx = buildContext();
667
+ const { orgs } = await ctx.client.listOrgs();
668
+ const org2 = orgs.find(
669
+ (o) => o.slug === options.org || o.id === options.org
670
+ );
671
+ if (!org2) fail(`no accessible org "${options.org}"`);
672
+ const { apps } = await ctx.client.listApps(org2.id);
673
+ const app2 = apps.find(
674
+ (a) => a.slug === options.app || a.id === options.app
675
+ );
676
+ if (!app2) fail(`no app "${options.app}" in ${org2.slug}`);
677
+ const { environments } = await ctx.client.listEnvs(org2.id, app2.id);
678
+ const env2 = environments.find(
679
+ (e) => e.slug === options.env || e.id === options.env
680
+ );
681
+ if (!env2) fail(`no environment "${options.env}" in ${app2.slug}`);
682
+ const path = writeProjectConfig({
683
+ orgId: org2.id,
684
+ appId: app2.id,
685
+ envId: env2.id,
686
+ org: org2.slug,
687
+ app: app2.slug,
688
+ env: env2.slug
689
+ });
690
+ console.error(`linked ${org2.slug}/${app2.slug}/${env2.slug} \u2192 ${path}`);
691
+ });
692
+ var org = program.command("org").description("manage organizations");
693
+ org.command("create").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
694
+ const ctx = buildContext();
695
+ const created = await ctx.client.createOrg({
696
+ name: options.name,
697
+ slug: options.slug
698
+ });
699
+ console.error(`created org ${created.org.slug} (${created.org.id})`);
700
+ });
701
+ var app = program.command("app").description("manage applications");
702
+ app.command("create").requiredOption("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
703
+ const ctx = buildContext();
704
+ const { orgs } = await ctx.client.listOrgs();
705
+ const orgRow = orgs.find(
706
+ (o) => o.slug === options.org || o.id === options.org
707
+ );
708
+ if (!orgRow) fail(`no accessible org "${options.org}"`);
709
+ const created = await ctx.client.createApp(orgRow.id, {
710
+ name: options.name,
711
+ slug: options.slug
712
+ });
713
+ console.error(`created app ${created.app.slug} (${created.app.id})`);
714
+ });
715
+ var env = program.command("env").description("manage environments");
716
+ env.command("create").description("create an environment (generates its data key locally)").requiredOption("--org <slug>").requiredOption("--app <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(
717
+ async (options) => {
718
+ const ctx = buildContext();
719
+ const { orgs } = await ctx.client.listOrgs();
720
+ const orgRow = orgs.find(
721
+ (o) => o.slug === options.org || o.id === options.org
722
+ );
723
+ if (!orgRow) fail(`no accessible org "${options.org}"`);
724
+ const { apps } = await ctx.client.listApps(orgRow.id);
725
+ const appRow = apps.find(
726
+ (a) => a.slug === options.app || a.id === options.app
727
+ );
728
+ if (!appRow) fail(`no app "${options.app}" in ${orgRow.slug}`);
729
+ const { user } = await ctx.client.me();
730
+ if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
731
+ const dek = generateDek();
732
+ const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
733
+ const created = await ctx.client.createEnv(orgRow.id, appRow.id, {
734
+ name: options.name,
735
+ slug: options.slug,
736
+ wrappedDek
737
+ });
738
+ console.error(
739
+ `created environment ${created.environment.slug} (${created.environment.id})`
740
+ );
741
+ }
742
+ );
743
+ var secrets = program.command("secrets").description("manage secrets in the linked environment");
744
+ secrets.command("list").description("list secret names (no values)").action(async () => {
745
+ const ctx = buildContext();
746
+ const project = requireProject();
747
+ const { secrets: rows } = await ctx.client.listSecrets(
748
+ project.orgId,
749
+ project.envId
750
+ );
751
+ for (const row of rows)
752
+ console.log(`${row.name} v${row.version} ${row.updatedAt}`);
753
+ });
754
+ secrets.command("get <name>").description("decrypt and print one secret value").action(async (name) => {
755
+ const ctx = buildContext();
756
+ const project = requireProject();
757
+ const values = await fetchDecryptedSecrets(
758
+ ctx,
759
+ project.orgId,
760
+ project.envId
761
+ );
762
+ const value = values[name];
763
+ if (value === void 0) fail(`no secret named ${name}`);
764
+ process.stdout.write(value);
765
+ if (process.stdout.isTTY) process.stdout.write("\n");
766
+ });
767
+ secrets.command("set <name> [value]").description(
768
+ "encrypt and store a secret (reads stdin when value is omitted or '-')"
769
+ ).action(async (name, value) => {
770
+ const ctx = buildContext();
771
+ const project = requireProject();
772
+ const plaintext = value === void 0 || value === "-" ? (await readStdin()).replace(/\n$/, "") : value;
773
+ await encryptAndSetSecret(
774
+ ctx,
775
+ project.orgId,
776
+ project.envId,
777
+ name,
778
+ plaintext
779
+ );
780
+ console.error(`${name} saved`);
781
+ });
782
+ secrets.command("rm <name>").description("delete a secret").action(async (name) => {
783
+ const ctx = buildContext();
784
+ const project = requireProject();
785
+ await ctx.client.deleteSecret(project.orgId, project.envId, name);
786
+ console.error(`${name} deleted`);
787
+ });
788
+ program.command("run").description(
789
+ "run a command with decrypted secrets injected into its environment"
790
+ ).passThroughOptions().argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts) => {
791
+ const ctx = buildContext();
792
+ const project = requireProject();
793
+ const values = await fetchDecryptedSecrets(
794
+ ctx,
795
+ project.orgId,
796
+ project.envId
797
+ );
798
+ const [cmd, ...args] = commandParts;
799
+ if (!cmd) fail("no command given");
800
+ const child = spawn(cmd, args, {
801
+ stdio: "inherit",
802
+ env: { ...process.env, ...values }
803
+ });
804
+ child.on("exit", (code, signal) => {
805
+ if (signal) process.kill(process.pid, signal);
806
+ process.exit(code ?? 1);
807
+ });
808
+ child.on("error", (err) => fail(`failed to start ${cmd}: ${err.message}`));
809
+ });
810
+ program.command("export").description("print decrypted secrets (dotenv, json, or shell)").option("--format <format>", "dotenv | json | shell", "dotenv").action(async (options) => {
811
+ if (!["dotenv", "json", "shell"].includes(options.format)) {
812
+ fail("format must be dotenv, json, or shell");
813
+ }
814
+ const ctx = buildContext();
815
+ const project = requireProject();
816
+ const values = await fetchDecryptedSecrets(
817
+ ctx,
818
+ project.orgId,
819
+ project.envId
820
+ );
821
+ console.log(formatSecrets(values, options.format));
822
+ });
823
+ program.command("grant").description(
824
+ "give a member or service token access to the linked environment's key"
825
+ ).option("--user <email>", "grant to an org member by email").option("--token <tokenId>", "grant to a service token by id (skt_\u2026)").action(async (options) => {
826
+ if (!options.user === !options.token)
827
+ fail("pass exactly one of --user or --token");
828
+ const ctx = buildContext();
829
+ const project = requireProject();
830
+ const dek = await getDek(ctx, project.orgId, project.envId);
831
+ let principalType;
832
+ let principalId;
833
+ let publicKeyJwk;
834
+ if (options.user) {
835
+ const { members } = await ctx.client.listMembers(project.orgId);
836
+ const member = members.find((m) => m.email === options.user);
837
+ if (!member) fail(`no member ${options.user}`);
838
+ if (!member.publicKeyJwk)
839
+ fail(`${options.user} has not completed key setup`);
840
+ [principalType, principalId, publicKeyJwk] = [
841
+ "user",
842
+ member.userId,
843
+ member.publicKeyJwk
844
+ ];
845
+ } else {
846
+ const { tokens } = await ctx.client.listTokens(project.orgId);
847
+ const token2 = tokens.find((t) => t.id === options.token);
848
+ if (!token2) fail(`no service token ${options.token}`);
849
+ [principalType, principalId, publicKeyJwk] = [
850
+ "service_token",
851
+ token2.id,
852
+ token2.publicKeyJwk
853
+ ];
854
+ }
855
+ const wrappedDek = await wrapDek(dek, publicKeyJwk);
856
+ await ctx.client.grantEnvKey(project.orgId, project.envId, {
857
+ principalType,
858
+ principalId,
859
+ wrappedDek
860
+ });
861
+ console.error(
862
+ `granted ${project.env ?? project.envId} access to ${principalId}`
863
+ );
864
+ });
865
+ var token = program.command("token").description("manage service tokens (CI, docker, agents)");
866
+ token.command("create").description("create a service token; prints the token once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--grant", "also grant access to the linked environment").action(async (options) => {
867
+ const ctx = buildContext();
868
+ const project = requireProject();
869
+ const created = await createServiceToken();
870
+ await ctx.client.createToken(project.orgId, {
871
+ name: options.name,
872
+ tokenId: created.tokenId,
873
+ tokenHash: created.tokenHash,
874
+ publicKeyJwk: created.publicKeyJwk
875
+ });
876
+ if (options.grant) {
877
+ const dek = await getDek(ctx, project.orgId, project.envId);
878
+ const wrappedDek = await wrapDek(dek, created.publicKeyJwk);
879
+ await ctx.client.grantEnvKey(project.orgId, project.envId, {
880
+ principalType: "service_token",
881
+ principalId: created.tokenId,
882
+ wrappedDek
883
+ });
884
+ }
885
+ console.error(
886
+ `token created${options.grant ? " and granted" : ""} \u2014 save it now, it is not stored:`
887
+ );
888
+ console.log(created.token);
889
+ });
890
+ token.command("list").description("list service tokens").action(async () => {
891
+ const ctx = buildContext();
892
+ const project = requireProject();
893
+ const { tokens } = await ctx.client.listTokens(project.orgId);
894
+ for (const t of tokens) {
895
+ const status = t.revokedAt ? "revoked" : t.expiresAt && Date.parse(t.expiresAt) < Date.now() ? "expired" : "active";
896
+ console.log(
897
+ `${t.id} ${t.name} ${status} last used: ${t.lastUsedAt ?? "never"}`
898
+ );
899
+ }
900
+ });
901
+ token.command("revoke <tokenId>").description("revoke a service token").action(async (tokenId) => {
902
+ const ctx = buildContext();
903
+ const project = requireProject();
904
+ await ctx.client.revokeToken(project.orgId, tokenId);
905
+ console.error(`${tokenId} revoked`);
906
+ });
907
+ program.command("audit").description("show the org audit trail").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
908
+ const ctx = buildContext();
909
+ const project = requireProject();
910
+ const { entries } = await ctx.client.listAudit(project.orgId, {
911
+ limit: Number.parseInt(options.limit, 10) || 50
912
+ });
913
+ for (const entry of entries) {
914
+ console.log(
915
+ `${entry.createdAt} ${entry.action} ${entry.actorType}:${entry.actorId} ${entry.resourceType}${entry.resourceId ? `:${entry.resourceId}` : ""}`
916
+ );
917
+ }
918
+ });
919
+ program.parseAsync().catch((err) => {
920
+ fail(err instanceof Error ? err.message : String(err));
921
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@seekrit/cli",
3
+ "version": "0.0.1",
4
+ "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "seekrit": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "typecheck": "tsc --noEmit"
22
+ },
23
+ "dependencies": {
24
+ "commander": "^15.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@seekrit/api-client": "workspace:*",
28
+ "@seekrit/core": "workspace:*",
29
+ "@seekrit/crypto": "workspace:*",
30
+ "@types/node": "^26.1.0",
31
+ "tsup": "^8.5.1"
32
+ }
33
+ }