@zonease/happy 0.6.39 → 0.6.40
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/bin/happy.mjs +10 -0
- package/dist/{api-CwOCQeLH.cjs → api-C7FyJfZ1.cjs} +87 -7
- package/dist/{api-CIZKyfSn.mjs → api-D6WL40-U.mjs} +86 -6
- package/dist/{auth-BIleVCw0.cjs → auth-C7ZScWEw.cjs} +2 -2
- package/dist/{auth-Cas9DR3b.cjs → auth-DCsIoZLT.cjs} +4 -4
- package/dist/{auth-0VnlXT9f.mjs → auth-DO9GexAR.mjs} +4 -4
- package/dist/{auth-BE8dD20T.mjs → auth-DxJ3VvrX.mjs} +2 -2
- package/dist/{browser-Dll5hxOV.mjs → browser-AoVGRX7y.mjs} +1 -1
- package/dist/{browser-Cc08m7of.cjs → browser-DzgO4oaD.cjs} +1 -1
- package/dist/{claudeLocal-WkgLbl1a.cjs → claudeLocal-C6BdzhDx.cjs} +1 -1
- package/dist/{claudeLocal-CPT3zcIU.mjs → claudeLocal-CWNGpJ0-.mjs} +1 -1
- package/dist/{config-Draa-5XC.cjs → config-CclB3QW9.cjs} +1 -1
- package/dist/{config-rTT4qTU5.mjs → config-DZk-mJ0K.mjs} +1 -1
- package/dist/{connect-BfC1tYP_.mjs → connect-BXLUKJ5P.mjs} +4 -4
- package/dist/{connect-CJZ73syK.cjs → connect-k9CqDJ-T.cjs} +4 -4
- package/dist/{controlClient-DZMIsKsT.cjs → controlClient-B23M-WPp.cjs} +10 -1
- package/dist/{controlClient-CM_QcJnB.mjs → controlClient-DB5tDU7V.mjs} +10 -2
- package/dist/deviceKeys-BTdo34Uz.mjs +175 -0
- package/dist/deviceKeys-C540DcXl.cjs +182 -0
- package/dist/{doctor-Cya9YSA0.cjs → doctor-BQGLhrcI.cjs} +2 -2
- package/dist/{doctor-DZYB6IVc.mjs → doctor-CU4JLGQ1.mjs} +2 -2
- package/dist/{downloadImage-DfSFLb-h.mjs → downloadImage-DZXI3mXL.mjs} +3 -3
- package/dist/{downloadImage-MQMrUUxS.cjs → downloadImage-cQ1Z7hkC.cjs} +5 -5
- package/dist/{enroll-C52x8oly.mjs → enroll-B55BQR56.mjs} +3 -3
- package/dist/{enroll-BxlbVftP.cjs → enroll-Bo-CNio1.cjs} +3 -3
- package/dist/{evidence-CjWBG5rX.cjs → evidence-DQp8QWcv.cjs} +1 -1
- package/dist/{evidence-Ccd9iYcj.mjs → evidence-DTYRAaOZ.mjs} +1 -1
- package/dist/{formatImageMessage-DbyG8c2j.cjs → formatImageMessage-CixDYY6G.cjs} +1 -1
- package/dist/{formatImageMessage-B4lSxlk2.mjs → formatImageMessage-DbKSSjMx.mjs} +1 -1
- package/dist/index.cjs +42 -42
- package/dist/index.mjs +42 -42
- package/dist/{install-DAeCn8ae.mjs → install-B1e6rdy7.mjs} +2 -2
- package/dist/{install-oRAjVLjP.mjs → install-CgUPGIoF.mjs} +1 -1
- package/dist/{install-C_cp8b2-.cjs → install-D7Ir7ciZ.cjs} +2 -2
- package/dist/{install-BokHRgFW.cjs → install-DSDufaPd.cjs} +1 -1
- package/dist/lib.cjs +3 -3
- package/dist/lib.d.cts +77 -0
- package/dist/lib.d.mts +77 -0
- package/dist/lib.mjs +3 -3
- package/dist/{loadableToolOutput-Cuygmd4K.mjs → loadableToolOutput-Bfei9wPq.mjs} +3 -3
- package/dist/{loadableToolOutput-DDCJjywt.cjs → loadableToolOutput-CbLGtvST.cjs} +3 -3
- package/dist/{persistence-JLUn_Nou.cjs → persistence-CBTtC_dm.cjs} +1 -1
- package/dist/{persistence-BFjvtUad.mjs → persistence-DKfF81pd.mjs} +2 -2
- package/dist/{run-DBZqN6Kn.mjs → run-BVZsMNwN.mjs} +45 -9
- package/dist/{run-BDn2aE3Q.cjs → run-CURx7LhK.cjs} +45 -9
- package/dist/{runClaude-DNHvERX0.mjs → runClaude-6Gi0sYQy.mjs} +14 -14
- package/dist/{runClaude-uaKxPZPm.cjs → runClaude-CL3Kt5ex.cjs} +15 -15
- package/dist/{runCodex-C-RTM0wA.mjs → runCodex-ClwEgJ7M.mjs} +13 -13
- package/dist/{runCodex-82UrZLme.cjs → runCodex-YKyVBkJ3.cjs} +13 -13
- package/dist/{runGemini-Bm6-4Xm2.mjs → runGemini-BPWxVr60.mjs} +15 -15
- package/dist/{runGemini-CXMr4Jwb.cjs → runGemini-Xz2W1spn.cjs} +15 -15
- package/dist/{runOneShot-ioARRHrx.mjs → runOneShot-CdCWiiLr.mjs} +2 -2
- package/dist/{runOneShot-1B6RMYT7.cjs → runOneShot-DjZyCvDI.cjs} +2 -2
- package/dist/{serve-CwibwU6N.mjs → serve-B48JttQ3.mjs} +6 -6
- package/dist/{serve-BtdAhkIE.cjs → serve-B8vaWQA0.cjs} +6 -6
- package/dist/{session-2sU4Bu1R.mjs → session-BXboDn2V.mjs} +5 -5
- package/dist/{session-D--F87zL.cjs → session-BZO25zxp.cjs} +5 -5
- package/dist/{sessionKeyCache-D2BwO91C.mjs → sessionKeyCache-B6cGadYL.mjs} +1 -1
- package/dist/{sessionKeyCache-4Tp8pJ3N.cjs → sessionKeyCache-CDmexJ9p.cjs} +1 -1
- package/dist/{sessionList-BCSw3qYt.mjs → sessionList-CgAYZpNj.mjs} +2 -2
- package/dist/{sessionList-ICJsjNtJ.cjs → sessionList-lQ-nqEhc.cjs} +2 -2
- package/dist/{sessionSummary-Bkz52BPh.cjs → sessionSummary-CcORiQSq.cjs} +3 -3
- package/dist/{sessionSummary-lHGBWQSb.mjs → sessionSummary-DCWQoaDP.mjs} +3 -3
- package/dist/{spawnHappyCLI-D_V-AAoP.cjs → spawnHappyCLI-KiazE3kL.cjs} +1 -1
- package/dist/{spawnHappyCLI-CoORX0YW.mjs → spawnHappyCLI-yBIP_Oqi.mjs} +1 -1
- package/dist/{ssh-DL_AkRAD.mjs → ssh-q5uZhqBf.mjs} +134 -103
- package/dist/{ssh-BTOlhhxi.cjs → ssh-yMllSHu5.cjs} +138 -106
- package/dist/{task-CSpaC9HX.cjs → task-_nr7J19j.cjs} +6 -6
- package/dist/{task-CQzrHRZe.mjs → task-wNX1N2i4.mjs} +6 -6
- package/dist/{uninstall-BiKyRAb1.cjs → uninstall-Buaz7d-X.cjs} +2 -2
- package/dist/{uninstall-nFMFGUVM.mjs → uninstall-Dxf6_4mQ.mjs} +2 -2
- package/dist/{update-Dw8pgsAT.cjs → update-CGlIfa8e.cjs} +3 -3
- package/dist/{update-BcqRHXf2.mjs → update-CnnoX2cV.mjs} +3 -3
- package/package.json +1 -1
- package/dist/deviceExec-CU1R9CRb.cjs +0 -84
- package/dist/deviceExec-D93AOVV4.mjs +0 -81
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { f as decodeBase64, g as encodeBase64, h as encrypt, e as decrypt, c as configuration } from './persistence-DKfF81pd.mjs';
|
|
3
|
+
import { decryptWithEphemeralKey } from './auth-DxJ3VvrX.mjs';
|
|
4
|
+
import tweetnacl from 'tweetnacl';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { writeFile, mkdir, readFile } from 'node:fs/promises';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { hostname } from 'node:os';
|
|
9
|
+
|
|
10
|
+
function resolveMachineKey(credentials, dataEncryptionKey) {
|
|
11
|
+
if (credentials.encryption.type === "legacy") {
|
|
12
|
+
return { key: credentials.encryption.secret, variant: "legacy" };
|
|
13
|
+
}
|
|
14
|
+
if (!dataEncryptionKey) return null;
|
|
15
|
+
const blob = decodeBase64(dataEncryptionKey);
|
|
16
|
+
if (blob.length < 1 || blob[0] !== 0) return null;
|
|
17
|
+
const opened = decryptWithEphemeralKey(blob.slice(1), credentials.encryption.machineKey);
|
|
18
|
+
return opened ? { key: opened, variant: "dataKey" } : null;
|
|
19
|
+
}
|
|
20
|
+
async function fetchMachines(credentials) {
|
|
21
|
+
const response = await axios.get(
|
|
22
|
+
`${configuration.serverUrl}/v1/machines`,
|
|
23
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
24
|
+
);
|
|
25
|
+
const data = response.data;
|
|
26
|
+
return Array.isArray(data) ? data : data.machines ?? [];
|
|
27
|
+
}
|
|
28
|
+
async function listDevices(credentials) {
|
|
29
|
+
const rows = await fetchMachines(credentials);
|
|
30
|
+
return rows.map((row) => {
|
|
31
|
+
const resolved = resolveMachineKey(credentials, row.dataEncryptionKey);
|
|
32
|
+
let metadata = null;
|
|
33
|
+
if (resolved && row.metadata) {
|
|
34
|
+
try {
|
|
35
|
+
metadata = decrypt(resolved.key, resolved.variant, decodeBase64(row.metadata));
|
|
36
|
+
} catch {
|
|
37
|
+
metadata = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
name: row.displayName || metadata?.displayName || metadata?.host || row.id.slice(0, 12),
|
|
43
|
+
description: row.description ?? null,
|
|
44
|
+
platform: metadata?.platform ?? null,
|
|
45
|
+
active: row.active,
|
|
46
|
+
lastActiveAt: row.activeAt ?? row.lastActiveAt ?? 0
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function deviceExec(socket, credentials, deviceId, command, options = {}) {
|
|
51
|
+
const row = options.skipMachineLookup ? null : await (async () => {
|
|
52
|
+
const rows = await fetchMachines(credentials);
|
|
53
|
+
const found = rows.find((candidate) => candidate.id === deviceId);
|
|
54
|
+
if (!found) {
|
|
55
|
+
throw new Error(`Device ${deviceId} not found`);
|
|
56
|
+
}
|
|
57
|
+
if (!found.active) {
|
|
58
|
+
throw new Error(`Device ${deviceId} is offline`);
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
})();
|
|
62
|
+
const resolved = options.deviceKeyBase64 ? { key: decodeBase64(options.deviceKeyBase64), variant: options.deviceKeyVariant ?? "dataKey" } : resolveMachineKey(credentials, row.dataEncryptionKey);
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
throw new Error(`Cannot resolve encryption key for device ${deviceId}. Re-select the device in the app.`);
|
|
65
|
+
}
|
|
66
|
+
const timeout = options.timeout ?? 6e4;
|
|
67
|
+
const params = encodeBase64(encrypt(resolved.key, resolved.variant, {
|
|
68
|
+
command,
|
|
69
|
+
cwd: options.cwd,
|
|
70
|
+
timeout
|
|
71
|
+
}));
|
|
72
|
+
const answer = await socket.timeout(timeout + 5e3).emitWithAck("rpc-call", {
|
|
73
|
+
method: `${deviceId}:bash`,
|
|
74
|
+
params
|
|
75
|
+
});
|
|
76
|
+
if (!answer?.ok) {
|
|
77
|
+
throw new Error(answer?.error || "Device RPC failed");
|
|
78
|
+
}
|
|
79
|
+
const decrypted = decrypt(resolved.key, resolved.variant, decodeBase64(answer.result));
|
|
80
|
+
if (!decrypted) {
|
|
81
|
+
throw new Error("Failed to decrypt device response");
|
|
82
|
+
}
|
|
83
|
+
return decrypted;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
87
|
+
const POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
88
|
+
function deviceKeysFile() {
|
|
89
|
+
return join(configuration.happyHomeDir, "device-keys.json");
|
|
90
|
+
}
|
|
91
|
+
async function readDeviceKeys() {
|
|
92
|
+
const file = deviceKeysFile();
|
|
93
|
+
if (!existsSync(file)) return {};
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
96
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function writeDeviceDirectory(entries) {
|
|
102
|
+
const file = deviceKeysFile();
|
|
103
|
+
await mkdir(dirname(file), { recursive: true });
|
|
104
|
+
const keys = {};
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
keys[entry.id] = { name: entry.name, key: entry.key, variant: entry.variant ?? "dataKey" };
|
|
107
|
+
}
|
|
108
|
+
await writeFile(file, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
109
|
+
}
|
|
110
|
+
async function pruneDeviceAliases(id, name) {
|
|
111
|
+
const keys = await readDeviceKeys();
|
|
112
|
+
const stale = Object.keys(keys).filter((candidate) => candidate !== id && keys[candidate].name === name);
|
|
113
|
+
if (stale.length === 0) return;
|
|
114
|
+
for (const candidate of stale) delete keys[candidate];
|
|
115
|
+
await writeFile(deviceKeysFile(), JSON.stringify(keys, null, 2), { mode: 384 });
|
|
116
|
+
}
|
|
117
|
+
async function readCachedDeviceKey(machineId) {
|
|
118
|
+
const keys = await readDeviceKeys();
|
|
119
|
+
const stored = keys[machineId];
|
|
120
|
+
if (!stored?.key) return null;
|
|
121
|
+
return { key: decodeBase64(stored.key), variant: stored.variant ?? "dataKey" };
|
|
122
|
+
}
|
|
123
|
+
async function readCachedDeviceNames() {
|
|
124
|
+
const keys = await readDeviceKeys();
|
|
125
|
+
return Object.fromEntries(Object.entries(keys).map(([id, entry]) => [id, entry.name]));
|
|
126
|
+
}
|
|
127
|
+
async function ensureDeviceKey(credentials, machineId, onPending) {
|
|
128
|
+
const cached = await readCachedDeviceKey(machineId);
|
|
129
|
+
if (cached) return cached;
|
|
130
|
+
const seed = tweetnacl.randomBytes(32);
|
|
131
|
+
const secretKey = tweetnacl.hash(seed).slice(0, 32);
|
|
132
|
+
const publicKey = tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey;
|
|
133
|
+
const created = await axios.post(
|
|
134
|
+
`${configuration.serverUrl}/v1/devices/key-requests`,
|
|
135
|
+
{ machineId, publicKey: encodeBase64(publicKey), label: `happy ssh from ${hostname()}` },
|
|
136
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
137
|
+
);
|
|
138
|
+
onPending?.({ id: created.data.id });
|
|
139
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
140
|
+
while (Date.now() < deadline) {
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
142
|
+
const status = await axios.get(
|
|
143
|
+
`${configuration.serverUrl}/v1/devices/key-requests/${created.data.id}`,
|
|
144
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
145
|
+
);
|
|
146
|
+
if (status.data.expired) {
|
|
147
|
+
throw new Error("Authorization request expired. Run the command again.");
|
|
148
|
+
}
|
|
149
|
+
if (status.data.approved && status.data.response) {
|
|
150
|
+
const opened = decryptWithEphemeralKey(decodeBase64(status.data.response), secretKey);
|
|
151
|
+
if (!opened) {
|
|
152
|
+
throw new Error("Could not open the device directory returned by the app");
|
|
153
|
+
}
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(Buffer.from(opened).toString("utf8"));
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error("Device directory from the app was malformed");
|
|
159
|
+
}
|
|
160
|
+
const devices = parsed.devices ?? [];
|
|
161
|
+
if (devices.length === 0) {
|
|
162
|
+
throw new Error("The app returned an empty device directory");
|
|
163
|
+
}
|
|
164
|
+
await writeDeviceDirectory(devices);
|
|
165
|
+
const target = devices.find((device) => device.id === machineId);
|
|
166
|
+
if (!target) {
|
|
167
|
+
throw new Error("The app did not include this device in the directory");
|
|
168
|
+
}
|
|
169
|
+
return { key: decodeBase64(target.key), variant: target.variant ?? "dataKey" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new Error("Timed out waiting for approval in the Happy app");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { readCachedDeviceKey as a, deviceExec as d, ensureDeviceKey as e, listDevices as l, pruneDeviceAliases as p, readCachedDeviceNames as r };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
5
|
+
var auth = require('./auth-C7ZScWEw.cjs');
|
|
6
|
+
var tweetnacl = require('tweetnacl');
|
|
7
|
+
var fs = require('node:fs');
|
|
8
|
+
var promises = require('node:fs/promises');
|
|
9
|
+
var path = require('node:path');
|
|
10
|
+
var os = require('node:os');
|
|
11
|
+
|
|
12
|
+
function resolveMachineKey(credentials, dataEncryptionKey) {
|
|
13
|
+
if (credentials.encryption.type === "legacy") {
|
|
14
|
+
return { key: credentials.encryption.secret, variant: "legacy" };
|
|
15
|
+
}
|
|
16
|
+
if (!dataEncryptionKey) return null;
|
|
17
|
+
const blob = logger.decodeBase64(dataEncryptionKey);
|
|
18
|
+
if (blob.length < 1 || blob[0] !== 0) return null;
|
|
19
|
+
const opened = auth.decryptWithEphemeralKey(blob.slice(1), credentials.encryption.machineKey);
|
|
20
|
+
return opened ? { key: opened, variant: "dataKey" } : null;
|
|
21
|
+
}
|
|
22
|
+
async function fetchMachines(credentials) {
|
|
23
|
+
const response = await axios.get(
|
|
24
|
+
`${logger.configuration.serverUrl}/v1/machines`,
|
|
25
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
26
|
+
);
|
|
27
|
+
const data = response.data;
|
|
28
|
+
return Array.isArray(data) ? data : data.machines ?? [];
|
|
29
|
+
}
|
|
30
|
+
async function listDevices(credentials) {
|
|
31
|
+
const rows = await fetchMachines(credentials);
|
|
32
|
+
return rows.map((row) => {
|
|
33
|
+
const resolved = resolveMachineKey(credentials, row.dataEncryptionKey);
|
|
34
|
+
let metadata = null;
|
|
35
|
+
if (resolved && row.metadata) {
|
|
36
|
+
try {
|
|
37
|
+
metadata = logger.decrypt(resolved.key, resolved.variant, logger.decodeBase64(row.metadata));
|
|
38
|
+
} catch {
|
|
39
|
+
metadata = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
id: row.id,
|
|
44
|
+
name: row.displayName || metadata?.displayName || metadata?.host || row.id.slice(0, 12),
|
|
45
|
+
description: row.description ?? null,
|
|
46
|
+
platform: metadata?.platform ?? null,
|
|
47
|
+
active: row.active,
|
|
48
|
+
lastActiveAt: row.activeAt ?? row.lastActiveAt ?? 0
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function deviceExec(socket, credentials, deviceId, command, options = {}) {
|
|
53
|
+
const row = options.skipMachineLookup ? null : await (async () => {
|
|
54
|
+
const rows = await fetchMachines(credentials);
|
|
55
|
+
const found = rows.find((candidate) => candidate.id === deviceId);
|
|
56
|
+
if (!found) {
|
|
57
|
+
throw new Error(`Device ${deviceId} not found`);
|
|
58
|
+
}
|
|
59
|
+
if (!found.active) {
|
|
60
|
+
throw new Error(`Device ${deviceId} is offline`);
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
})();
|
|
64
|
+
const resolved = options.deviceKeyBase64 ? { key: logger.decodeBase64(options.deviceKeyBase64), variant: options.deviceKeyVariant ?? "dataKey" } : resolveMachineKey(credentials, row.dataEncryptionKey);
|
|
65
|
+
if (!resolved) {
|
|
66
|
+
throw new Error(`Cannot resolve encryption key for device ${deviceId}. Re-select the device in the app.`);
|
|
67
|
+
}
|
|
68
|
+
const timeout = options.timeout ?? 6e4;
|
|
69
|
+
const params = logger.encodeBase64(logger.encrypt(resolved.key, resolved.variant, {
|
|
70
|
+
command,
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
timeout
|
|
73
|
+
}));
|
|
74
|
+
const answer = await socket.timeout(timeout + 5e3).emitWithAck("rpc-call", {
|
|
75
|
+
method: `${deviceId}:bash`,
|
|
76
|
+
params
|
|
77
|
+
});
|
|
78
|
+
if (!answer?.ok) {
|
|
79
|
+
throw new Error(answer?.error || "Device RPC failed");
|
|
80
|
+
}
|
|
81
|
+
const decrypted = logger.decrypt(resolved.key, resolved.variant, logger.decodeBase64(answer.result));
|
|
82
|
+
if (!decrypted) {
|
|
83
|
+
throw new Error("Failed to decrypt device response");
|
|
84
|
+
}
|
|
85
|
+
return decrypted;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
89
|
+
const POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
90
|
+
function deviceKeysFile() {
|
|
91
|
+
return path.join(logger.configuration.happyHomeDir, "device-keys.json");
|
|
92
|
+
}
|
|
93
|
+
async function readDeviceKeys() {
|
|
94
|
+
const file = deviceKeysFile();
|
|
95
|
+
if (!fs.existsSync(file)) return {};
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(await promises.readFile(file, "utf8"));
|
|
98
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
99
|
+
} catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function writeDeviceDirectory(entries) {
|
|
104
|
+
const file = deviceKeysFile();
|
|
105
|
+
await promises.mkdir(path.dirname(file), { recursive: true });
|
|
106
|
+
const keys = {};
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
keys[entry.id] = { name: entry.name, key: entry.key, variant: entry.variant ?? "dataKey" };
|
|
109
|
+
}
|
|
110
|
+
await promises.writeFile(file, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
111
|
+
}
|
|
112
|
+
async function pruneDeviceAliases(id, name) {
|
|
113
|
+
const keys = await readDeviceKeys();
|
|
114
|
+
const stale = Object.keys(keys).filter((candidate) => candidate !== id && keys[candidate].name === name);
|
|
115
|
+
if (stale.length === 0) return;
|
|
116
|
+
for (const candidate of stale) delete keys[candidate];
|
|
117
|
+
await promises.writeFile(deviceKeysFile(), JSON.stringify(keys, null, 2), { mode: 384 });
|
|
118
|
+
}
|
|
119
|
+
async function readCachedDeviceKey(machineId) {
|
|
120
|
+
const keys = await readDeviceKeys();
|
|
121
|
+
const stored = keys[machineId];
|
|
122
|
+
if (!stored?.key) return null;
|
|
123
|
+
return { key: logger.decodeBase64(stored.key), variant: stored.variant ?? "dataKey" };
|
|
124
|
+
}
|
|
125
|
+
async function readCachedDeviceNames() {
|
|
126
|
+
const keys = await readDeviceKeys();
|
|
127
|
+
return Object.fromEntries(Object.entries(keys).map(([id, entry]) => [id, entry.name]));
|
|
128
|
+
}
|
|
129
|
+
async function ensureDeviceKey(credentials, machineId, onPending) {
|
|
130
|
+
const cached = await readCachedDeviceKey(machineId);
|
|
131
|
+
if (cached) return cached;
|
|
132
|
+
const seed = tweetnacl.randomBytes(32);
|
|
133
|
+
const secretKey = tweetnacl.hash(seed).slice(0, 32);
|
|
134
|
+
const publicKey = tweetnacl.box.keyPair.fromSecretKey(secretKey).publicKey;
|
|
135
|
+
const created = await axios.post(
|
|
136
|
+
`${logger.configuration.serverUrl}/v1/devices/key-requests`,
|
|
137
|
+
{ machineId, publicKey: logger.encodeBase64(publicKey), label: `happy ssh from ${os.hostname()}` },
|
|
138
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
139
|
+
);
|
|
140
|
+
onPending?.({ id: created.data.id });
|
|
141
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
142
|
+
while (Date.now() < deadline) {
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
144
|
+
const status = await axios.get(
|
|
145
|
+
`${logger.configuration.serverUrl}/v1/devices/key-requests/${created.data.id}`,
|
|
146
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` }, timeout: 15e3 }
|
|
147
|
+
);
|
|
148
|
+
if (status.data.expired) {
|
|
149
|
+
throw new Error("Authorization request expired. Run the command again.");
|
|
150
|
+
}
|
|
151
|
+
if (status.data.approved && status.data.response) {
|
|
152
|
+
const opened = auth.decryptWithEphemeralKey(logger.decodeBase64(status.data.response), secretKey);
|
|
153
|
+
if (!opened) {
|
|
154
|
+
throw new Error("Could not open the device directory returned by the app");
|
|
155
|
+
}
|
|
156
|
+
let parsed;
|
|
157
|
+
try {
|
|
158
|
+
parsed = JSON.parse(Buffer.from(opened).toString("utf8"));
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error("Device directory from the app was malformed");
|
|
161
|
+
}
|
|
162
|
+
const devices = parsed.devices ?? [];
|
|
163
|
+
if (devices.length === 0) {
|
|
164
|
+
throw new Error("The app returned an empty device directory");
|
|
165
|
+
}
|
|
166
|
+
await writeDeviceDirectory(devices);
|
|
167
|
+
const target = devices.find((device) => device.id === machineId);
|
|
168
|
+
if (!target) {
|
|
169
|
+
throw new Error("The app did not include this device in the directory");
|
|
170
|
+
}
|
|
171
|
+
return { key: logger.decodeBase64(target.key), variant: target.variant ?? "dataKey" };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
throw new Error("Timed out waiting for approval in the Happy app");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
exports.deviceExec = deviceExec;
|
|
178
|
+
exports.ensureDeviceKey = ensureDeviceKey;
|
|
179
|
+
exports.listDevices = listDevices;
|
|
180
|
+
exports.pruneDeviceAliases = pruneDeviceAliases;
|
|
181
|
+
exports.readCachedDeviceKey = readCachedDeviceKey;
|
|
182
|
+
exports.readCachedDeviceNames = readCachedDeviceNames;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chalk = require('chalk');
|
|
4
|
-
var logger = require('./persistence-
|
|
5
|
-
var controlClient = require('./controlClient-
|
|
4
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
5
|
+
var controlClient = require('./controlClient-B23M-WPp.cjs');
|
|
6
6
|
var doctor = require('./doctor-C08m5Z9f.cjs');
|
|
7
7
|
var fs = require('node:fs');
|
|
8
8
|
var path = require('node:path');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { p as packageJson, c as configuration, a as readSettings, r as readCredentials, j as readDaemonState } from './persistence-
|
|
3
|
-
import { checkIfDaemonRunningAndCleanupStaleState } from './controlClient-
|
|
2
|
+
import { p as packageJson, c as configuration, a as readSettings, r as readCredentials, j as readDaemonState } from './persistence-DKfF81pd.mjs';
|
|
3
|
+
import { checkIfDaemonRunningAndCleanupStaleState } from './controlClient-DB5tDU7V.mjs';
|
|
4
4
|
import { findAllHappyProcesses } from './doctor-ON7pN5G5.mjs';
|
|
5
5
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as logger, r as readCredentials } from './persistence-
|
|
1
|
+
import { l as logger, r as readCredentials } from './persistence-DKfF81pd.mjs';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { join, resolve, dirname } from 'node:path';
|
|
4
4
|
import { p as projectPath } from './projectPath-BTBIMB57.mjs';
|
|
@@ -7,7 +7,7 @@ import { createServer } from 'node:http';
|
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
8
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
import { l as listDevices, d as deviceExec } from './
|
|
10
|
+
import { l as listDevices, d as deviceExec } from './deviceKeys-BTdo34Uz.mjs';
|
|
11
11
|
import { randomUUID } from 'node:crypto';
|
|
12
12
|
import { s as shouldEnableOrchestratorTools } from './prompt-BsAZsPbo.mjs';
|
|
13
13
|
import './mcpToolSchemas-CjeTfrAs.mjs';
|
|
@@ -16,7 +16,7 @@ import { execSync } from 'node:child_process';
|
|
|
16
16
|
import axios from 'axios';
|
|
17
17
|
import 'node:events';
|
|
18
18
|
import 'socket.io-client';
|
|
19
|
-
import './api-
|
|
19
|
+
import './api-D6WL40-U.mjs';
|
|
20
20
|
import 'tweetnacl';
|
|
21
21
|
import 'ws';
|
|
22
22
|
import '@stablelib/base64';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var logger = require('./persistence-
|
|
3
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
4
4
|
var crypto = require('crypto');
|
|
5
5
|
var path = require('node:path');
|
|
6
6
|
var projectPath = require('./projectPath-ChLhdOyt.cjs');
|
|
@@ -9,7 +9,7 @@ var node_http = require('node:http');
|
|
|
9
9
|
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
10
10
|
var types_js = require('@modelcontextprotocol/sdk/types.js');
|
|
11
11
|
var z = require('zod');
|
|
12
|
-
var
|
|
12
|
+
var deviceKeys = require('./deviceKeys-C540DcXl.cjs');
|
|
13
13
|
var node_crypto = require('node:crypto');
|
|
14
14
|
var prompt = require('./prompt-BTWDZZO0.cjs');
|
|
15
15
|
require('./mcpToolSchemas-DK42yoZ_.cjs');
|
|
@@ -18,7 +18,7 @@ var node_child_process = require('node:child_process');
|
|
|
18
18
|
var axios = require('axios');
|
|
19
19
|
require('node:events');
|
|
20
20
|
require('socket.io-client');
|
|
21
|
-
require('./api-
|
|
21
|
+
require('./api-C7FyJfZ1.cjs');
|
|
22
22
|
require('tweetnacl');
|
|
23
23
|
require('ws');
|
|
24
24
|
require('@stablelib/base64');
|
|
@@ -458,7 +458,7 @@ function createMcpServer(client, options) {
|
|
|
458
458
|
return { content: [{ type: "text", text: "No Happy credentials on this machine." }] };
|
|
459
459
|
}
|
|
460
460
|
try {
|
|
461
|
-
const devices = await
|
|
461
|
+
const devices = await deviceKeys.listDevices(credentials);
|
|
462
462
|
const target = client.targetDevices.map((device) => device.id);
|
|
463
463
|
const lines = devices.map((device) => {
|
|
464
464
|
const marks = [device.active ? "online" : "offline"];
|
|
@@ -493,7 +493,7 @@ function createMcpServer(client, options) {
|
|
|
493
493
|
return { content: [{ type: "text", text: "No device selected for this session. Pick one in the app or pass deviceId (see device_list)." }] };
|
|
494
494
|
}
|
|
495
495
|
try {
|
|
496
|
-
const result = await
|
|
496
|
+
const result = await deviceKeys.deviceExec(client.rpcSocket, credentials, deviceId, args.command, {
|
|
497
497
|
cwd: args.cwd,
|
|
498
498
|
timeout: args.timeout,
|
|
499
499
|
deviceKeyBase64: targets.find((device) => device.id === deviceId)?.key ?? null
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import tweetnacl from 'tweetnacl';
|
|
3
3
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
4
|
-
import { r as readCredentials, c as configuration,
|
|
5
|
-
import { decryptWithEphemeralKey } from './auth-
|
|
4
|
+
import { r as readCredentials, c as configuration, f as decodeBase64, u as updateSettings, w as writeCredentialsDataKey, o as libsodiumPublicKeyFromSecretKey } from './persistence-DKfF81pd.mjs';
|
|
5
|
+
import { decryptWithEphemeralKey } from './auth-DxJ3VvrX.mjs';
|
|
6
6
|
import 'node:fs/promises';
|
|
7
7
|
import 'node:fs';
|
|
8
8
|
import 'node:os';
|
|
@@ -12,7 +12,7 @@ import 'chalk';
|
|
|
12
12
|
import 'fs';
|
|
13
13
|
import 'qrcode-terminal';
|
|
14
14
|
import './time-DgsBLJA_.mjs';
|
|
15
|
-
import './browser-
|
|
15
|
+
import './browser-AoVGRX7y.mjs';
|
|
16
16
|
import 'open';
|
|
17
17
|
import 'react';
|
|
18
18
|
import 'ink';
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
var axios = require('axios');
|
|
4
4
|
var tweetnacl = require('tweetnacl');
|
|
5
5
|
var node_crypto = require('node:crypto');
|
|
6
|
-
var logger = require('./persistence-
|
|
7
|
-
var auth = require('./auth-
|
|
6
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
7
|
+
var auth = require('./auth-C7ZScWEw.cjs');
|
|
8
8
|
require('node:fs/promises');
|
|
9
9
|
require('node:fs');
|
|
10
10
|
require('node:os');
|
|
@@ -14,7 +14,7 @@ require('chalk');
|
|
|
14
14
|
require('fs');
|
|
15
15
|
require('qrcode-terminal');
|
|
16
16
|
require('./time-Bj2HRkKH.cjs');
|
|
17
|
-
require('./browser-
|
|
17
|
+
require('./browser-DzgO4oaD.cjs');
|
|
18
18
|
require('open');
|
|
19
19
|
require('react');
|
|
20
20
|
require('ink');
|
|
@@ -5,7 +5,7 @@ var fs = require('node:fs');
|
|
|
5
5
|
var promises = require('node:fs/promises');
|
|
6
6
|
var node_crypto = require('node:crypto');
|
|
7
7
|
var path = require('node:path');
|
|
8
|
-
var logger = require('./persistence-
|
|
8
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
9
9
|
var workflowReports = require('./workflowReports-BEWuQesA.cjs');
|
|
10
10
|
var workflowCommon = require('./workflowCommon-Bl568-rt.cjs');
|
|
11
11
|
require('node:os');
|
|
@@ -3,7 +3,7 @@ import { constants } from 'node:fs';
|
|
|
3
3
|
import { mkdir, appendFile, access, readFile } from 'node:fs/promises';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { basename, dirname, resolve } from 'node:path';
|
|
6
|
-
import { c as configuration } from './persistence-
|
|
6
|
+
import { c as configuration } from './persistence-DKfF81pd.mjs';
|
|
7
7
|
import { r as renderEvidenceReport, i as inferEvidenceKind } from './workflowReports-Dd8-4-8f.mjs';
|
|
8
8
|
import { p as parseWorkflowArgs, s as stringFlag, b as booleanFlag, a as printOrWriteOutput } from './workflowCommon-B0d01Y_a.mjs';
|
|
9
9
|
import 'node:os';
|