@seekrit/cli 1.9.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/index.js +492 -111
- package/dist/{mcp-ZYu1Q6Ft.js → mcp-D7rsPeBQ.js} +10 -16
- package/package.json +23 -7
package/README.md
CHANGED
|
@@ -30,9 +30,11 @@ seekrit login --token skt_…
|
|
|
30
30
|
# leaves this machine). Only needed once per account.
|
|
31
31
|
seekrit keys setup
|
|
32
32
|
|
|
33
|
-
# 3.
|
|
34
|
-
#
|
|
35
|
-
|
|
33
|
+
# 3. Set the current directory up. Creates an app named after the project (and a
|
|
34
|
+
# development environment) if there is none, and writes seekrit.json, which
|
|
35
|
+
# is safe to commit. With no credential at all it signs up first — add
|
|
36
|
+
# --org-name "<the real project>" --yes to do that without a prompt.
|
|
37
|
+
seekrit init # or: seekrit init --org acme --app storefront
|
|
36
38
|
|
|
37
39
|
# 4. Work with secrets. A service token names its own environment; on a user
|
|
38
40
|
# session, add --env production.
|
|
@@ -56,7 +58,7 @@ Run `seekrit <command> --help` for full flags. The
|
|
|
56
58
|
| `login [--token \| --dev-user] [--api-url]` | Save credentials to the global config. |
|
|
57
59
|
| `whoami` | Show the authenticated identity and org membership. |
|
|
58
60
|
| `keys setup` | Generate your keypair and protect it with a passphrase. |
|
|
59
|
-
| `init --org --app` |
|
|
61
|
+
| `init [--org] [--app] [--env] [--from-env] [--invite]` | Set this directory up: sign up if there is no credential, create the app and a `development` environment if missing, write `seekrit.json`. |
|
|
60
62
|
| `org create` / `app create` / `env create` | Create organizations, apps, and environments. |
|
|
61
63
|
| `secrets list` | List secret names (never values). |
|
|
62
64
|
| `secrets get <name> [--raw]` | Decrypt and print one secret value (`--raw` skips `${OTHER_SECRET}` expansion). |
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSyn
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { arch, homedir, hostname, platform, tmpdir, userInfo } from "node:os";
|
|
7
|
-
import { dirname, join, parse, resolve } from "node:path";
|
|
7
|
+
import { basename, 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";
|
|
@@ -1062,6 +1062,141 @@ function describeChange(kind, name, restoreFrom) {
|
|
|
1062
1062
|
}
|
|
1063
1063
|
}
|
|
1064
1064
|
//#endregion
|
|
1065
|
+
//#region ../../packages/core/src/connect.ts
|
|
1066
|
+
/** Workload authentication is separate from secret-decryption principals. */
|
|
1067
|
+
const workloadPublicKeySchema = z.object({
|
|
1068
|
+
kty: z.literal("EC"),
|
|
1069
|
+
crv: z.literal("P-256"),
|
|
1070
|
+
x: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
1071
|
+
y: z.string().regex(/^[A-Za-z0-9_-]{43}$/)
|
|
1072
|
+
}).strict();
|
|
1073
|
+
const audienceSchema = z.string().min(1).max(256).regex(/^[^\s]+$/);
|
|
1074
|
+
/** HTTPS endpoint syntax. Resolved-address safety is enforced by the server runtime. */
|
|
1075
|
+
const connectUrlSchema = z.string().max(2048).url().refine((value) => {
|
|
1076
|
+
try {
|
|
1077
|
+
const u = new URL(value);
|
|
1078
|
+
return u.protocol === "https:" && !u.username && !u.password && !u.hash && !u.search && !u.port && u.hostname.length <= 253 && u.hostname.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) && u.hostname.includes(".") && !/^[0-9.]+$/.test(u.hostname) && !u.hostname.endsWith(".") && !/(^|\.)(localhost|local|localdomain|internal|invalid|home|home\.arpa|onion)$/.test(u.hostname);
|
|
1079
|
+
} catch {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
}, "use a public HTTPS hostname without credentials, query, fragment, or port");
|
|
1083
|
+
const federationPolicySchema = z.object({
|
|
1084
|
+
issuer: connectUrlSchema,
|
|
1085
|
+
jwksUri: connectUrlSchema,
|
|
1086
|
+
audience: audienceSchema,
|
|
1087
|
+
subject: z.string().min(1).max(512),
|
|
1088
|
+
claims: z.record(z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/), z.string().min(1).max(512)).refine((claims) => Object.keys(claims).length <= 20 && Object.keys(claims).every((key) => ![
|
|
1089
|
+
"iss",
|
|
1090
|
+
"aud",
|
|
1091
|
+
"sub",
|
|
1092
|
+
"exp",
|
|
1093
|
+
"iat",
|
|
1094
|
+
"nbf"
|
|
1095
|
+
].includes(key))).default({})
|
|
1096
|
+
}).strict();
|
|
1097
|
+
z.object({
|
|
1098
|
+
name: z.string().trim().min(1).max(100),
|
|
1099
|
+
environmentId: z.string().min(1).max(128),
|
|
1100
|
+
publicKey: workloadPublicKeySchema.optional(),
|
|
1101
|
+
federation: federationPolicySchema.optional(),
|
|
1102
|
+
audiences: z.array(audienceSchema).max(20).default([]),
|
|
1103
|
+
maxTtlSeconds: z.number().int().min(60).max(900).default(300)
|
|
1104
|
+
}).strict().refine((v) => !!v.publicKey !== !!v.federation, "choose a public key or a federation policy");
|
|
1105
|
+
z.object({
|
|
1106
|
+
enabled: z.boolean().optional(),
|
|
1107
|
+
publicKey: workloadPublicKeySchema.optional()
|
|
1108
|
+
}).strict().refine((v) => v.enabled !== void 0 || v.publicKey !== void 0);
|
|
1109
|
+
const githubPermissionsSchema = z.record(z.string().regex(/^[a-z_]{1,64}$/), z.enum(["read", "write"])).refine((p) => Object.keys(p).length > 0 && Object.keys(p).length <= 40, "specify between 1 and 40 permissions");
|
|
1110
|
+
const githubScopeSchema = z.object({
|
|
1111
|
+
repositoryIds: z.array(z.number().int().positive().max(Number.MAX_SAFE_INTEGER)).min(1).max(100),
|
|
1112
|
+
permissions: githubPermissionsSchema
|
|
1113
|
+
}).strict();
|
|
1114
|
+
/** Exact paths or explicitly allowed descendants, without URL decoding ambiguity. */
|
|
1115
|
+
const connectPathSchema = z.string().min(1).max(1024).refine((path) => /^\/[A-Za-z0-9_./~-]*$/.test(path) && !path.includes("//") && !path.split("/").some((part) => part === "." || part === ".."), "use an absolute API path without encoded characters or dot segments");
|
|
1116
|
+
const proxyRuleSchema = z.object({
|
|
1117
|
+
path: connectPathSchema,
|
|
1118
|
+
descendants: z.boolean().default(false),
|
|
1119
|
+
methods: z.array(z.enum([
|
|
1120
|
+
"GET",
|
|
1121
|
+
"POST",
|
|
1122
|
+
"PUT",
|
|
1123
|
+
"PATCH",
|
|
1124
|
+
"DELETE",
|
|
1125
|
+
"HEAD"
|
|
1126
|
+
])).min(1).max(6)
|
|
1127
|
+
}).strict();
|
|
1128
|
+
const staticScopeSchema = z.object({ rules: z.array(proxyRuleSchema).min(1).max(30) }).strict();
|
|
1129
|
+
const oauthScopeSchema = z.object({ scopes: z.array(z.string().regex(/^[\x21\x23-\x5b\x5d-\x7e]{1,200}$/)).min(1).max(50).refine((scopes) => new Set(scopes).size === scopes.length, "scopes must be unique") }).strict();
|
|
1130
|
+
const oauthConfigSchema = oauthScopeSchema.extend({
|
|
1131
|
+
provider: z.literal("oauth"),
|
|
1132
|
+
grantType: z.enum(["client_credentials", "authorization_code"]),
|
|
1133
|
+
clientId: z.string().min(1).max(512),
|
|
1134
|
+
tokenEndpoint: connectUrlSchema,
|
|
1135
|
+
authorizationEndpoint: connectUrlSchema.optional(),
|
|
1136
|
+
tokenAuthMethod: z.enum(["client_secret_basic", "client_secret_post"]).default("client_secret_basic")
|
|
1137
|
+
});
|
|
1138
|
+
const customApiConfigSchema = staticScopeSchema.extend({
|
|
1139
|
+
provider: z.literal("custom_api"),
|
|
1140
|
+
origin: connectUrlSchema.refine((value) => {
|
|
1141
|
+
try {
|
|
1142
|
+
return new URL(value).pathname === "/";
|
|
1143
|
+
} catch {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
}, "use an origin without a path"),
|
|
1147
|
+
headerName: z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,63}$/).refine((v) => !/^(accept$|user-agent$|host|cookie|content-|proxy-|forwarded|x-forwarded-|cf-|connection|transfer-encoding|upgrade|trailer|te$)/i.test(v)),
|
|
1148
|
+
headerPrefix: z.string().max(64).regex(/^[\x20-\x7e]*$/).default("Bearer ")
|
|
1149
|
+
});
|
|
1150
|
+
const connectConfigSchema = z.discriminatedUnion("provider", [
|
|
1151
|
+
customApiConfigSchema,
|
|
1152
|
+
oauthConfigSchema,
|
|
1153
|
+
githubScopeSchema.extend({
|
|
1154
|
+
provider: z.literal("github_app"),
|
|
1155
|
+
appId: z.string().regex(/^[A-Za-z0-9_.-]{1,128}$/),
|
|
1156
|
+
installationId: z.string().regex(/^[1-9][0-9]{0,19}$/)
|
|
1157
|
+
}),
|
|
1158
|
+
staticScopeSchema.extend({
|
|
1159
|
+
provider: z.literal("static_key"),
|
|
1160
|
+
service: z.literal("resend"),
|
|
1161
|
+
rules: z.array(proxyRuleSchema.refine((rule) => rule.path === "/emails" || rule.path.startsWith("/emails/"), "the Resend connector supports only /emails and its descendants")).min(1).max(30)
|
|
1162
|
+
})
|
|
1163
|
+
]).refine((v) => v.provider !== "oauth" || v.grantType !== "authorization_code" || !!v.authorizationEndpoint, "authorization-code connections require an authorization endpoint");
|
|
1164
|
+
const connectGrantPolicySchema = z.discriminatedUnion("provider", [
|
|
1165
|
+
staticScopeSchema.extend({ provider: z.literal("custom_api") }),
|
|
1166
|
+
oauthScopeSchema.extend({ provider: z.literal("oauth") }),
|
|
1167
|
+
githubScopeSchema.extend({ provider: z.literal("github_app") }),
|
|
1168
|
+
staticScopeSchema.extend({ provider: z.literal("static_key") })
|
|
1169
|
+
]);
|
|
1170
|
+
z.object({
|
|
1171
|
+
name: z.string().trim().min(1).max(100),
|
|
1172
|
+
config: connectConfigSchema,
|
|
1173
|
+
credentialWrapped: z.string().startsWith("wd1.").max(32e3)
|
|
1174
|
+
}).strict();
|
|
1175
|
+
z.object({
|
|
1176
|
+
enabled: z.boolean().optional(),
|
|
1177
|
+
credentialWrapped: z.string().startsWith("wd1.").max(32e3).optional()
|
|
1178
|
+
}).strict().refine((v) => v.enabled !== void 0 || v.credentialWrapped !== void 0);
|
|
1179
|
+
z.object({
|
|
1180
|
+
workloadId: z.string().min(1).max(128),
|
|
1181
|
+
connectionId: z.string().min(1).max(128),
|
|
1182
|
+
policy: connectGrantPolicySchema
|
|
1183
|
+
}).strict();
|
|
1184
|
+
z.object({
|
|
1185
|
+
path: connectPathSchema,
|
|
1186
|
+
method: z.enum([
|
|
1187
|
+
"GET",
|
|
1188
|
+
"POST",
|
|
1189
|
+
"PUT",
|
|
1190
|
+
"PATCH",
|
|
1191
|
+
"DELETE",
|
|
1192
|
+
"HEAD"
|
|
1193
|
+
]).default("GET"),
|
|
1194
|
+
query: z.record(z.string().min(1).max(100), z.string().max(2048)).default({}),
|
|
1195
|
+
/** JSON only; callers cannot inject authorization or forwarding headers. */
|
|
1196
|
+
body: z.string().max(65536).optional()
|
|
1197
|
+
}).strict().refine((v) => !["GET", "HEAD"].includes(v.method) || v.body === void 0, "GET and HEAD cannot carry a body");
|
|
1198
|
+
z.object({ audience: audienceSchema }).strict();
|
|
1199
|
+
//#endregion
|
|
1065
1200
|
//#region ../../packages/core/src/dotenv.ts
|
|
1066
1201
|
/**
|
|
1067
1202
|
* `.env` parsing and serialization — the canonical definition of what a `.env`
|
|
@@ -1937,6 +2072,17 @@ function sshHostSetupInstructions(config) {
|
|
|
1937
2072
|
//#endregion
|
|
1938
2073
|
//#region ../../packages/core/src/types.ts
|
|
1939
2074
|
const AUDIT_ACTIONS = [
|
|
2075
|
+
"workload.created",
|
|
2076
|
+
"workload.updated",
|
|
2077
|
+
"workload.session_issued",
|
|
2078
|
+
"workload.identity_issued",
|
|
2079
|
+
"connect.connection_created",
|
|
2080
|
+
"connect.connection_updated",
|
|
2081
|
+
"connect.grant_created",
|
|
2082
|
+
"connect.grant_revoked",
|
|
2083
|
+
"connect.token_issued",
|
|
2084
|
+
"connect.proxy_requested",
|
|
2085
|
+
"connect.access_denied",
|
|
1940
2086
|
"org.created",
|
|
1941
2087
|
"org.updated",
|
|
1942
2088
|
"org.member_added",
|
|
@@ -5445,11 +5591,13 @@ function encodeOpensshPrivateKey(seed, pub, comment) {
|
|
|
5445
5591
|
//#region ../../packages/crypto/src/token.ts
|
|
5446
5592
|
/**
|
|
5447
5593
|
* Service tokens (CI, docker builds, agent proxies, k8s, …) are self-contained
|
|
5448
|
-
* principals
|
|
5449
|
-
*
|
|
5450
|
-
*
|
|
5594
|
+
* principals. One local bundle holds independent authentication and decryption
|
|
5595
|
+
* material. Only serviceTokenAuth(token) may be sent to the API; the private
|
|
5596
|
+
* key stays local. The server stores the auth credential hash and public key.
|
|
5451
5597
|
*
|
|
5452
|
-
*
|
|
5598
|
+
* Bundle: `skt_<id>_v2.<32-byte auth secret>.<private key pkcs8, base64url>`
|
|
5599
|
+
* Wire: `skt_<id>_v2.<32-byte auth secret>`
|
|
5600
|
+
* Legacy bundles remain readable offline but must never be transmitted.
|
|
5453
5601
|
*/
|
|
5454
5602
|
const TOKEN_PREFIX = "skt";
|
|
5455
5603
|
const CLI_SESSION_PREFIX = "skc";
|
|
@@ -5479,11 +5627,11 @@ async function createServiceToken() {
|
|
|
5479
5627
|
const { publicKeyJwk, privateKeyJwk } = await generateKeyPair();
|
|
5480
5628
|
const pkcs8 = await exportPrivateKeyPkcs8(await importPrivateKey(privateKeyJwk));
|
|
5481
5629
|
const tokenId = randomTokenId();
|
|
5482
|
-
const token = `${tokenId}
|
|
5630
|
+
const token = `${tokenId}_v2.${toBase64Url(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)))}.${toBase64Url(pkcs8)}`;
|
|
5483
5631
|
return {
|
|
5484
5632
|
token,
|
|
5485
5633
|
tokenId,
|
|
5486
|
-
tokenHash: await hashToken(token),
|
|
5634
|
+
tokenHash: await hashToken(serviceTokenAuth(token)),
|
|
5487
5635
|
publicKeyJwk
|
|
5488
5636
|
};
|
|
5489
5637
|
}
|
|
@@ -5496,8 +5644,8 @@ async function createHoneyToken() {
|
|
|
5496
5644
|
};
|
|
5497
5645
|
}
|
|
5498
5646
|
async function parseServiceToken(token) {
|
|
5499
|
-
const match = /^(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)$/.exec(token);
|
|
5500
|
-
if (!match) throw new SeekritCryptoError("MALFORMED_TOKEN", "not a valid seekrit service token");
|
|
5647
|
+
const match = /^(skt_[0-9A-Za-z]+)_(?:v2\.[A-Za-z0-9_-]{43}\.)?([A-Za-z0-9_-]+)$/.exec(token);
|
|
5648
|
+
if (!match || match[0] !== token) throw new SeekritCryptoError("MALFORMED_TOKEN", "not a valid seekrit service token");
|
|
5501
5649
|
const [, tokenId, keyB64] = match;
|
|
5502
5650
|
try {
|
|
5503
5651
|
return {
|
|
@@ -5508,6 +5656,18 @@ async function parseServiceToken(token) {
|
|
|
5508
5656
|
throw new SeekritCryptoError("MALFORMED_TOKEN", "service token private key is corrupted");
|
|
5509
5657
|
}
|
|
5510
5658
|
}
|
|
5659
|
+
/** Extract only the independent auth credential. Never fall back to a raw token. */
|
|
5660
|
+
function serviceTokenAuth(token) {
|
|
5661
|
+
const match = /^(skt_[0-9A-Za-z]+_v2\.[A-Za-z0-9_-]{43})(?:\.[A-Za-z0-9_-]+)?$/.exec(token);
|
|
5662
|
+
const auth = match?.[1];
|
|
5663
|
+
if (!match || match[0] !== token || !auth) throw new SeekritCryptoError("MALFORMED_TOKEN", "a v2 service token is required for API access; replace legacy tokens in the dashboard");
|
|
5664
|
+
return auth;
|
|
5665
|
+
}
|
|
5666
|
+
/** Preserve human/session credentials while stripping service-token private keys. */
|
|
5667
|
+
function apiBearerToken(token) {
|
|
5668
|
+
const value = token.trim();
|
|
5669
|
+
return isServiceToken(value) ? serviceTokenAuth(value) : token;
|
|
5670
|
+
}
|
|
5511
5671
|
function isServiceToken(value) {
|
|
5512
5672
|
return value.startsWith(`${TOKEN_PREFIX}_`);
|
|
5513
5673
|
}
|
|
@@ -5540,7 +5700,7 @@ async function createAgentTaskToken() {
|
|
|
5540
5700
|
}
|
|
5541
5701
|
//#endregion
|
|
5542
5702
|
//#region package.json
|
|
5543
|
-
var version = "
|
|
5703
|
+
var version = "2.0.0";
|
|
5544
5704
|
//#endregion
|
|
5545
5705
|
//#region ../../packages/api-client/src/index.ts
|
|
5546
5706
|
/**
|
|
@@ -5579,13 +5739,15 @@ var SeekritClient = class {
|
|
|
5579
5739
|
...extraHeaders
|
|
5580
5740
|
};
|
|
5581
5741
|
if (this.client) headers["x-seekrit-client"] = this.client;
|
|
5582
|
-
if (this.auth.type === "bearer") headers.authorization = `Bearer ${this.auth.token}`;
|
|
5742
|
+
if (this.auth.type === "bearer") headers.authorization = `Bearer ${apiBearerToken(this.auth.token)}`;
|
|
5583
5743
|
else if (this.auth.type === "dynamic") {
|
|
5584
5744
|
const token = await this.auth.getToken();
|
|
5585
5745
|
if (!token) throw new SeekritApiError(401, "unauthorized", "session expired");
|
|
5586
|
-
headers.authorization = `Bearer ${token}`;
|
|
5587
|
-
} else if (this.auth.type === "m2m")
|
|
5588
|
-
|
|
5746
|
+
headers.authorization = `Bearer ${apiBearerToken(token)}`;
|
|
5747
|
+
} else if (this.auth.type === "m2m") {
|
|
5748
|
+
if (isServiceToken(this.auth.clientId.trim()) || isServiceToken(this.auth.clientSecret.trim())) throw new Error("service tokens must use bearer authentication, not machine client credentials");
|
|
5749
|
+
headers.authorization = `Basic ${btoa(`${this.auth.clientId}:${this.auth.clientSecret}`)}`;
|
|
5750
|
+
} else headers["x-seekrit-dev-user"] = this.auth.email;
|
|
5589
5751
|
if (body !== void 0) headers["content-type"] = "application/json";
|
|
5590
5752
|
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
5591
5753
|
method,
|
|
@@ -5945,6 +6107,42 @@ var SeekritClient = class {
|
|
|
5945
6107
|
deleteHoneyToken(orgId, honeyTokenId) {
|
|
5946
6108
|
return this.request("DELETE", `/v1/orgs/${orgId}/honey-tokens/${honeyTokenId}`);
|
|
5947
6109
|
}
|
|
6110
|
+
getConnect(orgId) {
|
|
6111
|
+
return this.request("GET", `/v1/orgs/${orgId}/connect`);
|
|
6112
|
+
}
|
|
6113
|
+
getConnectBrokerKey(orgId) {
|
|
6114
|
+
return this.request("GET", `/v1/orgs/${orgId}/connect/broker-key`);
|
|
6115
|
+
}
|
|
6116
|
+
createWorkload(orgId, input) {
|
|
6117
|
+
return this.request("POST", `/v1/orgs/${orgId}/connect/workloads`, input);
|
|
6118
|
+
}
|
|
6119
|
+
updateWorkload(orgId, id, input) {
|
|
6120
|
+
return this.request("PATCH", `/v1/orgs/${orgId}/connect/workloads/${encodeURIComponent(id)}`, input);
|
|
6121
|
+
}
|
|
6122
|
+
createConnectConnection(orgId, input) {
|
|
6123
|
+
return this.request("POST", `/v1/orgs/${orgId}/connect/connections`, input);
|
|
6124
|
+
}
|
|
6125
|
+
updateConnectConnection(orgId, id, input) {
|
|
6126
|
+
return this.request("PATCH", `/v1/orgs/${orgId}/connect/connections/${encodeURIComponent(id)}`, input);
|
|
6127
|
+
}
|
|
6128
|
+
createConnectGrant(orgId, input) {
|
|
6129
|
+
return this.request("POST", `/v1/orgs/${orgId}/connect/grants`, input);
|
|
6130
|
+
}
|
|
6131
|
+
startConnectOAuth(orgId, id) {
|
|
6132
|
+
return this.request("POST", `/v1/orgs/${orgId}/connect/connections/${encodeURIComponent(id)}/oauth/start`, {});
|
|
6133
|
+
}
|
|
6134
|
+
finishConnectOAuth(orgId, id, state, code) {
|
|
6135
|
+
return this.request("POST", `/v1/orgs/${orgId}/connect/connections/${encodeURIComponent(id)}/oauth/finish`, {
|
|
6136
|
+
state,
|
|
6137
|
+
code
|
|
6138
|
+
});
|
|
6139
|
+
}
|
|
6140
|
+
disconnectConnectOAuth(orgId, id) {
|
|
6141
|
+
return this.request("POST", `/v1/orgs/${orgId}/connect/connections/${encodeURIComponent(id)}/oauth/disconnect`, {});
|
|
6142
|
+
}
|
|
6143
|
+
revokeConnectGrant(orgId, id) {
|
|
6144
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/connect/grants/${encodeURIComponent(id)}`);
|
|
6145
|
+
}
|
|
5948
6146
|
listAgents(orgId) {
|
|
5949
6147
|
return this.request("GET", `/v1/orgs/${orgId}/agents`);
|
|
5950
6148
|
}
|
|
@@ -6023,6 +6221,7 @@ var SeekritClient = class {
|
|
|
6023
6221
|
* identity are three different answers.
|
|
6024
6222
|
*/
|
|
6025
6223
|
introspectAgentTask(token) {
|
|
6224
|
+
if (!token.startsWith("skd_")) throw new Error("expected a dispatched task token");
|
|
6026
6225
|
return this.request("POST", "/v1/tasks/introspect", { token });
|
|
6027
6226
|
}
|
|
6028
6227
|
/** End a run's authority now. Idempotent. */
|
|
@@ -6352,6 +6551,10 @@ async function unauthenticatedPost(baseUrl, path, body, fetchImpl, client) {
|
|
|
6352
6551
|
function startCliLogin(baseUrl, input, options = {}) {
|
|
6353
6552
|
return unauthenticatedPost(baseUrl, "/v1/cli-login", input, options.fetch ?? ((...args) => fetch(...args)), options.client);
|
|
6354
6553
|
}
|
|
6554
|
+
/** Create a workspace and a machine credential with no existing account. */
|
|
6555
|
+
function signup(baseUrl, input, options = {}) {
|
|
6556
|
+
return unauthenticatedPost(baseUrl, "/v1/signup", input, options.fetch ?? ((...args) => fetch(...args)), options.client);
|
|
6557
|
+
}
|
|
6355
6558
|
/** Ask whether a human has authorized the request yet. */
|
|
6356
6559
|
function pollCliLogin(baseUrl, code, options = {}) {
|
|
6357
6560
|
const fetchImpl = options.fetch ?? ((...args) => fetch(...args));
|
|
@@ -6500,6 +6703,7 @@ function promptEnter(question) {
|
|
|
6500
6703
|
});
|
|
6501
6704
|
});
|
|
6502
6705
|
}
|
|
6706
|
+
/** Ask a yes/no question on the terminal; anything but `y`/`yes` is no. */
|
|
6503
6707
|
function askYesNo(question) {
|
|
6504
6708
|
const rl = createInterface({
|
|
6505
6709
|
input: process.stdin,
|
|
@@ -6650,6 +6854,23 @@ async function getPolicySigner(ctx) {
|
|
|
6650
6854
|
jwk: policySignerJwk(privateKeyJwk)
|
|
6651
6855
|
};
|
|
6652
6856
|
}
|
|
6857
|
+
/**
|
|
6858
|
+
* The calling principal's public key JWK — what a freshly generated DEK is
|
|
6859
|
+
* wrapped to when creating an environment. Service tokens carry their private
|
|
6860
|
+
* key, so the public half is derived from it; users publish theirs at key
|
|
6861
|
+
* setup. Shared by `seekrit env create`, `seekrit init`, and the MCP server so
|
|
6862
|
+
* a token can create an environment everywhere, not only over MCP.
|
|
6863
|
+
*/
|
|
6864
|
+
async function principalPublicKeyJwk(ctx) {
|
|
6865
|
+
if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
|
|
6866
|
+
const { privateKey } = await parseServiceToken(ctx.auth.token);
|
|
6867
|
+
const { d: _d, key_ops: _ops, ext: _ext, ...pub } = await crypto.subtle.exportKey("jwk", privateKey);
|
|
6868
|
+
return JSON.stringify(pub);
|
|
6869
|
+
}
|
|
6870
|
+
const { user } = await ctx.client.me();
|
|
6871
|
+
if (!user.publicKeyJwk) fail("this user has no keypair yet — run `seekrit keys setup` first");
|
|
6872
|
+
return user.publicKeyJwk;
|
|
6873
|
+
}
|
|
6653
6874
|
/** Recover one environment's DEK for the current principal. */
|
|
6654
6875
|
async function getDek(ctx, orgId, envId) {
|
|
6655
6876
|
const [{ wrappedDek }, privateKey] = await Promise.all([ctx.client.getMyEnvKey(orgId, envId), getPrivateKey(ctx)]);
|
|
@@ -6752,7 +6973,11 @@ async function boundEnvTarget(ctx, opts) {
|
|
|
6752
6973
|
try {
|
|
6753
6974
|
({scope} = await ctx.client.resolve(opts.branch ? { branch: opts.branch } : {}));
|
|
6754
6975
|
} catch (err) {
|
|
6755
|
-
|
|
6976
|
+
if (err instanceof SeekritApiError && err.status < 500) {
|
|
6977
|
+
if (opts.env) return null;
|
|
6978
|
+
fail(`specify --env — this token has no environment of its own to fall back to (${err.message})`);
|
|
6979
|
+
}
|
|
6980
|
+
throw err;
|
|
6756
6981
|
}
|
|
6757
6982
|
const env = scope.branchOf ?? {
|
|
6758
6983
|
envId: scope.envId,
|
|
@@ -6779,7 +7004,10 @@ async function boundEnvTarget(ctx, opts) {
|
|
|
6779
7004
|
* it this way; this is the same rule, in one place, for both.
|
|
6780
7005
|
*/
|
|
6781
7006
|
async function resolveEnvTarget(ctx, opts) {
|
|
6782
|
-
if (!opts.app && !opts.group && isTokenAuth(ctx))
|
|
7007
|
+
if (!opts.app && !opts.group && isTokenAuth(ctx)) {
|
|
7008
|
+
const bound = await boundEnvTarget(ctx, opts);
|
|
7009
|
+
if (bound) return bound;
|
|
7010
|
+
}
|
|
6783
7011
|
const org = await resolveOrg(ctx, opts.org);
|
|
6784
7012
|
if (!opts.env) fail("specify --env");
|
|
6785
7013
|
if (opts.group) {
|
|
@@ -8781,10 +9009,8 @@ function registerAppCommands(program) {
|
|
|
8781
9009
|
const { apps } = await ctx.client.listApps(orgRef.id);
|
|
8782
9010
|
const appRow = apps.find((a) => a.slug === options.app || a.id === options.app);
|
|
8783
9011
|
if (!appRow) fail(`no app "${options.app}" in ${orgRef.slug}`);
|
|
8784
|
-
const { user } = await ctx.client.me();
|
|
8785
|
-
if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
|
|
8786
9012
|
const dek = generateDek();
|
|
8787
|
-
const wrappedDek = await wrapDek(dek,
|
|
9013
|
+
const wrappedDek = await wrapDek(dek, await principalPublicKeyJwk(ctx));
|
|
8788
9014
|
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
|
|
8789
9015
|
const created = await ctx.client.createEnv(orgRef.id, appRow.id, {
|
|
8790
9016
|
name: options.name,
|
|
@@ -9141,7 +9367,7 @@ const OFFLINE_DECRYPTOR_HTML = `<!doctype html>
|
|
|
9141
9367
|
|
|
9142
9368
|
/** skt_<id>_<pkcs8 base64url>: a service token carries its own private key. */
|
|
9143
9369
|
async function importToken(token) {
|
|
9144
|
-
var match = /^(skt_[0-9A-Za-z]+)_([A-Za-z0-9_-]+)$/.exec(String(token).trim());
|
|
9370
|
+
var match = /^(skt_[0-9A-Za-z]+)_(?:v2\\.[A-Za-z0-9_-]{43}\\.)?([A-Za-z0-9_-]+)$/.exec(String(token).trim());
|
|
9145
9371
|
if (!match) throw new Error("not a valid seekrit service token");
|
|
9146
9372
|
var privateKey = await crypto.subtle.importKey(
|
|
9147
9373
|
"pkcs8",
|
|
@@ -11854,74 +12080,6 @@ function registerHoneyTokenCommands(program) {
|
|
|
11854
12080
|
});
|
|
11855
12081
|
}
|
|
11856
12082
|
//#endregion
|
|
11857
|
-
//#region src/logsink.ts
|
|
11858
|
-
/** Collect repeated `--header Name: value` flags into a map. */
|
|
11859
|
-
function collectHeader(value, acc = {}) {
|
|
11860
|
-
const sep = value.indexOf(":");
|
|
11861
|
-
if (sep <= 0) fail(`expected "Name: value", got "${value}"`);
|
|
11862
|
-
acc[value.slice(0, sep).trim()] = value.slice(sep + 1).trim();
|
|
11863
|
-
return acc;
|
|
11864
|
-
}
|
|
11865
|
-
/**
|
|
11866
|
-
* The org's OTLP audit-log export — near-real-time shipping of every audit row
|
|
11867
|
-
* to a customer SIEM. Header *values* are write-only: they are encrypted at
|
|
11868
|
-
* rest and never echoed back, so `show` reports only the header names.
|
|
11869
|
-
*/
|
|
11870
|
-
function registerLogSinkCommands(program) {
|
|
11871
|
-
const sink = program.command("log-sink").description("stream the audit trail to your own OTLP collector (SIEM)");
|
|
11872
|
-
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) => {
|
|
11873
|
-
const ctx = buildContext();
|
|
11874
|
-
const ref = await resolveOrg(ctx, options.org);
|
|
11875
|
-
const { sink: config } = await ctx.client.getLogSink(ref.id);
|
|
11876
|
-
emit(options, { sink: config }, () => {
|
|
11877
|
-
if (!config) {
|
|
11878
|
-
console.error("no log sink configured — set one with `seekrit log-sink set`");
|
|
11879
|
-
return;
|
|
11880
|
-
}
|
|
11881
|
-
printFields([
|
|
11882
|
-
["endpoint", config.endpoint],
|
|
11883
|
-
["enabled", config.enabled ? "yes" : "no"],
|
|
11884
|
-
["headers", config.headerNames.length > 0 ? config.headerNames.join(", ") : "none"],
|
|
11885
|
-
["last success", config.lastSuccessAt ?? "never"],
|
|
11886
|
-
["last attempt", config.lastAttemptAt ?? "never"],
|
|
11887
|
-
["last error", config.lastError ?? "none"]
|
|
11888
|
-
]);
|
|
11889
|
-
});
|
|
11890
|
-
});
|
|
11891
|
-
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) => {
|
|
11892
|
-
if (options.header && options.clearHeaders) fail("pass either --header or --clear-headers, not both");
|
|
11893
|
-
const ctx = buildContext();
|
|
11894
|
-
const ref = await resolveOrg(ctx, options.org);
|
|
11895
|
-
const headers = options.clearHeaders ? {} : options.header;
|
|
11896
|
-
const { sink: config } = await ctx.client.setLogSink(ref.id, {
|
|
11897
|
-
endpoint,
|
|
11898
|
-
headers,
|
|
11899
|
-
enabled: !options.disabled
|
|
11900
|
-
});
|
|
11901
|
-
console.error(`log sink → ${config.endpoint} (${config.enabled ? "enabled" : "disabled"}) — test it with \`seekrit log-sink test\``);
|
|
11902
|
-
});
|
|
11903
|
-
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) => {
|
|
11904
|
-
const ctx = buildContext();
|
|
11905
|
-
const ref = await resolveOrg(ctx, options.org);
|
|
11906
|
-
const result = await ctx.client.testLogSink(ref.id);
|
|
11907
|
-
emit(options, result, () => {
|
|
11908
|
-
printFields([
|
|
11909
|
-
["result", result.ok ? "ok" : "failed"],
|
|
11910
|
-
["status", result.status],
|
|
11911
|
-
["error", result.error]
|
|
11912
|
-
]);
|
|
11913
|
-
});
|
|
11914
|
-
if (!result.ok) process.exitCode = 1;
|
|
11915
|
-
});
|
|
11916
|
-
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) => {
|
|
11917
|
-
const ctx = buildContext();
|
|
11918
|
-
const ref = await resolveOrg(ctx, options.org);
|
|
11919
|
-
await confirmDestructive(options.yes, `Remove ${ref.slug}'s audit log export?`);
|
|
11920
|
-
await ctx.client.deleteLogSink(ref.id);
|
|
11921
|
-
console.error("log sink removed");
|
|
11922
|
-
});
|
|
11923
|
-
}
|
|
11924
|
-
//#endregion
|
|
11925
12083
|
//#region src/m2m.ts
|
|
11926
12084
|
/**
|
|
11927
12085
|
* Resolve M2M client credentials from (in order) the process environment, a
|
|
@@ -11989,14 +12147,243 @@ async function ensureM2mAdminToken(dotenvVars = {}) {
|
|
|
11989
12147
|
if (!creds) return null;
|
|
11990
12148
|
const cached = readCachedM2mToken(creds.clientId);
|
|
11991
12149
|
if (cached) {
|
|
11992
|
-
|
|
11993
|
-
|
|
12150
|
+
let usable = false;
|
|
12151
|
+
try {
|
|
12152
|
+
serviceTokenAuth(cached);
|
|
12153
|
+
usable = true;
|
|
12154
|
+
} catch {}
|
|
12155
|
+
if (usable) {
|
|
12156
|
+
process.env.SEEKRIT_TOKEN = cached;
|
|
12157
|
+
return cached;
|
|
12158
|
+
}
|
|
11994
12159
|
}
|
|
11995
12160
|
const token = await mintAdminToken(process.env.SEEKRIT_API_URL ?? dotenvVars.SEEKRIT_API_URL ?? readGlobalConfig().apiUrl ?? "https://api.seekrit.dev", creds);
|
|
11996
12161
|
cacheM2mToken(creds.clientId, token);
|
|
11997
12162
|
process.env.SEEKRIT_TOKEN = token;
|
|
11998
12163
|
return token;
|
|
11999
12164
|
}
|
|
12165
|
+
/** Lowercase, hyphenated, at most 40 characters — what `slugSchema` accepts. */
|
|
12166
|
+
function slugify(name) {
|
|
12167
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "") || "workspace";
|
|
12168
|
+
}
|
|
12169
|
+
/** `acme-storefront` → `Acme Storefront`: a slug-ish name as a human reads it. */
|
|
12170
|
+
function titleCase(name) {
|
|
12171
|
+
return name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
12172
|
+
}
|
|
12173
|
+
/**
|
|
12174
|
+
* The project's name, from whichever manifest the directory carries — the
|
|
12175
|
+
* package name without its npm scope, the Python or Rust package name, the last
|
|
12176
|
+
* segment of a Go module path — falling back to the directory's own name. The
|
|
12177
|
+
* manifests are read leniently (a regex, not a parser) because a malformed one
|
|
12178
|
+
* should cost the default, not the command.
|
|
12179
|
+
*/
|
|
12180
|
+
function detectProjectName(dir = process.cwd()) {
|
|
12181
|
+
const read = (file) => {
|
|
12182
|
+
const path = join(dir, file);
|
|
12183
|
+
if (!existsSync(path)) return null;
|
|
12184
|
+
try {
|
|
12185
|
+
return readFileSync(path, "utf8");
|
|
12186
|
+
} catch {
|
|
12187
|
+
return null;
|
|
12188
|
+
}
|
|
12189
|
+
};
|
|
12190
|
+
const pkg = read("package.json");
|
|
12191
|
+
if (pkg) try {
|
|
12192
|
+
const name = JSON.parse(pkg).name;
|
|
12193
|
+
if (typeof name === "string" && name.trim()) return name.replace(/^@[^/]+\//, "");
|
|
12194
|
+
} catch {}
|
|
12195
|
+
const pyName = read("pyproject.toml")?.match(/^\[project\][^[]*?^name\s*=\s*"([^"]+)"/ms)?.[1];
|
|
12196
|
+
if (pyName) return pyName;
|
|
12197
|
+
const cargoName = read("Cargo.toml")?.match(/^\[package\][^[]*?^name\s*=\s*"([^"]+)"/ms)?.[1];
|
|
12198
|
+
if (cargoName) return cargoName;
|
|
12199
|
+
const goModule = read("go.mod")?.match(/^module\s+(\S+)/m)?.[1];
|
|
12200
|
+
if (goModule) return goModule.split("/").pop() ?? goModule;
|
|
12201
|
+
return basename(dir);
|
|
12202
|
+
}
|
|
12203
|
+
/**
|
|
12204
|
+
* What `init` would sign up as, from the flags and the directory. Pure, so the
|
|
12205
|
+
* naming rules are testable without a terminal or an API.
|
|
12206
|
+
*/
|
|
12207
|
+
function planWorkspace(options, project) {
|
|
12208
|
+
const orgName = options.orgName?.trim() || titleCase(project);
|
|
12209
|
+
const orgSlug = options.org ?? slugify(options.orgName?.trim() || project);
|
|
12210
|
+
if (!slugSchema.safeParse(orgSlug).success) fail(`"${orgSlug}" is not a valid org slug (lowercase letters, numbers, and hyphens) — pass --org <slug>`);
|
|
12211
|
+
return {
|
|
12212
|
+
orgName,
|
|
12213
|
+
orgSlug
|
|
12214
|
+
};
|
|
12215
|
+
}
|
|
12216
|
+
/** Sign up, save the credential, mint the admin token, and return a context. */
|
|
12217
|
+
async function signupAndAuthenticate(options, project) {
|
|
12218
|
+
const { orgName, orgSlug } = planWorkspace(options, project);
|
|
12219
|
+
const apiUrl = options.apiUrl ?? process.env.SEEKRIT_API_URL ?? readGlobalConfig().apiUrl ?? "https://api.seekrit.dev";
|
|
12220
|
+
if (!Boolean(options.yes || options.orgName)) {
|
|
12221
|
+
const question = `No credentials found. Create a new workspace "${orgName}" (${orgSlug}) at ${apiUrl}?`;
|
|
12222
|
+
if (!process.stdin.isTTY) fail(`${question} Re-run with --yes, name it with --org-name "<the real project>", or sign in first with \`seekrit login\`.`);
|
|
12223
|
+
if (!await askYesNo(question)) fail("aborted — nothing was created");
|
|
12224
|
+
}
|
|
12225
|
+
let result;
|
|
12226
|
+
try {
|
|
12227
|
+
result = await signup(apiUrl, {
|
|
12228
|
+
orgName,
|
|
12229
|
+
orgSlug
|
|
12230
|
+
}, { client: `cli/${version}` });
|
|
12231
|
+
} catch (err) {
|
|
12232
|
+
if (err instanceof SeekritApiError) fail(`signup failed: ${err.message}`);
|
|
12233
|
+
throw err;
|
|
12234
|
+
}
|
|
12235
|
+
writeGlobalConfig({
|
|
12236
|
+
clientId: result.m2m.clientId,
|
|
12237
|
+
clientSecret: result.m2m.clientSecret,
|
|
12238
|
+
...options.apiUrl ? { apiUrl: options.apiUrl } : {}
|
|
12239
|
+
});
|
|
12240
|
+
console.error(`created workspace ${result.org.slug} (${result.org.name}) — machine credential saved to ~/.config/seekrit/config.json`);
|
|
12241
|
+
await ensureM2mAdminToken();
|
|
12242
|
+
return {
|
|
12243
|
+
ctx: buildContext(),
|
|
12244
|
+
orgSlug: result.org.slug
|
|
12245
|
+
};
|
|
12246
|
+
}
|
|
12247
|
+
async function runInit(options) {
|
|
12248
|
+
const project = detectProjectName();
|
|
12249
|
+
let ctx = tryBuildContext();
|
|
12250
|
+
let createdOrgSlug;
|
|
12251
|
+
if (!ctx) ({ctx, orgSlug: createdOrgSlug} = await signupAndAuthenticate(options, project));
|
|
12252
|
+
const orgRef = await resolveOrg(ctx, options.org ?? createdOrgSlug);
|
|
12253
|
+
const appSlug = options.app ?? slugify(project);
|
|
12254
|
+
const { apps } = await ctx.client.listApps(orgRef.id);
|
|
12255
|
+
let app = apps.find((a) => a.slug === appSlug || a.id === appSlug);
|
|
12256
|
+
if (!app) {
|
|
12257
|
+
if (!slugSchema.safeParse(appSlug).success) fail(`"${appSlug}" is not a valid app slug — pass --app <slug>`);
|
|
12258
|
+
({app} = await ctx.client.createApp(orgRef.id, {
|
|
12259
|
+
name: options.app ? titleCase(options.app) : titleCase(project),
|
|
12260
|
+
slug: appSlug
|
|
12261
|
+
}));
|
|
12262
|
+
console.error(`created app ${app.slug}`);
|
|
12263
|
+
}
|
|
12264
|
+
const { environments } = await ctx.client.listEnvs(orgRef.id, app.id);
|
|
12265
|
+
const envSlug = options.env ?? "development";
|
|
12266
|
+
let env = environments.find((e) => e.slug === envSlug || e.id === envSlug);
|
|
12267
|
+
if (!env && (options.env || environments.length === 0)) {
|
|
12268
|
+
if (!slugSchema.safeParse(envSlug).success) fail(`"${envSlug}" is not a valid environment slug — pass --env <slug>`);
|
|
12269
|
+
const dek = generateDek();
|
|
12270
|
+
const wrappedDek = await wrapDek(dek, await principalPublicKeyJwk(ctx));
|
|
12271
|
+
const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
|
|
12272
|
+
({environment: env} = await ctx.client.createEnv(orgRef.id, app.id, {
|
|
12273
|
+
name: titleCase(envSlug),
|
|
12274
|
+
slug: envSlug,
|
|
12275
|
+
wrappedDek,
|
|
12276
|
+
recoveryWrappedDek
|
|
12277
|
+
}));
|
|
12278
|
+
console.error(`created environment ${app.slug}/${env.slug} (data key generated locally)`);
|
|
12279
|
+
}
|
|
12280
|
+
const path = writeProjectConfig({
|
|
12281
|
+
org: orgRef.slug,
|
|
12282
|
+
app: app.slug
|
|
12283
|
+
});
|
|
12284
|
+
console.error(`linked ${orgRef.slug}/${app.slug} → ${path}`);
|
|
12285
|
+
if (options.fromEnv) {
|
|
12286
|
+
if (!env) fail(`--from-env needs an environment — pass --env <slug>`);
|
|
12287
|
+
if (!existsSync(options.fromEnv)) fail(`no such file: ${options.fromEnv}`);
|
|
12288
|
+
const entries = parseDotenv(readFileSync(options.fromEnv, "utf8"));
|
|
12289
|
+
const names = Object.keys(entries);
|
|
12290
|
+
if (names.length === 0) fail(`no variables found in ${options.fromEnv}`);
|
|
12291
|
+
const invalid = names.filter((name) => !secretNameSchema.safeParse(name).success);
|
|
12292
|
+
if (invalid.length > 0) fail(`invalid secret name(s): ${invalid.join(", ")} (must match [A-Za-z_][A-Za-z0-9_]*)`);
|
|
12293
|
+
const result = await importSecrets(ctx, orgRef.id, env.id, entries);
|
|
12294
|
+
if (result.queued.length > 0) console.error(`queued ${result.queued.length} change request(s) for ${app.slug}/${env.slug} — nothing is stored until a human approves them`);
|
|
12295
|
+
else console.error(`imported ${names.length} secret(s) from ${options.fromEnv} into ${app.slug}/${env.slug} — delete the file now; the values live here`);
|
|
12296
|
+
}
|
|
12297
|
+
if (options.invite) {
|
|
12298
|
+
const { invite } = await ctx.client.createInvite(orgRef.id, {
|
|
12299
|
+
email: options.invite,
|
|
12300
|
+
role: "admin"
|
|
12301
|
+
});
|
|
12302
|
+
console.error(`invited ${invite.email} as admin — they claim ${orgRef.slug} on first sign-in at the dashboard. Once they have set up keys, grant them the environment: seekrit grant --app ${app.slug} --env ${env?.slug ?? "<env>"} --user ${invite.email}`);
|
|
12303
|
+
}
|
|
12304
|
+
const target = env ? `${app.slug}/${env.slug}` : app.slug;
|
|
12305
|
+
const envFlag = env ? ` --env ${env.slug}` : " --env <slug>";
|
|
12306
|
+
console.error([
|
|
12307
|
+
"",
|
|
12308
|
+
`ready: ${orgRef.slug}/${target}`,
|
|
12309
|
+
` seekrit secrets set DATABASE_URL${envFlag} # store a value (prompted, never echoed)`,
|
|
12310
|
+
` seekrit run${envFlag} -- <your command> # inject secrets into a process`,
|
|
12311
|
+
...options.invite ? [] : [` seekrit init --invite you@example.com # hand the workspace to a human`]
|
|
12312
|
+
].join("\n"));
|
|
12313
|
+
}
|
|
12314
|
+
function registerInitCommand(program) {
|
|
12315
|
+
program.command("init").description("set this directory up: sign up if needed, create the app and a development environment, write seekrit.json").option("--org <slug>", "organization slug (an existing org, or the one to create)").option("--org-name <name>", "display name for a new workspace — the real project or company (e.g. \"Acme Storefront\")").option("--app <slug>", "application slug (defaults to the project name; created if missing)").option("--env <slug>", "environment to use or create (a new app gets `development`)").option("--from-env <file>", "import a .env file into that environment").option("--invite <email>", "invite a human as admin so they can claim the workspace").option("--api-url <url>", "API base URL").option("-y, --yes", "create a workspace without asking").action(async (options) => {
|
|
12316
|
+
await runInit(options);
|
|
12317
|
+
});
|
|
12318
|
+
}
|
|
12319
|
+
//#endregion
|
|
12320
|
+
//#region src/logsink.ts
|
|
12321
|
+
/** Collect repeated `--header Name: value` flags into a map. */
|
|
12322
|
+
function collectHeader(value, acc = {}) {
|
|
12323
|
+
const sep = value.indexOf(":");
|
|
12324
|
+
if (sep <= 0) fail(`expected "Name: value", got "${value}"`);
|
|
12325
|
+
acc[value.slice(0, sep).trim()] = value.slice(sep + 1).trim();
|
|
12326
|
+
return acc;
|
|
12327
|
+
}
|
|
12328
|
+
/**
|
|
12329
|
+
* The org's OTLP audit-log export — near-real-time shipping of every audit row
|
|
12330
|
+
* to a customer SIEM. Header *values* are write-only: they are encrypted at
|
|
12331
|
+
* rest and never echoed back, so `show` reports only the header names.
|
|
12332
|
+
*/
|
|
12333
|
+
function registerLogSinkCommands(program) {
|
|
12334
|
+
const sink = program.command("log-sink").description("stream the audit trail to your own OTLP collector (SIEM)");
|
|
12335
|
+
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) => {
|
|
12336
|
+
const ctx = buildContext();
|
|
12337
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
12338
|
+
const { sink: config } = await ctx.client.getLogSink(ref.id);
|
|
12339
|
+
emit(options, { sink: config }, () => {
|
|
12340
|
+
if (!config) {
|
|
12341
|
+
console.error("no log sink configured — set one with `seekrit log-sink set`");
|
|
12342
|
+
return;
|
|
12343
|
+
}
|
|
12344
|
+
printFields([
|
|
12345
|
+
["endpoint", config.endpoint],
|
|
12346
|
+
["enabled", config.enabled ? "yes" : "no"],
|
|
12347
|
+
["headers", config.headerNames.length > 0 ? config.headerNames.join(", ") : "none"],
|
|
12348
|
+
["last success", config.lastSuccessAt ?? "never"],
|
|
12349
|
+
["last attempt", config.lastAttemptAt ?? "never"],
|
|
12350
|
+
["last error", config.lastError ?? "none"]
|
|
12351
|
+
]);
|
|
12352
|
+
});
|
|
12353
|
+
});
|
|
12354
|
+
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) => {
|
|
12355
|
+
if (options.header && options.clearHeaders) fail("pass either --header or --clear-headers, not both");
|
|
12356
|
+
const ctx = buildContext();
|
|
12357
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
12358
|
+
const headers = options.clearHeaders ? {} : options.header;
|
|
12359
|
+
const { sink: config } = await ctx.client.setLogSink(ref.id, {
|
|
12360
|
+
endpoint,
|
|
12361
|
+
headers,
|
|
12362
|
+
enabled: !options.disabled
|
|
12363
|
+
});
|
|
12364
|
+
console.error(`log sink → ${config.endpoint} (${config.enabled ? "enabled" : "disabled"}) — test it with \`seekrit log-sink test\``);
|
|
12365
|
+
});
|
|
12366
|
+
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) => {
|
|
12367
|
+
const ctx = buildContext();
|
|
12368
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
12369
|
+
const result = await ctx.client.testLogSink(ref.id);
|
|
12370
|
+
emit(options, result, () => {
|
|
12371
|
+
printFields([
|
|
12372
|
+
["result", result.ok ? "ok" : "failed"],
|
|
12373
|
+
["status", result.status],
|
|
12374
|
+
["error", result.error]
|
|
12375
|
+
]);
|
|
12376
|
+
});
|
|
12377
|
+
if (!result.ok) process.exitCode = 1;
|
|
12378
|
+
});
|
|
12379
|
+
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) => {
|
|
12380
|
+
const ctx = buildContext();
|
|
12381
|
+
const ref = await resolveOrg(ctx, options.org);
|
|
12382
|
+
await confirmDestructive(options.yes, `Remove ${ref.slug}'s audit log export?`);
|
|
12383
|
+
await ctx.client.deleteLogSink(ref.id);
|
|
12384
|
+
console.error("log sink removed");
|
|
12385
|
+
});
|
|
12386
|
+
}
|
|
12000
12387
|
//#endregion
|
|
12001
12388
|
//#region src/mongodb.ts
|
|
12002
12389
|
/**
|
|
@@ -12352,7 +12739,7 @@ function collect$5(value, acc) {
|
|
|
12352
12739
|
* release-please-config.json), so the pin follows the crate without anyone
|
|
12353
12740
|
* remembering to move it.
|
|
12354
12741
|
*/
|
|
12355
|
-
const PROXY_VERSION = "0.
|
|
12742
|
+
const PROXY_VERSION = "0.13.0";
|
|
12356
12743
|
const BIN = "seekrit-proxy";
|
|
12357
12744
|
/**
|
|
12358
12745
|
* Host → Rust target triple.
|
|
@@ -12388,7 +12775,7 @@ function versionPrefix(version) {
|
|
|
12388
12775
|
return version.startsWith("v") ? version : `v${version}`;
|
|
12389
12776
|
}
|
|
12390
12777
|
function resolveVersion(explicit) {
|
|
12391
|
-
return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.
|
|
12778
|
+
return explicit ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.13.0";
|
|
12392
12779
|
}
|
|
12393
12780
|
function resolveBaseUrl(explicit) {
|
|
12394
12781
|
return (explicit ?? process.env.SEEKRIT_PROXY_BASE_URL ?? "https://proxy.seekrit.dev").replace(/\/+$/, "");
|
|
@@ -12546,7 +12933,7 @@ const PLUGIN_BIN = "seekrit-nomad-secrets";
|
|
|
12546
12933
|
* entry in release-please-config.json), so the pin follows the crate without
|
|
12547
12934
|
* anyone remembering to move it — the same arrangement `PROXY_VERSION` has.
|
|
12548
12935
|
*/
|
|
12549
|
-
const NOMAD_PLUGIN_VERSION = "0.
|
|
12936
|
+
const NOMAD_PLUGIN_VERSION = "0.3.0";
|
|
12550
12937
|
/** Public artifact host. Mirrors GitHub Releases; public, no egress fees. */
|
|
12551
12938
|
const DEFAULT_BASE_URL = "https://nomad.seekrit.dev";
|
|
12552
12939
|
/** Where the plugin reads its node config, matching `config::DEFAULT_CONFIG_PATH`. */
|
|
@@ -12633,7 +13020,7 @@ async function fetchBytes(url) {
|
|
|
12633
13020
|
* let Nomad fingerprint a half-written executable.
|
|
12634
13021
|
*/
|
|
12635
13022
|
async function installPlugin(pluginDir, options = {}) {
|
|
12636
|
-
const version = options.version ?? "0.
|
|
13023
|
+
const version = options.version ?? "0.3.0";
|
|
12637
13024
|
const baseUrl = (options.baseUrl ?? "https://nomad.seekrit.dev").replace(/\/+$/, "");
|
|
12638
13025
|
const { target, exe } = detectTarget();
|
|
12639
13026
|
if (exe !== "") fail("Nomad secret provider plugins are not supported on Windows clients");
|
|
@@ -14338,10 +14725,10 @@ Next:
|
|
|
14338
14725
|
});
|
|
14339
14726
|
console.log(path);
|
|
14340
14727
|
const { target } = detectTarget();
|
|
14341
|
-
process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.
|
|
14728
|
+
process.stderr.write(`seekrit-proxy ${versionPrefix(options.proxyVersion ?? "0.13.0")} (${target})\n`);
|
|
14342
14729
|
});
|
|
14343
14730
|
proxy.command("where").description("show which binary `seekrit proxy run` would use, without fetching it").option("--proxy-version <version>", `version to report (default: ${PROXY_VERSION})`).option("--json", "machine-readable output").action((options) => {
|
|
14344
|
-
const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.
|
|
14731
|
+
const version = options.proxyVersion ?? process.env.SEEKRIT_PROXY_VERSION ?? "0.13.0";
|
|
14345
14732
|
const override = process.env.SEEKRIT_PROXY_BIN;
|
|
14346
14733
|
const { target, exe } = detectTarget();
|
|
14347
14734
|
const path = override ?? proxyBinaryPath(version, target, exe);
|
|
@@ -14400,7 +14787,7 @@ Next:
|
|
|
14400
14787
|
process.stdout.write(renderComposeSnippet(plan, {
|
|
14401
14788
|
service: options.service,
|
|
14402
14789
|
workload: options.workload,
|
|
14403
|
-
image: options.image ?? `seekritdev/proxy:0.
|
|
14790
|
+
image: options.image ?? `seekritdev/proxy:0.13.0`,
|
|
14404
14791
|
publish: Boolean(options.publish)
|
|
14405
14792
|
}));
|
|
14406
14793
|
});
|
|
@@ -15863,18 +16250,7 @@ program.command("keys").description("manage your encryption keys").command("setu
|
|
|
15863
16250
|
});
|
|
15864
16251
|
console.error("keys generated and uploaded — your passphrase never leaves this machine");
|
|
15865
16252
|
});
|
|
15866
|
-
program
|
|
15867
|
-
const ctx = buildContext();
|
|
15868
|
-
const org = await resolveOrg(ctx, options.org);
|
|
15869
|
-
const { apps } = await ctx.client.listApps(org.id);
|
|
15870
|
-
const app = apps.find((a) => a.slug === options.app || a.id === options.app);
|
|
15871
|
-
if (!app) fail(`no app "${options.app}" in ${org.slug}`);
|
|
15872
|
-
const path = writeProjectConfig({
|
|
15873
|
-
org: org.slug,
|
|
15874
|
-
app: app.slug
|
|
15875
|
-
});
|
|
15876
|
-
console.error(`linked ${org.slug}/${app.slug} → ${path} (environment is selected by the service token at runtime)`);
|
|
15877
|
-
});
|
|
16253
|
+
registerInitCommand(program);
|
|
15878
16254
|
registerOrgCommands(program);
|
|
15879
16255
|
registerAppCommands(program);
|
|
15880
16256
|
registerGroupCommands(program);
|
|
@@ -16270,6 +16646,11 @@ program.command("export").description("print decrypted secrets (dotenv, json, or
|
|
|
16270
16646
|
registerAccessCommands(program);
|
|
16271
16647
|
registerHoneyTokenCommands(program);
|
|
16272
16648
|
const token = program.command("token").description("manage service tokens (CI, docker, agents)");
|
|
16649
|
+
token.command("auth").description("print only the v2 authentication component for a manual API request").action(() => {
|
|
16650
|
+
const ctx = buildContext();
|
|
16651
|
+
if (ctx.auth.type !== "bearer" || !isServiceToken(ctx.auth.token)) fail("token auth requires a v2 service token in SEEKRIT_TOKEN or your saved configuration");
|
|
16652
|
+
console.log(serviceTokenAuth(ctx.auth.token));
|
|
16653
|
+
});
|
|
16273
16654
|
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) => {
|
|
16274
16655
|
const ctx = buildContext();
|
|
16275
16656
|
const role = options.admin ? "admin" : "member";
|
|
@@ -16367,7 +16748,7 @@ registerKmsCommands(program);
|
|
|
16367
16748
|
registerRotationCommands(program);
|
|
16368
16749
|
registerRecoveryCommands(program);
|
|
16369
16750
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
16370
|
-
const { runMcpServer } = await import("./mcp-
|
|
16751
|
+
const { runMcpServer } = await import("./mcp-D7rsPeBQ.js");
|
|
16371
16752
|
await runMcpServer();
|
|
16372
16753
|
});
|
|
16373
16754
|
registerAuditCommands(program);
|
|
@@ -16382,4 +16763,4 @@ program.parseAsync(argv).catch((err) => {
|
|
|
16382
16763
|
fail(describeError(err));
|
|
16383
16764
|
});
|
|
16384
16765
|
//#endregion
|
|
16385
|
-
export { signatureKeyRef as A, generateDek as B,
|
|
16766
|
+
export { signatureKeyRef as A, generateDek as B, version as C, importSigningKey as D, generateSigningKeyMaterial as E, generateEncryptKeyMaterial as F, parseBranchTtl as H, kmsBlobKeyRef as I, kmsDecrypt as L, generatePostgresCredential as M, generateMysqlCredential as N, importVerifyingKey as O, generateDataKey as P, kmsEncrypt as R, writeProjectConfig as S, parseServiceToken as T, toBase64 as V, getDek as _, kmsResolveRecipient as a, tryBuildContext as b, fetchDecryptedSecrets as c, resolveApp as d, resolveAppEnv as f, resolveOrg as g, resolveGroup as h, kmsResolveKey as i, verifyMessage as j, signMessage as k, fetchDecryptedVersion as l, resolveEnvTarget as m, kmsCallerIdentity as n, syncGrantsForGroupLink as o, resolveBranch as p, kmsRecoverMaterial as r, encryptAndSetSecret as s, ensureM2mAdminToken as t, materializeEnv as u, isTokenAuth as v, createServiceToken as w, setFailThrows as x, principalPublicKeyJwk as y, wrapDek as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as signatureKeyRef, B as generateDek, C as
|
|
1
|
+
import { A as signatureKeyRef, B as generateDek, C as version, D as importSigningKey, E as generateSigningKeyMaterial, F as generateEncryptKeyMaterial, H as parseBranchTtl, I as kmsBlobKeyRef, L as kmsDecrypt, M as generatePostgresCredential, N as generateMysqlCredential, O as importVerifyingKey, P as generateDataKey, R as kmsEncrypt, S as writeProjectConfig, T as parseServiceToken, V as toBase64, _ as getDek, a as kmsResolveRecipient, b as tryBuildContext, c as fetchDecryptedSecrets, d as resolveApp, f as resolveAppEnv, g as resolveOrg, h as resolveGroup, i as kmsResolveKey, j as verifyMessage, k as signMessage, l as fetchDecryptedVersion, m as resolveEnvTarget, n as kmsCallerIdentity, o as syncGrantsForGroupLink, p as resolveBranch, r as kmsRecoverMaterial, s as encryptAndSetSecret, t as ensureM2mAdminToken, u as materializeEnv, v as isTokenAuth, w as createServiceToken, x as setFailThrows, y as principalPublicKeyJwk, z as wrapDek } from "./index.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -50,9 +50,18 @@ function serverInstructions() {
|
|
|
50
50
|
"Auth comes from SEEKRIT_TOKEN (a skt_… token), SEEKRIT_CLIENT_ID +",
|
|
51
51
|
"SEEKRIT_CLIENT_SECRET (machine credentials — an admin token is minted and",
|
|
52
52
|
"cached automatically on first use), or the config saved by `seekrit login`.",
|
|
53
|
+
"No credential at all? `npx -y @seekrit/cli init --org-name \"<the real project>\" --yes`",
|
|
54
|
+
"signs up, saves the credential, and creates an app with a development environment.",
|
|
53
55
|
"Decrypting under a user session (not a token) additionally needs",
|
|
54
56
|
"SEEKRIT_PASSPHRASE set in this server's env — there's no TTY to prompt on.",
|
|
55
57
|
"",
|
|
58
|
+
"NOT THE TOOL WHEN: the value is a throwaway for one local experiment (a shell",
|
|
59
|
+
"variable is fine — say so); the runtime already injects secrets natively and the",
|
|
60
|
+
"team uses it (offer seekrit as the source that syncs into it, not a second copy);",
|
|
61
|
+
"the policy forbids any SaaS holding even ciphertext; or what's being stored is",
|
|
62
|
+
"user data rather than configuration. An honest \"you don't need this here\" beats",
|
|
63
|
+
"an install.",
|
|
64
|
+
"",
|
|
56
65
|
"Model: org → apps/groups → environments → secrets. Call `whoami` first, then",
|
|
57
66
|
"`get_started` for the recommended end-to-end recipe. Prefer `run_command` over",
|
|
58
67
|
"`get_secret reveal:true` — it injects secrets into a subprocess so plaintext",
|
|
@@ -119,21 +128,6 @@ function getCtx() {
|
|
|
119
128
|
function ensureDecryptable(ctx) {
|
|
120
129
|
if (!isTokenAuth(ctx) && !process.env.SEEKRIT_PASSPHRASE) throw new Error("this operation decrypts data; set SEEKRIT_PASSPHRASE in the MCP server env, or use a service token (SEEKRIT_TOKEN)");
|
|
121
130
|
}
|
|
122
|
-
/**
|
|
123
|
-
* The calling principal's public key JWK — needed to wrap a freshly generated
|
|
124
|
-
* DEK when creating an environment. Service tokens carry their private key, so
|
|
125
|
-
* we derive the public half from it; users publish theirs at key setup.
|
|
126
|
-
*/
|
|
127
|
-
async function principalPublicKeyJwk(ctx) {
|
|
128
|
-
if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
|
|
129
|
-
const { privateKey } = await parseServiceToken(ctx.auth.token);
|
|
130
|
-
const { d: _d, key_ops: _ops, ext: _ext, ...pub } = await crypto.subtle.exportKey("jwk", privateKey);
|
|
131
|
-
return JSON.stringify(pub);
|
|
132
|
-
}
|
|
133
|
-
const { user } = await ctx.client.me();
|
|
134
|
-
if (!user.publicKeyJwk) throw new Error("this user has no keypair yet — run `seekrit keys setup` first");
|
|
135
|
-
return user.publicKeyJwk;
|
|
136
|
-
}
|
|
137
131
|
/** Run a child command with `env` and capture its output (values never returned). */
|
|
138
132
|
function runChild(cmd, args, env, cwd) {
|
|
139
133
|
return new Promise((resolve, reject) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekrit/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
|
|
5
|
+
"homepage": "https://seekrit.dev/docs/guides/cli",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"seekrit",
|
|
8
|
+
"secrets",
|
|
9
|
+
"secrets-manager",
|
|
10
|
+
"dotenv",
|
|
11
|
+
"env",
|
|
12
|
+
"credentials",
|
|
13
|
+
"api-keys",
|
|
14
|
+
"zero-knowledge",
|
|
15
|
+
"end-to-end-encryption",
|
|
16
|
+
"mcp",
|
|
17
|
+
"ai-agents",
|
|
18
|
+
"credential-broker",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
5
21
|
"type": "module",
|
|
6
22
|
"publishConfig": {
|
|
7
23
|
"access": "public"
|
|
@@ -24,15 +40,15 @@
|
|
|
24
40
|
"dependencies": {
|
|
25
41
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
26
42
|
"commander": "^15.0.0",
|
|
27
|
-
"zod": "^4.6.
|
|
43
|
+
"zod": "^4.6.7"
|
|
28
44
|
},
|
|
29
45
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^26.6.
|
|
31
|
-
"tsdown": "^0.23.
|
|
32
|
-
"vitest": "^5.0.
|
|
46
|
+
"@types/node": "^26.6.5",
|
|
47
|
+
"tsdown": "^0.23.1",
|
|
48
|
+
"vitest": "^5.0.5",
|
|
49
|
+
"@seekrit/crypto": "0.0.1",
|
|
33
50
|
"@seekrit/api-client": "0.0.1",
|
|
34
|
-
"@seekrit/core": "0.0.1"
|
|
35
|
-
"@seekrit/crypto": "0.0.1"
|
|
51
|
+
"@seekrit/core": "0.0.1"
|
|
36
52
|
},
|
|
37
53
|
"scripts": {
|
|
38
54
|
"build": "tsdown",
|