@clef-sh/core 0.1.15 → 0.1.16

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.mjs CHANGED
@@ -2638,6 +2638,12 @@ var ManifestParser = class {
2638
2638
  "service_identities"
2639
2639
  );
2640
2640
  }
2641
+ if (siObj.pack_only !== void 0 && typeof siObj.pack_only !== "boolean") {
2642
+ throw new ManifestValidationError(
2643
+ `Service identity '${siName}' has a non-boolean 'pack_only' field.`,
2644
+ "service_identities"
2645
+ );
2646
+ }
2641
2647
  if (!Array.isArray(siObj.namespaces) || siObj.namespaces.length === 0) {
2642
2648
  throw new ManifestValidationError(
2643
2649
  `Service identity '${siName}' must have a non-empty 'namespaces' array.`,
@@ -2743,7 +2749,8 @@ var ManifestParser = class {
2743
2749
  name: siName,
2744
2750
  description: siObj.description ?? "",
2745
2751
  namespaces: siObj.namespaces,
2746
- environments: parsedEnvs
2752
+ environments: parsedEnvs,
2753
+ ...siObj.pack_only === true ? { pack_only: true } : {}
2747
2754
  };
2748
2755
  });
2749
2756
  const siNames = /* @__PURE__ */ new Set();
@@ -5089,6 +5096,18 @@ var LintRunner = class {
5089
5096
  });
5090
5097
  }
5091
5098
  }
5099
+ if (si.pack_only) {
5100
+ const ageRecipients = Object.values(si.environments).filter((cfg) => !isKmsEnvelope(cfg) && cfg.recipient).map((cfg) => cfg.recipient);
5101
+ if (ageRecipients.length >= 2 && new Set(ageRecipients).size === 1) {
5102
+ issues.push({
5103
+ severity: "warning",
5104
+ category: "service-identity",
5105
+ file: "clef.yaml",
5106
+ message: `Runtime identity '${si.name}' uses a shared recipient across all environments. A compromised key in any environment decrypts artifacts for all environments. Consider per-environment keys for runtime workloads.`
5107
+ });
5108
+ }
5109
+ continue;
5110
+ }
5092
5111
  for (const cell of existingCells) {
5093
5112
  const envConfig = si.environments[cell.environment];
5094
5113
  if (!envConfig) continue;
@@ -6448,12 +6467,10 @@ var ServiceIdentityManager = class {
6448
6467
  * Create a new service identity with per-environment age key pairs or KMS envelope config.
6449
6468
  * For age-only: generates keys, updates the manifest, and registers public keys as SOPS recipients.
6450
6469
  * For KMS: stores KMS config in manifest, no age keys generated.
6451
- *
6452
- * @param kmsEnvConfigs - Optional per-environment KMS config. When provided, those envs use
6453
- * KMS envelope encryption instead of generating age keys.
6454
- * @returns The created identity definition and the per-environment private keys (empty for KMS envs).
6470
+ * For pack-only (runtime) identities: keys are generated but NOT registered on SOPS files.
6455
6471
  */
6456
- async create(name, namespaces, description, manifest, repoRoot, kmsEnvConfigs) {
6472
+ async create(name, namespaces, description, manifest, repoRoot, options) {
6473
+ const { kmsEnvConfigs, sharedRecipient, packOnly } = options ?? {};
6457
6474
  if (manifest.service_identities?.some((si) => si.name === name)) {
6458
6475
  throw new Error(`Service identity '${name}' already exists.`);
6459
6476
  }
@@ -6465,23 +6482,25 @@ var ServiceIdentityManager = class {
6465
6482
  }
6466
6483
  const environments = {};
6467
6484
  const privateKeys = {};
6485
+ const sharedKey = sharedRecipient ? await generateAgeIdentity() : void 0;
6468
6486
  for (const env of manifest.environments) {
6469
6487
  const kmsConfig = kmsEnvConfigs?.[env.name];
6470
6488
  if (kmsConfig) {
6471
6489
  environments[env.name] = { kms: kmsConfig };
6472
6490
  } else {
6473
- const identity = await generateAgeIdentity();
6474
- environments[env.name] = { recipient: identity.publicKey };
6475
- privateKeys[env.name] = identity.privateKey;
6491
+ const ageIdentity = sharedKey ?? await generateAgeIdentity();
6492
+ environments[env.name] = { recipient: ageIdentity.publicKey };
6493
+ privateKeys[env.name] = ageIdentity.privateKey;
6476
6494
  }
6477
6495
  }
6478
6496
  const definition = {
6479
6497
  name,
6480
6498
  description,
6481
6499
  namespaces,
6482
- environments
6500
+ environments,
6501
+ ...packOnly ? { pack_only: true } : {}
6483
6502
  };
6484
- const cells = this.matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists && namespaces.includes(c.namespace));
6503
+ const cells = packOnly ? [] : this.matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists && namespaces.includes(c.namespace));
6485
6504
  await this.tx.run(repoRoot, {
6486
6505
  description: `clef service create ${name}`,
6487
6506
  paths: this.txPaths(repoRoot, cells),
@@ -6495,12 +6514,13 @@ var ServiceIdentityManager = class {
6495
6514
  name,
6496
6515
  description,
6497
6516
  namespaces,
6498
- environments
6517
+ environments,
6518
+ ...packOnly ? { pack_only: true } : {}
6499
6519
  });
6500
6520
  writeManifestYaml(repoRoot, doc);
6501
6521
  }
6502
6522
  });
6503
- return { identity: definition, privateKeys };
6523
+ return { identity: definition, privateKeys, sharedRecipient: sharedKey !== void 0 };
6504
6524
  }
6505
6525
  /**
6506
6526
  * List all service identities from the manifest.
@@ -6523,7 +6543,7 @@ var ServiceIdentityManager = class {
6523
6543
  if (!identity) {
6524
6544
  throw new Error(`Service identity '${name}' not found.`);
6525
6545
  }
6526
- const scopedCells = this.matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists && identity.namespaces.includes(c.namespace));
6546
+ const scopedCells = identity.pack_only ? [] : this.matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists && identity.namespaces.includes(c.namespace));
6527
6547
  await this.tx.run(repoRoot, {
6528
6548
  description: `clef service delete ${name}`,
6529
6549
  paths: this.txPaths(repoRoot, scopedCells),
@@ -6579,7 +6599,7 @@ var ServiceIdentityManager = class {
6579
6599
  const envs = siDoc.environments;
6580
6600
  for (const [envName, kmsConfig] of Object.entries(kmsEnvConfigs)) {
6581
6601
  const oldConfig = identity.environments[envName];
6582
- if (oldConfig?.recipient && !isKmsEnvelope(oldConfig)) {
6602
+ if (!identity.pack_only && oldConfig?.recipient && !isKmsEnvelope(oldConfig)) {
6583
6603
  const scopedCells = cells.filter((c) => c.environment === envName);
6584
6604
  for (const cell of scopedCells) {
6585
6605
  try {
@@ -6598,8 +6618,10 @@ var ServiceIdentityManager = class {
6598
6618
  }
6599
6619
  /**
6600
6620
  * Register a service identity's public keys as SOPS recipients on scoped matrix files.
6621
+ * Pack-only (runtime) identities skip registration entirely.
6601
6622
  */
6602
6623
  async registerRecipients(identity, manifest, repoRoot) {
6624
+ if (identity.pack_only) return;
6603
6625
  const cells = this.matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.exists);
6604
6626
  for (const cell of cells) {
6605
6627
  if (!identity.namespaces.includes(cell.namespace)) continue;
@@ -6649,18 +6671,20 @@ var ServiceIdentityManager = class {
6649
6671
  description: `clef service update ${name}: add namespaces ${toAdd.join(",")}`,
6650
6672
  paths: this.txPaths(repoRoot, cells),
6651
6673
  mutate: async () => {
6652
- for (const cell of cells) {
6653
- const envConfig = identity.environments[cell.environment];
6654
- if (!envConfig) continue;
6655
- if (isKmsEnvelope(envConfig)) continue;
6656
- if (!envConfig.recipient) continue;
6657
- try {
6658
- await this.encryption.addRecipient(cell.filePath, envConfig.recipient);
6659
- affectedFiles.push(cell.filePath);
6660
- } catch (err) {
6661
- const message = err instanceof Error ? err.message : String(err);
6662
- if (!message.includes("already")) {
6663
- throw err;
6674
+ if (!identity.pack_only) {
6675
+ for (const cell of cells) {
6676
+ const envConfig = identity.environments[cell.environment];
6677
+ if (!envConfig) continue;
6678
+ if (isKmsEnvelope(envConfig)) continue;
6679
+ if (!envConfig.recipient) continue;
6680
+ try {
6681
+ await this.encryption.addRecipient(cell.filePath, envConfig.recipient);
6682
+ affectedFiles.push(cell.filePath);
6683
+ } catch (err) {
6684
+ const message = err instanceof Error ? err.message : String(err);
6685
+ if (!message.includes("already")) {
6686
+ throw err;
6687
+ }
6664
6688
  }
6665
6689
  }
6666
6690
  }
@@ -6708,15 +6732,17 @@ var ServiceIdentityManager = class {
6708
6732
  description: `clef service update ${name}: remove namespaces ${namespacesToRemove.join(",")}`,
6709
6733
  paths: this.txPaths(repoRoot, cells),
6710
6734
  mutate: async () => {
6711
- for (const cell of cells) {
6712
- const envConfig = identity.environments[cell.environment];
6713
- if (!envConfig) continue;
6714
- if (isKmsEnvelope(envConfig)) continue;
6715
- if (!envConfig.recipient) continue;
6716
- try {
6717
- await this.encryption.removeRecipient(cell.filePath, envConfig.recipient);
6718
- affectedFiles.push(cell.filePath);
6719
- } catch {
6735
+ if (!identity.pack_only) {
6736
+ for (const cell of cells) {
6737
+ const envConfig = identity.environments[cell.environment];
6738
+ if (!envConfig) continue;
6739
+ if (isKmsEnvelope(envConfig)) continue;
6740
+ if (!envConfig.recipient) continue;
6741
+ try {
6742
+ await this.encryption.removeRecipient(cell.filePath, envConfig.recipient);
6743
+ affectedFiles.push(cell.filePath);
6744
+ } catch {
6745
+ }
6720
6746
  }
6721
6747
  }
6722
6748
  const doc = readManifestYaml(repoRoot);
@@ -6779,7 +6805,7 @@ var ServiceIdentityManager = class {
6779
6805
  description: `clef service add-env ${name} ${envName}`,
6780
6806
  paths: this.txPaths(repoRoot, cells),
6781
6807
  mutate: async () => {
6782
- if (!isKmsEnvelope(envConfig) && envConfig.recipient) {
6808
+ if (!identity.pack_only && !isKmsEnvelope(envConfig) && envConfig.recipient) {
6783
6809
  for (const cell of cells) {
6784
6810
  try {
6785
6811
  await this.encryption.addRecipient(cell.filePath, envConfig.recipient);
@@ -6838,7 +6864,7 @@ var ServiceIdentityManager = class {
6838
6864
  if (targetEnvNames.size === 0) {
6839
6865
  return newPrivateKeys;
6840
6866
  }
6841
- const cells = this.matrixManager.resolveMatrix(manifest, repoRoot).filter(
6867
+ const cells = identity.pack_only ? [] : this.matrixManager.resolveMatrix(manifest, repoRoot).filter(
6842
6868
  (c) => c.exists && identity.namespaces.includes(c.namespace) && targetEnvNames.has(c.environment)
6843
6869
  );
6844
6870
  await this.tx.run(repoRoot, {
@@ -6855,13 +6881,15 @@ var ServiceIdentityManager = class {
6855
6881
  const oldRecipient = identity.environments[envName].recipient;
6856
6882
  const newPublicKey = newPublicKeys[envName];
6857
6883
  envs[envName] = { recipient: newPublicKey };
6858
- const scopedCells = cells.filter((c) => c.environment === envName);
6859
- for (const cell of scopedCells) {
6860
- try {
6861
- await this.encryption.removeRecipient(cell.filePath, oldRecipient);
6862
- } catch {
6884
+ if (!identity.pack_only) {
6885
+ const scopedCells = cells.filter((c) => c.environment === envName);
6886
+ for (const cell of scopedCells) {
6887
+ try {
6888
+ await this.encryption.removeRecipient(cell.filePath, oldRecipient);
6889
+ } catch {
6890
+ }
6891
+ await this.encryption.addRecipient(cell.filePath, newPublicKey);
6863
6892
  }
6864
- await this.encryption.addRecipient(cell.filePath, newPublicKey);
6865
6893
  }
6866
6894
  }
6867
6895
  writeManifestYaml(repoRoot, doc);
@@ -6901,6 +6929,17 @@ var ServiceIdentityManager = class {
6901
6929
  });
6902
6930
  }
6903
6931
  }
6932
+ if (si.pack_only) {
6933
+ const ageRecipients = Object.values(si.environments).filter((cfg) => !isKmsEnvelope(cfg) && cfg.recipient).map((cfg) => cfg.recipient);
6934
+ if (ageRecipients.length >= 2 && new Set(ageRecipients).size === 1) {
6935
+ issues.push({
6936
+ identity: si.name,
6937
+ type: "runtime_shared_recipient",
6938
+ message: `Runtime identity '${si.name}' uses a shared recipient across all environments. A compromised key in any environment decrypts artifacts for all environments. Consider per-environment keys for runtime workloads.`
6939
+ });
6940
+ }
6941
+ continue;
6942
+ }
6904
6943
  for (const cell of cells) {
6905
6944
  const envConfig = si.environments[cell.environment];
6906
6945
  if (!envConfig) continue;