@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 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. Link the current directory to an org / app. Writes seekrit.json, which is
34
- # safe to commit. The environment comes from the token at runtime.
35
- seekrit init --org acme --app storefront
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` | Link this directory to an org/app (`seekrit.json`). The environment comes from the token. |
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: the token string itself carries the private key, so the server
5449
- * never holds it. The server stores only the SHA-256 hash of the full token
5450
- * (for authentication) and the public key (for wrapping DEK grants).
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
- * Format: `skt_<token id>_<private key pkcs8, base64url>`
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}_${toBase64Url(pkcs8)}`;
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 = "1.9.4";
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") headers.authorization = `Basic ${btoa(`${this.auth.clientId}:${this.auth.clientSecret}`)}`;
5588
- else headers["x-seekrit-dev-user"] = this.auth.email;
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
- fail(`specify --env — this token has no environment of its own to fall back to (${err instanceof Error ? err.message : String(err)})`);
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)) return boundEnvTarget(ctx, opts);
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, user.publicKeyJwk);
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
- process.env.SEEKRIT_TOKEN = cached;
11993
- return cached;
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.12.5";
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.12.5";
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.2.3";
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.2.3";
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.12.5")} (${target})\n`);
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.12.5";
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.12.5`,
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.command("init").description("link this directory to an org/app (environment comes from the token)").requiredOption("--org <slug>", "organization slug").requiredOption("--app <slug>", "application slug").action(async (options) => {
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-ZYu1Q6Ft.js");
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, createServiceToken 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, version as S, parseServiceToken as T, toBase64 as V, getDek as _, kmsResolveRecipient as a, setFailThrows 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, isServiceToken as w, writeProjectConfig as x, tryBuildContext as y, wrapDek as z };
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 createServiceToken, 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 version, T as parseServiceToken, V as toBase64, _ as getDek, a as kmsResolveRecipient, b as setFailThrows, 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 isServiceToken, x as writeProjectConfig, y as tryBuildContext, z as wrapDek } from "./index.js";
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": "1.9.4",
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.5"
43
+ "zod": "^4.6.7"
28
44
  },
29
45
  "devDependencies": {
30
- "@types/node": "^26.6.4",
31
- "tsdown": "^0.23.0",
32
- "vitest": "^5.0.3",
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",