@gtc6244/immediacy-daemon 0.2.2 → 0.2.4
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/src/crypto.d.ts +18 -39
- package/dist/src/crypto.js +33 -152
- package/dist/src/crypto.js.map +1 -1
- package/dist/src/devices.d.ts +46 -0
- package/dist/src/devices.js +114 -0
- package/dist/src/devices.js.map +1 -0
- package/dist/src/envelope.d.ts +3 -3
- package/dist/src/envelope.js +3 -3
- package/dist/src/forward.d.ts +4 -4
- package/dist/src/forward.js +1 -1
- package/dist/src/forward.js.map +1 -1
- package/dist/src/framing.d.ts +76 -0
- package/dist/src/framing.js +133 -0
- package/dist/src/framing.js.map +1 -0
- package/dist/src/hub.d.ts +21 -9
- package/dist/src/hub.js +6 -5
- package/dist/src/hub.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +79 -17
- package/dist/src/index.js.map +1 -1
- package/dist/src/lan.d.ts +103 -0
- package/dist/src/lan.js +182 -0
- package/dist/src/lan.js.map +1 -0
- package/dist/src/pairing.d.ts +69 -0
- package/dist/src/pairing.js +125 -0
- package/dist/src/pairing.js.map +1 -0
- package/dist/src/protocol.d.ts +75 -21
- package/dist/src/relay-transport.d.ts +62 -0
- package/dist/src/relay-transport.js +180 -0
- package/dist/src/relay-transport.js.map +1 -0
- package/dist/src/relay-wire.d.ts +25 -0
- package/dist/src/relay-wire.js +37 -0
- package/dist/src/relay-wire.js.map +1 -0
- package/dist/src/rendezvous.d.ts +57 -0
- package/dist/src/rendezvous.js +152 -0
- package/dist/src/rendezvous.js.map +1 -0
- package/dist/src/server.d.ts +19 -7
- package/dist/src/server.js +181 -61
- package/dist/src/server.js.map +1 -1
- package/dist/src/session-socket.d.ts +34 -0
- package/dist/src/session-socket.js +84 -0
- package/dist/src/session-socket.js.map +1 -0
- package/dist/src/session.d.ts +151 -0
- package/dist/src/session.js +241 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/tcp-transport.d.ts +37 -0
- package/dist/src/tcp-transport.js +106 -0
- package/dist/src/tcp-transport.js.map +1 -0
- package/dist/src/telemetry.d.ts +69 -0
- package/dist/src/telemetry.js +64 -0
- package/dist/src/telemetry.js.map +1 -0
- package/dist/src/transport.d.ts +75 -0
- package/dist/src/transport.js +136 -0
- package/dist/src/transport.js.map +1 -0
- package/dist/src/workspace.d.ts +4 -4
- package/dist/src/workspace.js.map +1 -1
- package/package.json +16 -4
package/dist/src/crypto.d.ts
CHANGED
|
@@ -1,49 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Wire codecs — the (de)serialization seam for the envelope transport.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* This is IN ADDITION to Cloudflare Access — Access does authn/authz at the
|
|
12
|
-
* edge; the box keeps payload contents private from the edge.
|
|
4
|
+
* Since the Cloudflare-tunnel cutover (docs/WEBRTC_MIGRATION.md Phase 5) the only
|
|
5
|
+
* E2E path is the §5.3 session handshake, whose `SecureChannel` seals each frame
|
|
6
|
+
* with AES-256-GCM. The legacy NaCl `crypto_box` layer (paired to a Cloudflare
|
|
7
|
+
* Access gate) has been removed. Two codecs remain:
|
|
8
|
+
* - clearCodec: cleartext JSON (local dev, and a socket before it seals).
|
|
9
|
+
* - sessionCodec: seals the envelope payload with a handshake SecureChannel.
|
|
13
10
|
*/
|
|
14
11
|
import type { Envelope } from "./protocol.js";
|
|
15
|
-
|
|
16
|
-
publicKey: Buffer;
|
|
17
|
-
secretKey: Buffer;
|
|
18
|
-
}
|
|
19
|
-
/** Load the daemon keypair, generating + persisting one on first run. */
|
|
20
|
-
export declare function loadOrCreateKeypair(): Promise<Keypair>;
|
|
21
|
-
/** One paired client. `pub` is base64 of its X25519 public key. */
|
|
22
|
-
export interface Peer {
|
|
23
|
-
pub: string;
|
|
24
|
-
addedAt: number;
|
|
25
|
-
label?: string;
|
|
26
|
-
}
|
|
27
|
-
/** True iff `b64` is canonical base64 of a full-length crypto_box public key. */
|
|
28
|
-
export declare function isValidPublicKeyB64(b64: string): boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Every paired client. Reads `peers.json`, migrating a legacy single-peer
|
|
31
|
-
* `peer.json` (and honoring the PEER_PUBLIC_KEY dev override) on first read so
|
|
32
|
-
* an existing pairing keeps working after the multi-device upgrade.
|
|
33
|
-
*/
|
|
34
|
-
export declare function loadPeers(): Promise<Peer[]>;
|
|
35
|
-
/**
|
|
36
|
-
* Add a client public key to the paired set (idempotent) and persist. Returns
|
|
37
|
-
* the updated list. Trust-on-first-use: the caller has already cleared the
|
|
38
|
-
* Cloudflare Access gate, which is the shared pairing secret.
|
|
39
|
-
*/
|
|
40
|
-
export declare function addPeer(pub: string, label?: string): Promise<Peer[]>;
|
|
12
|
+
import type { SecureChannel } from "./session.js";
|
|
41
13
|
/** Codec abstraction so the server/hub don't care whether crypto is on. */
|
|
42
14
|
export interface Codec {
|
|
43
15
|
encode(env: Envelope): string;
|
|
44
16
|
decode(raw: string): Envelope;
|
|
45
17
|
}
|
|
46
|
-
/** Cleartext codec (
|
|
18
|
+
/** Cleartext codec (local dev, and any socket before it completes a handshake). */
|
|
47
19
|
export declare const clearCodec: Codec;
|
|
48
|
-
/**
|
|
49
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Session codec (docs/WEBRTC_MIGRATION.md §5.3). Seals/opens the envelope
|
|
22
|
+
* `payload` with a `SecureChannel` established by the signed handshake, on a
|
|
23
|
+
* single ordered socket. On the wire the payload is `{ s: base64(cipher) }`;
|
|
24
|
+
* the nonce is the channel's per-direction counter (not transmitted), so this
|
|
25
|
+
* REQUIRES ordered, non-lossy delivery — true for the WS/loopback socket it runs
|
|
26
|
+
* on.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sessionCodec(channel: SecureChannel): Codec;
|
package/dist/src/crypto.js
CHANGED
|
@@ -1,169 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Wire codecs — the (de)serialization seam for the envelope transport.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* This is IN ADDITION to Cloudflare Access — Access does authn/authz at the
|
|
12
|
-
* edge; the box keeps payload contents private from the edge.
|
|
4
|
+
* Since the Cloudflare-tunnel cutover (docs/WEBRTC_MIGRATION.md Phase 5) the only
|
|
5
|
+
* E2E path is the §5.3 session handshake, whose `SecureChannel` seals each frame
|
|
6
|
+
* with AES-256-GCM. The legacy NaCl `crypto_box` layer (paired to a Cloudflare
|
|
7
|
+
* Access gate) has been removed. Two codecs remain:
|
|
8
|
+
* - clearCodec: cleartext JSON (local dev, and a socket before it seals).
|
|
9
|
+
* - sessionCodec: seals the envelope payload with a handshake SecureChannel.
|
|
13
10
|
*/
|
|
14
|
-
import sodium from "sodium-native";
|
|
15
|
-
import { existsSync } from "node:fs";
|
|
16
|
-
import { mkdir, readFile, writeFile, chmod } from "node:fs/promises";
|
|
17
|
-
import { homedir } from "node:os";
|
|
18
|
-
import { dirname, join } from "node:path";
|
|
19
11
|
import { ProtocolError } from "./envelope.js";
|
|
20
|
-
|
|
21
|
-
const PEER_FILE = process.env.PEER_FILE ?? join(homedir(), ".immediacy", "peer.json");
|
|
22
|
-
// Multi-device pairing: the set of every client public key we've paired with,
|
|
23
|
-
// so encryption isn't pinned to one device (the old single PEER_FILE). Each
|
|
24
|
-
// entry is bound to its own socket at connect time (per-socket sealing).
|
|
25
|
-
const PEERS_FILE = process.env.PEERS_FILE ?? join(homedir(), ".immediacy", "peers.json");
|
|
26
|
-
/** Load the daemon keypair, generating + persisting one on first run. */
|
|
27
|
-
export async function loadOrCreateKeypair() {
|
|
28
|
-
if (existsSync(KEYS_FILE)) {
|
|
29
|
-
const j = JSON.parse(await readFile(KEYS_FILE, "utf8"));
|
|
30
|
-
return {
|
|
31
|
-
publicKey: Buffer.from(j.publicKey, "base64"),
|
|
32
|
-
secretKey: Buffer.from(j.secretKey, "base64"),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const publicKey = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES);
|
|
36
|
-
const secretKey = Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES);
|
|
37
|
-
sodium.crypto_box_keypair(publicKey, secretKey);
|
|
38
|
-
await mkdir(dirname(KEYS_FILE), { recursive: true });
|
|
39
|
-
await writeFile(KEYS_FILE, JSON.stringify({
|
|
40
|
-
publicKey: publicKey.toString("base64"),
|
|
41
|
-
secretKey: secretKey.toString("base64"),
|
|
42
|
-
}), "utf8");
|
|
43
|
-
await chmod(KEYS_FILE, 0o600); // secret key — never world-readable
|
|
44
|
-
return { publicKey, secretKey };
|
|
45
|
-
}
|
|
46
|
-
/** True iff `b64` is canonical base64 of a full-length crypto_box public key. */
|
|
47
|
-
export function isValidPublicKeyB64(b64) {
|
|
48
|
-
// Node's Buffer.from(x, "base64") never throws and silently drops invalid
|
|
49
|
-
// characters, so a decoded-length check alone lets malformed input through.
|
|
50
|
-
// Require a strict base64 alphabet up front, then confirm the value
|
|
51
|
-
// round-trips to the exact same canonical string at the expected length.
|
|
52
|
-
if (typeof b64 !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(b64))
|
|
53
|
-
return false;
|
|
54
|
-
const buf = Buffer.from(b64, "base64");
|
|
55
|
-
return (buf.length === sodium.crypto_box_PUBLICKEYBYTES && buf.toString("base64") === b64);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Every paired client. Reads `peers.json`, migrating a legacy single-peer
|
|
59
|
-
* `peer.json` (and honoring the PEER_PUBLIC_KEY dev override) on first read so
|
|
60
|
-
* an existing pairing keeps working after the multi-device upgrade.
|
|
61
|
-
*/
|
|
62
|
-
export async function loadPeers() {
|
|
63
|
-
const peers = [];
|
|
64
|
-
const seen = new Set();
|
|
65
|
-
const push = (pub, label) => {
|
|
66
|
-
if (!pub || seen.has(pub) || !isValidPublicKeyB64(pub))
|
|
67
|
-
return;
|
|
68
|
-
seen.add(pub);
|
|
69
|
-
peers.push({ pub, addedAt: Date.now(), label });
|
|
70
|
-
};
|
|
71
|
-
if (existsSync(PEERS_FILE)) {
|
|
72
|
-
try {
|
|
73
|
-
const j = JSON.parse(await readFile(PEERS_FILE, "utf8"));
|
|
74
|
-
for (const p of j.peers ?? []) {
|
|
75
|
-
// Validate on the way in too: a corrupted or hand-edited peers.json must
|
|
76
|
-
// not seed invalid keys into the paired set (matches the push() helper).
|
|
77
|
-
if (p && typeof p.pub === "string" && !seen.has(p.pub) && isValidPublicKeyB64(p.pub)) {
|
|
78
|
-
seen.add(p.pub);
|
|
79
|
-
peers.push({ pub: p.pub, addedAt: p.addedAt ?? Date.now(), label: p.label });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
/* corrupt file — fall through to migration/env seeds */
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else if (existsSync(PEER_FILE)) {
|
|
88
|
-
// Migrate the legacy single-peer file.
|
|
89
|
-
try {
|
|
90
|
-
const j = JSON.parse(await readFile(PEER_FILE, "utf8"));
|
|
91
|
-
if (j.publicKey)
|
|
92
|
-
push(j.publicKey, "migrated");
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
/* ignore */
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (process.env.PEER_PUBLIC_KEY)
|
|
99
|
-
push(process.env.PEER_PUBLIC_KEY, "env");
|
|
100
|
-
return peers;
|
|
101
|
-
}
|
|
102
|
-
// Serializes the peers.json read-modify-write so two devices pairing at the
|
|
103
|
-
// same time can't each read the old file and clobber the other's entry.
|
|
104
|
-
let addChain = Promise.resolve([]);
|
|
105
|
-
/**
|
|
106
|
-
* Add a client public key to the paired set (idempotent) and persist. Returns
|
|
107
|
-
* the updated list. Trust-on-first-use: the caller has already cleared the
|
|
108
|
-
* Cloudflare Access gate, which is the shared pairing secret.
|
|
109
|
-
*/
|
|
110
|
-
export function addPeer(pub, label) {
|
|
111
|
-
addChain = addChain.then(async () => {
|
|
112
|
-
const peers = await loadPeers();
|
|
113
|
-
if (!peers.some((p) => p.pub === pub)) {
|
|
114
|
-
peers.push({ pub, addedAt: Date.now(), label });
|
|
115
|
-
await mkdir(dirname(PEERS_FILE), { recursive: true });
|
|
116
|
-
await writeFile(PEERS_FILE, JSON.stringify({ peers }, null, 2), "utf8");
|
|
117
|
-
await chmod(PEERS_FILE, 0o600);
|
|
118
|
-
}
|
|
119
|
-
return peers;
|
|
120
|
-
});
|
|
121
|
-
return addChain;
|
|
122
|
-
}
|
|
123
|
-
/** Upper bound for one sealed payload — matches the largest legitimate frame
|
|
124
|
-
* (file contents are truncated daemon-side well below this). */
|
|
12
|
+
/** Upper bound for one sealed payload — matches the largest legitimate frame. */
|
|
125
13
|
const MAX_SEALED_BYTES = 16 * 1024 * 1024;
|
|
126
|
-
/** Cleartext codec (
|
|
14
|
+
/** Cleartext codec (local dev, and any socket before it completes a handshake). */
|
|
127
15
|
export const clearCodec = {
|
|
128
16
|
encode: (env) => JSON.stringify(env),
|
|
129
17
|
decode: (raw) => parseEnvelope(raw),
|
|
130
18
|
};
|
|
131
|
-
/**
|
|
132
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Session codec (docs/WEBRTC_MIGRATION.md §5.3). Seals/opens the envelope
|
|
21
|
+
* `payload` with a `SecureChannel` established by the signed handshake, on a
|
|
22
|
+
* single ordered socket. On the wire the payload is `{ s: base64(cipher) }`;
|
|
23
|
+
* the nonce is the channel's per-direction counter (not transmitted), so this
|
|
24
|
+
* REQUIRES ordered, non-lossy delivery — true for the WS/loopback socket it runs
|
|
25
|
+
* on.
|
|
26
|
+
*/
|
|
27
|
+
export function sessionCodec(channel) {
|
|
133
28
|
return {
|
|
134
29
|
encode(env) {
|
|
135
30
|
const plaintext = Buffer.from(JSON.stringify(env.payload), "utf8");
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const cipher = Buffer.alloc(plaintext.length + sodium.crypto_box_MACBYTES);
|
|
139
|
-
sodium.crypto_box_easy(cipher, plaintext, nonce, peerPublicKey, self.secretKey);
|
|
140
|
-
const sealed = {
|
|
141
|
-
...env,
|
|
142
|
-
payload: { n: nonce.toString("base64"), c: cipher.toString("base64") },
|
|
143
|
-
};
|
|
144
|
-
return JSON.stringify(sealed);
|
|
31
|
+
const cipher = channel.seal(plaintext);
|
|
32
|
+
return JSON.stringify({ ...env, payload: { s: cipher.toString("base64") } });
|
|
145
33
|
},
|
|
146
34
|
decode(raw) {
|
|
147
35
|
const env = parseEnvelope(raw, /*allowSealed*/ true);
|
|
148
36
|
const sealed = env.payload;
|
|
149
|
-
if (typeof sealed.
|
|
150
|
-
throw new ProtocolError("not_sealed", "payload is not a sealed
|
|
151
|
-
}
|
|
152
|
-
const nonce = Buffer.from(sealed.n, "base64");
|
|
153
|
-
const cipher = Buffer.from(sealed.c, "base64");
|
|
154
|
-
// Validate sizes before allocating: a short ciphertext would make the
|
|
155
|
-
// alloc below negative (RangeError), and an oversized frame would
|
|
156
|
-
// allocate unbounded memory — both should fail as protocol errors.
|
|
157
|
-
if (nonce.length !== sodium.crypto_box_NONCEBYTES) {
|
|
158
|
-
throw new ProtocolError("bad_sealed_box", "nonce has the wrong length");
|
|
37
|
+
if (typeof sealed.s !== "string") {
|
|
38
|
+
throw new ProtocolError("not_sealed", "payload is not a sealed session frame");
|
|
159
39
|
}
|
|
160
|
-
|
|
40
|
+
const cipher = Buffer.from(sealed.s, "base64");
|
|
41
|
+
if (cipher.length > MAX_SEALED_BYTES) {
|
|
161
42
|
throw new ProtocolError("bad_sealed_box", "ciphertext length out of range");
|
|
162
43
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
44
|
+
let plaintext;
|
|
45
|
+
try {
|
|
46
|
+
plaintext = channel.open(cipher);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new ProtocolError("decrypt_failed", "session frame authentication failed");
|
|
50
|
+
}
|
|
167
51
|
env.payload = JSON.parse(plaintext.toString("utf8"));
|
|
168
52
|
return env;
|
|
169
53
|
},
|
|
@@ -183,11 +67,8 @@ function parseEnvelope(raw, allowSealed = false) {
|
|
|
183
67
|
}
|
|
184
68
|
const e = parsed;
|
|
185
69
|
// `v` is informational (routing + skew detection), NOT a hard compatibility
|
|
186
|
-
// gate.
|
|
187
|
-
//
|
|
188
|
-
// for routing (see this module's header). Additive-only bumps like v1→v2 stay
|
|
189
|
-
// wire-compatible, so decoding must tolerate any positive integer version. A
|
|
190
|
-
// missing, non-numeric, or non-positive-integer `v` is a malformed frame.
|
|
70
|
+
// gate. A missing, non-numeric, or non-positive-integer `v` is a malformed
|
|
71
|
+
// frame; any positive integer decodes (additive-only bumps stay compatible).
|
|
191
72
|
if (!Number.isSafeInteger(e.v) || e.v <= 0) {
|
|
192
73
|
throw new ProtocolError("bad_version", `missing/invalid version ${String(e.v)}`);
|
|
193
74
|
}
|
package/dist/src/crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeviceTable — the daemon's set of paired devices (§3.2).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the legacy `peers.json` (X25519 crypto_box TOFU record). Persisted at
|
|
5
|
+
* `~/.immediacy/devices.json` (mode 0600). Each entry pins a device's *Ed25519*
|
|
6
|
+
* public key — the long-term identity the §5.3 session handshake verifies
|
|
7
|
+
* against. Membership in this table IS authorization: a device whose key is not
|
|
8
|
+
* present (never paired, or revoked) must fail its next handshake.
|
|
9
|
+
*
|
|
10
|
+
* Writes are serialized and land via atomic rename (mirrors
|
|
11
|
+
* `RendezvousIdentity.persist` / `DaemonIdentity.persist`).
|
|
12
|
+
*/
|
|
13
|
+
/** One paired device. `publicKey` is base64 of its Ed25519 public key. */
|
|
14
|
+
export interface PairedDevice {
|
|
15
|
+
publicKey: string;
|
|
16
|
+
label: string;
|
|
17
|
+
pairedAt: string;
|
|
18
|
+
lastSeenAt: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class DeviceTable {
|
|
21
|
+
private devices;
|
|
22
|
+
/** Serializes read-modify-write so concurrent pairings can't clobber. */
|
|
23
|
+
private writeChain;
|
|
24
|
+
/** Load persisted devices, dropping any entry with a non-canonical key. */
|
|
25
|
+
load(): Promise<void>;
|
|
26
|
+
/** Every paired device, newest-paired last (insertion order). */
|
|
27
|
+
list(): PairedDevice[];
|
|
28
|
+
/** True iff `publicKey` is a currently-paired (non-revoked) device. */
|
|
29
|
+
has(publicKey: string): boolean;
|
|
30
|
+
/** The paired device for `publicKey`, or undefined. */
|
|
31
|
+
get(publicKey: string): PairedDevice | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Pair a device (idempotent). A fresh key is appended; an existing key keeps
|
|
34
|
+
* its `pairedAt` and only updates the label. Rejects non-canonical Ed25519
|
|
35
|
+
* keys. Returns the stored entry.
|
|
36
|
+
*/
|
|
37
|
+
add(publicKey: string, label: string): Promise<PairedDevice>;
|
|
38
|
+
/**
|
|
39
|
+
* Revoke a device. Returns true if it was present. The revoked key is gone
|
|
40
|
+
* from the authorized set, so its next handshake fails (Phase-1 acceptance).
|
|
41
|
+
*/
|
|
42
|
+
revoke(publicKey: string): Promise<boolean>;
|
|
43
|
+
/** Update a device's `lastSeenAt` (no-op for unknown keys). */
|
|
44
|
+
touch(publicKey: string): Promise<void>;
|
|
45
|
+
private persist;
|
|
46
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeviceTable — the daemon's set of paired devices (§3.2).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the legacy `peers.json` (X25519 crypto_box TOFU record). Persisted at
|
|
5
|
+
* `~/.immediacy/devices.json` (mode 0600). Each entry pins a device's *Ed25519*
|
|
6
|
+
* public key — the long-term identity the §5.3 session handshake verifies
|
|
7
|
+
* against. Membership in this table IS authorization: a device whose key is not
|
|
8
|
+
* present (never paired, or revoked) must fail its next handshake.
|
|
9
|
+
*
|
|
10
|
+
* Writes are serialized and land via atomic rename (mirrors
|
|
11
|
+
* `RendezvousIdentity.persist` / `DaemonIdentity.persist`).
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { mkdir, readFile, rename, writeFile, chmod } from "node:fs/promises";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { isValidSignPublicKeyB64 } from "./rendezvous.js";
|
|
18
|
+
const DEVICES_FILE = process.env.DEVICES_FILE ?? join(homedir(), ".immediacy", "devices.json");
|
|
19
|
+
/** A label a malicious/confused client can't blow up to a multi-KB blob. */
|
|
20
|
+
const MAX_LABEL_LEN = 200;
|
|
21
|
+
export class DeviceTable {
|
|
22
|
+
devices = new Map();
|
|
23
|
+
/** Serializes read-modify-write so concurrent pairings can't clobber. */
|
|
24
|
+
writeChain = Promise.resolve();
|
|
25
|
+
/** Load persisted devices, dropping any entry with a non-canonical key. */
|
|
26
|
+
async load() {
|
|
27
|
+
this.devices.clear();
|
|
28
|
+
if (!existsSync(DEVICES_FILE))
|
|
29
|
+
return;
|
|
30
|
+
let stored = {};
|
|
31
|
+
try {
|
|
32
|
+
stored = JSON.parse(await readFile(DEVICES_FILE, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return; // corrupt file — start empty rather than crash
|
|
36
|
+
}
|
|
37
|
+
for (const d of stored.devices ?? []) {
|
|
38
|
+
// Validate on the way in: a hand-edited/corrupt file must not seed an
|
|
39
|
+
// invalid key into the authorized set.
|
|
40
|
+
if (!d || !isValidSignPublicKeyB64(d.publicKey))
|
|
41
|
+
continue;
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
this.devices.set(d.publicKey, {
|
|
44
|
+
publicKey: d.publicKey,
|
|
45
|
+
label: typeof d.label === "string" ? d.label.slice(0, MAX_LABEL_LEN) : "",
|
|
46
|
+
pairedAt: typeof d.pairedAt === "string" ? d.pairedAt : now,
|
|
47
|
+
lastSeenAt: typeof d.lastSeenAt === "string" ? d.lastSeenAt : now,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Every paired device, newest-paired last (insertion order). */
|
|
52
|
+
list() {
|
|
53
|
+
return [...this.devices.values()];
|
|
54
|
+
}
|
|
55
|
+
/** True iff `publicKey` is a currently-paired (non-revoked) device. */
|
|
56
|
+
has(publicKey) {
|
|
57
|
+
return this.devices.has(publicKey);
|
|
58
|
+
}
|
|
59
|
+
/** The paired device for `publicKey`, or undefined. */
|
|
60
|
+
get(publicKey) {
|
|
61
|
+
return this.devices.get(publicKey);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Pair a device (idempotent). A fresh key is appended; an existing key keeps
|
|
65
|
+
* its `pairedAt` and only updates the label. Rejects non-canonical Ed25519
|
|
66
|
+
* keys. Returns the stored entry.
|
|
67
|
+
*/
|
|
68
|
+
async add(publicKey, label) {
|
|
69
|
+
if (!isValidSignPublicKeyB64(publicKey)) {
|
|
70
|
+
throw new Error("device public key is not a canonical Ed25519 key");
|
|
71
|
+
}
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
const cleanLabel = (label ?? "").slice(0, MAX_LABEL_LEN);
|
|
74
|
+
const existing = this.devices.get(publicKey);
|
|
75
|
+
const entry = existing
|
|
76
|
+
? { ...existing, label: cleanLabel, lastSeenAt: now }
|
|
77
|
+
: { publicKey, label: cleanLabel, pairedAt: now, lastSeenAt: now };
|
|
78
|
+
this.devices.set(publicKey, entry);
|
|
79
|
+
await this.persist();
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Revoke a device. Returns true if it was present. The revoked key is gone
|
|
84
|
+
* from the authorized set, so its next handshake fails (Phase-1 acceptance).
|
|
85
|
+
*/
|
|
86
|
+
async revoke(publicKey) {
|
|
87
|
+
const had = this.devices.delete(publicKey);
|
|
88
|
+
if (had)
|
|
89
|
+
await this.persist();
|
|
90
|
+
return had;
|
|
91
|
+
}
|
|
92
|
+
/** Update a device's `lastSeenAt` (no-op for unknown keys). */
|
|
93
|
+
async touch(publicKey) {
|
|
94
|
+
const entry = this.devices.get(publicKey);
|
|
95
|
+
if (!entry)
|
|
96
|
+
return;
|
|
97
|
+
entry.lastSeenAt = new Date().toISOString();
|
|
98
|
+
await this.persist();
|
|
99
|
+
}
|
|
100
|
+
persist() {
|
|
101
|
+
const snapshot = JSON.stringify({ devices: [...this.devices.values()] }, null, 2);
|
|
102
|
+
this.writeChain = this.writeChain
|
|
103
|
+
.catch(() => { })
|
|
104
|
+
.then(async () => {
|
|
105
|
+
await mkdir(dirname(DEVICES_FILE), { recursive: true });
|
|
106
|
+
const tmp = `${DEVICES_FILE}.${process.pid}.tmp`;
|
|
107
|
+
await writeFile(tmp, snapshot, "utf8");
|
|
108
|
+
await chmod(tmp, 0o600); // pins authorization — keep private
|
|
109
|
+
await rename(tmp, DEVICES_FILE);
|
|
110
|
+
});
|
|
111
|
+
return this.writeChain;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=devices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/devices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,YAAY,GAChB,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;AAE5E,4EAA4E;AAC5E,MAAM,aAAa,GAAG,GAAG,CAAC;AAc1B,MAAM,OAAO,WAAW;IACd,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAClD,yEAAyE;IACjE,UAAU,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAEtD,2EAA2E;IAC3E,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;YAAE,OAAO;QACtC,IAAI,MAAM,GAAoB,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAoB,CAAC;QAC/E,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,+CAA+C;QACzD,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;YACrC,sEAAsE;YACtE,uCAAuC;YACvC,IAAI,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC,SAAS,CAAC;gBAAE,SAAS;YAC1D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE;gBAC5B,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE;gBACzE,QAAQ,EAAE,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG;gBAC3D,UAAU,EAAE,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG;aAClE,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,iEAAiE;IACjE,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,uEAAuE;IACvE,GAAG,CAAC,SAAiB;QACnB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACrC,CAAC;IAED,uDAAuD;IACvD,GAAG,CAAC,SAAiB;QACnB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAG,CAAC,SAAiB,EAAE,KAAa;QACxC,IAAI,CAAC,uBAAuB,CAAC,SAAS,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QACtE,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,UAAU,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;QACzD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAiB,QAAQ;YAClC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE;YACrD,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;QACrE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACnC,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,SAAiB;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,GAAG;YAAE,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAC9B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,+DAA+D;IAC/D,KAAK,CAAC,KAAK,CAAC,SAAiB;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,KAAK,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC5C,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;IAEO,OAAO;QACb,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAC7B,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,EAAmB,EACxD,IAAI,EACJ,CAAC,CACF,CAAC;QACF,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU;aAC9B,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;aACf,IAAI,CAAC,KAAK,IAAI,EAAE;YACf,MAAM,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACxD,MAAM,GAAG,GAAG,GAAG,YAAY,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;YACjD,MAAM,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACvC,MAAM,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,oCAAoC;YAC7D,MAAM,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;QACL,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;CACF"}
|
package/dist/src/envelope.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Envelope construction + the shared ProtocolError.
|
|
3
3
|
*
|
|
4
|
-
* The (de)serialization seam lives in `crypto.ts` (`clearCodec` / `
|
|
5
|
-
* `parseEnvelope`), because whether `payload` is cleartext or
|
|
6
|
-
* is a per-socket codec decision. This
|
|
4
|
+
* The (de)serialization seam lives in `crypto.ts` (`clearCodec` / `sessionCodec`
|
|
5
|
+
* → `parseEnvelope`), because whether `payload` is cleartext or sealed by the
|
|
6
|
+
* session handshake is a per-socket codec decision. This only stamps envelopes.
|
|
7
7
|
*/
|
|
8
8
|
import { Envelope, MessageType, PayloadFor } from "./protocol.js";
|
|
9
9
|
/** Build a fully-stamped envelope ready to send. */
|
package/dist/src/envelope.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Envelope construction + the shared ProtocolError.
|
|
3
3
|
*
|
|
4
|
-
* The (de)serialization seam lives in `crypto.ts` (`clearCodec` / `
|
|
5
|
-
* `parseEnvelope`), because whether `payload` is cleartext or
|
|
6
|
-
* is a per-socket codec decision. This
|
|
4
|
+
* The (de)serialization seam lives in `crypto.ts` (`clearCodec` / `sessionCodec`
|
|
5
|
+
* → `parseEnvelope`), because whether `payload` is cleartext or sealed by the
|
|
6
|
+
* session handshake is a per-socket codec decision. This only stamps envelopes.
|
|
7
7
|
*/
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { PROTOCOL_VERSION, } from "./protocol.js";
|
package/dist/src/forward.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* accepts becomes a `forward.open` here. We dial `127.0.0.1:<port>` on the
|
|
6
6
|
* daemon box and pipe bytes in both directions as base64 `forward.data` frames,
|
|
7
7
|
* multiplexed over the one WSS connection by `streamId`. This is a raw TCP
|
|
8
|
-
* bridge (not HTTP-aware), so HTTP,
|
|
8
|
+
* bridge (not HTTP-aware), so HTTP, SocketLike HMR, and SSE all pass through
|
|
9
9
|
* unchanged — the dev server sees an ordinary local client.
|
|
10
10
|
*
|
|
11
11
|
* Streams are per-socket, like the terminal PTY: the reply bytes go back to the
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* only ports the `isOwned` check confirms belong to the named workspace's
|
|
17
17
|
* worktree (see `ports.ts`). An `open` for any other port is refused immediately.
|
|
18
18
|
*/
|
|
19
|
-
import type {
|
|
19
|
+
import type { SocketLike } from "./hub.js";
|
|
20
20
|
import type { Hub } from "./hub.js";
|
|
21
21
|
export declare class ForwardManager {
|
|
22
22
|
private hub;
|
|
@@ -31,7 +31,7 @@ export declare class ForwardManager {
|
|
|
31
31
|
* port against the worktree-owned set, dials loopback, and wires both
|
|
32
32
|
* directions. Any failure closes the stream back to the client with a reason.
|
|
33
33
|
*/
|
|
34
|
-
open(client:
|
|
34
|
+
open(client: SocketLike, ws: string, streamId: string, port: number): void;
|
|
35
35
|
/** Pause a stream's source read until its client-bound WS queue drains. */
|
|
36
36
|
private pauseUntilDrained;
|
|
37
37
|
/** A chunk of browser→server bytes for an open stream. */
|
|
@@ -39,7 +39,7 @@ export declare class ForwardManager {
|
|
|
39
39
|
/** The client closed its end; drop the daemon-side socket (no echo back). */
|
|
40
40
|
close(streamId: string): void;
|
|
41
41
|
/** Tear down every stream owned by a socket that just disconnected. */
|
|
42
|
-
reapClient(client:
|
|
42
|
+
reapClient(client: SocketLike): void;
|
|
43
43
|
/** Refuse before a socket ever existed: just tell the client it's closed. */
|
|
44
44
|
private refuse;
|
|
45
45
|
/**
|
package/dist/src/forward.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* accepts becomes a `forward.open` here. We dial `127.0.0.1:<port>` on the
|
|
6
6
|
* daemon box and pipe bytes in both directions as base64 `forward.data` frames,
|
|
7
7
|
* multiplexed over the one WSS connection by `streamId`. This is a raw TCP
|
|
8
|
-
* bridge (not HTTP-aware), so HTTP,
|
|
8
|
+
* bridge (not HTTP-aware), so HTTP, SocketLike HMR, and SSE all pass through
|
|
9
9
|
* unchanged — the dev server sees an ordinary local client.
|
|
10
10
|
*
|
|
11
11
|
* Streams are per-socket, like the terminal PTY: the reply bytes go back to the
|
package/dist/src/forward.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forward.js","sourceRoot":"","sources":["../../src/forward.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,GAAG,MAAM,UAAU,CAAC;AAG3B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,iFAAiF;AACjF,MAAM,WAAW,GAAG,GAAG,CAAC;AACxB,uDAAuD;AACvD,MAAM,QAAQ,GAAG,CAAC,CAAC;AACnB,MAAM,QAAQ,GAAG,KAAK,CAAC;AACvB;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ;AAC3C,2EAA2E;AAC3E,MAAM,aAAa,GAAG,EAAE,CAAC;AACzB,wEAAwE;AACxE,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAWlC,MAAM,OAAO,cAAc;IAIf;IAEA;IALF,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE5C,YACU,GAAQ;IAChB,0EAA0E;IAClE,OAA8C;QAF9C,QAAG,GAAH,GAAG,CAAK;QAER,YAAO,GAAP,OAAO,CAAuC;IACrD,CAAC;IAEJ;;;;OAIG;IACH,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"forward.js","sourceRoot":"","sources":["../../src/forward.ts"],"names":[],"mappings":"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"}
|