@clef-sh/cloud 0.1.18-beta.85
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/bundled.d.ts +6 -0
- package/dist/bundled.d.ts.map +1 -0
- package/dist/cli.d.mts +9 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +604 -0
- package/dist/cli.js.map +7 -0
- package/dist/cli.mjs +579 -0
- package/dist/cli.mjs.map +7 -0
- package/dist/commands/cloud.d.ts +20 -0
- package/dist/commands/cloud.d.ts.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/device-flow.d.ts +46 -0
- package/dist/device-flow.d.ts.map +1 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +522 -0
- package/dist/index.js.map +7 -0
- package/dist/index.mjs +478 -0
- package/dist/index.mjs.map +7 -0
- package/dist/keyservice.d.ts +20 -0
- package/dist/keyservice.d.ts.map +1 -0
- package/dist/pack-client.d.ts +34 -0
- package/dist/pack-client.d.ts.map +1 -0
- package/dist/report-client.d.ts +18 -0
- package/dist/report-client.d.ts.map +1 -0
- package/dist/resolver.d.ts +24 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/sops.d.ts +36 -0
- package/dist/sops.d.ts.map +1 -0
- package/dist/token-refresh.d.ts +27 -0
- package/dist/token-refresh.d.ts.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +95 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/commands/cloud.ts
|
|
9
|
+
import * as path4 from "path";
|
|
10
|
+
import {
|
|
11
|
+
ManifestParser,
|
|
12
|
+
MatrixManager as MatrixManager2,
|
|
13
|
+
readManifestYaml,
|
|
14
|
+
writeManifestYaml
|
|
15
|
+
} from "@clef-sh/core";
|
|
16
|
+
|
|
17
|
+
// src/keyservice.ts
|
|
18
|
+
import { spawn } from "child_process";
|
|
19
|
+
import * as readline from "readline";
|
|
20
|
+
var PORT_REGEX = /^PORT=(\d+)$/;
|
|
21
|
+
var STARTUP_TIMEOUT_MS = 5e3;
|
|
22
|
+
var SHUTDOWN_TIMEOUT_MS = 3e3;
|
|
23
|
+
async function spawnKeyservice(options) {
|
|
24
|
+
const args = ["--addr", "127.0.0.1:0"];
|
|
25
|
+
if (options.endpoint) {
|
|
26
|
+
args.push("--endpoint", options.endpoint);
|
|
27
|
+
}
|
|
28
|
+
const child = spawn(options.binaryPath, args, {
|
|
29
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
30
|
+
env: { ...process.env, CLEF_CLOUD_TOKEN: options.token }
|
|
31
|
+
});
|
|
32
|
+
const port = await readPort(child);
|
|
33
|
+
const addr = `tcp://127.0.0.1:${port}`;
|
|
34
|
+
return {
|
|
35
|
+
addr,
|
|
36
|
+
kill: () => killGracefully(child)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function readPort(child) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let settled = false;
|
|
42
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
43
|
+
function settle() {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
if (!settled) {
|
|
49
|
+
settled = true;
|
|
50
|
+
settle();
|
|
51
|
+
child.kill("SIGKILL");
|
|
52
|
+
reject(new Error("Keyservice did not start within 5 seconds."));
|
|
53
|
+
}
|
|
54
|
+
}, STARTUP_TIMEOUT_MS);
|
|
55
|
+
rl.on("line", (line) => {
|
|
56
|
+
const match = PORT_REGEX.exec(line);
|
|
57
|
+
if (match && !settled) {
|
|
58
|
+
settled = true;
|
|
59
|
+
settle();
|
|
60
|
+
resolve(parseInt(match[1], 10));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
child.on("error", (err) => {
|
|
64
|
+
if (!settled) {
|
|
65
|
+
settled = true;
|
|
66
|
+
settle();
|
|
67
|
+
reject(new Error(`Failed to start keyservice: ${err.message}`));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
child.on("exit", (code) => {
|
|
71
|
+
if (!settled) {
|
|
72
|
+
settled = true;
|
|
73
|
+
settle();
|
|
74
|
+
reject(new Error(`Keyservice exited unexpectedly with code ${code}.`));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function killGracefully(child) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
if (child.exitCode !== null) {
|
|
82
|
+
resolve();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
child.kill("SIGKILL");
|
|
87
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
88
|
+
child.on("exit", () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
child.kill("SIGTERM");
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/resolver.ts
|
|
97
|
+
import * as fs2 from "fs";
|
|
98
|
+
import * as path2 from "path";
|
|
99
|
+
|
|
100
|
+
// src/bundled.ts
|
|
101
|
+
import * as fs from "fs";
|
|
102
|
+
import * as path from "path";
|
|
103
|
+
function tryBundledKeyservice() {
|
|
104
|
+
const platform = process.platform;
|
|
105
|
+
const arch = process.arch;
|
|
106
|
+
const archName = arch === "x64" ? "x64" : arch === "arm64" ? "arm64" : null;
|
|
107
|
+
if (!archName) return null;
|
|
108
|
+
const platformName = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : platform === "win32" ? "win32" : null;
|
|
109
|
+
if (!platformName) return null;
|
|
110
|
+
const packageName = `@clef-sh/keyservice-${platformName}-${archName}`;
|
|
111
|
+
const binName = platform === "win32" ? "clef-keyservice.exe" : "clef-keyservice";
|
|
112
|
+
try {
|
|
113
|
+
const packageMain = __require.resolve(`${packageName}/package.json`);
|
|
114
|
+
const packageDir = path.dirname(packageMain);
|
|
115
|
+
const binPath = path.join(packageDir, "bin", binName);
|
|
116
|
+
return fs.existsSync(binPath) ? binPath : null;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/resolver.ts
|
|
123
|
+
function validateKeyservicePath(candidate) {
|
|
124
|
+
if (!path2.isAbsolute(candidate)) {
|
|
125
|
+
throw new Error(`CLEF_KEYSERVICE_PATH must be an absolute path, got '${candidate}'.`);
|
|
126
|
+
}
|
|
127
|
+
const segments = candidate.split(/[/\\]/);
|
|
128
|
+
if (segments.includes("..")) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`CLEF_KEYSERVICE_PATH contains '..' path segments ('${candidate}'). Use an absolute path without directory traversal.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
var cached;
|
|
135
|
+
function resolveKeyservicePath() {
|
|
136
|
+
if (cached) return cached;
|
|
137
|
+
const envPath = process.env.CLEF_KEYSERVICE_PATH?.trim();
|
|
138
|
+
if (envPath) {
|
|
139
|
+
validateKeyservicePath(envPath);
|
|
140
|
+
if (!fs2.existsSync(envPath)) {
|
|
141
|
+
throw new Error(`CLEF_KEYSERVICE_PATH points to '${envPath}' but the file does not exist.`);
|
|
142
|
+
}
|
|
143
|
+
cached = { path: envPath, source: "env" };
|
|
144
|
+
return cached;
|
|
145
|
+
}
|
|
146
|
+
const bundledPath = tryBundledKeyservice();
|
|
147
|
+
if (bundledPath) {
|
|
148
|
+
cached = { path: bundledPath, source: "bundled" };
|
|
149
|
+
return cached;
|
|
150
|
+
}
|
|
151
|
+
cached = { path: "clef-keyservice", source: "system" };
|
|
152
|
+
return cached;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/credentials.ts
|
|
156
|
+
import * as fs3 from "fs";
|
|
157
|
+
import * as path3 from "path";
|
|
158
|
+
import * as os from "os";
|
|
159
|
+
import * as YAML from "yaml";
|
|
160
|
+
|
|
161
|
+
// src/constants.ts
|
|
162
|
+
var CLOUD_DEFAULT_ENDPOINT = "https://api.clef.sh";
|
|
163
|
+
|
|
164
|
+
// src/credentials.ts
|
|
165
|
+
var CREDENTIALS_FILENAME = "credentials.yaml";
|
|
166
|
+
function readCloudCredentials() {
|
|
167
|
+
const credPath = path3.join(os.homedir(), ".clef", CREDENTIALS_FILENAME);
|
|
168
|
+
let raw;
|
|
169
|
+
try {
|
|
170
|
+
raw = YAML.parse(fs3.readFileSync(credPath, "utf-8"));
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (!raw || typeof raw !== "object") return null;
|
|
175
|
+
const obj = raw;
|
|
176
|
+
const refreshToken = typeof obj.refreshToken === "string" ? obj.refreshToken : "";
|
|
177
|
+
const accessToken = typeof obj.accessToken === "string" ? obj.accessToken : void 0;
|
|
178
|
+
const accessTokenExpiry = typeof obj.accessTokenExpiry === "number" ? obj.accessTokenExpiry : void 0;
|
|
179
|
+
const endpoint = typeof obj.endpoint === "string" ? obj.endpoint : CLOUD_DEFAULT_ENDPOINT;
|
|
180
|
+
const cognitoDomain = typeof obj.cognitoDomain === "string" ? obj.cognitoDomain : void 0;
|
|
181
|
+
const clientId = typeof obj.clientId === "string" ? obj.clientId : void 0;
|
|
182
|
+
if (!refreshToken && endpoint === CLOUD_DEFAULT_ENDPOINT) return null;
|
|
183
|
+
return { refreshToken, accessToken, accessTokenExpiry, endpoint, cognitoDomain, clientId };
|
|
184
|
+
}
|
|
185
|
+
function writeCloudCredentials(credentials) {
|
|
186
|
+
const clefDir = path3.join(os.homedir(), ".clef");
|
|
187
|
+
fs3.mkdirSync(clefDir, { recursive: true, mode: 448 });
|
|
188
|
+
const credPath = path3.join(clefDir, CREDENTIALS_FILENAME);
|
|
189
|
+
const content = Object.fromEntries(
|
|
190
|
+
Object.entries(credentials).filter(([, v]) => v !== void 0)
|
|
191
|
+
);
|
|
192
|
+
fs3.writeFileSync(credPath, YAML.stringify(content), { mode: 384 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/device-flow.ts
|
|
196
|
+
async function initiateDeviceFlow(endpoint, options) {
|
|
197
|
+
const base = endpoint ?? CLOUD_DEFAULT_ENDPOINT;
|
|
198
|
+
const payload = {
|
|
199
|
+
clientType: "cli",
|
|
200
|
+
clientVersion: options.clientVersion,
|
|
201
|
+
repoName: options.repoName,
|
|
202
|
+
flow: options.flow
|
|
203
|
+
};
|
|
204
|
+
if (options.environment) {
|
|
205
|
+
payload.environment = options.environment;
|
|
206
|
+
}
|
|
207
|
+
let res;
|
|
208
|
+
try {
|
|
209
|
+
res = await fetch(`${base}/api/v1/device/init`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: { "Content-Type": "application/json" },
|
|
212
|
+
body: JSON.stringify(payload)
|
|
213
|
+
});
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
216
|
+
const reason = cause instanceof Error ? cause.message : err instanceof Error ? err.message : String(err);
|
|
217
|
+
throw new Error(`Could not reach Clef Cloud at ${base}: ${reason}`);
|
|
218
|
+
}
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
const body = await res.text().catch(() => "");
|
|
221
|
+
throw new Error(`Device flow init failed (${res.status}): ${body}`);
|
|
222
|
+
}
|
|
223
|
+
const json = await res.json();
|
|
224
|
+
const session = json.data ?? json;
|
|
225
|
+
if (session.pollUrl && !session.pollUrl.startsWith("http")) {
|
|
226
|
+
session.pollUrl = `${base}${session.pollUrl}`;
|
|
227
|
+
}
|
|
228
|
+
return session;
|
|
229
|
+
}
|
|
230
|
+
async function pollDeviceFlow(pollUrl) {
|
|
231
|
+
let res;
|
|
232
|
+
try {
|
|
233
|
+
res = await fetch(pollUrl);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
236
|
+
const reason = cause instanceof Error ? cause.message : err instanceof Error ? err.message : String(err);
|
|
237
|
+
throw new Error(`Could not reach Clef Cloud poll endpoint: ${reason}`);
|
|
238
|
+
}
|
|
239
|
+
if (!res.ok) {
|
|
240
|
+
const body = await res.text().catch(() => "");
|
|
241
|
+
throw new Error(`Device flow poll failed (${res.status}): ${body}`);
|
|
242
|
+
}
|
|
243
|
+
const json = await res.json();
|
|
244
|
+
return json.data ?? json;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/pack-client.ts
|
|
248
|
+
import { MatrixManager } from "@clef-sh/core";
|
|
249
|
+
|
|
250
|
+
// src/token-refresh.ts
|
|
251
|
+
async function refreshAccessToken(config) {
|
|
252
|
+
const url = `${config.cognitoDomain}/oauth2/token`;
|
|
253
|
+
const body = new URLSearchParams({
|
|
254
|
+
grant_type: "refresh_token",
|
|
255
|
+
client_id: config.clientId,
|
|
256
|
+
refresh_token: config.refreshToken
|
|
257
|
+
});
|
|
258
|
+
const res = await fetch(url, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
261
|
+
body: body.toString()
|
|
262
|
+
});
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
const text = await res.text().catch(() => "");
|
|
265
|
+
if (res.status === 400 && text.includes("invalid_grant")) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
"Refresh token expired or revoked. Run 'clef cloud login' to re-authenticate."
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`Token refresh failed (${res.status}): ${text}`);
|
|
271
|
+
}
|
|
272
|
+
const data = await res.json();
|
|
273
|
+
return {
|
|
274
|
+
accessToken: data.access_token,
|
|
275
|
+
idToken: data.id_token,
|
|
276
|
+
expiresIn: data.expires_in
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/sops.ts
|
|
281
|
+
async function resolveAccessToken() {
|
|
282
|
+
const clefToken = process.env.CLEF_TOKEN;
|
|
283
|
+
if (clefToken) {
|
|
284
|
+
const creds2 = readCloudCredentials();
|
|
285
|
+
return { accessToken: clefToken, endpoint: creds2?.endpoint };
|
|
286
|
+
}
|
|
287
|
+
const creds = readCloudCredentials();
|
|
288
|
+
const refreshToken = process.env.CLEF_CLOUD_REFRESH_TOKEN ?? creds?.refreshToken;
|
|
289
|
+
if (!refreshToken) {
|
|
290
|
+
throw new Error("Not authenticated. Run 'clef cloud login' to connect to Clef Cloud.");
|
|
291
|
+
}
|
|
292
|
+
if (creds?.accessToken && creds?.accessTokenExpiry && Date.now() < creds.accessTokenExpiry - 6e4) {
|
|
293
|
+
return { accessToken: creds.accessToken, endpoint: creds?.endpoint };
|
|
294
|
+
}
|
|
295
|
+
if (!creds?.cognitoDomain || !creds?.clientId) {
|
|
296
|
+
throw new Error("Missing Cognito configuration. Run 'clef cloud login' to re-authenticate.");
|
|
297
|
+
}
|
|
298
|
+
const result = await refreshAccessToken({
|
|
299
|
+
cognitoDomain: creds.cognitoDomain,
|
|
300
|
+
clientId: creds.clientId,
|
|
301
|
+
refreshToken
|
|
302
|
+
});
|
|
303
|
+
writeCloudCredentials({
|
|
304
|
+
...creds,
|
|
305
|
+
refreshToken,
|
|
306
|
+
accessToken: result.accessToken,
|
|
307
|
+
accessTokenExpiry: Date.now() + result.expiresIn * 1e3
|
|
308
|
+
});
|
|
309
|
+
return { accessToken: result.accessToken, endpoint: creds?.endpoint };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/commands/cloud.ts
|
|
313
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
314
|
+
function registerCloudCommands(program, deps) {
|
|
315
|
+
const { formatter, sym, runner } = deps;
|
|
316
|
+
const cloud = program.command("cloud").description("Manage Clef Cloud integration.");
|
|
317
|
+
cloud.command("status").description("Show Clef Cloud integration status.").action(async () => {
|
|
318
|
+
try {
|
|
319
|
+
const repoRoot = program.opts().dir || process.cwd();
|
|
320
|
+
const parser = new ManifestParser();
|
|
321
|
+
let manifest;
|
|
322
|
+
try {
|
|
323
|
+
manifest = parser.parse(path4.join(repoRoot, "clef.yaml"));
|
|
324
|
+
} catch {
|
|
325
|
+
formatter.print(`${sym("info")} No clef.yaml found in ${repoRoot}`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
formatter.print(`${sym("clef")} Clef Cloud Status
|
|
329
|
+
`);
|
|
330
|
+
if (manifest.cloud) {
|
|
331
|
+
formatter.print(` Integration: ${manifest.cloud.integrationId}`);
|
|
332
|
+
formatter.print(` Key ID: ${manifest.cloud.keyId}`);
|
|
333
|
+
} else {
|
|
334
|
+
formatter.print(` Cloud: not configured`);
|
|
335
|
+
formatter.hint("\n Run 'clef cloud init --env <environment>' to set up Cloud.");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const cloudEnvs = manifest.environments.filter((e) => e.sops?.backend === "cloud");
|
|
339
|
+
const defaultCloud = manifest.sops.default_backend === "cloud";
|
|
340
|
+
if (cloudEnvs.length > 0 || defaultCloud) {
|
|
341
|
+
const envNames = defaultCloud ? manifest.environments.map((e) => e.name) : cloudEnvs.map((e) => e.name);
|
|
342
|
+
formatter.print(` Environments: ${envNames.join(", ")}`);
|
|
343
|
+
} else {
|
|
344
|
+
formatter.print(` Environments: none using cloud backend`);
|
|
345
|
+
}
|
|
346
|
+
const creds = readCloudCredentials();
|
|
347
|
+
if (creds) {
|
|
348
|
+
formatter.print(` Auth: authenticated`);
|
|
349
|
+
formatter.print(` Endpoint: ${creds.endpoint}`);
|
|
350
|
+
} else {
|
|
351
|
+
formatter.print(` Auth: not authenticated`);
|
|
352
|
+
formatter.hint(" Run 'clef cloud login' to authenticate.");
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const ks = resolveKeyservicePath();
|
|
356
|
+
formatter.print(` Keyservice: ${ks.source} (${ks.path})`);
|
|
357
|
+
} catch {
|
|
358
|
+
formatter.print(` Keyservice: not found`);
|
|
359
|
+
formatter.hint(" Install the cloud package: npm install @clef-sh/cloud");
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
363
|
+
formatter.error(message);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
cloud.command("init").description("Set up Clef Cloud for an environment.").requiredOption("--env <environment>", "Target environment (e.g., production)").action(async (opts) => {
|
|
368
|
+
try {
|
|
369
|
+
const repoRoot = program.opts().dir || process.cwd();
|
|
370
|
+
const parser = new ManifestParser();
|
|
371
|
+
const manifest = parser.parse(path4.join(repoRoot, "clef.yaml"));
|
|
372
|
+
const targetEnv = manifest.environments.find((e) => e.name === opts.env);
|
|
373
|
+
if (!targetEnv) {
|
|
374
|
+
formatter.error(
|
|
375
|
+
`Environment '${opts.env}' not found in clef.yaml. Available: ${manifest.environments.map((e) => e.name).join(", ")}`
|
|
376
|
+
);
|
|
377
|
+
process.exit(1);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (targetEnv.sops?.backend === "cloud" && manifest.cloud) {
|
|
381
|
+
formatter.info(
|
|
382
|
+
`Environment '${opts.env}' is already using Cloud backend (${manifest.cloud.keyId}). Nothing to do.`
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
let keyservicePath;
|
|
387
|
+
try {
|
|
388
|
+
keyservicePath = resolveKeyservicePath().path;
|
|
389
|
+
} catch {
|
|
390
|
+
formatter.error(
|
|
391
|
+
"Keyservice binary not found. Install the cloud package: npm install @clef-sh/cloud"
|
|
392
|
+
);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
formatter.print(`${sym("clef")} Clef Cloud
|
|
397
|
+
`);
|
|
398
|
+
const existingCreds = readCloudCredentials();
|
|
399
|
+
const cloudEndpoint = existingCreds?.endpoint ?? CLOUD_DEFAULT_ENDPOINT;
|
|
400
|
+
formatter.print(` Endpoint: ${cloudEndpoint}`);
|
|
401
|
+
formatter.print(
|
|
402
|
+
` Creds: ${existingCreds ? `authenticated=${existingCreds.refreshToken ? "yes" : "no"}, endpoint=${existingCreds.endpoint}` : "none"}`
|
|
403
|
+
);
|
|
404
|
+
let integrationId;
|
|
405
|
+
let keyId;
|
|
406
|
+
let deviceFlowAccessToken;
|
|
407
|
+
if (existingCreds && existingCreds.refreshToken && manifest.cloud) {
|
|
408
|
+
integrationId = manifest.cloud.integrationId;
|
|
409
|
+
keyId = manifest.cloud.keyId;
|
|
410
|
+
formatter.print(` Using existing Cloud integration: ${keyId}`);
|
|
411
|
+
} else {
|
|
412
|
+
formatter.print(` Opening browser to set up Cloud for ${opts.env}...`);
|
|
413
|
+
const session = await initiateDeviceFlow(cloudEndpoint, {
|
|
414
|
+
repoName: path4.basename(repoRoot),
|
|
415
|
+
environment: opts.env,
|
|
416
|
+
clientVersion: deps.cliVersion,
|
|
417
|
+
flow: "setup"
|
|
418
|
+
});
|
|
419
|
+
formatter.print(` If the browser doesn't open, visit:
|
|
420
|
+
${session.loginUrl}
|
|
421
|
+
`);
|
|
422
|
+
await deps.openBrowser(session.loginUrl, runner);
|
|
423
|
+
formatter.print(` Waiting for authorization... (press Ctrl+C to cancel)`);
|
|
424
|
+
const result = await pollUntilComplete(session.pollUrl);
|
|
425
|
+
if (result.status !== "complete" || !result.token || !result.integrationId || !result.keyId) {
|
|
426
|
+
formatter.error(
|
|
427
|
+
result.status === "expired" ? "Session expired. Run 'clef cloud init' again." : "Setup cancelled."
|
|
428
|
+
);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
integrationId = result.integrationId;
|
|
433
|
+
keyId = result.keyId;
|
|
434
|
+
const creds = {
|
|
435
|
+
refreshToken: result.token,
|
|
436
|
+
endpoint: existingCreds?.endpoint,
|
|
437
|
+
cognitoDomain: result.cognitoDomain,
|
|
438
|
+
clientId: result.clientId
|
|
439
|
+
};
|
|
440
|
+
if (result.accessToken && result.accessTokenExpiresIn) {
|
|
441
|
+
creds.accessToken = result.accessToken;
|
|
442
|
+
creds.accessTokenExpiry = Date.now() + result.accessTokenExpiresIn * 1e3;
|
|
443
|
+
deviceFlowAccessToken = result.accessToken;
|
|
444
|
+
}
|
|
445
|
+
writeCloudCredentials(creds);
|
|
446
|
+
formatter.success("Authorized");
|
|
447
|
+
}
|
|
448
|
+
formatter.print(`
|
|
449
|
+
Provisioning Cloud backend for ${opts.env}...`);
|
|
450
|
+
formatter.print(` ${sym("success")} KMS key provisioned: ${keyId}`);
|
|
451
|
+
formatter.print(`
|
|
452
|
+
Migrating ${opts.env} secrets to Cloud backend...`);
|
|
453
|
+
const cloudManifest = structuredClone(manifest);
|
|
454
|
+
cloudManifest.cloud = { integrationId, keyId };
|
|
455
|
+
const cloudEnv = cloudManifest.environments.find((e) => e.name === opts.env);
|
|
456
|
+
if (cloudEnv) {
|
|
457
|
+
cloudEnv.sops = { backend: "cloud" };
|
|
458
|
+
}
|
|
459
|
+
const matrixManager = new MatrixManager2();
|
|
460
|
+
const cells = matrixManager.resolveMatrix(manifest, repoRoot).filter((c) => c.environment === opts.env && c.exists);
|
|
461
|
+
if (cells.length === 0) {
|
|
462
|
+
formatter.print(` No encrypted files found for ${opts.env}.`);
|
|
463
|
+
} else {
|
|
464
|
+
const ageSopsClient = await deps.createSopsClient(repoRoot, runner);
|
|
465
|
+
const { accessToken, endpoint: ksEndpoint } = deviceFlowAccessToken ? { accessToken: deviceFlowAccessToken, endpoint: cloudEndpoint } : await resolveAccessToken();
|
|
466
|
+
const ksHandle = await spawnKeyservice({
|
|
467
|
+
binaryPath: keyservicePath,
|
|
468
|
+
token: accessToken,
|
|
469
|
+
endpoint: ksEndpoint
|
|
470
|
+
});
|
|
471
|
+
try {
|
|
472
|
+
const cloudSopsClient = await deps.createSopsClient(repoRoot, runner, ksHandle.addr);
|
|
473
|
+
for (const cell of cells) {
|
|
474
|
+
const decrypted = await ageSopsClient.decrypt(cell.filePath);
|
|
475
|
+
await cloudSopsClient.encrypt(
|
|
476
|
+
cell.filePath,
|
|
477
|
+
decrypted.values,
|
|
478
|
+
cloudManifest,
|
|
479
|
+
cell.environment
|
|
480
|
+
);
|
|
481
|
+
const relPath = path4.relative(repoRoot, cell.filePath);
|
|
482
|
+
formatter.print(` ${sym("success")} ${relPath}`);
|
|
483
|
+
}
|
|
484
|
+
formatter.print(`
|
|
485
|
+
Verifying encrypted files...`);
|
|
486
|
+
for (const cell of cells) {
|
|
487
|
+
await cloudSopsClient.decrypt(cell.filePath);
|
|
488
|
+
const relPath = path4.relative(repoRoot, cell.filePath);
|
|
489
|
+
formatter.print(` ${sym("success")} ${relPath}`);
|
|
490
|
+
}
|
|
491
|
+
} finally {
|
|
492
|
+
await ksHandle.kill();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const rawManifest = readManifestYaml(repoRoot);
|
|
496
|
+
rawManifest.cloud = { integrationId, keyId };
|
|
497
|
+
const envs = rawManifest.environments;
|
|
498
|
+
const targetRawEnv = envs.find((e) => e.name === opts.env);
|
|
499
|
+
if (targetRawEnv) {
|
|
500
|
+
targetRawEnv.sops = { backend: "cloud" };
|
|
501
|
+
}
|
|
502
|
+
writeManifestYaml(repoRoot, rawManifest);
|
|
503
|
+
formatter.print(`
|
|
504
|
+
${sym("success")} Cloud setup complete.
|
|
505
|
+
`);
|
|
506
|
+
formatter.print(` Your ${opts.env} environment now uses Clef Cloud for encryption.`);
|
|
507
|
+
formatter.print(` Other environments continue to use age keys locally.
|
|
508
|
+
`);
|
|
509
|
+
formatter.hint(" Commit your changes: git add clef.yaml && git commit");
|
|
510
|
+
} catch (err) {
|
|
511
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
512
|
+
formatter.error(message);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
cloud.command("login").description("Authenticate with Clef Cloud.").action(async () => {
|
|
517
|
+
try {
|
|
518
|
+
formatter.print(`${sym("clef")} Clef Cloud
|
|
519
|
+
`);
|
|
520
|
+
const existingCreds = readCloudCredentials();
|
|
521
|
+
const endpoint = existingCreds?.endpoint;
|
|
522
|
+
const session = await initiateDeviceFlow(endpoint, {
|
|
523
|
+
repoName: path4.basename(process.cwd()),
|
|
524
|
+
clientVersion: deps.cliVersion,
|
|
525
|
+
flow: "login"
|
|
526
|
+
});
|
|
527
|
+
formatter.print(` Opening browser to log in...`);
|
|
528
|
+
formatter.print(` If the browser doesn't open, visit:
|
|
529
|
+
${session.loginUrl}
|
|
530
|
+
`);
|
|
531
|
+
const opened = await deps.openBrowser(session.loginUrl, runner);
|
|
532
|
+
if (!opened) {
|
|
533
|
+
formatter.warn("Could not open browser automatically. Visit the URL above.");
|
|
534
|
+
}
|
|
535
|
+
formatter.print(` Waiting for authorization... (press Ctrl+C to cancel)`);
|
|
536
|
+
const result = await pollUntilComplete(session.pollUrl);
|
|
537
|
+
if (result.status === "expired") {
|
|
538
|
+
formatter.error("Session expired. Run 'clef cloud login' again.");
|
|
539
|
+
process.exit(1);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (result.status === "cancelled") {
|
|
543
|
+
formatter.info("Login cancelled.");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (result.token) {
|
|
547
|
+
const creds = {
|
|
548
|
+
refreshToken: result.token,
|
|
549
|
+
endpoint,
|
|
550
|
+
cognitoDomain: result.cognitoDomain,
|
|
551
|
+
clientId: result.clientId
|
|
552
|
+
};
|
|
553
|
+
if (result.accessToken && result.accessTokenExpiresIn) {
|
|
554
|
+
creds.accessToken = result.accessToken;
|
|
555
|
+
creds.accessTokenExpiry = Date.now() + result.accessTokenExpiresIn * 1e3;
|
|
556
|
+
}
|
|
557
|
+
writeCloudCredentials(creds);
|
|
558
|
+
formatter.success("Logged in. Credentials saved to ~/.clef/credentials.yaml");
|
|
559
|
+
}
|
|
560
|
+
} catch (err) {
|
|
561
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
562
|
+
formatter.error(message);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async function pollUntilComplete(pollUrl) {
|
|
568
|
+
for (; ; ) {
|
|
569
|
+
const result = await pollDeviceFlow(pollUrl);
|
|
570
|
+
if (result.status === "complete" || result.status === "expired" || result.status === "cancelled") {
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
export {
|
|
577
|
+
registerCloudCommands
|
|
578
|
+
};
|
|
579
|
+
//# sourceMappingURL=cli.mjs.map
|