@seekrit/cli 0.47.0 → 1.1.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 +1118 -420
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { arch, homedir, hostname, platform, tmpdir, userInfo } from "node:os";
|
|
@@ -8,6 +8,7 @@ import { dirname, join, parse, resolve } from "node:path";
|
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { Writable } from "node:stream";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
11
12
|
/** Default lifetime of a published bundle (7 days), in seconds. */
|
|
12
13
|
const POLICY_DEFAULT_TTL_SECONDS = 10080 * 60;
|
|
13
14
|
/** Bounds on a bundle's lifetime: an hour at the short end, 90 days at the long. */
|
|
@@ -4986,7 +4987,7 @@ async function createAgentTaskToken() {
|
|
|
4986
4987
|
}
|
|
4987
4988
|
//#endregion
|
|
4988
4989
|
//#region package.json
|
|
4989
|
-
var version = "
|
|
4990
|
+
var version = "1.1.0";
|
|
4990
4991
|
//#endregion
|
|
4991
4992
|
//#region ../../packages/api-client/src/index.ts
|
|
4992
4993
|
var SeekritApiError = class extends Error {
|
|
@@ -5765,7 +5766,18 @@ function parseDurationSeconds(input, flag) {
|
|
|
5765
5766
|
d: 86400
|
|
5766
5767
|
}[m[2] || "s"] ?? 1);
|
|
5767
5768
|
}
|
|
5768
|
-
/**
|
|
5769
|
+
/**
|
|
5770
|
+
* Prompt without echoing input (for passphrases).
|
|
5771
|
+
*
|
|
5772
|
+
* Piping the answer in (`echo … | seekrit secrets get …`) is supported and
|
|
5773
|
+
* common in CI, so this reads stdin rather than insisting on a TTY. But stdin
|
|
5774
|
+
* can also close with nothing on it — `< /dev/null`, a closed pipe, an agent
|
|
5775
|
+
* spawning us with no stdin — and readline signals that by emitting `close`
|
|
5776
|
+
* without ever calling the `question` callback. Left unhandled the promise
|
|
5777
|
+
* never settles, the event loop drains, and Node exits **0** having printed
|
|
5778
|
+
* nothing: `V=$(seekrit secrets get X)` silently yields an empty value and a
|
|
5779
|
+
* success status. So treat EOF-without-an-answer as the error it is.
|
|
5780
|
+
*/
|
|
5769
5781
|
function promptHidden(question) {
|
|
5770
5782
|
const muted = new Writable({ write(_chunk, _encoding, callback) {
|
|
5771
5783
|
callback();
|
|
@@ -5776,8 +5788,15 @@ function promptHidden(question) {
|
|
|
5776
5788
|
output: muted,
|
|
5777
5789
|
terminal: true
|
|
5778
5790
|
});
|
|
5779
|
-
return new Promise((resolve) => {
|
|
5791
|
+
return new Promise((resolve, reject) => {
|
|
5792
|
+
let answered = false;
|
|
5793
|
+
rl.on("close", () => {
|
|
5794
|
+
if (answered) return;
|
|
5795
|
+
process.stderr.write("\n");
|
|
5796
|
+
reject(/* @__PURE__ */ new Error("no passphrase on stdin — set SEEKRIT_PASSPHRASE, pipe it in, or run this in a terminal"));
|
|
5797
|
+
});
|
|
5780
5798
|
rl.question("", (answer) => {
|
|
5799
|
+
answered = true;
|
|
5781
5800
|
rl.close();
|
|
5782
5801
|
process.stderr.write("\n");
|
|
5783
5802
|
resolve(answer);
|
|
@@ -5853,9 +5872,23 @@ const CLI_CLIENT = `cli/${version}`;
|
|
|
5853
5872
|
* `flag > env > .env` credential resolution. Empty for every command but
|
|
5854
5873
|
* `seekrit run`, which loads `.env` before authenticating.
|
|
5855
5874
|
*/
|
|
5875
|
+
/**
|
|
5876
|
+
* A `SEEKRIT_*` value, or undefined if it is absent *or blank*.
|
|
5877
|
+
*
|
|
5878
|
+
* Blank has to mean absent. An unset CI secret, a `${VAR}` that expanded to
|
|
5879
|
+
* nothing, a bare `export SEEKRIT_TOKEN=` — all arrive as `""`, and `??` only
|
|
5880
|
+
* falls back on null/undefined. Left alone, an empty `SEEKRIT_API_URL` makes
|
|
5881
|
+
* every request relative and an empty `SEEKRIT_TOKEN` authenticates as a
|
|
5882
|
+
* bearer of nothing: a 401 where the honest answer is "you have no
|
|
5883
|
+
* credentials", pointing at the API instead of at the missing variable.
|
|
5884
|
+
*/
|
|
5885
|
+
function present(value) {
|
|
5886
|
+
const trimmed = value?.trim();
|
|
5887
|
+
return trimmed ? trimmed : void 0;
|
|
5888
|
+
}
|
|
5856
5889
|
function tryBuildContext(dotenvVars = {}) {
|
|
5857
5890
|
const config = readGlobalConfig();
|
|
5858
|
-
const fromEnv = (key) => process.env[key] ?? dotenvVars[key];
|
|
5891
|
+
const fromEnv = (key) => present(process.env[key]) ?? present(dotenvVars[key]);
|
|
5859
5892
|
const apiUrl = fromEnv("SEEKRIT_API_URL") ?? config.apiUrl ?? "https://api.seekrit.dev";
|
|
5860
5893
|
const token = fromEnv("SEEKRIT_TOKEN") ?? config.token ?? config.sessionToken;
|
|
5861
5894
|
const devUser = fromEnv("SEEKRIT_DEV_USER") ?? config.devUser;
|
|
@@ -5888,6 +5921,24 @@ function isTokenAuth(ctx) {
|
|
|
5888
5921
|
return ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token);
|
|
5889
5922
|
}
|
|
5890
5923
|
/**
|
|
5924
|
+
* Render an unhandled error for the top-level handler.
|
|
5925
|
+
*
|
|
5926
|
+
* `fetch` reports every transport failure as the same opaque `TypeError: fetch
|
|
5927
|
+
* failed` and hides the reason (ECONNREFUSED, DNS, TLS) in `cause`. That is the
|
|
5928
|
+
* single most common thing to go wrong — a stale `SEEKRIT_API_URL`, a dev
|
|
5929
|
+
* server that isn't up, a VPN that is down — and "error: fetch failed" names
|
|
5930
|
+
* neither the address we tried nor why it failed. So unwrap it and say both.
|
|
5931
|
+
*/
|
|
5932
|
+
function describeError(err) {
|
|
5933
|
+
if (!(err instanceof Error)) return String(err);
|
|
5934
|
+
if (err.message !== "fetch failed") return err.message;
|
|
5935
|
+
const config = readGlobalConfig();
|
|
5936
|
+
const apiUrl = present(process.env.SEEKRIT_API_URL) ?? config.apiUrl ?? "https://api.seekrit.dev";
|
|
5937
|
+
const cause = err.cause;
|
|
5938
|
+
const reason = cause instanceof Error ? cause.code ?? cause.message : void 0;
|
|
5939
|
+
return `cannot reach the seekrit API at ${apiUrl}${reason ? ` (${reason})` : ""} — check SEEKRIT_API_URL, your network, and that the API is up`;
|
|
5940
|
+
}
|
|
5941
|
+
/**
|
|
5891
5942
|
* Recover the calling principal's private key:
|
|
5892
5943
|
* - service tokens carry their private key in the token string;
|
|
5893
5944
|
* - users fetch their passphrase-encrypted key from the API and unlock it.
|
|
@@ -6150,7 +6201,7 @@ async function resolvePrincipal(ctx, orgId, options) {
|
|
|
6150
6201
|
}
|
|
6151
6202
|
/** The `--org/--app/--group/--env` selection every grant command shares. */
|
|
6152
6203
|
function withEnvTarget(cmd) {
|
|
6153
|
-
return cmd.option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "target a group environment instead of an app").requiredOption("--env <slug>");
|
|
6204
|
+
return cmd.option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").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");
|
|
6154
6205
|
}
|
|
6155
6206
|
/**
|
|
6156
6207
|
* Environment key grants — who can decrypt what.
|
|
@@ -6185,7 +6236,7 @@ function registerAccessCommands(program) {
|
|
|
6185
6236
|
col("id", (g) => g.principalId)
|
|
6186
6237
|
], "nobody holds a key for this environment"));
|
|
6187
6238
|
});
|
|
6188
|
-
withEnvTarget(grant.command("rm").alias("revoke").description("take away a member's or token's access to an environment's key").option("--user <email>", "revoke an org member by email").option("--token <tokenId>", "revoke a service token by id (skt_…)").option("--yes", "skip the confirmation prompt")).action(async (options) => {
|
|
6239
|
+
withEnvTarget(grant.command("rm").alias("revoke").description("take away a member's or token's access to an environment's key").option("--user <email>", "revoke an org member by email").option("--token <tokenId>", "revoke a service token by id (skt_…)").option("-y, --yes", "skip the confirmation prompt")).action(async (options) => {
|
|
6189
6240
|
if (!options.user === !options.token) fail("pass exactly one of --user or --token");
|
|
6190
6241
|
const ctx = buildContext();
|
|
6191
6242
|
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
@@ -6231,7 +6282,7 @@ function registerAccountCommands(program) {
|
|
|
6231
6282
|
col("id", (s) => `${s.id}${s.id === currentSessionId ? " (this one)" : ""}`)
|
|
6232
6283
|
], options.all ? "no CLI sessions" : "no active CLI sessions (try --all)"));
|
|
6233
6284
|
});
|
|
6234
|
-
session.command("revoke <sessionId>").description("sign a device out — its token stops working immediately").option("--yes", "skip the confirmation prompt").action(async (sessionId, options) => {
|
|
6285
|
+
session.command("revoke <sessionId>").description("sign a device out — its token stops working immediately").option("-y, --yes", "skip the confirmation prompt").action(async (sessionId, options) => {
|
|
6235
6286
|
const ctx = buildContext();
|
|
6236
6287
|
const self = ctx.auth.type === "bearer" && isCliSessionToken(ctx.auth.token) && parseCliSessionToken(ctx.auth.token).sessionId === sessionId;
|
|
6237
6288
|
await confirmDestructive(options.yes, self ? `${sessionId} is the session you are using right now — sign it out?` : `Sign out ${sessionId}?`);
|
|
@@ -6962,7 +7013,7 @@ async function kmsRecoverMaterial(ctx, orgId, keyId, version) {
|
|
|
6962
7013
|
}
|
|
6963
7014
|
function registerKmsCommands(program) {
|
|
6964
7015
|
const kms = program.command("kms").description("managed keys for application-layer encryption & signing (client-side)");
|
|
6965
|
-
kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$7, []).option("--grant-token <tokenId>", "also grant a service token (repeatable)", collect$7, []).action(async (options) => {
|
|
7016
|
+
kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$7, []).option("--grant-token <tokenId>", "also grant a service token (repeatable)", collect$7, []).action(async (options) => {
|
|
6966
7017
|
if (options.purpose !== "encrypt" && options.purpose !== "sign") fail("--purpose must be encrypt or sign");
|
|
6967
7018
|
if (options.app && options.group) fail("pass at most one of --app or --group");
|
|
6968
7019
|
const ctx = buildContext();
|
|
@@ -7015,7 +7066,7 @@ function registerKmsCommands(program) {
|
|
|
7015
7066
|
const { key } = await ctx.client.createKmsKey(org.id, input);
|
|
7016
7067
|
console.error(`created ${key.purpose} key ${key.name} (${key.id}), ${grants.length} grant(s)`);
|
|
7017
7068
|
});
|
|
7018
|
-
kms.command("ls").description("list keys you can see").option("--org <slug>").action(async (options) => {
|
|
7069
|
+
kms.command("ls").description("list keys you can see").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7019
7070
|
const ctx = buildContext();
|
|
7020
7071
|
const org = await resolveOrg(ctx, options.org);
|
|
7021
7072
|
const { keys } = await ctx.client.listKmsKeys(org.id);
|
|
@@ -7029,7 +7080,7 @@ function registerKmsCommands(program) {
|
|
|
7029
7080
|
console.log(`${k.name}\t${k.purpose}\tv${k.currentVersion}\t${scope}\t${k.id}${state}`);
|
|
7030
7081
|
}
|
|
7031
7082
|
});
|
|
7032
|
-
kms.command("grant").description("grant a principal use of a key's current version").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--user <email>", "grant an org member").option("--token <tokenId>", "grant a service token").action(async (options) => {
|
|
7083
|
+
kms.command("grant").description("grant a principal use of a key's current version").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--user <email>", "grant an org member").option("--token <tokenId>", "grant a service token").action(async (options) => {
|
|
7033
7084
|
if (!options.user === !options.token) fail("pass exactly one of --user or --token");
|
|
7034
7085
|
const ctx = buildContext();
|
|
7035
7086
|
const org = await resolveOrg(ctx, options.org);
|
|
@@ -7043,19 +7094,20 @@ function registerKmsCommands(program) {
|
|
|
7043
7094
|
});
|
|
7044
7095
|
console.error(`granted ${key.name} to ${recipient.principalId}`);
|
|
7045
7096
|
});
|
|
7046
|
-
kms.command("revoke").description("revoke a principal from a key (all versions)").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--user <email>").option("--token <tokenId>").action(async (options) => {
|
|
7097
|
+
kms.command("revoke").description("revoke a principal from a key (all versions)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--user <email>", "revoke this user's grant").option("--token <tokenId>", "revoke this service token's grant").option("-y, --yes", "skip the confirmation").action(async (options) => {
|
|
7047
7098
|
if (!options.user === !options.token) fail("pass exactly one of --user or --token");
|
|
7048
7099
|
const ctx = buildContext();
|
|
7049
7100
|
const org = await resolveOrg(ctx, options.org);
|
|
7050
7101
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
7051
7102
|
const recipient = await kmsResolveRecipient(ctx, org.id, options);
|
|
7103
|
+
await confirmDestructive(options.yes, `Revoke ${recipient.principalId} from ${key.name}? Anything already decrypted stays decrypted.`);
|
|
7052
7104
|
await ctx.client.revokeKmsKey(org.id, key.id, {
|
|
7053
7105
|
principalType: recipient.principalType,
|
|
7054
7106
|
principalId: recipient.principalId
|
|
7055
7107
|
});
|
|
7056
7108
|
console.error(`revoked ${recipient.principalId} from ${key.name}`);
|
|
7057
7109
|
});
|
|
7058
|
-
kms.command("rotate").description("add a new key version and re-wrap it for every current grantee").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
|
|
7110
|
+
kms.command("rotate").description("add a new key version and re-wrap it for every current grantee").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7059
7111
|
const ctx = buildContext();
|
|
7060
7112
|
const org = await resolveOrg(ctx, options.org);
|
|
7061
7113
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
@@ -7089,21 +7141,23 @@ function registerKmsCommands(program) {
|
|
|
7089
7141
|
});
|
|
7090
7142
|
console.error(`rotated ${rotated.name} to v${rotated.currentVersion} (${grants.length} grantees)`);
|
|
7091
7143
|
});
|
|
7092
|
-
kms.command("disable").description("disable a key (blocks all use: encrypt/decrypt/sign and new grants/rotations)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
|
|
7144
|
+
kms.command("disable").description("disable a key (blocks all use: encrypt/decrypt/sign and new grants/rotations)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (options) => {
|
|
7093
7145
|
const ctx = buildContext();
|
|
7094
7146
|
const org = await resolveOrg(ctx, options.org);
|
|
7095
7147
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
7148
|
+
await confirmDestructive(options.yes, `Disable ${key.name}? Every encrypt, decrypt, and sign against it starts failing.`);
|
|
7096
7149
|
await ctx.client.disableKmsKey(org.id, key.id);
|
|
7097
7150
|
console.error(`disabled ${key.name}`);
|
|
7098
7151
|
});
|
|
7099
|
-
kms.command("delete").description("delete a key (hides it from all listings; the name frees up for reuse)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
|
|
7152
|
+
kms.command("delete").description("delete a key (hides it from all listings; the name frees up for reuse)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (options) => {
|
|
7100
7153
|
const ctx = buildContext();
|
|
7101
7154
|
const org = await resolveOrg(ctx, options.org);
|
|
7102
7155
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
7156
|
+
await confirmDestructive(options.yes, `Delete ${key.name}? Any ciphertext or signature still relying on it becomes unrecoverable.`);
|
|
7103
7157
|
await ctx.client.deleteKmsKey(org.id, key.id);
|
|
7104
7158
|
console.error(`deleted ${key.name}`);
|
|
7105
7159
|
});
|
|
7106
|
-
kms.command("encrypt").description("encrypt stdin under a key (prints a ce1 ciphertext blob)").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--context <ctx>", "encryption context bound as AAD (required identically to decrypt)").action(async (options) => {
|
|
7160
|
+
kms.command("encrypt").description("encrypt stdin under a key (prints a ce1 ciphertext blob)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--context <ctx>", "encryption context bound as AAD (required identically to decrypt)").action(async (options) => {
|
|
7107
7161
|
const ctx = buildContext();
|
|
7108
7162
|
const org = await resolveOrg(ctx, options.org);
|
|
7109
7163
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
@@ -7116,7 +7170,7 @@ function registerKmsCommands(program) {
|
|
|
7116
7170
|
}, plaintext, options.context ?? "");
|
|
7117
7171
|
console.log(blob);
|
|
7118
7172
|
});
|
|
7119
|
-
kms.command("decrypt").description("decrypt a ce1 blob from stdin").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--context <ctx>", "the same encryption context used to encrypt").action(async (options) => {
|
|
7173
|
+
kms.command("decrypt").description("decrypt a ce1 blob from stdin").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--context <ctx>", "the same encryption context used to encrypt").action(async (options) => {
|
|
7120
7174
|
const ctx = buildContext();
|
|
7121
7175
|
const org = await resolveOrg(ctx, options.org);
|
|
7122
7176
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
@@ -7125,7 +7179,7 @@ function registerKmsCommands(program) {
|
|
|
7125
7179
|
const { material } = await kmsRecoverMaterial(ctx, org.id, key.id, ref.version);
|
|
7126
7180
|
console.log(await kmsDecrypt(material, blob, options.context ?? ""));
|
|
7127
7181
|
});
|
|
7128
|
-
kms.command("generate-data-key").description("generate a data key: prints JSON {plaintextBase64, wrapped}").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
|
|
7182
|
+
kms.command("generate-data-key").description("generate a data key: prints JSON {plaintextBase64, wrapped}").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7129
7183
|
const ctx = buildContext();
|
|
7130
7184
|
const org = await resolveOrg(ctx, options.org);
|
|
7131
7185
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
@@ -7140,7 +7194,7 @@ function registerKmsCommands(program) {
|
|
|
7140
7194
|
wrapped: dk.wrapped
|
|
7141
7195
|
}));
|
|
7142
7196
|
});
|
|
7143
|
-
kms.command("open-data-key").description("recover a data key from a dk1 blob on stdin (prints plaintext base64)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
|
|
7197
|
+
kms.command("open-data-key").description("recover a data key from a dk1 blob on stdin (prints plaintext base64)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7144
7198
|
const ctx = buildContext();
|
|
7145
7199
|
const org = await resolveOrg(ctx, options.org);
|
|
7146
7200
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
@@ -7149,7 +7203,7 @@ function registerKmsCommands(program) {
|
|
|
7149
7203
|
const { material } = await kmsRecoverMaterial(ctx, org.id, key.id, ref.version);
|
|
7150
7204
|
console.log(toBase64(await decryptDataKey(material, wrapped)));
|
|
7151
7205
|
});
|
|
7152
|
-
kms.command("sign").description("sign stdin with a signing key (prints an sg1 signature)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
|
|
7206
|
+
kms.command("sign").description("sign stdin with a signing key (prints an sg1 signature)").requiredOption("--key <name>", "key name or id").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7153
7207
|
const ctx = buildContext();
|
|
7154
7208
|
const org = await resolveOrg(ctx, options.org);
|
|
7155
7209
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
@@ -7162,7 +7216,7 @@ function registerKmsCommands(program) {
|
|
|
7162
7216
|
version: currentVersion
|
|
7163
7217
|
}, message));
|
|
7164
7218
|
});
|
|
7165
|
-
kms.command("verify").description("verify an sg1 signature over stdin (exit 0 = valid)").requiredOption("--key <name>", "key name or id").requiredOption("--signature <sg1>", "the signature blob").option("--org <slug>").action(async (options) => {
|
|
7219
|
+
kms.command("verify").description("verify an sg1 signature over stdin (exit 0 = valid)").requiredOption("--key <name>", "key name or id").requiredOption("--signature <sg1>", "the signature blob").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7166
7220
|
const ctx = buildContext();
|
|
7167
7221
|
const org = await resolveOrg(ctx, options.org);
|
|
7168
7222
|
const key = await kmsResolveKey(ctx, org.id, options.key);
|
|
@@ -7264,7 +7318,7 @@ async function buildRecoveryConfig(ctx, orgId, thresholdRaw, custodianRefs) {
|
|
|
7264
7318
|
}
|
|
7265
7319
|
function registerRecoveryCommands(program) {
|
|
7266
7320
|
const recovery = program.command("recovery").description("customer-controlled M-of-N recovery (zero-knowledge)");
|
|
7267
|
-
recovery.command("status").description("show recovery configuration and environment coverage").option("--org <slug>").action(async (options) => {
|
|
7321
|
+
recovery.command("status").description("show recovery configuration and environment coverage").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7268
7322
|
const ctx = buildContext();
|
|
7269
7323
|
const org = await resolveOrg(ctx, options.org);
|
|
7270
7324
|
const { recovery: status } = await ctx.client.getRecovery(org.id);
|
|
@@ -7278,7 +7332,7 @@ function registerRecoveryCommands(program) {
|
|
|
7278
7332
|
for (const cst of status.custodians) console.log(` - ${cst.label ?? cst.principalId} (${cst.principalType}, share #${cst.shareIndex})`);
|
|
7279
7333
|
if (status.coverage.unprotectedEnvIds.length > 0) console.log(`${status.coverage.unprotectedEnvIds.length} environment(s) not yet protected — run \`seekrit recovery sync\``);
|
|
7280
7334
|
});
|
|
7281
|
-
recovery.command("setup").description("enable recovery: split a fresh recovery key across custodians").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>").action(async (options) => {
|
|
7335
|
+
recovery.command("setup").description("enable recovery: split a fresh recovery key across custodians").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7282
7336
|
const ctx = buildContext();
|
|
7283
7337
|
const org = await resolveOrg(ctx, options.org);
|
|
7284
7338
|
const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
|
|
@@ -7291,12 +7345,12 @@ function registerRecoveryCommands(program) {
|
|
|
7291
7345
|
console.error(`recovery-protected ${wrapped} environment(s) you can decrypt`);
|
|
7292
7346
|
if (skipped > 0) console.error(`${skipped} environment(s) need another admin to run \`seekrit recovery sync\``);
|
|
7293
7347
|
});
|
|
7294
|
-
recovery.command("sync").description("recovery-protect environments you can decrypt but that aren't yet covered").option("--org <slug>").action(async (options) => {
|
|
7348
|
+
recovery.command("sync").description("recovery-protect environments you can decrypt but that aren't yet covered").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7295
7349
|
const ctx = buildContext();
|
|
7296
7350
|
const { wrapped, skipped } = await syncRecoveryGrants(ctx, (await resolveOrg(ctx, options.org)).id);
|
|
7297
7351
|
console.error(`recovery-protected ${wrapped} environment(s); skipped ${skipped} you cannot decrypt`);
|
|
7298
7352
|
});
|
|
7299
|
-
recovery.command("rotate").description("rotate the recovery key (new keypair, custodians, and env re-wraps)").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>").action(async (options) => {
|
|
7353
|
+
recovery.command("rotate").description("rotate the recovery key (new keypair, custodians, and env re-wraps)").requiredOption("--threshold <M>", "custodians required to recover (M in M-of-N)").option("--custodian <email|skt_id>", "a recovery custodian (repeatable)", collect$6, []).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7300
7354
|
const ctx = buildContext();
|
|
7301
7355
|
const org = await resolveOrg(ctx, options.org);
|
|
7302
7356
|
const config = await buildRecoveryConfig(ctx, org.id, options.threshold, options.custodian);
|
|
@@ -7309,13 +7363,14 @@ function registerRecoveryCommands(program) {
|
|
|
7309
7363
|
console.error(`re-wrapped ${wrapped} environment(s) you can decrypt to the new recovery key`);
|
|
7310
7364
|
if (skipped > 0) console.error(`${skipped} environment(s) still need another admin to run \`seekrit recovery sync\``);
|
|
7311
7365
|
});
|
|
7312
|
-
recovery.command("disable").description("disable recovery and remove all recovery grants").option("--org <slug>").action(async (options) => {
|
|
7366
|
+
recovery.command("disable").description("disable recovery and remove all recovery grants").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (options) => {
|
|
7313
7367
|
const ctx = buildContext();
|
|
7314
7368
|
const org = await resolveOrg(ctx, options.org);
|
|
7369
|
+
await confirmDestructive(options.yes, `Disable recovery for ${org.slug}? Every custodian share is dropped, and the org loses its break-glass path.`);
|
|
7315
7370
|
await ctx.client.disableRecovery(org.id);
|
|
7316
7371
|
console.error("recovery disabled; recovery grants removed");
|
|
7317
7372
|
});
|
|
7318
|
-
recovery.command("request").description("start a recovery ceremony (defaults to recovering access for yourself)").option("--target-user <email>", "recover access for another member").option("--target-token <id>", "recover access for a service token").option("--reason <text>", "note recorded in the audit trail").option("--org <slug>").action(async (options) => {
|
|
7373
|
+
recovery.command("request").description("start a recovery ceremony (defaults to recovering access for yourself)").option("--target-user <email>", "recover access for another member").option("--target-token <id>", "recover access for a service token").option("--reason <text>", "note recorded in the audit trail").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
7319
7374
|
const ctx = buildContext();
|
|
7320
7375
|
const org = await resolveOrg(ctx, options.org);
|
|
7321
7376
|
const target = options.targetUser || options.targetToken ? await kmsResolveRecipient(ctx, org.id, {
|
|
@@ -7332,7 +7387,7 @@ function registerRecoveryCommands(program) {
|
|
|
7332
7387
|
console.error(` custodians run: seekrit recovery approve ${request.id}`);
|
|
7333
7388
|
console.error(` then the target: seekrit recovery complete ${request.id}`);
|
|
7334
7389
|
});
|
|
7335
|
-
recovery.command("approve").description("as a custodian, contribute your share to a recovery request").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
7390
|
+
recovery.command("approve").description("as a custodian, contribute your share to a recovery request").argument("<requestId>").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (requestId, options) => {
|
|
7336
7391
|
const ctx = buildContext();
|
|
7337
7392
|
const org = await resolveOrg(ctx, options.org);
|
|
7338
7393
|
const { request } = await ctx.client.getRecoveryRequest(org.id, requestId);
|
|
@@ -7345,7 +7400,7 @@ function registerRecoveryCommands(program) {
|
|
|
7345
7400
|
});
|
|
7346
7401
|
console.error(`contributed share #${myShare.shareIndex}: ${res.contributed}/${res.threshold} collected${res.quorumReached ? " — quorum reached" : ""}`);
|
|
7347
7402
|
});
|
|
7348
|
-
recovery.command("complete").description("as the recovery target, reconstruct the key and restore your access").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
7403
|
+
recovery.command("complete").description("as the recovery target, reconstruct the key and restore your access").argument("<requestId>").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (requestId, options) => {
|
|
7349
7404
|
const ctx = buildContext();
|
|
7350
7405
|
const org = await resolveOrg(ctx, options.org);
|
|
7351
7406
|
const { request, contributions, quorumReached } = await ctx.client.getRecoveryRequest(org.id, requestId);
|
|
@@ -7369,9 +7424,10 @@ function registerRecoveryCommands(program) {
|
|
|
7369
7424
|
});
|
|
7370
7425
|
console.error(`recovery complete: restored access to ${restored.length} environment(s)`);
|
|
7371
7426
|
});
|
|
7372
|
-
recovery.command("cancel").description("cancel an open recovery request").argument("<requestId>").option("--org <slug>").action(async (requestId, options) => {
|
|
7427
|
+
recovery.command("cancel").description("cancel an open recovery request").argument("<requestId>").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (requestId, options) => {
|
|
7373
7428
|
const ctx = buildContext();
|
|
7374
7429
|
const org = await resolveOrg(ctx, options.org);
|
|
7430
|
+
await confirmDestructive(options.yes, `Cancel recovery request ${requestId}? Approvals already collected are discarded.`);
|
|
7375
7431
|
await ctx.client.cancelRecoveryRequest(org.id, requestId);
|
|
7376
7432
|
console.error(`recovery request ${requestId} canceled`);
|
|
7377
7433
|
});
|
|
@@ -7385,7 +7441,7 @@ function registerRecoveryCommands(program) {
|
|
|
7385
7441
|
*/
|
|
7386
7442
|
function registerAppCommands(program) {
|
|
7387
7443
|
const app = program.command("app").description("manage applications");
|
|
7388
|
-
app.command("list").alias("ls").description("list applications in an organization").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
7444
|
+
app.command("list").alias("ls").description("list applications in an organization").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
7389
7445
|
const ctx = buildContext();
|
|
7390
7446
|
const ref = await resolveOrg(ctx, options.org);
|
|
7391
7447
|
const { apps } = await ctx.client.listApps(ref.id);
|
|
@@ -7396,7 +7452,7 @@ function registerAppCommands(program) {
|
|
|
7396
7452
|
col("id", (a) => a.id)
|
|
7397
7453
|
], "no applications — create one with `seekrit app create`"));
|
|
7398
7454
|
});
|
|
7399
|
-
app.command("show [slug]").description("show an application, its environments, and your access to each").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (slug, options) => {
|
|
7455
|
+
app.command("show [slug]").description("show an application, its environments, and your access to each").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (slug, options) => {
|
|
7400
7456
|
const ctx = buildContext();
|
|
7401
7457
|
const ref = await resolveApp(ctx, {
|
|
7402
7458
|
org: options.org,
|
|
@@ -7432,7 +7488,7 @@ function registerAppCommands(program) {
|
|
|
7432
7488
|
}
|
|
7433
7489
|
});
|
|
7434
7490
|
});
|
|
7435
|
-
app.command("create").description("create an application").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
7491
|
+
app.command("create").description("create an application").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
|
|
7436
7492
|
const ctx = buildContext();
|
|
7437
7493
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
7438
7494
|
const created = await ctx.client.createApp(orgRef.id, {
|
|
@@ -7441,7 +7497,7 @@ function registerAppCommands(program) {
|
|
|
7441
7497
|
});
|
|
7442
7498
|
console.error(`created app ${created.app.slug} (${created.app.id})`);
|
|
7443
7499
|
});
|
|
7444
|
-
app.command("rename [slug]").description("change an application's display name (the slug is permanent)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
|
|
7500
|
+
app.command("rename [slug]").description("change an application's display name (the slug is permanent)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
|
|
7445
7501
|
const ctx = buildContext();
|
|
7446
7502
|
const ref = await resolveApp(ctx, {
|
|
7447
7503
|
org: options.org,
|
|
@@ -7450,7 +7506,7 @@ function registerAppCommands(program) {
|
|
|
7450
7506
|
const { app: row } = await ctx.client.updateApp(ref.orgId, ref.id, { name: options.name });
|
|
7451
7507
|
console.error(`renamed ${row.slug} to "${row.name}"`);
|
|
7452
7508
|
});
|
|
7453
|
-
app.command("rm <slug>").alias("delete").description("delete an application and every environment and secret in it").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (slug, options) => {
|
|
7509
|
+
app.command("rm <slug>").alias("delete").description("delete an application and every environment and secret in it").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (slug, options) => {
|
|
7454
7510
|
const ctx = buildContext();
|
|
7455
7511
|
const ref = await resolveApp(ctx, {
|
|
7456
7512
|
org: options.org,
|
|
@@ -7462,7 +7518,7 @@ function registerAppCommands(program) {
|
|
|
7462
7518
|
console.error(`deleted app ${ref.slug}`);
|
|
7463
7519
|
});
|
|
7464
7520
|
const env = program.command("env").description("manage environments");
|
|
7465
|
-
env.command("list").alias("ls").description("list an application's environments").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (options) => {
|
|
7521
|
+
env.command("list").alias("ls").description("list an application's environments").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--json", "print the raw API response").action(async (options) => {
|
|
7466
7522
|
const ctx = buildContext();
|
|
7467
7523
|
const ref = await resolveApp(ctx, options);
|
|
7468
7524
|
const { environments } = await ctx.client.getApp(ref.orgId, ref.id);
|
|
@@ -7473,7 +7529,7 @@ function registerAppCommands(program) {
|
|
|
7473
7529
|
col("id", (e) => e.id)
|
|
7474
7530
|
], "no environments — create one with `seekrit env create`"));
|
|
7475
7531
|
});
|
|
7476
|
-
env.command("show").description("show one environment: composed groups, who holds a key, secret count").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
7532
|
+
env.command("show").description("show one environment: composed groups, who holds a key, secret count").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "environment slug").option("--json", "print the raw API response").action(async (options) => {
|
|
7477
7533
|
const ctx = buildContext();
|
|
7478
7534
|
const target = await resolveAppEnv(ctx, options);
|
|
7479
7535
|
const [{ environment }, { groups }, { secrets }, { branches }] = await Promise.all([
|
|
@@ -7513,7 +7569,7 @@ function registerAppCommands(program) {
|
|
|
7513
7569
|
}
|
|
7514
7570
|
});
|
|
7515
7571
|
});
|
|
7516
|
-
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) => {
|
|
7572
|
+
env.command("create").description("create an application environment (generates its data key locally)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--app <slug>", "application slug").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
|
|
7517
7573
|
const ctx = buildContext();
|
|
7518
7574
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
7519
7575
|
const { apps } = await ctx.client.listApps(orgRef.id);
|
|
@@ -7532,7 +7588,7 @@ function registerAppCommands(program) {
|
|
|
7532
7588
|
});
|
|
7533
7589
|
console.error(`created environment ${created.environment.slug} (${created.environment.id})`);
|
|
7534
7590
|
});
|
|
7535
|
-
env.command("rm").alias("delete").description("delete an environment and every secret in it").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "delete a group environment instead of an app one").requiredOption("--env <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
|
|
7591
|
+
env.command("rm").alias("delete").description("delete an environment and every secret in it").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "delete a group environment instead of an app one").requiredOption("--env <slug>", "environment slug").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
|
|
7536
7592
|
const ctx = buildContext();
|
|
7537
7593
|
const target = await resolveEnvTarget(ctx, options);
|
|
7538
7594
|
const { secrets } = await ctx.client.listSecrets(target.orgId, target.envId);
|
|
@@ -7541,7 +7597,7 @@ function registerAppCommands(program) {
|
|
|
7541
7597
|
console.error(`deleted ${target.label}`);
|
|
7542
7598
|
});
|
|
7543
7599
|
const envGroups = env.command("groups").description("compose shared groups into an application environment");
|
|
7544
|
-
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) => {
|
|
7600
|
+
envGroups.command("add").description("compose a group into an app environment").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--app <slug>", "application slug").requiredOption("--env <slug>", "environment slug").requiredOption("--group <slug>", "group slug").option("--position <n>", "precedence among groups (higher wins)").action(async (options) => {
|
|
7545
7601
|
const ctx = buildContext();
|
|
7546
7602
|
const target = await resolveAppEnv(ctx, options);
|
|
7547
7603
|
const group = await resolveGroup(ctx, {
|
|
@@ -7554,7 +7610,7 @@ function registerAppCommands(program) {
|
|
|
7554
7610
|
});
|
|
7555
7611
|
console.error(`composed ${group.slug} into ${target.appSlug}/${target.envSlug}`);
|
|
7556
7612
|
});
|
|
7557
|
-
envGroups.command("list").alias("ls").description("list groups composed into an app environment").option("--org <slug>").requiredOption("--app <slug>").requiredOption("--env <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
7613
|
+
envGroups.command("list").alias("ls").description("list groups composed into an app environment").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--app <slug>", "application slug").requiredOption("--env <slug>", "environment slug").option("--json", "print the raw API response").action(async (options) => {
|
|
7558
7614
|
const ctx = buildContext();
|
|
7559
7615
|
const target = await resolveAppEnv(ctx, options);
|
|
7560
7616
|
const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
|
|
@@ -7564,13 +7620,14 @@ function registerAppCommands(program) {
|
|
|
7564
7620
|
col("name", (g) => g.name)
|
|
7565
7621
|
], "no groups composed into this environment"));
|
|
7566
7622
|
});
|
|
7567
|
-
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) => {
|
|
7623
|
+
envGroups.command("rm").description("remove a group from an app environment").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--app <slug>", "application slug").requiredOption("--env <slug>", "environment slug").requiredOption("--group <slug>", "group slug").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
|
|
7568
7624
|
const ctx = buildContext();
|
|
7569
7625
|
const target = await resolveAppEnv(ctx, options);
|
|
7570
7626
|
const group = await resolveGroup(ctx, {
|
|
7571
7627
|
org: options.org,
|
|
7572
7628
|
group: options.group
|
|
7573
7629
|
});
|
|
7630
|
+
await confirmDestructive(options.yes, `Remove ${group.slug} from ${target.appSlug}/${target.envSlug}? Everything it contributed disappears from that environment.`);
|
|
7574
7631
|
await ctx.client.unlinkEnvGroup(target.orgId, target.envId, group.id);
|
|
7575
7632
|
console.error(`removed ${group.slug} from ${target.appSlug}/${target.envSlug}`);
|
|
7576
7633
|
});
|
|
@@ -8593,7 +8650,7 @@ async function decryptArchive(archive, key, filter) {
|
|
|
8593
8650
|
}
|
|
8594
8651
|
function registerArchiveCommands(program) {
|
|
8595
8652
|
const archive = program.command("archive").description("export the whole org as one signed file, and open it offline");
|
|
8596
|
-
archive.command("create").description("download a signed archive of everything seekrit stores for the org").option("--org <slug>").option("-o, --out <file>", "write the archive here (default: ./seekrit-<org>-<date>.json)").option("--no-versions", "omit each secret's ciphertext history").option("--no-audit", "omit the audit trail").option("--audit-limit <n>", "keep at most this many of the newest audit rows", Number).option("--json", "print the archive to stdout instead of writing a file").action(async (options) => {
|
|
8653
|
+
archive.command("create").description("download a signed archive of everything seekrit stores for the org").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-o, --out <file>", "write the archive here (default: ./seekrit-<org>-<date>.json)").option("--no-versions", "omit each secret's ciphertext history").option("--no-audit", "omit the audit trail").option("--audit-limit <n>", "keep at most this many of the newest audit rows", Number).option("--json", "print the archive to stdout instead of writing a file").action(async (options) => {
|
|
8597
8654
|
const ctx = buildContext();
|
|
8598
8655
|
const ref = await resolveOrg(ctx, options.org);
|
|
8599
8656
|
const input = {
|
|
@@ -8706,7 +8763,7 @@ function registerArchiveCommands(program) {
|
|
|
8706
8763
|
} catch {}
|
|
8707
8764
|
fail("none of the recovery shares in this archive unwrap with that key");
|
|
8708
8765
|
});
|
|
8709
|
-
archive.command("decrypt <file>").description("decrypt an archive's secrets with your own key (offline)").option("-o, --out <dir>", "write one file per environment into this directory").option("--stdout", "print plaintext to stdout instead of writing files").option("--env <label>", "only this environment (app/env, group@env, slug, or id)").option("--format <format>", "dotenv | json | shell", "dotenv").option("--token <skt_…>", "decrypt as a service token instead of a passphrase").option("--key-file <path>", "decrypt with a private key JWK file").option("--share <file>", "custodian share for an offline quorum (repeatable)", collect$5).option("--yes", "skip the confirmation when printing plaintext to stdout").action(async (file, options) => {
|
|
8766
|
+
archive.command("decrypt <file>").description("decrypt an archive's secrets with your own key (offline)").option("-o, --out <dir>", "write one file per environment into this directory").option("--stdout", "print plaintext to stdout instead of writing files").option("--env <label>", "only this environment (app/env, group@env, slug, or id)").option("--format <format>", "dotenv | json | shell", "dotenv").option("--token <skt_…>", "decrypt as a service token instead of a passphrase").option("--key-file <path>", "decrypt with a private key JWK file").option("--share <file>", "custodian share for an offline quorum (repeatable)", collect$5).option("-y, --yes", "skip the confirmation when printing plaintext to stdout").action(async (file, options) => {
|
|
8710
8767
|
if (!options.out && !options.stdout) fail("choose a destination: --out <dir> to write files, or --stdout to print plaintext");
|
|
8711
8768
|
if (![
|
|
8712
8769
|
"dotenv",
|
|
@@ -8764,7 +8821,7 @@ function parseLimit(raw) {
|
|
|
8764
8821
|
*/
|
|
8765
8822
|
function registerAuditCommands(program) {
|
|
8766
8823
|
const audit = program.command("audit").description("read the org audit trail");
|
|
8767
|
-
audit.command("list", { isDefault: true }).alias("ls").description("show the org audit trail").option("--org <slug>").option("--limit <n>", `entries per page (max ${MAX_PAGE})`, "50").option("--action <action>", "only this action, e.g. env.key_granted").option("--resource-type <type>", "only this resource type, e.g. environment").option("--cursor <cursor>", "continue from a previous page's cursor").option("--all", "page through the whole trail, not just the first page").option("--metadata", "include each entry's metadata as JSON").option("--json", "print the raw API response").action(async (options) => {
|
|
8824
|
+
audit.command("list", { isDefault: true }).alias("ls").description("show the org audit trail").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--limit <n>", `entries per page (max ${MAX_PAGE})`, "50").option("--action <action>", "only this action, e.g. env.key_granted").option("--resource-type <type>", "only this resource type, e.g. environment").option("--cursor <cursor>", "continue from a previous page's cursor").option("--all", "page through the whole trail, not just the first page").option("--metadata", "include each entry's metadata as JSON").option("--json", "print the raw API response").action(async (options) => {
|
|
8768
8825
|
if (options.action && !AUDIT_ACTIONS.includes(options.action)) fail(`unknown action "${options.action}" — see \`seekrit audit actions\``);
|
|
8769
8826
|
const ctx = buildContext();
|
|
8770
8827
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -8847,7 +8904,7 @@ function resolveBaseCredential(opts) {
|
|
|
8847
8904
|
function registerAwsCommands(program) {
|
|
8848
8905
|
const aws = program.command("aws").description("temporary AWS credentials (STS AssumeRole, zero-knowledge)");
|
|
8849
8906
|
const target = aws.command("target").description("manage AWS role targets");
|
|
8850
|
-
target.command("add").description("register an assumable IAM role to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--role-arn <arn>", "the IAM role to assume, arn:aws:iam::<acct>:role/<name>").requiredOption("--region <region>", "region whose STS endpoint to call, e.g. us-east-1").option("--org <slug>").option("--external-id <id>", "STS ExternalId the role's trust policy requires").option("--session-policy <file>", "path to an inline session policy JSON (further restricts)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--access-key-id <id>", "base IAM access key id (else AWS_ACCESS_KEY_ID)").option("--secret-access-key <secret>", "base IAM secret (else AWS_SECRET_ACCESS_KEY)").action(async (options) => {
|
|
8907
|
+
target.command("add").description("register an assumable IAM role to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--role-arn <arn>", "the IAM role to assume, arn:aws:iam::<acct>:role/<name>").requiredOption("--region <region>", "region whose STS endpoint to call, e.g. us-east-1").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--external-id <id>", "STS ExternalId the role's trust policy requires").option("--session-policy <file>", "path to an inline session policy JSON (further restricts)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--access-key-id <id>", "base IAM access key id (else AWS_ACCESS_KEY_ID)").option("--secret-access-key <secret>", "base IAM secret (else AWS_SECRET_ACCESS_KEY)").action(async (options) => {
|
|
8851
8908
|
const ctx = buildContext();
|
|
8852
8909
|
const org = await resolveOrg(ctx, options.org);
|
|
8853
8910
|
const sessionPolicy = options.sessionPolicy ? readFileSync(options.sessionPolicy, "utf8").trim() : void 0;
|
|
@@ -8872,7 +8929,7 @@ function registerAwsCommands(program) {
|
|
|
8872
8929
|
console.error("\nEnsure the role trusts the admin principal, then `seekrit aws lease`:\n");
|
|
8873
8930
|
console.log(awsTrustPolicyInstructions(config));
|
|
8874
8931
|
});
|
|
8875
|
-
target.command("list").description("list AWS role targets").option("--org <slug>").action(async (options) => {
|
|
8932
|
+
target.command("list").description("list AWS role targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
8876
8933
|
const ctx = buildContext();
|
|
8877
8934
|
const org = await resolveOrg(ctx, options.org);
|
|
8878
8935
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -8882,7 +8939,7 @@ function registerAwsCommands(program) {
|
|
|
8882
8939
|
console.log(`${t.id}\t${t.name}\t${cfg.region}\t${cfg.roleArn}`);
|
|
8883
8940
|
}
|
|
8884
8941
|
});
|
|
8885
|
-
target.command("trust <targetId>").description("reprint the IAM trust-policy setup for an AWS target").option("--org <slug>").action(async (targetId, options) => {
|
|
8942
|
+
target.command("trust <targetId>").description("reprint the IAM trust-policy setup for an AWS target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (targetId, options) => {
|
|
8886
8943
|
const ctx = buildContext();
|
|
8887
8944
|
const org = await resolveOrg(ctx, options.org);
|
|
8888
8945
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -8892,13 +8949,14 @@ function registerAwsCommands(program) {
|
|
|
8892
8949
|
if (cfg.provider !== "aws") fail("not an aws target (see `seekrit pg`/`seekrit ssh`)");
|
|
8893
8950
|
console.log(awsTrustPolicyInstructions(cfg));
|
|
8894
8951
|
});
|
|
8895
|
-
target.command("rm <targetId>").description("delete an AWS role target").option("--org <slug>").action(async (targetId, options) => {
|
|
8952
|
+
target.command("rm <targetId>").description("delete an AWS role target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
|
|
8896
8953
|
const ctx = buildContext();
|
|
8897
8954
|
const org = await resolveOrg(ctx, options.org);
|
|
8955
|
+
await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
|
|
8898
8956
|
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
8899
8957
|
console.error(`deleted ${targetId}`);
|
|
8900
8958
|
});
|
|
8901
|
-
aws.command("lease <target>").description("mint short-lived AWS credentials; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 15m, 1h, 12h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
8959
|
+
aws.command("lease <target>").description("mint short-lived AWS credentials; prints ready-to-source export lines").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--ttl <duration>", "credential lifetime, e.g. 15m, 1h, 12h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
8902
8960
|
const ctx = buildContext();
|
|
8903
8961
|
const org = await resolveOrg(ctx, options.org);
|
|
8904
8962
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -8928,7 +8986,7 @@ function registerAwsCommands(program) {
|
|
|
8928
8986
|
console.log(`export AWS_REGION=${cred.region}`);
|
|
8929
8987
|
}
|
|
8930
8988
|
});
|
|
8931
|
-
aws.command("leases").description("list AWS leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
8989
|
+
aws.command("leases").description("list AWS leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
8932
8990
|
const ctx = buildContext();
|
|
8933
8991
|
const org = await resolveOrg(ctx, options.org);
|
|
8934
8992
|
const { leases } = await ctx.client.listLeases(org.id);
|
|
@@ -8937,9 +8995,10 @@ function registerAwsCommands(program) {
|
|
|
8937
8995
|
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
8938
8996
|
}
|
|
8939
8997
|
});
|
|
8940
|
-
aws.command("revoke <leaseId>").description("mark a lease revoked in the ledger (STS credentials stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
|
|
8998
|
+
aws.command("revoke <leaseId>").description("mark a lease revoked in the ledger (STS credentials stay valid until they expire)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
|
|
8941
8999
|
const ctx = buildContext();
|
|
8942
9000
|
const org = await resolveOrg(ctx, options.org);
|
|
9001
|
+
await confirmDestructive(options.yes, `Revoke lease ${leaseId}? Credentials already issued stay valid until they expire.`);
|
|
8943
9002
|
await ctx.client.revokeLease(org.id, leaseId);
|
|
8944
9003
|
console.error(`revoked ${leaseId} (issued credentials remain valid until they expire)`);
|
|
8945
9004
|
});
|
|
@@ -8960,7 +9019,7 @@ function usageLine(usage) {
|
|
|
8960
9019
|
*/
|
|
8961
9020
|
function registerBillingCommands(program) {
|
|
8962
9021
|
const billing = program.command("billing").description("plan, usage, and subscription");
|
|
8963
|
-
billing.command("show", { isDefault: true }).description("show the org's plan, what it includes, and current usage").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
9022
|
+
billing.command("show", { isDefault: true }).description("show the org's plan, what it includes, and current usage").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
8964
9023
|
const ctx = buildContext();
|
|
8965
9024
|
const ref = await resolveOrg(ctx, options.org);
|
|
8966
9025
|
const info = await ctx.client.getBilling(ref.id);
|
|
@@ -8987,7 +9046,7 @@ function registerBillingCommands(program) {
|
|
|
8987
9046
|
}
|
|
8988
9047
|
});
|
|
8989
9048
|
});
|
|
8990
|
-
billing.command("entitlements").description("list every entitlement this org resolves to").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
9049
|
+
billing.command("entitlements").description("list every entitlement this org resolves to").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
8991
9050
|
const ctx = buildContext();
|
|
8992
9051
|
const ref = await resolveOrg(ctx, options.org);
|
|
8993
9052
|
const info = await ctx.client.getBilling(ref.id);
|
|
@@ -8997,7 +9056,7 @@ function registerBillingCommands(program) {
|
|
|
8997
9056
|
col("source", (e) => e.source)
|
|
8998
9057
|
]));
|
|
8999
9058
|
});
|
|
9000
|
-
billing.command("checkout <family>").description(`start a self-serve upgrade (${VISIBLE_PLAN_FAMILY_IDS.join(" | ")}) — prints a URL`).option("--org <slug>").action(async (family, options) => {
|
|
9059
|
+
billing.command("checkout <family>").description(`start a self-serve upgrade (${VISIBLE_PLAN_FAMILY_IDS.join(" | ")}) — prints a URL`).option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (family, options) => {
|
|
9001
9060
|
if (!PLAN_FAMILY_IDS.includes(family)) fail(`unknown plan "${family}" — one of: ${PLAN_FAMILY_IDS.join(", ")}`);
|
|
9002
9061
|
const ctx = buildContext();
|
|
9003
9062
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -9005,14 +9064,14 @@ function registerBillingCommands(program) {
|
|
|
9005
9064
|
console.error("open this to complete checkout:");
|
|
9006
9065
|
console.log(url);
|
|
9007
9066
|
});
|
|
9008
|
-
billing.command("portal").description("open the billing portal (prints a URL) to manage payment and invoices").option("--org <slug>").action(async (options) => {
|
|
9067
|
+
billing.command("portal").description("open the billing portal (prints a URL) to manage payment and invoices").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
9009
9068
|
const ctx = buildContext();
|
|
9010
9069
|
const ref = await resolveOrg(ctx, options.org);
|
|
9011
9070
|
const { url } = await ctx.client.openBillingPortal(ref.id);
|
|
9012
9071
|
console.error("open this to manage billing:");
|
|
9013
9072
|
console.log(url);
|
|
9014
9073
|
});
|
|
9015
|
-
billing.command("cancel").description("cancel the subscription and drop back to the Free plan").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
|
|
9074
|
+
billing.command("cancel").description("cancel the subscription and drop back to the Free plan").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
|
|
9016
9075
|
const ctx = buildContext();
|
|
9017
9076
|
const ref = await resolveOrg(ctx, options.org);
|
|
9018
9077
|
await confirmDestructive(options.yes, `Cancel ${ref.slug}'s subscription and move it to the Free plan?`);
|
|
@@ -9055,7 +9114,7 @@ async function resolveBranchParent(ctx, opts) {
|
|
|
9055
9114
|
*/
|
|
9056
9115
|
function registerBranchCommands(program) {
|
|
9057
9116
|
const branch = program.command("branch").description("ephemeral per-PR / preview configs layered on an environment");
|
|
9058
|
-
branch.command("create <slug>").description("fork an environment into an ephemeral branch (inherits its secrets)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--from <env>", "the environment to branch").option("--name <name>", "display name (defaults to the slug)").option("--ttl <duration>", "lifetime: 12h, 7d, 2w, … or `never`", "7d").option("--no-share", "don't give the parent's other readers access to this branch").action(async (slug, options) => {
|
|
9117
|
+
branch.command("create <slug>").description("fork an environment into an ephemeral branch (inherits its secrets)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--from <env>", "the environment to branch").option("--name <name>", "display name (defaults to the slug)").option("--ttl <duration>", "lifetime: 12h, 7d, 2w, … or `never`", "7d").option("--no-share", "don't give the parent's other readers access to this branch").action(async (slug, options) => {
|
|
9059
9118
|
const ctx = buildContext();
|
|
9060
9119
|
const parent = await resolveBranchParent(ctx, {
|
|
9061
9120
|
org: options.org,
|
|
@@ -9093,7 +9152,7 @@ function registerBranchCommands(program) {
|
|
|
9093
9152
|
console.error(created.branch.expiresAt ? `expires ${created.branch.expiresAt}` : "no expiry — delete it explicitly when the PR closes");
|
|
9094
9153
|
if (grants.length > 0) console.error(`shared with ${grants.length} other reader(s)`);
|
|
9095
9154
|
});
|
|
9096
|
-
branch.command("list").alias("ls").description("list branches in an application (or of one environment)").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--env <slug>", "only branches of this environment").option("--json", "print the raw API response").action(async (options) => {
|
|
9155
|
+
branch.command("list").alias("ls").description("list branches in an application (or of one environment)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--env <slug>", "only branches of this environment").option("--json", "print the raw API response").action(async (options) => {
|
|
9097
9156
|
const ctx = buildContext();
|
|
9098
9157
|
const branches = options.env ? await (async () => {
|
|
9099
9158
|
const parent = await resolveAppEnv(ctx, options);
|
|
@@ -9109,10 +9168,11 @@ function registerBranchCommands(program) {
|
|
|
9109
9168
|
col("id", (b) => b.id)
|
|
9110
9169
|
], "no branches — create one with `seekrit branch create`"));
|
|
9111
9170
|
});
|
|
9112
|
-
branch.command("delete <slug>").alias("rm").description("tear down a branch and everything it overrode").option("--org <slug>").option("--app <slug>", "application slug (defaults to seekrit.json)").action(async (slug, options) => {
|
|
9171
|
+
branch.command("delete <slug>").alias("rm").description("tear down a branch and everything it overrode").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("-y, --yes", "skip the confirmation").action(async (slug, options) => {
|
|
9113
9172
|
const ctx = buildContext();
|
|
9114
9173
|
const app = await resolveApp(ctx, options);
|
|
9115
9174
|
const target = await resolveBranch(ctx, app, slug);
|
|
9175
|
+
await confirmDestructive(options.yes, `Tear down branch ${app.slug}#${target.slug} and every override in it?`);
|
|
9116
9176
|
await ctx.client.deleteBranch(app.orgId, target.id);
|
|
9117
9177
|
console.error(`deleted branch ${app.slug}#${target.slug}`);
|
|
9118
9178
|
});
|
|
@@ -9325,7 +9385,7 @@ function resolveServiceAccountKey(opts) {
|
|
|
9325
9385
|
function registerGcpCommands(program) {
|
|
9326
9386
|
const gcp = program.command("gcp").description("temporary GCP credentials (IAM generateAccessToken, zero-knowledge)");
|
|
9327
9387
|
const target = gcp.command("target").description("manage GCP service-account targets");
|
|
9328
|
-
target.command("add").description("register an impersonable service account to issue temporary tokens from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--service-account <email>", "the service account to impersonate, name@project.iam.gserviceaccount.com").option("--org <slug>").option("--scope <scope>", "OAuth scope to grant (repeatable; default cloud-platform)", collectList$1).option("--delegate <email>", "delegation-chain service account (repeatable)", collectList$1).option("--max-ttl <duration>", "clamp requested token lifetime, e.g. 1h").option("--key-file <path>", "source SA key JSON (else GOOGLE_APPLICATION_CREDENTIALS)").action(async (options) => {
|
|
9388
|
+
target.command("add").description("register an impersonable service account to issue temporary tokens from").requiredOption("--name <name>", "display name, e.g. prod-deploy").requiredOption("--service-account <email>", "the service account to impersonate, name@project.iam.gserviceaccount.com").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--scope <scope>", "OAuth scope to grant (repeatable; default cloud-platform)", collectList$1).option("--delegate <email>", "delegation-chain service account (repeatable)", collectList$1).option("--max-ttl <duration>", "clamp requested token lifetime, e.g. 1h").option("--key-file <path>", "source SA key JSON (else GOOGLE_APPLICATION_CREDENTIALS)").action(async (options) => {
|
|
9329
9389
|
const ctx = buildContext();
|
|
9330
9390
|
const org = await resolveOrg(ctx, options.org);
|
|
9331
9391
|
const config = {
|
|
@@ -9348,7 +9408,7 @@ function registerGcpCommands(program) {
|
|
|
9348
9408
|
console.error("\nGrant the source SA the token-creator role, then `seekrit gcp lease`:\n");
|
|
9349
9409
|
console.log(gcpSetupInstructions(config));
|
|
9350
9410
|
});
|
|
9351
|
-
target.command("list").description("list GCP service-account targets").option("--org <slug>").action(async (options) => {
|
|
9411
|
+
target.command("list").description("list GCP service-account targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
9352
9412
|
const ctx = buildContext();
|
|
9353
9413
|
const org = await resolveOrg(ctx, options.org);
|
|
9354
9414
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -9358,7 +9418,7 @@ function registerGcpCommands(program) {
|
|
|
9358
9418
|
console.log(`${t.id}\t${t.name}\t${cfg.serviceAccount}`);
|
|
9359
9419
|
}
|
|
9360
9420
|
});
|
|
9361
|
-
target.command("setup <targetId>").description("reprint the IAM setup for a GCP target").option("--org <slug>").action(async (targetId, options) => {
|
|
9421
|
+
target.command("setup <targetId>").description("reprint the IAM setup for a GCP target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (targetId, options) => {
|
|
9362
9422
|
const ctx = buildContext();
|
|
9363
9423
|
const org = await resolveOrg(ctx, options.org);
|
|
9364
9424
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -9368,13 +9428,14 @@ function registerGcpCommands(program) {
|
|
|
9368
9428
|
if (cfg.provider !== "gcp") fail("not a gcp target (see `seekrit aws`/`seekrit ssh`)");
|
|
9369
9429
|
console.log(gcpSetupInstructions(cfg));
|
|
9370
9430
|
});
|
|
9371
|
-
target.command("rm <targetId>").description("delete a GCP service-account target").option("--org <slug>").action(async (targetId, options) => {
|
|
9431
|
+
target.command("rm <targetId>").description("delete a GCP service-account target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
|
|
9372
9432
|
const ctx = buildContext();
|
|
9373
9433
|
const org = await resolveOrg(ctx, options.org);
|
|
9434
|
+
await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
|
|
9374
9435
|
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
9375
9436
|
console.error(`deleted ${targetId}`);
|
|
9376
9437
|
});
|
|
9377
|
-
gcp.command("lease <target>").description("mint a short-lived GCP access token; prints ready-to-source export lines").option("--org <slug>").option("--ttl <duration>", "token lifetime, e.g. 15m, 1h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
9438
|
+
gcp.command("lease <target>").description("mint a short-lived GCP access token; prints ready-to-source export lines").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--ttl <duration>", "token lifetime, e.g. 15m, 1h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
9378
9439
|
const ctx = buildContext();
|
|
9379
9440
|
const org = await resolveOrg(ctx, options.org);
|
|
9380
9441
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -9399,7 +9460,7 @@ function registerGcpCommands(program) {
|
|
|
9399
9460
|
console.log(`export GOOGLE_OAUTH_ACCESS_TOKEN=${cred.accessToken}`);
|
|
9400
9461
|
}
|
|
9401
9462
|
});
|
|
9402
|
-
gcp.command("leases").description("list GCP leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
9463
|
+
gcp.command("leases").description("list GCP leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
9403
9464
|
const ctx = buildContext();
|
|
9404
9465
|
const org = await resolveOrg(ctx, options.org);
|
|
9405
9466
|
const { leases } = await ctx.client.listLeases(org.id);
|
|
@@ -9408,9 +9469,10 @@ function registerGcpCommands(program) {
|
|
|
9408
9469
|
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
9409
9470
|
}
|
|
9410
9471
|
});
|
|
9411
|
-
gcp.command("revoke <leaseId>").description("mark a lease revoked in the ledger (tokens stay valid until they expire)").option("--org <slug>").action(async (leaseId, options) => {
|
|
9472
|
+
gcp.command("revoke <leaseId>").description("mark a lease revoked in the ledger (tokens stay valid until they expire)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
|
|
9412
9473
|
const ctx = buildContext();
|
|
9413
9474
|
const org = await resolveOrg(ctx, options.org);
|
|
9475
|
+
await confirmDestructive(options.yes, `Revoke lease ${leaseId}? Tokens already issued stay valid until they expire.`);
|
|
9414
9476
|
await ctx.client.revokeLease(org.id, leaseId);
|
|
9415
9477
|
console.error(`revoked ${leaseId} (issued tokens remain valid until they expire)`);
|
|
9416
9478
|
});
|
|
@@ -9424,7 +9486,7 @@ function registerGcpCommands(program) {
|
|
|
9424
9486
|
*/
|
|
9425
9487
|
function registerGroupCommands(program) {
|
|
9426
9488
|
const group = program.command("group").description("manage shared groups (reusable secret bags)");
|
|
9427
|
-
group.command("list").alias("ls").description("list shared groups in an organization").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
9489
|
+
group.command("list").alias("ls").description("list shared groups in an organization").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
9428
9490
|
const ctx = buildContext();
|
|
9429
9491
|
const ref = await resolveOrg(ctx, options.org);
|
|
9430
9492
|
const { groups } = await ctx.client.listGroups(ref.id);
|
|
@@ -9435,7 +9497,7 @@ function registerGroupCommands(program) {
|
|
|
9435
9497
|
col("id", (g) => g.id)
|
|
9436
9498
|
], "no groups — create one with `seekrit group create`"));
|
|
9437
9499
|
});
|
|
9438
|
-
group.command("show <slug>").description("show a group and the environments (value sets) it holds").option("--org <slug>").option("--json", "print the raw API response").action(async (slug, options) => {
|
|
9500
|
+
group.command("show <slug>").description("show a group and the environments (value sets) it holds").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (slug, options) => {
|
|
9439
9501
|
const ctx = buildContext();
|
|
9440
9502
|
const ref = await resolveGroup(ctx, {
|
|
9441
9503
|
org: options.org,
|
|
@@ -9461,7 +9523,7 @@ function registerGroupCommands(program) {
|
|
|
9461
9523
|
], "no environments — create one with `seekrit group env create`");
|
|
9462
9524
|
});
|
|
9463
9525
|
});
|
|
9464
|
-
group.command("create").description("create a shared group").option("--org <slug>").requiredOption("--name <name>").requiredOption("--slug <slug>").action(async (options) => {
|
|
9526
|
+
group.command("create").description("create a shared group").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
|
|
9465
9527
|
const ctx = buildContext();
|
|
9466
9528
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
9467
9529
|
const created = await ctx.client.createGroup(orgRef.id, {
|
|
@@ -9470,7 +9532,7 @@ function registerGroupCommands(program) {
|
|
|
9470
9532
|
});
|
|
9471
9533
|
console.error(`created group ${created.group.slug} (${created.group.id})`);
|
|
9472
9534
|
});
|
|
9473
|
-
group.command("rename <slug>").description("change a group's display name (the slug is permanent)").option("--org <slug>").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
|
|
9535
|
+
group.command("rename <slug>").description("change a group's display name (the slug is permanent)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "new display name").action(async (slug, options) => {
|
|
9474
9536
|
const ctx = buildContext();
|
|
9475
9537
|
const ref = await resolveGroup(ctx, {
|
|
9476
9538
|
org: options.org,
|
|
@@ -9479,7 +9541,7 @@ function registerGroupCommands(program) {
|
|
|
9479
9541
|
const { group: row } = await ctx.client.updateGroup(ref.orgId, ref.id, { name: options.name });
|
|
9480
9542
|
console.error(`renamed ${row.slug} to "${row.name}"`);
|
|
9481
9543
|
});
|
|
9482
|
-
group.command("rm <slug>").alias("delete").description("delete a group, its environments, and their secrets").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (slug, options) => {
|
|
9544
|
+
group.command("rm <slug>").alias("delete").description("delete a group, its environments, and their secrets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (slug, options) => {
|
|
9483
9545
|
const ctx = buildContext();
|
|
9484
9546
|
const ref = await resolveGroup(ctx, {
|
|
9485
9547
|
org: options.org,
|
|
@@ -9491,7 +9553,7 @@ function registerGroupCommands(program) {
|
|
|
9491
9553
|
console.error(`deleted group ${ref.slug}`);
|
|
9492
9554
|
});
|
|
9493
9555
|
const groupEnv = group.command("env").description("manage a group’s environments (per-slug value sets / variants)");
|
|
9494
|
-
groupEnv.command("list").alias("ls").description("list a group's environments").option("--org <slug>").requiredOption("--group <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
9556
|
+
groupEnv.command("list").alias("ls").description("list a group's environments").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--group <slug>", "group slug").option("--json", "print the raw API response").action(async (options) => {
|
|
9495
9557
|
const ctx = buildContext();
|
|
9496
9558
|
const ref = await resolveGroup(ctx, options);
|
|
9497
9559
|
const { environments } = await ctx.client.listGroupEnvs(ref.orgId, ref.id);
|
|
@@ -9502,7 +9564,7 @@ function registerGroupCommands(program) {
|
|
|
9502
9564
|
col("id", (e) => e.id)
|
|
9503
9565
|
], "no environments — create one with `seekrit group env create`"));
|
|
9504
9566
|
});
|
|
9505
|
-
groupEnv.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) => {
|
|
9567
|
+
groupEnv.command("create").description("create a group environment (generates its data key locally)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--group <slug>", "group slug").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
|
|
9506
9568
|
const ctx = buildContext();
|
|
9507
9569
|
const groupRef = await resolveGroup(ctx, {
|
|
9508
9570
|
org: options.org,
|
|
@@ -9523,6 +9585,224 @@ function registerGroupCommands(program) {
|
|
|
9523
9585
|
});
|
|
9524
9586
|
}
|
|
9525
9587
|
//#endregion
|
|
9588
|
+
//#region src/hermes.ts
|
|
9589
|
+
/**
|
|
9590
|
+
* `seekrit hermes` — wire seekrit into [Hermes Agent](https://hermes-agent.nousresearch.com)
|
|
9591
|
+
* as a **secret source**.
|
|
9592
|
+
*
|
|
9593
|
+
* Hermes reads credentials from `~/.hermes/.env` and the process environment, and
|
|
9594
|
+
* a *secret source* is the documented way to fill that environment from
|
|
9595
|
+
* somewhere else at startup. Implementing one is Python, and it lives in the
|
|
9596
|
+
* Python SDK (`seekrit.hermes`) rather than here — a `SecretSource` subclass and
|
|
9597
|
+
* the `register(ctx)` hook Hermes calls. This command exists for the two parts
|
|
9598
|
+
* that are not Python: the `config.yaml` block, and the plugin directory for a
|
|
9599
|
+
* Hermes install that cannot see the SDK's entry point.
|
|
9600
|
+
*
|
|
9601
|
+
* **`config.yaml` is printed, not written.** It is YAML that belongs to
|
|
9602
|
+
* somebody's agent, with comments and anchors this CLI carries no parser for;
|
|
9603
|
+
* a rewrite through a JSON round trip would delete them. The plugin directory
|
|
9604
|
+
* *is* written, because those two files are entirely ours.
|
|
9605
|
+
*
|
|
9606
|
+
* There are two sources, and which one to use is a real choice rather than a
|
|
9607
|
+
* default. `seekrit` is **bulk**: one environment, whole, nothing to enumerate.
|
|
9608
|
+
* `seekrit_refs` is **mapped**: explicit `VAR: skt://NAME` bindings, which is
|
|
9609
|
+
* what you need to rename a secret, read more than one environment, or win a
|
|
9610
|
+
* contested variable — Hermes lets a mapped claim beat a bulk one.
|
|
9611
|
+
*/
|
|
9612
|
+
/** The two source names `seekrit.hermes` registers. */
|
|
9613
|
+
const BULK_SOURCE = "seekrit";
|
|
9614
|
+
const MAPPED_SOURCE = "seekrit_refs";
|
|
9615
|
+
/** Hermes' home: `$HERMES_HOME`, else `~/.hermes`. */
|
|
9616
|
+
function hermesHome(env = process.env) {
|
|
9617
|
+
const home = env.HERMES_HOME?.trim();
|
|
9618
|
+
return home && home.length > 0 ? resolve(home) : join(homedir(), ".hermes");
|
|
9619
|
+
}
|
|
9620
|
+
const NAME_RE$1 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9621
|
+
/**
|
|
9622
|
+
* Build an `skt://` reference.
|
|
9623
|
+
*
|
|
9624
|
+
* A service token is bound to one environment, so a reference names a *token*
|
|
9625
|
+
* (by the alias its `tokens:` map gives it) and a secret in that token's
|
|
9626
|
+
* environment — not an arbitrary app and env, which nothing on the read path
|
|
9627
|
+
* could resolve.
|
|
9628
|
+
*/
|
|
9629
|
+
function reference(name, tokenAlias) {
|
|
9630
|
+
return `skt://${tokenAlias ? `${tokenAlias}/` : ""}${name}`;
|
|
9631
|
+
}
|
|
9632
|
+
/**
|
|
9633
|
+
* The `secrets:` block for `~/.hermes/config.yaml`.
|
|
9634
|
+
*
|
|
9635
|
+
* Hand-rolled rather than serialized, because the shape is fixed and small and
|
|
9636
|
+
* the output is meant to be *read* — a generic emitter would quote keys and
|
|
9637
|
+
* flow-fold lists in ways that make a config someone has to paste look foreign.
|
|
9638
|
+
*/
|
|
9639
|
+
function configBlock(options = {}) {
|
|
9640
|
+
const source = options.mapped ? MAPPED_SOURCE : BULK_SOURCE;
|
|
9641
|
+
const lines = [
|
|
9642
|
+
"secrets:",
|
|
9643
|
+
` sources: [${source}]`,
|
|
9644
|
+
` ${source}:`,
|
|
9645
|
+
" enabled: true"
|
|
9646
|
+
];
|
|
9647
|
+
if (options.tokenEnv && options.tokenEnv !== "SEEKRIT_TOKEN") lines.push(` token_env: ${options.tokenEnv}`);
|
|
9648
|
+
if (options.mapped) {
|
|
9649
|
+
const tokens = Object.entries(options.tokens ?? {});
|
|
9650
|
+
if (tokens.length > 0) {
|
|
9651
|
+
lines.push(" tokens:");
|
|
9652
|
+
for (const [alias, variable] of tokens) lines.push(` ${alias}: ${variable}`);
|
|
9653
|
+
}
|
|
9654
|
+
lines.push(" env:");
|
|
9655
|
+
const bindings = options.bindings ?? [];
|
|
9656
|
+
if (bindings.length === 0) lines.push(` OPENAI_API_KEY: ${reference("OPENAI_API_KEY")}`);
|
|
9657
|
+
else for (const b of bindings) lines.push(` ${b.variable}: ${reference(b.name, b.tokenAlias)}`);
|
|
9658
|
+
}
|
|
9659
|
+
return `${lines.join("\n")}\n`;
|
|
9660
|
+
}
|
|
9661
|
+
/** `plugin.yaml` for the directory-install path. */
|
|
9662
|
+
function pluginYaml() {
|
|
9663
|
+
return [
|
|
9664
|
+
"name: seekrit",
|
|
9665
|
+
"description: >-",
|
|
9666
|
+
" Resolve Hermes provider credentials from seekrit — end-to-end encrypted",
|
|
9667
|
+
" secrets, decrypted in this process by your own service token.",
|
|
9668
|
+
""
|
|
9669
|
+
].join("\n");
|
|
9670
|
+
}
|
|
9671
|
+
/**
|
|
9672
|
+
* `__init__.py` for the directory-install path.
|
|
9673
|
+
*
|
|
9674
|
+
* Re-exporting `register` is the whole file: the implementation lives in the
|
|
9675
|
+
* published SDK, so a plugin scaffolded once keeps up with SDK releases instead
|
|
9676
|
+
* of pinning a copy of the resolver into somebody's home directory.
|
|
9677
|
+
*/
|
|
9678
|
+
function pluginInit() {
|
|
9679
|
+
return [
|
|
9680
|
+
"\"\"\"seekrit secret sources for Hermes Agent.",
|
|
9681
|
+
"",
|
|
9682
|
+
"Hermes calls `register(ctx)` from this module. The implementation lives in",
|
|
9683
|
+
"the `seekrit` package (`pip install seekrit`), so this file stays a re-export",
|
|
9684
|
+
"and never has to be regenerated.",
|
|
9685
|
+
"\"\"\"",
|
|
9686
|
+
"",
|
|
9687
|
+
"from seekrit.hermes import register # noqa: F401",
|
|
9688
|
+
""
|
|
9689
|
+
].join("\n");
|
|
9690
|
+
}
|
|
9691
|
+
/**
|
|
9692
|
+
* Parse a `VAR=NAME` or `VAR=alias/NAME` binding for `--bind`.
|
|
9693
|
+
*
|
|
9694
|
+
* The left side is the environment variable Hermes will set; the right side is
|
|
9695
|
+
* the seekrit secret it comes from. They differ often enough — that is most of
|
|
9696
|
+
* why the mapped source exists — that inferring one from the other would be
|
|
9697
|
+
* wrong more than it was convenient.
|
|
9698
|
+
*/
|
|
9699
|
+
function parseBinding(raw) {
|
|
9700
|
+
const eq = raw.indexOf("=");
|
|
9701
|
+
if (eq <= 0) return void 0;
|
|
9702
|
+
const variable = raw.slice(0, eq).trim();
|
|
9703
|
+
const target = raw.slice(eq + 1).trim();
|
|
9704
|
+
if (!NAME_RE$1.test(variable) || target.length === 0) return void 0;
|
|
9705
|
+
const slash = target.indexOf("/");
|
|
9706
|
+
if (slash === -1) return NAME_RE$1.test(target) ? {
|
|
9707
|
+
variable,
|
|
9708
|
+
name: target
|
|
9709
|
+
} : void 0;
|
|
9710
|
+
const tokenAlias = target.slice(0, slash).trim();
|
|
9711
|
+
const name = target.slice(slash + 1).trim();
|
|
9712
|
+
if (tokenAlias.length === 0 || !NAME_RE$1.test(name)) return void 0;
|
|
9713
|
+
return {
|
|
9714
|
+
variable,
|
|
9715
|
+
name,
|
|
9716
|
+
tokenAlias
|
|
9717
|
+
};
|
|
9718
|
+
}
|
|
9719
|
+
/** Parse a `--token alias=VAR` pair. */
|
|
9720
|
+
function parseTokenAlias(raw) {
|
|
9721
|
+
const eq = raw.indexOf("=");
|
|
9722
|
+
if (eq <= 0) return void 0;
|
|
9723
|
+
const alias = raw.slice(0, eq).trim();
|
|
9724
|
+
const variable = raw.slice(eq + 1).trim();
|
|
9725
|
+
if (alias.length === 0 || !NAME_RE$1.test(variable)) return void 0;
|
|
9726
|
+
return {
|
|
9727
|
+
alias,
|
|
9728
|
+
variable
|
|
9729
|
+
};
|
|
9730
|
+
}
|
|
9731
|
+
function registerHermesCommands(program) {
|
|
9732
|
+
const hermes = program.command("hermes").description("wire seekrit into a Hermes Agent as a secret source (`seekrit hermes --help`)");
|
|
9733
|
+
hermes.command("init").description("print the Hermes config that resolves credentials from seekrit").option("--mapped", `use the ${MAPPED_SOURCE} source (explicit VAR -> secret bindings)`).option("--token-env <var>", "variable holding the service token", "SEEKRIT_TOKEN").option("--bind <VAR=NAME...>", "a mapped binding, e.g. OPENAI_API_KEY=OPENAI_KEY").option("--token <alias=VAR...>", "name a second token, e.g. billing=SEEKRIT_TOKEN_BILLING").option("--plugin-dir", "also scaffold $HERMES_HOME/plugins/seekrit (for a pip-less install)").option("--home <path>", "Hermes home (default: $HERMES_HOME or ~/.hermes)").option("--json", "machine-readable output").action((options) => {
|
|
9734
|
+
const bindings = (options.bind ?? []).map((raw) => {
|
|
9735
|
+
const parsed = parseBinding(raw);
|
|
9736
|
+
if (!parsed) fail(`--bind must be VAR=NAME or VAR=alias/NAME (got "${raw}")`);
|
|
9737
|
+
return parsed;
|
|
9738
|
+
});
|
|
9739
|
+
const tokens = {};
|
|
9740
|
+
for (const raw of options.token ?? []) {
|
|
9741
|
+
const parsed = parseTokenAlias(raw);
|
|
9742
|
+
if (!parsed) fail(`--token must be alias=VARIABLE (got "${raw}")`);
|
|
9743
|
+
tokens[parsed.alias] = parsed.variable;
|
|
9744
|
+
}
|
|
9745
|
+
for (const binding of bindings) if (binding.tokenAlias && !tokens[binding.tokenAlias]) fail(`--bind ${binding.variable} names token alias "${binding.tokenAlias}", so pass --token ${binding.tokenAlias}=<VARIABLE> too`);
|
|
9746
|
+
if (!options.mapped && (bindings.length > 0 || Object.keys(tokens).length > 0)) fail("--bind and --token describe the mapped source: pass --mapped as well");
|
|
9747
|
+
const source = options.mapped ? MAPPED_SOURCE : BULK_SOURCE;
|
|
9748
|
+
const block = configBlock({
|
|
9749
|
+
mapped: options.mapped,
|
|
9750
|
+
tokenEnv: options.tokenEnv,
|
|
9751
|
+
bindings,
|
|
9752
|
+
tokens
|
|
9753
|
+
});
|
|
9754
|
+
const home = options.home ? resolve(options.home) : hermesHome();
|
|
9755
|
+
const pluginDir = join(home, "plugins", "seekrit");
|
|
9756
|
+
const written = [];
|
|
9757
|
+
if (options.pluginDir) {
|
|
9758
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
9759
|
+
const files = [["plugin.yaml", pluginYaml()], ["__init__.py", pluginInit()]];
|
|
9760
|
+
for (const [name, contents] of files) {
|
|
9761
|
+
const path = join(pluginDir, name);
|
|
9762
|
+
writeFileSync(path, contents);
|
|
9763
|
+
written.push(path);
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9766
|
+
emit(options, {
|
|
9767
|
+
source,
|
|
9768
|
+
configPath: join(home, "config.yaml"),
|
|
9769
|
+
block,
|
|
9770
|
+
written
|
|
9771
|
+
}, () => {
|
|
9772
|
+
section("hermes secret source");
|
|
9773
|
+
printFields([
|
|
9774
|
+
["source", source],
|
|
9775
|
+
["shape", options.mapped ? "mapped (explicit bindings)" : "bulk (whole environment)"],
|
|
9776
|
+
["config", join(home, "config.yaml")],
|
|
9777
|
+
["plugin dir", options.pluginDir ? pluginDir : "not scaffolded"]
|
|
9778
|
+
]);
|
|
9779
|
+
console.log();
|
|
9780
|
+
console.log("1. Install the SDK into the environment Hermes runs in:");
|
|
9781
|
+
console.log(" pip install seekrit");
|
|
9782
|
+
console.log();
|
|
9783
|
+
console.log("2. Put the service token in ~/.hermes/.env (not config.yaml):");
|
|
9784
|
+
console.log(` ${options.tokenEnv ?? "SEEKRIT_TOKEN"}=skt_...`);
|
|
9785
|
+
console.log();
|
|
9786
|
+
console.log("3. Enable the plugin and merge this into ~/.hermes/config.yaml:");
|
|
9787
|
+
console.log(" hermes plugins enable seekrit");
|
|
9788
|
+
console.log();
|
|
9789
|
+
console.log(block.trimEnd());
|
|
9790
|
+
console.log();
|
|
9791
|
+
if (written.length > 0) {
|
|
9792
|
+
console.log("Scaffolded:");
|
|
9793
|
+
for (const path of written) console.log(` ${path}`);
|
|
9794
|
+
console.log();
|
|
9795
|
+
}
|
|
9796
|
+
console.log("The source overrides values already in .env or your shell, so a rotation wins — list a variable under `preserve_existing` when a local override is the point.");
|
|
9797
|
+
});
|
|
9798
|
+
});
|
|
9799
|
+
hermes.command("ref <name>").description("print the skt:// reference to bind a Hermes variable to").option("--token-alias <alias>", "resolve through a second token named in `tokens:`").option("--json", "machine-readable output").action((name, options) => {
|
|
9800
|
+
if (!NAME_RE$1.test(name)) fail(`not a valid secret name: "${name}" (names are [A-Za-z_][A-Za-z0-9_]*)`);
|
|
9801
|
+
const ref = reference(name, options.tokenAlias);
|
|
9802
|
+
emit(options, { reference: ref }, () => console.log(ref));
|
|
9803
|
+
});
|
|
9804
|
+
}
|
|
9805
|
+
//#endregion
|
|
9526
9806
|
//#region src/honey.ts
|
|
9527
9807
|
/**
|
|
9528
9808
|
* Honey tokens — decoy credentials that unlock nothing and alert when used.
|
|
@@ -9534,7 +9814,7 @@ function registerGroupCommands(program) {
|
|
|
9534
9814
|
*/
|
|
9535
9815
|
function registerHoneyTokenCommands(program) {
|
|
9536
9816
|
const honey = program.command("honey-token").description("plant decoy credentials that alert when anyone tries to use them");
|
|
9537
|
-
honey.command("create").description("mint a decoy credential; prints it once").requiredOption("--name <name>", "display name, e.g. legacy-ci-bait").option("--org <slug>").option("--placement <note>", "where you're planting it (echoed in the alert email)").action(async (options) => {
|
|
9817
|
+
honey.command("create").description("mint a decoy credential; prints it once").requiredOption("--name <name>", "display name, e.g. legacy-ci-bait").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--placement <note>", "where you're planting it (echoed in the alert email)").action(async (options) => {
|
|
9538
9818
|
const ctx = buildContext();
|
|
9539
9819
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
9540
9820
|
const created = await createHoneyToken();
|
|
@@ -9547,7 +9827,7 @@ function registerHoneyTokenCommands(program) {
|
|
|
9547
9827
|
console.error("decoy created — save it now, it is not stored. Plant it somewhere a thief would look, NOT anywhere your own tooling reads: a deploy script that tries it by mistake trips the alarm just as loudly. It grants nothing.");
|
|
9548
9828
|
console.log(created.token);
|
|
9549
9829
|
});
|
|
9550
|
-
honey.command("list").alias("ls").description("list decoy credentials and whether any have been tripped").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
9830
|
+
honey.command("list").alias("ls").description("list decoy credentials and whether any have been tripped").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
9551
9831
|
const ctx = buildContext();
|
|
9552
9832
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
9553
9833
|
const { honeyTokens } = await ctx.client.listHoneyTokens(orgRef.id);
|
|
@@ -9560,7 +9840,7 @@ function registerHoneyTokenCommands(program) {
|
|
|
9560
9840
|
col("id", (t) => t.id)
|
|
9561
9841
|
], "no decoys planted — create one with `seekrit honey-token create`"));
|
|
9562
9842
|
});
|
|
9563
|
-
honey.command("delete <honeyTokenId>").alias("rm").description("delete a decoy (stops it alerting)").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (honeyTokenId, options) => {
|
|
9843
|
+
honey.command("delete <honeyTokenId>").alias("rm").description("delete a decoy (stops it alerting)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (honeyTokenId, options) => {
|
|
9564
9844
|
const ctx = buildContext();
|
|
9565
9845
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
9566
9846
|
await confirmDestructive(options.yes, `Delete ${honeyTokenId}? Wherever you planted it goes back to being unwatched — pull the bait too.`);
|
|
@@ -9584,7 +9864,7 @@ function collectHeader(value, acc = {}) {
|
|
|
9584
9864
|
*/
|
|
9585
9865
|
function registerLogSinkCommands(program) {
|
|
9586
9866
|
const sink = program.command("log-sink").description("stream the audit trail to your own OTLP collector (SIEM)");
|
|
9587
|
-
sink.command("show", { isDefault: true }).description("show the configured log sink and its delivery health").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
9867
|
+
sink.command("show", { isDefault: true }).description("show the configured log sink and its delivery health").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
9588
9868
|
const ctx = buildContext();
|
|
9589
9869
|
const ref = await resolveOrg(ctx, options.org);
|
|
9590
9870
|
const { sink: config } = await ctx.client.getLogSink(ref.id);
|
|
@@ -9603,7 +9883,7 @@ function registerLogSinkCommands(program) {
|
|
|
9603
9883
|
]);
|
|
9604
9884
|
});
|
|
9605
9885
|
});
|
|
9606
|
-
sink.command("set <endpoint>").description("point the audit export at an OTLP/HTTP logs endpoint").option("--org <slug>").option("--header <name: value>", "auth header to send (repeatable; values are write-only)", collectHeader).option("--clear-headers", "send no headers at all (drops the stored ones)").option("--disabled", "save the config but stop shipping").action(async (endpoint, options) => {
|
|
9886
|
+
sink.command("set <endpoint>").description("point the audit export at an OTLP/HTTP logs endpoint").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--header <name: value>", "auth header to send (repeatable; values are write-only)", collectHeader).option("--clear-headers", "send no headers at all (drops the stored ones)").option("--disabled", "save the config but stop shipping").action(async (endpoint, options) => {
|
|
9607
9887
|
if (options.header && options.clearHeaders) fail("pass either --header or --clear-headers, not both");
|
|
9608
9888
|
const ctx = buildContext();
|
|
9609
9889
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -9615,7 +9895,7 @@ function registerLogSinkCommands(program) {
|
|
|
9615
9895
|
});
|
|
9616
9896
|
console.error(`log sink → ${config.endpoint} (${config.enabled ? "enabled" : "disabled"}) — test it with \`seekrit log-sink test\``);
|
|
9617
9897
|
});
|
|
9618
|
-
sink.command("test").description("send a probe to the configured endpoint and report the result").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
9898
|
+
sink.command("test").description("send a probe to the configured endpoint and report the result").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
9619
9899
|
const ctx = buildContext();
|
|
9620
9900
|
const ref = await resolveOrg(ctx, options.org);
|
|
9621
9901
|
const result = await ctx.client.testLogSink(ref.id);
|
|
@@ -9628,7 +9908,7 @@ function registerLogSinkCommands(program) {
|
|
|
9628
9908
|
});
|
|
9629
9909
|
if (!result.ok) process.exitCode = 1;
|
|
9630
9910
|
});
|
|
9631
|
-
sink.command("rm").alias("delete").description("stop exporting the audit trail and forget the endpoint").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (options) => {
|
|
9911
|
+
sink.command("rm").alias("delete").description("stop exporting the audit trail and forget the endpoint").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (options) => {
|
|
9632
9912
|
const ctx = buildContext();
|
|
9633
9913
|
const ref = await resolveOrg(ctx, options.org);
|
|
9634
9914
|
await confirmDestructive(options.yes, `Remove ${ref.slug}'s audit log export?`);
|
|
@@ -9762,7 +10042,7 @@ function resolveAdminUri(uri) {
|
|
|
9762
10042
|
function registerMongoCommands(program) {
|
|
9763
10043
|
const mongo = program.command("mongodb").description("temporary MongoDB credentials (createUser, zero-knowledge delivery)");
|
|
9764
10044
|
const target = mongo.command("target").description("manage MongoDB targets");
|
|
9765
|
-
target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$4, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
|
|
10045
|
+
target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$4, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
|
|
9766
10046
|
const ctx = buildContext();
|
|
9767
10047
|
const org = await resolveOrg(ctx, options.org);
|
|
9768
10048
|
const adminUri = resolveAdminUri(options.uri);
|
|
@@ -9793,7 +10073,7 @@ function registerMongoCommands(program) {
|
|
|
9793
10073
|
console.error("\nEnsure a provisioning user exists, then `seekrit mongodb lease`:\n");
|
|
9794
10074
|
console.log(mongoAdminSetupInstructions(config));
|
|
9795
10075
|
});
|
|
9796
|
-
target.command("list").description("list MongoDB targets").option("--org <slug>").action(async (options) => {
|
|
10076
|
+
target.command("list").description("list MongoDB targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
9797
10077
|
const ctx = buildContext();
|
|
9798
10078
|
const org = await resolveOrg(ctx, options.org);
|
|
9799
10079
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -9804,13 +10084,14 @@ function registerMongoCommands(program) {
|
|
|
9804
10084
|
console.log(`${t.id}\t${t.name}\t${host}\t${cfg.connection.database}\t${cfg.accessLevel ?? "readonly"}`);
|
|
9805
10085
|
}
|
|
9806
10086
|
});
|
|
9807
|
-
target.command("rm <targetId>").description("delete a MongoDB target").option("--org <slug>").action(async (targetId, options) => {
|
|
10087
|
+
target.command("rm <targetId>").description("delete a MongoDB target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
|
|
9808
10088
|
const ctx = buildContext();
|
|
9809
10089
|
const org = await resolveOrg(ctx, options.org);
|
|
10090
|
+
await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
|
|
9810
10091
|
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
9811
10092
|
console.error(`deleted ${targetId}`);
|
|
9812
10093
|
});
|
|
9813
|
-
mongo.command("lease <target>").description("mint short-lived MongoDB credentials; prints a ready-to-use connection URI").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 30m, 1h, 8h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
10094
|
+
mongo.command("lease <target>").description("mint short-lived MongoDB credentials; prints a ready-to-use connection URI").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--ttl <duration>", "credential lifetime, e.g. 30m, 1h, 8h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
9814
10095
|
const ctx = buildContext();
|
|
9815
10096
|
const org = await resolveOrg(ctx, options.org);
|
|
9816
10097
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -9829,7 +10110,7 @@ function registerMongoCommands(program) {
|
|
|
9829
10110
|
if (options.json) console.log(JSON.stringify(cred, null, 2));
|
|
9830
10111
|
else console.log(`export MONGODB_URI='${cred.uri}'`);
|
|
9831
10112
|
});
|
|
9832
|
-
mongo.command("leases").description("list MongoDB leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
10113
|
+
mongo.command("leases").description("list MongoDB leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
9833
10114
|
const ctx = buildContext();
|
|
9834
10115
|
const org = await resolveOrg(ctx, options.org);
|
|
9835
10116
|
const { leases } = await ctx.client.listLeases(org.id);
|
|
@@ -9838,9 +10119,10 @@ function registerMongoCommands(program) {
|
|
|
9838
10119
|
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
9839
10120
|
}
|
|
9840
10121
|
});
|
|
9841
|
-
mongo.command("revoke <leaseId>").description("revoke a lease now (drops the MongoDB user immediately)").option("--org <slug>").action(async (leaseId, options) => {
|
|
10122
|
+
mongo.command("revoke <leaseId>").description("revoke a lease now (drops the MongoDB user immediately)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
|
|
9842
10123
|
const ctx = buildContext();
|
|
9843
10124
|
const org = await resolveOrg(ctx, options.org);
|
|
10125
|
+
await confirmDestructive(options.yes, `Revoke lease ${leaseId} now? Its MongoDB user is dropped immediately.`);
|
|
9844
10126
|
await ctx.client.revokeLease(org.id, leaseId);
|
|
9845
10127
|
console.error(`revoked ${leaseId} (the MongoDB user has been dropped)`);
|
|
9846
10128
|
});
|
|
@@ -9922,7 +10204,7 @@ function generateUserName$1(prefix = "tmp") {
|
|
|
9922
10204
|
function registerMysqlCommands(program) {
|
|
9923
10205
|
const mysql = program.command("mysql").description("temporary MySQL/MariaDB credentials (short-lived, zero-knowledge)");
|
|
9924
10206
|
const target = mysql.command("target").description("manage provisioning targets");
|
|
9925
|
-
target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$3, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$3, []).action(async (options) => {
|
|
10207
|
+
target.command("add").description("register a MySQL/MariaDB server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "3306").requiredOption("--database <name>", "database to grant access to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--user-host <host>", "host part of created accounts ('name'@'<host>')", "%").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin mysql:// connection string (or set SEEKRIT_MYSQL_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$3, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$3, []).action(async (options) => {
|
|
9926
10208
|
const ctx = buildContext();
|
|
9927
10209
|
const org = await resolveOrg(ctx, options.org);
|
|
9928
10210
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -9964,7 +10246,7 @@ function registerMysqlCommands(program) {
|
|
|
9964
10246
|
});
|
|
9965
10247
|
console.error(`registered ${accessLevel} target ${created.name} (${created.id})`);
|
|
9966
10248
|
});
|
|
9967
|
-
target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
|
|
10249
|
+
target.command("list").description("list provisioning targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
9968
10250
|
const ctx = buildContext();
|
|
9969
10251
|
const org = await resolveOrg(ctx, options.org);
|
|
9970
10252
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -9974,13 +10256,14 @@ function registerMysqlCommands(program) {
|
|
|
9974
10256
|
console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
|
|
9975
10257
|
}
|
|
9976
10258
|
});
|
|
9977
|
-
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
|
|
10259
|
+
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
|
|
9978
10260
|
const ctx = buildContext();
|
|
9979
10261
|
const org = await resolveOrg(ctx, options.org);
|
|
10262
|
+
await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
|
|
9980
10263
|
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
9981
10264
|
console.error(`removed ${targetId}`);
|
|
9982
10265
|
});
|
|
9983
|
-
mysql.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>").option("--user <name>", "user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
|
|
10266
|
+
mysql.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--user <name>", "user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
|
|
9984
10267
|
const ctx = buildContext();
|
|
9985
10268
|
const org = await resolveOrg(ctx, options.org);
|
|
9986
10269
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -10006,7 +10289,7 @@ function registerMysqlCommands(program) {
|
|
|
10006
10289
|
}, null, 2));
|
|
10007
10290
|
else console.log(url);
|
|
10008
10291
|
});
|
|
10009
|
-
mysql.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
10292
|
+
mysql.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
10010
10293
|
const ctx = buildContext();
|
|
10011
10294
|
const org = await resolveOrg(ctx, options.org);
|
|
10012
10295
|
const { leases } = await ctx.client.listLeases(org.id);
|
|
@@ -10015,9 +10298,10 @@ function registerMysqlCommands(program) {
|
|
|
10015
10298
|
console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
|
|
10016
10299
|
}
|
|
10017
10300
|
});
|
|
10018
|
-
mysql.command("revoke <leaseId>").description("revoke a lease now (drops the user immediately)").option("--org <slug>").action(async (leaseId, options) => {
|
|
10301
|
+
mysql.command("revoke <leaseId>").description("revoke a lease now (drops the user immediately)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
|
|
10019
10302
|
const ctx = buildContext();
|
|
10020
10303
|
const org = await resolveOrg(ctx, options.org);
|
|
10304
|
+
await confirmDestructive(options.yes, `Revoke lease ${leaseId} now? Its MySQL user is dropped immediately.`);
|
|
10021
10305
|
await ctx.client.revokeLease(org.id, leaseId);
|
|
10022
10306
|
console.error(`revoked ${leaseId}`);
|
|
10023
10307
|
});
|
|
@@ -10027,106 +10311,695 @@ function collect$3(value, acc) {
|
|
|
10027
10311
|
acc.push(value);
|
|
10028
10312
|
return acc;
|
|
10029
10313
|
}
|
|
10030
|
-
//#endregion
|
|
10031
|
-
//#region src/orgs.ts
|
|
10032
10314
|
/**
|
|
10033
|
-
*
|
|
10034
|
-
*
|
|
10035
|
-
* `
|
|
10315
|
+
* Fetch + decrypt every secret in a single environment.
|
|
10316
|
+
*
|
|
10317
|
+
* `${OTHER_SECRET}` references are expanded (see `@seekrit/core`'s
|
|
10318
|
+
* `interpolate`) unless `raw` is set. Only this environment's own secrets are in
|
|
10319
|
+
* scope here — a reference to a secret inherited from a composed group is left
|
|
10320
|
+
* literal, because the group layers aren't fetched. `materializeEnv` is the
|
|
10321
|
+
* fully-layered view.
|
|
10036
10322
|
*/
|
|
10037
|
-
async function
|
|
10323
|
+
async function fetchDecryptedSecrets(ctx, orgId, envId, opts = {}) {
|
|
10324
|
+
const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
|
|
10325
|
+
const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
|
|
10326
|
+
return interpolateValues(Object.fromEntries(entries), !opts.raw).values;
|
|
10327
|
+
}
|
|
10328
|
+
/**
|
|
10329
|
+
* Expand `${OTHER_SECRET}` references in a merged variable set. A cycle becomes
|
|
10330
|
+
* the CLI's standard fatal exit — it is a config bug with no correct value to
|
|
10331
|
+
* emit. Pass `enabled: false` (`--no-interpolate`) to hand the set back as-is.
|
|
10332
|
+
*/
|
|
10333
|
+
function interpolateValues(values, enabled = true) {
|
|
10334
|
+
if (!enabled) return {
|
|
10335
|
+
values,
|
|
10336
|
+
interpolated: [],
|
|
10337
|
+
unresolvedRefs: []
|
|
10338
|
+
};
|
|
10038
10339
|
try {
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10340
|
+
const { values: expandedValues, expanded, unresolved } = interpolateSecrets(values);
|
|
10341
|
+
return {
|
|
10342
|
+
values: expandedValues,
|
|
10343
|
+
interpolated: expanded,
|
|
10344
|
+
unresolvedRefs: unresolved
|
|
10345
|
+
};
|
|
10346
|
+
} catch (err) {
|
|
10347
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
10042
10348
|
}
|
|
10043
10349
|
}
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
]);
|
|
10350
|
+
/**
|
|
10351
|
+
* Decrypt one historical version of a secret. Ciphertext is bound to
|
|
10352
|
+
* `(envId, name)` as AAD and neither changes across versions, so an old blob
|
|
10353
|
+
* opens with the environment's current data key — no special handling needed.
|
|
10354
|
+
*/
|
|
10355
|
+
async function fetchDecryptedVersion(ctx, orgId, envId, name, version) {
|
|
10356
|
+
const [dek, { versions }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecretVersions(orgId, envId, name, { limit: 200 })]);
|
|
10357
|
+
const row = versions.find((v) => v.version === version);
|
|
10358
|
+
if (!row) fail(`${name} has no version ${version} in its ${versions.length} newest versions`);
|
|
10359
|
+
return decryptSecret(dek, row.ciphertext, secretAad(envId, name));
|
|
10360
|
+
}
|
|
10361
|
+
async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
|
|
10362
|
+
const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
|
|
10363
|
+
await ctx.client.setSecret(orgId, envId, name, ciphertext);
|
|
10364
|
+
}
|
|
10365
|
+
/**
|
|
10366
|
+
* Encrypt and store many secrets into one environment. The DEK is fetched once
|
|
10367
|
+
* (so user auth prompts for the passphrase a single time, not per variable),
|
|
10368
|
+
* then each value is encrypted locally and written. Existing names are
|
|
10369
|
+
* overwritten; the result splits them into created vs. updated for a summary.
|
|
10370
|
+
* Callers validate the names first — a rejected name aborts before any write.
|
|
10371
|
+
*/
|
|
10372
|
+
async function importSecrets(ctx, orgId, envId, entries) {
|
|
10373
|
+
const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
|
|
10374
|
+
const existing = new Set(secrets.map((s) => s.name));
|
|
10375
|
+
const result = {
|
|
10376
|
+
created: [],
|
|
10377
|
+
updated: []
|
|
10378
|
+
};
|
|
10379
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
10380
|
+
const ciphertext = await encryptSecret(dek, value, secretAad(envId, name));
|
|
10381
|
+
await ctx.client.setSecret(orgId, envId, name, ciphertext);
|
|
10382
|
+
(existing.has(name) ? result.updated : result.created).push(name);
|
|
10383
|
+
}
|
|
10384
|
+
return result;
|
|
10385
|
+
}
|
|
10386
|
+
/**
|
|
10387
|
+
* Resolve the full, layered environment for a running app: composed group
|
|
10388
|
+
* secrets (lowest precedence) → the app env's own secrets → `.env` files.
|
|
10389
|
+
* Each layer's DEK is unwrapped once with the principal's private key and its
|
|
10390
|
+
* ciphertext decrypted locally. `process.env` is NOT applied here — callers
|
|
10391
|
+
* that spawn a process layer it on top so the live shell always wins.
|
|
10392
|
+
*
|
|
10393
|
+
* `${OTHER_SECRET}` references are expanded last, against the merged set, so a
|
|
10394
|
+
* reference always resolves to whichever layer won the name.
|
|
10395
|
+
*/
|
|
10396
|
+
async function materializeEnv(ctx, opts) {
|
|
10397
|
+
const query = {};
|
|
10398
|
+
if (opts.with && Object.keys(opts.with).length > 0) query.with = opts.with;
|
|
10399
|
+
if (opts.branch) query.branch = opts.branch;
|
|
10400
|
+
if (!isTokenAuth(ctx)) {
|
|
10401
|
+
if (!opts.envId) fail("specify --app and --env, or authenticate with a service token (SEEKRIT_TOKEN)");
|
|
10402
|
+
query.env = opts.envId;
|
|
10403
|
+
}
|
|
10404
|
+
const { scope, layers } = await resolveWithCache(ctx, query, opts.cache);
|
|
10405
|
+
const privateKey = await getPrivateKey(ctx);
|
|
10406
|
+
const values = {};
|
|
10407
|
+
const provenance = {};
|
|
10408
|
+
for (const layer of layers) {
|
|
10409
|
+
const dek = await unwrapDek(layer.wrappedDek, privateKey);
|
|
10410
|
+
let label;
|
|
10411
|
+
if (layer.source === "group") label = `group:${layer.groupSlug}@${layer.slug}`;
|
|
10412
|
+
else if (layer.source === "branch") label = `branch:${scope.appSlug}#${layer.slug}`;
|
|
10413
|
+
else label = `app:${scope.appSlug}/${layer.slug}`;
|
|
10414
|
+
for (const secret of layer.secrets) {
|
|
10415
|
+
values[secret.name] = await decryptSecret(dek, secret.ciphertext, secretAad(layer.environmentId, secret.name));
|
|
10416
|
+
provenance[secret.name] = label;
|
|
10417
|
+
}
|
|
10418
|
+
}
|
|
10419
|
+
const loadedEnvFiles = overlayEnvFiles(values, provenance, opts.envFiles);
|
|
10051
10420
|
return {
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10421
|
+
...interpolateValues(values, opts.interpolate !== false),
|
|
10422
|
+
provenance,
|
|
10423
|
+
scope,
|
|
10424
|
+
loadedEnvFiles
|
|
10056
10425
|
};
|
|
10057
10426
|
}
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
const
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10427
|
+
/**
|
|
10428
|
+
* Resolve, going through the last-known-good cache when one is configured.
|
|
10429
|
+
*
|
|
10430
|
+
* Always live first: the cache exists for when the call cannot land, not to
|
|
10431
|
+
* save a round trip, so a recovered network is picked up on the very next
|
|
10432
|
+
* invocation. A *refused* resolve (401/403/…) drops the entry rather than
|
|
10433
|
+
* falling back to it — otherwise revoking a token would keep working offline
|
|
10434
|
+
* until the entry aged out.
|
|
10435
|
+
*/
|
|
10436
|
+
async function resolveWithCache(ctx, query, cache) {
|
|
10437
|
+
if (!cache) return ctx.client.resolve(query);
|
|
10438
|
+
try {
|
|
10439
|
+
const response = await ctx.client.resolve(query);
|
|
10440
|
+
try {
|
|
10441
|
+
cache.write(JSON.stringify(response));
|
|
10442
|
+
} catch (err) {
|
|
10443
|
+
warn(`could not update the cache: ${errorMessage(err)}`);
|
|
10444
|
+
}
|
|
10445
|
+
return response;
|
|
10446
|
+
} catch (err) {
|
|
10447
|
+
if (!mayFallBack(err)) {
|
|
10448
|
+
cache.invalidate();
|
|
10449
|
+
throw err;
|
|
10450
|
+
}
|
|
10451
|
+
const found = cache.read();
|
|
10452
|
+
if (found.kind === "hit") {
|
|
10453
|
+
warn(`${errorMessage(err)} — using cached secrets fetched ${humanize(found.ageMs)} ago`);
|
|
10454
|
+
return JSON.parse(found.body);
|
|
10455
|
+
}
|
|
10456
|
+
if (found.kind === "expired") warn(`cached secrets are ${humanize(found.ageMs)} old, past --cache-max-age`);
|
|
10457
|
+
else if (found.kind === "unusable") warn(`ignoring the cached secrets: ${found.reason}`);
|
|
10458
|
+
throw err;
|
|
10459
|
+
}
|
|
10460
|
+
}
|
|
10461
|
+
function warn(text) {
|
|
10462
|
+
process.stderr.write(`seekrit: ${text}\n`);
|
|
10463
|
+
}
|
|
10464
|
+
function errorMessage(err) {
|
|
10465
|
+
return err instanceof Error ? err.message : String(err);
|
|
10466
|
+
}
|
|
10467
|
+
/**
|
|
10468
|
+
* Overlay `.env` files onto an existing value/provenance set (later files win).
|
|
10469
|
+
* Missing files are skipped. Returns the files that were actually loaded. Used
|
|
10470
|
+
* both by {@link materializeEnv} and by `seekrit run`'s degraded path, where
|
|
10471
|
+
* managed secrets are unavailable but `.env` should still apply.
|
|
10472
|
+
*/
|
|
10473
|
+
function overlayEnvFiles(values, provenance, envFiles) {
|
|
10474
|
+
const loaded = [];
|
|
10475
|
+
for (const file of envFiles) {
|
|
10476
|
+
if (!existsSync(file)) continue;
|
|
10477
|
+
loaded.push(file);
|
|
10478
|
+
for (const [name, value] of Object.entries(parseDotenv(readFileSync(file, "utf8")))) {
|
|
10479
|
+
values[name] = value;
|
|
10480
|
+
provenance[name] = `dotenv:${file}`;
|
|
10481
|
+
}
|
|
10482
|
+
}
|
|
10483
|
+
return loaded;
|
|
10484
|
+
}
|
|
10485
|
+
/**
|
|
10486
|
+
* Print a name → source table to stderr (never the secret values). Names whose
|
|
10487
|
+
* value had references expanded are marked, and dangling references are called
|
|
10488
|
+
* out afterwards — a typo'd `${NAME}` is otherwise invisible, since it is
|
|
10489
|
+
* deliberately passed through as literal text.
|
|
10490
|
+
*/
|
|
10491
|
+
function printExplain(provenance, refs = {}) {
|
|
10492
|
+
const interpolated = new Set(refs.interpolated ?? []);
|
|
10493
|
+
const names = Object.keys(provenance).sort();
|
|
10494
|
+
const width = names.reduce((w, n) => Math.max(w, n.length), 0);
|
|
10495
|
+
for (const name of names) {
|
|
10496
|
+
const marker = interpolated.has(name) ? " (interpolated)" : "";
|
|
10497
|
+
process.stderr.write(`${name.padEnd(width)} ${provenance[name]}${marker}\n`);
|
|
10498
|
+
}
|
|
10499
|
+
if (refs.unresolved?.length) process.stderr.write(`\nunresolved reference(s), left as literal text: ${refs.unresolved.join(", ")}\n`);
|
|
10500
|
+
}
|
|
10501
|
+
/** Plugin id and integration id declared by `@seekrit/openclaw-plugin`. */
|
|
10502
|
+
const PLUGIN_ID = "seekrit";
|
|
10503
|
+
const PLUGIN_INTEGRATION_ID = "seekrit";
|
|
10504
|
+
/** The provider alias written into `secrets.providers`. */
|
|
10505
|
+
const PROVIDER_ALIAS = "seekrit";
|
|
10506
|
+
/**
|
|
10507
|
+
* Environment the resolver needs, and nothing else.
|
|
10508
|
+
*
|
|
10509
|
+
* OpenClaw hands an exec provider an empty environment apart from this
|
|
10510
|
+
* allowlist, which is a feature: it is the difference between "the resolver can
|
|
10511
|
+
* read the credential it needs" and "the resolver inherits the whole gateway
|
|
10512
|
+
* environment". `PATH` is here because Node's own startup needs it, `HOME` for
|
|
10513
|
+
* the CLI's login session, and the `SEEKRIT_*` set is how a machine credential
|
|
10514
|
+
* reaches a process nobody gets to pass flags to.
|
|
10515
|
+
*/
|
|
10516
|
+
const PASS_ENV = [
|
|
10517
|
+
"PATH",
|
|
10518
|
+
"HOME",
|
|
10519
|
+
"USERPROFILE",
|
|
10520
|
+
"APPDATA",
|
|
10521
|
+
"LOCALAPPDATA",
|
|
10522
|
+
"TEMP",
|
|
10523
|
+
"TMP",
|
|
10524
|
+
"SYSTEMROOT",
|
|
10525
|
+
"WINDIR",
|
|
10526
|
+
"XDG_CONFIG_HOME",
|
|
10527
|
+
"XDG_CACHE_HOME",
|
|
10528
|
+
"NODE_EXTRA_CA_CERTS",
|
|
10529
|
+
"SEEKRIT_TOKEN",
|
|
10530
|
+
"SEEKRIT_CLIENT_ID",
|
|
10531
|
+
"SEEKRIT_CLIENT_SECRET",
|
|
10532
|
+
"SEEKRIT_API_URL",
|
|
10533
|
+
"SEEKRIT_ORG",
|
|
10534
|
+
"SEEKRIT_APP",
|
|
10535
|
+
"SEEKRIT_ENV",
|
|
10536
|
+
"SEEKRIT_BRANCH"
|
|
10537
|
+
];
|
|
10538
|
+
/**
|
|
10539
|
+
* Generous, because the first resolve of a cold start does real work — an M2M
|
|
10540
|
+
* token mint, a resolve, and a key unwrap — and OpenClaw fails startup rather
|
|
10541
|
+
* than degrading when this expires. The 1Password integration picks 90s for the
|
|
10542
|
+
* same reason (its CLI may prompt for biometrics); 30s is enough here because
|
|
10543
|
+
* nothing in this path is interactive.
|
|
10544
|
+
*/
|
|
10545
|
+
const TIMEOUT_MS = 3e4;
|
|
10546
|
+
/** A seekrit secret name: what `seekrit secrets set` accepts. */
|
|
10547
|
+
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10548
|
+
/** Slugs as the API spells them. */
|
|
10549
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
10550
|
+
/**
|
|
10551
|
+
* Parse an exec SecretRef id.
|
|
10552
|
+
*
|
|
10553
|
+
* Two shapes, mirroring 1Password's `op://vault/item/field` — a bare name for
|
|
10554
|
+
* the environment the credential already points at, and an explicit
|
|
10555
|
+
* `app/env/NAME` triple for a provider serving more than one environment:
|
|
10556
|
+
*
|
|
10557
|
+
* OPENAI_API_KEY
|
|
10558
|
+
* billing-api/production/STRIPE_SECRET_KEY
|
|
10559
|
+
*
|
|
10560
|
+
* A bare name is the common case and the one a service token wants, since the
|
|
10561
|
+
* token is already bound to an environment and there is nothing to disambiguate.
|
|
10562
|
+
*/
|
|
10563
|
+
function parseSecretId(id) {
|
|
10564
|
+
const trimmed = id.trim();
|
|
10565
|
+
if (trimmed.length === 0) return void 0;
|
|
10566
|
+
const parts = trimmed.split("/");
|
|
10567
|
+
if (parts.length === 1) {
|
|
10568
|
+
const [name = ""] = parts;
|
|
10569
|
+
return NAME_RE.test(name) ? { name } : void 0;
|
|
10570
|
+
}
|
|
10571
|
+
if (parts.length !== 3) return void 0;
|
|
10572
|
+
const [app = "", env = "", name = ""] = parts;
|
|
10573
|
+
if (!SLUG_RE.test(app) || !SLUG_RE.test(env) || !NAME_RE.test(name)) return void 0;
|
|
10574
|
+
return {
|
|
10575
|
+
app,
|
|
10576
|
+
env,
|
|
10577
|
+
name
|
|
10578
|
+
};
|
|
10579
|
+
}
|
|
10580
|
+
/** The (app, env) pair an id resolves against — `""` for the credential's own. */
|
|
10581
|
+
function scopeKey(id) {
|
|
10582
|
+
return id.app && id.env ? `${id.app}/${id.env}` : "";
|
|
10583
|
+
}
|
|
10584
|
+
/**
|
|
10585
|
+
* Parse a request, tolerantly in exactly one direction.
|
|
10586
|
+
*
|
|
10587
|
+
* Non-string and empty ids are dropped rather than rejected — a request whose
|
|
10588
|
+
* shape we do not recognize should still resolve the ids we do, because the
|
|
10589
|
+
* alternative fails OpenClaw's whole startup over one malformed entry. A body
|
|
10590
|
+
* that is not an object with an `ids` array is a different matter: there is
|
|
10591
|
+
* nothing to answer, so it throws.
|
|
10592
|
+
*/
|
|
10593
|
+
function parseExecRequest(input) {
|
|
10594
|
+
const parsed = JSON.parse(input);
|
|
10595
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("exec SecretRef request must be a JSON object");
|
|
10596
|
+
const ids = parsed.ids;
|
|
10597
|
+
if (!Array.isArray(ids)) throw new Error("exec SecretRef request must carry an `ids` array");
|
|
10598
|
+
return { ids: ids.filter((id) => typeof id === "string" && id.trim().length > 0) };
|
|
10599
|
+
}
|
|
10600
|
+
/** Assemble a response, omitting `errors` entirely when everything resolved. */
|
|
10601
|
+
function buildExecResponse(values, errors) {
|
|
10602
|
+
return {
|
|
10603
|
+
protocolVersion: 1,
|
|
10604
|
+
values,
|
|
10605
|
+
...Object.keys(errors).length > 0 ? { errors } : {}
|
|
10606
|
+
};
|
|
10607
|
+
}
|
|
10608
|
+
/**
|
|
10609
|
+
* Resolve every requested id, reading each distinct environment exactly once.
|
|
10610
|
+
*
|
|
10611
|
+
* A batch is normally one environment, but a provider may serve several, and
|
|
10612
|
+
* resolving per id would mean an unwrap per id. Grouping keeps a 40-variable
|
|
10613
|
+
* gateway to one round trip.
|
|
10614
|
+
*
|
|
10615
|
+
* `resolveScope` is injected so the protocol can be tested without a network,
|
|
10616
|
+
* an API, or a key.
|
|
10617
|
+
*/
|
|
10618
|
+
async function resolveIds(ids, resolveScope) {
|
|
10619
|
+
const values = {};
|
|
10620
|
+
const errors = {};
|
|
10621
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
10622
|
+
for (const id of ids) {
|
|
10623
|
+
const parsed = parseSecretId(id);
|
|
10624
|
+
if (!parsed) {
|
|
10625
|
+
errors[id] = { code: "INVALID_ID" };
|
|
10626
|
+
continue;
|
|
10627
|
+
}
|
|
10628
|
+
const key = scopeKey(parsed);
|
|
10629
|
+
const bucket = byScope.get(key) ?? {
|
|
10630
|
+
scope: parsed,
|
|
10631
|
+
ids: []
|
|
10632
|
+
};
|
|
10633
|
+
bucket.ids.push({
|
|
10634
|
+
id,
|
|
10635
|
+
name: parsed.name
|
|
10636
|
+
});
|
|
10637
|
+
byScope.set(key, bucket);
|
|
10638
|
+
}
|
|
10639
|
+
for (const { scope, ids: wanted } of byScope.values()) {
|
|
10640
|
+
let resolved;
|
|
10641
|
+
try {
|
|
10642
|
+
resolved = await resolveScope(scope);
|
|
10643
|
+
} catch (err) {
|
|
10644
|
+
const code = classify(err);
|
|
10645
|
+
for (const { id } of wanted) errors[id] = { code };
|
|
10646
|
+
continue;
|
|
10647
|
+
}
|
|
10648
|
+
for (const { id, name } of wanted) {
|
|
10649
|
+
const value = resolved[name];
|
|
10650
|
+
if (value === void 0) errors[id] = { code: "NOT_FOUND" };
|
|
10651
|
+
else values[id] = value;
|
|
10652
|
+
}
|
|
10653
|
+
}
|
|
10654
|
+
return buildExecResponse(values, errors);
|
|
10655
|
+
}
|
|
10656
|
+
/**
|
|
10657
|
+
* Bucket a failure into a code an operator can act on.
|
|
10658
|
+
*
|
|
10659
|
+
* Deliberately coarse and deliberately message-free: it reads the error's own
|
|
10660
|
+
* text to pick a bucket and then throws that text away, because the text may
|
|
10661
|
+
* quote a token.
|
|
10662
|
+
*/
|
|
10663
|
+
function classify(err) {
|
|
10664
|
+
const message = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
10665
|
+
if (/unauthor|forbidden|token|credential|passphrase|decrypt|sign in|log in|login/.test(message)) return "UNAUTHORIZED";
|
|
10666
|
+
return "UNAVAILABLE";
|
|
10667
|
+
}
|
|
10668
|
+
/** OpenClaw's config directory: `$OPENCLAW_HOME`, else `~/.openclaw`. */
|
|
10669
|
+
function openclawHome(env = process.env) {
|
|
10670
|
+
const home = env.OPENCLAW_HOME?.trim();
|
|
10671
|
+
return home && home.length > 0 ? resolve(home) : join(homedir(), ".openclaw");
|
|
10672
|
+
}
|
|
10673
|
+
/**
|
|
10674
|
+
* The provider block for an installed `@seekrit/openclaw-plugin`.
|
|
10675
|
+
*
|
|
10676
|
+
* Preferred over the exec shape below for one reason that matters on upgrade:
|
|
10677
|
+
* OpenClaw reads the command out of the plugin's manifest at every startup, so
|
|
10678
|
+
* a plugin release that moves its resolver keeps working. A copied command is a
|
|
10679
|
+
* path pinned in someone's config forever.
|
|
10680
|
+
*/
|
|
10681
|
+
function pluginProvider() {
|
|
10682
|
+
return {
|
|
10683
|
+
source: "exec",
|
|
10684
|
+
pluginIntegration: {
|
|
10685
|
+
pluginId: PLUGIN_ID,
|
|
10686
|
+
integrationId: PLUGIN_INTEGRATION_ID
|
|
10687
|
+
}
|
|
10688
|
+
};
|
|
10689
|
+
}
|
|
10690
|
+
/**
|
|
10691
|
+
* The provider block for a plain CLI install, with no plugin.
|
|
10692
|
+
*
|
|
10693
|
+
* `realpathSync` on both paths is not tidiness. OpenClaw's guard rejects a
|
|
10694
|
+
* symlinked command outright, and a Node installed by a version manager is
|
|
10695
|
+
* almost always reached through one — so resolving here is the difference
|
|
10696
|
+
* between a provider that works and a startup failure whose message points at
|
|
10697
|
+
* the config rather than at the shim.
|
|
10698
|
+
*/
|
|
10699
|
+
function execProvider(cliEntry, nodeBinary = process.execPath) {
|
|
10700
|
+
const command = realTo(nodeBinary);
|
|
10701
|
+
const entry = realTo(cliEntry);
|
|
10702
|
+
return {
|
|
10703
|
+
source: "exec",
|
|
10704
|
+
command,
|
|
10705
|
+
args: [
|
|
10706
|
+
entry,
|
|
10707
|
+
"openclaw",
|
|
10708
|
+
"resolve"
|
|
10709
|
+
],
|
|
10710
|
+
passEnv: [...PASS_ENV],
|
|
10711
|
+
jsonOnly: true,
|
|
10712
|
+
timeoutMs: TIMEOUT_MS,
|
|
10713
|
+
noOutputTimeoutMs: TIMEOUT_MS,
|
|
10714
|
+
trustedDirs: [.../* @__PURE__ */ new Set([dirname(command), dirname(entry)])]
|
|
10715
|
+
};
|
|
10716
|
+
}
|
|
10717
|
+
function realTo(path) {
|
|
10718
|
+
try {
|
|
10719
|
+
return realpathSync(path);
|
|
10720
|
+
} catch {
|
|
10721
|
+
return resolve(path);
|
|
10722
|
+
}
|
|
10723
|
+
}
|
|
10724
|
+
/** This CLI's own entry file — what `execProvider` hands to Node. */
|
|
10725
|
+
function cliEntryPath() {
|
|
10726
|
+
return fileURLToPath(new URL("index.js", import.meta.url));
|
|
10727
|
+
}
|
|
10728
|
+
/**
|
|
10729
|
+
* Add the seekrit provider to a parsed config without disturbing anything else.
|
|
10730
|
+
*
|
|
10731
|
+
* An existing `seekrit` entry that differs is left alone unless forced, for the
|
|
10732
|
+
* same reason `seekrit paperclip init` leaves an existing MCP server alone:
|
|
10733
|
+
* someone who pinned a timeout or narrowed `passEnv` did it deliberately, and
|
|
10734
|
+
* silently reverting a security narrowing is the worst kind of helpful.
|
|
10735
|
+
*/
|
|
10736
|
+
function mergeProvider(existing, provider, force) {
|
|
10737
|
+
const secrets = { ...existing.secrets ?? {} };
|
|
10738
|
+
const providers = { ...secrets.providers ?? {} };
|
|
10739
|
+
const current = providers[PROVIDER_ALIAS];
|
|
10740
|
+
if (current && !force && JSON.stringify(current) !== JSON.stringify(provider)) return {
|
|
10741
|
+
merged: existing,
|
|
10742
|
+
changed: false
|
|
10743
|
+
};
|
|
10744
|
+
if (current && JSON.stringify(current) === JSON.stringify(provider)) return {
|
|
10745
|
+
merged: existing,
|
|
10746
|
+
changed: false
|
|
10747
|
+
};
|
|
10748
|
+
providers[PROVIDER_ALIAS] = provider;
|
|
10749
|
+
return {
|
|
10750
|
+
merged: {
|
|
10751
|
+
...existing,
|
|
10752
|
+
secrets: {
|
|
10753
|
+
...secrets,
|
|
10754
|
+
providers
|
|
10755
|
+
}
|
|
10756
|
+
},
|
|
10757
|
+
changed: true
|
|
10758
|
+
};
|
|
10759
|
+
}
|
|
10760
|
+
/** A SecretRef pointing at this provider — what replaces a plaintext key. */
|
|
10761
|
+
function secretRef(id) {
|
|
10762
|
+
return {
|
|
10763
|
+
source: "exec",
|
|
10764
|
+
provider: PROVIDER_ALIAS,
|
|
10765
|
+
id
|
|
10766
|
+
};
|
|
10767
|
+
}
|
|
10768
|
+
/**
|
|
10769
|
+
* Read `openclaw.json`, or decline to.
|
|
10770
|
+
*
|
|
10771
|
+
* The file is JSON5: comments and trailing commas are legal, and a parse/write
|
|
10772
|
+
* round trip through `JSON` would delete every comment in someone's gateway
|
|
10773
|
+
* config. So a file that is not also valid strict JSON is not edited at all —
|
|
10774
|
+
* the caller prints the block to paste instead. Losing a config's comments to
|
|
10775
|
+
* be helpful is worse than asking for one paste.
|
|
10776
|
+
*/
|
|
10777
|
+
function readOpenclawConfig(path) {
|
|
10778
|
+
if (!existsSync(path)) return { config: {} };
|
|
10779
|
+
let raw;
|
|
10780
|
+
try {
|
|
10781
|
+
raw = readFileSync(path, "utf8");
|
|
10782
|
+
} catch (err) {
|
|
10783
|
+
fail(`could not read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
10784
|
+
}
|
|
10785
|
+
if (raw.trim().length === 0) return { config: {} };
|
|
10786
|
+
try {
|
|
10787
|
+
const parsed = JSON.parse(raw);
|
|
10788
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return { json5: true };
|
|
10789
|
+
return { config: parsed };
|
|
10790
|
+
} catch {
|
|
10791
|
+
return { json5: true };
|
|
10792
|
+
}
|
|
10793
|
+
}
|
|
10794
|
+
function registerOpenclawCommands(program) {
|
|
10795
|
+
const openclaw = program.command("openclaw").description("wire seekrit into OpenClaw as a SecretRef provider (`seekrit openclaw --help`)");
|
|
10796
|
+
openclaw.command("init").description("add the seekrit exec SecretRef provider to openclaw.json").option("--config <path>", "path to openclaw.json (default: $OPENCLAW_HOME/openclaw.json)").option("--plugin", "point at an installed @seekrit/openclaw-plugin (recommended)").option("--exec", "point directly at this CLI, with no plugin installed").option("--write", "write the file instead of printing the block to paste").option("--force", "replace a seekrit provider entry that already differs").option("--json", "machine-readable output").action((options) => {
|
|
10797
|
+
if (options.plugin && options.exec) fail("--plugin and --exec are mutually exclusive");
|
|
10798
|
+
const usePlugin = options.exec !== true;
|
|
10799
|
+
let provider;
|
|
10800
|
+
if (usePlugin) provider = pluginProvider();
|
|
10801
|
+
else {
|
|
10802
|
+
const entry = cliEntryPath();
|
|
10803
|
+
if (!existsSync(entry)) fail(`cannot find this CLI's entry file at ${entry} — --exec needs an installed seekrit (npm i -g @seekrit/cli), or use --plugin`);
|
|
10804
|
+
provider = execProvider(entry);
|
|
10805
|
+
}
|
|
10806
|
+
const path = options.config ? resolve(options.config) : join(openclawHome(), "openclaw.json");
|
|
10807
|
+
const read = readOpenclawConfig(path);
|
|
10808
|
+
const json5 = "json5" in read;
|
|
10809
|
+
const merged = json5 ? void 0 : mergeProvider(read.config, provider, options.force === true);
|
|
10810
|
+
const wrote = Boolean(options.write) && merged?.changed === true;
|
|
10811
|
+
if (wrote && merged) writeFileSync(path, `${JSON.stringify(merged.merged, null, 2)}\n`, { mode: 384 });
|
|
10812
|
+
emit(options, {
|
|
10813
|
+
path,
|
|
10814
|
+
provider,
|
|
10815
|
+
wrote,
|
|
10816
|
+
json5
|
|
10817
|
+
}, () => {
|
|
10818
|
+
section("openclaw secret provider");
|
|
10819
|
+
printFields([
|
|
10820
|
+
["config", path],
|
|
10821
|
+
["provider", PROVIDER_ALIAS],
|
|
10822
|
+
["shape", usePlugin ? "plugin integration" : "exec (this CLI)"],
|
|
10823
|
+
["written", wrote ? "yes" : "no"]
|
|
10824
|
+
]);
|
|
10825
|
+
console.log();
|
|
10826
|
+
if (json5) {
|
|
10827
|
+
console.log(`${path} is JSON5 (comments or trailing commas), which a rewrite here would delete.`);
|
|
10828
|
+
console.log("Merge this into it by hand:");
|
|
10829
|
+
} else if (!options.write) console.log("Merge this into your config, or re-run with --write:");
|
|
10830
|
+
else if (!wrote) console.log(`A different \`${PROVIDER_ALIAS}\` provider is already configured — left as it is. Re-run with --force to replace it.`);
|
|
10831
|
+
else console.log("Written. The block now in your config:");
|
|
10832
|
+
console.log();
|
|
10833
|
+
console.log(JSON.stringify({ secrets: { providers: { [PROVIDER_ALIAS]: provider } } }, null, 2));
|
|
10834
|
+
console.log();
|
|
10835
|
+
if (usePlugin) {
|
|
10836
|
+
console.log("Install the plugin, if you have not already:");
|
|
10837
|
+
console.log(" openclaw plugins install npm:@seekrit/openclaw-plugin");
|
|
10838
|
+
console.log(" openclaw plugins enable seekrit");
|
|
10839
|
+
console.log();
|
|
10840
|
+
}
|
|
10841
|
+
console.log("Then replace a plaintext credential with a ref, e.g.:");
|
|
10842
|
+
console.log(` ${JSON.stringify(secretRef("OPENAI_API_KEY"))}`);
|
|
10843
|
+
console.log();
|
|
10844
|
+
console.log("And check your work:");
|
|
10845
|
+
console.log(" openclaw secrets audit --check --allow-exec");
|
|
10846
|
+
console.log();
|
|
10847
|
+
console.log("Resolution is eager: OpenClaw reads every ref once at startup. After a rotation, run `openclaw secrets reload`.");
|
|
10848
|
+
});
|
|
10849
|
+
});
|
|
10850
|
+
openclaw.command("ref <name>").description("print the SecretRef object to paste in place of a plaintext credential").option("--app <slug>", "address a specific application (with --env)").option("--env <slug>", "address a specific environment (with --app)").option("--json", "machine-readable output").action((name, options) => {
|
|
10851
|
+
if (Boolean(options.app) !== Boolean(options.env)) fail("--app and --env go together (an id names both, or neither)");
|
|
10852
|
+
const id = options.app && options.env ? `${options.app}/${options.env}/${name}` : name;
|
|
10853
|
+
if (!parseSecretId(id)) fail(`not a valid SecretRef id: "${id}" (names are [A-Za-z_][A-Za-z0-9_]*)`);
|
|
10854
|
+
const ref = secretRef(id);
|
|
10855
|
+
emit(options, ref, () => console.log(JSON.stringify(ref)));
|
|
10856
|
+
});
|
|
10857
|
+
openclaw.command("resolve").description("resolve exec SecretRef ids from stdin (OpenClaw calls this; not for humans)").action(async () => {
|
|
10858
|
+
setFailThrows(true);
|
|
10859
|
+
let request;
|
|
10860
|
+
try {
|
|
10861
|
+
request = parseExecRequest(await readStdin());
|
|
10862
|
+
} catch (err) {
|
|
10863
|
+
process.stderr.write(`seekrit: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
10864
|
+
process.exitCode = 2;
|
|
10865
|
+
return;
|
|
10866
|
+
}
|
|
10867
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
10868
|
+
let ctx;
|
|
10869
|
+
const response = await resolveIds(request.ids, (scope) => {
|
|
10870
|
+
const key = scopeKey(scope);
|
|
10871
|
+
const cached = scopes.get(key);
|
|
10872
|
+
if (cached) return cached;
|
|
10873
|
+
ctx ??= buildContext();
|
|
10874
|
+
const pending = resolveEnvironment(ctx, scope);
|
|
10875
|
+
scopes.set(key, pending);
|
|
10876
|
+
return pending;
|
|
10877
|
+
});
|
|
10878
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
10879
|
+
});
|
|
10880
|
+
}
|
|
10881
|
+
/**
|
|
10882
|
+
* Resolve one environment's full variable set.
|
|
10883
|
+
*
|
|
10884
|
+
* `.env` overlays are deliberately switched off (`envFiles: []`). Everywhere
|
|
10885
|
+
* else in this CLI they are a convenience for a developer's shell; here the
|
|
10886
|
+
* working directory belongs to the OpenClaw gateway, and letting a file in it
|
|
10887
|
+
* override a gateway credential would make a stray `.env` a privilege
|
|
10888
|
+
* escalation. Reference expansion stays on — a `${OTHER_SECRET}` reference is
|
|
10889
|
+
* stored data, not a local override.
|
|
10890
|
+
*/
|
|
10891
|
+
async function resolveEnvironment(ctx, scope) {
|
|
10892
|
+
let envId;
|
|
10893
|
+
if (!isTokenAuth(ctx)) envId = (await resolveAppEnv(ctx, {
|
|
10894
|
+
app: scope.app,
|
|
10895
|
+
env: scope.env
|
|
10896
|
+
})).envId;
|
|
10897
|
+
const { values } = await materializeEnv(ctx, {
|
|
10898
|
+
envId,
|
|
10899
|
+
envFiles: []
|
|
10900
|
+
});
|
|
10901
|
+
return values;
|
|
10902
|
+
}
|
|
10903
|
+
//#endregion
|
|
10904
|
+
//#region src/orgs.ts
|
|
10905
|
+
/**
|
|
10906
|
+
* Counts for `org show`. Each list is admin-gated to a different degree, so a
|
|
10907
|
+
* member who can't read one still gets the rest: a failed call reports as
|
|
10908
|
+
* `null`, which `printFields` drops and JSON preserves as "not visible to you".
|
|
10909
|
+
*/
|
|
10910
|
+
async function countOrNull(load) {
|
|
10911
|
+
try {
|
|
10912
|
+
return await load();
|
|
10913
|
+
} catch {
|
|
10914
|
+
return null;
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
async function orgOverview(ctx, orgId) {
|
|
10918
|
+
const [apps, groups, members, tokens] = await Promise.all([
|
|
10919
|
+
countOrNull(async () => (await ctx.client.listApps(orgId)).apps.length),
|
|
10920
|
+
countOrNull(async () => (await ctx.client.listGroups(orgId)).groups.length),
|
|
10921
|
+
countOrNull(async () => (await ctx.client.listMembers(orgId)).members.length),
|
|
10922
|
+
countOrNull(async () => (await ctx.client.listTokens(orgId)).tokens.length)
|
|
10923
|
+
]);
|
|
10924
|
+
return {
|
|
10925
|
+
apps,
|
|
10926
|
+
groups,
|
|
10927
|
+
members,
|
|
10928
|
+
tokens
|
|
10929
|
+
};
|
|
10930
|
+
}
|
|
10931
|
+
function registerOrgCommands(program) {
|
|
10932
|
+
const org = program.command("org").description("manage organizations");
|
|
10933
|
+
org.command("list").alias("ls").description("list the organizations you can access").option("--json", "print the raw API response").action(async (options) => {
|
|
10934
|
+
const { orgs } = await buildContext().client.listOrgs();
|
|
10935
|
+
emit(options, { orgs }, () => printTable(orgs, [
|
|
10936
|
+
col("slug", (o) => o.slug),
|
|
10937
|
+
col("name", (o) => o.name),
|
|
10938
|
+
col("role", (o) => o.role),
|
|
10939
|
+
col("id", (o) => o.id)
|
|
10940
|
+
], "no organizations — create one with `seekrit org create`"));
|
|
10941
|
+
});
|
|
10942
|
+
org.command("show [slug]").description("show an organization and what it contains").option("--org <slug>", "organization slug (or pass it as the argument)").option("--json", "print the raw API response").action(async (slug, options) => {
|
|
10943
|
+
const ctx = buildContext();
|
|
10944
|
+
const ref = await resolveOrg(ctx, slug ?? options.org);
|
|
10945
|
+
const { org: row } = await ctx.client.getOrg(ref.id);
|
|
10946
|
+
const counts = await orgOverview(ctx, ref.id);
|
|
10947
|
+
const shown = (n) => n === null ? "—" : n;
|
|
10948
|
+
emit(options, {
|
|
10949
|
+
org: row,
|
|
10950
|
+
counts
|
|
10951
|
+
}, () => {
|
|
10952
|
+
printFields([
|
|
10953
|
+
["slug", row.slug],
|
|
10954
|
+
["name", row.name],
|
|
10955
|
+
["id", row.id],
|
|
10956
|
+
["your role", row.role],
|
|
10957
|
+
["created", row.createdAt],
|
|
10958
|
+
["applications", shown(counts.apps)],
|
|
10959
|
+
["groups", shown(counts.groups)],
|
|
10960
|
+
["members", shown(counts.members)],
|
|
10961
|
+
["service tokens", shown(counts.tokens)]
|
|
10962
|
+
]);
|
|
10963
|
+
});
|
|
10964
|
+
});
|
|
10965
|
+
org.command("create").description("create an organization (you become its owner)").requiredOption("--name <name>", "display name").requiredOption("--slug <slug>", "url-safe identifier (permanent)").action(async (options) => {
|
|
10966
|
+
const created = await buildContext().client.createOrg({
|
|
10967
|
+
name: options.name,
|
|
10968
|
+
slug: options.slug
|
|
10969
|
+
});
|
|
10970
|
+
console.error(`created org ${created.org.slug} (${created.org.id})`);
|
|
10971
|
+
});
|
|
10972
|
+
org.command("rename").description("change an organization's display name (the slug is permanent)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "new display name").action(async (options) => {
|
|
10973
|
+
const ctx = buildContext();
|
|
10974
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
10975
|
+
const { org: row } = await ctx.client.updateOrg(ref.id, { name: options.name });
|
|
10976
|
+
console.error(`renamed ${row.slug} to "${row.name}"`);
|
|
10977
|
+
});
|
|
10978
|
+
org.command("member").description("view organization members").command("list").alias("ls").description("list members, and whether each has finished key setup").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
10979
|
+
const ctx = buildContext();
|
|
10980
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
10981
|
+
const { members } = await ctx.client.listMembers(ref.id);
|
|
10982
|
+
emit(options, { members }, () => printTable(members, [
|
|
10983
|
+
col("email", (m) => m.email),
|
|
10984
|
+
col("role", (m) => m.role),
|
|
10985
|
+
col("name", (m) => m.name),
|
|
10986
|
+
col("keys", (m) => m.publicKeyJwk ? "ready" : "pending"),
|
|
10987
|
+
col("id", (m) => m.userId)
|
|
10988
|
+
], "no members"));
|
|
10989
|
+
});
|
|
10990
|
+
const invite = org.command("invite").description("manage pending invitations");
|
|
10991
|
+
invite.command("list").alias("ls").description("list outstanding invitations").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
10992
|
+
const ctx = buildContext();
|
|
10993
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
10994
|
+
const { invites } = await ctx.client.listInvites(ref.id);
|
|
10995
|
+
emit(options, { invites }, () => printTable(invites, [
|
|
10996
|
+
col("email", (i) => i.email),
|
|
10124
10997
|
col("role", (i) => i.role),
|
|
10125
10998
|
col("invited", (i) => i.createdAt),
|
|
10126
10999
|
col("id", (i) => i.id)
|
|
10127
11000
|
], "no pending invitations"));
|
|
10128
11001
|
});
|
|
10129
|
-
invite.command("add <email>").description("invite someone to the organization").option("--org <slug>").option("--role <role>", "admin | member", "member").action(async (email, options) => {
|
|
11002
|
+
invite.command("add <email>").description("invite someone to the organization").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--role <role>", "admin | member", "member").action(async (email, options) => {
|
|
10130
11003
|
if (options.role !== "admin" && options.role !== "member") fail("--role must be admin or member");
|
|
10131
11004
|
const ctx = buildContext();
|
|
10132
11005
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -10136,13 +11009,13 @@ function registerOrgCommands(program) {
|
|
|
10136
11009
|
});
|
|
10137
11010
|
console.error(`invited ${row.email} as ${row.role} (${row.id}) — they join when they first sign in`);
|
|
10138
11011
|
});
|
|
10139
|
-
invite.command("rm <inviteId>").description("rescind an invitation").option("--org <slug>").action(async (inviteId, options) => {
|
|
11012
|
+
invite.command("rm <inviteId>").description("rescind an invitation").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (inviteId, options) => {
|
|
10140
11013
|
const ctx = buildContext();
|
|
10141
11014
|
const ref = await resolveOrg(ctx, options.org);
|
|
10142
11015
|
await ctx.client.revokeInvite(ref.id, inviteId);
|
|
10143
11016
|
console.error(`${inviteId} revoked`);
|
|
10144
11017
|
});
|
|
10145
|
-
org.command("mfa").description("show or set the org-wide second-factor requirement").option("--org <slug>").option("--set <policy>", "required | optional").option("--json", "print the raw API response").action(async (options) => {
|
|
11018
|
+
org.command("mfa").description("show or set the org-wide second-factor requirement").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--set <policy>", "required | optional").option("--json", "print the raw API response").action(async (options) => {
|
|
10146
11019
|
const ctx = buildContext();
|
|
10147
11020
|
const ref = await resolveOrg(ctx, options.org);
|
|
10148
11021
|
if (options.set !== void 0 && options.set !== "required" && options.set !== "optional") fail("--set must be required or optional");
|
|
@@ -10155,7 +11028,7 @@ function registerOrgCommands(program) {
|
|
|
10155
11028
|
console.log(policy.required ? "required for all members" : "optional");
|
|
10156
11029
|
});
|
|
10157
11030
|
});
|
|
10158
|
-
org.command("tree").description("print the org's applications, environments, and groups as a tree").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
11031
|
+
org.command("tree").description("print the org's applications, environments, and groups as a tree").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
10159
11032
|
const ctx = buildContext();
|
|
10160
11033
|
const ref = await resolveOrg(ctx, options.org);
|
|
10161
11034
|
const [{ apps }, { groups }] = await Promise.all([ctx.client.listApps(ref.id), ctx.client.listGroups(ref.id)]);
|
|
@@ -11021,7 +11894,7 @@ function generateRoleName(prefix = "tmp") {
|
|
|
11021
11894
|
function registerPgCommands(program) {
|
|
11022
11895
|
const pg = program.command("pg").description("temporary Postgres credentials (short-lived, zero-knowledge)");
|
|
11023
11896
|
const target = pg.command("target").description("manage provisioning targets");
|
|
11024
|
-
target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$2, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$2, []).action(async (options) => {
|
|
11897
|
+
target.command("add").description("register a Postgres cluster to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-db").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--host <host>", "database host").option("--port <port>", "database port", "5432").requiredOption("--database <name>", "database to connect to").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--schema <name>", "schema for preset grants", "public").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin postgres:// connection string (or set SEEKRIT_PG_ADMIN_URL); wrapped locally").option("--create-statement <sql>", "custom CREATE template (repeatable)", collect$2, []).option("--revoke-statement <sql>", "custom REVOKE template (repeatable)", collect$2, []).action(async (options) => {
|
|
11025
11898
|
const ctx = buildContext();
|
|
11026
11899
|
const org = await resolveOrg(ctx, options.org);
|
|
11027
11900
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -11067,7 +11940,7 @@ function registerPgCommands(program) {
|
|
|
11067
11940
|
console.log(bootstrap);
|
|
11068
11941
|
}
|
|
11069
11942
|
});
|
|
11070
|
-
target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
|
|
11943
|
+
target.command("list").description("list provisioning targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
11071
11944
|
const ctx = buildContext();
|
|
11072
11945
|
const org = await resolveOrg(ctx, options.org);
|
|
11073
11946
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -11077,7 +11950,7 @@ function registerPgCommands(program) {
|
|
|
11077
11950
|
console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${cfg.connection.database}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
|
|
11078
11951
|
}
|
|
11079
11952
|
});
|
|
11080
|
-
target.command("setup-sql <targetId>").description("print the one-time group-role setup SQL for a preset target").option("--org <slug>").action(async (targetId, options) => {
|
|
11953
|
+
target.command("setup-sql <targetId>").description("print the one-time group-role setup SQL for a preset target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (targetId, options) => {
|
|
11081
11954
|
const ctx = buildContext();
|
|
11082
11955
|
const org = await resolveOrg(ctx, options.org);
|
|
11083
11956
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -11089,13 +11962,14 @@ function registerPgCommands(program) {
|
|
|
11089
11962
|
if (!bootstrap) fail("this is a custom target — it has no generated setup SQL");
|
|
11090
11963
|
console.log(bootstrap);
|
|
11091
11964
|
});
|
|
11092
|
-
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
|
|
11965
|
+
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
|
|
11093
11966
|
const ctx = buildContext();
|
|
11094
11967
|
const org = await resolveOrg(ctx, options.org);
|
|
11968
|
+
await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
|
|
11095
11969
|
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
11096
11970
|
console.error(`removed ${targetId}`);
|
|
11097
11971
|
});
|
|
11098
|
-
pg.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>").option("--role <name>", "role name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
|
|
11972
|
+
pg.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--role <name>", "role name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
|
|
11099
11973
|
const ctx = buildContext();
|
|
11100
11974
|
const org = await resolveOrg(ctx, options.org);
|
|
11101
11975
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -11121,15 +11995,16 @@ function registerPgCommands(program) {
|
|
|
11121
11995
|
}, null, 2));
|
|
11122
11996
|
else console.log(url);
|
|
11123
11997
|
});
|
|
11124
|
-
pg.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
11998
|
+
pg.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
11125
11999
|
const ctx = buildContext();
|
|
11126
12000
|
const org = await resolveOrg(ctx, options.org);
|
|
11127
12001
|
const { leases } = await ctx.client.listLeases(org.id);
|
|
11128
12002
|
for (const l of leases) console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
|
|
11129
12003
|
});
|
|
11130
|
-
pg.command("revoke <leaseId>").description("revoke a lease now (drops the role immediately)").option("--org <slug>").action(async (leaseId, options) => {
|
|
12004
|
+
pg.command("revoke <leaseId>").description("revoke a lease now (drops the role immediately)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
|
|
11131
12005
|
const ctx = buildContext();
|
|
11132
12006
|
const org = await resolveOrg(ctx, options.org);
|
|
12007
|
+
await confirmDestructive(options.yes, `Revoke lease ${leaseId} now? Its Postgres role is dropped immediately.`);
|
|
11133
12008
|
await ctx.client.revokeLease(org.id, leaseId);
|
|
11134
12009
|
console.error(`revoked ${leaseId}`);
|
|
11135
12010
|
});
|
|
@@ -11626,7 +12501,7 @@ function generateUserName(prefix = "tmp") {
|
|
|
11626
12501
|
function registerRedisCommands(program) {
|
|
11627
12502
|
const redis = program.command("redis").description("temporary Redis credentials (short-lived, zero-knowledge)");
|
|
11628
12503
|
const target = redis.command("target").description("manage provisioning targets");
|
|
11629
|
-
target.command("add").description("register a Redis server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-cache").option("--org <slug>").requiredOption("--host <host>", "redis host").option("--port <port>", "redis port", "6379").option("--db <index>", "logical database index (the /<n> in the URL)").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin redis:// (or rediss://) connection string (or set SEEKRIT_REDIS_ADMIN_URL); wrapped locally").option("--create-statement <cmd>", "custom SETUSER template (repeatable)", collect$1, []).option("--revoke-statement <cmd>", "custom DELUSER template (repeatable)", collect$1, []).action(async (options) => {
|
|
12504
|
+
target.command("add").description("register a Redis server to lease credentials from").requiredOption("--name <name>", "display name, e.g. prod-cache").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--host <host>", "redis host").option("--port <port>", "redis port", "6379").option("--db <index>", "logical database index (the /<n> in the URL)").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--executor <mode>", "in_do | remote", "in_do").option("--provisioner-url <url>", "customer provisioner URL (remote executor)").option("--hmac-key <base64>", "shared HMAC key for the remote executor (or set SEEKRIT_PROVISIONER_HMAC_KEY); `seekrit provisioner keygen` mints one").option("--admin-url <url>", "in_do: admin redis:// (or rediss://) connection string (or set SEEKRIT_REDIS_ADMIN_URL); wrapped locally").option("--create-statement <cmd>", "custom SETUSER template (repeatable)", collect$1, []).option("--revoke-statement <cmd>", "custom DELUSER template (repeatable)", collect$1, []).action(async (options) => {
|
|
11630
12505
|
const ctx = buildContext();
|
|
11631
12506
|
const org = await resolveOrg(ctx, options.org);
|
|
11632
12507
|
const executor = options.executor === "remote" ? "remote" : "in_do";
|
|
@@ -11667,7 +12542,7 @@ function registerRedisCommands(program) {
|
|
|
11667
12542
|
});
|
|
11668
12543
|
console.error(`registered ${accessLevel} target ${created.name} (${created.id})`);
|
|
11669
12544
|
});
|
|
11670
|
-
target.command("list").description("list provisioning targets").option("--org <slug>").action(async (options) => {
|
|
12545
|
+
target.command("list").description("list provisioning targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
11671
12546
|
const ctx = buildContext();
|
|
11672
12547
|
const org = await resolveOrg(ctx, options.org);
|
|
11673
12548
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -11678,13 +12553,14 @@ function registerRedisCommands(program) {
|
|
|
11678
12553
|
console.log(`${t.id}\t${t.name}\t${cfg.connection.host}:${cfg.connection.port}/${db}\t${cfg.accessLevel ?? "custom"}\t${cfg.executor}`);
|
|
11679
12554
|
}
|
|
11680
12555
|
});
|
|
11681
|
-
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>").action(async (targetId, options) => {
|
|
12556
|
+
target.command("rm <targetId>").description("remove a provisioning target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
|
|
11682
12557
|
const ctx = buildContext();
|
|
11683
12558
|
const org = await resolveOrg(ctx, options.org);
|
|
12559
|
+
await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
|
|
11684
12560
|
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
11685
12561
|
console.error(`removed ${targetId}`);
|
|
11686
12562
|
});
|
|
11687
|
-
redis.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>").option("--user <name>", "ACL user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
|
|
12563
|
+
redis.command("lease <target>").description("mint a temporary credential; prints a ready-to-use connection URL").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--user <name>", "ACL user name to create (default: a random tmp_ name)").option("--ttl <duration>", "lifetime, e.g. 30m, 1h, 7d", "1h").option("--json", "print the full connection as JSON").action(async (targetRef, options) => {
|
|
11688
12564
|
const ctx = buildContext();
|
|
11689
12565
|
const org = await resolveOrg(ctx, options.org);
|
|
11690
12566
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -11710,7 +12586,7 @@ function registerRedisCommands(program) {
|
|
|
11710
12586
|
}, null, 2));
|
|
11711
12587
|
else console.log(url);
|
|
11712
12588
|
});
|
|
11713
|
-
redis.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
12589
|
+
redis.command("leases").description("list leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
11714
12590
|
const ctx = buildContext();
|
|
11715
12591
|
const org = await resolveOrg(ctx, options.org);
|
|
11716
12592
|
const { leases } = await ctx.client.listLeases(org.id);
|
|
@@ -11719,9 +12595,10 @@ function registerRedisCommands(program) {
|
|
|
11719
12595
|
console.log(`${l.id}\t${l.status}\t${l.resourceRef}\texpires ${l.expiresAt}`);
|
|
11720
12596
|
}
|
|
11721
12597
|
});
|
|
11722
|
-
redis.command("revoke <leaseId>").description("revoke a lease now (deletes the ACL user immediately)").option("--org <slug>").action(async (leaseId, options) => {
|
|
12598
|
+
redis.command("revoke <leaseId>").description("revoke a lease now (deletes the ACL user immediately)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
|
|
11723
12599
|
const ctx = buildContext();
|
|
11724
12600
|
const org = await resolveOrg(ctx, options.org);
|
|
12601
|
+
await confirmDestructive(options.yes, `Revoke lease ${leaseId} now? Its Redis ACL user is deleted immediately.`);
|
|
11725
12602
|
await ctx.client.revokeLease(org.id, leaseId);
|
|
11726
12603
|
console.error(`revoked ${leaseId}`);
|
|
11727
12604
|
});
|
|
@@ -11813,7 +12690,7 @@ function buildConfig$1(kind, options) {
|
|
|
11813
12690
|
}
|
|
11814
12691
|
function registerRotationCommands(program) {
|
|
11815
12692
|
const rotation = program.command("rotation").description("managed rotation of stored secret values (scheduled, zero-knowledge)");
|
|
11816
|
-
rotation.command("enable <secretName>").description("configure rotation for an existing secret").option("--org <slug>").option("--app <slug>").option("--group <slug>", "rotate a secret in a group environment").requiredOption("--env <slug>").requiredOption("--kind <kind>", "generated | postgres | mysql | redis").requiredOption("--every <duration>", "rotation cadence, e.g. 24h, 30d").option("--username <name>", "the EXISTING database account to re-key (db kinds)").option("--target <idOrName>", "registered lease target to rotate against (db kinds)").option("--user-host <host>", "MySQL account host part (default %)").option("--length <n>", "generated value length", "32").option("--alphabet <set>", "generated kind only: alphanumeric | hex | base64url | printable", "alphanumeric").option("--now", "rotate immediately as well as on the schedule").action(async (secretName, options) => {
|
|
12693
|
+
rotation.command("enable <secretName>").description("configure rotation for an existing secret").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application slug (defaults to seekrit.json)").option("--group <slug>", "rotate a secret in a group environment").requiredOption("--env <slug>", "environment slug").requiredOption("--kind <kind>", "generated | postgres | mysql | redis").requiredOption("--every <duration>", "rotation cadence, e.g. 24h, 30d").option("--username <name>", "the EXISTING database account to re-key (db kinds)").option("--target <idOrName>", "registered lease target to rotate against (db kinds)").option("--user-host <host>", "MySQL account host part (default %)").option("--length <n>", "generated value length", "32").option("--alphabet <set>", "generated kind only: alphanumeric | hex | base64url | printable", "alphanumeric").option("--now", "rotate immediately as well as on the schedule").action(async (secretName, options) => {
|
|
11817
12694
|
const ctx = buildContext();
|
|
11818
12695
|
const target = await resolveEnvTarget(ctx, options);
|
|
11819
12696
|
const config = buildConfig$1(options.kind, options);
|
|
@@ -11843,7 +12720,7 @@ function registerRotationCommands(program) {
|
|
|
11843
12720
|
else console.error(`first rotation: ${created.nextRotateAt}`);
|
|
11844
12721
|
console.log(created.id);
|
|
11845
12722
|
});
|
|
11846
|
-
rotation.command("list").description("list rotation policies (schedules only — never values)").option("--org <slug>").option("--json", "print the full policies as JSON").action(async (options) => {
|
|
12723
|
+
rotation.command("list").description("list rotation policies (schedules only — never values)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the full policies as JSON").action(async (options) => {
|
|
11847
12724
|
const ctx = buildContext();
|
|
11848
12725
|
const org = await resolveOrg(ctx, options.org);
|
|
11849
12726
|
const { rotations } = await ctx.client.listRotations(org.id);
|
|
@@ -11857,234 +12734,48 @@ function registerRotationCommands(program) {
|
|
|
11857
12734
|
}
|
|
11858
12735
|
for (const r of rotations) console.log(rotationLine(r));
|
|
11859
12736
|
});
|
|
11860
|
-
rotation.command("show <rotationOrSecret>").description("show one policy, including the last failure if any").option("--org <slug>").action(async (ref, options) => {
|
|
12737
|
+
rotation.command("show <rotationOrSecret>").description("show one policy, including the last failure if any").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
|
|
11861
12738
|
const ctx = buildContext();
|
|
11862
12739
|
const r = await resolveRotation(ctx, (await resolveOrg(ctx, options.org)).id, ref);
|
|
11863
12740
|
console.log(JSON.stringify(r, null, 2));
|
|
11864
12741
|
});
|
|
11865
|
-
rotation.command("rotate <rotationOrSecret>").description("rotate now (the same path the scheduler uses)").option("--org <slug>").action(async (ref, options) => {
|
|
12742
|
+
rotation.command("rotate <rotationOrSecret>").description("rotate now (the same path the scheduler uses)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
|
|
11866
12743
|
const ctx = buildContext();
|
|
11867
12744
|
const org = await resolveOrg(ctx, options.org);
|
|
11868
12745
|
const r = await resolveRotation(ctx, org.id, ref);
|
|
11869
12746
|
const { version, rotatedAt } = await ctx.client.rotateSecretNow(org.id, r.id);
|
|
11870
12747
|
console.error(`rotated ${r.secretName} at ${rotatedAt} — now at version ${version}. Read it with \`seekrit secrets get ${r.secretName}\`.`);
|
|
11871
12748
|
});
|
|
11872
|
-
rotation.command("pause <rotationOrSecret>").description("stop rotating, keeping the policy").option("--org <slug>").action(async (ref, options) => {
|
|
12749
|
+
rotation.command("pause <rotationOrSecret>").description("stop rotating, keeping the policy").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
|
|
11873
12750
|
const ctx = buildContext();
|
|
11874
12751
|
const org = await resolveOrg(ctx, options.org);
|
|
11875
12752
|
const r = await resolveRotation(ctx, org.id, ref);
|
|
11876
12753
|
await ctx.client.updateRotation(org.id, r.id, { status: "paused" });
|
|
11877
12754
|
console.error(`paused rotation of ${r.secretName}`);
|
|
11878
12755
|
});
|
|
11879
|
-
rotation.command("resume <rotationOrSecret>").description("resume rotating (also clears a failed streak)").option("--org <slug>").action(async (ref, options) => {
|
|
12756
|
+
rotation.command("resume <rotationOrSecret>").description("resume rotating (also clears a failed streak)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
|
|
11880
12757
|
const ctx = buildContext();
|
|
11881
12758
|
const org = await resolveOrg(ctx, options.org);
|
|
11882
12759
|
const r = await resolveRotation(ctx, org.id, ref);
|
|
11883
12760
|
const { rotation: updated } = await ctx.client.updateRotation(org.id, r.id, { status: "active" });
|
|
11884
12761
|
console.error(`resumed rotation of ${r.secretName} — next ${updated.nextRotateAt}`);
|
|
11885
12762
|
});
|
|
11886
|
-
rotation.command("set-interval <rotationOrSecret>").description("change the cadence").requiredOption("--every <duration>", "new cadence, e.g. 24h, 30d").option("--org <slug>").action(async (ref, options) => {
|
|
12763
|
+
rotation.command("set-interval <rotationOrSecret>").description("change the cadence").requiredOption("--every <duration>", "new cadence, e.g. 24h, 30d").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (ref, options) => {
|
|
11887
12764
|
const ctx = buildContext();
|
|
11888
12765
|
const org = await resolveOrg(ctx, options.org);
|
|
11889
12766
|
const r = await resolveRotation(ctx, org.id, ref);
|
|
11890
12767
|
const { rotation: updated } = await ctx.client.updateRotation(org.id, r.id, { intervalSeconds: parseDurationSeconds(options.every, "--every") });
|
|
11891
12768
|
console.error(`${updated.secretName} now rotates every ${formatInterval(updated.intervalSeconds)} — next ${updated.nextRotateAt}`);
|
|
11892
12769
|
});
|
|
11893
|
-
rotation.command("disable <rotationOrSecret>").description("stop rotating and remove the policy (the secret is untouched)").option("--org <slug>").action(async (ref, options) => {
|
|
12770
|
+
rotation.command("disable <rotationOrSecret>").description("stop rotating and remove the policy (the secret is untouched)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (ref, options) => {
|
|
11894
12771
|
const ctx = buildContext();
|
|
11895
12772
|
const org = await resolveOrg(ctx, options.org);
|
|
11896
12773
|
const r = await resolveRotation(ctx, org.id, ref);
|
|
12774
|
+
await confirmDestructive(options.yes, `Stop rotating ${r.secretName} and delete its policy? The current value stays as it is.`);
|
|
11897
12775
|
const { rotatorRevoked } = await ctx.client.disableRotation(org.id, r.id);
|
|
11898
12776
|
console.error(`disabled rotation of ${r.secretName}${rotatorRevoked ? " — rotator key access revoked for this environment" : ""}`);
|
|
11899
12777
|
});
|
|
11900
12778
|
}
|
|
11901
|
-
/**
|
|
11902
|
-
* Fetch + decrypt every secret in a single environment.
|
|
11903
|
-
*
|
|
11904
|
-
* `${OTHER_SECRET}` references are expanded (see `@seekrit/core`'s
|
|
11905
|
-
* `interpolate`) unless `raw` is set. Only this environment's own secrets are in
|
|
11906
|
-
* scope here — a reference to a secret inherited from a composed group is left
|
|
11907
|
-
* literal, because the group layers aren't fetched. `materializeEnv` is the
|
|
11908
|
-
* fully-layered view.
|
|
11909
|
-
*/
|
|
11910
|
-
async function fetchDecryptedSecrets(ctx, orgId, envId, opts = {}) {
|
|
11911
|
-
const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
|
|
11912
|
-
const entries = await Promise.all(secrets.map(async (secret) => [secret.name, await decryptSecret(dek, secret.ciphertext, secretAad(envId, secret.name))]));
|
|
11913
|
-
return interpolateValues(Object.fromEntries(entries), !opts.raw).values;
|
|
11914
|
-
}
|
|
11915
|
-
/**
|
|
11916
|
-
* Expand `${OTHER_SECRET}` references in a merged variable set. A cycle becomes
|
|
11917
|
-
* the CLI's standard fatal exit — it is a config bug with no correct value to
|
|
11918
|
-
* emit. Pass `enabled: false` (`--no-interpolate`) to hand the set back as-is.
|
|
11919
|
-
*/
|
|
11920
|
-
function interpolateValues(values, enabled = true) {
|
|
11921
|
-
if (!enabled) return {
|
|
11922
|
-
values,
|
|
11923
|
-
interpolated: [],
|
|
11924
|
-
unresolvedRefs: []
|
|
11925
|
-
};
|
|
11926
|
-
try {
|
|
11927
|
-
const { values: expandedValues, expanded, unresolved } = interpolateSecrets(values);
|
|
11928
|
-
return {
|
|
11929
|
-
values: expandedValues,
|
|
11930
|
-
interpolated: expanded,
|
|
11931
|
-
unresolvedRefs: unresolved
|
|
11932
|
-
};
|
|
11933
|
-
} catch (err) {
|
|
11934
|
-
return fail(err instanceof Error ? err.message : String(err));
|
|
11935
|
-
}
|
|
11936
|
-
}
|
|
11937
|
-
/**
|
|
11938
|
-
* Decrypt one historical version of a secret. Ciphertext is bound to
|
|
11939
|
-
* `(envId, name)` as AAD and neither changes across versions, so an old blob
|
|
11940
|
-
* opens with the environment's current data key — no special handling needed.
|
|
11941
|
-
*/
|
|
11942
|
-
async function fetchDecryptedVersion(ctx, orgId, envId, name, version) {
|
|
11943
|
-
const [dek, { versions }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecretVersions(orgId, envId, name, { limit: 200 })]);
|
|
11944
|
-
const row = versions.find((v) => v.version === version);
|
|
11945
|
-
if (!row) fail(`${name} has no version ${version} in its ${versions.length} newest versions`);
|
|
11946
|
-
return decryptSecret(dek, row.ciphertext, secretAad(envId, name));
|
|
11947
|
-
}
|
|
11948
|
-
async function encryptAndSetSecret(ctx, orgId, envId, name, value) {
|
|
11949
|
-
const ciphertext = await encryptSecret(await getDek(ctx, orgId, envId), value, secretAad(envId, name));
|
|
11950
|
-
await ctx.client.setSecret(orgId, envId, name, ciphertext);
|
|
11951
|
-
}
|
|
11952
|
-
/**
|
|
11953
|
-
* Encrypt and store many secrets into one environment. The DEK is fetched once
|
|
11954
|
-
* (so user auth prompts for the passphrase a single time, not per variable),
|
|
11955
|
-
* then each value is encrypted locally and written. Existing names are
|
|
11956
|
-
* overwritten; the result splits them into created vs. updated for a summary.
|
|
11957
|
-
* Callers validate the names first — a rejected name aborts before any write.
|
|
11958
|
-
*/
|
|
11959
|
-
async function importSecrets(ctx, orgId, envId, entries) {
|
|
11960
|
-
const [dek, { secrets }] = await Promise.all([getDek(ctx, orgId, envId), ctx.client.listSecrets(orgId, envId)]);
|
|
11961
|
-
const existing = new Set(secrets.map((s) => s.name));
|
|
11962
|
-
const result = {
|
|
11963
|
-
created: [],
|
|
11964
|
-
updated: []
|
|
11965
|
-
};
|
|
11966
|
-
for (const [name, value] of Object.entries(entries)) {
|
|
11967
|
-
const ciphertext = await encryptSecret(dek, value, secretAad(envId, name));
|
|
11968
|
-
await ctx.client.setSecret(orgId, envId, name, ciphertext);
|
|
11969
|
-
(existing.has(name) ? result.updated : result.created).push(name);
|
|
11970
|
-
}
|
|
11971
|
-
return result;
|
|
11972
|
-
}
|
|
11973
|
-
/**
|
|
11974
|
-
* Resolve the full, layered environment for a running app: composed group
|
|
11975
|
-
* secrets (lowest precedence) → the app env's own secrets → `.env` files.
|
|
11976
|
-
* Each layer's DEK is unwrapped once with the principal's private key and its
|
|
11977
|
-
* ciphertext decrypted locally. `process.env` is NOT applied here — callers
|
|
11978
|
-
* that spawn a process layer it on top so the live shell always wins.
|
|
11979
|
-
*
|
|
11980
|
-
* `${OTHER_SECRET}` references are expanded last, against the merged set, so a
|
|
11981
|
-
* reference always resolves to whichever layer won the name.
|
|
11982
|
-
*/
|
|
11983
|
-
async function materializeEnv(ctx, opts) {
|
|
11984
|
-
const query = {};
|
|
11985
|
-
if (opts.with && Object.keys(opts.with).length > 0) query.with = opts.with;
|
|
11986
|
-
if (opts.branch) query.branch = opts.branch;
|
|
11987
|
-
if (!isTokenAuth(ctx)) {
|
|
11988
|
-
if (!opts.envId) fail("specify --app and --env, or authenticate with a service token (SEEKRIT_TOKEN)");
|
|
11989
|
-
query.env = opts.envId;
|
|
11990
|
-
}
|
|
11991
|
-
const { scope, layers } = await resolveWithCache(ctx, query, opts.cache);
|
|
11992
|
-
const privateKey = await getPrivateKey(ctx);
|
|
11993
|
-
const values = {};
|
|
11994
|
-
const provenance = {};
|
|
11995
|
-
for (const layer of layers) {
|
|
11996
|
-
const dek = await unwrapDek(layer.wrappedDek, privateKey);
|
|
11997
|
-
let label;
|
|
11998
|
-
if (layer.source === "group") label = `group:${layer.groupSlug}@${layer.slug}`;
|
|
11999
|
-
else if (layer.source === "branch") label = `branch:${scope.appSlug}#${layer.slug}`;
|
|
12000
|
-
else label = `app:${scope.appSlug}/${layer.slug}`;
|
|
12001
|
-
for (const secret of layer.secrets) {
|
|
12002
|
-
values[secret.name] = await decryptSecret(dek, secret.ciphertext, secretAad(layer.environmentId, secret.name));
|
|
12003
|
-
provenance[secret.name] = label;
|
|
12004
|
-
}
|
|
12005
|
-
}
|
|
12006
|
-
const loadedEnvFiles = overlayEnvFiles(values, provenance, opts.envFiles);
|
|
12007
|
-
return {
|
|
12008
|
-
...interpolateValues(values, opts.interpolate !== false),
|
|
12009
|
-
provenance,
|
|
12010
|
-
scope,
|
|
12011
|
-
loadedEnvFiles
|
|
12012
|
-
};
|
|
12013
|
-
}
|
|
12014
|
-
/**
|
|
12015
|
-
* Resolve, going through the last-known-good cache when one is configured.
|
|
12016
|
-
*
|
|
12017
|
-
* Always live first: the cache exists for when the call cannot land, not to
|
|
12018
|
-
* save a round trip, so a recovered network is picked up on the very next
|
|
12019
|
-
* invocation. A *refused* resolve (401/403/…) drops the entry rather than
|
|
12020
|
-
* falling back to it — otherwise revoking a token would keep working offline
|
|
12021
|
-
* until the entry aged out.
|
|
12022
|
-
*/
|
|
12023
|
-
async function resolveWithCache(ctx, query, cache) {
|
|
12024
|
-
if (!cache) return ctx.client.resolve(query);
|
|
12025
|
-
try {
|
|
12026
|
-
const response = await ctx.client.resolve(query);
|
|
12027
|
-
try {
|
|
12028
|
-
cache.write(JSON.stringify(response));
|
|
12029
|
-
} catch (err) {
|
|
12030
|
-
warn(`could not update the cache: ${errorMessage(err)}`);
|
|
12031
|
-
}
|
|
12032
|
-
return response;
|
|
12033
|
-
} catch (err) {
|
|
12034
|
-
if (!mayFallBack(err)) {
|
|
12035
|
-
cache.invalidate();
|
|
12036
|
-
throw err;
|
|
12037
|
-
}
|
|
12038
|
-
const found = cache.read();
|
|
12039
|
-
if (found.kind === "hit") {
|
|
12040
|
-
warn(`${errorMessage(err)} — using cached secrets fetched ${humanize(found.ageMs)} ago`);
|
|
12041
|
-
return JSON.parse(found.body);
|
|
12042
|
-
}
|
|
12043
|
-
if (found.kind === "expired") warn(`cached secrets are ${humanize(found.ageMs)} old, past --cache-max-age`);
|
|
12044
|
-
else if (found.kind === "unusable") warn(`ignoring the cached secrets: ${found.reason}`);
|
|
12045
|
-
throw err;
|
|
12046
|
-
}
|
|
12047
|
-
}
|
|
12048
|
-
function warn(text) {
|
|
12049
|
-
process.stderr.write(`seekrit: ${text}\n`);
|
|
12050
|
-
}
|
|
12051
|
-
function errorMessage(err) {
|
|
12052
|
-
return err instanceof Error ? err.message : String(err);
|
|
12053
|
-
}
|
|
12054
|
-
/**
|
|
12055
|
-
* Overlay `.env` files onto an existing value/provenance set (later files win).
|
|
12056
|
-
* Missing files are skipped. Returns the files that were actually loaded. Used
|
|
12057
|
-
* both by {@link materializeEnv} and by `seekrit run`'s degraded path, where
|
|
12058
|
-
* managed secrets are unavailable but `.env` should still apply.
|
|
12059
|
-
*/
|
|
12060
|
-
function overlayEnvFiles(values, provenance, envFiles) {
|
|
12061
|
-
const loaded = [];
|
|
12062
|
-
for (const file of envFiles) {
|
|
12063
|
-
if (!existsSync(file)) continue;
|
|
12064
|
-
loaded.push(file);
|
|
12065
|
-
for (const [name, value] of Object.entries(parseDotenv(readFileSync(file, "utf8")))) {
|
|
12066
|
-
values[name] = value;
|
|
12067
|
-
provenance[name] = `dotenv:${file}`;
|
|
12068
|
-
}
|
|
12069
|
-
}
|
|
12070
|
-
return loaded;
|
|
12071
|
-
}
|
|
12072
|
-
/**
|
|
12073
|
-
* Print a name → source table to stderr (never the secret values). Names whose
|
|
12074
|
-
* value had references expanded are marked, and dangling references are called
|
|
12075
|
-
* out afterwards — a typo'd `${NAME}` is otherwise invisible, since it is
|
|
12076
|
-
* deliberately passed through as literal text.
|
|
12077
|
-
*/
|
|
12078
|
-
function printExplain(provenance, refs = {}) {
|
|
12079
|
-
const interpolated = new Set(refs.interpolated ?? []);
|
|
12080
|
-
const names = Object.keys(provenance).sort();
|
|
12081
|
-
const width = names.reduce((w, n) => Math.max(w, n.length), 0);
|
|
12082
|
-
for (const name of names) {
|
|
12083
|
-
const marker = interpolated.has(name) ? " (interpolated)" : "";
|
|
12084
|
-
process.stderr.write(`${name.padEnd(width)} ${provenance[name]}${marker}\n`);
|
|
12085
|
-
}
|
|
12086
|
-
if (refs.unresolved?.length) process.stderr.write(`\nunresolved reference(s), left as literal text: ${refs.unresolved.join(", ")}\n`);
|
|
12087
|
-
}
|
|
12088
12779
|
//#endregion
|
|
12089
12780
|
//#region src/ssh.ts
|
|
12090
12781
|
/**
|
|
@@ -12112,7 +12803,7 @@ function parseTtlSeconds(input) {
|
|
|
12112
12803
|
function registerSshCommands(program) {
|
|
12113
12804
|
const ssh = program.command("ssh").description("temporary SSH access (short-lived certificates, zero-knowledge)");
|
|
12114
12805
|
const target = ssh.command("target").description("manage SSH CA targets");
|
|
12115
|
-
target.command("add").description("create an SSH certificate authority to issue certs from").requiredOption("--name <name>", "display name, e.g. prod-fleet").option("--org <slug>").option("--host <host>", "default host the printed ssh command connects to").option("--user <login>", "default login user (a cert principal)").option("--principal <name>", "allow-list a principal certs may request (repeatable)", collect, []).option("--extension <name>", "cert extension to grant, e.g. permit-pty (repeatable)", collect, []).option("--max-ttl <duration>", "clamp requested cert lifetime, e.g. 8h").action(async (options) => {
|
|
12806
|
+
target.command("add").description("create an SSH certificate authority to issue certs from").requiredOption("--name <name>", "display name, e.g. prod-fleet").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--host <host>", "default host the printed ssh command connects to").option("--user <login>", "default login user (a cert principal)").option("--principal <name>", "allow-list a principal certs may request (repeatable)", collect, []).option("--extension <name>", "cert extension to grant, e.g. permit-pty (repeatable)", collect, []).option("--max-ttl <duration>", "clamp requested cert lifetime, e.g. 8h").action(async (options) => {
|
|
12116
12807
|
const ctx = buildContext();
|
|
12117
12808
|
const org = await resolveOrg(ctx, options.org);
|
|
12118
12809
|
const ca = await generateSshCaKeyPair(`seekrit-ca:${options.name}`);
|
|
@@ -12139,7 +12830,7 @@ function registerSshCommands(program) {
|
|
|
12139
12830
|
console.error("\nInstall the CA on your hosts, then issue certs with `seekrit ssh lease`:\n");
|
|
12140
12831
|
console.log(sshHostSetupInstructions(config));
|
|
12141
12832
|
});
|
|
12142
|
-
target.command("list").description("list SSH CA targets").option("--org <slug>").action(async (options) => {
|
|
12833
|
+
target.command("list").description("list SSH CA targets").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
12143
12834
|
const ctx = buildContext();
|
|
12144
12835
|
const org = await resolveOrg(ctx, options.org);
|
|
12145
12836
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -12151,7 +12842,7 @@ function registerSshCommands(program) {
|
|
|
12151
12842
|
console.log(`${t.id}\t${t.name}\t${where}\tprincipals=${principals}`);
|
|
12152
12843
|
}
|
|
12153
12844
|
});
|
|
12154
|
-
target.command("setup <targetId>").description("reprint the one-time host setup for an SSH target").option("--org <slug>").action(async (targetId, options) => {
|
|
12845
|
+
target.command("setup <targetId>").description("reprint the one-time host setup for an SSH target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (targetId, options) => {
|
|
12155
12846
|
const ctx = buildContext();
|
|
12156
12847
|
const org = await resolveOrg(ctx, options.org);
|
|
12157
12848
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -12161,13 +12852,14 @@ function registerSshCommands(program) {
|
|
|
12161
12852
|
if (cfg.provider !== "ssh") fail("not an ssh target (see `seekrit pg`)");
|
|
12162
12853
|
console.log(sshHostSetupInstructions(cfg));
|
|
12163
12854
|
});
|
|
12164
|
-
target.command("rm <targetId>").description("delete an SSH CA target").option("--org <slug>").action(async (targetId, options) => {
|
|
12855
|
+
target.command("rm <targetId>").description("delete an SSH CA target").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (targetId, options) => {
|
|
12165
12856
|
const ctx = buildContext();
|
|
12166
12857
|
const org = await resolveOrg(ctx, options.org);
|
|
12858
|
+
await confirmDestructive(options.yes, `Remove target ${targetId}? New leases against it stop working.`);
|
|
12167
12859
|
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
12168
12860
|
console.error(`deleted ${targetId}`);
|
|
12169
12861
|
});
|
|
12170
|
-
ssh.command("lease <target>").description("mint a short-lived SSH certificate; prints a ready-to-run ssh command").option("--org <slug>").option("--principal <name>", "login user to request (repeatable; default from target)", collect, []).option("--ttl <duration>", "certificate lifetime, e.g. 30m, 1h, 8h", "1h").option("--out <dir>", "directory to write the key + cert (default: a temp dir)").option("--json", "print key/cert paths and the certificate as JSON").action(async (targetRef, options) => {
|
|
12862
|
+
ssh.command("lease <target>").description("mint a short-lived SSH certificate; prints a ready-to-run ssh command").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--principal <name>", "login user to request (repeatable; default from target)", collect, []).option("--ttl <duration>", "certificate lifetime, e.g. 30m, 1h, 8h", "1h").option("--out <dir>", "directory to write the key + cert (default: a temp dir)").option("--json", "print key/cert paths and the certificate as JSON").action(async (targetRef, options) => {
|
|
12171
12863
|
const ctx = buildContext();
|
|
12172
12864
|
const org = await resolveOrg(ctx, options.org);
|
|
12173
12865
|
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
@@ -12205,7 +12897,7 @@ function registerSshCommands(program) {
|
|
|
12205
12897
|
}, null, 2));
|
|
12206
12898
|
else console.log(command);
|
|
12207
12899
|
});
|
|
12208
|
-
ssh.command("leases").description("list SSH leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
12900
|
+
ssh.command("leases").description("list SSH leases (the ledger — never secret material)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (options) => {
|
|
12209
12901
|
const ctx = buildContext();
|
|
12210
12902
|
const org = await resolveOrg(ctx, options.org);
|
|
12211
12903
|
const { leases } = await ctx.client.listLeases(org.id);
|
|
@@ -12214,9 +12906,10 @@ function registerSshCommands(program) {
|
|
|
12214
12906
|
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
12215
12907
|
}
|
|
12216
12908
|
});
|
|
12217
|
-
ssh.command("revoke <leaseId>").description("mark a lease revoked in the ledger (the cert stays valid until it expires)").option("--org <slug>").action(async (leaseId, options) => {
|
|
12909
|
+
ssh.command("revoke <leaseId>").description("mark a lease revoked in the ledger (the cert stays valid until it expires)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (leaseId, options) => {
|
|
12218
12910
|
const ctx = buildContext();
|
|
12219
12911
|
const org = await resolveOrg(ctx, options.org);
|
|
12912
|
+
await confirmDestructive(options.yes, `Revoke lease ${leaseId}? Certificates already issued stay valid until they expire.`);
|
|
12220
12913
|
await ctx.client.revokeLease(org.id, leaseId);
|
|
12221
12914
|
console.error(`revoked ${leaseId} (issued certs remain valid until they expire)`);
|
|
12222
12915
|
});
|
|
@@ -12732,7 +13425,7 @@ async function resolveConnection(ctx, orgId, ref) {
|
|
|
12732
13425
|
}
|
|
12733
13426
|
function registerSyncCommands(program) {
|
|
12734
13427
|
const sync = program.command("sync").description("push environments to a third-party platform (Vercel, Cloudflare, …)");
|
|
12735
|
-
sync.command("connections").alias("conns").description("list destination accounts seekrit can push to").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
13428
|
+
sync.command("connections").alias("conns").description("list destination accounts seekrit can push to").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
12736
13429
|
const ctx = buildContext();
|
|
12737
13430
|
const ref = await resolveOrg(ctx, options.org);
|
|
12738
13431
|
const { connections } = await ctx.client.listSyncConnections(ref.id);
|
|
@@ -12744,7 +13437,7 @@ function registerSyncCommands(program) {
|
|
|
12744
13437
|
col("id", (c) => c.id)
|
|
12745
13438
|
], "no connections — add one with `seekrit sync connect`"));
|
|
12746
13439
|
});
|
|
12747
|
-
sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").option("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").option("--azure-tenant-id <id>", "azure-key-vault: Entra Directory (tenant) ID, a GUID").option("--azure-client-id <id>", "azure-key-vault: Application (client) ID of the service principal, a GUID (its client secret is read from stdin)").option("--azure-cloud <cloud>", `azure-key-vault: ${AZURE_CLOUDS.join(" | ")}`, "public").option("--base-url <url>", "github-actions: GitHub Enterprise Server API root (omit for github.com) · langgraph-platform: self-hosted LangSmith control-plane root").option("--project-id <id>", "gcp: project ID or number whose Secret Manager to write").option("--langgraph-region <region>", `langgraph-platform: ${LANGGRAPH_PLATFORM_REGIONS.join(" | ")} (omit for us)`).option("--langgraph-tenant <id>", "langgraph-platform: LangSmith workspace UUID (only an org-scoped key needs it)").action(async (options) => {
|
|
13440
|
+
sync.command("connect").description("register a destination account (reads its API token from stdin)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--name <name>", "what to call this account, e.g. acme-vercel").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel").option("--team-id <id>", "vercel: Team id (omit for a personal account)").option("--token-kind <kind>", `railway: ${RAILWAY_TOKEN_KINDS.join(" | ")}`, "account").option("--account-id <id>", "cloudflare: account ID (32 hex, from the dashboard sidebar) · netlify: team slug or account ID").option("--region <region>", "aws: region ID, e.g. us-east-1").option("--access-key-id <id>", "aws: IAM access key ID (the secret key is read from stdin)").option("--azure-tenant-id <id>", "azure-key-vault: Entra Directory (tenant) ID, a GUID").option("--azure-client-id <id>", "azure-key-vault: Application (client) ID of the service principal, a GUID (its client secret is read from stdin)").option("--azure-cloud <cloud>", `azure-key-vault: ${AZURE_CLOUDS.join(" | ")}`, "public").option("--base-url <url>", "github-actions: GitHub Enterprise Server API root (omit for github.com) · langgraph-platform: self-hosted LangSmith control-plane root").option("--project-id <id>", "gcp: project ID or number whose Secret Manager to write").option("--langgraph-region <region>", `langgraph-platform: ${LANGGRAPH_PLATFORM_REGIONS.join(" | ")} (omit for us)`).option("--langgraph-tenant <id>", "langgraph-platform: LangSmith workspace UUID (only an org-scoped key needs it)").action(async (options) => {
|
|
12748
13441
|
const provider = assertProvider(options.provider);
|
|
12749
13442
|
const ctx = buildContext();
|
|
12750
13443
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -12761,7 +13454,7 @@ function registerSyncCommands(program) {
|
|
|
12761
13454
|
});
|
|
12762
13455
|
console.error(`connected ${created.connection.name} (${created.connection.id})`);
|
|
12763
13456
|
});
|
|
12764
|
-
destinationOptions(sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--json", "print the raw API response").action(async (connection, options) => {
|
|
13457
|
+
destinationOptions(sync.command("verify <connection>").description("check a stored credential against a destination").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--json", "print the raw API response").action(async (connection, options) => {
|
|
12765
13458
|
const provider = assertProvider(options.provider);
|
|
12766
13459
|
const ctx = buildContext();
|
|
12767
13460
|
const ref = await resolveOrg(ctx, options.org);
|
|
@@ -12770,7 +13463,7 @@ function registerSyncCommands(program) {
|
|
|
12770
13463
|
emit(options, result, () => printFields([["result", result.ok ? "ok" : "failed"], ["error", result.error ?? null]]));
|
|
12771
13464
|
if (!result.ok) process.exitCode = 1;
|
|
12772
13465
|
});
|
|
12773
|
-
sync.command("disconnect <connection>").description("delete a destination account, its bindings, and its keypair").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (connection, options) => {
|
|
13466
|
+
sync.command("disconnect <connection>").description("delete a destination account, its bindings, and its keypair").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (connection, options) => {
|
|
12774
13467
|
const ctx = buildContext();
|
|
12775
13468
|
const ref = await resolveOrg(ctx, options.org);
|
|
12776
13469
|
const conn = await resolveConnection(ctx, ref.id, connection);
|
|
@@ -12780,7 +13473,7 @@ function registerSyncCommands(program) {
|
|
|
12780
13473
|
await ctx.client.deleteSyncConnection(ref.id, conn.id);
|
|
12781
13474
|
console.error(`disconnected ${conn.name}`);
|
|
12782
13475
|
});
|
|
12783
|
-
sync.command("bindings").description("list which environments are syncing where").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
13476
|
+
sync.command("bindings").description("list which environments are syncing where").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
12784
13477
|
const ctx = buildContext();
|
|
12785
13478
|
const ref = await resolveOrg(ctx, options.org);
|
|
12786
13479
|
const [{ bindings }, { connections }] = await Promise.all([ctx.client.listSyncBindings(ref.id), ctx.client.listSyncConnections(ref.id)]);
|
|
@@ -12795,7 +13488,7 @@ function registerSyncCommands(program) {
|
|
|
12795
13488
|
col("id", (b) => b.id)
|
|
12796
13489
|
], "nothing is syncing — enable it with `seekrit sync enable`"));
|
|
12797
13490
|
});
|
|
12798
|
-
destinationOptions(sync.command("enable").description("start syncing one environment to a destination (lets seekrit decrypt it)").option("--org <slug>").requiredOption("--connection <name>", "destination account, by name or id").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "the environment to push").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--prefix <prefix>", "prepend this to every destination key name").option("--include <globs>", "comma-separated name globs to push (default: all)").option("--exclude <globs>", "comma-separated name globs to skip").option("--on-delete <action>", "delete | retain — what happens when a secret is removed", "delete").option("--mode <mode>", "auto (push on write) | manual", "auto").option("--acknowledge-decryption", "confirm that seekrit's servers may decrypt this environment to push it").action(async (options) => {
|
|
13491
|
+
destinationOptions(sync.command("enable").description("start syncing one environment to a destination (lets seekrit decrypt it)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").requiredOption("--connection <name>", "destination account, by name or id").option("--app <slug>", "application slug (defaults to seekrit.json)").requiredOption("--env <slug>", "the environment to push").option("--provider <kind>", SYNC_PROVIDER_KINDS.join(" | "), "vercel")).option("--prefix <prefix>", "prepend this to every destination key name").option("--include <globs>", "comma-separated name globs to push (default: all)").option("--exclude <globs>", "comma-separated name globs to skip").option("--on-delete <action>", "delete | retain — what happens when a secret is removed", "delete").option("--mode <mode>", "auto (push on write) | manual", "auto").option("--acknowledge-decryption", "confirm that seekrit's servers may decrypt this environment to push it").action(async (options) => {
|
|
12799
13492
|
const provider = assertProvider(options.provider);
|
|
12800
13493
|
if (options.onDelete !== "delete" && options.onDelete !== "retain") fail("--on-delete must be delete or retain");
|
|
12801
13494
|
if (options.mode !== "auto" && options.mode !== "manual") fail("--mode must be auto or manual");
|
|
@@ -12834,26 +13527,26 @@ function registerSyncCommands(program) {
|
|
|
12834
13527
|
console.error(`syncing ${target.appSlug}/${target.envSlug} → ${conn.name} ${describeDestination(destination)} (${binding.id})`);
|
|
12835
13528
|
if (binding.mode === "manual") console.error("mode is manual — push with `seekrit sync run`");
|
|
12836
13529
|
});
|
|
12837
|
-
sync.command("pause <bindingId>").description("stop pushing on this binding without deleting it").option("--org <slug>").action(async (bindingId, options) => {
|
|
13530
|
+
sync.command("pause <bindingId>").description("stop pushing on this binding without deleting it").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (bindingId, options) => {
|
|
12838
13531
|
const ctx = buildContext();
|
|
12839
13532
|
const ref = await resolveOrg(ctx, options.org);
|
|
12840
13533
|
await ctx.client.updateSyncBinding(ref.id, bindingId, { enabled: false });
|
|
12841
13534
|
console.error(`${bindingId} paused`);
|
|
12842
13535
|
});
|
|
12843
|
-
sync.command("resume <bindingId>").description("start pushing on a paused binding again").option("--org <slug>").action(async (bindingId, options) => {
|
|
13536
|
+
sync.command("resume <bindingId>").description("start pushing on a paused binding again").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").action(async (bindingId, options) => {
|
|
12844
13537
|
const ctx = buildContext();
|
|
12845
13538
|
const ref = await resolveOrg(ctx, options.org);
|
|
12846
13539
|
await ctx.client.updateSyncBinding(ref.id, bindingId, { enabled: true });
|
|
12847
13540
|
console.error(`${bindingId} resumed`);
|
|
12848
13541
|
});
|
|
12849
|
-
sync.command("disable <bindingId>").alias("rm").description("stop syncing an environment and revoke seekrit's key for it").option("--org <slug>").option("--yes", "skip the confirmation prompt").action(async (bindingId, options) => {
|
|
13542
|
+
sync.command("disable <bindingId>").alias("rm").description("stop syncing an environment and revoke seekrit's key for it").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation prompt").action(async (bindingId, options) => {
|
|
12850
13543
|
const ctx = buildContext();
|
|
12851
13544
|
const ref = await resolveOrg(ctx, options.org);
|
|
12852
13545
|
await confirmDestructive(options.yes, `Delete binding ${bindingId}? Values already pushed stay on the destination.`);
|
|
12853
13546
|
await ctx.client.deleteSyncBinding(ref.id, bindingId);
|
|
12854
13547
|
console.error(`${bindingId} deleted`);
|
|
12855
13548
|
});
|
|
12856
|
-
sync.command("run <bindingId>").description("push now, synchronously, and report what landed").option("--org <slug>").option("--json", "print the raw API response").action(async (bindingId, options) => {
|
|
13549
|
+
sync.command("run <bindingId>").description("push now, synchronously, and report what landed").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (bindingId, options) => {
|
|
12857
13550
|
const ctx = buildContext();
|
|
12858
13551
|
const ref = await resolveOrg(ctx, options.org);
|
|
12859
13552
|
const { run } = await ctx.client.runSyncBinding(ref.id, bindingId);
|
|
@@ -12872,7 +13565,7 @@ function registerSyncCommands(program) {
|
|
|
12872
13565
|
});
|
|
12873
13566
|
if (run.status !== "succeeded") process.exitCode = 1;
|
|
12874
13567
|
});
|
|
12875
|
-
sync.command("runs").description("show the sync run history").option("--org <slug>").option("--binding <id>", "only runs of this binding").option("--json", "print the raw API response").action(async (options) => {
|
|
13568
|
+
sync.command("runs").description("show the sync run history").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--binding <id>", "only runs of this binding").option("--json", "print the raw API response").action(async (options) => {
|
|
12876
13569
|
const ctx = buildContext();
|
|
12877
13570
|
const ref = await resolveOrg(ctx, options.org);
|
|
12878
13571
|
const { runs } = await ctx.client.listSyncRuns(ref.id, options.binding);
|
|
@@ -13038,6 +13731,7 @@ async function runLogout() {
|
|
|
13038
13731
|
console.error("not signed in");
|
|
13039
13732
|
return;
|
|
13040
13733
|
}
|
|
13734
|
+
if (!sessionToken) console.error(config.token ? "signed out — the stored service token was removed from this machine" : "signed out");
|
|
13041
13735
|
if (sessionToken) {
|
|
13042
13736
|
const api = new SeekritClient({
|
|
13043
13737
|
baseUrl: process.env.SEEKRIT_API_URL ?? config.apiUrl ?? "https://api.seekrit.dev",
|
|
@@ -13255,9 +13949,10 @@ withTarget(secrets.command("restore <name> <version>").description("roll a secre
|
|
|
13255
13949
|
const { secret, restoredFrom } = await ctx.client.restoreSecret(orgId, envId, name, parseVersion(version));
|
|
13256
13950
|
console.error(`${name} restored from v${restoredFrom} — now v${secret.version}`);
|
|
13257
13951
|
});
|
|
13258
|
-
withTarget(secrets.command("rm <name>").description("delete a secret")).action(async (name, options) => {
|
|
13952
|
+
withTarget(secrets.command("rm <name>").description("delete a secret and its whole version history").option("-y, --yes", "skip the confirmation")).action(async (name, options) => {
|
|
13259
13953
|
const ctx = buildContext();
|
|
13260
|
-
const { orgId, envId } = await resolveEnvTarget(ctx, options);
|
|
13954
|
+
const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
|
|
13955
|
+
await confirmDestructive(options.yes, `Delete ${name} from ${label}, including every earlier version? This cannot be undone.`);
|
|
13261
13956
|
await ctx.client.deleteSecret(orgId, envId, name);
|
|
13262
13957
|
console.error(`${name} deleted`);
|
|
13263
13958
|
});
|
|
@@ -13342,8 +14037,7 @@ async function materializeForRun(options) {
|
|
|
13342
14037
|
branch
|
|
13343
14038
|
}, dotenvVars);
|
|
13344
14039
|
} catch (err) {
|
|
13345
|
-
|
|
13346
|
-
console.error(`seekrit: continuing without seekrit-managed secrets: ${message}`);
|
|
14040
|
+
console.error(`seekrit: continuing without seekrit-managed secrets: ${describeError(err)}`);
|
|
13347
14041
|
const values = {};
|
|
13348
14042
|
const provenance = {};
|
|
13349
14043
|
overlayEnvFiles(values, provenance, envFiles);
|
|
@@ -13463,7 +14157,7 @@ async function reapStragglers(pids, signal) {
|
|
|
13463
14157
|
process.kill(pid, "SIGKILL");
|
|
13464
14158
|
} catch {}
|
|
13465
14159
|
}
|
|
13466
|
-
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) => {
|
|
14160
|
+
program.command("run").description("run a command with decrypted secrets injected (process env > .env > app > group)").passThroughOptions().option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").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) => {
|
|
13467
14161
|
const [cmd, ...args] = commandParts;
|
|
13468
14162
|
if (!cmd) fail("no command given");
|
|
13469
14163
|
const { values, provenance, interpolated, unresolvedRefs } = await materializeForRun(options);
|
|
@@ -13515,7 +14209,7 @@ program.command("run").description("run a command with decrypted secrets injecte
|
|
|
13515
14209
|
});
|
|
13516
14210
|
child.on("error", (err) => fail(`failed to start ${cmd}: ${err.message}`));
|
|
13517
14211
|
});
|
|
13518
|
-
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) => {
|
|
14212
|
+
program.command("export").description("print decrypted secrets (dotenv, json, or shell)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").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) => {
|
|
13519
14213
|
if (![
|
|
13520
14214
|
"dotenv",
|
|
13521
14215
|
"json",
|
|
@@ -13531,7 +14225,7 @@ program.command("export").description("print decrypted secrets (dotenv, json, or
|
|
|
13531
14225
|
registerAccessCommands(program);
|
|
13532
14226
|
registerHoneyTokenCommands(program);
|
|
13533
14227
|
const token = program.command("token").description("manage service tokens (CI, docker, agents)");
|
|
13534
|
-
token.command("create").description("create a service token (runtime, or --admin for provisioning); prints it once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--org <slug>").option("--app <slug>", "application to bind the token to (runtime tokens)").option("--env <slug>", "environment to bind the token to (runtime tokens)").option("--admin", "mint an org-scoped admin token that can provision structure (no env binding required)").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) => {
|
|
14228
|
+
token.command("create").description("create a service token (runtime, or --admin for provisioning); prints it once").requiredOption("--name <name>", "display name, e.g. ci-deploy").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--app <slug>", "application to bind the token to (runtime tokens)").option("--env <slug>", "environment to bind the token to (runtime tokens)").option("--admin", "mint an org-scoped admin token that can provision structure (no env binding required)").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) => {
|
|
13535
14229
|
const ctx = buildContext();
|
|
13536
14230
|
const role = options.admin ? "admin" : "member";
|
|
13537
14231
|
const boundToEnv = Boolean(options.app || options.env);
|
|
@@ -13580,7 +14274,7 @@ token.command("create").description("create a service token (runtime, or --admin
|
|
|
13580
14274
|
console.error(`${role} token created${granted ? " and granted" : ""} for ${scope} — save it now, it is not stored:`);
|
|
13581
14275
|
console.log(created.token);
|
|
13582
14276
|
});
|
|
13583
|
-
token.command("list").alias("ls").description("list service tokens").option("--org <slug>").option("--json", "print the raw API response").action(async (options) => {
|
|
14277
|
+
token.command("list").alias("ls").description("list service tokens").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("--json", "print the raw API response").action(async (options) => {
|
|
13584
14278
|
const ctx = buildContext();
|
|
13585
14279
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
13586
14280
|
const { tokens } = await ctx.client.listTokens(orgRef.id);
|
|
@@ -13594,15 +14288,17 @@ token.command("list").alias("ls").description("list service tokens").option("--o
|
|
|
13594
14288
|
col("id", (t) => t.id)
|
|
13595
14289
|
], "no service tokens — create one with `seekrit token create`"));
|
|
13596
14290
|
});
|
|
13597
|
-
token.command("revoke <tokenId>").description("revoke a service token").option("--org <slug>").action(async (tokenId, options) => {
|
|
14291
|
+
token.command("revoke <tokenId>").description("revoke a service token").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (tokenId, options) => {
|
|
13598
14292
|
const ctx = buildContext();
|
|
13599
14293
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
14294
|
+
await confirmDestructive(options.yes, `Revoke ${tokenId}? Anything still presenting it stops resolving immediately.`);
|
|
13600
14295
|
await ctx.client.revokeToken(orgRef.id, tokenId);
|
|
13601
14296
|
console.error(`${tokenId} revoked`);
|
|
13602
14297
|
});
|
|
13603
|
-
token.command("delete <tokenId>").description("permanently delete a revoked service token (revoke it first)").option("--org <slug>").action(async (tokenId, options) => {
|
|
14298
|
+
token.command("delete <tokenId>").description("permanently delete a revoked service token (revoke it first)").option("--org <slug>", "organization slug (defaults to seekrit.json, or your only org)").option("-y, --yes", "skip the confirmation").action(async (tokenId, options) => {
|
|
13604
14299
|
const ctx = buildContext();
|
|
13605
14300
|
const orgRef = await resolveOrg(ctx, options.org);
|
|
14301
|
+
await confirmDestructive(options.yes, `Permanently delete ${tokenId}? Its audit history keeps the id, but the token row is gone.`);
|
|
13606
14302
|
await ctx.client.deleteToken(orgRef.id, tokenId);
|
|
13607
14303
|
console.error(`${tokenId} deleted`);
|
|
13608
14304
|
});
|
|
@@ -13614,6 +14310,8 @@ registerProvisionerCommands(program);
|
|
|
13614
14310
|
registerProxyCommands(program);
|
|
13615
14311
|
registerAgentCommands(program);
|
|
13616
14312
|
registerPaperclipCommands(program);
|
|
14313
|
+
registerOpenclawCommands(program);
|
|
14314
|
+
registerHermesCommands(program);
|
|
13617
14315
|
registerSshCommands(program);
|
|
13618
14316
|
registerAwsCommands(program);
|
|
13619
14317
|
registerGcpCommands(program);
|
|
@@ -13633,7 +14331,7 @@ registerSyncCommands(program);
|
|
|
13633
14331
|
registerBillingCommands(program);
|
|
13634
14332
|
const argv = process.argv.map((arg) => arg === "-v" ? "--version" : arg);
|
|
13635
14333
|
program.parseAsync(argv).catch((err) => {
|
|
13636
|
-
fail(
|
|
14334
|
+
fail(describeError(err));
|
|
13637
14335
|
});
|
|
13638
14336
|
//#endregion
|
|
13639
14337
|
export { verifyMessage as A, toBase64 as B, isServiceToken as C, importVerifyingKey as D, importSigningKey as E, kmsBlobKeyRef as F, kmsDecrypt as I, kmsEncrypt as L, generateMysqlCredential as M, generateDataKey as N, signMessage as O, generateEncryptKeyMaterial as P, wrapDek as R, createServiceToken as S, generateSigningKeyMaterial as T, parseBranchTtl as V, isTokenAuth as _, ensureM2mAdminToken as a, writeProjectConfig as b, kmsResolveKey as c, resolveAppEnv as d, resolveBranch as f, getDek as g, resolveOrg as h, materializeEnv as i, generatePostgresCredential as j, signatureKeyRef as k, kmsResolveRecipient as l, resolveGroup as m, fetchDecryptedSecrets as n, kmsCallerIdentity as o, resolveEnvTarget as p, fetchDecryptedVersion as r, kmsRecoverMaterial as s, encryptAndSetSecret as t, resolveApp as u, tryBuildContext as v, parseServiceToken as w, version as x, setFailThrows as y, generateDek as z };
|