realtimeclipboard 0.5.0 → 0.7.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/README.md +4 -4
- package/cli/realtimeclipboard.mjs +32 -70
- package/cli/session.mjs +134 -0
- package/package.json +14 -5
- package/src/core/README.md +1 -1
- package/src/core/bus.js +5 -1
- package/src/core/config.js +282 -195
- package/src/core/crypto.js +13 -2
- package/src/core/keys.js +72 -4
- package/src/core/native.js +59 -15
- package/src/core/state.js +33 -2
- package/src/core/storage.js +16 -0
- package/src/core/surface.js +84 -0
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ touch the server.
|
|
|
19
19
|
**Status: beta, and it works end to end.** The relay is deployed and live,
|
|
20
20
|
the frontend is wired to it over a WebSocket with an SSE fallback, and the
|
|
21
21
|
32-check end-to-end suite runs two real peers with real crypto against the
|
|
22
|
-
production relay. See [
|
|
23
|
-
|
|
22
|
+
production relay. See [Known limitations](#known-limitations) for what is not
|
|
23
|
+
proven.
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
@@ -223,8 +223,8 @@ npm run release -- minor # verify, changelog, release PR, tag, deplo
|
|
|
223
223
|
| [CHANGELOG.md](CHANGELOG.md) | What shipped in each release — generated from the commits |
|
|
224
224
|
| [CLIPBOARD-FLOW.md](docs/CLIPBOARD-FLOW.md) | How the browser reaches the OS clipboard, and why background capture is impossible |
|
|
225
225
|
| [P2P-FILES.md](docs/P2P-FILES.md) | Thumbnails over the relay, bytes over WebRTC, and the corporate-network problem |
|
|
226
|
-
| [
|
|
227
|
-
| [
|
|
226
|
+
| [ACCESSIBILITY.md](docs/ACCESSIBILITY.md) | Keyboard paths, focus handling, and what the screen reader announces |
|
|
227
|
+
| [SELF-HOSTING.md](docs/SELF-HOSTING.md) | Running your own relay and pointing a build at it |
|
|
228
228
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | Setup, the commit convention, and the boundaries the checks enforce |
|
|
229
229
|
| [SECURITY.md](SECURITY.md) | Reporting a vulnerability privately, and what is in scope |
|
|
230
230
|
| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | Contributor Covenant 2.1 |
|
|
@@ -28,11 +28,9 @@ import { hostname } from "node:os";
|
|
|
28
28
|
import { readFileSync } from "node:fs";
|
|
29
29
|
|
|
30
30
|
import * as keys from "../src/core/keys.js";
|
|
31
|
-
import * as cryptoBox from "../src/core/crypto.js";
|
|
32
31
|
import * as relay from "../src/transport/relay.js";
|
|
33
|
-
import * as
|
|
34
|
-
import {
|
|
35
|
-
import { DEFAULT_RELAY_URL, normaliseRelay, TEXT, LOCK } from "../src/core/config.js";
|
|
32
|
+
import * as room from "./session.mjs";
|
|
33
|
+
import { DEFAULT_RELAY_URL, normaliseRelay, TEXT } from "../src/core/config.js";
|
|
36
34
|
import { INVISIBLE_SOURCE } from "../src/core/text.js";
|
|
37
35
|
|
|
38
36
|
/**
|
|
@@ -168,87 +166,43 @@ function die(msg, code = 1) {
|
|
|
168
166
|
/* --------------------------------------------------------------- session -- */
|
|
169
167
|
|
|
170
168
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* the
|
|
169
|
+
* Derived exactly as the browser derives it, because it is the same function —
|
|
170
|
+
* see cli/session.mjs. This wrapper exists only to turn a throw into an exit
|
|
171
|
+
* code, which is the part a pipe cares about.
|
|
174
172
|
*/
|
|
175
173
|
async function derive(rawKey, pin) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (pin) {
|
|
180
|
-
const clean = cryptoBox.normalisePin(pin);
|
|
181
|
-
if (!clean) die("that PIN is too short", 2);
|
|
182
|
-
const d = await cryptoBox.deriveLocked(key, clean);
|
|
183
|
-
return { key, roomHash: d.roomHash, aesKey: d.aesKey, auth: d.authToken, locked: true };
|
|
184
|
-
}
|
|
185
|
-
const open = await cryptoBox.deriveOpen(key);
|
|
186
|
-
return { key, roomHash: open.roomHash, aesKey: open.aesKey, auth: null, locked: false };
|
|
174
|
+
try { return await room.derive(rawKey, pin); }
|
|
175
|
+
catch (err) { die(err.message, 2); }
|
|
187
176
|
}
|
|
188
177
|
|
|
189
178
|
/**
|
|
190
|
-
* Connect and resolve once the relay has welcomed us.
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* in the browser the connection outlives any one call. Here there is a script
|
|
194
|
-
* waiting, so the bus event is adapted back into a promise — and a timeout,
|
|
195
|
-
* since a pipe that hangs forever is worse than one that fails.
|
|
179
|
+
* Connect and resolve once the relay has welcomed us. The adapter is shared; what
|
|
180
|
+
* stays here is the CLI's own reporting — a `--quiet`-aware note on a transport
|
|
181
|
+
* error, and a line on stderr for a clip this key cannot read.
|
|
196
182
|
*/
|
|
197
|
-
function connect(
|
|
183
|
+
function connect(sess, opts, onClip) {
|
|
198
184
|
const url = opts.relay ?? env.REALTIMECLIPBOARD_RELAY ?? undefined;
|
|
199
185
|
if (opts.relay && !normaliseRelay(opts.relay)) die(`"${opts.relay}" is not a usable relay address`, 2);
|
|
200
186
|
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
on(EV.CONN_STATE, ({ state, detail }) => {
|
|
212
|
-
if (state === "connected") { clearTimeout(timer); resolve(); }
|
|
213
|
-
if (state === "error" && detail) note(` ${detail}`, opts);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
relay.setFrameHandler(async msg => {
|
|
217
|
-
if (msg.t !== proto.T.CLIP || !msg.payload) return;
|
|
218
|
-
try {
|
|
219
|
-
const text = await cryptoBox.decrypt(session.aesKey, msg.payload, msg.iv);
|
|
220
|
-
// The lock beacon is a control frame wearing a clip's clothes: it is
|
|
221
|
-
// what lets a joiner tell "wrong PIN" from "first one here". Receivers
|
|
222
|
-
// drop it rather than render it, and a pipe must not see it either.
|
|
223
|
-
// Compared against the constant, not a guess at its shape: it starts
|
|
224
|
-
// with NUL, and "looks like a control character" would also
|
|
225
|
-
// swallow a legitimate clip that happened to.
|
|
226
|
-
if (text === LOCK.BEACON) return;
|
|
227
|
-
onClip(text, msg);
|
|
228
|
-
} catch {
|
|
229
|
-
// Undecryptable means a different secret, not a corrupt relay: someone
|
|
230
|
-
// in the room with another PIN, or a stale frame from a rotated key.
|
|
231
|
-
note(" (a clip arrived that this key cannot read)", opts);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
relay.connect({
|
|
236
|
-
roomHash: session.roomHash,
|
|
237
|
-
intent: "join",
|
|
238
|
-
name: `cli@${hostname()}`,
|
|
239
|
-
auth: session.auth,
|
|
240
|
-
...(url ? { url: normaliseRelay(url) } : {}),
|
|
241
|
-
});
|
|
187
|
+
return room.open({
|
|
188
|
+
session: sess,
|
|
189
|
+
name: `cli@${hostname()}`,
|
|
190
|
+
url: url ? normaliseRelay(url) : undefined,
|
|
191
|
+
timeoutMs: opts.timeout || 0,
|
|
192
|
+
onClip,
|
|
193
|
+
onState: (state, detail) => { if (state === "error" && detail) note(` ${detail}`, opts); },
|
|
194
|
+
onUndecryptable: () => note(" (a clip arrived that this key cannot read)", opts),
|
|
242
195
|
});
|
|
243
196
|
}
|
|
244
197
|
|
|
245
|
-
async function sendText(
|
|
198
|
+
async function sendText(sess, text, opts) {
|
|
246
199
|
if (!text) die("nothing on stdin to send", 2);
|
|
247
200
|
if (text.length > TEXT.MAX_CHARS) {
|
|
248
201
|
die(`that is ${text.length} characters; the limit is ${TEXT.MAX_CHARS}`, 2);
|
|
249
202
|
}
|
|
250
|
-
|
|
251
|
-
|
|
203
|
+
try {
|
|
204
|
+
await room.send(sess, text, `cli-${Date.now().toString(36)}`);
|
|
205
|
+
} catch (err) { die(err.message, 2); }
|
|
252
206
|
// The frame is handed to a socket, not delivered. Give it a moment to flush
|
|
253
207
|
// before the process exits out from under it.
|
|
254
208
|
await new Promise(r => setTimeout(r, 250));
|
|
@@ -338,7 +292,15 @@ if (cmd === "both") {
|
|
|
338
292
|
// Not readStdin(): that waits for EOF, and this mode has to send each line as
|
|
339
293
|
// it is typed while still printing what arrives in between.
|
|
340
294
|
const rl = createInterface({ input: stdin, terminal: false });
|
|
341
|
-
|
|
295
|
+
// Chained, not launched: each line is encrypted before it is sent, and two
|
|
296
|
+
// encryptions in flight complete in whichever order WebCrypto finishes them.
|
|
297
|
+
// A pasted block of lines could reach the relay out of order and be stamped
|
|
298
|
+
// with inverted `seq`, which is what receivers order and dedupe by.
|
|
299
|
+
let sending = Promise.resolve();
|
|
300
|
+
rl.on("line", line => {
|
|
301
|
+
if (!line.length) return;
|
|
302
|
+
sending = sending.then(() => sendText(session, line, opts)).catch(() => {});
|
|
303
|
+
});
|
|
342
304
|
rl.on("close", () => { relay.close(); exit(0); });
|
|
343
305
|
}
|
|
344
306
|
|
package/cli/session.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A room, as a promise-shaped API — for the surfaces that have a caller waiting.
|
|
3
|
+
*
|
|
4
|
+
* relay.js announces state on the bus because in a browser the connection
|
|
5
|
+
* outlives any one call. `cli/`, `mcp/` and `vscode/` all have something waiting
|
|
6
|
+
* instead, and all three had adapted the bus back into a promise separately.
|
|
7
|
+
* This is that adapter, once, because three copies of "derive, connect, decrypt,
|
|
8
|
+
* drop the beacon" is three chances for two ends to disagree silently.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately below the app: it touches no state, emits no bus events and knows
|
|
11
|
+
* nothing about history, editors or clipboards. It hands decrypted text to a
|
|
12
|
+
* callback and stops. What each surface does next is that surface's business.
|
|
13
|
+
*
|
|
14
|
+
* WHY IT LIVES IN cli/ AND NOT src/transport/, which is where it looks like it
|
|
15
|
+
* belongs: `src/` is the web app, and every .js under it is app shell — the
|
|
16
|
+
* precache check in tests/unit/static-check.mjs treats the directory itself as
|
|
17
|
+
* the list. A module no page ever loads would have to be precached anyway, or
|
|
18
|
+
* carved out with an exception. `cli/` is already the npm publish boundary and
|
|
19
|
+
* already the headless half of this codebase, so it is the honest home; `mcp/`
|
|
20
|
+
* and `vscode/` import it from here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as keys from "../src/core/keys.js";
|
|
24
|
+
import * as cryptoBox from "../src/core/crypto.js";
|
|
25
|
+
import { LOCK, TEXT, textBytes, NET } from "../src/core/config.js";
|
|
26
|
+
import { on, EV } from "../src/core/bus.js";
|
|
27
|
+
import * as relay from "../src/transport/relay.js";
|
|
28
|
+
import * as proto from "../src/transport/protocol.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Everything needed to talk in a room, derived exactly as the browser derives
|
|
32
|
+
* it — same salt, same iteration count, same HKDF info strings, because it is
|
|
33
|
+
* the same function.
|
|
34
|
+
*
|
|
35
|
+
* Throws rather than exiting: a library that calls process.exit() cannot be
|
|
36
|
+
* used by anything that wants to handle the failure.
|
|
37
|
+
*/
|
|
38
|
+
export async function derive(rawKey, pin) {
|
|
39
|
+
const key = keys.normalise(rawKey);
|
|
40
|
+
const refused = keys.rejectMessage(key);
|
|
41
|
+
if (refused) throw new Error(`"${rawKey}" cannot be used — ${refused}`);
|
|
42
|
+
|
|
43
|
+
if (pin) {
|
|
44
|
+
const clean = cryptoBox.normalisePin(pin);
|
|
45
|
+
if (!clean) throw new Error(`a PIN needs at least ${LOCK.MIN_PIN} characters`);
|
|
46
|
+
const d = await cryptoBox.deriveLocked(key, clean);
|
|
47
|
+
return { key, roomHash: d.roomHash, aesKey: d.aesKey, auth: d.authToken, locked: true };
|
|
48
|
+
}
|
|
49
|
+
const d = await cryptoBox.deriveOpen(key);
|
|
50
|
+
return { key, roomHash: d.roomHash, aesKey: d.aesKey, auth: null, locked: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Connect, and resolve once the relay has welcomed us.
|
|
55
|
+
*
|
|
56
|
+
* The connect bound is separate from any read timeout a caller keeps: "the relay
|
|
57
|
+
* never answered" and "nothing was ever sent to this room" are different
|
|
58
|
+
* failures, and a script handed one when it expected the other has been told a
|
|
59
|
+
* lie about its own network.
|
|
60
|
+
*
|
|
61
|
+
* onClip(text, frame) decrypted, beacon already dropped
|
|
62
|
+
* onUndecryptable() someone in the room on another secret — not an error
|
|
63
|
+
*/
|
|
64
|
+
export function open({ session, name, url, onClip, onUndecryptable, onState, timeoutMs = 0 }) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
let off = null;
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
off?.(); // or the subscription outlives every failed open
|
|
69
|
+
reject(new Error("the relay did not answer"));
|
|
70
|
+
}, Math.max(timeoutMs || 0, NET.PROBE_MS * 2.5));
|
|
71
|
+
|
|
72
|
+
off = on(EV.CONN_STATE, ({ state, detail }) => {
|
|
73
|
+
onState?.(state, detail);
|
|
74
|
+
if (state !== "connected") return;
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
off();
|
|
77
|
+
resolve(session);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
relay.setFrameHandler(async (msg) => {
|
|
81
|
+
if (msg.t !== proto.T.CLIP || !msg.payload) return;
|
|
82
|
+
let text;
|
|
83
|
+
try {
|
|
84
|
+
text = await cryptoBox.decrypt(session.aesKey, msg.payload, msg.iv);
|
|
85
|
+
} catch {
|
|
86
|
+
// Undecryptable means a different secret, not a corrupt relay: someone
|
|
87
|
+
// in the room on another PIN, or a stale frame from a rotated key.
|
|
88
|
+
onUndecryptable?.();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// A control frame wearing a clip's clothes — it is what lets a joiner tell
|
|
92
|
+
// "wrong PIN" from "first one here". Compared against the constant, never
|
|
93
|
+
// "looks like a control character", which would also swallow a legitimate
|
|
94
|
+
// clip that happened to start with NUL.
|
|
95
|
+
if (text === LOCK.BEACON) return;
|
|
96
|
+
onClip?.(text, msg);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
relay.connect({
|
|
100
|
+
roomHash: session.roomHash,
|
|
101
|
+
intent: "join",
|
|
102
|
+
name,
|
|
103
|
+
auth: session.auth,
|
|
104
|
+
...(url ? { url } : {}),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Bytes, not characters: the frame cap is what the relay actually enforces. */
|
|
110
|
+
export async function send(session, text, originId) {
|
|
111
|
+
if (!text) throw new Error("there is nothing to send");
|
|
112
|
+
if (textBytes(text) > TEXT.MAX_BYTES) {
|
|
113
|
+
throw new Error(`that is ${textBytes(text)} bytes; the limit is ${TEXT.MAX_BYTES}`);
|
|
114
|
+
}
|
|
115
|
+
const { payload, iv } = await cryptoBox.encrypt(session.aesKey, text);
|
|
116
|
+
return relay.send(proto.clip({ payload, iv, originId }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Announce a room being abandoned, so nobody is left connected to nothing. */
|
|
120
|
+
export async function evict(session, originId) {
|
|
121
|
+
try {
|
|
122
|
+
const { payload, iv } = await cryptoBox.encrypt(session.aesKey, LOCK.EVICT);
|
|
123
|
+
relay.send(proto.clip({ payload, iv, originId }));
|
|
124
|
+
} catch { /* leaving anyway */ }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Is the room actually usable right now? Callers that reconnect on demand ask
|
|
128
|
+
* this before tearing a live connection down and building it again. */
|
|
129
|
+
export const isOpen = () => relay.isOpen();
|
|
130
|
+
|
|
131
|
+
export function close() {
|
|
132
|
+
relay.setFrameHandler(() => {});
|
|
133
|
+
relay.close();
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtimeclipboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Live clipboard sharing. Static frontend of native ES modules — development needs no build, and `npm run build` exists only to assemble the deploy. Nothing here is needed to READ the app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,19 +34,28 @@
|
|
|
34
34
|
"node": ">=22"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"verify": "node tests/unit/static-check.mjs && node tests/unit/relay-url.mjs && node tests/unit/lock.mjs && node tests/unit/files.mjs && node tests/unit/transfer.mjs && node tests/unit/clipsize.mjs && node tests/unit/syncmode.mjs && node tests/unit/pasteguard.mjs && node tests/dom/dialog.mjs && node tests/dom/whatsnew.mjs && node tests/dom/tiles.mjs && node tests/dom/guide.mjs && node tests/dom/editor.mjs && node tests/dom/offer.mjs",
|
|
38
|
-
"test": "npm run verify && node tests/dom/bundle.mjs && node tests/live/e2e.mjs && node tests/live/boot.mjs && node tests/live/boot.mjs --locked && node tests/live/fallback.mjs",
|
|
37
|
+
"verify": "node tests/unit/static-check.mjs && node tests/unit/relay-url.mjs && node tests/unit/lock.mjs && node tests/unit/files.mjs && node tests/unit/transfer.mjs && node tests/unit/clipsize.mjs && node tests/unit/syncmode.mjs && node tests/unit/pasteguard.mjs && node tests/unit/sharelink.mjs && node tests/unit/keyfloor.mjs && node tests/unit/adpolicy.mjs && node tests/unit/vscode-host.mjs && node tests/unit/browser-worker.mjs && node tests/dom/dialog.mjs && node tests/dom/theme.mjs && node tests/dom/whatsnew.mjs && node tests/dom/tiles.mjs && node tests/dom/guide.mjs && node tests/dom/links.mjs && node tests/dom/editor.mjs && node tests/dom/capture.mjs && node tests/dom/offer.mjs",
|
|
38
|
+
"test": "npm run verify && npm run build:vscode && npm run build:mcp && npm run build:browser && node tests/dom/bundle.mjs && node tests/dom/extension.mjs && node tests/live/vscode.mjs && node tests/live/mcp.mjs && node tests/live/cli.mjs && node tests/live/e2e.mjs && node tests/live/boot.mjs && node tests/live/boot.mjs --locked && node tests/live/boot.mjs --qr && node tests/live/boot.mjs --locked --qr && node tests/live/fallback.mjs",
|
|
39
39
|
"test:static": "node tests/unit/static-check.mjs",
|
|
40
40
|
"test:lock": "node tests/unit/lock.mjs && node tests/dom/dialog.mjs",
|
|
41
41
|
"test:e2e": "node tests/live/e2e.mjs",
|
|
42
|
-
"test:boot": "node tests/live/boot.mjs && node tests/live/boot.mjs --locked",
|
|
42
|
+
"test:boot": "node tests/live/boot.mjs && node tests/live/boot.mjs --locked && node tests/live/boot.mjs --qr && node tests/live/boot.mjs --locked --qr",
|
|
43
43
|
"test:bundle": "node tests/dom/bundle.mjs",
|
|
44
|
+
"test:vscode": "node tools/build/build-vscode.mjs && node tests/dom/extension.mjs && node tests/live/vscode.mjs",
|
|
44
45
|
"test:tiles": "node tests/dom/tiles.mjs",
|
|
45
46
|
"test:fallback": "node tests/live/fallback.mjs",
|
|
46
47
|
"test:cli": "node tests/live/cli.mjs",
|
|
48
|
+
"test:mcp": "node tests/live/mcp.mjs",
|
|
49
|
+
"test:editor": "node tests/editor/host.mjs",
|
|
47
50
|
"build": "node tools/build/build.mjs",
|
|
48
51
|
"build:site": "node tools/build/build.mjs _site && node tools/check/site-check.mjs _site",
|
|
49
|
-
"
|
|
52
|
+
"size:record": "node tools/build/build.mjs _site --record-size",
|
|
53
|
+
"build:desktop": "node tools/build/build.mjs _desktop --desktop && node tools/check/desktop-check.mjs _desktop",
|
|
54
|
+
"build:vscode": "node tools/build/build-vscode.mjs",
|
|
55
|
+
"build:browser": "node tools/build/build-browser.mjs",
|
|
56
|
+
"build:mcp": "node tools/build/build-mcp.mjs",
|
|
57
|
+
"package:browser": "node tools/build/build-browser.mjs --zip",
|
|
58
|
+
"package:vscode": "node tools/build/build-vscode.mjs --package",
|
|
50
59
|
"build:og": "python tools/build/build-og-card.py",
|
|
51
60
|
"check:og": "python tools/build/build-og-card.py --check",
|
|
52
61
|
"build:icons": "python tools/build/build-icons.py",
|
package/src/core/README.md
CHANGED
|
@@ -9,7 +9,7 @@ and the node test suites run these files unchanged.
|
|
|
9
9
|
| `config.js` | Every tunable constant, the relay address, and the crypto parameters |
|
|
10
10
|
| `state.js` | Session state and the setters that announce changes |
|
|
11
11
|
| `crypto.js` | Room hashing, PBKDF2/HKDF derivation, AES-GCM. No libraries |
|
|
12
|
-
| `keys.js` | Share-key generation, normalisation,
|
|
12
|
+
| `keys.js` | Share-key generation, normalisation, the URL fragment, and the link a peer opens |
|
|
13
13
|
| `storage.js` | localStorage, wrapped so a disabled-storage browser degrades instead of throwing |
|
|
14
14
|
| `paths.js` | Where the app is served from, resolved once for the whole codebase |
|
|
15
15
|
| `device.js` | A human-readable name for this device, shown in the peer list |
|
package/src/core/bus.js
CHANGED
|
@@ -38,7 +38,7 @@ export const EV = {
|
|
|
38
38
|
TEXT_STREAMED: "text:streamed", // {text, caret, name, from} in
|
|
39
39
|
TIER_CHANGED: "clipboard:tier", // {tier, note}
|
|
40
40
|
PENDING_CLIP: "clipboard:pending",// {pending, text} — arrived while unfocused
|
|
41
|
-
CLIP_OFFERED: "clipboard:offered",// {text} — arrived
|
|
41
|
+
CLIP_OFFERED: "clipboard:offered",// {text, onClipboard} — arrived over unsent work; text:null retracts the offer
|
|
42
42
|
PEER_JOINED: "peers:joined", // {name} — a device entered the session
|
|
43
43
|
PEER_LEFT: "peers:left", // {name}
|
|
44
44
|
PERMISSION: "clipboard:permission", // {state} granted|prompt|denied
|
|
@@ -64,6 +64,10 @@ export const EV = {
|
|
|
64
64
|
// from LOCK_STATE because `state.locked` is still false: no session was ever
|
|
65
65
|
// opened to be locked.
|
|
66
66
|
LOCK_REQUIRED: "session:lockrequired",
|
|
67
|
+
// {key, reason} — a key was named and refused before any connection opened.
|
|
68
|
+
// Like LOCK_REQUIRED there is no session behind it, and for the same reason:
|
|
69
|
+
// the app must not look usable when nothing it does would go anywhere.
|
|
70
|
+
KEY_REJECTED: "session:keyrejected",
|
|
67
71
|
FOUNDER: "session:founder", // {founder} — first into this room? null = not yet known
|
|
68
72
|
|
|
69
73
|
// files
|