@seekrit/cli 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2086,6 +2086,8 @@ const AUDIT_ACTIONS = [
2086
2086
  "org.created",
2087
2087
  "org.updated",
2088
2088
  "org.member_added",
2089
+ "org.member_role_changed",
2090
+ "app.access_granted",
2089
2091
  "org.member_removed",
2090
2092
  "org.member_invited",
2091
2093
  "org.invite_revoked",
@@ -2308,7 +2310,7 @@ const nameSchema = z.string().trim().min(1).max(128);
2308
2310
  const emailSchema = z.string().trim().toLowerCase().pipe(z.email().max(320));
2309
2311
  /** Env-var style secret name: FOO, DATABASE_URL, apiKey2 … */
2310
2312
  const secretNameSchema = z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be a valid environment variable name");
2311
- z.enum([
2313
+ const orgRoleSchema = z.enum([
2312
2314
  "owner",
2313
2315
  "admin",
2314
2316
  "member"
@@ -2347,7 +2349,20 @@ z.object({
2347
2349
  name: nameSchema,
2348
2350
  slug: slugSchema
2349
2351
  });
2352
+ /** Compose a group into an application environment (matched by env slug). */
2353
+ const appAccessKeySchema = z.object({
2354
+ environmentId: z.string().min(1),
2355
+ userId: z.string().min(1),
2356
+ wrappedDek: z.string().min(1).max(8192)
2357
+ });
2358
+ const appAccessGrantsSchema = z.array(appAccessKeySchema).max(5e3).optional();
2350
2359
  z.object({
2360
+ userId: z.string().min(1),
2361
+ grants: z.array(appAccessKeySchema).max(5e3)
2362
+ });
2363
+ z.object({ role: orgRoleSchema });
2364
+ z.object({
2365
+ appAccessGrants: appAccessGrantsSchema,
2351
2366
  groupId: z.string().min(1),
2352
2367
  /** Precedence among an env's groups (higher wins). Appended if omitted. */
2353
2368
  position: z.number().int().min(0).optional(),
@@ -2358,6 +2373,11 @@ z.object({
2358
2373
  })).optional()
2359
2374
  });
2360
2375
  z.object({
2376
+ /** Wrap the new DEK to all persistent app recipients. */
2377
+ userGrants: z.array(z.object({
2378
+ userId: z.string().min(1),
2379
+ wrappedDek: z.string().min(1).max(8192)
2380
+ })).max(5e3).optional(),
2361
2381
  name: nameSchema,
2362
2382
  slug: slugSchema,
2363
2383
  /** Environment DEK wrapped to the creator's public key — created client-side. */
@@ -2419,7 +2439,23 @@ z.object({
2419
2439
  /** The same DEK wrapped to each of the parent's existing grant-holders. */
2420
2440
  grants: z.array(grantEnvironmentKeySchema).max(500).default([])
2421
2441
  });
2442
+ /**
2443
+ * Token creation is client-driven so the server never sees the token secret:
2444
+ * the client generates the keypair + token string and submits only the hash
2445
+ * and public key.
2446
+ */
2447
+ const environmentGrantBatchSchema = z.array(z.object({
2448
+ environmentId: z.string().min(1),
2449
+ wrappedDek: z.string().min(1).max(8192)
2450
+ })).max(5e3);
2451
+ z.object({
2452
+ principalType: principalTypeSchema,
2453
+ principalId: z.string().min(1),
2454
+ grants: environmentGrantBatchSchema
2455
+ });
2422
2456
  z.object({
2457
+ /** Commit the token and all default resolve keys together, when supplied. */
2458
+ initialGrants: environmentGrantBatchSchema.optional(),
2423
2459
  name: nameSchema,
2424
2460
  tokenId: z.string().regex(/^skt_[0-9A-Za-z]+$/),
2425
2461
  /** SHA-256 hash (base64url) of the full token string. */
@@ -5700,7 +5736,7 @@ async function createAgentTaskToken() {
5700
5736
  }
5701
5737
  //#endregion
5702
5738
  //#region package.json
5703
- var version = "2.1.0";
5739
+ var version = "2.2.0";
5704
5740
  //#endregion
5705
5741
  //#region ../../packages/api-client/src/index.ts
5706
5742
  /**
@@ -5837,6 +5873,56 @@ var SeekritClient = class {
5837
5873
  listMembers(orgId) {
5838
5874
  return this.request("GET", `/v1/orgs/${orgId}/members`);
5839
5875
  }
5876
+ updateMemberRole(orgId, userId, role) {
5877
+ return this.request("PATCH", `/v1/orgs/${orgId}/members/${userId}`, { role });
5878
+ }
5879
+ getAppAccess(orgId, appId) {
5880
+ return this.request("GET", `/v1/orgs/${orgId}/apps/${appId}/access`);
5881
+ }
5882
+ provisionAppAccess(orgId, appId, userId, grants) {
5883
+ return this.request("POST", `/v1/orgs/${orgId}/apps/${appId}/access`, {
5884
+ userId,
5885
+ grants
5886
+ });
5887
+ }
5888
+ accessRecipients(orgId, target) {
5889
+ const path = "appId" in target ? `apps/${target.appId}` : `groups/${target.groupId}`;
5890
+ return this.request("GET", `/v1/orgs/${orgId}/${path}/access-recipients`);
5891
+ }
5892
+ /** The caller owns the new DEK; only public keys and wrapped blobs cross the API. */
5893
+ async wrapNewEnvironmentAccess(orgId, target, dek) {
5894
+ const { recipients } = await this.accessRecipients(orgId, target);
5895
+ return Promise.all(recipients.map(async (r) => {
5896
+ if (!r.publicKeyJwk) throw new Error(`${r.email} must finish key setup before creating this environment`);
5897
+ return {
5898
+ userId: r.userId,
5899
+ wrappedDek: await wrapDek(dek, r.publicKeyJwk)
5900
+ };
5901
+ }));
5902
+ }
5903
+ async wrapGroupAppAccess(orgId, envId, groupId, getDek) {
5904
+ const { environment } = await this.getEnv(orgId, envId);
5905
+ if (!environment.applicationId) throw new Error("only app environments can compose groups");
5906
+ const { recipients } = await this.accessRecipients(orgId, { appId: environment.applicationId });
5907
+ if (!recipients.length) return [];
5908
+ const { environments } = await this.listGroupEnvs(orgId, groupId);
5909
+ const result = [];
5910
+ for (const env of environments) {
5911
+ const { grants } = await this.listEnvKeys(orgId, env.id);
5912
+ const missing = recipients.filter((r) => !grants.some((g) => g.principalType === "user" && g.principalId === r.userId));
5913
+ if (!missing.length) continue;
5914
+ const dek = await getDek(env.id);
5915
+ for (const recipient of missing) {
5916
+ if (!recipient.publicKeyJwk) throw new Error(`${recipient.email} must finish key setup first`);
5917
+ result.push({
5918
+ environmentId: env.id,
5919
+ userId: recipient.userId,
5920
+ wrappedDek: await wrapDek(dek, recipient.publicKeyJwk)
5921
+ });
5922
+ }
5923
+ }
5924
+ return result;
5925
+ }
5840
5926
  /**
5841
5927
  * The org-wide "require a second factor for all members" policy. `configured`
5842
5928
  * is false when the identity provider isn't wired up (local dev), in which
@@ -6021,6 +6107,46 @@ var SeekritClient = class {
6021
6107
  return this.request("POST", `/v1/orgs/${orgId}/change-requests/${id}/withdraw`);
6022
6108
  }
6023
6109
  /** The calling principal's wrapped DEK for this environment. */
6110
+ getEnvironmentKeyScope(orgId, envId) {
6111
+ return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key-scope`);
6112
+ }
6113
+ async prepareComposedKeyGrants(orgId, envId, principal, getDek) {
6114
+ const scope = await this.getEnvironmentKeyScope(orgId, envId);
6115
+ const grants = [];
6116
+ for (const layer of scope.environments) {
6117
+ if (scope.grants.some((g) => g.environmentId === layer.environmentId && g.principalType === principal.principalType && g.principalId === principal.principalId)) continue;
6118
+ try {
6119
+ const dek = await getDek(layer.environmentId);
6120
+ grants.push({
6121
+ environmentId: layer.environmentId,
6122
+ wrappedDek: await wrapDek(dek, principal.publicKeyJwk)
6123
+ });
6124
+ } catch (error) {
6125
+ if (error instanceof Error && error.message === "unlock cancelled") throw error;
6126
+ throw new Error(`Could not prepare access to ${layer.label}: ${error instanceof Error ? error.message : "key unavailable"}. No keys were granted.`);
6127
+ }
6128
+ }
6129
+ return grants;
6130
+ }
6131
+ grantComposedKeys(orgId, envId, input) {
6132
+ return this.request("POST", `/v1/orgs/${orgId}/envs/${envId}/keys/composed`, input);
6133
+ }
6134
+ /** Sharing an application environment includes the exact layers resolve reads. */
6135
+ async grantEnvironmentAccess(orgId, envId, principal, getDek) {
6136
+ const { environment } = await this.getEnv(orgId, envId);
6137
+ if (environment.applicationId) {
6138
+ const grants = await this.prepareComposedKeyGrants(orgId, envId, principal, getDek);
6139
+ await this.grantComposedKeys(orgId, envId, {
6140
+ principalType: principal.principalType,
6141
+ principalId: principal.principalId,
6142
+ grants
6143
+ });
6144
+ } else await this.grantEnvKey(orgId, envId, {
6145
+ principalType: principal.principalType,
6146
+ principalId: principal.principalId,
6147
+ wrappedDek: await wrapDek(await getDek(envId), principal.publicKeyJwk)
6148
+ });
6149
+ }
6024
6150
  getMyEnvKey(orgId, envId) {
6025
6151
  return this.request("GET", `/v1/orgs/${orgId}/envs/${envId}/key`);
6026
6152
  }
@@ -7154,12 +7280,11 @@ function registerAccessCommands(program) {
7154
7280
  const ctx = buildContext();
7155
7281
  const { orgId, envId, label } = await resolveEnvTarget(ctx, options);
7156
7282
  const principal = await resolvePrincipal(ctx, orgId, options);
7157
- const dek = await getDek(ctx, orgId, envId);
7158
- await ctx.client.grantEnvKey(orgId, envId, {
7283
+ await ctx.client.grantEnvironmentAccess(orgId, envId, {
7159
7284
  principalType: principal.type,
7160
7285
  principalId: principal.id,
7161
- wrappedDek: await wrapDek(dek, principal.publicKeyJwk)
7162
- });
7286
+ publicKeyJwk: principal.publicKeyJwk
7287
+ }, (id) => getDek(ctx, orgId, id));
7163
7288
  console.error(`granted ${label} access to ${principal.id}`);
7164
7289
  });
7165
7290
  withEnvTarget(grant.command("list").alias("ls").description("list who holds a key for an environment").option("--json", "print the raw API response")).action(async (options) => {
@@ -9013,6 +9138,7 @@ function registerAppCommands(program) {
9013
9138
  const wrappedDek = await wrapDek(dek, await principalPublicKeyJwk(ctx));
9014
9139
  const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
9015
9140
  const created = await ctx.client.createEnv(orgRef.id, appRow.id, {
9141
+ userGrants: await ctx.client.wrapNewEnvironmentAccess(orgRef.id, { appId: appRow.id }, dek),
9016
9142
  name: options.name,
9017
9143
  slug: options.slug,
9018
9144
  wrappedDek,
@@ -9038,6 +9164,7 @@ function registerAppCommands(program) {
9038
9164
  });
9039
9165
  const syncGrants = await syncGrantsForGroupLink(ctx, target.orgId, target.envId, target.envSlug, group.id);
9040
9166
  await ctx.client.linkEnvGroup(target.orgId, target.envId, {
9167
+ appAccessGrants: await ctx.client.wrapGroupAppAccess(target.orgId, target.envId, group.id, (id) => getDek(ctx, target.orgId, id)),
9041
9168
  groupId: group.id,
9042
9169
  position: options.position === void 0 ? void 0 : Number.parseInt(options.position, 10),
9043
9170
  syncGrants
@@ -11807,6 +11934,7 @@ function registerGroupCommands(program) {
11807
11934
  const wrappedDek = await wrapDek(dek, user.publicKeyJwk);
11808
11935
  const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, groupRef.orgId, dek);
11809
11936
  const created = await ctx.client.createGroupEnv(groupRef.orgId, groupRef.id, {
11937
+ userGrants: await ctx.client.wrapNewEnvironmentAccess(groupRef.orgId, { groupId: groupRef.id }, dek),
11810
11938
  name: options.name,
11811
11939
  slug: options.slug,
11812
11940
  wrappedDek,
@@ -12270,6 +12398,7 @@ async function runInit(options) {
12270
12398
  const wrappedDek = await wrapDek(dek, await principalPublicKeyJwk(ctx));
12271
12399
  const recoveryWrappedDek = await recoveryWrapForNewEnv(ctx, orgRef.id, dek);
12272
12400
  ({environment: env} = await ctx.client.createEnv(orgRef.id, app.id, {
12401
+ userGrants: await ctx.client.wrapNewEnvironmentAccess(orgRef.id, { appId: app.id }, dek),
12273
12402
  name: titleCase(envSlug),
12274
12403
  slug: envSlug,
12275
12404
  wrappedDek,
@@ -16826,42 +16955,43 @@ token.command("create").description("create a service token (runtime, or --admin
16826
16955
  if (boundToEnv && !(options.app && options.env)) fail("pass both --app and --env to bind a token to an environment");
16827
16956
  const target = boundToEnv ? await resolveAppEnv(ctx, options) : { orgId: (await resolveOrg(ctx, options.org)).id };
16828
16957
  const created = await createServiceToken();
16958
+ const granted = boundToEnv && options.grant !== false;
16959
+ const initialGrants = granted && "envId" in target ? await ctx.client.prepareComposedKeyGrants(target.orgId, target.envId, {
16960
+ principalType: "service_token",
16961
+ principalId: created.tokenId,
16962
+ publicKeyJwk: created.publicKeyJwk
16963
+ }, (id) => getDek(ctx, target.orgId, id)) : void 0;
16964
+ const extraGrants = [];
16965
+ if (granted) for (const [groupSlug, envSlug] of Object.entries(options.allow ?? {})) {
16966
+ const groupRef = await resolveGroup(ctx, {
16967
+ org: options.org,
16968
+ group: groupSlug
16969
+ });
16970
+ const { environments } = await ctx.client.listGroupEnvs(target.orgId, groupRef.id);
16971
+ const slice = environments.find((e) => e.slug === envSlug);
16972
+ if (!slice) fail(`group "${groupSlug}" has no "${envSlug}" environment`);
16973
+ extraGrants.push({
16974
+ environmentId: slice.id,
16975
+ wrappedDek: await wrapDek(await getDek(ctx, target.orgId, slice.id), created.publicKeyJwk)
16976
+ });
16977
+ }
16829
16978
  await ctx.client.createToken(target.orgId, {
16830
16979
  name: options.name,
16831
16980
  tokenId: created.tokenId,
16832
16981
  tokenHash: created.tokenHash,
16833
16982
  publicKeyJwk: created.publicKeyJwk,
16834
16983
  role,
16835
- environmentId: "envId" in target ? target.envId : null
16984
+ environmentId: "envId" in target ? target.envId : null,
16985
+ initialGrants
16836
16986
  });
16837
- const grantEnv = async (envId) => {
16838
- const dek = await getDek(ctx, target.orgId, envId);
16839
- await ctx.client.grantEnvKey(target.orgId, envId, {
16987
+ for (const extra of extraGrants) try {
16988
+ await ctx.client.grantEnvKey(target.orgId, extra.environmentId, {
16840
16989
  principalType: "service_token",
16841
16990
  principalId: created.tokenId,
16842
- wrappedDek: await wrapDek(dek, created.publicKeyJwk)
16991
+ wrappedDek: extra.wrappedDek
16843
16992
  });
16844
- };
16845
- const granted = boundToEnv && options.grant !== false;
16846
- if (granted && "envId" in target) {
16847
- await grantEnv(target.envId);
16848
- const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
16849
- for (const g of groups) {
16850
- const { environments } = await ctx.client.listGroupEnvs(target.orgId, g.groupId);
16851
- const slice = environments.find((e) => e.slug === target.envSlug);
16852
- if (!slice) fail(`group "${g.slug}" has no "${target.envSlug}" environment — create it before granting`);
16853
- await grantEnv(slice.id);
16854
- }
16855
- for (const [groupSlug, envSlug] of Object.entries(options.allow ?? {})) {
16856
- const groupRef = await resolveGroup(ctx, {
16857
- org: options.org,
16858
- group: groupSlug
16859
- });
16860
- const { environments } = await ctx.client.listGroupEnvs(target.orgId, groupRef.id);
16861
- const slice = environments.find((e) => e.slug === envSlug);
16862
- if (!slice) fail(`group "${groupSlug}" has no "${envSlug}" environment`);
16863
- await grantEnv(slice.id);
16864
- }
16993
+ } catch (error) {
16994
+ console.error(`Token created with its default environment keys, but extra grant ${extra.environmentId} failed: ${error instanceof Error ? error.message : "unknown error"}`);
16865
16995
  }
16866
16996
  const scope = "appSlug" in target ? `${target.appSlug}/${target.envSlug}` : "org-scoped (admin)";
16867
16997
  console.error(`${role} token created${granted ? " and granted" : ""} for ${scope} — save it now, it is not stored:`);
@@ -16915,7 +17045,7 @@ registerKmsCommands(program);
16915
17045
  registerRotationCommands(program);
16916
17046
  registerRecoveryCommands(program);
16917
17047
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
16918
- const { runMcpServer } = await import("./mcp-DZoBFmDF.js");
17048
+ const { runMcpServer } = await import("./mcp-Dgj-7S9y.js");
16919
17049
  await runMcpServer();
16920
17050
  });
16921
17051
  registerAuditCommands(program);
@@ -527,7 +527,8 @@ async function runMcpServer(options = {}) {
527
527
  return (await ctx.client.createEnv(orgRef.id, appRow.id, {
528
528
  name,
529
529
  slug,
530
- wrappedDek
530
+ wrappedDek,
531
+ userGrants: await ctx.client.wrapNewEnvironmentAccess(orgRef.id, { appId: appRow.id }, dek)
531
532
  })).environment;
532
533
  });
533
534
  tool("create_branch", "Fork an environment into an ephemeral branch (a per-PR / preview config). The branch inherits the parent's secrets by layering at read time — nothing is copied or re-encrypted — and holds only the values you override on it. Generates the branch's data key locally, grants it to the caller, and shares it with the parent's other readers.", { idempotent: false }, {
@@ -595,7 +596,8 @@ async function runMcpServer(options = {}) {
595
596
  return (await ctx.client.createGroupEnv(g.orgId, g.id, {
596
597
  name,
597
598
  slug,
598
- wrappedDek
599
+ wrappedDek,
600
+ userGrants: await ctx.client.wrapNewEnvironmentAccess(g.orgId, { groupId: g.id }, dek)
599
601
  })).environment;
600
602
  });
601
603
  tool("compose_group", "Compose a group into an application environment (higher position wins on name clashes).", { idempotent: true }, {
@@ -617,6 +619,7 @@ async function runMcpServer(options = {}) {
617
619
  });
618
620
  const syncGrants = await syncGrantsForGroupLink(ctx, target.orgId, target.envId, target.envSlug, g.id);
619
621
  return (await ctx.client.linkEnvGroup(target.orgId, target.envId, {
622
+ appAccessGrants: await ctx.client.wrapGroupAppAccess(target.orgId, target.envId, g.id, (id) => getDek(ctx, target.orgId, id)),
620
623
  groupId: g.id,
621
624
  position,
622
625
  syncGrants
@@ -819,32 +822,21 @@ async function runMcpServer(options = {}) {
819
822
  env
820
823
  }) : { orgId: (await resolveOrg(ctx, org)).id };
821
824
  const created = await createServiceToken();
825
+ if (boundToEnv) ensureDecryptable(ctx);
826
+ const initialGrants = "envId" in target ? await ctx.client.prepareComposedKeyGrants(target.orgId, target.envId, {
827
+ principalType: "service_token",
828
+ principalId: created.tokenId,
829
+ publicKeyJwk: created.publicKeyJwk
830
+ }, (id) => getDek(ctx, target.orgId, id)) : void 0;
822
831
  await ctx.client.createToken(target.orgId, {
823
832
  name,
824
833
  tokenId: created.tokenId,
825
834
  tokenHash: created.tokenHash,
826
835
  publicKeyJwk: created.publicKeyJwk,
827
836
  role,
828
- environmentId: "envId" in target ? target.envId : null
837
+ environmentId: "envId" in target ? target.envId : null,
838
+ initialGrants
829
839
  });
830
- if (boundToEnv && "envId" in target) {
831
- ensureDecryptable(ctx);
832
- const grantEnv = async (envId) => {
833
- const dek = await getDek(ctx, target.orgId, envId);
834
- await ctx.client.grantEnvKey(target.orgId, envId, {
835
- principalType: "service_token",
836
- principalId: created.tokenId,
837
- wrappedDek: await wrapDek(dek, created.publicKeyJwk)
838
- });
839
- };
840
- await grantEnv(target.envId);
841
- const { groups } = await ctx.client.listEnvGroups(target.orgId, target.envId);
842
- for (const g of groups) {
843
- const { environments } = await ctx.client.listGroupEnvs(target.orgId, g.groupId);
844
- const slice = environments.find((e) => e.slug === target.envSlug);
845
- if (slice) await grantEnv(slice.id);
846
- }
847
- }
848
840
  return {
849
841
  token: created.token,
850
842
  tokenId: created.tokenId,
@@ -873,7 +865,6 @@ async function runMcpServer(options = {}) {
873
865
  const ctx = getCtx();
874
866
  ensureDecryptable(ctx);
875
867
  const { orgId, envId, label } = await resolveEnvTarget(ctx, o);
876
- const dek = await getDek(ctx, orgId, envId);
877
868
  let principalType;
878
869
  let principalId;
879
870
  let publicKeyJwk;
@@ -897,11 +888,11 @@ async function runMcpServer(options = {}) {
897
888
  t.publicKeyJwk
898
889
  ];
899
890
  }
900
- await ctx.client.grantEnvKey(orgId, envId, {
891
+ await ctx.client.grantEnvironmentAccess(orgId, envId, {
901
892
  principalType,
902
893
  principalId,
903
- wrappedDek: await wrapDek(dek, publicKeyJwk)
904
- });
894
+ publicKeyJwk
895
+ }, (id) => getDek(ctx, orgId, id));
905
896
  return {
906
897
  ok: true,
907
898
  env: label,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "homepage": "https://seekrit.dev/docs/guides/cli",
6
6
  "keywords": [