youmd 0.8.2 → 0.8.6
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/commands/agent.d.ts +13 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +233 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +17 -0
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/env.d.ts +20 -4
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +613 -21
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +8 -2
- package/dist/commands/stack.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +8 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +200 -0
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.js +71 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +173 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +87 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/daemon.d.ts +3 -0
- package/dist/lib/daemon.d.ts.map +1 -1
- package/dist/lib/daemon.js +21 -0
- package/dist/lib/daemon.js.map +1 -1
- package/dist/lib/machine-bootstrap-prompt.d.ts.map +1 -1
- package/dist/lib/machine-bootstrap-prompt.js +372 -184
- package/dist/lib/machine-bootstrap-prompt.js.map +1 -1
- package/dist/lib/realtime-sync.d.ts +174 -0
- package/dist/lib/realtime-sync.d.ts.map +1 -0
- package/dist/lib/realtime-sync.js +300 -0
- package/dist/lib/realtime-sync.js.map +1 -0
- package/package.json +2 -1
- package/scripts/env-vault/README.md +9 -0
- package/scripts/env-vault/restore.sh +85 -12
- package/scripts/skillstack-sync/README.md +24 -6
- package/scripts/skillstack-sync/bootstrap-new-mac.sh +31 -0
- package/scripts/skillstack-sync/com.youmd.realtime-sync.plist +35 -0
- package/scripts/skillstack-sync/install-daemons.sh +4 -1
- package/skills/machine-bootstrap.md +114 -19
package/dist/commands/env.js
CHANGED
|
@@ -36,13 +36,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runEnvBackupScript = runEnvBackupScript;
|
|
39
40
|
exports.envBackupCommand = envBackupCommand;
|
|
41
|
+
exports.runEnvRestoreScript = runEnvRestoreScript;
|
|
40
42
|
exports.envRestoreCommand = envRestoreCommand;
|
|
43
|
+
exports.envVaultCommand = envVaultCommand;
|
|
41
44
|
const fs = __importStar(require("fs"));
|
|
42
45
|
const path = __importStar(require("path"));
|
|
46
|
+
const crypto = __importStar(require("crypto"));
|
|
43
47
|
const child_process = __importStar(require("child_process"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
44
49
|
const chalk_1 = __importDefault(require("chalk"));
|
|
45
50
|
const render_1 = require("../lib/render");
|
|
51
|
+
const api_1 = require("../lib/api");
|
|
46
52
|
// The compiled file lives at dist/commands/env.js.
|
|
47
53
|
// Walking up two levels lands at the package root, then into scripts/.
|
|
48
54
|
function resolveScript(name) {
|
|
@@ -56,32 +62,301 @@ function assertScriptExists(scriptPath) {
|
|
|
56
62
|
process.exit(1);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
|
-
function
|
|
65
|
+
function expandHome(value) {
|
|
66
|
+
if (!value)
|
|
67
|
+
return value;
|
|
68
|
+
if (value === "~")
|
|
69
|
+
return os.homedir();
|
|
70
|
+
if (value.startsWith("~/"))
|
|
71
|
+
return path.join(os.homedir(), value.slice(2));
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function defaultVaultOutDir() {
|
|
75
|
+
return path.join(os.homedir(), "Desktop", "youmd-env-vault");
|
|
76
|
+
}
|
|
77
|
+
function defaultCloudVaultDir() {
|
|
78
|
+
return path.join(os.homedir(), ".youmd", "secret-vault");
|
|
79
|
+
}
|
|
80
|
+
function deviceKeyDir() {
|
|
81
|
+
return path.join(defaultCloudVaultDir(), "devices");
|
|
82
|
+
}
|
|
83
|
+
function deviceKeyPath() {
|
|
84
|
+
return path.join(deviceKeyDir(), "current-device-key.json");
|
|
85
|
+
}
|
|
86
|
+
function chmodSafe(filePath, mode) {
|
|
87
|
+
try {
|
|
88
|
+
fs.chmodSync(filePath, mode);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Windows and some external filesystems may ignore POSIX modes.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function deviceIdFromPublicKey(publicKeyPem) {
|
|
95
|
+
return `svd_${crypto.createHash("sha256").update(publicKeyPem).digest("hex").slice(0, 24)}`;
|
|
96
|
+
}
|
|
97
|
+
function defaultDeviceName() {
|
|
98
|
+
return `${os.hostname()} (${process.platform})`;
|
|
99
|
+
}
|
|
100
|
+
function createLocalDeviceKey(deviceName) {
|
|
101
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
102
|
+
modulusLength: 3072,
|
|
103
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
104
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
105
|
+
});
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
return {
|
|
108
|
+
schemaVersion: "you-md/secret-vault-device-key/v1",
|
|
109
|
+
deviceId: deviceIdFromPublicKey(publicKey),
|
|
110
|
+
deviceName: deviceName || defaultDeviceName(),
|
|
111
|
+
hostName: os.hostname(),
|
|
112
|
+
platform: process.platform,
|
|
113
|
+
keyAlgorithm: "rsa-oaep-sha256",
|
|
114
|
+
publicKeyPem: publicKey,
|
|
115
|
+
privateKeyPem: privateKey,
|
|
116
|
+
createdAt: now,
|
|
117
|
+
updatedAt: now,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function loadOrCreateLocalDeviceKey(deviceName) {
|
|
121
|
+
const keyPath = deviceKeyPath();
|
|
122
|
+
if (fs.existsSync(keyPath)) {
|
|
123
|
+
const parsed = JSON.parse(fs.readFileSync(keyPath, "utf-8"));
|
|
124
|
+
if (parsed.schemaVersion === "you-md/secret-vault-device-key/v1" &&
|
|
125
|
+
parsed.publicKeyPem &&
|
|
126
|
+
parsed.privateKeyPem) {
|
|
127
|
+
const updated = {
|
|
128
|
+
...parsed,
|
|
129
|
+
deviceId: parsed.deviceId || deviceIdFromPublicKey(parsed.publicKeyPem),
|
|
130
|
+
deviceName: deviceName || parsed.deviceName || defaultDeviceName(),
|
|
131
|
+
hostName: os.hostname(),
|
|
132
|
+
platform: process.platform,
|
|
133
|
+
keyAlgorithm: "rsa-oaep-sha256",
|
|
134
|
+
updatedAt: Date.now(),
|
|
135
|
+
};
|
|
136
|
+
fs.writeFileSync(keyPath, JSON.stringify(updated, null, 2), { mode: 0o600 });
|
|
137
|
+
chmodSafe(keyPath, 0o600);
|
|
138
|
+
return updated;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const record = createLocalDeviceKey(deviceName);
|
|
142
|
+
fs.mkdirSync(deviceKeyDir(), { recursive: true, mode: 0o700 });
|
|
143
|
+
chmodSafe(defaultCloudVaultDir(), 0o700);
|
|
144
|
+
chmodSafe(deviceKeyDir(), 0o700);
|
|
145
|
+
fs.writeFileSync(keyPath, JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
146
|
+
chmodSafe(keyPath, 0o600);
|
|
147
|
+
return record;
|
|
148
|
+
}
|
|
149
|
+
async function registerCurrentSecretVaultDevice(deviceName) {
|
|
150
|
+
const key = loadOrCreateLocalDeviceKey(deviceName);
|
|
151
|
+
const response = await (0, api_1.registerSecretVaultDevice)({
|
|
152
|
+
deviceId: key.deviceId,
|
|
153
|
+
deviceName: key.deviceName,
|
|
154
|
+
hostName: key.hostName,
|
|
155
|
+
platform: key.platform,
|
|
156
|
+
publicKeyPem: key.publicKeyPem,
|
|
157
|
+
keyAlgorithm: "rsa-oaep-sha256",
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
key,
|
|
161
|
+
device: response.ok ? response.data.device : undefined,
|
|
162
|
+
status: response.status,
|
|
163
|
+
ok: response.ok,
|
|
164
|
+
error: response.ok ? undefined : response.data,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function wrapPassphraseForDevice(passphrase, publicKeyPem) {
|
|
168
|
+
return crypto.publicEncrypt({
|
|
169
|
+
key: publicKeyPem,
|
|
170
|
+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
171
|
+
oaepHash: "sha256",
|
|
172
|
+
}, Buffer.from(passphrase, "utf-8")).toString("base64");
|
|
173
|
+
}
|
|
174
|
+
function unwrapPassphraseForCurrentDevice(wrappedPassphraseBase64, privateKeyPem) {
|
|
175
|
+
return crypto.privateDecrypt({
|
|
176
|
+
key: privateKeyPem,
|
|
177
|
+
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
178
|
+
oaepHash: "sha256",
|
|
179
|
+
}, Buffer.from(wrappedPassphraseBase64, "base64")).toString("utf-8");
|
|
180
|
+
}
|
|
181
|
+
function keychainAccount() {
|
|
182
|
+
return process.env.USER || process.env.LOGNAME || os.userInfo().username || "houston";
|
|
183
|
+
}
|
|
184
|
+
function readPassphraseFromKeychain() {
|
|
185
|
+
if (process.platform !== "darwin")
|
|
186
|
+
return null;
|
|
187
|
+
const service = process.env.YOUMD_ENV_VAULT_KEYCHAIN_SERVICE || "youmd-env-vault";
|
|
188
|
+
const result = child_process.spawnSync("security", ["find-generic-password", "-a", keychainAccount(), "-s", service, "-w"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
189
|
+
if (result.status !== 0)
|
|
190
|
+
return null;
|
|
191
|
+
const passphrase = result.stdout.replace(/\r?\n$/, "");
|
|
192
|
+
return passphrase || null;
|
|
193
|
+
}
|
|
194
|
+
function promptForPassphrase() {
|
|
195
|
+
if (!process.stdin.isTTY)
|
|
196
|
+
return null;
|
|
197
|
+
const script = [
|
|
198
|
+
'printf "You.md vault passphrase: " >&2',
|
|
199
|
+
'IFS= read -r -s PW',
|
|
200
|
+
'printf "\\n" >&2',
|
|
201
|
+
'printf "%s" "$PW"',
|
|
202
|
+
].join("; ");
|
|
203
|
+
const result = child_process.spawnSync("bash", ["-lc", script], {
|
|
204
|
+
encoding: "utf-8",
|
|
205
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
206
|
+
});
|
|
207
|
+
if (result.status !== 0)
|
|
208
|
+
return null;
|
|
209
|
+
return result.stdout || null;
|
|
210
|
+
}
|
|
211
|
+
function acquireVaultPassphrase(opts) {
|
|
212
|
+
if (process.env.ENV_VAULT_PASS)
|
|
213
|
+
return process.env.ENV_VAULT_PASS;
|
|
214
|
+
const keychain = readPassphraseFromKeychain();
|
|
215
|
+
if (keychain)
|
|
216
|
+
return keychain;
|
|
217
|
+
if (!opts.allowPrompt)
|
|
218
|
+
return null;
|
|
219
|
+
return promptForPassphrase();
|
|
220
|
+
}
|
|
221
|
+
function runWithVaultPassphrase(passphrase, fn) {
|
|
222
|
+
const previous = process.env.ENV_VAULT_PASS;
|
|
223
|
+
process.env.ENV_VAULT_PASS = passphrase;
|
|
224
|
+
try {
|
|
225
|
+
return fn();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
if (previous === undefined) {
|
|
229
|
+
delete process.env.ENV_VAULT_PASS;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
process.env.ENV_VAULT_PASS = previous;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function validatePassphraseAgainstLatestVault(passphrase) {
|
|
237
|
+
const response = await (0, api_1.downloadLatestSecretEnvVaultSnapshot)();
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
error: secretVaultErrorMessage(response.status, response.data, "failed to download encrypted env vault for passphrase validation"),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "youmd-vault-share-"));
|
|
245
|
+
const targetPath = path.join(tempDir, response.data.snapshot.fileName.replace(/[\\/]/g, "-"));
|
|
246
|
+
try {
|
|
247
|
+
fs.writeFileSync(targetPath, Buffer.from(response.data.encryptedArchiveBase64, "base64"), { mode: 0o600 });
|
|
248
|
+
const status = runWithVaultPassphrase(passphrase, () => runEnvRestoreScript(targetPath, {
|
|
249
|
+
list: true,
|
|
250
|
+
mapExisting: true,
|
|
251
|
+
existingOnly: true,
|
|
252
|
+
skipAgentAuth: true,
|
|
253
|
+
}));
|
|
254
|
+
if (status !== 0) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
error: "env-vault passphrase did not decrypt the latest Secret Vault snapshot",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return { ok: true };
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
try {
|
|
264
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// best effort cleanup
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function runScript(scriptPath, args) {
|
|
272
|
+
const result = child_process.spawnSync("bash", [scriptPath, ...args], {
|
|
273
|
+
stdio: "inherit",
|
|
274
|
+
});
|
|
275
|
+
if (result.error) {
|
|
276
|
+
console.error(chalk_1.default.hex("#C46A3A")(` error spawning vault script: ${result.error.message}`));
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
return result.status ?? 0;
|
|
280
|
+
}
|
|
281
|
+
function latestVaultFile(outDir) {
|
|
282
|
+
if (!fs.existsSync(outDir))
|
|
283
|
+
return null;
|
|
284
|
+
const candidates = fs
|
|
285
|
+
.readdirSync(outDir)
|
|
286
|
+
.filter((fileName) => /^env-vault-.+\.tar\.(enc|age|gpg)$/.test(fileName))
|
|
287
|
+
.map((fileName) => path.join(outDir, fileName))
|
|
288
|
+
.sort();
|
|
289
|
+
return candidates.at(-1) ?? null;
|
|
290
|
+
}
|
|
291
|
+
function manifestForVault(vaultPath) {
|
|
292
|
+
const match = path.basename(vaultPath).match(/^env-vault-(.+)\.tar\.(enc|age|gpg)$/);
|
|
293
|
+
if (!match)
|
|
294
|
+
return null;
|
|
295
|
+
const candidate = path.join(path.dirname(vaultPath), `manifest-${match[1]}.txt`);
|
|
296
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
297
|
+
}
|
|
298
|
+
function sha256File(filePath) {
|
|
299
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
300
|
+
}
|
|
301
|
+
function parseSafeManifest(manifestText) {
|
|
302
|
+
let encryptionTool = "unknown";
|
|
303
|
+
let projectCount = 0;
|
|
304
|
+
let variableCount = 0;
|
|
305
|
+
let agentAuthIncluded = false;
|
|
306
|
+
for (const line of manifestText.split(/\r?\n/)) {
|
|
307
|
+
const encryptionMatch = line.match(/^Encryption:\s*(.+)$/);
|
|
308
|
+
if (encryptionMatch)
|
|
309
|
+
encryptionTool = encryptionMatch[1].trim() || encryptionTool;
|
|
310
|
+
const envMatch = line.match(/^[^/\s]+\/\.env\.local:\s*(\d+)\s+variable/);
|
|
311
|
+
if (envMatch) {
|
|
312
|
+
projectCount += 1;
|
|
313
|
+
variableCount += Number(envMatch[1]);
|
|
314
|
+
}
|
|
315
|
+
if (/^\s*agent-auth\/.+:\s*\d+\s+bytes\b/.test(line)) {
|
|
316
|
+
agentAuthIncluded = true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { encryptionTool, projectCount, variableCount, agentAuthIncluded };
|
|
320
|
+
}
|
|
321
|
+
function vaultExtension(filePath) {
|
|
322
|
+
const match = path.basename(filePath).match(/\.tar\.(enc|age|gpg)$/);
|
|
323
|
+
return match?.[1] ?? "enc";
|
|
324
|
+
}
|
|
325
|
+
function secretVaultErrorMessage(status, data, fallback) {
|
|
326
|
+
const base = (0, api_1.apiErrorMessage)(data) ?? fallback;
|
|
327
|
+
if (status === 401) {
|
|
328
|
+
return `${base}. Run \`youmd login\` or \`youmd login --key <vault-scoped-key>\` on this trusted device.`;
|
|
329
|
+
}
|
|
330
|
+
if (status === 403) {
|
|
331
|
+
return `${base}. This key may be missing the \`vault\` scope; create or mint a fresh machine/bootstrap key with \`vault\`, then rerun.`;
|
|
332
|
+
}
|
|
333
|
+
if (status === 404) {
|
|
334
|
+
return `${base}. The installed CLI or hosted You.md API may be stale; run the curl installer/update path, then retry.`;
|
|
335
|
+
}
|
|
336
|
+
return base;
|
|
337
|
+
}
|
|
338
|
+
function runEnvBackupScript(opts) {
|
|
60
339
|
const scriptPath = resolveScript("backup.sh");
|
|
61
340
|
assertScriptExists(scriptPath);
|
|
62
341
|
const args = [];
|
|
63
342
|
if (opts.preflight)
|
|
64
343
|
args.push("--preflight");
|
|
65
344
|
if (opts.root)
|
|
66
|
-
args.push("--root", opts.root);
|
|
345
|
+
args.push("--root", expandHome(opts.root));
|
|
67
346
|
if (opts.out)
|
|
68
|
-
args.push("--out", opts.out);
|
|
347
|
+
args.push("--out", expandHome(opts.out));
|
|
69
348
|
// Print the spinner line then stop it before handing the TTY to bash
|
|
70
349
|
// (the script needs an interactive TTY for the passphrase prompt).
|
|
71
350
|
const spinner = new render_1.BrailleSpinner(opts.preflight ? "checking env vault readiness..." : "sealing your env vault...");
|
|
72
351
|
spinner.start();
|
|
73
352
|
spinner.stop("handing off to vault script");
|
|
74
353
|
console.log("");
|
|
75
|
-
|
|
76
|
-
stdio: "inherit",
|
|
77
|
-
});
|
|
78
|
-
if (result.error) {
|
|
79
|
-
console.error(chalk_1.default.hex("#C46A3A")(` error spawning backup script: ${result.error.message}`));
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
process.exit(result.status ?? 0);
|
|
354
|
+
return runScript(scriptPath, args);
|
|
83
355
|
}
|
|
84
|
-
function
|
|
356
|
+
function envBackupCommand(opts) {
|
|
357
|
+
process.exit(runEnvBackupScript(opts));
|
|
358
|
+
}
|
|
359
|
+
function runEnvRestoreScript(vault, opts) {
|
|
85
360
|
const scriptPath = resolveScript("restore.sh");
|
|
86
361
|
assertScriptExists(scriptPath);
|
|
87
362
|
// Build arg list — restore.sh expects: [--list] [--force] [--root <path>] <vault-file>
|
|
@@ -90,20 +365,337 @@ function envRestoreCommand(vault, opts) {
|
|
|
90
365
|
args.push("--list");
|
|
91
366
|
if (opts.force)
|
|
92
367
|
args.push("--force");
|
|
368
|
+
if (opts.mapExisting)
|
|
369
|
+
args.push("--map-existing");
|
|
370
|
+
if (opts.existingOnly)
|
|
371
|
+
args.push("--existing-only");
|
|
372
|
+
if (opts.skipAgentAuth)
|
|
373
|
+
args.push("--skip-agent-auth");
|
|
93
374
|
if (opts.root)
|
|
94
|
-
args.push("--root", opts.root);
|
|
95
|
-
args.push(vault);
|
|
375
|
+
args.push("--root", expandHome(opts.root));
|
|
376
|
+
args.push(expandHome(vault));
|
|
96
377
|
const spinner = new render_1.BrailleSpinner(opts.list ? "inspecting the vault..." : "opening the vault...");
|
|
97
378
|
spinner.start();
|
|
98
379
|
spinner.stop("handing off to restore script");
|
|
99
380
|
console.log("");
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
381
|
+
return runScript(scriptPath, args);
|
|
382
|
+
}
|
|
383
|
+
function envRestoreCommand(vault, opts) {
|
|
384
|
+
process.exit(runEnvRestoreScript(vault, opts));
|
|
385
|
+
}
|
|
386
|
+
async function envVaultCommand(action, opts = {}) {
|
|
387
|
+
const subcommand = action || "help";
|
|
388
|
+
if (subcommand === "device-register") {
|
|
389
|
+
const spinner = new render_1.BrailleSpinner("registering this Mac as a trusted Secret Vault device...");
|
|
390
|
+
spinner.start();
|
|
391
|
+
const result = await registerCurrentSecretVaultDevice(opts.deviceName);
|
|
392
|
+
spinner.stop(result.ok ? "trusted device registered" : "device registration failed");
|
|
393
|
+
if (!result.ok) {
|
|
394
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(result.status, result.error, "failed to register Secret Vault device")}`));
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
397
|
+
if (opts.json) {
|
|
398
|
+
console.log(JSON.stringify({
|
|
399
|
+
success: true,
|
|
400
|
+
device: result.device,
|
|
401
|
+
localKeyPath: deviceKeyPath(),
|
|
402
|
+
secretValuesExposed: false,
|
|
403
|
+
}, null, 2));
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
console.log(chalk_1.default.hex("#C46A3A")(" You.md Secret Vault device"));
|
|
407
|
+
console.log(` device: ${result.key.deviceId}`);
|
|
408
|
+
console.log(` name: ${result.device?.deviceName ?? result.key.deviceName}`);
|
|
409
|
+
console.log(` host: ${result.device?.hostName ?? result.key.hostName}`);
|
|
410
|
+
console.log(` key: ${deviceKeyPath()}`);
|
|
411
|
+
console.log(chalk_1.default.dim(" private key stays local; public key only was synced"));
|
|
412
|
+
console.log(chalk_1.default.dim(" secret values exposed: false"));
|
|
413
|
+
}
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
if (subcommand === "device-list") {
|
|
417
|
+
const response = await (0, api_1.listSecretVaultDevices)();
|
|
418
|
+
if (!response.ok) {
|
|
419
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(response.status, response.data, "failed to list Secret Vault devices")}`));
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
if (opts.json) {
|
|
423
|
+
console.log(JSON.stringify(response.data, null, 2));
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
const devices = response.data.devices;
|
|
427
|
+
if (devices.length === 0) {
|
|
428
|
+
console.log(chalk_1.default.dim(" no trusted Secret Vault devices registered yet"));
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
console.log(chalk_1.default.hex("#C46A3A")(" You.md Secret Vault trusted devices"));
|
|
432
|
+
for (const device of devices) {
|
|
433
|
+
const lastSeen = device.lastSeenAt ? new Date(device.lastSeenAt).toISOString() : "never";
|
|
434
|
+
const fingerprint = crypto.createHash("sha256").update(device.publicKeyPem).digest("hex");
|
|
435
|
+
const status = device.revokedAt || !device.trusted ? "revoked" : "trusted";
|
|
436
|
+
console.log(` ${device.deviceId} ${status}`);
|
|
437
|
+
console.log(chalk_1.default.dim(` ${device.deviceName} · ${device.hostName ?? "unknown host"} · last seen ${lastSeen}`));
|
|
438
|
+
console.log(chalk_1.default.dim(` public key fingerprint ${fingerprint.slice(0, 12)}...${fingerprint.slice(-8)}`));
|
|
439
|
+
}
|
|
440
|
+
console.log(chalk_1.default.dim(" secret values exposed: false"));
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
if (subcommand === "share") {
|
|
444
|
+
const registration = await registerCurrentSecretVaultDevice(opts.deviceName);
|
|
445
|
+
if (!registration.ok) {
|
|
446
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(registration.status, registration.error, "failed to register source Secret Vault device")}`));
|
|
447
|
+
return 1;
|
|
448
|
+
}
|
|
449
|
+
const snapshotsResponse = await (0, api_1.listSecretEnvVaultSnapshots)({ limit: 1 });
|
|
450
|
+
if (!snapshotsResponse.ok) {
|
|
451
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(snapshotsResponse.status, snapshotsResponse.data, "failed to list encrypted env vault snapshots")}`));
|
|
452
|
+
return 1;
|
|
453
|
+
}
|
|
454
|
+
const snapshot = snapshotsResponse.data.snapshots[0];
|
|
455
|
+
if (!snapshot) {
|
|
456
|
+
console.error(chalk_1.default.hex("#C46A3A")(" no encrypted env vault snapshot exists yet"));
|
|
457
|
+
console.error(chalk_1.default.dim(" run `youmd env vault push --root ~/Desktop/CODE_2025 --out ~/Desktop/youmd-env-vault` on the source Mac first"));
|
|
458
|
+
return 1;
|
|
459
|
+
}
|
|
460
|
+
const devicesResponse = await (0, api_1.listSecretVaultDevices)();
|
|
461
|
+
if (!devicesResponse.ok) {
|
|
462
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(devicesResponse.status, devicesResponse.data, "failed to list Secret Vault devices")}`));
|
|
463
|
+
return 1;
|
|
464
|
+
}
|
|
465
|
+
const devices = devicesResponse.data.devices.filter((device) => device.trusted && !device.revokedAt);
|
|
466
|
+
if (devices.length === 0) {
|
|
467
|
+
console.error(chalk_1.default.hex("#C46A3A")(" no trusted Secret Vault devices are registered yet"));
|
|
468
|
+
console.error(chalk_1.default.dim(" run `youmd env vault device-register` on the new Mac, then rerun `youmd env vault share` here"));
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
471
|
+
const passphrase = acquireVaultPassphrase({ allowPrompt: true });
|
|
472
|
+
if (!passphrase) {
|
|
473
|
+
console.error(chalk_1.default.hex("#C46A3A")(" could not load the env-vault passphrase on this trusted source Mac"));
|
|
474
|
+
console.error(chalk_1.default.dim(" set ENV_VAULT_PASS for this command, store macOS Keychain service `youmd-env-vault`, or rerun in an interactive Terminal"));
|
|
475
|
+
return 1;
|
|
476
|
+
}
|
|
477
|
+
const validation = await validatePassphraseAgainstLatestVault(passphrase);
|
|
478
|
+
if (!validation.ok) {
|
|
479
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${validation.error}`));
|
|
480
|
+
console.error(chalk_1.default.dim(" no trusted-device envelopes were written"));
|
|
481
|
+
return 1;
|
|
482
|
+
}
|
|
483
|
+
const spinner = new render_1.BrailleSpinner("wrapping vault access for trusted devices...");
|
|
484
|
+
spinner.start();
|
|
485
|
+
let shared = 0;
|
|
486
|
+
for (const device of devices) {
|
|
487
|
+
const wrappedPassphraseBase64 = wrapPassphraseForDevice(passphrase, device.publicKeyPem);
|
|
488
|
+
const response = await (0, api_1.upsertSecretVaultKeyEnvelope)({
|
|
489
|
+
snapshotId: snapshot.id,
|
|
490
|
+
deviceId: device.deviceId,
|
|
491
|
+
wrappedPassphraseBase64,
|
|
492
|
+
wrapAlgorithm: "rsa-oaep-sha256",
|
|
493
|
+
sourceHost: os.hostname(),
|
|
494
|
+
});
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
spinner.stop("vault sharing failed");
|
|
497
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(response.status, response.data, `failed to share vault access for ${device.deviceName}`)}`));
|
|
498
|
+
return 1;
|
|
499
|
+
}
|
|
500
|
+
shared += 1;
|
|
501
|
+
}
|
|
502
|
+
spinner.stop("trusted-device vault access shared");
|
|
503
|
+
if (opts.json) {
|
|
504
|
+
console.log(JSON.stringify({
|
|
505
|
+
success: true,
|
|
506
|
+
snapshot: {
|
|
507
|
+
id: snapshot.id,
|
|
508
|
+
fileName: snapshot.fileName,
|
|
509
|
+
projectCount: snapshot.projectCount,
|
|
510
|
+
variableCount: snapshot.variableCount,
|
|
511
|
+
createdAt: snapshot.createdAt,
|
|
512
|
+
},
|
|
513
|
+
devicesShared: shared,
|
|
514
|
+
secretValuesExposed: false,
|
|
515
|
+
}, null, 2));
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
console.log(chalk_1.default.hex("#C46A3A")(" You.md Secret Vault shared"));
|
|
519
|
+
console.log(` snapshot: ${snapshot.fileName}`);
|
|
520
|
+
console.log(` devices: ${shared}`);
|
|
521
|
+
console.log(chalk_1.default.dim(" wrapped passphrases only; raw .env.local values were never uploaded or printed"));
|
|
522
|
+
console.log(chalk_1.default.dim(" new Mac restore command: youmd env vault pull --restore --root ~/Desktop/CODE_YOU --map-existing --existing-only --skip-agent-auth"));
|
|
523
|
+
}
|
|
524
|
+
return 0;
|
|
525
|
+
}
|
|
526
|
+
if (subcommand === "push") {
|
|
527
|
+
const outDir = expandHome(opts.out) ?? defaultVaultOutDir();
|
|
528
|
+
const rootDir = expandHome(opts.root);
|
|
529
|
+
const backupStatus = runEnvBackupScript({ root: rootDir, out: outDir, preflight: opts.preflight });
|
|
530
|
+
if (backupStatus !== 0)
|
|
531
|
+
return backupStatus;
|
|
532
|
+
if (opts.preflight)
|
|
533
|
+
return 0;
|
|
534
|
+
const vaultPath = latestVaultFile(outDir);
|
|
535
|
+
if (!vaultPath) {
|
|
536
|
+
console.error(chalk_1.default.hex("#C46A3A")(` no env-vault-*.tar.* file found in ${outDir}`));
|
|
537
|
+
return 1;
|
|
538
|
+
}
|
|
539
|
+
const manifestPath = manifestForVault(vaultPath);
|
|
540
|
+
const manifestText = manifestPath ? fs.readFileSync(manifestPath, "utf-8") : "";
|
|
541
|
+
const parsed = parseSafeManifest(manifestText);
|
|
542
|
+
const archive = fs.readFileSync(vaultPath);
|
|
543
|
+
const sha256 = sha256File(vaultPath);
|
|
544
|
+
const manifestSha256 = manifestPath ? sha256File(manifestPath) : undefined;
|
|
545
|
+
const extension = vaultExtension(vaultPath);
|
|
546
|
+
const spinner = new render_1.BrailleSpinner("syncing encrypted env vault to You.md Secret Vault...");
|
|
547
|
+
spinner.start();
|
|
548
|
+
const response = await (0, api_1.uploadSecretEnvVaultSnapshot)({
|
|
549
|
+
label: opts.label,
|
|
550
|
+
fileName: path.basename(vaultPath),
|
|
551
|
+
contentType: "application/octet-stream",
|
|
552
|
+
encryption: {
|
|
553
|
+
tool: parsed.encryptionTool,
|
|
554
|
+
extension,
|
|
555
|
+
formatVersion: 1,
|
|
556
|
+
},
|
|
557
|
+
encryptedArchiveBase64: archive.toString("base64"),
|
|
558
|
+
sha256,
|
|
559
|
+
manifestText: manifestText || undefined,
|
|
560
|
+
manifestSha256,
|
|
561
|
+
projectCount: parsed.projectCount,
|
|
562
|
+
variableCount: parsed.variableCount,
|
|
563
|
+
agentAuthIncluded: parsed.agentAuthIncluded,
|
|
564
|
+
sourceHost: os.hostname(),
|
|
565
|
+
sourceRoot: rootDir,
|
|
566
|
+
});
|
|
567
|
+
spinner.stop(response.ok ? "encrypted vault synced" : "vault sync failed");
|
|
568
|
+
if (!response.ok) {
|
|
569
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(response.status, response.data, "failed to upload encrypted env vault")}`));
|
|
570
|
+
return 1;
|
|
571
|
+
}
|
|
572
|
+
const snapshot = response.data.snapshot;
|
|
573
|
+
if (opts.json) {
|
|
574
|
+
console.log(JSON.stringify(response.data, null, 2));
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
console.log(chalk_1.default.hex("#C46A3A")(" You.md Secret Vault"));
|
|
578
|
+
console.log(` snapshot: ${snapshot.fileName}`);
|
|
579
|
+
console.log(` projects: ${snapshot.projectCount} variables: ${snapshot.variableCount ?? 0} bytes: ${snapshot.sizeBytes}`);
|
|
580
|
+
console.log(` sha256: ${snapshot.sha256.slice(0, 12)}...${snapshot.sha256.slice(-8)}`);
|
|
581
|
+
console.log(chalk_1.default.dim(" secret values exposed: false"));
|
|
582
|
+
}
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
585
|
+
if (subcommand === "list") {
|
|
586
|
+
const response = await (0, api_1.listSecretEnvVaultSnapshots)({ limit: 12 });
|
|
587
|
+
if (!response.ok) {
|
|
588
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(response.status, response.data, "failed to list encrypted env vault snapshots")}`));
|
|
589
|
+
return 1;
|
|
590
|
+
}
|
|
591
|
+
if (opts.json) {
|
|
592
|
+
console.log(JSON.stringify(response.data, null, 2));
|
|
593
|
+
return 0;
|
|
594
|
+
}
|
|
595
|
+
const snapshots = response.data.snapshots;
|
|
596
|
+
if (snapshots.length === 0) {
|
|
597
|
+
console.log(chalk_1.default.dim(" no encrypted env vault snapshots uploaded yet"));
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
console.log(chalk_1.default.hex("#C46A3A")(" You.md Secret Vault snapshots"));
|
|
601
|
+
for (const snapshot of snapshots) {
|
|
602
|
+
const created = new Date(snapshot.createdAt).toISOString();
|
|
603
|
+
console.log(` ${created} ${snapshot.fileName}`);
|
|
604
|
+
console.log(chalk_1.default.dim(` ${snapshot.projectCount} projects · ${snapshot.variableCount ?? 0} vars · ${snapshot.sizeBytes} bytes · ${snapshot.sourceHost ?? "unknown host"}`));
|
|
605
|
+
}
|
|
606
|
+
console.log(chalk_1.default.dim(" secret values exposed: false"));
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
if (subcommand === "pull") {
|
|
610
|
+
const outDir = expandHome(opts.out) ?? defaultCloudVaultDir();
|
|
611
|
+
fs.mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
612
|
+
let trustedDevicePassphrase = null;
|
|
613
|
+
if (opts.restore) {
|
|
614
|
+
const registration = await registerCurrentSecretVaultDevice(opts.deviceName);
|
|
615
|
+
if (!registration.ok) {
|
|
616
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(registration.status, registration.error, "failed to register Secret Vault device before restore")}`));
|
|
617
|
+
return 1;
|
|
618
|
+
}
|
|
619
|
+
const envelopeResponse = await (0, api_1.getSecretVaultKeyEnvelope)(registration.key.deviceId);
|
|
620
|
+
if (envelopeResponse.ok) {
|
|
621
|
+
try {
|
|
622
|
+
trustedDevicePassphrase = unwrapPassphraseForCurrentDevice(envelopeResponse.data.envelope.wrappedPassphraseBase64, registration.key.privateKeyPem);
|
|
623
|
+
if (!opts.printPath) {
|
|
624
|
+
console.log(chalk_1.default.dim(` trusted-device envelope unlocked for ${registration.key.deviceId}`));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
console.error(chalk_1.default.hex("#C46A3A")(" Secret Vault envelope exists but this Mac could not decrypt it"));
|
|
629
|
+
console.error(chalk_1.default.dim(" remove/re-register this device key or rerun `youmd env vault share` on the source Mac"));
|
|
630
|
+
return 1;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
trustedDevicePassphrase = acquireVaultPassphrase({ allowPrompt: process.stdin.isTTY });
|
|
635
|
+
if (!trustedDevicePassphrase) {
|
|
636
|
+
if (!opts.printPath) {
|
|
637
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(envelopeResponse.status, envelopeResponse.data, "no trusted-device vault envelope exists for this Mac yet")}`));
|
|
638
|
+
console.error(chalk_1.default.dim(` this Mac is registered as ${registration.key.deviceId}`));
|
|
639
|
+
console.error(chalk_1.default.dim(" on the source Mac, run: youmd env vault share"));
|
|
640
|
+
console.error(chalk_1.default.dim(" then rerun this restore command; raw .env.local values still never touch the browser or chat"));
|
|
641
|
+
}
|
|
642
|
+
return 1;
|
|
643
|
+
}
|
|
644
|
+
if (!opts.printPath) {
|
|
645
|
+
console.log(chalk_1.default.dim(" no trusted-device envelope yet; using local ENV_VAULT_PASS/Keychain/passphrase fallback"));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const response = await (0, api_1.downloadLatestSecretEnvVaultSnapshot)();
|
|
650
|
+
if (!response.ok) {
|
|
651
|
+
if (!opts.printPath) {
|
|
652
|
+
console.error(chalk_1.default.hex("#C46A3A")(` ${secretVaultErrorMessage(response.status, response.data, "failed to download encrypted env vault snapshot")}`));
|
|
653
|
+
}
|
|
654
|
+
return 1;
|
|
655
|
+
}
|
|
656
|
+
const snapshot = response.data.snapshot;
|
|
657
|
+
const safeName = snapshot.fileName.replace(/[\\/]/g, "-");
|
|
658
|
+
const targetPath = path.join(outDir, safeName);
|
|
659
|
+
fs.writeFileSync(targetPath, Buffer.from(response.data.encryptedArchiveBase64, "base64"), { mode: 0o600 });
|
|
660
|
+
if (snapshot.manifestText) {
|
|
661
|
+
const manifestName = `manifest-${snapshot.id}.txt`;
|
|
662
|
+
fs.writeFileSync(path.join(outDir, manifestName), snapshot.manifestText, { mode: 0o600 });
|
|
663
|
+
}
|
|
664
|
+
if (opts.printPath) {
|
|
665
|
+
console.log(targetPath);
|
|
666
|
+
}
|
|
667
|
+
else if (opts.json) {
|
|
668
|
+
console.log(JSON.stringify({ ...response.data, encryptedArchiveBase64: undefined, path: targetPath }, null, 2));
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
console.log(chalk_1.default.hex("#C46A3A")(" pulled encrypted env vault"));
|
|
672
|
+
console.log(` path: ${targetPath}`);
|
|
673
|
+
console.log(` snapshot: ${snapshot.fileName}`);
|
|
674
|
+
console.log(` projects: ${snapshot.projectCount} variables: ${snapshot.variableCount ?? 0}`);
|
|
675
|
+
console.log(chalk_1.default.dim(" secret values exposed: false"));
|
|
676
|
+
}
|
|
677
|
+
if (opts.restore) {
|
|
678
|
+
return runWithVaultPassphrase(trustedDevicePassphrase ?? "", () => runEnvRestoreScript(targetPath, {
|
|
679
|
+
root: opts.root,
|
|
680
|
+
force: opts.force,
|
|
681
|
+
list: false,
|
|
682
|
+
mapExisting: opts.mapExisting ?? true,
|
|
683
|
+
existingOnly: opts.existingOnly ?? true,
|
|
684
|
+
skipAgentAuth: opts.skipAgentAuth ?? true,
|
|
685
|
+
}));
|
|
686
|
+
}
|
|
687
|
+
return 0;
|
|
106
688
|
}
|
|
107
|
-
|
|
689
|
+
console.log("usage: youmd env vault <push|pull|list|device-register|device-list|share> [options]");
|
|
690
|
+
console.log(" vault push encrypt local .env.local files and upload ciphertext to You.md Secret Vault");
|
|
691
|
+
console.log(" vault pull download the latest encrypted account vault to ~/.youmd/secret-vault");
|
|
692
|
+
console.log(" vault pull --restore --root <dir>");
|
|
693
|
+
console.log(" restore into cloned project dirs with safe fresh-machine defaults");
|
|
694
|
+
console.log(" vault list show encrypted vault snapshots without exposing values");
|
|
695
|
+
console.log(" vault device-register");
|
|
696
|
+
console.log(" register this Mac's local public key as a trusted Secret Vault device");
|
|
697
|
+
console.log(" vault device-list show trusted devices and public-key fingerprints only");
|
|
698
|
+
console.log(" vault share wrap the latest vault passphrase to every trusted device");
|
|
699
|
+
return 0;
|
|
108
700
|
}
|
|
109
701
|
//# sourceMappingURL=env.js.map
|