@oharness/server 0.9.2 → 0.10.0
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/i18n.js +356 -62
- package/dist/i18n.js.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/page/client/account.js +430 -23
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +20 -1
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/composer.js +709 -58
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/computer.d.ts +27 -0
- package/dist/page/client/computer.js +358 -0
- package/dist/page/client/computer.js.map +1 -0
- package/dist/page/client/dialogs.js +93 -6
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +94 -7
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/files.js +86 -1
- package/dist/page/client/files.js.map +1 -1
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +12 -6
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/routines.js +296 -41
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +229 -51
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +273 -19
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/tasks.d.ts +8 -0
- package/dist/page/client/tasks.js +293 -0
- package/dist/page/client/tasks.js.map +1 -0
- package/dist/page/client/transcript.js +174 -6
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/client/usage.js +1 -11
- package/dist/page/client/usage.js.map +1 -1
- package/dist/page/style.js +706 -104
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +161 -54
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +14 -0
- package/dist/prefs.js +8 -1
- package/dist/prefs.js.map +1 -1
- package/dist/remote-crypto.d.ts +78 -0
- package/dist/remote-crypto.js +216 -0
- package/dist/remote-crypto.js.map +1 -0
- package/dist/remote.d.ts +103 -0
- package/dist/remote.js +498 -0
- package/dist/remote.js.map +1 -0
- package/dist/server.d.ts +70 -0
- package/dist/server.js +1291 -49
- package/dist/server.js.map +1 -1
- package/dist/starters.d.ts +92 -0
- package/dist/starters.js +213 -0
- package/dist/starters.js.map +1 -0
- package/dist/theme.js +5 -0
- package/dist/theme.js.map +1 -1
- package/dist/turn-shots.d.ts +27 -0
- package/dist/turn-shots.js +71 -0
- package/dist/turn-shots.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHash, createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, sign, verify, } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* The cryptography of remote control, Mac side. The phone's half is
|
|
4
|
+
* HarnessKit's `RemoteCrypto.swift`, and the two must agree byte for byte.
|
|
5
|
+
*
|
|
6
|
+
* Every channel starts with a handshake that the relay forwards and cannot
|
|
7
|
+
* take part in:
|
|
8
|
+
*
|
|
9
|
+
* phone → mac {hs:"hello", v:1, device:{id,name}, identity, eph}
|
|
10
|
+
* mac → phone {hs:"welcome", identity, eph, sig} sig = Ed25519_mac(T)
|
|
11
|
+
* phone → mac {hs:"proof", sig} sig = Ed25519_phone(T)
|
|
12
|
+
*
|
|
13
|
+
* `identity` is the side's long-term Ed25519 public key and `eph` a fresh
|
|
14
|
+
* X25519 public key, each as 32 raw bytes in base64. The transcript is
|
|
15
|
+
*
|
|
16
|
+
* T = SHA-256("oharness-remote-v1" ‖ 0x00 ‖ channel ‖ 0x00 ‖
|
|
17
|
+
* phoneIdentity ‖ phoneEph ‖ macIdentity ‖ macEph)
|
|
18
|
+
*
|
|
19
|
+
* which binds both identities, both ephemeral keys and this channel. Each
|
|
20
|
+
* direction gets its own AES-256-GCM key:
|
|
21
|
+
*
|
|
22
|
+
* key = HKDF-SHA256(ikm = X25519(eph, eph'), salt = T, info = "oharness phone->mac" | "oharness mac->phone")
|
|
23
|
+
*
|
|
24
|
+
* and every sealed frame is `s.` + base64url(counter₈ ‖ ciphertext ‖ tag₁₆),
|
|
25
|
+
* nonce = 0x00000000 ‖ counter₈. Counters start at 0 and must arrive in
|
|
26
|
+
* order: the relay is one ordered socket, so anything else is a replay or a
|
|
27
|
+
* drop, and either way the channel is closed.
|
|
28
|
+
*
|
|
29
|
+
* The pairing code both screens show is the first four bytes of
|
|
30
|
+
* SHA-256("oharness-remote-sas" ‖ T), big-endian, mod 1 000 000, six digits.
|
|
31
|
+
* A relay that put itself in the middle would hold two different
|
|
32
|
+
* transcripts, so the two screens would show two different codes.
|
|
33
|
+
*/
|
|
34
|
+
const TRANSCRIPT_LABEL = "oharness-remote-v1";
|
|
35
|
+
const SAS_LABEL = "oharness-remote-sas";
|
|
36
|
+
const INFO_PHONE_TO_MAC = "oharness phone->mac";
|
|
37
|
+
const INFO_MAC_TO_PHONE = "oharness mac->phone";
|
|
38
|
+
export function b64(bytes) {
|
|
39
|
+
return Buffer.from(bytes).toString("base64");
|
|
40
|
+
}
|
|
41
|
+
export function unb64(text, length) {
|
|
42
|
+
if (typeof text !== "string")
|
|
43
|
+
throw new Error("expected base64");
|
|
44
|
+
const bytes = Buffer.from(text, "base64");
|
|
45
|
+
if (length !== undefined && bytes.length !== length)
|
|
46
|
+
throw new Error(`expected ${length} bytes`);
|
|
47
|
+
return bytes;
|
|
48
|
+
}
|
|
49
|
+
export function newIdentity() {
|
|
50
|
+
const { privateKey } = generateKeyPairSync("ed25519");
|
|
51
|
+
const pkcs8 = privateKey.export({ format: "der", type: "pkcs8" }).toString("base64");
|
|
52
|
+
return { identity: loadIdentity(pkcs8), pkcs8 };
|
|
53
|
+
}
|
|
54
|
+
export function loadIdentity(pkcs8) {
|
|
55
|
+
const privateKey = createPrivateKey({ key: Buffer.from(pkcs8, "base64"), format: "der", type: "pkcs8" });
|
|
56
|
+
return { privateKey, publicRaw: rawPublic(createPublicKey(privateKey)) };
|
|
57
|
+
}
|
|
58
|
+
function rawPublic(key) {
|
|
59
|
+
const jwk = key.export({ format: "jwk" });
|
|
60
|
+
if (typeof jwk.x !== "string")
|
|
61
|
+
throw new Error("not an OKP key");
|
|
62
|
+
return Buffer.from(jwk.x, "base64url");
|
|
63
|
+
}
|
|
64
|
+
function okpPublic(raw, crv) {
|
|
65
|
+
return createPublicKey({ key: { kty: "OKP", crv, x: raw.toString("base64url") }, format: "jwk" });
|
|
66
|
+
}
|
|
67
|
+
export function transcript(channel, phoneIdentity, phoneEph, macIdentity, macEph) {
|
|
68
|
+
return createHash("sha256")
|
|
69
|
+
.update(TRANSCRIPT_LABEL)
|
|
70
|
+
.update(Buffer.from([0]))
|
|
71
|
+
.update(channel)
|
|
72
|
+
.update(Buffer.from([0]))
|
|
73
|
+
.update(phoneIdentity)
|
|
74
|
+
.update(phoneEph)
|
|
75
|
+
.update(macIdentity)
|
|
76
|
+
.update(macEph)
|
|
77
|
+
.digest();
|
|
78
|
+
}
|
|
79
|
+
export function pairingCode(t) {
|
|
80
|
+
const digest = createHash("sha256").update(SAS_LABEL).update(t).digest();
|
|
81
|
+
return String(digest.readUInt32BE(0) % 1_000_000).padStart(6, "0");
|
|
82
|
+
}
|
|
83
|
+
/** One direction's key and counter. */
|
|
84
|
+
class Direction {
|
|
85
|
+
counter = 0n;
|
|
86
|
+
key;
|
|
87
|
+
constructor(key) {
|
|
88
|
+
this.key = key;
|
|
89
|
+
}
|
|
90
|
+
seal(plaintext) {
|
|
91
|
+
const counter = Buffer.alloc(8);
|
|
92
|
+
counter.writeBigUInt64BE(this.counter++);
|
|
93
|
+
const cipher = createCipheriv("aes-256-gcm", this.key, Buffer.concat([Buffer.alloc(4), counter]));
|
|
94
|
+
const body = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final(), cipher.getAuthTag()]);
|
|
95
|
+
return "s." + Buffer.concat([counter, body]).toString("base64url");
|
|
96
|
+
}
|
|
97
|
+
open(frame) {
|
|
98
|
+
if (!frame.startsWith("s."))
|
|
99
|
+
throw new Error("expected a sealed frame");
|
|
100
|
+
const bytes = Buffer.from(frame.slice(2), "base64url");
|
|
101
|
+
if (bytes.length < 8 + 16)
|
|
102
|
+
throw new Error("sealed frame too short");
|
|
103
|
+
const counter = bytes.readBigUInt64BE(0);
|
|
104
|
+
if (counter !== this.counter)
|
|
105
|
+
throw new Error("sealed frame out of order");
|
|
106
|
+
const decipher = createDecipheriv("aes-256-gcm", this.key, Buffer.concat([Buffer.alloc(4), bytes.subarray(0, 8)]));
|
|
107
|
+
decipher.setAuthTag(bytes.subarray(bytes.length - 16));
|
|
108
|
+
const plaintext = Buffer.concat([decipher.update(bytes.subarray(8, bytes.length - 16)), decipher.final()]).toString("utf8");
|
|
109
|
+
this.counter++;
|
|
110
|
+
return plaintext;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function readHello(message) {
|
|
114
|
+
if (message["hs"] !== "hello" || message["v"] !== 1)
|
|
115
|
+
throw new Error("expected a v1 hello");
|
|
116
|
+
const device = message["device"];
|
|
117
|
+
if (!device || typeof device.id !== "string" || !/^[A-Za-z0-9_-]{8,64}$/.test(device.id))
|
|
118
|
+
throw new Error("hello without a device id");
|
|
119
|
+
const name = typeof device.name === "string" && device.name.trim() ? device.name.trim().slice(0, 80) : device.id;
|
|
120
|
+
return { device: { id: device.id, name }, identity: unb64(message["identity"], 32), eph: unb64(message["eph"], 32) };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The Mac's side of one channel's handshake: answer the hello, then check the
|
|
124
|
+
* proof. `welcome()` is sent as plaintext; after `accept()` every frame is sealed.
|
|
125
|
+
*/
|
|
126
|
+
export class MacHandshake {
|
|
127
|
+
transcript;
|
|
128
|
+
code;
|
|
129
|
+
sendKey;
|
|
130
|
+
receiveKey;
|
|
131
|
+
welcomeFrame;
|
|
132
|
+
hello;
|
|
133
|
+
constructor(channel, hello, identity) {
|
|
134
|
+
this.hello = hello;
|
|
135
|
+
const { privateKey: ephPrivate, publicKey: ephPublic } = generateKeyPairSync("x25519");
|
|
136
|
+
const macEph = rawPublic(ephPublic);
|
|
137
|
+
this.transcript = transcript(channel, hello.identity, hello.eph, identity.publicRaw, macEph);
|
|
138
|
+
this.code = pairingCode(this.transcript);
|
|
139
|
+
const shared = diffieHellman({ privateKey: ephPrivate, publicKey: okpPublic(hello.eph, "X25519") });
|
|
140
|
+
this.receiveKey = Buffer.from(hkdfSync("sha256", shared, this.transcript, INFO_PHONE_TO_MAC, 32));
|
|
141
|
+
this.sendKey = Buffer.from(hkdfSync("sha256", shared, this.transcript, INFO_MAC_TO_PHONE, 32));
|
|
142
|
+
this.welcomeFrame = {
|
|
143
|
+
hs: "welcome",
|
|
144
|
+
identity: b64(identity.publicRaw),
|
|
145
|
+
eph: b64(macEph),
|
|
146
|
+
sig: b64(sign(null, this.transcript, identity.privateKey)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
welcome() {
|
|
150
|
+
return this.welcomeFrame;
|
|
151
|
+
}
|
|
152
|
+
/** Checks the phone's proof; on success, the sealed session for this channel. */
|
|
153
|
+
accept(message) {
|
|
154
|
+
if (message["hs"] !== "proof")
|
|
155
|
+
throw new Error("expected the proof");
|
|
156
|
+
const ok = verify(null, this.transcript, okpPublic(this.hello.identity, "Ed25519"), unb64(message["sig"], 64));
|
|
157
|
+
if (!ok)
|
|
158
|
+
throw new Error("the phone's proof does not verify");
|
|
159
|
+
return new SealedSession(new Direction(this.sendKey), new Direction(this.receiveKey));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export class SealedSession {
|
|
163
|
+
outgoing;
|
|
164
|
+
incoming;
|
|
165
|
+
constructor(outgoing, incoming) {
|
|
166
|
+
this.outgoing = outgoing;
|
|
167
|
+
this.incoming = incoming;
|
|
168
|
+
}
|
|
169
|
+
seal(message) {
|
|
170
|
+
return this.outgoing.seal(JSON.stringify(message));
|
|
171
|
+
}
|
|
172
|
+
open(frame) {
|
|
173
|
+
const value = JSON.parse(this.incoming.open(frame));
|
|
174
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
175
|
+
throw new Error("sealed frame is not an object");
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ---- The phone's half, in TypeScript: used by tests, and by anything that
|
|
180
|
+
// ---- wants to drive a Mac from Node. It mirrors RemoteCrypto.swift.
|
|
181
|
+
export class PhoneHandshake {
|
|
182
|
+
eph = generateKeyPairSync("x25519");
|
|
183
|
+
ephRaw = rawPublic(this.eph.publicKey);
|
|
184
|
+
channel;
|
|
185
|
+
device;
|
|
186
|
+
identity;
|
|
187
|
+
constructor(channel, device, identity) {
|
|
188
|
+
this.channel = channel;
|
|
189
|
+
this.device = device;
|
|
190
|
+
this.identity = identity;
|
|
191
|
+
}
|
|
192
|
+
hello() {
|
|
193
|
+
return { hs: "hello", v: 1, device: this.device, identity: b64(this.identity.publicRaw), eph: b64(this.ephRaw) };
|
|
194
|
+
}
|
|
195
|
+
/** Verifies the welcome and returns the proof to send, the code, and the sealed session. */
|
|
196
|
+
finish(welcome) {
|
|
197
|
+
if (welcome["hs"] !== "welcome")
|
|
198
|
+
throw new Error("expected the welcome");
|
|
199
|
+
const macIdentity = unb64(welcome["identity"], 32);
|
|
200
|
+
const macEph = unb64(welcome["eph"], 32);
|
|
201
|
+
const t = transcript(this.channel, this.identity.publicRaw, this.ephRaw, macIdentity, macEph);
|
|
202
|
+
if (!verify(null, t, okpPublic(macIdentity, "Ed25519"), unb64(welcome["sig"], 64))) {
|
|
203
|
+
throw new Error("the Mac's signature does not verify");
|
|
204
|
+
}
|
|
205
|
+
const shared = diffieHellman({ privateKey: this.eph.privateKey, publicKey: okpPublic(macEph, "X25519") });
|
|
206
|
+
const send = Buffer.from(hkdfSync("sha256", shared, t, INFO_PHONE_TO_MAC, 32));
|
|
207
|
+
const receive = Buffer.from(hkdfSync("sha256", shared, t, INFO_MAC_TO_PHONE, 32));
|
|
208
|
+
return {
|
|
209
|
+
proof: { hs: "proof", sig: b64(sign(null, t, this.identity.privateKey)) },
|
|
210
|
+
code: pairingCode(t),
|
|
211
|
+
macIdentity,
|
|
212
|
+
session: new SealedSession(new Direction(send), new Direction(receive)),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=remote-crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-crypto.js","sourceRoot":"","sources":["../src/remote-crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type Identity } from "./remote-crypto.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Remote control: this machine, reachable from a phone through the relay.
|
|
4
|
+
*
|
|
5
|
+
* The Mac dials out — it never listens on anything but loopback — and holds
|
|
6
|
+
* one WebSocket to the relay while remote is on. A phone on the same account
|
|
7
|
+
* opens a channel to it; each request that arrives on the channel is replayed
|
|
8
|
+
* against this process's own HTTP server, so the phone speaks exactly the
|
|
9
|
+
* routes the page speaks and there is no second API to keep in step.
|
|
10
|
+
*
|
|
11
|
+
* A channel is useless until its handshake (remote-crypto.ts) finishes: the
|
|
12
|
+
* phone proves which device it is, the Mac proves it is this Mac, and every
|
|
13
|
+
* frame after that is sealed end to end. A phone this Mac has not seen before
|
|
14
|
+
* is then held until someone at the Mac allows it — the same account is what
|
|
15
|
+
* gets a phone to the door, and the Allow is what opens it. Both screens show
|
|
16
|
+
* a six-digit code derived from the handshake, so a relay that put itself in
|
|
17
|
+
* the middle would be caught by two codes that disagree.
|
|
18
|
+
*
|
|
19
|
+
* Once a channel is ready, inside each sealed frame:
|
|
20
|
+
*
|
|
21
|
+
* ← {pair: "ok"} | {pair: "pending", code} | {pair: "denied"}
|
|
22
|
+
* → {id, method, path, query?, body?} ← {id, status, body}
|
|
23
|
+
* → {id, subscribe: "/events"} ← {sub: id, event} … ← {sub: id, end: true}
|
|
24
|
+
* → {id, unsubscribe: <sub id>} ← {id, status: 200, body: {ok: true}}
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* What a phone may ask for. Chat and its controls, read-only views of what the
|
|
28
|
+
* agent produced — not the terminal, the browser, the file system, settings,
|
|
29
|
+
* sign-in or computer use. A route not here is refused before anything runs.
|
|
30
|
+
*/
|
|
31
|
+
export declare const REMOTE_ROUTES: ReadonlySet<string>;
|
|
32
|
+
/** The relay a Mac dials when nothing names another. */
|
|
33
|
+
export declare const DEFAULT_RELAY = "wss://relay.oharness.ai/v1/connect";
|
|
34
|
+
/**
|
|
35
|
+
* Where remote control connects, and whether this run is told to use it
|
|
36
|
+
* whatever the setting says.
|
|
37
|
+
*
|
|
38
|
+
* OHARNESS_REMOTE_RELAY names another relay for development (a local one is
|
|
39
|
+
* `ws://127.0.0.1:4490/v1/connect`) and, as it did before there was a switch,
|
|
40
|
+
* turns remote on by itself. Without it the relay is ours and the switch in
|
|
41
|
+
* Settings decides.
|
|
42
|
+
*/
|
|
43
|
+
export declare function remoteRelay(env?: NodeJS.ProcessEnv): {
|
|
44
|
+
relay: string;
|
|
45
|
+
always: boolean;
|
|
46
|
+
};
|
|
47
|
+
export interface RemoteMachine {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
}
|
|
51
|
+
export interface RemoteOptions {
|
|
52
|
+
/** The relay's WebSocket endpoint, `wss://…/v1/connect` (or ws:// on loopback). */
|
|
53
|
+
relay: string;
|
|
54
|
+
/** The account token to present; asked again on every reconnect, since it expires. */
|
|
55
|
+
token: () => Promise<string | undefined>;
|
|
56
|
+
machine: RemoteMachine;
|
|
57
|
+
/** This Mac's long-term identity, from `remoteMachine()`. */
|
|
58
|
+
identity: Identity;
|
|
59
|
+
/** This process's own server, as `startWebServer` reports it (its URL carries the token). */
|
|
60
|
+
local: string;
|
|
61
|
+
/**
|
|
62
|
+
* Asks the person at the Mac whether a new phone may connect. Settles false
|
|
63
|
+
* when the signal aborts (the phone gave up), and is never asked about a
|
|
64
|
+
* phone that is already paired.
|
|
65
|
+
*/
|
|
66
|
+
approve: (request: PairingRequest, signal: AbortSignal) => Promise<boolean>;
|
|
67
|
+
/** Told when an approval is no longer wanted, so its card can close. */
|
|
68
|
+
withdrawn?: (id: string) => void;
|
|
69
|
+
log?: (line: string) => void;
|
|
70
|
+
}
|
|
71
|
+
export interface RemoteHandle {
|
|
72
|
+
readonly connected: boolean;
|
|
73
|
+
/** Ends every channel a phone holds open, once it has been forgotten. */
|
|
74
|
+
forget(deviceId: string): void;
|
|
75
|
+
close(): void;
|
|
76
|
+
}
|
|
77
|
+
/** A phone this Mac has allowed. */
|
|
78
|
+
export interface PairedDevice {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
/** The phone's Ed25519 identity, base64 of 32 raw bytes. */
|
|
82
|
+
publicKey: string;
|
|
83
|
+
pairedAt: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* This Mac's id and identity for remote control, made on first use and kept
|
|
87
|
+
* in ~/.oharness/remote.json. The identity is what a phone pins: replacing it
|
|
88
|
+
* makes every paired phone refuse this Mac until it is paired again.
|
|
89
|
+
*/
|
|
90
|
+
export declare function remoteMachine(): Promise<{
|
|
91
|
+
machine: RemoteMachine;
|
|
92
|
+
identity: Identity;
|
|
93
|
+
}>;
|
|
94
|
+
export declare function pairedDevices(): Promise<PairedDevice[]>;
|
|
95
|
+
export declare function revokeDevice(id: string): Promise<boolean>;
|
|
96
|
+
/** A phone waiting at the door, as the Mac's page is asked about it. */
|
|
97
|
+
export interface PairingRequest {
|
|
98
|
+
/** The channel id, which is also how the answer finds its way back. */
|
|
99
|
+
id: string;
|
|
100
|
+
device: RemoteMachine;
|
|
101
|
+
code: string;
|
|
102
|
+
}
|
|
103
|
+
export declare function startRemote(options: RemoteOptions): RemoteHandle;
|