@clef-sh/core 0.1.23 → 0.1.24-beta.169

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
@@ -2268,6 +2268,7 @@ function keyPreview(key) {
2268
2268
  var CLEF_MANIFEST_FILENAME = "clef.yaml";
2269
2269
  var VALID_BACKENDS = ["age", "awskms", "gcpkms", "azurekv", "pgp", "hsm"];
2270
2270
  var PKCS11_URI_PATTERN = /^pkcs11:[a-zA-Z][a-zA-Z0-9_-]*=[^;]+/;
2271
+ var AWS_KMS_ARN_PATTERN = /^arn:aws(?:-[a-z]+)*:kms:[a-z0-9-]+:\d+:(key|alias)\/.+$/;
2271
2272
  var VALID_TOP_LEVEL_KEYS = [
2272
2273
  "version",
2273
2274
  "environments",
@@ -2790,11 +2791,22 @@ var ManifestParser = class {
2790
2791
  "service_identities"
2791
2792
  );
2792
2793
  }
2794
+ if (kmsObj.provider === "aws" && !AWS_KMS_ARN_PATTERN.test(kmsObj.keyId)) {
2795
+ throw new ManifestValidationError(
2796
+ `Service identity '${siName}' environment '${envName}': kms.keyId must be a full AWS KMS ARN (e.g. arn:aws:kms:us-east-1:123456789012:key/abcd-1234), got '${kmsObj.keyId}'.`,
2797
+ "service_identities"
2798
+ );
2799
+ }
2800
+ if (Object.prototype.hasOwnProperty.call(kmsObj, "region")) {
2801
+ throw new ManifestValidationError(
2802
+ `Service identity '${siName}' environment '${envName}': kms.region is no longer accepted; the region is read from the AWS KMS key ARN. Remove this field.`,
2803
+ "service_identities"
2804
+ );
2805
+ }
2793
2806
  parsedEnvs[envName] = {
2794
2807
  kms: {
2795
2808
  provider: kmsObj.provider,
2796
- keyId: kmsObj.keyId,
2797
- region: typeof kmsObj.region === "string" ? kmsObj.region : void 0
2809
+ keyId: kmsObj.keyId
2798
2810
  }
2799
2811
  };
2800
2812
  }
@@ -3439,11 +3451,17 @@ var MatrixManager = class {
3439
3451
  await sopsClient.encrypt(cell.filePath, {}, manifest, cell.environment);
3440
3452
  }
3441
3453
  /**
3442
- * Decrypt each cell and return key counts, pending counts, and cross-environment issues.
3454
+ * Read each cell and return key counts, pending counts, and cross-environment issues.
3455
+ *
3456
+ * The SOPS client parameter is currently unused — keys are read from the
3457
+ * plaintext YAML structure directly, no decryption needed. It is retained
3458
+ * in the signature for back-compat with callers that may need to swap to a
3459
+ * decrypt-based implementation later (e.g. for backends that don't expose
3460
+ * key names without decryption).
3443
3461
  *
3444
3462
  * @param manifest - Parsed manifest.
3445
3463
  * @param repoRoot - Absolute path to the repository root.
3446
- * @param sopsClient - SOPS client used to decrypt each cell.
3464
+ * @param _sopsClient - Reserved for future use; pass any `EncryptionBackend`.
3447
3465
  */
3448
3466
  async getMatrixStatus(manifest, repoRoot, _sopsClient) {
3449
3467
  const cells = this.resolveMatrix(manifest, repoRoot);
@@ -4200,6 +4218,42 @@ var GitIntegration = class {
4200
4218
  }
4201
4219
  return result.stdout;
4202
4220
  }
4221
+ /**
4222
+ * Of the given paths, return those that exist on disk but are untracked by git.
4223
+ *
4224
+ * Used by {@link TransactionManager} to refuse mutations whose declared paths
4225
+ * include untracked-but-existing files. Rollback uses `git reset --hard` to
4226
+ * restore content, which can only restore files that exist in a commit. An
4227
+ * untracked file in the declared paths would be silently destroyed by the
4228
+ * rollback's `git clean` — so we refuse upfront.
4229
+ *
4230
+ * @param repoRoot - Working directory for the git command.
4231
+ * @param paths - Paths to check (relative to repoRoot).
4232
+ * @returns Subset of `paths` that are untracked. Non-existent paths are
4233
+ * excluded; tracked paths are excluded; directories are reported by
4234
+ * their porcelain entry (with trailing slash if untracked-as-dir).
4235
+ * @throws {@link GitOperationError} On failure.
4236
+ */
4237
+ async getUntrackedAmongPaths(repoRoot, paths) {
4238
+ if (paths.length === 0) return [];
4239
+ const result = await this.runner.run("git", ["status", "--porcelain", "--", ...paths], {
4240
+ cwd: repoRoot
4241
+ });
4242
+ if (result.exitCode !== 0) {
4243
+ throw new GitOperationError(
4244
+ `Failed to check tracked status: ${result.stderr.trim()}`,
4245
+ "Inspect with 'git status' and resolve any repository errors."
4246
+ );
4247
+ }
4248
+ const untracked = [];
4249
+ for (const line of result.stdout.split("\n")) {
4250
+ if (line === "") continue;
4251
+ if (line[0] === "?") {
4252
+ untracked.push(line.substring(3));
4253
+ }
4254
+ }
4255
+ return untracked;
4256
+ }
4203
4257
  /**
4204
4258
  * Parse `git status --porcelain` into staged, unstaged, and untracked lists.
4205
4259
  *
@@ -4474,6 +4528,14 @@ var TransactionManager = class {
4474
4528
  "Commit or stash your changes first, or pass --allow-dirty to proceed (rollback will be best-effort)."
4475
4529
  );
4476
4530
  }
4531
+ const untrackedDeclared = await this.git.getUntrackedAmongPaths(repoRoot, opts.paths);
4532
+ if (untrackedDeclared.length > 0) {
4533
+ throw new TransactionPreflightError(
4534
+ "untracked-paths",
4535
+ `Refusing to mutate: the following declared paths are untracked and would not survive rollback: ${untrackedDeclared.join(", ")}`,
4536
+ "Commit these files first (`git add` + `git commit`), then retry. Rollback can only restore content that exists in a commit."
4537
+ );
4538
+ }
4477
4539
  try {
4478
4540
  await opts.mutate();
4479
4541
  } catch (mutateErr) {
@@ -8192,23 +8254,18 @@ async function resolveIdentitySecrets(identityName, environment, manifest, repoR
8192
8254
  const cells = matrixManager.resolveMatrix(manifest, repoRoot).filter(
8193
8255
  (c) => c.exists && identity.namespaces.includes(c.namespace) && c.environment === environment
8194
8256
  );
8195
- const isMultiNamespace = identity.namespaces.length > 1;
8196
- const collisions = [];
8197
8257
  for (const cell of cells) {
8198
8258
  const decrypted = await encryption.decrypt(cell.filePath);
8259
+ const bucket = allValues[cell.namespace] ??= {};
8199
8260
  for (const [key, value] of Object.entries(decrypted.values)) {
8200
- const qualifiedKey = isMultiNamespace ? `${cell.namespace}__${key}` : key;
8201
- if (qualifiedKey in allValues && allValues[qualifiedKey] !== value) {
8202
- collisions.push(qualifiedKey);
8261
+ if (key in bucket && bucket[key] !== value) {
8262
+ throw new Error(
8263
+ `Key collision in namespace '${cell.namespace}': '${key}' set to different values.`
8264
+ );
8203
8265
  }
8204
- allValues[qualifiedKey] = value;
8266
+ bucket[key] = value;
8205
8267
  }
8206
8268
  }
8207
- if (collisions.length > 0) {
8208
- throw new Error(
8209
- `Key collision detected in bundle: ${collisions.join(", ")}. Keys with the same name but different values exist across namespaces.`
8210
- );
8211
- }
8212
8269
  return {
8213
8270
  values: allValues,
8214
8271
  identity,
@@ -8451,7 +8508,12 @@ var ArtifactPacker = class {
8451
8508
  const json = JSON.stringify(artifact, null, 2);
8452
8509
  const output = config.output ?? new FilePackOutput(config.outputPath ?? "artifact.json");
8453
8510
  await output.write(artifact, json);
8454
- const keys = Object.keys(resolved.values);
8511
+ const keys = [];
8512
+ for (const [ns, bucket] of Object.entries(resolved.values)) {
8513
+ for (const k of Object.keys(bucket)) {
8514
+ keys.push(`${ns}__${k}`);
8515
+ }
8516
+ }
8455
8517
  return {
8456
8518
  outputPath: config.outputPath ?? "",
8457
8519
  namespaceCount: resolved.identity.namespaces.length,