@clef-sh/core 0.1.27 → 0.1.28

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
@@ -2269,11 +2269,98 @@ function keyPreview(key) {
2269
2269
  return `age1\u2026${last8}`;
2270
2270
  }
2271
2271
 
2272
+ // src/kms/aws-arn.ts
2273
+ var PARTITION_PATTERN = /^aws(?:-[a-z]+)*$/;
2274
+ var REGION_PATTERN = /^[a-z]{2,}(?:-[a-z]+)+-\d+$/;
2275
+ var ACCOUNT_PATTERN = /^\d{12}$/;
2276
+ function validateAwsKmsArn(input) {
2277
+ if (typeof input !== "string") {
2278
+ return { ok: false, reason: "value must be a string" };
2279
+ }
2280
+ if (input.length === 0) {
2281
+ return { ok: false, reason: "value is empty" };
2282
+ }
2283
+ if (!input.startsWith("arn:")) {
2284
+ return {
2285
+ ok: false,
2286
+ reason: "expected an ARN starting with 'arn:' (got a bare key id, alias name, or other format). Use a full ARN like 'arn:aws:kms:us-east-1:123456789012:alias/<name>'."
2287
+ };
2288
+ }
2289
+ const segments = input.split(":");
2290
+ if (segments.length < 6) {
2291
+ return {
2292
+ ok: false,
2293
+ reason: `expected 6 colon-delimited segments (arn:aws:kms:<region>:<account>:<resource>), got ${segments.length}. Check that the region and account aren't missing.`
2294
+ };
2295
+ }
2296
+ if (segments.length > 6) {
2297
+ return {
2298
+ ok: false,
2299
+ reason: `expected exactly 6 colon-delimited segments, got ${segments.length}. Check for stray ':' characters.`
2300
+ };
2301
+ }
2302
+ const [, partition, service, region, account, resource] = segments;
2303
+ if (!PARTITION_PATTERN.test(partition)) {
2304
+ return {
2305
+ ok: false,
2306
+ reason: `partition segment '${partition}' is not recognized. Expected 'aws', 'aws-us-gov', 'aws-cn', etc.`
2307
+ };
2308
+ }
2309
+ if (service !== "kms") {
2310
+ return {
2311
+ ok: false,
2312
+ reason: `service segment must be 'kms', got '${service}'.`
2313
+ };
2314
+ }
2315
+ if (region.length === 0) {
2316
+ return {
2317
+ ok: false,
2318
+ reason: "region segment is empty (look for '::' between 'kms' and the account id). Set a region like 'us-east-1' before reconstructing the ARN \u2014 common cause: a $REGION shell variable was unset when the ARN was built."
2319
+ };
2320
+ }
2321
+ if (!REGION_PATTERN.test(region)) {
2322
+ return {
2323
+ ok: false,
2324
+ reason: `region segment '${region}' doesn't look like an AWS region (expected e.g. 'us-east-1', 'eu-west-2').`
2325
+ };
2326
+ }
2327
+ if (account.length === 0) {
2328
+ return {
2329
+ ok: false,
2330
+ reason: "account segment is empty. Provide the 12-digit AWS account id."
2331
+ };
2332
+ }
2333
+ if (!ACCOUNT_PATTERN.test(account)) {
2334
+ return {
2335
+ ok: false,
2336
+ reason: `account segment '${account}' must be exactly 12 digits.`
2337
+ };
2338
+ }
2339
+ if (!resource || resource.length === 0) {
2340
+ return {
2341
+ ok: false,
2342
+ reason: "resource segment is empty. Expected 'key/<id>' or 'alias/<name>' after the account."
2343
+ };
2344
+ }
2345
+ if (!resource.startsWith("key/") && !resource.startsWith("alias/")) {
2346
+ return {
2347
+ ok: false,
2348
+ reason: `resource '${resource}' must start with 'key/' or 'alias/'.`
2349
+ };
2350
+ }
2351
+ if (resource === "key/" || resource === "alias/") {
2352
+ return {
2353
+ ok: false,
2354
+ reason: "resource id is empty after 'key/' or 'alias/'."
2355
+ };
2356
+ }
2357
+ return { ok: true };
2358
+ }
2359
+
2272
2360
  // src/manifest/parser.ts
2273
2361
  var CLEF_MANIFEST_FILENAME = "clef.yaml";
2274
2362
  var VALID_BACKENDS = ["age", "awskms", "gcpkms", "azurekv", "pgp", "hsm"];
2275
2363
  var PKCS11_URI_PATTERN = /^pkcs11:[a-zA-Z][a-zA-Z0-9_-]*=[^;]+/;
2276
- var AWS_KMS_ARN_PATTERN = /^arn:aws(?:-[a-z]+)*:kms:[a-z0-9-]+:\d+:(key|alias)\/.+$/;
2277
2364
  var VALID_TOP_LEVEL_KEYS = [
2278
2365
  "version",
2279
2366
  "environments",
@@ -2796,11 +2883,14 @@ var ManifestParser = class {
2796
2883
  "service_identities"
2797
2884
  );
2798
2885
  }
2799
- if (kmsObj.provider === "aws" && !AWS_KMS_ARN_PATTERN.test(kmsObj.keyId)) {
2800
- throw new ManifestValidationError(
2801
- `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}'.`,
2802
- "service_identities"
2803
- );
2886
+ if (kmsObj.provider === "aws") {
2887
+ const arnValidation = validateAwsKmsArn(kmsObj.keyId);
2888
+ if (!arnValidation.ok) {
2889
+ throw new ManifestValidationError(
2890
+ `Service identity '${siName}' environment '${envName}': kms.keyId is not a valid AWS KMS ARN \u2014 ${arnValidation.reason} (got '${kmsObj.keyId}'). Expected shape: arn:aws:kms:<region>:<account>:key/<id> or arn:aws:kms:<region>:<account>:alias/<name>.`,
2891
+ "service_identities"
2892
+ );
2893
+ }
2804
2894
  }
2805
2895
  if (Object.prototype.hasOwnProperty.call(kmsObj, "region")) {
2806
2896
  throw new ManifestValidationError(
@@ -2875,6 +2965,18 @@ function readManifestYaml(repoRoot) {
2875
2965
  return YAML2.parse(raw);
2876
2966
  }
2877
2967
  function writeManifestYaml(repoRoot, doc) {
2968
+ const parser = new ManifestParser();
2969
+ try {
2970
+ parser.validate(doc);
2971
+ } catch (err) {
2972
+ if (err instanceof ManifestValidationError) {
2973
+ throw new ManifestValidationError(
2974
+ `Refusing to write invalid manifest: ${err.message}`,
2975
+ err.field
2976
+ );
2977
+ }
2978
+ throw err;
2979
+ }
2878
2980
  const manifestPath = path.join(repoRoot, CLEF_MANIFEST_FILENAME);
2879
2981
  import_write_file_atomic.default.sync(manifestPath, YAML2.stringify(doc));
2880
2982
  }
@@ -8933,6 +9035,8 @@ var BackendMigrator = class {
8933
9035
  warnings: ["All files already use the target backend and key. Nothing to migrate."]
8934
9036
  };
8935
9037
  }
9038
+ const preMigrationWarnings = [];
9039
+ this.checkAgeRecipientsWarning(manifest, target, environment, preMigrationWarnings);
8936
9040
  if (dryRun) {
8937
9041
  const warnings2 = [];
8938
9042
  for (const cell of toMigrate) {
@@ -8949,7 +9053,7 @@ var BackendMigrator = class {
8949
9053
  } else {
8950
9054
  warnings2.push(`Would update global default_backend \u2192 ${target.backend}`);
8951
9055
  }
8952
- this.checkAgeRecipientsWarning(manifest, target, environment, warnings2);
9056
+ warnings2.push(...preMigrationWarnings);
8953
9057
  return {
8954
9058
  migratedFiles: [],
8955
9059
  skippedFiles,
@@ -9005,7 +9109,12 @@ var BackendMigrator = class {
9005
9109
  rolledBack: true,
9006
9110
  error: migrationError.message,
9007
9111
  verifiedFiles: [],
9008
- warnings: ["All changes have been rolled back."]
9112
+ // Surface pre-migration warnings even on rollback. The new manifest
9113
+ // validator can reject the write (e.g. per-env recipients vs.
9114
+ // non-age backend), and without these warnings the user only sees
9115
+ // an opaque "rolled back" message — not the actionable hint about
9116
+ // what to clean up first.
9117
+ warnings: ["All changes have been rolled back.", ...preMigrationWarnings]
9009
9118
  };
9010
9119
  }
9011
9120
  const verifiedFiles = [];
@@ -9028,7 +9137,7 @@ var BackendMigrator = class {
9028
9137
  }
9029
9138
  }
9030
9139
  }
9031
- this.checkAgeRecipientsWarning(manifest, target, environment, warnings);
9140
+ warnings.push(...preMigrationWarnings);
9032
9141
  return { migratedFiles, skippedFiles, rolledBack: false, verifiedFiles, warnings };
9033
9142
  }
9034
9143
  // ── Private helpers ──────────────────────────────────────────────────
@@ -9862,6 +9971,7 @@ export {
9862
9971
  tryBundledKeyservice,
9863
9972
  upsertRequest,
9864
9973
  validateAgePublicKey,
9974
+ validateAwsKmsArn,
9865
9975
  validatePackedArtifact,
9866
9976
  validateResetScope,
9867
9977
  verifySignature,