realtimeclipboard 0.3.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/LICENSE +21 -0
- package/README.md +243 -0
- package/cli/CLAUDE.md +28 -0
- package/cli/README.md +24 -0
- package/cli/realtimeclipboard.mjs +303 -0
- package/package.json +67 -0
- package/src/README.md +24 -0
- package/src/core/CLAUDE.md +31 -0
- package/src/core/README.md +18 -0
- package/src/core/bus.js +84 -0
- package/src/core/config.js +414 -0
- package/src/core/crypto.js +215 -0
- package/src/core/device.js +55 -0
- package/src/core/history.js +208 -0
- package/src/core/keys.js +150 -0
- package/src/core/paths.js +68 -0
- package/src/core/state.js +193 -0
- package/src/core/storage.js +182 -0
- package/src/transport/CLAUDE.md +30 -0
- package/src/transport/README.md +16 -0
- package/src/transport/protocol.js +68 -0
- package/src/transport/relay.js +423 -0
- package/src/transport/sse.js +244 -0
- package/src/transport/ws.js +76 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket channel — the default way to reach the relay.
|
|
3
|
+
*
|
|
4
|
+
* A *channel* moves frames and nothing else. It knows how to open a pipe, push
|
|
5
|
+
* a frame down it, hand incoming frames up, and say when it is finished. It
|
|
6
|
+
* knows nothing about hello, heartbeats, backoff, reconnection or which
|
|
7
|
+
* transport should be used next — all of that lives in relay.js, once, for both
|
|
8
|
+
* channels. See transport/sse.js for the other implementation of this contract.
|
|
9
|
+
*
|
|
10
|
+
* create({ url, roomHash, onOpen, onFrame, onDown }) -> { send, close, isOpen }
|
|
11
|
+
*
|
|
12
|
+
* onOpen() the channel is usable; frames may be sent
|
|
13
|
+
* onFrame(msg) one parsed inbound frame
|
|
14
|
+
* onDown({code, reason})
|
|
15
|
+
* the channel is finished and will not recover. Fired at
|
|
16
|
+
* most once, and never after close() — a deliberate close
|
|
17
|
+
* is not an event anyone needs to react to.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as proto from "./protocol.js";
|
|
21
|
+
|
|
22
|
+
export const LABEL = "WebSocket";
|
|
23
|
+
|
|
24
|
+
export const available = () => typeof WebSocket !== "undefined";
|
|
25
|
+
|
|
26
|
+
export function create({ url, roomHash, auth = null, onOpen, onFrame, onDown }) {
|
|
27
|
+
let done = false;
|
|
28
|
+
|
|
29
|
+
const finish = (code, reason) => {
|
|
30
|
+
if (done) return;
|
|
31
|
+
done = true;
|
|
32
|
+
onDown({ code, reason });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let sock;
|
|
36
|
+
try {
|
|
37
|
+
// `?a=` is a locked session's admission token. Not a secret in the sense
|
|
38
|
+
// the PIN is — it is HKDF output the relay compares against the room's
|
|
39
|
+
// first arrival — but it is still session material, so it rides in the
|
|
40
|
+
// query rather than the path for the same reason `sid` does on the SSE
|
|
41
|
+
// path: paths are what proxies and access logs like to keep.
|
|
42
|
+
sock = new WebSocket(
|
|
43
|
+
`${url.replace(/\/+$/, "")}/ws/${roomHash}${auth ? `?a=${encodeURIComponent(auth)}` : ""}`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Constructing the socket throws synchronously on a malformed URL, and on
|
|
46
|
+
// a Content-Security-Policy that forbids the scheme — which is one of the
|
|
47
|
+
// ways a managed browser blocks WebSockets outright. Report it as a normal
|
|
48
|
+
// channel failure so the caller can fall back rather than crash on boot.
|
|
49
|
+
Promise.resolve().then(() => finish(0, err.message || "could not open"));
|
|
50
|
+
return { label: LABEL, send: () => false, close: () => { done = true; }, isOpen: () => false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
sock.onopen = () => onOpen();
|
|
54
|
+
sock.onmessage = e => onFrame(proto.parse(e.data));
|
|
55
|
+
|
|
56
|
+
// onerror carries no useful detail in any browser, and a close event always
|
|
57
|
+
// follows it — so the close is the single place failure is reported from.
|
|
58
|
+
sock.onerror = () => {};
|
|
59
|
+
sock.onclose = ev => finish(ev.code, ev.reason);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
label: LABEL,
|
|
63
|
+
isOpen: () => sock.readyState === WebSocket.OPEN,
|
|
64
|
+
|
|
65
|
+
send(obj) {
|
|
66
|
+
if (sock.readyState !== WebSocket.OPEN) return false;
|
|
67
|
+
sock.send(JSON.stringify(obj));
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
close(code = 1000) {
|
|
72
|
+
done = true;
|
|
73
|
+
try { sock.close(code); } catch { /* already gone */ }
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|