@seekrit/cli 0.30.0 → 0.31.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/dist/index.js +256 -9
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { homedir, hostname, tmpdir, userInfo } from "node:os";
|
|
7
7
|
import { dirname, join, parse } from "node:path";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { Writable } from "node:stream";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
/** All catalog keys as a runtime array (for iteration / zod enums). */
|
|
11
12
|
const ENTITLEMENT_KEYS = Object.keys({
|
|
12
13
|
"feature.kms": {
|
|
@@ -2719,7 +2720,7 @@ function isCliSessionToken(value) {
|
|
|
2719
2720
|
}
|
|
2720
2721
|
//#endregion
|
|
2721
2722
|
//#region package.json
|
|
2722
|
-
var version = "0.
|
|
2723
|
+
var version = "0.31.0";
|
|
2723
2724
|
//#endregion
|
|
2724
2725
|
//#region ../../packages/api-client/src/index.ts
|
|
2725
2726
|
var SeekritApiError = class extends Error {
|
|
@@ -3406,7 +3407,8 @@ function tryBuildContext(dotenvVars = {}) {
|
|
|
3406
3407
|
auth,
|
|
3407
3408
|
client: CLI_CLIENT
|
|
3408
3409
|
}),
|
|
3409
|
-
auth
|
|
3410
|
+
auth,
|
|
3411
|
+
apiUrl
|
|
3410
3412
|
};
|
|
3411
3413
|
}
|
|
3412
3414
|
function buildContext() {
|
|
@@ -4812,6 +4814,163 @@ function registerBranchCommands(program) {
|
|
|
4812
4814
|
console.error(`deleted branch ${app.slug}#${target.slug}`);
|
|
4813
4815
|
});
|
|
4814
4816
|
}
|
|
4817
|
+
/** Domain separators — must match `crates/seekrit-cache`. */
|
|
4818
|
+
const KEY_DOMAIN = "seekrit-lkg-cache-v1";
|
|
4819
|
+
const TOKEN_DOMAIN = "seekrit-token-fp-v1";
|
|
4820
|
+
/** 24h: long enough to ride out an outage, short enough to bound the tail. */
|
|
4821
|
+
const DEFAULT_MAX_AGE_MS = 1440 * 60 * 1e3;
|
|
4822
|
+
/**
|
|
4823
|
+
* Derive the key for one resolve request. Overrides are sorted, so flag order
|
|
4824
|
+
* never splits the cache. Byte-for-byte identical to `CacheKey::new` in
|
|
4825
|
+
* `crates/seekrit-cache`.
|
|
4826
|
+
*/
|
|
4827
|
+
function cacheKey(apiUrl, token, branch, overrides = {}) {
|
|
4828
|
+
const pairs = Object.entries(overrides).map(([group, env]) => `${group}:${env}`).sort();
|
|
4829
|
+
return {
|
|
4830
|
+
key: createHash("sha256").update([
|
|
4831
|
+
KEY_DOMAIN,
|
|
4832
|
+
apiUrl.replace(/\/+$/, ""),
|
|
4833
|
+
token,
|
|
4834
|
+
branch ?? "",
|
|
4835
|
+
pairs.join(",")
|
|
4836
|
+
].join("\n")).digest("hex"),
|
|
4837
|
+
tokenFingerprint: tokenFingerprint(token)
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
/** The hex SHA-256 identifying a token without storing it. */
|
|
4841
|
+
function tokenFingerprint(token) {
|
|
4842
|
+
return createHash("sha256").update(`${TOKEN_DOMAIN}\n${token}`).digest("hex");
|
|
4843
|
+
}
|
|
4844
|
+
/**
|
|
4845
|
+
* `$XDG_CACHE_HOME/seekrit`, else `~/.cache/seekrit` — beside the config
|
|
4846
|
+
* directory holding the credential, never a shared temp directory.
|
|
4847
|
+
*/
|
|
4848
|
+
function defaultCacheDir() {
|
|
4849
|
+
return join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "seekrit");
|
|
4850
|
+
}
|
|
4851
|
+
/** Render an age for a log line, rounded to its largest whole unit. */
|
|
4852
|
+
function humanize(ms) {
|
|
4853
|
+
const secs = Math.floor(ms / 1e3);
|
|
4854
|
+
if (secs < 60) return `${secs}s`;
|
|
4855
|
+
if (secs < 3600) return `${Math.floor(secs / 60)}m`;
|
|
4856
|
+
if (secs < 86400) return `${Math.floor(secs / 3600)}h`;
|
|
4857
|
+
return `${Math.floor(secs / 86400)}d`;
|
|
4858
|
+
}
|
|
4859
|
+
/**
|
|
4860
|
+
* Whether a failed resolve means the API was *unreachable* (the cache may stand
|
|
4861
|
+
* in) rather than *refusing us* (it must not). A refusal is an answer, and
|
|
4862
|
+
* revocation is supposed to take effect the moment it arrives.
|
|
4863
|
+
*/
|
|
4864
|
+
function mayFallBack(err) {
|
|
4865
|
+
if (err instanceof SeekritApiError) return err.status >= 500 || err.status === 429;
|
|
4866
|
+
return true;
|
|
4867
|
+
}
|
|
4868
|
+
/** An opened cache, bound to one resolve request. */
|
|
4869
|
+
var LkgCache = class {
|
|
4870
|
+
dir;
|
|
4871
|
+
maxAgeMs;
|
|
4872
|
+
id;
|
|
4873
|
+
constructor(id, options = {}) {
|
|
4874
|
+
this.id = id;
|
|
4875
|
+
this.dir = options.dir ?? defaultCacheDir();
|
|
4876
|
+
this.maxAgeMs = options.maxAgeMs ?? 864e5;
|
|
4877
|
+
}
|
|
4878
|
+
path() {
|
|
4879
|
+
return join(this.dir, `${this.id.key}.json`);
|
|
4880
|
+
}
|
|
4881
|
+
read() {
|
|
4882
|
+
const path = this.path();
|
|
4883
|
+
if (!existsSync(path)) return { kind: "missing" };
|
|
4884
|
+
let envelope;
|
|
4885
|
+
try {
|
|
4886
|
+
envelope = JSON.parse(readFileSync(path, "utf8"));
|
|
4887
|
+
} catch (err) {
|
|
4888
|
+
return {
|
|
4889
|
+
kind: "unusable",
|
|
4890
|
+
reason: `corrupt cache entry: ${message(err)}`
|
|
4891
|
+
};
|
|
4892
|
+
}
|
|
4893
|
+
if (envelope.version !== 1) return {
|
|
4894
|
+
kind: "unusable",
|
|
4895
|
+
reason: `cache entry is format v${envelope.version}, this build reads v1`
|
|
4896
|
+
};
|
|
4897
|
+
if (envelope.tokenFingerprint !== this.id.tokenFingerprint) return {
|
|
4898
|
+
kind: "unusable",
|
|
4899
|
+
reason: "cache entry belongs to a different token"
|
|
4900
|
+
};
|
|
4901
|
+
if (typeof envelope.body !== "string") return {
|
|
4902
|
+
kind: "unusable",
|
|
4903
|
+
reason: "cache entry has no body"
|
|
4904
|
+
};
|
|
4905
|
+
const ageMs = Math.max(0, Date.now() - envelope.fetchedAt * 1e3);
|
|
4906
|
+
if (ageMs >= this.maxAgeMs) return {
|
|
4907
|
+
kind: "expired",
|
|
4908
|
+
ageMs
|
|
4909
|
+
};
|
|
4910
|
+
return {
|
|
4911
|
+
kind: "hit",
|
|
4912
|
+
body: envelope.body,
|
|
4913
|
+
ageMs
|
|
4914
|
+
};
|
|
4915
|
+
}
|
|
4916
|
+
/**
|
|
4917
|
+
* Record a freshly-fetched response. Written to a temporary file and renamed,
|
|
4918
|
+
* so a concurrent reader sees either the old entry or the new one — never a
|
|
4919
|
+
* half-written file. Throws only on genuinely unexpected I/O; callers treat a
|
|
4920
|
+
* failed write as a warning, never as a failed resolve.
|
|
4921
|
+
*/
|
|
4922
|
+
write(body) {
|
|
4923
|
+
mkdirSync(this.dir, {
|
|
4924
|
+
recursive: true,
|
|
4925
|
+
mode: 448
|
|
4926
|
+
});
|
|
4927
|
+
const envelope = {
|
|
4928
|
+
version: 1,
|
|
4929
|
+
fetchedAt: Math.floor(Date.now() / 1e3),
|
|
4930
|
+
tokenFingerprint: this.id.tokenFingerprint,
|
|
4931
|
+
body
|
|
4932
|
+
};
|
|
4933
|
+
const tmp = join(this.dir, `${this.id.key}.${process.pid}.tmp`);
|
|
4934
|
+
try {
|
|
4935
|
+
writeFileSync(tmp, JSON.stringify(envelope), { mode: 384 });
|
|
4936
|
+
renameSync(tmp, this.path());
|
|
4937
|
+
} catch (err) {
|
|
4938
|
+
rmSync(tmp, { force: true });
|
|
4939
|
+
throw err;
|
|
4940
|
+
}
|
|
4941
|
+
this.pruneExpired();
|
|
4942
|
+
}
|
|
4943
|
+
/** Drop this entry — used when the API refuses the token. */
|
|
4944
|
+
invalidate() {
|
|
4945
|
+
rmSync(this.path(), { force: true });
|
|
4946
|
+
}
|
|
4947
|
+
/**
|
|
4948
|
+
* Delete entries past their usefulness (a rotated token or a changed
|
|
4949
|
+
* `--with` leaves one nothing will read again). Best-effort and silent, and
|
|
4950
|
+
* it only ever touches files this cache named: `<64 hex>.json`.
|
|
4951
|
+
*/
|
|
4952
|
+
pruneExpired() {
|
|
4953
|
+
let names;
|
|
4954
|
+
try {
|
|
4955
|
+
names = readdirSync(this.dir);
|
|
4956
|
+
} catch {
|
|
4957
|
+
return;
|
|
4958
|
+
}
|
|
4959
|
+
for (const name of names) {
|
|
4960
|
+
if (!/^[0-9a-f]{64}\.json$/.test(name)) continue;
|
|
4961
|
+
const path = join(this.dir, name);
|
|
4962
|
+
let expired = true;
|
|
4963
|
+
try {
|
|
4964
|
+
const envelope = JSON.parse(readFileSync(path, "utf8"));
|
|
4965
|
+
expired = Math.max(0, Date.now() - envelope.fetchedAt * 1e3) >= this.maxAgeMs;
|
|
4966
|
+
} catch {}
|
|
4967
|
+
if (expired) rmSync(path, { force: true });
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
};
|
|
4971
|
+
function message(err) {
|
|
4972
|
+
return err instanceof Error ? err.message : String(err);
|
|
4973
|
+
}
|
|
4815
4974
|
//#endregion
|
|
4816
4975
|
//#region src/format.ts
|
|
4817
4976
|
function shellQuote(value) {
|
|
@@ -6070,7 +6229,7 @@ async function materializeEnv(ctx, opts) {
|
|
|
6070
6229
|
if (!opts.envId) fail("specify --app and --env, or authenticate with a service token (SEEKRIT_TOKEN)");
|
|
6071
6230
|
query.env = opts.envId;
|
|
6072
6231
|
}
|
|
6073
|
-
const { scope, layers } = await ctx.
|
|
6232
|
+
const { scope, layers } = await resolveWithCache(ctx, query, opts.cache);
|
|
6074
6233
|
const privateKey = await getPrivateKey(ctx);
|
|
6075
6234
|
const values = {};
|
|
6076
6235
|
const provenance = {};
|
|
@@ -6094,6 +6253,46 @@ async function materializeEnv(ctx, opts) {
|
|
|
6094
6253
|
};
|
|
6095
6254
|
}
|
|
6096
6255
|
/**
|
|
6256
|
+
* Resolve, going through the last-known-good cache when one is configured.
|
|
6257
|
+
*
|
|
6258
|
+
* Always live first: the cache exists for when the call cannot land, not to
|
|
6259
|
+
* save a round trip, so a recovered network is picked up on the very next
|
|
6260
|
+
* invocation. A *refused* resolve (401/403/…) drops the entry rather than
|
|
6261
|
+
* falling back to it — otherwise revoking a token would keep working offline
|
|
6262
|
+
* until the entry aged out.
|
|
6263
|
+
*/
|
|
6264
|
+
async function resolveWithCache(ctx, query, cache) {
|
|
6265
|
+
if (!cache) return ctx.client.resolve(query);
|
|
6266
|
+
try {
|
|
6267
|
+
const response = await ctx.client.resolve(query);
|
|
6268
|
+
try {
|
|
6269
|
+
cache.write(JSON.stringify(response));
|
|
6270
|
+
} catch (err) {
|
|
6271
|
+
warn(`could not update the cache: ${errorMessage(err)}`);
|
|
6272
|
+
}
|
|
6273
|
+
return response;
|
|
6274
|
+
} catch (err) {
|
|
6275
|
+
if (!mayFallBack(err)) {
|
|
6276
|
+
cache.invalidate();
|
|
6277
|
+
throw err;
|
|
6278
|
+
}
|
|
6279
|
+
const found = cache.read();
|
|
6280
|
+
if (found.kind === "hit") {
|
|
6281
|
+
warn(`${errorMessage(err)} — using cached secrets fetched ${humanize(found.ageMs)} ago`);
|
|
6282
|
+
return JSON.parse(found.body);
|
|
6283
|
+
}
|
|
6284
|
+
if (found.kind === "expired") warn(`cached secrets are ${humanize(found.ageMs)} old, past --cache-max-age`);
|
|
6285
|
+
else if (found.kind === "unusable") warn(`ignoring the cached secrets: ${found.reason}`);
|
|
6286
|
+
throw err;
|
|
6287
|
+
}
|
|
6288
|
+
}
|
|
6289
|
+
function warn(text) {
|
|
6290
|
+
process.stderr.write(`seekrit: ${text}\n`);
|
|
6291
|
+
}
|
|
6292
|
+
function errorMessage(err) {
|
|
6293
|
+
return err instanceof Error ? err.message : String(err);
|
|
6294
|
+
}
|
|
6295
|
+
/**
|
|
6097
6296
|
* Overlay `.env` files onto an existing value/provenance set (later files win).
|
|
6098
6297
|
* Missing files are skipped. Returns the files that were actually loaded. Used
|
|
6099
6298
|
* both by {@link materializeEnv} and by `seekrit run`'s degraded path, where
|
|
@@ -6840,8 +7039,55 @@ withTarget(secrets.command("rm <name>").description("delete a secret")).action(a
|
|
|
6840
7039
|
await ctx.client.deleteSecret(orgId, envId, name);
|
|
6841
7040
|
console.error(`${name} deleted`);
|
|
6842
7041
|
});
|
|
7042
|
+
/**
|
|
7043
|
+
* Open the last-known-good cache for this request, or `undefined` when it is
|
|
7044
|
+
* off. `dotenvVars` carries `SEEKRIT_*` values read from a `.env` file, so the
|
|
7045
|
+
* flags follow the same `flag > env > .env` precedence as the credentials.
|
|
7046
|
+
*
|
|
7047
|
+
* **Service tokens only.** A user session's private key is fetched from the API
|
|
7048
|
+
* and unlocked with a passphrase, so caching the resolve response alone would
|
|
7049
|
+
* not make an offline run work — and the token is what the cache key is built
|
|
7050
|
+
* from. `--cache` under user auth is a no-op we say out loud rather than a
|
|
7051
|
+
* silent one.
|
|
7052
|
+
*/
|
|
7053
|
+
function openCache(ctx, options, dotenvVars = {}) {
|
|
7054
|
+
const lookup = (key) => process.env[key] ?? dotenvVars[key];
|
|
7055
|
+
const truthy = (value) => [
|
|
7056
|
+
"1",
|
|
7057
|
+
"true",
|
|
7058
|
+
"yes",
|
|
7059
|
+
"on"
|
|
7060
|
+
].includes((value ?? "").trim().toLowerCase());
|
|
7061
|
+
if (!(options.cache ?? truthy(lookup("SEEKRIT_CACHE")))) return void 0;
|
|
7062
|
+
if (!isTokenAuth(ctx)) {
|
|
7063
|
+
process.stderr.write("seekrit: --cache needs a service token (SEEKRIT_TOKEN); continuing without it\n");
|
|
7064
|
+
return;
|
|
7065
|
+
}
|
|
7066
|
+
const token = ctx.auth.type === "bearer" ? ctx.auth.token : "";
|
|
7067
|
+
const maxAgeRaw = options.cacheMaxAge ?? lookup("SEEKRIT_CACHE_MAX_AGE");
|
|
7068
|
+
const maxAgeMs = maxAgeRaw ? parseDurationMs(maxAgeRaw) : DEFAULT_MAX_AGE_MS;
|
|
7069
|
+
const branch = options.branch ?? process.env.SEEKRIT_BRANCH ?? dotenvVars.SEEKRIT_BRANCH;
|
|
7070
|
+
return new LkgCache(cacheKey(ctx.apiUrl, token, branch, options.with), {
|
|
7071
|
+
dir: options.cacheDir ?? lookup("SEEKRIT_CACHE_DIR"),
|
|
7072
|
+
maxAgeMs
|
|
7073
|
+
});
|
|
7074
|
+
}
|
|
7075
|
+
/** `30s` / `15m` / `24h` / `7d` / bare seconds → ms. Matches the Rust parser. */
|
|
7076
|
+
function parseDurationMs(raw) {
|
|
7077
|
+
const match = /^(\d+)\s*([smhd]?)$/.exec(raw.trim());
|
|
7078
|
+
if (!match) fail(`--cache-max-age: not a duration: "${raw}" (try 15m, 24h, 7d)`);
|
|
7079
|
+
const value = Number(match[1]);
|
|
7080
|
+
if (value === 0) fail("--cache-max-age: must be greater than zero");
|
|
7081
|
+
return value * ({
|
|
7082
|
+
"": 1,
|
|
7083
|
+
s: 1,
|
|
7084
|
+
m: 60,
|
|
7085
|
+
h: 3600,
|
|
7086
|
+
d: 86400
|
|
7087
|
+
}[match[2] ?? ""] ?? 1) * 1e3;
|
|
7088
|
+
}
|
|
6843
7089
|
/** Resolve the layered environment for the current principal. */
|
|
6844
|
-
async function materialize(ctx, options) {
|
|
7090
|
+
async function materialize(ctx, options, dotenvVars = {}) {
|
|
6845
7091
|
let envId;
|
|
6846
7092
|
if (!isTokenAuth(ctx)) envId = (await resolveAppEnv(ctx, options)).envId;
|
|
6847
7093
|
return materializeEnv(ctx, {
|
|
@@ -6849,7 +7095,8 @@ async function materialize(ctx, options) {
|
|
|
6849
7095
|
branch: options.branch ?? process.env.SEEKRIT_BRANCH,
|
|
6850
7096
|
with: options.with,
|
|
6851
7097
|
envFiles: options.envFile ?? [".env"],
|
|
6852
|
-
interpolate: options.interpolate
|
|
7098
|
+
interpolate: options.interpolate,
|
|
7099
|
+
cache: openCache(ctx, options, dotenvVars)
|
|
6853
7100
|
});
|
|
6854
7101
|
}
|
|
6855
7102
|
/**
|
|
@@ -6871,7 +7118,7 @@ async function materializeForRun(options) {
|
|
|
6871
7118
|
return await materialize(ctx, {
|
|
6872
7119
|
...options,
|
|
6873
7120
|
branch
|
|
6874
|
-
});
|
|
7121
|
+
}, dotenvVars);
|
|
6875
7122
|
} catch (err) {
|
|
6876
7123
|
const message = err instanceof Error ? err.message : String(err);
|
|
6877
7124
|
console.error(`seekrit: continuing without seekrit-managed secrets: ${message}`);
|
|
@@ -6994,7 +7241,7 @@ async function reapStragglers(pids, signal) {
|
|
|
6994
7241
|
process.kill(pid, "SIGKILL");
|
|
6995
7242
|
} catch {}
|
|
6996
7243
|
}
|
|
6997
|
-
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("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").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) => {
|
|
7244
|
+
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("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").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").option("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").argument("<command...>", "command to run (prefix with -- to pass flags)").action(async (commandParts, options) => {
|
|
6998
7245
|
const [cmd, ...args] = commandParts;
|
|
6999
7246
|
if (!cmd) fail("no command given");
|
|
7000
7247
|
const { values, provenance, interpolated, unresolvedRefs } = await materializeForRun(options);
|
|
@@ -7046,7 +7293,7 @@ program.command("run").description("run a command with decrypted secrets injecte
|
|
|
7046
7293
|
});
|
|
7047
7294
|
child.on("error", (err) => fail(`failed to start ${cmd}: ${err.message}`));
|
|
7048
7295
|
});
|
|
7049
|
-
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("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").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) => {
|
|
7296
|
+
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("--branch <slug>", "read a branch of that environment (or $SEEKRIT_BRANCH)").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("--cache", "keep a last-known-good copy of the encrypted response and fall back to it when the API is unreachable (off by default; SEEKRIT_CACHE=1)").option("--cache-dir <path>", "where to keep it (default: $XDG_CACHE_HOME/seekrit)").option("--cache-max-age <duration>", "how stale a cached copy may be and still be used, e.g. 15m, 24h, 7d (default: 24h)").option("--format <format>", "dotenv | json | shell", "dotenv").action(async (options) => {
|
|
7050
7297
|
if (![
|
|
7051
7298
|
"dotenv",
|
|
7052
7299
|
"json",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -26,13 +26,15 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.0",
|
|
28
28
|
"tsdown": "^0.22.3",
|
|
29
|
-
"
|
|
29
|
+
"vitest": "^4.1.9",
|
|
30
30
|
"@seekrit/core": "0.0.1",
|
|
31
|
+
"@seekrit/api-client": "0.0.1",
|
|
31
32
|
"@seekrit/crypto": "0.0.1"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "tsdown",
|
|
35
36
|
"dev": "tsdown --watch",
|
|
37
|
+
"test": "vitest run",
|
|
36
38
|
"typecheck": "tsc --noEmit"
|
|
37
39
|
}
|
|
38
40
|
}
|