@seekrit/cli 0.0.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +131 -0
  2. package/dist/index.js +1072 -821
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,921 +1,1172 @@
1
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;
2
+ import { spawn } from "node:child_process";
3
+ import { Command } from "commander";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join, parse } from "node:path";
7
+ import { createInterface } from "node:readline";
8
+ import { Writable } from "node:stream";
9
+ //#region ../../packages/crypto/src/encoding.ts
10
+ const CHUNK = 32768;
11
+ /** Base64url (no padding) — portable across browsers, Workers, and Node. */
8
12
  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(/=+$/, "");
13
+ let binary = "";
14
+ for (let i = 0; i < bytes.length; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
15
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
14
16
  }
15
17
  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;
18
+ const base64 = text.replaceAll("-", "+").replaceAll("_", "/");
19
+ const binary = atob(base64);
20
+ const bytes = new Uint8Array(binary.length);
21
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
22
+ return bytes;
21
23
  }
22
24
  function utf8Encode(text) {
23
- return new TextEncoder().encode(text);
25
+ return new TextEncoder().encode(text);
24
26
  }
25
27
  function utf8Decode(bytes) {
26
- return new TextDecoder().decode(bytes);
28
+ return new TextDecoder().decode(bytes);
27
29
  }
28
-
29
- // ../../packages/crypto/src/errors.ts
30
+ //#endregion
31
+ //#region ../../packages/crypto/src/errors.ts
30
32
  var SeekritCryptoError = class extends Error {
31
- code;
32
- constructor(code, message) {
33
- super(message);
34
- this.name = "SeekritCryptoError";
35
- this.code = code;
36
- }
33
+ code;
34
+ constructor(code, message) {
35
+ super(message);
36
+ this.name = "SeekritCryptoError";
37
+ this.code = code;
38
+ }
37
39
  };
40
+ /**
41
+ * Split a versioned blob like `sc1.<b64>.<b64>` and verify the prefix.
42
+ * AES-GCM auth failure downstream surfaces as DECRYPT_FAILED — that is also
43
+ * the "wrong passphrase" signal for passphrase-encrypted blobs.
44
+ */
38
45
  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;
46
+ const parts = blob.split(".");
47
+ if (parts[0] !== prefix) throw new SeekritCryptoError("UNSUPPORTED_VERSION", `expected a "${prefix}" blob, got "${parts[0] ?? ""}"`);
48
+ if (parts.length !== segments + 1 || parts.some((p) => p.length === 0)) throw new SeekritCryptoError("MALFORMED_BLOB", `malformed "${prefix}" blob`);
49
+ return parts.slice(1);
50
+ }
51
+ //#endregion
52
+ //#region ../../packages/crypto/src/aes.ts
53
+ const SECRET_PREFIX = "sc1";
54
+ const IV_LENGTH = 12;
55
+ /** Generate a fresh 256-bit data encryption key for an environment. */
55
56
  function generateDek() {
56
- return crypto.getRandomValues(new Uint8Array(32));
57
+ return crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
57
58
  }
58
59
  async function importDek(dek, usage) {
59
- return crypto.subtle.importKey("raw", dek, { name: "AES-GCM" }, false, [usage]);
60
+ return crypto.subtle.importKey("raw", dek, { name: "AES-GCM" }, false, [usage]);
60
61
  }
62
+ /**
63
+ * Encrypt a secret value with the environment DEK.
64
+ *
65
+ * @param aad Authenticated context binding the ciphertext to its location
66
+ * (e.g. `environmentId/SECRET_NAME`) so blobs cannot be swapped between
67
+ * secrets or environments without detection.
68
+ */
61
69
  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
+ const key = await importDek(dek, "encrypt");
71
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
72
+ const ciphertext = await crypto.subtle.encrypt({
73
+ name: "AES-GCM",
74
+ iv,
75
+ additionalData: utf8Encode(aad)
76
+ }, key, utf8Encode(plaintext));
77
+ return `${SECRET_PREFIX}.${toBase64Url(iv)}.${toBase64Url(new Uint8Array(ciphertext))}`;
70
78
  }
71
79
  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
- }
80
+ const [ivB64, ctB64] = splitBlob(blob, SECRET_PREFIX, 2);
81
+ const key = await importDek(dek, "decrypt");
82
+ try {
83
+ const plaintext = await crypto.subtle.decrypt({
84
+ name: "AES-GCM",
85
+ iv: fromBase64Url(ivB64),
86
+ additionalData: utf8Encode(aad)
87
+ }, key, fromBase64Url(ctB64));
88
+ return utf8Decode(new Uint8Array(plaintext));
89
+ } catch {
90
+ throw new SeekritCryptoError("DECRYPT_FAILED", "secret decryption failed: wrong key, tampered data, or mismatched context");
91
+ }
91
92
  }
93
+ /** AAD binding a secret ciphertext to its environment + name. */
92
94
  function secretAad(environmentId, secretName) {
93
- return `${environmentId}/${secretName}`;
95
+ return `${environmentId}/${secretName}`;
94
96
  }
95
-
96
- // ../../packages/crypto/src/keys.ts
97
+ //#endregion
98
+ //#region ../../packages/crypto/src/keys.ts
97
99
  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
- };
100
+ const pair = await crypto.subtle.generateKey({
101
+ name: "ECDH",
102
+ namedCurve: "P-256"
103
+ }, true, ["deriveBits"]);
104
+ const [publicJwk, privateJwk] = await Promise.all([crypto.subtle.exportKey("jwk", pair.publicKey), crypto.subtle.exportKey("jwk", pair.privateKey)]);
105
+ return {
106
+ publicKeyJwk: JSON.stringify(publicJwk),
107
+ privateKeyJwk: JSON.stringify(privateJwk)
108
+ };
109
109
  }
110
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
- );
111
+ return crypto.subtle.importKey("jwk", JSON.parse(publicKeyJwk), {
112
+ name: "ECDH",
113
+ namedCurve: "P-256"
114
+ }, true, []);
118
115
  }
119
116
  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
- );
117
+ return crypto.subtle.importKey("jwk", JSON.parse(privateKeyJwk), {
118
+ name: "ECDH",
119
+ namedCurve: "P-256"
120
+ }, true, ["deriveBits"]);
127
121
  }
128
122
  async function exportPrivateKeyPkcs8(key) {
129
- return new Uint8Array(await crypto.subtle.exportKey("pkcs8", key));
123
+ return new Uint8Array(await crypto.subtle.exportKey("pkcs8", key));
130
124
  }
131
125
  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;
126
+ return crypto.subtle.importKey("pkcs8", pkcs8, {
127
+ name: "ECDH",
128
+ namedCurve: "P-256"
129
+ }, true, ["deriveBits"]);
130
+ }
131
+ //#endregion
132
+ //#region ../../packages/crypto/src/passphrase.ts
133
+ /**
134
+ * User private keys are stored server-side encrypted under a key derived from
135
+ * the user's passphrase, so any browser or CLI session can fetch and unlock
136
+ * them without the server ever seeing the passphrase or plaintext key.
137
+ *
138
+ * KDF is PBKDF2-HMAC-SHA256 (WebCrypto-native everywhere). The blob embeds
139
+ * its own salt + iteration count for future agility; bumping ITERATIONS only
140
+ * affects newly written blobs. TODO: revisit Argon2id via WASM later.
141
+ *
142
+ * Blob format: `pk1.<iterations>.<salt>.<iv>.<ciphertext>`
143
+ */
144
+ const PK_PREFIX = "pk1";
145
+ /** OWASP-recommended minimum for PBKDF2-HMAC-SHA256. */
146
+ const PBKDF2_ITERATIONS = 6e5;
144
147
  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
- );
148
+ const material = await crypto.subtle.importKey("raw", utf8Encode(passphrase), "PBKDF2", false, ["deriveKey"]);
149
+ return crypto.subtle.deriveKey({
150
+ name: "PBKDF2",
151
+ hash: "SHA-256",
152
+ salt,
153
+ iterations
154
+ }, material, {
155
+ name: "AES-GCM",
156
+ length: 256
157
+ }, false, [usage]);
159
158
  }
160
159
  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(".");
160
+ const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
161
+ const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
162
+ const kek = await deriveKek(passphrase, salt, PBKDF2_ITERATIONS, "encrypt");
163
+ const ciphertext = await crypto.subtle.encrypt({
164
+ name: "AES-GCM",
165
+ iv
166
+ }, kek, utf8Encode(privateKeyJwk));
167
+ return [
168
+ PK_PREFIX,
169
+ String(PBKDF2_ITERATIONS),
170
+ toBase64Url(salt),
171
+ toBase64Url(iv),
172
+ toBase64Url(new Uint8Array(ciphertext))
173
+ ].join(".");
176
174
  }
175
+ /** Wrong passphrase surfaces as SeekritCryptoError with code DECRYPT_FAILED. */
177
176
  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";
177
+ const [iterStr, saltB64, ivB64, ctB64] = splitBlob(blob, PK_PREFIX, 4);
178
+ const iterations = Number.parseInt(iterStr, 10);
179
+ if (!Number.isFinite(iterations) || iterations < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "invalid PBKDF2 iteration count");
180
+ const kek = await deriveKek(passphrase, fromBase64Url(saltB64), iterations, "decrypt");
181
+ try {
182
+ const plaintext = await crypto.subtle.decrypt({
183
+ name: "AES-GCM",
184
+ iv: fromBase64Url(ivB64)
185
+ }, kek, fromBase64Url(ctB64));
186
+ return utf8Decode(new Uint8Array(plaintext));
187
+ } catch {
188
+ throw new SeekritCryptoError("DECRYPT_FAILED", "wrong passphrase or corrupted key blob");
189
+ }
190
+ }
191
+ //#endregion
192
+ //#region ../../packages/crypto/src/token.ts
193
+ /**
194
+ * Service tokens (CI, docker builds, agent proxies, k8s, …) are self-contained
195
+ * principals: the token string itself carries the private key, so the server
196
+ * never holds it. The server stores only the SHA-256 hash of the full token
197
+ * (for authentication) and the public key (for wrapping DEK grants).
198
+ *
199
+ * Format: `skt_<token id>_<private key pkcs8, base64url>`
200
+ */
201
+ const TOKEN_PREFIX = "skt";
202
+ const TOKEN_ID_LENGTH = 22;
203
+ const ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
200
204
  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));
205
+ let out = "";
206
+ while (out.length < TOKEN_ID_LENGTH) {
207
+ const bytes = crypto.getRandomValues(new Uint8Array(TOKEN_ID_LENGTH - out.length));
208
+ for (const byte of bytes) {
209
+ if (byte < 248) out += ID_ALPHABET[byte % 62];
210
+ if (out.length === TOKEN_ID_LENGTH) break;
211
+ }
212
+ }
213
+ return `${TOKEN_PREFIX}_${out}`;
214
+ }
215
+ async function hashToken(token) {
216
+ const digest = await crypto.subtle.digest("SHA-256", utf8Encode(token));
217
+ return toBase64Url(new Uint8Array(digest));
214
218
  }
215
219
  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
- }
220
+ const { publicKeyJwk, privateKeyJwk } = await generateKeyPair();
221
+ const pkcs8 = await exportPrivateKeyPkcs8(await importPrivateKey(privateKeyJwk));
222
+ const tokenId = randomTokenId();
223
+ const token = `${tokenId}_${toBase64Url(pkcs8)}`;
224
+ return {
225
+ token,
226
+ tokenId,
227
+ tokenHash: await hashToken(token),
228
+ publicKeyJwk
229
+ };
230
+ }
231
+ async function parseServiceToken(token) {
232
+ const match = /^(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)$/.exec(token);
233
+ if (!match) throw new SeekritCryptoError("MALFORMED_TOKEN", "not a valid seekrit service token");
234
+ const [, tokenId, keyB64] = match;
235
+ try {
236
+ return {
237
+ tokenId,
238
+ privateKey: await importPrivateKeyPkcs8(fromBase64Url(keyB64))
239
+ };
240
+ } catch {
241
+ throw new SeekritCryptoError("MALFORMED_TOKEN", "service token private key is corrupted");
242
+ }
235
243
  }
236
244
  function isServiceToken(value) {
237
- return value.startsWith(`${TOKEN_PREFIX}_`);
245
+ return value.startsWith(`${TOKEN_PREFIX}_`);
238
246
  }
239
-
240
- // ../../packages/crypto/src/wrap.ts
241
- var WRAP_PREFIX = "wd1";
242
- var HKDF_INFO = "seekrit/wrap-dek/v1";
247
+ //#endregion
248
+ //#region ../../packages/crypto/src/wrap.ts
249
+ /**
250
+ * ECIES-style key wrapping: an ephemeral P-256 keypair performs ECDH against
251
+ * the recipient's public key; the shared secret is run through HKDF-SHA256 to
252
+ * derive a one-time AES-256-GCM wrapping key. Only the holder of the
253
+ * recipient private key can unwrap.
254
+ *
255
+ * Blob format: `wd1.<ephemeral pub (raw)>.<hkdf salt>.<iv>.<ciphertext>`
256
+ */
257
+ const WRAP_PREFIX = "wd1";
258
+ const HKDF_INFO = "seekrit/wrap-dek/v1";
243
259
  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
- );
260
+ const ecdh = {
261
+ name: "ECDH",
262
+ public: peerPublicKey
263
+ };
264
+ const sharedBits = await crypto.subtle.deriveBits(ecdh, ownPrivateKey, 256);
265
+ const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
266
+ return crypto.subtle.deriveKey({
267
+ name: "HKDF",
268
+ hash: "SHA-256",
269
+ salt,
270
+ info: utf8Encode(HKDF_INFO)
271
+ }, hkdfKey, {
272
+ name: "AES-GCM",
273
+ length: 256
274
+ }, false, [usage]);
259
275
  }
276
+ /** Wrap an environment DEK to a principal's public key. */
260
277
  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(".");
278
+ const recipientKey = await importPublicKey(recipientPublicKeyJwk);
279
+ const ephemeral = await crypto.subtle.generateKey({
280
+ name: "ECDH",
281
+ namedCurve: "P-256"
282
+ }, true, ["deriveBits"]);
283
+ const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
284
+ const wrappingKey = await deriveWrappingKey(ephemeral.privateKey, recipientKey, salt, "encrypt");
285
+ const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
286
+ const ciphertext = await crypto.subtle.encrypt({
287
+ name: "AES-GCM",
288
+ iv
289
+ }, wrappingKey, dek);
290
+ const ephemeralRaw = new Uint8Array(await crypto.subtle.exportKey("raw", ephemeral.publicKey));
291
+ return [
292
+ WRAP_PREFIX,
293
+ toBase64Url(ephemeralRaw),
294
+ toBase64Url(salt),
295
+ toBase64Url(iv),
296
+ toBase64Url(new Uint8Array(ciphertext))
297
+ ].join(".");
283
298
  }
299
+ /** Unwrap an environment DEK with the principal's private ECDH key. */
284
300
  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";
301
+ const [ephB64, saltB64, ivB64, ctB64] = splitBlob(wrapped, WRAP_PREFIX, 4);
302
+ const wrappingKey = await deriveWrappingKey(privateKey, await crypto.subtle.importKey("raw", fromBase64Url(ephB64), {
303
+ name: "ECDH",
304
+ namedCurve: "P-256"
305
+ }, false, []), fromBase64Url(saltB64), "decrypt");
306
+ try {
307
+ const dek = await crypto.subtle.decrypt({
308
+ name: "AES-GCM",
309
+ iv: fromBase64Url(ivB64)
310
+ }, wrappingKey, fromBase64Url(ctB64));
311
+ return new Uint8Array(dek);
312
+ } catch {
313
+ throw new SeekritCryptoError("DECRYPT_FAILED", "DEK unwrap failed: wrong private key or tampered grant");
314
+ }
315
+ }
316
+ //#endregion
317
+ //#region package.json
318
+ var version = "0.2.0";
319
+ const PROJECT_FILE = "seekrit.json";
323
320
  function globalConfigPath() {
324
- return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
321
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "seekrit", "config.json");
325
322
  }
326
323
  function readGlobalConfig() {
327
- const path = globalConfigPath();
328
- if (!existsSync(path)) return {};
329
- return JSON.parse(readFileSync(path, "utf8"));
324
+ const path = globalConfigPath();
325
+ if (!existsSync(path)) return {};
326
+ return JSON.parse(readFileSync(path, "utf8"));
330
327
  }
331
328
  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 });
329
+ const path = globalConfigPath();
330
+ const merged = {
331
+ ...readGlobalConfig(),
332
+ ...update
333
+ };
334
+ mkdirSync(dirname(path), { recursive: true });
335
+ writeFileSync(path, `${JSON.stringify(merged, null, 2)}\n`, { mode: 384 });
337
336
  }
337
+ /** Walk up from cwd looking for seekrit.json. */
338
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
- }
339
+ let dir = startDir;
340
+ const { root } = parse(dir);
341
+ while (true) {
342
+ const candidate = join(dir, PROJECT_FILE);
343
+ if (existsSync(candidate)) return JSON.parse(readFileSync(candidate, "utf8"));
344
+ if (dir === root) return null;
345
+ dir = dirname(dir);
346
+ }
349
347
  }
350
348
  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;
349
+ const path = join(dir, PROJECT_FILE);
350
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
351
+ return path;
355
352
  }
356
-
357
- // ../../packages/api-client/src/index.ts
353
+ //#endregion
354
+ //#region ../../packages/api-client/src/index.ts
358
355
  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
- }
356
+ status;
357
+ code;
358
+ constructor(status, code, message) {
359
+ super(message);
360
+ this.name = "SeekritApiError";
361
+ this.status = status;
362
+ this.code = code;
363
+ }
367
364
  };
368
365
  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
- }
366
+ baseUrl;
367
+ auth;
368
+ fetchImpl;
369
+ constructor(options) {
370
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
371
+ this.auth = options.auth;
372
+ this.fetchImpl = options.fetch ?? ((...args) => fetch(...args));
373
+ }
374
+ async request(method, path, body) {
375
+ const headers = { accept: "application/json" };
376
+ if (this.auth.type === "bearer") headers.authorization = `Bearer ${this.auth.token}`;
377
+ else if (this.auth.type === "dynamic") {
378
+ const token = await this.auth.getToken();
379
+ if (!token) throw new SeekritApiError(401, "unauthorized", "session expired");
380
+ headers.authorization = `Bearer ${token}`;
381
+ } else headers["x-seekrit-dev-user"] = this.auth.email;
382
+ if (body !== void 0) headers["content-type"] = "application/json";
383
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
384
+ method,
385
+ headers,
386
+ body: body === void 0 ? void 0 : JSON.stringify(body)
387
+ });
388
+ if (!res.ok) {
389
+ const fallback = { error: {
390
+ code: "internal",
391
+ message: `HTTP ${res.status}`
392
+ } };
393
+ const payload = await res.json().catch(() => fallback);
394
+ throw new SeekritApiError(res.status, payload.error?.code ?? "internal", payload.error?.message ?? `HTTP ${res.status}`);
395
+ }
396
+ return await res.json();
397
+ }
398
+ me() {
399
+ return this.request("GET", "/v1/me");
400
+ }
401
+ getMyKeys() {
402
+ return this.request("GET", "/v1/me/keys");
403
+ }
404
+ setMyKeys(input) {
405
+ return this.request("PUT", "/v1/me/keys", input);
406
+ }
407
+ listOrgs() {
408
+ return this.request("GET", "/v1/orgs");
409
+ }
410
+ createOrg(input) {
411
+ return this.request("POST", "/v1/orgs", input);
412
+ }
413
+ getOrg(orgId) {
414
+ return this.request("GET", `/v1/orgs/${orgId}`);
415
+ }
416
+ listMembers(orgId) {
417
+ return this.request("GET", `/v1/orgs/${orgId}/members`);
418
+ }
419
+ listApps(orgId) {
420
+ return this.request("GET", `/v1/orgs/${orgId}/apps`);
421
+ }
422
+ createApp(orgId, input) {
423
+ return this.request("POST", `/v1/orgs/${orgId}/apps`, input);
424
+ }
425
+ getApp(orgId, appId) {
426
+ return this.request("GET", `/v1/orgs/${orgId}/apps/${appId}`);
427
+ }
428
+ deleteApp(orgId, appId) {
429
+ return this.request("DELETE", `/v1/orgs/${orgId}/apps/${appId}`);
430
+ }
431
+ listEnvs(orgId, appId) {
432
+ return this.request("GET", `/v1/orgs/${orgId}/apps/${appId}/envs`);
433
+ }
434
+ createEnv(orgId, appId, input) {
435
+ return this.request("POST", `/v1/orgs/${orgId}/apps/${appId}/envs`, input);
436
+ }
437
+ getEnv(orgId, envId) {
438
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}`);
439
+ }
440
+ deleteEnv(orgId, envId) {
441
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}`);
442
+ }
443
+ listGroups(orgId) {
444
+ return this.request("GET", `/v1/orgs/${orgId}/groups`);
445
+ }
446
+ createGroup(orgId, input) {
447
+ return this.request("POST", `/v1/orgs/${orgId}/groups`, input);
448
+ }
449
+ getGroup(orgId, groupId) {
450
+ return this.request("GET", `/v1/orgs/${orgId}/groups/${groupId}`);
451
+ }
452
+ deleteGroup(orgId, groupId) {
453
+ return this.request("DELETE", `/v1/orgs/${orgId}/groups/${groupId}`);
454
+ }
455
+ listGroupEnvs(orgId, groupId) {
456
+ return this.request("GET", `/v1/orgs/${orgId}/groups/${groupId}/envs`);
457
+ }
458
+ createGroupEnv(orgId, groupId, input) {
459
+ return this.request("POST", `/v1/orgs/${orgId}/groups/${groupId}/envs`, input);
460
+ }
461
+ listEnvGroups(orgId, envId) {
462
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/groups`);
463
+ }
464
+ linkEnvGroup(orgId, envId, input) {
465
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/groups`, input);
466
+ }
467
+ unlinkEnvGroup(orgId, envId, groupId) {
468
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/groups/${groupId}`);
469
+ }
470
+ /** Ordered ciphertext layers + wrapped DEKs for the calling principal. */
471
+ resolve(query = {}) {
472
+ const params = new URLSearchParams();
473
+ if (query.env) params.set("env", query.env);
474
+ for (const [group, slug] of Object.entries(query.with ?? {})) params.append("with", `${group}:${slug}`);
475
+ const qs = params.size > 0 ? `?${params}` : "";
476
+ return this.request("GET", `/v1/resolve${qs}`);
477
+ }
478
+ listSecrets(orgId, envId) {
479
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/secrets`);
480
+ }
481
+ setSecret(orgId, envId, name, ciphertext) {
482
+ return this.request("PUT", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`, { ciphertext });
483
+ }
484
+ deleteSecret(orgId, envId, name) {
485
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/secrets/${name}`);
486
+ }
487
+ /** The calling principal's wrapped DEK for this environment. */
488
+ getMyEnvKey(orgId, envId) {
489
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key`);
490
+ }
491
+ listEnvKeys(orgId, envId) {
492
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/keys`);
493
+ }
494
+ grantEnvKey(orgId, envId, input) {
495
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/keys`, input);
496
+ }
497
+ revokeEnvKey(orgId, envId, grantId) {
498
+ return this.request("DELETE", `/v1/orgs/${orgId}/envs/${envId}/keys/${grantId}`);
499
+ }
500
+ listTokens(orgId) {
501
+ return this.request("GET", `/v1/orgs/${orgId}/tokens`);
502
+ }
503
+ createToken(orgId, input) {
504
+ return this.request("POST", `/v1/orgs/${orgId}/tokens`, input);
505
+ }
506
+ revokeToken(orgId, tokenId) {
507
+ return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}`);
508
+ }
509
+ listAudit(orgId, query = {}) {
510
+ const params = new URLSearchParams();
511
+ if (query.cursor) params.set("cursor", query.cursor);
512
+ if (query.limit) params.set("limit", String(query.limit));
513
+ if (query.action) params.set("action", query.action);
514
+ if (query.resourceType) params.set("resourceType", query.resourceType);
515
+ const qs = params.size > 0 ? `?${params}` : "";
516
+ return this.request("GET", `/v1/orgs/${orgId}/audit${qs}`);
517
+ }
498
518
  };
499
-
500
- // src/io.ts
501
- import { createInterface } from "readline";
502
- import { Writable } from "stream";
519
+ //#endregion
520
+ //#region src/io.ts
503
521
  function fail(message) {
504
- console.error(`error: ${message}`);
505
- process.exit(1);
522
+ console.error(`error: ${message}`);
523
+ process.exit(1);
506
524
  }
525
+ /** Prompt without echoing input (for passphrases). */
507
526
  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
- });
527
+ const muted = new Writable({ write(_chunk, _encoding, callback) {
528
+ callback();
529
+ } });
530
+ process.stderr.write(question);
531
+ const rl = createInterface({
532
+ input: process.stdin,
533
+ output: muted,
534
+ terminal: true
535
+ });
536
+ return new Promise((resolve) => {
537
+ rl.question("", (answer) => {
538
+ rl.close();
539
+ process.stderr.write("\n");
540
+ resolve(answer);
541
+ });
542
+ });
522
543
  }
544
+ /** Read all of stdin (for `seekrit secrets set NAME -` piping). */
523
545
  async function readStdin() {
524
- const chunks = [];
525
- for await (const chunk of process.stdin) chunks.push(chunk);
526
- return Buffer.concat(chunks).toString("utf8");
546
+ const chunks = [];
547
+ for await (const chunk of process.stdin) chunks.push(chunk);
548
+ return Buffer.concat(chunks).toString("utf8");
549
+ }
550
+ //#endregion
551
+ //#region src/context.ts
552
+ /**
553
+ * Build the client context from configured credentials, or return null when
554
+ * none are set. `seekrit run` uses this to degrade to a plain launcher instead
555
+ * of exiting; every other command goes through `buildContext`, which fails.
556
+ */
557
+ function tryBuildContext() {
558
+ const config = readGlobalConfig();
559
+ const apiUrl = process.env.SEEKRIT_API_URL ?? config.apiUrl ?? "http://localhost:8787";
560
+ const token = process.env.SEEKRIT_TOKEN ?? config.token;
561
+ const devUser = process.env.SEEKRIT_DEV_USER ?? config.devUser;
562
+ let auth;
563
+ if (token) auth = {
564
+ type: "bearer",
565
+ token
566
+ };
567
+ else if (devUser) auth = {
568
+ type: "dev",
569
+ email: devUser
570
+ };
571
+ else return null;
572
+ return {
573
+ client: new SeekritClient({
574
+ baseUrl: apiUrl,
575
+ auth
576
+ }),
577
+ auth
578
+ };
527
579
  }
528
-
529
- // src/context.ts
530
580
  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;
581
+ const ctx = tryBuildContext();
582
+ if (!ctx) fail("no credentials found run `seekrit login --token skt_…`, or set SEEKRIT_TOKEN / SEEKRIT_DEV_USER");
583
+ return ctx;
551
584
  }
585
+ function isTokenAuth(ctx) {
586
+ return ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token);
587
+ }
588
+ /**
589
+ * Recover the calling principal's private key:
590
+ * - service tokens carry their private key in the token string;
591
+ * - users fetch their passphrase-encrypted key from the API and unlock it.
592
+ */
593
+ async function getPrivateKey(ctx) {
594
+ if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
595
+ const { privateKey } = await parseServiceToken(ctx.auth.token);
596
+ return privateKey;
597
+ }
598
+ const { encryptedPrivateKey } = await ctx.client.getMyKeys();
599
+ return importPrivateKey(await decryptPrivateKey(process.env.SEEKRIT_PASSPHRASE ?? await promptHidden("Passphrase: "), encryptedPrivateKey));
600
+ }
601
+ /** Recover one environment's DEK for the current principal. */
552
602
  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
603
+ const [{ wrappedDek }, privateKey] = await Promise.all([ctx.client.getMyEnvKey(orgId, envId), getPrivateKey(ctx)]);
604
+ return unwrapDek(wrappedDek, privateKey);
605
+ }
606
+ //#endregion
607
+ //#region src/format.ts
565
608
  function needsQuoting(value) {
566
- return /[\s"'`$\\#]/.test(value) || value === "";
609
+ return /[\s"'`$\\#]/.test(value) || value === "";
567
610
  }
568
611
  function dotenvQuote(value) {
569
- if (!needsQuoting(value)) return value;
570
- return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n")}"`;
612
+ if (!needsQuoting(value)) return value;
613
+ return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n")}"`;
571
614
  }
572
615
  function shellQuote(value) {
573
- return `'${value.replaceAll("'", `'\\''`)}'`;
616
+ return `'${value.replaceAll("'", `'\\''`)}'`;
574
617
  }
575
618
  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
619
+ const names = Object.keys(values).sort();
620
+ switch (format) {
621
+ case "json": return JSON.stringify(values, names, 2);
622
+ case "shell": return names.map((name) => `export ${name}=${shellQuote(values[name] ?? "")}`).join("\n");
623
+ case "dotenv": return names.map((name) => `${name}=${dotenvQuote(values[name] ?? "")}`).join("\n");
624
+ }
625
+ }
626
+ //#endregion
627
+ //#region src/dotenv.ts
628
+ /**
629
+ * Minimal `.env` parser: `KEY=VALUE`, `#` comments, an optional `export`
630
+ * prefix, and single/double-quoted values (double quotes honor `\n \t \r \" \\`
631
+ * escapes; unquoted values drop trailing ` # comments`). Multiline values are
632
+ * not supported — keep those in seekrit itself.
633
+ */
634
+ function parseDotenv(content) {
635
+ const out = {};
636
+ for (const raw of content.split(/\r?\n/)) {
637
+ let line = raw.trim();
638
+ if (!line || line.startsWith("#")) continue;
639
+ if (line.startsWith("export ")) line = line.slice(7).trimStart();
640
+ const eq = line.indexOf("=");
641
+ if (eq === -1) continue;
642
+ const key = line.slice(0, eq).trim();
643
+ if (!key) continue;
644
+ let value = line.slice(eq + 1).trim();
645
+ const quote = value[0];
646
+ if (value.length >= 2 && (quote === "\"" || quote === "'") && value.at(-1) === quote) {
647
+ value = value.slice(1, -1);
648
+ if (quote === "\"") value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
649
+ } else {
650
+ const comment = value.indexOf(" #");
651
+ if (comment !== -1) value = value.slice(0, comment).trim();
652
+ }
653
+ out[key] = value;
654
+ }
655
+ return out;
656
+ }
657
+ //#endregion
658
+ //#region src/secrets.ts
659
+ /** Fetch + decrypt every secret in a single environment. */
588
660
  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);
661
+ const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
662
+ const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
663
+ return Object.fromEntries(entries);
602
664
  }
603
665
  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");
666
+ const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
667
+ await ctx.client.setSecret(orgId, envId, name, ciphertext);
668
+ }
669
+ /**
670
+ * Resolve the full, layered environment for a running app: composed group
671
+ * secrets (lowest precedence) → the app env's own secrets → `.env` files.
672
+ * Each layer's DEK is unwrapped once with the principal's private key and its
673
+ * ciphertext decrypted locally. `process.env` is NOT applied here — callers
674
+ * that spawn a process layer it on top so the live shell always wins.
675
+ */
676
+ async function materializeEnv(ctx, opts) {
677
+ const query = {};
678
+ if (opts.with && Object.keys(opts.with).length > 0) query.with = opts.with;
679
+ if (!isTokenAuth(ctx)) {
680
+ if (!opts.envId) fail("specify --app and --env, or authenticate with a service token (SEEKRIT_TOKEN)");
681
+ query.env = opts.envId;
682
+ }
683
+ const { scope, layers } = await ctx.client.resolve(query);
684
+ const privateKey = await getPrivateKey(ctx);
685
+ const values = {};
686
+ const provenance = {};
687
+ for (const layer of layers) {
688
+ const dek = await unwrapDek(layer.wrappedDek, privateKey);
689
+ const label = layer.source === "group" ? `group:${layer.groupSlug}@${layer.slug}` : `app:${scope.appSlug}/${layer.slug}`;
690
+ for (const secret of layer.secrets) {
691
+ values[secret.name] = await decryptSecret(dek, secret.ciphertext, secretAad(layer.environmentId, secret.name));
692
+ provenance[secret.name] = label;
693
+ }
694
+ }
695
+ return {
696
+ values,
697
+ provenance,
698
+ scope,
699
+ loadedEnvFiles: overlayEnvFiles(values, provenance, opts.envFiles)
700
+ };
701
+ }
702
+ /**
703
+ * Overlay `.env` files onto an existing value/provenance set (later files win).
704
+ * Missing files are skipped. Returns the files that were actually loaded. Used
705
+ * both by {@link materializeEnv} and by `seekrit run`'s degraded path, where
706
+ * managed secrets are unavailable but `.env` should still apply.
707
+ */
708
+ function overlayEnvFiles(values, provenance, envFiles) {
709
+ const loaded = [];
710
+ for (const file of envFiles) {
711
+ if (!existsSync(file)) continue;
712
+ loaded.push(file);
713
+ for (const [name, value] of Object.entries(parseDotenv(readFileSync(file, "utf8")))) {
714
+ values[name] = value;
715
+ provenance[name] = `dotenv:${file}`;
716
+ }
717
+ }
718
+ return loaded;
719
+ }
720
+ /** Print a name → source table to stderr (never the secret values). */
721
+ function printExplain(provenance) {
722
+ const names = Object.keys(provenance).sort();
723
+ const width = names.reduce((w, n) => Math.max(w, n.length), 0);
724
+ for (const name of names) process.stderr.write(`${name.padEnd(width)} ${provenance[name]}\n`);
725
+ }
726
+ //#endregion
727
+ //#region src/target.ts
728
+ /** Resolve the target org from a flag, the committed config, or a lone org. */
729
+ async function resolveOrg(ctx, orgSlug) {
730
+ const wanted = orgSlug ?? findProjectConfig()?.org;
731
+ const { orgs } = await ctx.client.listOrgs();
732
+ if (wanted) {
733
+ const org = orgs.find((o) => o.slug === wanted || o.id === wanted);
734
+ if (!org) fail(`no accessible org "${wanted}"`);
735
+ return {
736
+ id: org.id,
737
+ slug: org.slug
738
+ };
739
+ }
740
+ const only = orgs[0];
741
+ if (orgs.length === 1 && only) return {
742
+ id: only.id,
743
+ slug: only.slug
744
+ };
745
+ fail("specify --org (or run `seekrit init`)");
746
+ }
747
+ /**
748
+ * Resolve an environment to operate on — an application env (`--app --env`,
749
+ * or the config's app + `--env`) or a group env (`--group --env`).
750
+ */
751
+ async function resolveEnvTarget(ctx, opts) {
752
+ const org = await resolveOrg(ctx, opts.org);
753
+ if (!opts.env) fail("specify --env");
754
+ if (opts.group) {
755
+ const { groups } = await ctx.client.listGroups(org.id);
756
+ const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
757
+ if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
758
+ const { environments } = await ctx.client.listGroupEnvs(org.id, group.id);
759
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
760
+ if (!env) fail(`no environment "${opts.env}" in group ${group.slug}`);
761
+ return {
762
+ orgId: org.id,
763
+ envId: env.id,
764
+ label: `${group.slug}@${env.slug}`
765
+ };
766
+ }
767
+ const appSlug = opts.app ?? findProjectConfig()?.app;
768
+ if (!appSlug) fail("specify --app or --group (or run `seekrit init`)");
769
+ const { apps } = await ctx.client.listApps(org.id);
770
+ const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
771
+ if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
772
+ const { environments } = await ctx.client.listEnvs(org.id, app.id);
773
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
774
+ if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
775
+ return {
776
+ orgId: org.id,
777
+ envId: env.id,
778
+ label: `${app.slug}/${env.slug}`
779
+ };
780
+ }
781
+ /** Resolve an application environment, keeping ids + slugs (for token binding). */
782
+ async function resolveAppEnv(ctx, opts) {
783
+ const org = await resolveOrg(ctx, opts.org);
784
+ const appSlug = opts.app ?? findProjectConfig()?.app;
785
+ if (!appSlug) fail("specify --app (or run `seekrit init`)");
786
+ if (!opts.env) fail("specify --env");
787
+ const { apps } = await ctx.client.listApps(org.id);
788
+ const app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
789
+ if (!app) fail(`no app "${appSlug}" in ${org.slug}`);
790
+ const { environments } = await ctx.client.listEnvs(org.id, app.id);
791
+ const env = environments.find((e) => e.slug === opts.env || e.id === opts.env);
792
+ if (!env) fail(`no environment "${opts.env}" in ${app.slug}`);
793
+ return {
794
+ orgId: org.id,
795
+ appId: app.id,
796
+ appSlug: app.slug,
797
+ envId: env.id,
798
+ envSlug: env.slug
799
+ };
800
+ }
801
+ /** Resolve a group by slug within the target org. */
802
+ async function resolveGroup(ctx, opts) {
803
+ const org = await resolveOrg(ctx, opts.org);
804
+ if (!opts.group) fail("specify --group");
805
+ const { groups } = await ctx.client.listGroups(org.id);
806
+ const group = groups.find((g) => g.slug === opts.group || g.id === opts.group);
807
+ if (!group) fail(`no group "${opts.group}" in ${org.slug}`);
808
+ return {
809
+ orgId: org.id,
810
+ id: group.id,
811
+ slug: group.slug
812
+ };
813
+ }
814
+ //#endregion
815
+ //#region src/index.ts
816
+ /** Collect repeated `--with group=env` flags into a map. */
817
+ function collectKv(value, acc = {}) {
818
+ const eq = value.indexOf("=");
819
+ if (eq === -1) fail(`expected group=env, got "${value}"`);
820
+ acc[value.slice(0, eq)] = value.slice(eq + 1);
821
+ return acc;
822
+ }
823
+ /** Collect repeated flags (e.g. --env-file) into a list. */
824
+ function collectList(value, acc = []) {
825
+ acc.push(value);
826
+ return acc;
827
+ }
828
+ const program = new Command("seekrit").description("End-to-end encrypted secrets manager").version(version).enablePositionalOptions();
829
+ program.command("login").description("store credentials for the API").option("--token <token>", "service token (skt_…)").option("--dev-user <email>", "dev-mode identity (local API with AUTH_MODE=dev)").option("--api-url <url>", "API base URL").action((options) => {
830
+ if (options.token && !isServiceToken(options.token)) fail("token must start with skt_");
831
+ writeGlobalConfig({
832
+ ...options.token ? { token: options.token } : {},
833
+ ...options.devUser ? { devUser: options.devUser } : {},
834
+ ...options.apiUrl ? { apiUrl: options.apiUrl } : {}
835
+ });
836
+ console.error("credentials saved");
626
837
  });
627
838
  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})`);
839
+ const ctx = buildContext();
840
+ if (isTokenAuth(ctx) && ctx.auth.type === "bearer") {
841
+ const { tokenId } = await parseServiceToken(ctx.auth.token);
842
+ try {
843
+ const { scope } = await ctx.client.resolve();
844
+ console.log(`service token ${tokenId} ${scope.orgSlug}/${scope.appSlug}/${scope.envSlug}`);
845
+ } catch {
846
+ const { orgs } = await ctx.client.listOrgs();
847
+ console.log(`service token ${tokenId} (org: ${orgs[0]?.slug ?? "unknown"})`);
848
+ }
849
+ return;
850
+ }
851
+ const { user, orgs } = await ctx.client.me();
852
+ console.log(`${user.email}${user.hasKeys ? "" : " (key setup pending — run `seekrit keys setup`)"}`);
853
+ for (const org of orgs) console.log(` ${org.slug} (${org.role})`);
854
+ });
855
+ program.command("keys").description("manage your encryption keys").command("setup").description("generate your keypair and protect it with a passphrase").action(async () => {
856
+ const ctx = buildContext();
857
+ const passphrase = process.env.SEEKRIT_PASSPHRASE ?? await promptHidden("New passphrase: ");
858
+ if (!process.env.SEEKRIT_PASSPHRASE) {
859
+ if (await promptHidden("Confirm passphrase: ") !== passphrase) fail("passphrases do not match");
860
+ }
861
+ if (passphrase.length < 8) fail("passphrase must be at least 8 characters");
862
+ const pair = await generateKeyPair();
863
+ const encryptedPrivateKey = await encryptPrivateKey(passphrase, pair.privateKeyJwk);
864
+ await ctx.client.setMyKeys({
865
+ publicKeyJwk: pair.publicKeyJwk,
866
+ encryptedPrivateKey
867
+ });
868
+ console.error("keys generated and uploaded — your passphrase never leaves this machine");
642
869
  });
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
- );
870
+ program.command("init").description("link this directory to an org/app (environment comes from the token)").requiredOption("--org <slug>", "organization slug").requiredOption("--app <slug>", "application slug").action(async (options) => {
871
+ const ctx = buildContext();
872
+ const org = await resolveOrg(ctx, options.org);
873
+ const { apps } = await ctx.client.listApps(org.id);
874
+ const app = apps.find((a) => a.slug === options.app || a.id === options.app);
875
+ if (!app) fail(`no app "${options.app}" in ${org.slug}`);
876
+ const path = writeProjectConfig({
877
+ org: org.slug,
878
+ app: app.slug
879
+ });
880
+ console.error(`linked ${org.slug}/${app.slug} ${path} (environment is selected by the service token at runtime)`);
664
881
  });
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}`);
882
+ program.command("org").description("manage organizations").command("create").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
883
+ const created = await buildContext().client.createOrg({
884
+ name: options.name,
885
+ slug: options.slug
886
+ });
887
+ console.error(`created org ${created.org.slug} (${created.org.id})`);
691
888
  });
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})`);
889
+ program.command("app").description("manage applications").command("create").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
890
+ const ctx = buildContext();
891
+ const orgRef = await resolveOrg(ctx, options.org);
892
+ const created = await ctx.client.createApp(orgRef.id, {
893
+ name: options.name,
894
+ slug: options.slug
895
+ });
896
+ console.error(`created app ${created.app.slug} (${created.app.id})`);
700
897
  });
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})`);
898
+ const env = program.command("env").description("manage environments");
899
+ env.command("create").description("create an application environment (generates its data key locally)").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
900
+ const ctx = buildContext();
901
+ const orgRef = await resolveOrg(ctx, options.org);
902
+ const { apps } = await ctx.client.listApps(orgRef.id);
903
+ const appRow = apps.find((a) => a.slug === options.app || a.id === options.app);
904
+ if (!appRow) fail(`no app "${options.app}" in ${orgRef.slug}`);
905
+ const { user } = await ctx.client.me();
906
+ if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
907
+ const wrappedDek = await wrapDek(generateDek(), user.publicKeyJwk);
908
+ const created = await ctx.client.createEnv(orgRef.id, appRow.id, {
909
+ name: options.name,
910
+ slug: options.slug,
911
+ wrappedDek
912
+ });
913
+ console.error(`created environment ${created.environment.slug} (${created.environment.id})`);
714
914
  });
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}`);
915
+ const envGroups = env.command("groups").description("compose shared groups into an application environment");
916
+ envGroups.command("add").description("compose a group into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").requiredOption("--group <slug>").option("--position <n>", "precedence among groups (higher wins)").action(async (options) => {
917
+ const ctx = buildContext();
918
+ const target = await resolveAppEnv(ctx, options);
919
+ const group = await resolveGroup(ctx, {
920
+ org: options.org,
921
+ group: options.group
922
+ });
923
+ await ctx.client.linkEnvGroup(target.orgId, target.envId, {
924
+ groupId: group.id,
925
+ position: options.position === void 0 ? void 0 : Number.parseInt(options.position, 10)
926
+ });
927
+ console.error(`composed ${group.slug} into ${target.appSlug}/${target.envSlug}`);
753
928
  });
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");
929
+ envGroups.command("list").description("list groups composed into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").action(async (options) => {
930
+ const ctx = buildContext();
931
+ const target = await resolveAppEnv(ctx, options);
932
+ const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
933
+ for (const g of groups) console.log(`${g.position}\t${g.slug}\t${g.name}`);
766
934
  });
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`);
935
+ envGroups.command("rm").description("remove a group from an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").requiredOption("--group <slug>").action(async (options) => {
936
+ const ctx = buildContext();
937
+ const target = await resolveAppEnv(ctx, options);
938
+ const group = await resolveGroup(ctx, {
939
+ org: options.org,
940
+ group: options.group
941
+ });
942
+ await ctx.client.unlinkEnvGroup(target.orgId, target.envId, group.id);
943
+ console.error(`removed ${group.slug} from ${target.appSlug}/${target.envSlug}`);
781
944
  });
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`);
945
+ const group = program.command("group").description("manage shared groups (reusable secret bags)");
946
+ group.command("create").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
947
+ const ctx = buildContext();
948
+ const orgRef = await resolveOrg(ctx, options.org);
949
+ const created = await ctx.client.createGroup(orgRef.id, {
950
+ name: options.name,
951
+ slug: options.slug
952
+ });
953
+ console.error(`created group ${created.group.slug} (${created.group.id})`);
787
954
  });
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}`));
955
+ group.command("env").description("manage a group’s environments (per-slug value sets / variants)").command("create").description("create a group environment (generates its data key locally)").option("--org <slug>").requiredOption("--group <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
956
+ const ctx = buildContext();
957
+ const groupRef = await resolveGroup(ctx, {
958
+ org: options.org,
959
+ group: options.group
960
+ });
961
+ const { user } = await ctx.client.me();
962
+ if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
963
+ const wrappedDek = await wrapDek(generateDek(), user.publicKeyJwk);
964
+ const created = await ctx.client.createGroupEnv(groupRef.orgId, groupRef.id, {
965
+ name: options.name,
966
+ slug: options.slug,
967
+ wrappedDek
968
+ });
969
+ console.error(`created ${groupRef.slug}@${created.environment.slug} (${created.environment.id})`);
970
+ });
971
+ /** Attach the environment-selection flags shared by every `secrets` command. */
972
+ function withTarget(cmd) {
973
+ return cmd.option("--org <slug>", "organization slug").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "target a group environment instead of an app").requiredOption("--env <slug>", "environment slug");
974
+ }
975
+ const secrets = program.command("secrets").description("manage secrets in an application or group environment");
976
+ withTarget(secrets.command("list").description("list secret names (no values)")).action(async (options) => {
977
+ const ctx = buildContext();
978
+ const { orgId, envId } = await resolveEnvTarget(ctx, options);
979
+ const { secrets: rows } = await ctx.client.listSecrets(orgId, envId);
980
+ for (const row of rows) console.log(`${row.name}\tv${row.version}\t${row.updatedAt}`);
981
+ });
982
+ withTarget(secrets.command("get <name>").description("decrypt and print one secret value")).action(async (name, options) => {
983
+ const ctx = buildContext();
984
+ const { orgId, envId } = await resolveEnvTarget(ctx, options);
985
+ const value = (await fetchDecryptedSecrets(ctx, orgId, envId))[name];
986
+ if (value === void 0) fail(`no secret named ${name}`);
987
+ process.stdout.write(value);
988
+ if (process.stdout.isTTY) process.stdout.write("\n");
989
+ });
990
+ withTarget(secrets.command("set <name> [value]").description("encrypt and store a secret (reads stdin when value is omitted or '-')")).action(async (name, value, options) => {
991
+ const ctx = buildContext();
992
+ const { orgId, envId } = await resolveEnvTarget(ctx, options);
993
+ await encryptAndSetSecret(ctx, orgId, envId, name, value === void 0 || value === "-" ? (await readStdin()).replace(/\n$/, "") : value);
994
+ console.error(`${name} saved`);
995
+ });
996
+ withTarget(secrets.command("rm <name>").description("delete a secret")).action(async (name, options) => {
997
+ const ctx = buildContext();
998
+ const { orgId, envId } = await resolveEnvTarget(ctx, options);
999
+ await ctx.client.deleteSecret(orgId, envId, name);
1000
+ console.error(`${name} deleted`);
1001
+ });
1002
+ /** Resolve the layered environment for the current principal. */
1003
+ async function materialize(ctx, options) {
1004
+ let envId;
1005
+ if (!isTokenAuth(ctx)) envId = (await resolveAppEnv(ctx, options)).envId;
1006
+ return materializeEnv(ctx, {
1007
+ envId,
1008
+ with: options.with,
1009
+ envFiles: options.envFile ?? [".env"]
1010
+ });
1011
+ }
1012
+ /**
1013
+ * Best-effort variant of {@link materialize} for `seekrit run`: if credentials
1014
+ * are missing or seekrit can't be reached (network, auth, or decryption
1015
+ * failure), log why and fall back to the `.env` overlay alone so the command
1016
+ * still runs. This keeps `seekrit run` working across environments that lack a
1017
+ * token or API — mirroring the `seekrit-run` launcher.
1018
+ */
1019
+ async function materializeForRun(options) {
1020
+ const envFiles = options.envFile ?? [".env"];
1021
+ try {
1022
+ const ctx = tryBuildContext();
1023
+ if (!ctx) throw new Error("no credentials found (set SEEKRIT_TOKEN / SEEKRIT_DEV_USER or run `seekrit login`)");
1024
+ return await materialize(ctx, options);
1025
+ } catch (err) {
1026
+ const message = err instanceof Error ? err.message : String(err);
1027
+ console.error(`seekrit: continuing without seekrit-managed secrets: ${message}`);
1028
+ const values = {};
1029
+ const provenance = {};
1030
+ overlayEnvFiles(values, provenance, envFiles);
1031
+ return {
1032
+ values,
1033
+ provenance
1034
+ };
1035
+ }
1036
+ }
1037
+ program.command("run").description("run a command with decrypted secrets injected (process env > .env > app > group)").passThroughOptions().option("--org <slug>").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--with <group=env>", "override one group’s slice for this run", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts, options) => {
1038
+ const [cmd, ...args] = commandParts;
1039
+ if (!cmd) fail("no command given");
1040
+ const { values, provenance } = await materializeForRun(options);
1041
+ if (options.explain) {
1042
+ for (const name of Object.keys(values)) if (process.env[name] !== void 0 && process.env[name] !== values[name]) provenance[name] = "env";
1043
+ printExplain(provenance);
1044
+ }
1045
+ const child = spawn(cmd, args, {
1046
+ stdio: "inherit",
1047
+ env: {
1048
+ ...values,
1049
+ ...process.env
1050
+ }
1051
+ });
1052
+ child.on("exit", (code, signal) => {
1053
+ if (signal) process.kill(process.pid, signal);
1054
+ process.exit(code ?? 1);
1055
+ });
1056
+ child.on("error", (err) => fail(`failed to start ${cmd}: ${err.message}`));
809
1057
  });
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));
1058
+ program.command("export").description("print decrypted secrets (dotenv, json, or shell)").option("--org <slug>").option("--app <slug>", "application slug (token path infers this)").option("--env <slug>", "environment slug (token path infers this)").option("--with <group=env>", "override one group’s slice", collectKv).option("--env-file <path>", "a .env file to overlay (repeatable; default .env)", collectList).option("--explain", "print where each variable resolved from (to stderr)").option("--format <format>", "dotenv | json | shell", "dotenv").action(async (options) => {
1059
+ if (![
1060
+ "dotenv",
1061
+ "json",
1062
+ "shell"
1063
+ ].includes(options.format)) fail("format must be dotenv, json, or shell");
1064
+ const { values, provenance } = await materialize(buildContext(), options);
1065
+ if (options.explain) printExplain(provenance);
1066
+ console.log(formatSecrets(values, options.format));
822
1067
  });
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
- );
1068
+ program.command("grant").description("give a member or service token access to an environment's key").option("--org <slug>").option("--app <slug>").option("--group <slug>", "grant a group environment instead of an app").requiredOption("--env <slug>").option("--user <email>", "grant to an org member by email").option("--token <tokenId>", "grant to a service token by id (skt_…)").action(async (options) => {
1069
+ if (!options.user === !options.token) fail("pass exactly one of --user or --token");
1070
+ const ctx = buildContext();
1071
+ const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
1072
+ const dek = await getDek(ctx, orgId, envId);
1073
+ let principalType;
1074
+ let principalId;
1075
+ let publicKeyJwk;
1076
+ if (options.user) {
1077
+ const { members } = await ctx.client.listMembers(orgId);
1078
+ const member = members.find((m) => m.email === options.user);
1079
+ if (!member) fail(`no member ${options.user}`);
1080
+ if (!member.publicKeyJwk) fail(`${options.user} has not completed key setup`);
1081
+ [principalType, principalId, publicKeyJwk] = [
1082
+ "user",
1083
+ member.userId,
1084
+ member.publicKeyJwk
1085
+ ];
1086
+ } else {
1087
+ const { tokens } = await ctx.client.listTokens(orgId);
1088
+ const token = tokens.find((t) => t.id === options.token);
1089
+ if (!token) fail(`no service token ${options.token}`);
1090
+ [principalType, principalId, publicKeyJwk] = [
1091
+ "service_token",
1092
+ token.id,
1093
+ token.publicKeyJwk
1094
+ ];
1095
+ }
1096
+ const wrappedDek = await wrapDek(dek, publicKeyJwk);
1097
+ await ctx.client.grantEnvKey(orgId, envId, {
1098
+ principalType,
1099
+ principalId,
1100
+ wrappedDek
1101
+ });
1102
+ console.error(`granted ${label} access to ${principalId}`);
864
1103
  });
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);
1104
+ const token = program.command("token").description("manage service tokens (CI, docker, agents)");
1105
+ token.command("create").description("create a service token bound to an app environment; prints it once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--org <slug>").requiredOption("--app <slug>", "application to bind the token to").requiredOption("--env <slug>", "environment to bind the token to").option("--allow <group=env>", "also grant an alternate group slice (for `run --with`)", collectKv).option("--no-grant", "skip auto-granting the env + composed group keys").action(async (options) => {
1106
+ const ctx = buildContext();
1107
+ const target = await resolveAppEnv(ctx, options);
1108
+ const created = await createServiceToken();
1109
+ await ctx.client.createToken(target.orgId, {
1110
+ name: options.name,
1111
+ tokenId: created.tokenId,
1112
+ tokenHash: created.tokenHash,
1113
+ publicKeyJwk: created.publicKeyJwk,
1114
+ environmentId: target.envId
1115
+ });
1116
+ const grantEnv = async (envId) => {
1117
+ const dek = await getDek(ctx, target.orgId, envId);
1118
+ await ctx.client.grantEnvKey(target.orgId, envId, {
1119
+ principalType: "service_token",
1120
+ principalId: created.tokenId,
1121
+ wrappedDek: await wrapDek(dek, created.publicKeyJwk)
1122
+ });
1123
+ };
1124
+ if (options.grant !== false) {
1125
+ await grantEnv(target.envId);
1126
+ const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
1127
+ for (const g of groups) {
1128
+ const { environments } = await ctx.client.listGroupEnvs(target.orgId, g.groupId);
1129
+ const slice = environments.find((e) => e.slug === target.envSlug);
1130
+ if (!slice) fail(`group "${g.slug}" has no "${target.envSlug}" environment — create it before granting`);
1131
+ await grantEnv(slice.id);
1132
+ }
1133
+ for (const [groupSlug, envSlug] of Object.entries(options.allow ?? {})) {
1134
+ const groupRef = await resolveGroup(ctx, {
1135
+ org: options.org,
1136
+ group: groupSlug
1137
+ });
1138
+ const { environments } = await ctx.client.listGroupEnvs(target.orgId, groupRef.id);
1139
+ const slice = environments.find((e) => e.slug === envSlug);
1140
+ if (!slice) fail(`group "${groupSlug}" has no "${envSlug}" environment`);
1141
+ await grantEnv(slice.id);
1142
+ }
1143
+ }
1144
+ console.error(`token created${options.grant !== false ? " and granted" : ""} for ${target.appSlug}/${target.envSlug} — save it now, it is not stored:`);
1145
+ console.log(created.token);
889
1146
  });
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
- }
1147
+ token.command("list").description("list service tokens").option("--org <slug>").action(async (options) => {
1148
+ const ctx = buildContext();
1149
+ const orgRef = await resolveOrg(ctx, options.org);
1150
+ const { tokens } = await ctx.client.listTokens(orgRef.id);
1151
+ for (const t of tokens) {
1152
+ const status = t.revokedAt ? "revoked" : t.expiresAt && Date.parse(t.expiresAt) < Date.now() ? "expired" : "active";
1153
+ console.log(`${t.id}\t${t.name}\t${status}\tlast used: ${t.lastUsedAt ?? "never"}`);
1154
+ }
900
1155
  });
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`);
1156
+ token.command("revoke <tokenId>").description("revoke a service token").option("--org <slug>").action(async (tokenId, options) => {
1157
+ const ctx = buildContext();
1158
+ const orgRef = await resolveOrg(ctx, options.org);
1159
+ await ctx.client.revokeToken(orgRef.id, tokenId);
1160
+ console.error(`${tokenId} revoked`);
906
1161
  });
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
- }
1162
+ program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
1163
+ const ctx = buildContext();
1164
+ const orgRef = await resolveOrg(ctx, options.org);
1165
+ const { entries } = await ctx.client.listAudit(orgRef.id, { limit: Number.parseInt(options.limit, 10) || 50 });
1166
+ for (const entry of entries) console.log(`${entry.createdAt}\t${entry.action}\t${entry.actorType}:${entry.actorId}\t${entry.resourceType}${entry.resourceId ? `:${entry.resourceId}` : ""}`);
918
1167
  });
919
1168
  program.parseAsync().catch((err) => {
920
- fail(err instanceof Error ? err.message : String(err));
1169
+ fail(err instanceof Error ? err.message : String(err));
921
1170
  });
1171
+ //#endregion
1172
+ export {};