@clef-sh/core 0.1.11-beta.71 → 0.1.11-beta.74

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
@@ -155,7 +155,7 @@ function keyPreview(key) {
155
155
 
156
156
  // src/manifest/parser.ts
157
157
  var CLEF_MANIFEST_FILENAME = "clef.yaml";
158
- var VALID_BACKENDS = ["age", "awskms", "gcpkms", "azurekv", "pgp"];
158
+ var VALID_BACKENDS = ["age", "awskms", "gcpkms", "azurekv", "pgp", "cloud"];
159
159
  var VALID_TOP_LEVEL_KEYS = [
160
160
  "version",
161
161
  "environments",
@@ -680,7 +680,26 @@ var ManifestParser = class {
680
680
  "cloud"
681
681
  );
682
682
  }
683
- cloud = { integrationId: cloudObj.integrationId };
683
+ if (typeof cloudObj.keyId !== "string" || cloudObj.keyId.length === 0) {
684
+ throw new ManifestValidationError(
685
+ "Field 'cloud.keyId' is required and must be a non-empty string.",
686
+ "cloud"
687
+ );
688
+ }
689
+ if (!/^clef:[a-z0-9_]+\/[a-z0-9_-]+$/.test(cloudObj.keyId)) {
690
+ throw new ManifestValidationError(
691
+ `Field 'cloud.keyId' has invalid format '${cloudObj.keyId}'. Must match: clef:<integrationId>/<keyAlias>`,
692
+ "cloud"
693
+ );
694
+ }
695
+ cloud = { integrationId: cloudObj.integrationId, keyId: cloudObj.keyId };
696
+ }
697
+ const usesCloudBackend = sopsConfig.default_backend === "cloud" || environments.some((e) => e.sops?.backend === "cloud");
698
+ if (usesCloudBackend && !cloud) {
699
+ throw new ManifestValidationError(
700
+ "One or more environments use the 'cloud' backend but the manifest is missing the top-level 'cloud' block with 'integrationId' and 'keyId'.",
701
+ "cloud"
702
+ );
684
703
  }
685
704
  return {
686
705
  version: 1,
@@ -2088,14 +2107,18 @@ var SopsClient = class {
2088
2107
  * to the subprocess environment.
2089
2108
  * @param sopsPath - Optional explicit path to the sops binary. When omitted,
2090
2109
  * resolved automatically via {@link resolveSopsPath}.
2110
+ * @param keyserviceAddr - Optional keyservice address (e.g. `tcp://127.0.0.1:12345`).
2111
+ * When set, all SOPS invocations include `--enable-local-keyservice=false --keyservice <addr>`.
2091
2112
  */
2092
- constructor(runner, ageKeyFile, ageKey, sopsPath) {
2113
+ constructor(runner, ageKeyFile, ageKey, sopsPath, keyserviceAddr) {
2093
2114
  this.runner = runner;
2094
2115
  this.ageKeyFile = ageKeyFile;
2095
2116
  this.ageKey = ageKey;
2096
2117
  this.sopsCommand = sopsPath ?? resolveSopsPath().path;
2118
+ this.keyserviceArgs = keyserviceAddr ? ["--enable-local-keyservice=false", "--keyservice", keyserviceAddr] : [];
2097
2119
  }
2098
2120
  sopsCommand;
2121
+ keyserviceArgs;
2099
2122
  buildSopsEnv() {
2100
2123
  const env = {};
2101
2124
  if (this.ageKey) {
@@ -2120,7 +2143,7 @@ var SopsClient = class {
2120
2143
  const env = this.buildSopsEnv();
2121
2144
  const result = await this.runner.run(
2122
2145
  this.sopsCommand,
2123
- ["decrypt", "--output-type", fmt, filePath],
2146
+ [...this.keyserviceArgs, "decrypt", "--output-type", fmt, filePath],
2124
2147
  {
2125
2148
  ...env ? { env } : {}
2126
2149
  }
@@ -2186,6 +2209,7 @@ var SopsClient = class {
2186
2209
  [
2187
2210
  "--config",
2188
2211
  configPath,
2212
+ ...this.keyserviceArgs,
2189
2213
  "encrypt",
2190
2214
  ...args,
2191
2215
  "--input-type",
@@ -2240,7 +2264,7 @@ var SopsClient = class {
2240
2264
  const env = this.buildSopsEnv();
2241
2265
  const result = await this.runner.run(
2242
2266
  this.sopsCommand,
2243
- ["rotate", "-i", "--add-age", key, filePath],
2267
+ [...this.keyserviceArgs, "rotate", "-i", "--add-age", key, filePath],
2244
2268
  {
2245
2269
  ...env ? { env } : {}
2246
2270
  }
@@ -2264,7 +2288,7 @@ var SopsClient = class {
2264
2288
  const env = this.buildSopsEnv();
2265
2289
  const result = await this.runner.run(
2266
2290
  this.sopsCommand,
2267
- ["rotate", "-i", "--rm-age", key, filePath],
2291
+ [...this.keyserviceArgs, "rotate", "-i", "--rm-age", key, filePath],
2268
2292
  {
2269
2293
  ...env ? { env } : {}
2270
2294
  }
@@ -2376,7 +2400,13 @@ var SopsClient = class {
2376
2400
  }
2377
2401
  detectBackend(sops) {
2378
2402
  if (sops.age && Array.isArray(sops.age) && sops.age.length > 0) return "age";
2379
- if (sops.kms && Array.isArray(sops.kms) && sops.kms.length > 0) return "awskms";
2403
+ if (sops.kms && Array.isArray(sops.kms) && sops.kms.length > 0) {
2404
+ const firstArn = sops.kms[0]?.arn;
2405
+ if (typeof firstArn === "string" && firstArn.startsWith("clef:")) {
2406
+ return "cloud";
2407
+ }
2408
+ return "awskms";
2409
+ }
2380
2410
  if (sops.gcp_kms && Array.isArray(sops.gcp_kms) && sops.gcp_kms.length > 0)
2381
2411
  return "gcpkms";
2382
2412
  if (sops.azure_kv && Array.isArray(sops.azure_kv) && sops.azure_kv.length > 0)
@@ -2390,6 +2420,7 @@ var SopsClient = class {
2390
2420
  const entries = sops.age;
2391
2421
  return entries?.map((e) => String(e.recipient ?? "")) ?? [];
2392
2422
  }
2423
+ case "cloud":
2393
2424
  case "awskms": {
2394
2425
  const entries = sops.kms;
2395
2426
  return entries?.map((e) => String(e.arn ?? "")) ?? [];
@@ -2451,6 +2482,13 @@ var SopsClient = class {
2451
2482
  args.push("--pgp", config.pgp_fingerprint);
2452
2483
  }
2453
2484
  break;
2485
+ case "cloud": {
2486
+ const cloudKeyId = manifest.cloud?.keyId;
2487
+ if (cloudKeyId) {
2488
+ args.push("--kms", cloudKeyId);
2489
+ }
2490
+ break;
2491
+ }
2454
2492
  }
2455
2493
  return args;
2456
2494
  }
@@ -4675,7 +4713,8 @@ var BACKEND_KEY_FIELDS = {
4675
4713
  awskms: "aws_kms_arn",
4676
4714
  gcpkms: "gcp_kms_resource_id",
4677
4715
  azurekv: "azure_kv_url",
4678
- pgp: "pgp_fingerprint"
4716
+ pgp: "pgp_fingerprint",
4717
+ cloud: void 0
4679
4718
  };
4680
4719
  var ALL_KEY_FIELDS = Object.values(BACKEND_KEY_FIELDS).filter(
4681
4720
  (v) => v !== void 0
@@ -4863,6 +4902,276 @@ var BackendMigrator = class {
4863
4902
  }
4864
4903
  }
4865
4904
  };
4905
+
4906
+ // src/cloud/keyservice.ts
4907
+ import { spawn } from "child_process";
4908
+ import * as readline from "readline";
4909
+ var PORT_REGEX = /^PORT=(\d+)$/;
4910
+ var STARTUP_TIMEOUT_MS = 5e3;
4911
+ var SHUTDOWN_TIMEOUT_MS = 3e3;
4912
+ async function spawnKeyservice(options) {
4913
+ const args = ["--addr", "127.0.0.1:0"];
4914
+ if (options.endpoint) {
4915
+ args.push("--endpoint", options.endpoint);
4916
+ }
4917
+ const child = spawn(options.binaryPath, args, {
4918
+ stdio: ["ignore", "pipe", "pipe"],
4919
+ env: { ...process.env, CLEF_CLOUD_TOKEN: options.token }
4920
+ });
4921
+ const port = await readPort(child);
4922
+ const addr = `tcp://127.0.0.1:${port}`;
4923
+ return {
4924
+ addr,
4925
+ kill: () => killGracefully(child)
4926
+ };
4927
+ }
4928
+ function readPort(child) {
4929
+ return new Promise((resolve, reject) => {
4930
+ let settled = false;
4931
+ const rl = readline.createInterface({ input: child.stdout });
4932
+ function settle() {
4933
+ clearTimeout(timer);
4934
+ rl.close();
4935
+ }
4936
+ const timer = setTimeout(() => {
4937
+ if (!settled) {
4938
+ settled = true;
4939
+ settle();
4940
+ child.kill("SIGKILL");
4941
+ reject(new Error("Keyservice did not start within 5 seconds."));
4942
+ }
4943
+ }, STARTUP_TIMEOUT_MS);
4944
+ rl.on("line", (line) => {
4945
+ const match = PORT_REGEX.exec(line);
4946
+ if (match && !settled) {
4947
+ settled = true;
4948
+ settle();
4949
+ resolve(parseInt(match[1], 10));
4950
+ }
4951
+ });
4952
+ child.on("error", (err) => {
4953
+ if (!settled) {
4954
+ settled = true;
4955
+ settle();
4956
+ reject(new Error(`Failed to start keyservice: ${err.message}`));
4957
+ }
4958
+ });
4959
+ child.on("exit", (code) => {
4960
+ if (!settled) {
4961
+ settled = true;
4962
+ settle();
4963
+ reject(new Error(`Keyservice exited unexpectedly with code ${code}.`));
4964
+ }
4965
+ });
4966
+ });
4967
+ }
4968
+ function killGracefully(child) {
4969
+ return new Promise((resolve) => {
4970
+ if (child.exitCode !== null) {
4971
+ resolve();
4972
+ return;
4973
+ }
4974
+ const timer = setTimeout(() => {
4975
+ child.kill("SIGKILL");
4976
+ }, SHUTDOWN_TIMEOUT_MS);
4977
+ child.on("exit", () => {
4978
+ clearTimeout(timer);
4979
+ resolve();
4980
+ });
4981
+ child.kill("SIGTERM");
4982
+ });
4983
+ }
4984
+
4985
+ // src/cloud/resolver.ts
4986
+ import * as fs19 from "fs";
4987
+ import * as path22 from "path";
4988
+
4989
+ // src/cloud/bundled.ts
4990
+ import * as fs18 from "fs";
4991
+ import * as path21 from "path";
4992
+ function tryBundledKeyservice() {
4993
+ const platform = process.platform;
4994
+ const arch = process.arch;
4995
+ const archName = arch === "x64" ? "x64" : arch === "arm64" ? "arm64" : null;
4996
+ if (!archName) return null;
4997
+ const platformName = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : platform === "win32" ? "win32" : null;
4998
+ if (!platformName) return null;
4999
+ const packageName = `@clef-sh/keyservice-${platformName}-${archName}`;
5000
+ const binName = platform === "win32" ? "clef-keyservice.exe" : "clef-keyservice";
5001
+ try {
5002
+ const packageMain = __require.resolve(`${packageName}/package.json`);
5003
+ const packageDir = path21.dirname(packageMain);
5004
+ const binPath = path21.join(packageDir, "bin", binName);
5005
+ return fs18.existsSync(binPath) ? binPath : null;
5006
+ } catch {
5007
+ return null;
5008
+ }
5009
+ }
5010
+
5011
+ // src/cloud/resolver.ts
5012
+ function validateKeyservicePath(candidate) {
5013
+ if (!path22.isAbsolute(candidate)) {
5014
+ throw new Error(`CLEF_KEYSERVICE_PATH must be an absolute path, got '${candidate}'.`);
5015
+ }
5016
+ const segments = candidate.split(/[/\\]/);
5017
+ if (segments.includes("..")) {
5018
+ throw new Error(
5019
+ `CLEF_KEYSERVICE_PATH contains '..' path segments ('${candidate}'). Use an absolute path without directory traversal.`
5020
+ );
5021
+ }
5022
+ }
5023
+ var cached2;
5024
+ function resolveKeyservicePath() {
5025
+ if (cached2) return cached2;
5026
+ const envPath = process.env.CLEF_KEYSERVICE_PATH?.trim();
5027
+ if (envPath) {
5028
+ validateKeyservicePath(envPath);
5029
+ if (!fs19.existsSync(envPath)) {
5030
+ throw new Error(`CLEF_KEYSERVICE_PATH points to '${envPath}' but the file does not exist.`);
5031
+ }
5032
+ cached2 = { path: envPath, source: "env" };
5033
+ return cached2;
5034
+ }
5035
+ const bundledPath = tryBundledKeyservice();
5036
+ if (bundledPath) {
5037
+ cached2 = { path: bundledPath, source: "bundled" };
5038
+ return cached2;
5039
+ }
5040
+ cached2 = { path: "clef-keyservice", source: "system" };
5041
+ return cached2;
5042
+ }
5043
+ function resetKeyserviceResolution() {
5044
+ cached2 = void 0;
5045
+ }
5046
+
5047
+ // src/cloud/credentials.ts
5048
+ import * as fs20 from "fs";
5049
+ import * as path23 from "path";
5050
+ import * as os2 from "os";
5051
+ import * as YAML12 from "yaml";
5052
+
5053
+ // src/cloud/constants.ts
5054
+ var CLOUD_DEFAULT_ENDPOINT = "https://api.clef.sh";
5055
+
5056
+ // src/cloud/credentials.ts
5057
+ var CREDENTIALS_FILENAME = "credentials.yaml";
5058
+ function readCloudCredentials() {
5059
+ const credPath = path23.join(os2.homedir(), ".clef", CREDENTIALS_FILENAME);
5060
+ let raw;
5061
+ try {
5062
+ raw = YAML12.parse(fs20.readFileSync(credPath, "utf-8"));
5063
+ } catch {
5064
+ return null;
5065
+ }
5066
+ if (!raw || typeof raw !== "object") return null;
5067
+ const obj = raw;
5068
+ if (typeof obj.token !== "string" || obj.token.length === 0) return null;
5069
+ return {
5070
+ token: obj.token,
5071
+ endpoint: typeof obj.endpoint === "string" ? obj.endpoint : CLOUD_DEFAULT_ENDPOINT
5072
+ };
5073
+ }
5074
+ function writeCloudCredentials(credentials) {
5075
+ const clefDir = path23.join(os2.homedir(), ".clef");
5076
+ fs20.mkdirSync(clefDir, { recursive: true, mode: 448 });
5077
+ const credPath = path23.join(clefDir, CREDENTIALS_FILENAME);
5078
+ const content = { token: credentials.token };
5079
+ if (credentials.endpoint) {
5080
+ content.endpoint = credentials.endpoint;
5081
+ }
5082
+ fs20.writeFileSync(credPath, YAML12.stringify(content), { mode: 384 });
5083
+ }
5084
+
5085
+ // src/cloud/device-flow.ts
5086
+ async function initiateDeviceFlow(endpoint, options) {
5087
+ const base = endpoint ?? CLOUD_DEFAULT_ENDPOINT;
5088
+ const res = await fetch(`${base}/api/v1/device/init`, {
5089
+ method: "POST",
5090
+ headers: { "Content-Type": "application/json" },
5091
+ body: JSON.stringify({
5092
+ clientType: "cli",
5093
+ clientVersion: options.clientVersion,
5094
+ repoName: options.repoName,
5095
+ environment: options.environment
5096
+ })
5097
+ });
5098
+ if (!res.ok) {
5099
+ const body = await res.text().catch(() => "");
5100
+ throw new Error(`Device flow init failed (${res.status}): ${body}`);
5101
+ }
5102
+ return await res.json();
5103
+ }
5104
+ async function pollDeviceFlow(pollUrl) {
5105
+ const res = await fetch(pollUrl);
5106
+ if (!res.ok) {
5107
+ const body = await res.text().catch(() => "");
5108
+ throw new Error(`Device flow poll failed (${res.status}): ${body}`);
5109
+ }
5110
+ return await res.json();
5111
+ }
5112
+
5113
+ // src/cloud/pack-client.ts
5114
+ import * as fs21 from "fs";
5115
+ import * as path24 from "path";
5116
+ var CloudPackClient = class {
5117
+ endpoint;
5118
+ constructor(endpoint) {
5119
+ this.endpoint = endpoint ?? CLOUD_DEFAULT_ENDPOINT;
5120
+ }
5121
+ async pack(token, config) {
5122
+ const matrixManager = new MatrixManager();
5123
+ const cells = matrixManager.resolveMatrix(config.manifest, config.repoRoot).filter((c) => c.environment === config.environment && c.exists);
5124
+ const formData = new FormData();
5125
+ const configJson = JSON.stringify({
5126
+ identity: config.identity,
5127
+ environment: config.environment,
5128
+ ...config.ttl ? { ttl: config.ttl } : {}
5129
+ });
5130
+ formData.append("config", new Blob([configJson], { type: "application/json" }));
5131
+ const manifestPath = path24.join(config.repoRoot, "clef.yaml");
5132
+ const manifestContent = fs21.readFileSync(manifestPath, "utf-8");
5133
+ formData.append("manifest", new Blob([manifestContent], { type: "text/yaml" }));
5134
+ for (const cell of cells) {
5135
+ const relPath = path24.relative(config.repoRoot, cell.filePath);
5136
+ const content = fs21.readFileSync(cell.filePath, "utf-8");
5137
+ formData.append(`files`, new Blob([content], { type: "text/yaml" }), relPath);
5138
+ }
5139
+ const res = await fetch(`${this.endpoint}/api/v1/cloud/pack`, {
5140
+ method: "POST",
5141
+ headers: { Authorization: `Bearer ${token}` },
5142
+ body: formData
5143
+ });
5144
+ if (!res.ok) {
5145
+ const body = await res.text().catch(() => "");
5146
+ throw new Error(`Cloud pack failed (${res.status}): ${body}`);
5147
+ }
5148
+ return await res.json();
5149
+ }
5150
+ };
5151
+ var CloudArtifactClient = class {
5152
+ endpoint;
5153
+ constructor(endpoint) {
5154
+ this.endpoint = endpoint ?? CLOUD_DEFAULT_ENDPOINT;
5155
+ }
5156
+ async upload(token, config) {
5157
+ const content = fs21.readFileSync(config.artifactPath, "utf-8");
5158
+ const res = await fetch(
5159
+ `${this.endpoint}/api/v1/cloud/artifacts/${config.identity}/${config.environment}`,
5160
+ {
5161
+ method: "PUT",
5162
+ headers: {
5163
+ Authorization: `Bearer ${token}`,
5164
+ "Content-Type": "application/json"
5165
+ },
5166
+ body: content
5167
+ }
5168
+ );
5169
+ if (!res.ok) {
5170
+ const body = await res.text().catch(() => "");
5171
+ throw new Error(`Artifact upload failed (${res.status}): ${body}`);
5172
+ }
5173
+ }
5174
+ };
4866
5175
  export {
4867
5176
  ArtifactPacker,
4868
5177
  BackendMigrator,
@@ -4872,7 +5181,9 @@ export {
4872
5181
  CLEF_SUPPORTED_EXTENSIONS,
4873
5182
  ClefError,
4874
5183
  CloudApiError,
5184
+ CloudArtifactClient,
4875
5185
  CloudClient,
5186
+ CloudPackClient,
4876
5187
  ConsumptionClient,
4877
5188
  DiffEngine,
4878
5189
  DriftDetector,
@@ -4916,6 +5227,7 @@ export {
4916
5227
  generateRandomValue,
4917
5228
  generateSigningKeyPair,
4918
5229
  getPendingKeys,
5230
+ initiateDeviceFlow,
4919
5231
  isHighEntropy,
4920
5232
  isKmsEnvelope,
4921
5233
  isPending,
@@ -4933,13 +5245,17 @@ export {
4933
5245
  parseIgnoreContent,
4934
5246
  parseJson,
4935
5247
  parseYaml,
5248
+ pollDeviceFlow,
5249
+ readCloudCredentials,
4936
5250
  readManifestYaml,
4937
5251
  redactValue,
4938
5252
  removeRequest as removeAccessRequest,
4939
5253
  requestsFilePath,
5254
+ resetKeyserviceResolution,
4940
5255
  resetSopsResolution,
4941
5256
  resolveBackendConfig,
4942
5257
  resolveIdentitySecrets,
5258
+ resolveKeyservicePath,
4943
5259
  resolveRecipientsForEnvironment,
4944
5260
  resolveSopsPath,
4945
5261
  saveMetadata,
@@ -4949,9 +5265,11 @@ export {
4949
5265
  shouldIgnoreMatch,
4950
5266
  signEd25519,
4951
5267
  signKms,
5268
+ spawnKeyservice,
4952
5269
  upsertRequest,
4953
5270
  validateAgePublicKey,
4954
5271
  verifySignature,
5272
+ writeCloudCredentials,
4955
5273
  writeManifestYaml
4956
5274
  };
4957
5275
  //# sourceMappingURL=index.mjs.map