@seekrit/cli 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/index.js +217 -20
- package/dist/{mcp-t-sJ_SvE.js → mcp-COWsshZZ.js} +4 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ Run `seekrit <command> --help` for full flags. The
|
|
|
58
58
|
| `init --org --app --env` | Link this directory to an environment (`seekrit.json`). |
|
|
59
59
|
| `org create` / `app create` / `env create` | Create organizations, apps, and environments. |
|
|
60
60
|
| `secrets list` | List secret names (never values). |
|
|
61
|
-
| `secrets get <name
|
|
61
|
+
| `secrets get <name> [--raw]` | Decrypt and print one secret value (`--raw` skips `${OTHER_SECRET}` expansion). |
|
|
62
62
|
| `secrets set <name> [value]` | Encrypt and store a secret (reads stdin if value is omitted or `-`). |
|
|
63
63
|
| `secrets rm <name>` | Delete a secret. |
|
|
64
64
|
| `run <command…>` | Run a command with decrypted secrets in its environment. |
|
|
@@ -71,6 +71,11 @@ Run `seekrit <command> --help` for full flags. The
|
|
|
71
71
|
(e.g. `seekrit run -- node app.js --port 3000`). `secrets set NAME -` reads the
|
|
72
72
|
value from stdin, so you can pipe: `printf '%s' "$VALUE" | seekrit secrets set NAME -`.
|
|
73
73
|
|
|
74
|
+
A value may reference another secret as `${OTHER_SECRET}`: stored literally,
|
|
75
|
+
expanded on read against the merged environment (`--no-interpolate` on
|
|
76
|
+
`run`/`export` opts out). See the
|
|
77
|
+
[references guide](https://seekrit.dev/docs/guides/references).
|
|
78
|
+
|
|
74
79
|
## Service tokens (CI, Docker, agents)
|
|
75
80
|
|
|
76
81
|
A service token carries its own private key in the token string; the server
|
package/dist/index.js
CHANGED
|
@@ -143,6 +143,152 @@ const SUBSCRIPTION_STATUSES = [
|
|
|
143
143
|
"canceled",
|
|
144
144
|
"paused"
|
|
145
145
|
];
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region ../../packages/core/src/interpolate.ts
|
|
148
|
+
/**
|
|
149
|
+
* Secret references: `${OTHER_SECRET}` inside a secret value.
|
|
150
|
+
*
|
|
151
|
+
* This is the **canonical specification** of the expansion. It is pure string
|
|
152
|
+
* work over an already-decrypted variable set, so it runs wherever plaintext
|
|
153
|
+
* legitimately exists — the CLI, the browser, the language SDKs, and the Rust
|
|
154
|
+
* clients (`crates/seekrit-core/src/interpolate.rs` mirrors it, pinned by the
|
|
155
|
+
* shared golden fixture in `apps/run/testdata/vectors.json`).
|
|
156
|
+
*
|
|
157
|
+
* Expansion happens at **read time**, on the client, never on write and never
|
|
158
|
+
* on the server: the API only ever holds the ciphertext of the literal
|
|
159
|
+
* `${OTHER_SECRET}` text, so referencing costs nothing against the
|
|
160
|
+
* zero-knowledge invariant. It also means a reference stays live — rotating
|
|
161
|
+
* `DB_PASSWORD` updates every value that references it, with no re-encryption.
|
|
162
|
+
*
|
|
163
|
+
* The rules, in full:
|
|
164
|
+
*
|
|
165
|
+
* - `${NAME}` is replaced with the value of `NAME` in the *same fully-merged
|
|
166
|
+
* set* — after group → app-env → `.env` layering, so a reference always sees
|
|
167
|
+
* the value that layer precedence actually selected.
|
|
168
|
+
* - `NAME` must be a valid secret name (`[A-Za-z_][A-Za-z0-9_]*`, matching
|
|
169
|
+
* `secretNameSchema`). Anything else — `${1}`, `${FOO:-bar}`, `${a.b}` — is
|
|
170
|
+
* left exactly as written, so shell and CI template syntax passes through
|
|
171
|
+
* untouched.
|
|
172
|
+
* - A reference to a name that is not in the set is **left literal** and
|
|
173
|
+
* reported in {@link InterpolationResult.unresolved}. Erroring would mean a
|
|
174
|
+
* stored value that happens to contain `${GITHUB_SHA}` could break a whole
|
|
175
|
+
* environment's resolve; leaving it alone is the safe default, and the report
|
|
176
|
+
* is there to surface typos (`seekrit run --explain` prints it).
|
|
177
|
+
* - Expansion is recursive: a referenced value may itself contain references.
|
|
178
|
+
* - `$${NAME}` is an escape producing the literal text `${NAME}`. A `$$` not
|
|
179
|
+
* followed by `{` is ordinary text (passwords full of `$` are safe).
|
|
180
|
+
* - A reference **cycle** throws {@link InterpolationError}. Unlike an unknown
|
|
181
|
+
* name, a cycle can only be a configuration mistake — every name in it
|
|
182
|
+
* exists — and there is no value that could be correct to emit.
|
|
183
|
+
*
|
|
184
|
+
* `process.env` is deliberately *not* a reference source: `seekrit run` layers
|
|
185
|
+
* the live shell on top of the resolved set afterwards, and letting a stored
|
|
186
|
+
* secret pull in arbitrary host environment variables would be a surprising
|
|
187
|
+
* (and machine-dependent) way to change a secret's value.
|
|
188
|
+
*/
|
|
189
|
+
/** A reference name — the same grammar as `secretNameSchema`. */
|
|
190
|
+
const REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
191
|
+
/**
|
|
192
|
+
* Cap on a single expanded value. Nested references can multiply length
|
|
193
|
+
* (`A=${B}${B}`, `B=${C}${C}`, …), which memoization makes fast but does not
|
|
194
|
+
* make small. A megabyte is far above any real secret and far below anything
|
|
195
|
+
* that would exhaust a container.
|
|
196
|
+
*/
|
|
197
|
+
const MAX_EXPANDED_LENGTH = 1048576;
|
|
198
|
+
/**
|
|
199
|
+
* Split a value into literal runs and references — the single tokenizer every
|
|
200
|
+
* rule above is expressed in terms of, so expansion and inspection can never
|
|
201
|
+
* disagree about what counts as a reference.
|
|
202
|
+
*/
|
|
203
|
+
function* scan(text) {
|
|
204
|
+
let i = 0;
|
|
205
|
+
while (i < text.length) {
|
|
206
|
+
const dollar = text.indexOf("$", i);
|
|
207
|
+
if (dollar === -1) {
|
|
208
|
+
yield { literal: text.slice(i) };
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (dollar > i) yield { literal: text.slice(i, dollar) };
|
|
212
|
+
if (text[dollar + 1] === "$" && text[dollar + 2] === "{") {
|
|
213
|
+
yield { literal: "${" };
|
|
214
|
+
i = dollar + 3;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const close = text[dollar + 1] === "{" ? text.indexOf("}", dollar + 2) : -1;
|
|
218
|
+
const reference = close === -1 ? null : text.slice(dollar + 2, close);
|
|
219
|
+
if (reference !== null && REFERENCE_NAME.test(reference)) {
|
|
220
|
+
yield {
|
|
221
|
+
reference,
|
|
222
|
+
raw: text.slice(dollar, close + 1)
|
|
223
|
+
};
|
|
224
|
+
i = close + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
yield { literal: "$" };
|
|
228
|
+
i = dollar + 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
var InterpolationError = class extends Error {
|
|
232
|
+
code;
|
|
233
|
+
/**
|
|
234
|
+
* For `cycle`, the reference chain that closed on itself, starting and ending
|
|
235
|
+
* on the same name (`["A", "B", "A"]`). For `too_large`, the single name
|
|
236
|
+
* whose expansion blew the cap.
|
|
237
|
+
*/
|
|
238
|
+
chain;
|
|
239
|
+
constructor(code, message, chain) {
|
|
240
|
+
super(message);
|
|
241
|
+
this.name = "InterpolationError";
|
|
242
|
+
this.code = code;
|
|
243
|
+
this.chain = chain;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Expand `${NAME}` references throughout a decrypted variable set.
|
|
248
|
+
*
|
|
249
|
+
* Pure: the input is never mutated. Throws {@link InterpolationError} on a
|
|
250
|
+
* reference cycle (see the module comment for the complete rule set).
|
|
251
|
+
*/
|
|
252
|
+
function interpolateSecrets(values) {
|
|
253
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
254
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
255
|
+
const expanded = [];
|
|
256
|
+
/** The chain currently being expanded — the cycle detector. */
|
|
257
|
+
const stack = [];
|
|
258
|
+
/** Expand one present name's value, memoized so each is expanded once. */
|
|
259
|
+
function resolve(name) {
|
|
260
|
+
const cached = resolved.get(name);
|
|
261
|
+
if (cached !== void 0) return cached;
|
|
262
|
+
const cycleAt = stack.indexOf(name);
|
|
263
|
+
if (cycleAt !== -1) {
|
|
264
|
+
const chain = [...stack.slice(cycleAt), name];
|
|
265
|
+
throw new InterpolationError("cycle", `secret reference cycle: ${chain.join(" → ")}`, chain);
|
|
266
|
+
}
|
|
267
|
+
stack.push(name);
|
|
268
|
+
let out = "";
|
|
269
|
+
for (const segment of scan(values[name])) if ("literal" in segment) out += segment.literal;
|
|
270
|
+
else if (Object.hasOwn(values, segment.reference)) out += resolve(segment.reference);
|
|
271
|
+
else {
|
|
272
|
+
unresolved.add(segment.reference);
|
|
273
|
+
out += segment.raw;
|
|
274
|
+
}
|
|
275
|
+
stack.pop();
|
|
276
|
+
if (out.length > MAX_EXPANDED_LENGTH) throw new InterpolationError("too_large", `${name} expands to more than ${MAX_EXPANDED_LENGTH} bytes — check its references`, [name]);
|
|
277
|
+
resolved.set(name, out);
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
const result = {};
|
|
281
|
+
for (const name of Object.keys(values)) {
|
|
282
|
+
const value = resolve(name);
|
|
283
|
+
result[name] = value;
|
|
284
|
+
if (value !== values[name]) expanded.push(name);
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
values: result,
|
|
288
|
+
expanded,
|
|
289
|
+
unresolved: [...unresolved].sort()
|
|
290
|
+
};
|
|
291
|
+
}
|
|
146
292
|
z.enum([
|
|
147
293
|
"postgres",
|
|
148
294
|
"mysql",
|
|
@@ -1978,7 +2124,7 @@ function isServiceToken(value) {
|
|
|
1978
2124
|
}
|
|
1979
2125
|
//#endregion
|
|
1980
2126
|
//#region package.json
|
|
1981
|
-
var version = "0.
|
|
2127
|
+
var version = "0.25.0";
|
|
1982
2128
|
//#endregion
|
|
1983
2129
|
//#region ../../packages/api-client/src/index.ts
|
|
1984
2130
|
var SeekritApiError = class extends Error {
|
|
@@ -4095,11 +4241,41 @@ function collect$1(value, acc) {
|
|
|
4095
4241
|
acc.push(value);
|
|
4096
4242
|
return acc;
|
|
4097
4243
|
}
|
|
4098
|
-
/**
|
|
4099
|
-
|
|
4244
|
+
/**
|
|
4245
|
+
* Fetch + decrypt every secret in a single environment.
|
|
4246
|
+
*
|
|
4247
|
+
* `${OTHER_SECRET}` references are expanded (see `@seekrit/core`'s
|
|
4248
|
+
* `interpolate`) unless `raw` is set. Only this environment's own secrets are in
|
|
4249
|
+
* scope here — a reference to a secret inherited from a composed group is left
|
|
4250
|
+
* literal, because the group layers aren't fetched. `materializeEnv` is the
|
|
4251
|
+
* fully-layered view.
|
|
4252
|
+
*/
|
|
4253
|
+
async function fetchDecryptedSecrets(ctx, orgId, envId, opts = {}) {
|
|
4100
4254
|
const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
|
|
4101
4255
|
const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
|
|
4102
|
-
return Object.fromEntries(entries);
|
|
4256
|
+
return interpolateValues(Object.fromEntries(entries), !opts.raw).values;
|
|
4257
|
+
}
|
|
4258
|
+
/**
|
|
4259
|
+
* Expand `${OTHER_SECRET}` references in a merged variable set. A cycle becomes
|
|
4260
|
+
* the CLI's standard fatal exit — it is a config bug with no correct value to
|
|
4261
|
+
* emit. Pass `enabled: false` (`--no-interpolate`) to hand the set back as-is.
|
|
4262
|
+
*/
|
|
4263
|
+
function interpolateValues(values, enabled = true) {
|
|
4264
|
+
if (!enabled) return {
|
|
4265
|
+
values,
|
|
4266
|
+
interpolated: [],
|
|
4267
|
+
unresolvedRefs: []
|
|
4268
|
+
};
|
|
4269
|
+
try {
|
|
4270
|
+
const { values: expandedValues, expanded, unresolved } = interpolateSecrets(values);
|
|
4271
|
+
return {
|
|
4272
|
+
values: expandedValues,
|
|
4273
|
+
interpolated: expanded,
|
|
4274
|
+
unresolvedRefs: unresolved
|
|
4275
|
+
};
|
|
4276
|
+
} catch (err) {
|
|
4277
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
4278
|
+
}
|
|
4103
4279
|
}
|
|
4104
4280
|
/**
|
|
4105
4281
|
* Decrypt one historical version of a secret. Ciphertext is bound to
|
|
@@ -4143,6 +4319,9 @@ async function importSecrets(ctx, orgId, envId, entries) {
|
|
|
4143
4319
|
* Each layer's DEK is unwrapped once with the principal's private key and its
|
|
4144
4320
|
* ciphertext decrypted locally. `process.env` is NOT applied here — callers
|
|
4145
4321
|
* that spawn a process layer it on top so the live shell always wins.
|
|
4322
|
+
*
|
|
4323
|
+
* `${OTHER_SECRET}` references are expanded last, against the merged set, so a
|
|
4324
|
+
* reference always resolves to whichever layer won the name.
|
|
4146
4325
|
*/
|
|
4147
4326
|
async function materializeEnv(ctx, opts) {
|
|
4148
4327
|
const query = {};
|
|
@@ -4163,11 +4342,12 @@ async function materializeEnv(ctx, opts) {
|
|
|
4163
4342
|
provenance[secret.name] = label;
|
|
4164
4343
|
}
|
|
4165
4344
|
}
|
|
4345
|
+
const loadedEnvFiles = overlayEnvFiles(values, provenance, opts.envFiles);
|
|
4166
4346
|
return {
|
|
4167
|
-
values,
|
|
4347
|
+
...interpolateValues(values, opts.interpolate !== false),
|
|
4168
4348
|
provenance,
|
|
4169
4349
|
scope,
|
|
4170
|
-
loadedEnvFiles
|
|
4350
|
+
loadedEnvFiles
|
|
4171
4351
|
};
|
|
4172
4352
|
}
|
|
4173
4353
|
/**
|
|
@@ -4188,11 +4368,21 @@ function overlayEnvFiles(values, provenance, envFiles) {
|
|
|
4188
4368
|
}
|
|
4189
4369
|
return loaded;
|
|
4190
4370
|
}
|
|
4191
|
-
/**
|
|
4192
|
-
|
|
4371
|
+
/**
|
|
4372
|
+
* Print a name → source table to stderr (never the secret values). Names whose
|
|
4373
|
+
* value had references expanded are marked, and dangling references are called
|
|
4374
|
+
* out afterwards — a typo'd `${NAME}` is otherwise invisible, since it is
|
|
4375
|
+
* deliberately passed through as literal text.
|
|
4376
|
+
*/
|
|
4377
|
+
function printExplain(provenance, refs = {}) {
|
|
4378
|
+
const interpolated = new Set(refs.interpolated ?? []);
|
|
4193
4379
|
const names = Object.keys(provenance).sort();
|
|
4194
4380
|
const width = names.reduce((w, n) => Math.max(w, n.length), 0);
|
|
4195
|
-
for (const name of names)
|
|
4381
|
+
for (const name of names) {
|
|
4382
|
+
const marker = interpolated.has(name) ? " (interpolated)" : "";
|
|
4383
|
+
process.stderr.write(`${name.padEnd(width)} ${provenance[name]}${marker}\n`);
|
|
4384
|
+
}
|
|
4385
|
+
if (refs.unresolved?.length) process.stderr.write(`\nunresolved reference(s), left as literal text: ${refs.unresolved.join(", ")}\n`);
|
|
4196
4386
|
}
|
|
4197
4387
|
//#endregion
|
|
4198
4388
|
//#region src/ssh.ts
|
|
@@ -4521,11 +4711,11 @@ withTarget(secrets.command("list").description("list secret names (no values)"))
|
|
|
4521
4711
|
const { secrets: rows } = await ctx.client.listSecrets(orgId, envId);
|
|
4522
4712
|
for (const row of rows) console.log(`${row.name}\tv${row.version}\t${row.updatedAt}`);
|
|
4523
4713
|
});
|
|
4524
|
-
withTarget(secrets.command("get <name>").description("decrypt and print one secret value").option("--version <n>", "print an earlier version instead of the current one")).action(async (name, options) => {
|
|
4714
|
+
withTarget(secrets.command("get <name>").description("decrypt and print one secret value").option("--raw", "print the stored value without expanding ${OTHER_SECRET} references").option("--version <n>", "print an earlier version instead of the current one")).action(async (name, options) => {
|
|
4525
4715
|
const ctx = buildContext();
|
|
4526
4716
|
const { orgId, envId } = await resolveEnvTarget(ctx, options);
|
|
4527
4717
|
let value;
|
|
4528
|
-
if (options.version === void 0) value = (await fetchDecryptedSecrets(ctx, orgId, envId))[name];
|
|
4718
|
+
if (options.version === void 0) value = (await fetchDecryptedSecrets(ctx, orgId, envId, { raw: options.raw }))[name];
|
|
4529
4719
|
else value = await fetchDecryptedVersion(ctx, orgId, envId, name, parseVersion(options.version));
|
|
4530
4720
|
if (value === void 0) fail(`no secret named ${name}`);
|
|
4531
4721
|
process.stdout.write(value);
|
|
@@ -4591,7 +4781,8 @@ async function materialize(ctx, options) {
|
|
|
4591
4781
|
return materializeEnv(ctx, {
|
|
4592
4782
|
envId,
|
|
4593
4783
|
with: options.with,
|
|
4594
|
-
envFiles: options.envFile ?? [".env"]
|
|
4784
|
+
envFiles: options.envFile ?? [".env"],
|
|
4785
|
+
interpolate: options.interpolate
|
|
4595
4786
|
});
|
|
4596
4787
|
}
|
|
4597
4788
|
/**
|
|
@@ -4617,7 +4808,7 @@ async function materializeForRun(options) {
|
|
|
4617
4808
|
const provenance = {};
|
|
4618
4809
|
overlayEnvFiles(values, provenance, envFiles);
|
|
4619
4810
|
return {
|
|
4620
|
-
values,
|
|
4811
|
+
...interpolateValues(values, options.interpolate !== false),
|
|
4621
4812
|
provenance
|
|
4622
4813
|
};
|
|
4623
4814
|
}
|
|
@@ -4732,13 +4923,16 @@ async function reapStragglers(pids, signal) {
|
|
|
4732
4923
|
process.kill(pid, "SIGKILL");
|
|
4733
4924
|
} catch {}
|
|
4734
4925
|
}
|
|
4735
|
-
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) => {
|
|
4926
|
+
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)").option("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts, options) => {
|
|
4736
4927
|
const [cmd, ...args] = commandParts;
|
|
4737
4928
|
if (!cmd) fail("no command given");
|
|
4738
|
-
const { values, provenance } = await materializeForRun(options);
|
|
4929
|
+
const { values, provenance, interpolated, unresolvedRefs } = await materializeForRun(options);
|
|
4739
4930
|
if (options.explain) {
|
|
4740
4931
|
for (const name of Object.keys(values)) if (process.env[name] !== void 0 && process.env[name] !== values[name]) provenance[name] = "env";
|
|
4741
|
-
printExplain(provenance
|
|
4932
|
+
printExplain(provenance, {
|
|
4933
|
+
interpolated,
|
|
4934
|
+
unresolved: unresolvedRefs
|
|
4935
|
+
});
|
|
4742
4936
|
}
|
|
4743
4937
|
const posix = process.platform !== "win32";
|
|
4744
4938
|
const child = spawn(cmd, args, {
|
|
@@ -4781,14 +4975,17 @@ program.command("run").description("run a command with decrypted secrets injecte
|
|
|
4781
4975
|
});
|
|
4782
4976
|
child.on("error", (err) => fail(`failed to start ${cmd}: ${err.message}`));
|
|
4783
4977
|
});
|
|
4784
|
-
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) => {
|
|
4978
|
+
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("--no-interpolate", "leave ${OTHER_SECRET} references as literal text").option("--format <format>", "dotenv | json | shell", "dotenv").action(async (options) => {
|
|
4785
4979
|
if (![
|
|
4786
4980
|
"dotenv",
|
|
4787
4981
|
"json",
|
|
4788
4982
|
"shell"
|
|
4789
4983
|
].includes(options.format)) fail("format must be dotenv, json, or shell");
|
|
4790
|
-
const { values, provenance } = await materialize(buildContext(), options);
|
|
4791
|
-
if (options.explain) printExplain(provenance
|
|
4984
|
+
const { values, provenance, interpolated, unresolvedRefs } = await materialize(buildContext(), options);
|
|
4985
|
+
if (options.explain) printExplain(provenance, {
|
|
4986
|
+
interpolated,
|
|
4987
|
+
unresolved: unresolvedRefs
|
|
4988
|
+
});
|
|
4792
4989
|
console.log(formatSecrets(values, options.format));
|
|
4793
4990
|
});
|
|
4794
4991
|
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) => {
|
|
@@ -4909,7 +5106,7 @@ registerMongoCommands(program);
|
|
|
4909
5106
|
registerKmsCommands(program);
|
|
4910
5107
|
registerRecoveryCommands(program);
|
|
4911
5108
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
4912
|
-
const { runMcpServer } = await import("./mcp-
|
|
5109
|
+
const { runMcpServer } = await import("./mcp-COWsshZZ.js");
|
|
4913
5110
|
await runMcpServer();
|
|
4914
5111
|
});
|
|
4915
5112
|
program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
|
|
@@ -573,7 +573,7 @@ async function runMcpServer(options = {}) {
|
|
|
573
573
|
await ctx.client.unlinkEnvGroup(target.orgId, target.envId, g.id);
|
|
574
574
|
return { ok: true };
|
|
575
575
|
});
|
|
576
|
-
tool("set_secret", "Encrypt a value locally and store it in an environment.", {
|
|
576
|
+
tool("set_secret", "Encrypt a value locally and store it in an environment. A value may reference another secret as ${OTHER_SECRET}: the reference is stored literally and expanded whenever the secret is read, so it tracks the referenced value. Write $${OTHER_SECRET} for a literal.", {
|
|
577
577
|
...targetShape,
|
|
578
578
|
name: z.string(),
|
|
579
579
|
value: z.string()
|
|
@@ -587,10 +587,11 @@ async function runMcpServer(options = {}) {
|
|
|
587
587
|
name: o.name
|
|
588
588
|
};
|
|
589
589
|
});
|
|
590
|
-
tool("get_secret", "Return one secret. By default only reports presence + version; pass reveal:true to decrypt the plaintext into this response (avoid unless the value is actually needed — prefer run_command). Pass `version` to read an earlier version instead of the current one.", {
|
|
590
|
+
tool("get_secret", "Return one secret. By default only reports presence + version; pass reveal:true to decrypt the plaintext into this response (avoid unless the value is actually needed — prefer run_command). A revealed current value has its ${OTHER_SECRET} references expanded against this environment's own secrets; pass raw:true for the stored text instead. Pass `version` to read an earlier version instead of the current one (always as stored, never expanded).", {
|
|
591
591
|
...targetShape,
|
|
592
592
|
name: z.string(),
|
|
593
593
|
reveal: z.boolean().optional(),
|
|
594
|
+
raw: z.boolean().optional().describe("skip ${OTHER_SECRET} expansion (with reveal)"),
|
|
594
595
|
version: z.number().int().positive().optional().describe("an earlier version from list_secret_versions (default: current)")
|
|
595
596
|
}, async (o) => {
|
|
596
597
|
const ctx = getCtx();
|
|
@@ -615,7 +616,7 @@ async function runMcpServer(options = {}) {
|
|
|
615
616
|
revealed: true
|
|
616
617
|
};
|
|
617
618
|
}
|
|
618
|
-
const values = await fetchDecryptedSecrets(ctx, orgId, envId);
|
|
619
|
+
const values = await fetchDecryptedSecrets(ctx, orgId, envId, { raw: o.raw });
|
|
619
620
|
if (!(o.name in values)) throw new Error(`no secret named ${o.name}`);
|
|
620
621
|
return {
|
|
621
622
|
name: o.name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"@types/node": "^26.1.0",
|
|
28
28
|
"tsdown": "^0.22.3",
|
|
29
29
|
"@seekrit/api-client": "0.0.1",
|
|
30
|
-
"@seekrit/
|
|
31
|
-
"@seekrit/
|
|
30
|
+
"@seekrit/crypto": "0.0.1",
|
|
31
|
+
"@seekrit/core": "0.0.1"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsdown",
|