@getpaseo/relay 0.1.2

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.
@@ -0,0 +1,3 @@
1
+ export declare function arrayBufferToBase64(buffer: ArrayBuffer): string;
2
+ export declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
3
+ //# sourceMappingURL=base64.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base64.d.ts","sourceRoot":"","sources":["../src/base64.ts"],"names":[],"mappings":"AAEA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAE/D;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAY/D"}
package/dist/base64.js ADDED
@@ -0,0 +1,17 @@
1
+ import { fromByteArray, toByteArray } from "base64-js";
2
+ export function arrayBufferToBase64(buffer) {
3
+ return fromByteArray(new Uint8Array(buffer));
4
+ }
5
+ export function base64ToArrayBuffer(base64) {
6
+ const normalized = (() => {
7
+ const trimmed = base64.trim();
8
+ const standard = trimmed.replace(/-/g, "+").replace(/_/g, "/");
9
+ const padLen = (4 - (standard.length % 4)) % 4;
10
+ return standard + "=".repeat(padLen);
11
+ })();
12
+ const bytes = toByteArray(normalized);
13
+ const out = new Uint8Array(bytes.byteLength);
14
+ out.set(bytes);
15
+ return out.buffer;
16
+ }
17
+ //# sourceMappingURL=base64.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base64.js","sourceRoot":"","sources":["../src/base64.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEvD,MAAM,UAAU,mBAAmB,CAAC,MAAmB;IACrD,OAAO,aAAa,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE;QACvB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC/D,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC,CAAC,EAAE,CAAC;IAEL,MAAM,KAAK,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC7C,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACf,OAAO,GAAG,CAAC,MAAM,CAAC;AACpB,CAAC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Cloudflare Durable Objects adapter for the relay.
3
+ *
4
+ * This module provides a Durable Object class that can be deployed to
5
+ * Cloudflare Workers. It uses WebSocket hibernation for cost efficiency.
6
+ *
7
+ * Each session gets its own Durable Object instance, identified by session ID.
8
+ *
9
+ * Wrangler config:
10
+ * ```jsonc
11
+ * {
12
+ * "durable_objects": {
13
+ * "bindings": [{ "name": "RELAY", "class_name": "RelayDurableObject" }]
14
+ * },
15
+ * "migrations": [{ "tag": "v1", "new_classes": ["RelayDurableObject"] }]
16
+ * }
17
+ * ```
18
+ */
19
+ interface DurableObjectState {
20
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;
21
+ getWebSockets(tag?: string): WebSocket[];
22
+ }
23
+ interface Env {
24
+ RELAY: DurableObjectNamespace;
25
+ }
26
+ interface DurableObjectNamespace {
27
+ idFromName(name: string): DurableObjectId;
28
+ get(id: DurableObjectId): DurableObjectStub;
29
+ }
30
+ interface DurableObjectId {
31
+ toString(): string;
32
+ }
33
+ interface DurableObjectStub {
34
+ fetch(request: Request): Promise<Response>;
35
+ }
36
+ export declare class RelayDurableObject {
37
+ private state;
38
+ private pendingClientFrames;
39
+ constructor(state: DurableObjectState);
40
+ private hasServerDataSocket;
41
+ private nudgeOrResetControlForClient;
42
+ private bufferClientFrame;
43
+ private flushClientFrames;
44
+ private listConnectedClientIds;
45
+ private notifyControls;
46
+ fetch(request: Request): Promise<Response>;
47
+ /**
48
+ * Called when a WebSocket message is received (wakes from hibernation).
49
+ */
50
+ webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void;
51
+ /**
52
+ * Called when a WebSocket closes (wakes from hibernation).
53
+ */
54
+ webSocketClose(ws: WebSocket, code: number, reason: string, _wasClean: boolean): void;
55
+ /**
56
+ * Called on WebSocket error.
57
+ */
58
+ webSocketError(ws: WebSocket, error: unknown): void;
59
+ }
60
+ /**
61
+ * Worker entry point that routes requests to the appropriate Durable Object.
62
+ */
63
+ declare const _default: {
64
+ fetch(request: Request, env: Env): Promise<Response>;
65
+ };
66
+ export default _default;
67
+ //# sourceMappingURL=cloudflare-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-adapter.d.ts","sourceRoot":"","sources":["../src/cloudflare-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,UAAU,kBAAkB;IAC1B,eAAe,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACtD,aAAa,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;CAC1C;AAOD,UAAU,GAAG;IACX,KAAK,EAAE,sBAAsB,CAAC;CAC/B;AAED,UAAU,sBAAsB;IAC9B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAAC;IAC1C,GAAG,CAAC,EAAE,EAAE,eAAe,GAAG,iBAAiB,CAAC;CAC7C;AAED,UAAU,eAAe;IACvB,QAAQ,IAAI,MAAM,CAAC;CACpB;AAED,UAAU,iBAAiB;IACzB,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5C;AAiBD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,mBAAmB,CAAkD;gBAEjE,KAAK,EAAE,kBAAkB;IAKrC,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,4BAA4B;IA+BpC,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,cAAc;IAgBhB,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoGhD;;OAEG;IACH,gBAAgB,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAsDpE;;OAEG;IACH,cAAc,CACZ,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,OAAO,GACjB,IAAI;IAkCP;;OAEG;IACH,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;CAOpD;AAED;;GAEG;;mBAEoB,OAAO,OAAO,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC;;AAD5D,wBA0BE"}
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Cloudflare Durable Objects adapter for the relay.
3
+ *
4
+ * This module provides a Durable Object class that can be deployed to
5
+ * Cloudflare Workers. It uses WebSocket hibernation for cost efficiency.
6
+ *
7
+ * Each session gets its own Durable Object instance, identified by session ID.
8
+ *
9
+ * Wrangler config:
10
+ * ```jsonc
11
+ * {
12
+ * "durable_objects": {
13
+ * "bindings": [{ "name": "RELAY", "class_name": "RelayDurableObject" }]
14
+ * },
15
+ * "migrations": [{ "tag": "v1", "new_classes": ["RelayDurableObject"] }]
16
+ * }
17
+ * ```
18
+ */
19
+ export class RelayDurableObject {
20
+ constructor(state) {
21
+ this.pendingClientFrames = new Map();
22
+ this.state = state;
23
+ }
24
+ hasServerDataSocket(clientId) {
25
+ try {
26
+ return this.state.getWebSockets(`server:${clientId}`).length > 0;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ nudgeOrResetControlForClient(clientId) {
33
+ // If the daemon's control WS becomes half-open, the DO can't reliably detect it via ws.send errors
34
+ // (Cloudflare may accept writes even if the other side is no longer reading).
35
+ //
36
+ // Instead, observe whether the daemon reacts by opening the per-client server-data socket.
37
+ // If it doesn't, nudge with a sync message; if still no reaction, force-close the control
38
+ // socket(s) so the daemon reconnects.
39
+ const initialDelayMs = 10000;
40
+ const secondDelayMs = 5000;
41
+ setTimeout(() => {
42
+ if (this.hasServerDataSocket(clientId))
43
+ return;
44
+ // First nudge: send a full sync list.
45
+ this.notifyControls({ type: "sync", clientIds: this.listConnectedClientIds() });
46
+ setTimeout(() => {
47
+ if (this.hasServerDataSocket(clientId))
48
+ return;
49
+ // Still nothing: assume control is stuck and force a reconnect.
50
+ for (const ws of this.state.getWebSockets("server-control")) {
51
+ try {
52
+ ws.close(1011, "Control unresponsive");
53
+ }
54
+ catch {
55
+ // ignore
56
+ }
57
+ }
58
+ }, secondDelayMs);
59
+ }, initialDelayMs);
60
+ }
61
+ bufferClientFrame(clientId, message) {
62
+ const existing = this.pendingClientFrames.get(clientId) ?? [];
63
+ existing.push(message);
64
+ // Prevent unbounded memory growth if a daemon never connects.
65
+ if (existing.length > 200) {
66
+ existing.splice(0, existing.length - 200);
67
+ }
68
+ this.pendingClientFrames.set(clientId, existing);
69
+ }
70
+ flushClientFrames(clientId, serverWs) {
71
+ const frames = this.pendingClientFrames.get(clientId);
72
+ if (!frames || frames.length === 0)
73
+ return;
74
+ this.pendingClientFrames.delete(clientId);
75
+ for (const frame of frames) {
76
+ try {
77
+ serverWs.send(frame);
78
+ }
79
+ catch {
80
+ // If we can't flush, re-buffer and let the daemon re-establish.
81
+ this.bufferClientFrame(clientId, frame);
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ listConnectedClientIds() {
87
+ const out = new Set();
88
+ for (const ws of this.state.getWebSockets("client")) {
89
+ try {
90
+ const attachment = ws.deserializeAttachment();
91
+ if (attachment?.role === "client" && typeof attachment.clientId === "string" && attachment.clientId) {
92
+ out.add(attachment.clientId);
93
+ }
94
+ }
95
+ catch {
96
+ // ignore
97
+ }
98
+ }
99
+ return Array.from(out);
100
+ }
101
+ notifyControls(message) {
102
+ const text = JSON.stringify(message);
103
+ for (const ws of this.state.getWebSockets("server-control")) {
104
+ try {
105
+ ws.send(text);
106
+ }
107
+ catch {
108
+ // If the control socket is dead, close it so the daemon can reconnect.
109
+ try {
110
+ ws.close(1011, "Control send failed");
111
+ }
112
+ catch {
113
+ // ignore
114
+ }
115
+ }
116
+ }
117
+ }
118
+ async fetch(request) {
119
+ const url = new URL(request.url);
120
+ const role = url.searchParams.get("role");
121
+ const serverId = url.searchParams.get("serverId");
122
+ const clientIdRaw = url.searchParams.get("clientId");
123
+ const clientId = typeof clientIdRaw === "string" ? clientIdRaw.trim() : "";
124
+ if (!role || (role !== "server" && role !== "client")) {
125
+ return new Response("Missing or invalid role parameter", { status: 400 });
126
+ }
127
+ if (!serverId) {
128
+ return new Response("Missing serverId parameter", { status: 400 });
129
+ }
130
+ // Clients must provide a clientId so the daemon can create an independent
131
+ // E2EE channel per client connection.
132
+ if (role === "client" && !clientId) {
133
+ return new Response("Missing clientId parameter", { status: 400 });
134
+ }
135
+ const upgradeHeader = request.headers.get("Upgrade");
136
+ if (!upgradeHeader || upgradeHeader.toLowerCase() !== "websocket") {
137
+ return new Response("Expected WebSocket upgrade", { status: 426 });
138
+ }
139
+ const isServerControl = role === "server" && !clientId;
140
+ const isServerData = role === "server" && !!clientId;
141
+ // Close any existing connection with the same identity.
142
+ // - server-control: single per serverId
143
+ // - server-data: single per clientId
144
+ // - client: single per clientId
145
+ if (isServerControl) {
146
+ for (const ws of this.state.getWebSockets("server-control")) {
147
+ ws.close(1008, "Replaced by new connection");
148
+ }
149
+ }
150
+ else if (isServerData) {
151
+ for (const ws of this.state.getWebSockets(`server:${clientId}`)) {
152
+ ws.close(1008, "Replaced by new connection");
153
+ }
154
+ }
155
+ else {
156
+ for (const ws of this.state.getWebSockets(`client:${clientId}`)) {
157
+ ws.close(1008, "Replaced by new connection");
158
+ }
159
+ }
160
+ // Create WebSocket pair
161
+ const pair = new globalThis.WebSocketPair();
162
+ const [client, server] = [pair[0], pair[1]];
163
+ const tags = [];
164
+ if (role === "client") {
165
+ tags.push("client", `client:${clientId}`);
166
+ }
167
+ else if (isServerControl) {
168
+ tags.push("server-control");
169
+ }
170
+ else {
171
+ tags.push("server", `server:${clientId}`);
172
+ }
173
+ // Accept with hibernation support, tagged for lookup.
174
+ this.state.acceptWebSocket(server, tags);
175
+ // Store attachment for hibernation recovery
176
+ const attachment = {
177
+ serverId,
178
+ role,
179
+ clientId: clientId || null,
180
+ createdAt: Date.now(),
181
+ };
182
+ server.serializeAttachment(attachment);
183
+ console.log(`[Relay DO] ${role}${isServerControl ? "(control)" : ""}${isServerData ? `(data:${clientId})` : role === "client" ? `(${clientId})` : ""} connected to session ${serverId}`);
184
+ if (role === "client") {
185
+ this.notifyControls({ type: "client_connected", clientId });
186
+ this.nudgeOrResetControlForClient(clientId);
187
+ }
188
+ if (isServerControl) {
189
+ // Send current client list so the daemon can attach existing clients.
190
+ try {
191
+ server.send(JSON.stringify({ type: "sync", clientIds: this.listConnectedClientIds() }));
192
+ }
193
+ catch {
194
+ // ignore
195
+ }
196
+ }
197
+ if (isServerData && clientId) {
198
+ this.flushClientFrames(clientId, server);
199
+ }
200
+ return new Response(null, {
201
+ status: 101,
202
+ webSocket: client,
203
+ });
204
+ }
205
+ /**
206
+ * Called when a WebSocket message is received (wakes from hibernation).
207
+ */
208
+ webSocketMessage(ws, message) {
209
+ const attachment = ws.deserializeAttachment();
210
+ if (!attachment) {
211
+ console.error("[Relay DO] Message from WebSocket without attachment");
212
+ return;
213
+ }
214
+ const { role, clientId } = attachment;
215
+ if (!clientId) {
216
+ // Control channel: support simple app-level keepalive.
217
+ if (typeof message === "string") {
218
+ try {
219
+ const parsed = JSON.parse(message);
220
+ if (parsed?.type === "ping") {
221
+ try {
222
+ ws.send(JSON.stringify({ type: "pong", ts: Date.now() }));
223
+ }
224
+ catch {
225
+ // ignore
226
+ }
227
+ }
228
+ }
229
+ catch {
230
+ // ignore non-JSON control payloads
231
+ }
232
+ }
233
+ return;
234
+ }
235
+ if (role === "client") {
236
+ const servers = this.state.getWebSockets(`server:${clientId}`);
237
+ if (servers.length === 0) {
238
+ this.bufferClientFrame(clientId, message);
239
+ return;
240
+ }
241
+ for (const target of servers) {
242
+ try {
243
+ target.send(message);
244
+ }
245
+ catch (error) {
246
+ console.error(`[Relay DO] Failed to forward client->server(${clientId}):`, error);
247
+ }
248
+ }
249
+ return;
250
+ }
251
+ // server data socket -> client
252
+ const targets = this.state.getWebSockets(`client:${clientId}`);
253
+ for (const target of targets) {
254
+ try {
255
+ target.send(message);
256
+ }
257
+ catch (error) {
258
+ console.error(`[Relay DO] Failed to forward server->client(${clientId}):`, error);
259
+ }
260
+ }
261
+ }
262
+ /**
263
+ * Called when a WebSocket closes (wakes from hibernation).
264
+ */
265
+ webSocketClose(ws, code, reason, _wasClean) {
266
+ const attachment = ws.deserializeAttachment();
267
+ if (!attachment)
268
+ return;
269
+ console.log(`[Relay DO] ${attachment.role}${attachment.clientId ? `(${attachment.clientId})` : ""} disconnected from session ${attachment.serverId} (${code}: ${reason})`);
270
+ if (attachment.role === "client" && attachment.clientId) {
271
+ this.pendingClientFrames.delete(attachment.clientId);
272
+ // Close the matching server-data socket so the daemon can clean up quickly.
273
+ for (const serverWs of this.state.getWebSockets(`server:${attachment.clientId}`)) {
274
+ try {
275
+ serverWs.close(1001, "Client disconnected");
276
+ }
277
+ catch {
278
+ // ignore
279
+ }
280
+ }
281
+ this.notifyControls({ type: "client_disconnected", clientId: attachment.clientId });
282
+ return;
283
+ }
284
+ if (attachment.role === "server" && attachment.clientId) {
285
+ // Force the client to reconnect and re-handshake when the daemon side drops.
286
+ for (const clientWs of this.state.getWebSockets(`client:${attachment.clientId}`)) {
287
+ try {
288
+ clientWs.close(1012, "Server disconnected");
289
+ }
290
+ catch {
291
+ // ignore
292
+ }
293
+ }
294
+ }
295
+ }
296
+ /**
297
+ * Called on WebSocket error.
298
+ */
299
+ webSocketError(ws, error) {
300
+ const attachment = ws.deserializeAttachment();
301
+ console.error(`[Relay DO] WebSocket error for ${attachment?.role ?? "unknown"}:`, error);
302
+ }
303
+ }
304
+ /**
305
+ * Worker entry point that routes requests to the appropriate Durable Object.
306
+ */
307
+ export default {
308
+ async fetch(request, env) {
309
+ const url = new URL(request.url);
310
+ // Health check
311
+ if (url.pathname === "/health") {
312
+ return new Response(JSON.stringify({ status: "ok" }), {
313
+ headers: { "Content-Type": "application/json" },
314
+ });
315
+ }
316
+ // Relay endpoint
317
+ if (url.pathname === "/ws") {
318
+ const serverId = url.searchParams.get("serverId");
319
+ if (!serverId) {
320
+ return new Response("Missing serverId parameter", { status: 400 });
321
+ }
322
+ // Route to Durable Object instance for this session
323
+ const id = env.RELAY.idFromName(serverId);
324
+ const stub = env.RELAY.get(id);
325
+ return stub.fetch(request);
326
+ }
327
+ return new Response("Not found", { status: 404 });
328
+ },
329
+ };
330
+ //# sourceMappingURL=cloudflare-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-adapter.js","sourceRoot":"","sources":["../src/cloudflare-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * E2EE crypto primitives using NaCl (tweetnacl).
3
+ *
4
+ * - Key exchange: Curve25519 (nacl.box.before)
5
+ * - Encryption: XSalsa20-Poly1305 (nacl.box.after / open.after)
6
+ *
7
+ * Bundle format (binary):
8
+ * [nonce (24 bytes)] [ciphertext...]
9
+ *
10
+ * Transport format:
11
+ * The encrypted-channel sends the bundle as base64 text over WebSocket.
12
+ */
13
+ export type KeyPair = {
14
+ publicKey: Uint8Array;
15
+ secretKey: Uint8Array;
16
+ };
17
+ export type SharedKey = Uint8Array;
18
+ export declare function generateKeyPair(): KeyPair;
19
+ export declare function exportPublicKey(publicKey: Uint8Array): string;
20
+ export declare function importPublicKey(base64: string): Uint8Array;
21
+ export declare function exportSecretKey(secretKey: Uint8Array): string;
22
+ export declare function importSecretKey(base64: string): Uint8Array;
23
+ export declare function deriveSharedKey(ourSecretKey: Uint8Array, peerPublicKey: Uint8Array): SharedKey;
24
+ /**
25
+ * Encrypts data and returns the binary bundle:
26
+ * [nonce (24)] [ciphertext...]
27
+ */
28
+ export declare function encrypt(sharedKey: SharedKey, data: string | ArrayBuffer): ArrayBuffer;
29
+ export declare function decrypt(sharedKey: SharedKey, data: ArrayBuffer): string | ArrayBuffer;
30
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;GAWG;AAKH,MAAM,MAAM,OAAO,GAAG;IACpB,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC;AA+CnC,wBAAgB,eAAe,IAAI,OAAO,CAIzC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAK7D;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAM1D;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,CAK7D;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAM1D;AAED,wBAAgB,eAAe,CAC7B,YAAY,EAAE,UAAU,EACxB,aAAa,EAAE,UAAU,GACxB,SAAS,CAQX;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CASrF;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,WAAW,CAmBrF"}
package/dist/crypto.js ADDED
@@ -0,0 +1,126 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * E2EE crypto primitives using NaCl (tweetnacl).
4
+ *
5
+ * - Key exchange: Curve25519 (nacl.box.before)
6
+ * - Encryption: XSalsa20-Poly1305 (nacl.box.after / open.after)
7
+ *
8
+ * Bundle format (binary):
9
+ * [nonce (24 bytes)] [ciphertext...]
10
+ *
11
+ * Transport format:
12
+ * The encrypted-channel sends the bundle as base64 text over WebSocket.
13
+ */
14
+ import nacl from "tweetnacl";
15
+ import { fromByteArray, toByteArray } from "base64-js";
16
+ const NONCE_LENGTH = nacl.box.nonceLength; // 24
17
+ let prngReady = false;
18
+ function ensurePrng() {
19
+ if (prngReady)
20
+ return;
21
+ try {
22
+ nacl.randomBytes(1);
23
+ prngReady = true;
24
+ return;
25
+ }
26
+ catch {
27
+ // fallthrough
28
+ }
29
+ const cryptoObj = globalThis.crypto;
30
+ if (cryptoObj?.getRandomValues) {
31
+ nacl.setPRNG((x, n) => {
32
+ cryptoObj.getRandomValues(x.subarray(0, n));
33
+ });
34
+ prngReady = true;
35
+ return;
36
+ }
37
+ throw new Error("No secure PRNG available for tweetnacl (missing crypto.getRandomValues)");
38
+ }
39
+ function encodeBase64(bytes) {
40
+ return fromByteArray(bytes);
41
+ }
42
+ function decodeBase64(base64) {
43
+ return toByteArray(base64);
44
+ }
45
+ function toUint8(data) {
46
+ return typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
47
+ }
48
+ function toArrayBuffer(bytes) {
49
+ const out = new Uint8Array(bytes.byteLength);
50
+ out.set(bytes);
51
+ return out.buffer;
52
+ }
53
+ export function generateKeyPair() {
54
+ ensurePrng();
55
+ const { publicKey, secretKey } = nacl.box.keyPair();
56
+ return { publicKey, secretKey };
57
+ }
58
+ export function exportPublicKey(publicKey) {
59
+ if (!(publicKey instanceof Uint8Array) || publicKey.byteLength !== nacl.box.publicKeyLength) {
60
+ throw new Error(`Invalid public key length (expected ${nacl.box.publicKeyLength})`);
61
+ }
62
+ return encodeBase64(publicKey);
63
+ }
64
+ export function importPublicKey(base64) {
65
+ const bytes = decodeBase64(base64);
66
+ if (bytes.byteLength !== nacl.box.publicKeyLength) {
67
+ throw new Error(`Invalid public key length (expected ${nacl.box.publicKeyLength})`);
68
+ }
69
+ return bytes;
70
+ }
71
+ export function exportSecretKey(secretKey) {
72
+ if (!(secretKey instanceof Uint8Array) || secretKey.byteLength !== nacl.box.secretKeyLength) {
73
+ throw new Error(`Invalid secret key length (expected ${nacl.box.secretKeyLength})`);
74
+ }
75
+ return encodeBase64(secretKey);
76
+ }
77
+ export function importSecretKey(base64) {
78
+ const bytes = decodeBase64(base64);
79
+ if (bytes.byteLength !== nacl.box.secretKeyLength) {
80
+ throw new Error(`Invalid secret key length (expected ${nacl.box.secretKeyLength})`);
81
+ }
82
+ return bytes;
83
+ }
84
+ export function deriveSharedKey(ourSecretKey, peerPublicKey) {
85
+ if (ourSecretKey.byteLength !== nacl.box.secretKeyLength) {
86
+ throw new Error(`Invalid secret key length (expected ${nacl.box.secretKeyLength})`);
87
+ }
88
+ if (peerPublicKey.byteLength !== nacl.box.publicKeyLength) {
89
+ throw new Error(`Invalid peer public key length (expected ${nacl.box.publicKeyLength})`);
90
+ }
91
+ return nacl.box.before(peerPublicKey, ourSecretKey);
92
+ }
93
+ /**
94
+ * Encrypts data and returns the binary bundle:
95
+ * [nonce (24)] [ciphertext...]
96
+ */
97
+ export function encrypt(sharedKey, data) {
98
+ ensurePrng();
99
+ const nonce = nacl.randomBytes(NONCE_LENGTH);
100
+ const plaintext = toUint8(data);
101
+ const ciphertext = nacl.box.after(plaintext, nonce, sharedKey);
102
+ const out = new Uint8Array(nonce.byteLength + ciphertext.byteLength);
103
+ out.set(nonce, 0);
104
+ out.set(ciphertext, nonce.byteLength);
105
+ return toArrayBuffer(out);
106
+ }
107
+ export function decrypt(sharedKey, data) {
108
+ const bytes = new Uint8Array(data);
109
+ if (bytes.byteLength < NONCE_LENGTH) {
110
+ throw new Error("Ciphertext bundle too short");
111
+ }
112
+ const nonce = bytes.slice(0, NONCE_LENGTH);
113
+ const ciphertext = bytes.slice(NONCE_LENGTH);
114
+ const opened = nacl.box.open.after(ciphertext, nonce, sharedKey);
115
+ if (!opened) {
116
+ throw new Error("Decryption failed");
117
+ }
118
+ const plaintext = toArrayBuffer(opened);
119
+ try {
120
+ return new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
121
+ }
122
+ catch {
123
+ return plaintext;
124
+ }
125
+ }
126
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
package/dist/e2ee.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { createClientChannel, createDaemonChannel, EncryptedChannel, } from "./encrypted-channel.js";
2
+ export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js";
3
+ export { generateKeyPair, exportPublicKey, importPublicKey, exportSecretKey, importSecretKey, } from "./crypto.js";
4
+ export type { KeyPair, SharedKey } from "./crypto.js";
5
+ //# sourceMappingURL=e2ee.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2ee.d.ts","sourceRoot":"","sources":["../src/e2ee.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EACL,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
package/dist/e2ee.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createClientChannel, createDaemonChannel, EncryptedChannel, } from "./encrypted-channel.js";
2
+ export { generateKeyPair, exportPublicKey, importPublicKey, exportSecretKey, importSecretKey, } from "./crypto.js";
3
+ //# sourceMappingURL=e2ee.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2ee.js","sourceRoot":"","sources":["../src/e2ee.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Encrypted channel that wraps a WebSocket-like transport.
3
+ *
4
+ * Handles ECDH handshake and encrypts/decrypts all messages.
5
+ * Works identically for daemon and client sides.
6
+ */
7
+ import { type KeyPair, type SharedKey } from "./crypto.js";
8
+ export interface Transport {
9
+ send(data: string | ArrayBuffer): void;
10
+ close(code?: number, reason?: string): void;
11
+ onmessage: ((data: string | ArrayBuffer) => void) | null;
12
+ onclose: ((code: number, reason: string) => void) | null;
13
+ onerror: ((error: Error) => void) | null;
14
+ }
15
+ export interface EncryptedChannelEvents {
16
+ onopen?: () => void;
17
+ onmessage?: (data: string | ArrayBuffer) => void;
18
+ onclose?: (code: number, reason: string) => void;
19
+ onerror?: (error: Error) => void;
20
+ }
21
+ type ChannelState = "connecting" | "handshaking" | "open" | "closed";
22
+ type EncryptedChannelOptions = {
23
+ /**
24
+ * If set, the channel can validate repeated plaintext `{type:"hello"}`
25
+ * messages even after it is open.
26
+ *
27
+ * This is useful for robustness when the client retries the handshake
28
+ * (e.g., it didn't observe the daemon's `{type:"ready"}` yet). In that case,
29
+ * the daemon should re-send `{type:"ready"}` without changing keys.
30
+ */
31
+ daemonKeyPair?: KeyPair;
32
+ };
33
+ /**
34
+ * Creates an encrypted channel as the initiator (client).
35
+ *
36
+ * The client:
37
+ * 1. Receives daemon's public key via QR code
38
+ * 2. Generates own keypair
39
+ * 3. Sends hello with own public key
40
+ * 4. Derives shared key and starts encrypted communication
41
+ */
42
+ export declare function createClientChannel(transport: Transport, daemonPublicKeyB64: string, events?: EncryptedChannelEvents): Promise<EncryptedChannel>;
43
+ /**
44
+ * Creates an encrypted channel as the responder (daemon).
45
+ *
46
+ * The daemon:
47
+ * 1. Has pre-generated keypair (public key was in QR)
48
+ * 2. Waits for client's hello with their public key
49
+ * 3. Derives shared key and starts encrypted communication
50
+ */
51
+ export declare function createDaemonChannel(transport: Transport, daemonKeyPair: KeyPair, events?: EncryptedChannelEvents): Promise<EncryptedChannel>;
52
+ /**
53
+ * Encrypted channel that wraps a transport with E2EE.
54
+ */
55
+ export declare class EncryptedChannel {
56
+ private transport;
57
+ private sharedKey;
58
+ private state;
59
+ private events;
60
+ private options;
61
+ private pendingSends;
62
+ private onOpenCallbacks;
63
+ private onCloseCallbacks;
64
+ constructor(transport: Transport, sharedKey: SharedKey, events?: EncryptedChannelEvents, options?: EncryptedChannelOptions);
65
+ setState(state: ChannelState): void;
66
+ private handleMessage;
67
+ send(data: string | ArrayBuffer): Promise<void>;
68
+ private flushPendingSends;
69
+ close(code?: number, reason?: string): void;
70
+ isOpen(): boolean;
71
+ onTransitionToOpen(cb: () => void): void;
72
+ onClose(cb: () => void): void;
73
+ }
74
+ export {};
75
+ //# sourceMappingURL=encrypted-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-channel.d.ts","sourceRoot":"","sources":["../src/encrypted-channel.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,OAAO,EAOL,KAAK,OAAO,EACZ,KAAK,SAAS,EACf,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IACvC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,KAAK,IAAI,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,KAAK,YAAY,GAAG,YAAY,GAAG,aAAa,GAAG,MAAM,GAAG,QAAQ,CAAC;AAErE,KAAK,uBAAuB,GAAG;IAC7B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAcF;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,SAAS,EACpB,kBAAkB,EAAE,MAAM,EAC1B,MAAM,GAAE,sBAA2B,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAkD3B;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,OAAO,EACtB,MAAM,GAAE,sBAA2B,GAClC,OAAO,CAAC,gBAAgB,CAAC,CA2D3B;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,gBAAgB,CAAyB;gBAG/C,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,SAAS,EACpB,MAAM,GAAE,sBAA2B,EACnC,OAAO,GAAE,uBAA4B;IAkBvC,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;YAIrB,aAAa;IAmHrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;YAkBvC,iBAAiB;IAS/B,KAAK,CAAC,IAAI,SAAO,EAAE,MAAM,SAAmB,GAAG,IAAI;IAKnD,MAAM,IAAI,OAAO;IAIjB,kBAAkB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAIxC,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;CAG9B"}
@@ -0,0 +1,305 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Encrypted channel that wraps a WebSocket-like transport.
4
+ *
5
+ * Handles ECDH handshake and encrypts/decrypts all messages.
6
+ * Works identically for daemon and client sides.
7
+ */
8
+ import { generateKeyPair, exportPublicKey, importPublicKey, deriveSharedKey, encrypt, decrypt, } from "./crypto.js";
9
+ import { arrayBufferToBase64, base64ToArrayBuffer } from "./base64.js";
10
+ const HANDSHAKE_RETRY_MS = 1000;
11
+ const MAX_PENDING_SENDS = 200;
12
+ /**
13
+ * Creates an encrypted channel as the initiator (client).
14
+ *
15
+ * The client:
16
+ * 1. Receives daemon's public key via QR code
17
+ * 2. Generates own keypair
18
+ * 3. Sends hello with own public key
19
+ * 4. Derives shared key and starts encrypted communication
20
+ */
21
+ export async function createClientChannel(transport, daemonPublicKeyB64, events = {}) {
22
+ const keyPair = generateKeyPair();
23
+ const daemonPublicKey = importPublicKey(daemonPublicKeyB64);
24
+ const sharedKey = deriveSharedKey(keyPair.secretKey, daemonPublicKey);
25
+ const channel = new EncryptedChannel(transport, sharedKey, events);
26
+ // Send hello with our public key
27
+ const ourPublicKeyB64 = exportPublicKey(keyPair.publicKey);
28
+ const hello = { type: "hello", key: ourPublicKeyB64 };
29
+ const helloText = JSON.stringify(hello);
30
+ let retry = null;
31
+ const emitSendError = (error) => {
32
+ const err = error instanceof Error ? error : new Error(String(error));
33
+ events.onerror?.(err);
34
+ };
35
+ const sendHello = () => {
36
+ try {
37
+ transport.send(helloText);
38
+ return true;
39
+ }
40
+ catch (error) {
41
+ // This can happen during daemon restarts while the socket transitions
42
+ // through CLOSING/CLOSED states. Report it but do not throw from timers.
43
+ emitSendError(error);
44
+ return false;
45
+ }
46
+ };
47
+ const clearRetry = () => {
48
+ if (retry) {
49
+ clearInterval(retry);
50
+ retry = null;
51
+ }
52
+ };
53
+ channel.onTransitionToOpen(() => clearRetry());
54
+ channel.onClose(() => clearRetry());
55
+ sendHello();
56
+ retry = setInterval(() => {
57
+ if (channel.isOpen()) {
58
+ clearRetry();
59
+ return;
60
+ }
61
+ sendHello();
62
+ }, HANDSHAKE_RETRY_MS);
63
+ // Avoid keeping Node processes alive (e.g. tests) if the handshake is stuck.
64
+ retry.unref?.();
65
+ return channel;
66
+ }
67
+ /**
68
+ * Creates an encrypted channel as the responder (daemon).
69
+ *
70
+ * The daemon:
71
+ * 1. Has pre-generated keypair (public key was in QR)
72
+ * 2. Waits for client's hello with their public key
73
+ * 3. Derives shared key and starts encrypted communication
74
+ */
75
+ export async function createDaemonChannel(transport, daemonKeyPair, events = {}) {
76
+ return new Promise((resolve, reject) => {
77
+ const bufferedMessages = [];
78
+ const shouldIgnorePostHelloPlaintext = (data) => {
79
+ try {
80
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data);
81
+ const parsed = JSON.parse(text);
82
+ return parsed.type === "hello" || parsed.type === "ready";
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ };
88
+ transport.onmessage = async (data) => {
89
+ try {
90
+ const helloText = typeof data === "string" ? data : new TextDecoder().decode(data);
91
+ const msg = JSON.parse(helloText);
92
+ if (msg.type !== "hello" || !msg.key) {
93
+ throw new Error("Invalid hello message");
94
+ }
95
+ // Buffer any subsequent messages that arrive while we're doing async
96
+ // WebCrypto work to derive the shared key. Without this, it's possible
97
+ // for the next message (already encrypted) to be misinterpreted as a
98
+ // second hello, causing the handshake to fail.
99
+ transport.onmessage = (next) => {
100
+ bufferedMessages.push(next);
101
+ };
102
+ const clientPublicKey = importPublicKey(msg.key);
103
+ const sharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientPublicKey);
104
+ const channel = new EncryptedChannel(transport, sharedKey, events, { daemonKeyPair });
105
+ transport.send(JSON.stringify({ type: "ready" }));
106
+ channel.setState("open");
107
+ events.onopen?.();
108
+ for (const buffered of bufferedMessages) {
109
+ if (shouldIgnorePostHelloPlaintext(buffered))
110
+ continue;
111
+ transport.onmessage?.(buffered);
112
+ }
113
+ resolve(channel);
114
+ }
115
+ catch (error) {
116
+ reject(error);
117
+ }
118
+ };
119
+ transport.onerror = (error) => {
120
+ reject(error);
121
+ };
122
+ transport.onclose = (code, reason) => {
123
+ reject(new Error(`Connection closed during handshake: ${code} ${reason}`));
124
+ };
125
+ });
126
+ }
127
+ /**
128
+ * Encrypted channel that wraps a transport with E2EE.
129
+ */
130
+ export class EncryptedChannel {
131
+ constructor(transport, sharedKey, events = {}, options = {}) {
132
+ this.state = "handshaking";
133
+ this.pendingSends = [];
134
+ this.onOpenCallbacks = [];
135
+ this.onCloseCallbacks = [];
136
+ this.transport = transport;
137
+ this.sharedKey = sharedKey;
138
+ this.events = events;
139
+ this.options = options;
140
+ transport.onmessage = (data) => this.handleMessage(data);
141
+ transport.onclose = (code, reason) => {
142
+ this.state = "closed";
143
+ this.events.onclose?.(code, reason);
144
+ for (const cb of this.onCloseCallbacks)
145
+ cb();
146
+ };
147
+ transport.onerror = (error) => {
148
+ this.events.onerror?.(error);
149
+ };
150
+ }
151
+ setState(state) {
152
+ this.state = state;
153
+ }
154
+ async handleMessage(data) {
155
+ if (this.state === "handshaking") {
156
+ try {
157
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data);
158
+ const msg = JSON.parse(text);
159
+ if (msg.type === "ready") {
160
+ this.state = "open";
161
+ this.events.onopen?.();
162
+ for (const cb of this.onOpenCallbacks)
163
+ cb();
164
+ await this.flushPendingSends();
165
+ }
166
+ }
167
+ catch {
168
+ // ignore non-ready handshake traffic
169
+ }
170
+ return;
171
+ }
172
+ if (this.state !== "open")
173
+ return;
174
+ try {
175
+ const ciphertext = await (async () => {
176
+ // Handle (or ignore) any stray plaintext handshake traffic.
177
+ try {
178
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data);
179
+ if (text.trim().startsWith("{")) {
180
+ const parsed = JSON.parse(text);
181
+ if (parsed.type === "hello" && typeof parsed.key === "string") {
182
+ if (this.options.daemonKeyPair) {
183
+ try {
184
+ const clientPublicKey = importPublicKey(parsed.key);
185
+ const nextSharedKey = deriveSharedKey(this.options.daemonKeyPair.secretKey, clientPublicKey);
186
+ // If it's the same client key (handshake retry), re-send
187
+ // "ready" but do not re-key. Re-keying here would desync
188
+ // the channel and cause decrypt failures.
189
+ if (keysEqual(nextSharedKey, this.sharedKey)) {
190
+ this.transport.send(JSON.stringify({ type: "ready" }));
191
+ return null;
192
+ }
193
+ // Different key implies a new client connection (common with relays
194
+ // where the daemon's socket stays open while the client reconnects).
195
+ // Re-key and re-send "ready". Drop any queued sends to avoid leaking
196
+ // messages between logical client sessions.
197
+ this.state = "handshaking";
198
+ this.sharedKey = nextSharedKey;
199
+ this.pendingSends = [];
200
+ this.transport.send(JSON.stringify({ type: "ready" }));
201
+ this.state = "open";
202
+ await this.flushPendingSends();
203
+ return null;
204
+ }
205
+ catch (error) {
206
+ throw error;
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ if (parsed.type === "ready") {
212
+ return null;
213
+ }
214
+ // Any other JSON-looking payload is plaintext app traffic, which
215
+ // means the peer is not encrypting (or we are out of sync).
216
+ throw new Error("Received plaintext frame on encrypted channel");
217
+ }
218
+ }
219
+ catch (error) {
220
+ // If we detected plaintext protocol mismatch, fail hard.
221
+ if (error instanceof Error && error.message.includes("plaintext frame")) {
222
+ throw error;
223
+ }
224
+ // Otherwise ignore JSON parse/TextDecoder failures and fall back to
225
+ // decoding ciphertext below.
226
+ }
227
+ if (typeof data === "string") {
228
+ return base64ToArrayBuffer(data);
229
+ }
230
+ // Some WebSocket implementations deliver text frames as ArrayBuffer.
231
+ // Our protocol always transmits ciphertext as base64 text.
232
+ try {
233
+ const decoded = new TextDecoder().decode(data);
234
+ return base64ToArrayBuffer(decoded);
235
+ }
236
+ catch {
237
+ return data;
238
+ }
239
+ })();
240
+ if (ciphertext) {
241
+ const plaintext = await decrypt(this.sharedKey, ciphertext);
242
+ this.events.onmessage?.(plaintext);
243
+ }
244
+ }
245
+ catch (error) {
246
+ const err = error instanceof Error ? error : new Error(String(error));
247
+ // Treat decryption/protocol errors as fatal so the peer can reconnect and
248
+ // re-handshake. Emitting an error event here can cause higher-level code
249
+ // to tear down the session without triggering a clean reconnect.
250
+ try {
251
+ this.transport.close(1011, err.message);
252
+ }
253
+ catch {
254
+ // ignore
255
+ }
256
+ }
257
+ }
258
+ async send(data) {
259
+ if (this.state === "handshaking") {
260
+ if (this.pendingSends.length >= MAX_PENDING_SENDS) {
261
+ this.pendingSends.shift();
262
+ }
263
+ this.pendingSends.push(data);
264
+ return;
265
+ }
266
+ if (this.state !== "open") {
267
+ throw new Error("Channel not open");
268
+ }
269
+ const ciphertext = await encrypt(this.sharedKey, data);
270
+ // Send as base64 for WebSocket text compatibility
271
+ this.transport.send(arrayBufferToBase64(ciphertext));
272
+ }
273
+ async flushPendingSends() {
274
+ if (this.state !== "open")
275
+ return;
276
+ const pending = this.pendingSends;
277
+ this.pendingSends = [];
278
+ for (const item of pending) {
279
+ await this.send(item);
280
+ }
281
+ }
282
+ close(code = 1000, reason = "Normal closure") {
283
+ this.state = "closed";
284
+ this.transport.close(code, reason);
285
+ }
286
+ isOpen() {
287
+ return this.state === "open";
288
+ }
289
+ onTransitionToOpen(cb) {
290
+ this.onOpenCallbacks.push(cb);
291
+ }
292
+ onClose(cb) {
293
+ this.onCloseCallbacks.push(cb);
294
+ }
295
+ }
296
+ function keysEqual(a, b) {
297
+ if (a.byteLength !== b.byteLength)
298
+ return false;
299
+ for (let i = 0; i < a.byteLength; i += 1) {
300
+ if (a[i] !== b[i])
301
+ return false;
302
+ }
303
+ return true;
304
+ }
305
+ //# sourceMappingURL=encrypted-channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-channel.js","sourceRoot":"","sources":["../src/encrypted-channel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export type { ConnectionRole, RelaySessionAttachment, } from "./types.js";
2
+ export { generateKeyPair, exportPublicKey, importPublicKey, deriveSharedKey, encrypt, decrypt, } from "./crypto.js";
3
+ export { createClientChannel, createDaemonChannel, EncryptedChannel, } from "./encrypted-channel.js";
4
+ export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,EACf,OAAO,EACP,OAAO,GACR,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { generateKeyPair, exportPublicKey, importPublicKey, deriveSharedKey, encrypt, decrypt, } from "./crypto.js";
2
+ export { createClientChannel, createDaemonChannel, EncryptedChannel, } from "./encrypted-channel.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,EACf,OAAO,EACP,OAAO,GACR,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Relay connection types and interfaces.
3
+ *
4
+ * The relay bridges two WebSocket connections:
5
+ * - Server (daemon): The Paseo server connecting to the relay
6
+ * - Client (app): The mobile/web app connecting to the relay
7
+ *
8
+ * Messages are forwarded bidirectionally without modification.
9
+ */
10
+ export type ConnectionRole = "server" | "client";
11
+ export interface RelaySessionAttachment {
12
+ serverId: string;
13
+ role: ConnectionRole;
14
+ /**
15
+ * Unique id for the client connection. Allows the daemon to create an
16
+ * independent socket + E2EE channel per connected client.
17
+ */
18
+ clientId?: string | null;
19
+ createdAt: number;
20
+ }
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEjD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,cAAc,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Relay connection types and interfaces.
3
+ *
4
+ * The relay bridges two WebSocket connections:
5
+ * - Server (daemon): The Paseo server connecting to the relay
6
+ * - Client (app): The mobile/web app connecting to the relay
7
+ *
8
+ * Messages are forwarded bidirectionally without modification.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@getpaseo/relay",
3
+ "version": "0.1.2",
4
+ "description": "Paseo relay for bridging daemon and client connections",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "types": "./dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "node": "./dist/index.js",
17
+ "default": "./src/index.ts"
18
+ },
19
+ "./e2ee": {
20
+ "types": "./dist/e2ee.d.ts",
21
+ "node": "./dist/e2ee.js",
22
+ "default": "./src/e2ee.ts"
23
+ },
24
+ "./cloudflare": {
25
+ "types": "./dist/cloudflare-adapter.d.ts",
26
+ "node": "./dist/cloudflare-adapter.js",
27
+ "default": "./src/cloudflare-adapter.ts"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false",
32
+ "prepack": "npm run build",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "vitest run",
35
+ "test:watch": "vitest"
36
+ },
37
+ "dependencies": {
38
+ "base64-js": "^1.5.1",
39
+ "tweetnacl": "^1.0.3",
40
+ "ws": "^8.14.2"
41
+ },
42
+ "devDependencies": {
43
+ "@cloudflare/workers-types": "^4.20241230.0",
44
+ "@types/node": "^20.9.0",
45
+ "@types/ws": "^8.5.8",
46
+ "typescript": "^5.2.2",
47
+ "vitest": "^3.2.4"
48
+ }
49
+ }