@oh-just-another/collab 0.57.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/awareness.d.ts +60 -0
- package/dist/awareness.d.ts.map +1 -0
- package/dist/awareness.js +89 -0
- package/dist/awareness.js.map +1 -0
- package/dist/bind-awareness.d.ts +33 -0
- package/dist/bind-awareness.d.ts.map +1 -0
- package/dist/bind-awareness.js +125 -0
- package/dist/bind-awareness.js.map +1 -0
- package/dist/bind-editor.d.ts +34 -0
- package/dist/bind-editor.d.ts.map +1 -0
- package/dist/bind-editor.js +87 -0
- package/dist/bind-editor.js.map +1 -0
- package/dist/branch-doc.d.ts +53 -0
- package/dist/branch-doc.d.ts.map +1 -0
- package/dist/branch-doc.js +188 -0
- package/dist/branch-doc.js.map +1 -0
- package/dist/constants.d.ts +26 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +26 -0
- package/dist/constants.js.map +1 -0
- package/dist/encryption.d.ts +74 -0
- package/dist/encryption.d.ts.map +1 -0
- package/dist/encryption.js +124 -0
- package/dist/encryption.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/mentions.d.ts +33 -0
- package/dist/mentions.d.ts.map +1 -0
- package/dist/mentions.js +57 -0
- package/dist/mentions.js.map +1 -0
- package/dist/merge.d.ts +59 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +2 -0
- package/dist/merge.js.map +1 -0
- package/dist/scene-doc.d.ts +41 -0
- package/dist/scene-doc.d.ts.map +1 -0
- package/dist/scene-doc.js +111 -0
- package/dist/scene-doc.js.map +1 -0
- package/dist/transport-provider.d.ts +21 -0
- package/dist/transport-provider.d.ts.map +1 -0
- package/dist/transport-provider.js +105 -0
- package/dist/transport-provider.js.map +1 -0
- package/dist/yjs-history.d.ts +68 -0
- package/dist/yjs-history.d.ts.map +1 -0
- package/dist/yjs-history.js +215 -0
- package/dist/yjs-history.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { getElement, getElementWorldBounds } from "@oh-just-another/scene";
|
|
2
|
+
import { PEER_CURSOR_BROADCAST_INTERVAL_MS, } from "@oh-just-another/state";
|
|
3
|
+
/**
|
|
4
|
+
* Wire an `Editor` to a `CollabAwareness`:
|
|
5
|
+
*
|
|
6
|
+
* - publishes the local user payload + cursor + selection into
|
|
7
|
+
* awareness, throttled so a fast drag does not flood the room;
|
|
8
|
+
* - pushes the room's other-peer cursors / selections back into the
|
|
9
|
+
* editor so the overlay paints them.
|
|
10
|
+
*
|
|
11
|
+
* Returns an unbind function. Clearing the editor selection or cursor
|
|
12
|
+
* leaves awareness in a sane state.
|
|
13
|
+
*
|
|
14
|
+
* The editor exposes `onCursorMove` / `setPeerCursors` /
|
|
15
|
+
* `setPeerSelections` as the extension points; `@state` knows nothing
|
|
16
|
+
* about Yjs / awareness.
|
|
17
|
+
*/
|
|
18
|
+
export const bindAwareness = (editor, awareness, options) => {
|
|
19
|
+
const throttle = options.cursorThrottleMs ?? PEER_CURSOR_BROADCAST_INTERVAL_MS;
|
|
20
|
+
let lastBroadcastAt = 0;
|
|
21
|
+
let pendingCursor = null;
|
|
22
|
+
let flushTimer = null;
|
|
23
|
+
// Initial identity push so peers see us before our first move.
|
|
24
|
+
awareness.updateLocal({ user: options.user });
|
|
25
|
+
const broadcastNow = () => {
|
|
26
|
+
if (!pendingCursor)
|
|
27
|
+
return;
|
|
28
|
+
awareness.updateLocal({ cursor: pendingCursor });
|
|
29
|
+
lastBroadcastAt = Date.now();
|
|
30
|
+
pendingCursor = null;
|
|
31
|
+
flushTimer = null;
|
|
32
|
+
};
|
|
33
|
+
const unsubscribeCursor = editor.onCursorMove((point) => {
|
|
34
|
+
pendingCursor = point;
|
|
35
|
+
const elapsed = Date.now() - lastBroadcastAt;
|
|
36
|
+
if (elapsed >= throttle) {
|
|
37
|
+
broadcastNow();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
flushTimer ??= setTimeout(broadcastNow, throttle - elapsed);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Local selection → awareness, with a side effect: any editor change
|
|
44
|
+
// (selection OR scene mutation) re-resolves peer selection bounds so
|
|
45
|
+
// a peer's halo follows their selected shapes as they get edited.
|
|
46
|
+
let lastSelectionKey = null;
|
|
47
|
+
const unsubscribeSelection = editor.subscribe(() => {
|
|
48
|
+
const ids = [...editor.selection].sort();
|
|
49
|
+
const key = ids.join(",");
|
|
50
|
+
if (key !== lastSelectionKey) {
|
|
51
|
+
lastSelectionKey = key;
|
|
52
|
+
awareness.updateLocal({ selection: ids });
|
|
53
|
+
}
|
|
54
|
+
// Re-resolve peer selection bounds from the current scene.
|
|
55
|
+
refreshPeerSelections();
|
|
56
|
+
});
|
|
57
|
+
// Awareness → editor. Each change recomputes both cursors and
|
|
58
|
+
// selections from the room state and pushes the materialised arrays
|
|
59
|
+
// to the editor (which paints them on the overlay).
|
|
60
|
+
let lastPeers = awareness.getPeers();
|
|
61
|
+
const applyPeers = (peers) => {
|
|
62
|
+
lastPeers = peers;
|
|
63
|
+
const cursors = [];
|
|
64
|
+
const selections = [];
|
|
65
|
+
for (const p of peers) {
|
|
66
|
+
if (p.clientId === awareness.clientId)
|
|
67
|
+
continue;
|
|
68
|
+
if (p.cursor) {
|
|
69
|
+
cursors.push({
|
|
70
|
+
position: p.cursor,
|
|
71
|
+
color: p.user.color,
|
|
72
|
+
name: p.user.name,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (p.selection && p.selection.length > 0) {
|
|
76
|
+
const bounds = resolveSelectionBounds(editor, p.selection);
|
|
77
|
+
if (bounds.length > 0) {
|
|
78
|
+
selections.push({ color: p.user.color, bounds });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
editor.setPeerCursors(cursors);
|
|
83
|
+
editor.setPeerSelections(selections);
|
|
84
|
+
};
|
|
85
|
+
// Re-resolve only the selections (cursors don't depend on scene state).
|
|
86
|
+
// Called when the editor scene changes — peer halos should track shape
|
|
87
|
+
// movement / resize made by anyone.
|
|
88
|
+
const refreshPeerSelections = () => {
|
|
89
|
+
const selections = [];
|
|
90
|
+
for (const p of lastPeers) {
|
|
91
|
+
if (p.clientId === awareness.clientId)
|
|
92
|
+
continue;
|
|
93
|
+
if (!p.selection || p.selection.length === 0)
|
|
94
|
+
continue;
|
|
95
|
+
const bounds = resolveSelectionBounds(editor, p.selection);
|
|
96
|
+
if (bounds.length > 0)
|
|
97
|
+
selections.push({ color: p.user.color, bounds });
|
|
98
|
+
}
|
|
99
|
+
editor.setPeerSelections(selections);
|
|
100
|
+
};
|
|
101
|
+
// Push current snapshot immediately so a late-binding sees existing peers.
|
|
102
|
+
applyPeers(lastPeers);
|
|
103
|
+
const unsubscribeAwareness = awareness.onPeers(applyPeers);
|
|
104
|
+
return () => {
|
|
105
|
+
unsubscribeCursor();
|
|
106
|
+
unsubscribeSelection();
|
|
107
|
+
unsubscribeAwareness();
|
|
108
|
+
if (flushTimer !== null)
|
|
109
|
+
clearTimeout(flushTimer);
|
|
110
|
+
awareness.updateLocal({ cursor: null, selection: null });
|
|
111
|
+
editor.setPeerCursors([]);
|
|
112
|
+
editor.setPeerSelections([]);
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
const resolveSelectionBounds = (editor, ids) => {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const id of ids) {
|
|
118
|
+
const shape = getElement(editor.scene, id);
|
|
119
|
+
if (!shape)
|
|
120
|
+
continue;
|
|
121
|
+
out.push(getElementWorldBounds(shape));
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=bind-awareness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bind-awareness.js","sourceRoot":"","sources":["../src/bind-awareness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type * as Y from "yjs";
|
|
2
|
+
import type { Editor } from "@oh-just-another/state";
|
|
3
|
+
import { SceneDoc } from "./scene-doc.js";
|
|
4
|
+
export interface BindEditorOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Milliseconds to wait for a remote sync response before deciding to
|
|
7
|
+
* seed the CRDT from the local scene. Defaults to 0 (no wait — the
|
|
8
|
+
* connecting tab seeds immediately if the doc looks empty).
|
|
9
|
+
*
|
|
10
|
+
* Set this to a small value (e.g. 200 ms) in transports where peers
|
|
11
|
+
* may answer asynchronously (BroadcastChannel, WebSocket). Joiners
|
|
12
|
+
* will then adopt the room's existing state instead of clobbering it
|
|
13
|
+
* with their own.
|
|
14
|
+
*/
|
|
15
|
+
readonly waitForSyncMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Wire an `Editor` to a `SceneDoc` (or raw `Y.Doc`) so that local
|
|
19
|
+
* changes propagate to the CRDT and remote CRDT changes feed back into
|
|
20
|
+
* the editor.
|
|
21
|
+
*
|
|
22
|
+
* Sync handshake: when `waitForSyncMs > 0` and the CRDT looks empty
|
|
23
|
+
* at bind-time, we wait that long for the first inbound update. If it
|
|
24
|
+
* arrives, the editor adopts the room's state (`loadScene(snapshot)`).
|
|
25
|
+
* If the timeout elapses (we're the only peer), we seed the room with
|
|
26
|
+
* the editor's current scene.
|
|
27
|
+
*
|
|
28
|
+
* Self-origin transactions are tagged so the local subscriber doesn't
|
|
29
|
+
* apply its own change twice.
|
|
30
|
+
*
|
|
31
|
+
* Returns an unbind function.
|
|
32
|
+
*/
|
|
33
|
+
export declare const bindEditor: (editor: Editor, source: SceneDoc | Y.Doc, options?: BindEditorOptions) => (() => void);
|
|
34
|
+
//# sourceMappingURL=bind-editor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bind-editor.d.ts","sourceRoot":"","sources":["../src/bind-editor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,GACrB,QAAQ,MAAM,EACd,QAAQ,QAAQ,GAAG,CAAC,CAAC,GAAG,EACxB,UAAS,iBAAsB,KAC9B,CAAC,MAAM,IAAI,CAqEb,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { SceneDoc } from "./scene-doc.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wire an `Editor` to a `SceneDoc` (or raw `Y.Doc`) so that local
|
|
4
|
+
* changes propagate to the CRDT and remote CRDT changes feed back into
|
|
5
|
+
* the editor.
|
|
6
|
+
*
|
|
7
|
+
* Sync handshake: when `waitForSyncMs > 0` and the CRDT looks empty
|
|
8
|
+
* at bind-time, we wait that long for the first inbound update. If it
|
|
9
|
+
* arrives, the editor adopts the room's state (`loadScene(snapshot)`).
|
|
10
|
+
* If the timeout elapses (we're the only peer), we seed the room with
|
|
11
|
+
* the editor's current scene.
|
|
12
|
+
*
|
|
13
|
+
* Self-origin transactions are tagged so the local subscriber doesn't
|
|
14
|
+
* apply its own change twice.
|
|
15
|
+
*
|
|
16
|
+
* Returns an unbind function.
|
|
17
|
+
*/
|
|
18
|
+
export const bindEditor = (editor, source, options = {}) => {
|
|
19
|
+
const sceneDoc = source instanceof SceneDoc ? source : new SceneDoc(source);
|
|
20
|
+
const origin = Symbol("editor-binding");
|
|
21
|
+
const waitMs = options.waitForSyncMs ?? 0;
|
|
22
|
+
let lastSyncedScene = editor.scene;
|
|
23
|
+
let disposed = false;
|
|
24
|
+
// Local change → CRDT.
|
|
25
|
+
const unsubscribeLocal = editor.subscribe(() => {
|
|
26
|
+
if (disposed)
|
|
27
|
+
return;
|
|
28
|
+
if (editor.scene === lastSyncedScene)
|
|
29
|
+
return;
|
|
30
|
+
sceneDoc.applyDelta(lastSyncedScene, editor.scene, origin);
|
|
31
|
+
lastSyncedScene = editor.scene;
|
|
32
|
+
});
|
|
33
|
+
// CRDT change → local. Filter out self-origin updates by transaction tag.
|
|
34
|
+
const onUpdate = (_update, originOfUpdate) => {
|
|
35
|
+
if (disposed || originOfUpdate === origin)
|
|
36
|
+
return;
|
|
37
|
+
const snapshot = sceneDoc.snapshot();
|
|
38
|
+
editor.loadScene(snapshot, { preserveHistory: true });
|
|
39
|
+
lastSyncedScene = editor.scene;
|
|
40
|
+
};
|
|
41
|
+
sceneDoc.doc.on("update", onUpdate);
|
|
42
|
+
// Initial seed vs adopt.
|
|
43
|
+
const isCurrentlyEmpty = () => sceneDoc.elements.size === 0 && sceneDoc.links.size === 0 && sceneDoc.layers.size === 0;
|
|
44
|
+
const seedFromEditor = () => {
|
|
45
|
+
if (disposed)
|
|
46
|
+
return;
|
|
47
|
+
sceneDoc.replace(editor.scene, origin);
|
|
48
|
+
lastSyncedScene = editor.scene;
|
|
49
|
+
};
|
|
50
|
+
const adoptFromCrdt = () => {
|
|
51
|
+
if (disposed)
|
|
52
|
+
return;
|
|
53
|
+
const snapshot = sceneDoc.snapshot();
|
|
54
|
+
editor.loadScene(snapshot);
|
|
55
|
+
lastSyncedScene = editor.scene;
|
|
56
|
+
};
|
|
57
|
+
if (!isCurrentlyEmpty()) {
|
|
58
|
+
adoptFromCrdt();
|
|
59
|
+
}
|
|
60
|
+
else if (waitMs <= 0) {
|
|
61
|
+
seedFromEditor();
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Wait briefly for a peer to answer the implicit sync request that the
|
|
65
|
+
// TransportProvider sent on connect. If anything lands in the doc
|
|
66
|
+
// during that window, adopt; otherwise seed.
|
|
67
|
+
const timer = setTimeout(() => {
|
|
68
|
+
if (disposed)
|
|
69
|
+
return;
|
|
70
|
+
if (isCurrentlyEmpty())
|
|
71
|
+
seedFromEditor();
|
|
72
|
+
// Else: an update arrived → onUpdate already called loadScene.
|
|
73
|
+
}, waitMs);
|
|
74
|
+
return () => {
|
|
75
|
+
disposed = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
unsubscribeLocal();
|
|
78
|
+
sceneDoc.doc.off("update", onUpdate);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return () => {
|
|
82
|
+
disposed = true;
|
|
83
|
+
unsubscribeLocal();
|
|
84
|
+
sceneDoc.doc.off("update", onUpdate);
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=bind-editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bind-editor.js","sourceRoot":"","sources":["../src/bind-editor.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAgB1C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,MAAc,EACd,MAAwB,EACxB,UAA6B,EAAE,EACjB,EAAE;IAChB,MAAM,QAAQ,GAAG,MAAM,YAAY,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,IAAI,CAAC,CAAC;IAE1C,IAAI,eAAe,GAAU,MAAM,CAAC,KAAK,CAAC;IAC1C,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,uBAAuB;IACvB,MAAM,gBAAgB,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE;QAC7C,IAAI,QAAQ;YAAE,OAAO;QACrB,IAAI,MAAM,CAAC,KAAK,KAAK,eAAe;YAAE,OAAO;QAC7C,QAAQ,CAAC,UAAU,CAAC,eAAe,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC3D,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,0EAA0E;IAC1E,MAAM,QAAQ,GAAG,CAAC,OAAmB,EAAE,cAAuB,EAAQ,EAAE;QACtE,IAAI,QAAQ,IAAI,cAAc,KAAK,MAAM;YAAE,OAAO;QAClD,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACrC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC;IACjC,CAAC,CAAC;IACF,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAEpC,yBAAyB;IACzB,MAAM,gBAAgB,GAAG,GAAY,EAAE,CACrC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC;IAE1F,MAAM,cAAc,GAAG,GAAS,EAAE;QAChC,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACvC,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC;IACjC,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,GAAS,EAAE;QAC/B,IAAI,QAAQ;YAAE,OAAO;QACrB,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACrC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC3B,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC;IACjC,CAAC,CAAC;IAEF,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;QACxB,aAAa,EAAE,CAAC;IAClB,CAAC;SAAM,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QACvB,cAAc,EAAE,CAAC;IACnB,CAAC;SAAM,CAAC;QACN,uEAAuE;QACvE,kEAAkE;QAClE,6CAA6C;QAC7C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,QAAQ;gBAAE,OAAO;YACrB,IAAI,gBAAgB,EAAE;gBAAE,cAAc,EAAE,CAAC;YACzC,+DAA+D;QACjE,CAAC,EAAE,MAAM,CAAC,CAAC;QAEX,OAAO,GAAG,EAAE;YACV,QAAQ,GAAG,IAAI,CAAC;YAChB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,gBAAgB,EAAE,CAAC;YACnB,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACvC,CAAC,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,EAAE;QACV,QAAQ,GAAG,IAAI,CAAC;QAChB,gBAAgB,EAAE,CAAC;QACnB,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACvC,CAAC,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import type { Scene } from "@oh-just-another/scene";
|
|
3
|
+
import { SceneDoc } from "./scene-doc.js";
|
|
4
|
+
import type { BranchId, BranchMergeAPI, ConflictResolution, MergeReport } from "./merge.js";
|
|
5
|
+
export declare class BranchDoc implements BranchMergeAPI {
|
|
6
|
+
readonly doc: Y.Doc;
|
|
7
|
+
private readonly branches;
|
|
8
|
+
private readonly subdocs;
|
|
9
|
+
private readonly sceneDocs;
|
|
10
|
+
constructor(doc?: Y.Doc);
|
|
11
|
+
/**
|
|
12
|
+
* Create the root branch from an initial scene. Idempotent —
|
|
13
|
+
* if `id` already exists, returns the existing metadata
|
|
14
|
+
* unchanged.
|
|
15
|
+
*/
|
|
16
|
+
ensureRoot(id: string, name: string, initialScene: Scene): BranchId;
|
|
17
|
+
/**
|
|
18
|
+
* Fork a new branch from an existing one. The new subdoc starts
|
|
19
|
+
* with a copy of the parent's current scene; that scene is also
|
|
20
|
+
* remembered as the common ancestor for future merges back into
|
|
21
|
+
* any other branch.
|
|
22
|
+
*/
|
|
23
|
+
createBranch(id: string, name: string, parentBranchId: string): BranchId;
|
|
24
|
+
/** SceneDoc for the named branch (throws when unknown). */
|
|
25
|
+
sceneDocFor(branchId: string): SceneDoc;
|
|
26
|
+
/** Y.Doc subdoc passthrough — required by `BranchMergeAPI`. */
|
|
27
|
+
branchToDoc(branchId: BranchId): Y.Doc;
|
|
28
|
+
/**
|
|
29
|
+
* Merge `source` into `target`. Uses the source branch's stored
|
|
30
|
+
* ancestor as the three-way merge base. Auto-applied changes
|
|
31
|
+
* land in the returned `autoMerged` scene; conflicts wait for
|
|
32
|
+
* `applyConflictResolution`.
|
|
33
|
+
*/
|
|
34
|
+
mergeBranch(source: BranchId, target: BranchId): Promise<MergeReport>;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a merge report and return the final scene. Pure —
|
|
37
|
+
* does NOT push the result back into any branch's subdoc; the
|
|
38
|
+
* host commits it by calling `sceneDocFor(targetId).replace(
|
|
39
|
+
* finalScene)` when it wants to materialise the merge.
|
|
40
|
+
*/
|
|
41
|
+
applyConflictResolution(report: MergeReport, resolutions: readonly ConflictResolution[]): Promise<Scene>;
|
|
42
|
+
/**
|
|
43
|
+
* Commit `scene` into `targetBranchId`'s subdoc and re-baseline
|
|
44
|
+
* the source branch's ancestor so a follow-up merge starts from
|
|
45
|
+
* the post-merge state. Call after `applyConflictResolution`
|
|
46
|
+
* when the host is happy with the result.
|
|
47
|
+
*/
|
|
48
|
+
commitMerge(sourceBranchId: string, targetBranchId: string, mergedScene: Scene): void;
|
|
49
|
+
private requireBranch;
|
|
50
|
+
private requireSceneDoc;
|
|
51
|
+
private requireSubdoc;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=branch-doc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-doc.d.ts","sourceRoot":"","sources":["../src/branch-doc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAKpD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,kBAAkB,EAElB,WAAW,EACZ,MAAM,YAAY,CAAC;AAwCpB,qBAAa,SAAU,YAAW,cAAc;IAC9C,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA+B;gBAE7C,GAAG,GAAE,CAAC,CAAC,GAAiB;IAKpC;;;;OAIG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,GAAG,QAAQ;IAyBnE;;;;;OAKG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,QAAQ;IAoBxE,2DAA2D;IAC3D,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ;IAIvC,+DAA+D;IAC/D,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,CAAC,CAAC,GAAG;IAItC;;;;;OAKG;IACH,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC;IA6BrE;;;;;OAKG;IACH,uBAAuB,CACrB,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,SAAS,kBAAkB,EAAE,GACzC,OAAO,CAAC,KAAK,CAAC;IAoBjB;;;;;OAKG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,GAAG,IAAI;IAUrF,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,aAAa;CAUtB"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { applyConflictResolutions, mergeScenesThreeWay, } from "@oh-just-another/scene";
|
|
3
|
+
import { SceneDoc } from "./scene-doc.js";
|
|
4
|
+
export class BranchDoc {
|
|
5
|
+
doc;
|
|
6
|
+
branches;
|
|
7
|
+
subdocs = new Map();
|
|
8
|
+
sceneDocs = new Map();
|
|
9
|
+
constructor(doc = new Y.Doc()) {
|
|
10
|
+
this.doc = doc;
|
|
11
|
+
this.branches = doc.getMap("branches");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create the root branch from an initial scene. Idempotent —
|
|
15
|
+
* if `id` already exists, returns the existing metadata
|
|
16
|
+
* unchanged.
|
|
17
|
+
*/
|
|
18
|
+
ensureRoot(id, name, initialScene) {
|
|
19
|
+
const existing = this.branches.get(id);
|
|
20
|
+
if (existing) {
|
|
21
|
+
return {
|
|
22
|
+
id: existing.id,
|
|
23
|
+
name: existing.name,
|
|
24
|
+
parentVersionId: existing.parentBranchId,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const subdoc = new Y.Doc();
|
|
28
|
+
const sceneDoc = new SceneDoc(subdoc);
|
|
29
|
+
sceneDoc.replace(initialScene, "branch-init");
|
|
30
|
+
const meta = {
|
|
31
|
+
id,
|
|
32
|
+
name,
|
|
33
|
+
parentBranchId: null,
|
|
34
|
+
subdocGuid: subdoc.guid,
|
|
35
|
+
ancestorScene: cloneScene(initialScene),
|
|
36
|
+
};
|
|
37
|
+
this.branches.set(id, meta);
|
|
38
|
+
this.subdocs.set(id, subdoc);
|
|
39
|
+
this.sceneDocs.set(id, sceneDoc);
|
|
40
|
+
return { id, name, parentVersionId: null };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fork a new branch from an existing one. The new subdoc starts
|
|
44
|
+
* with a copy of the parent's current scene; that scene is also
|
|
45
|
+
* remembered as the common ancestor for future merges back into
|
|
46
|
+
* any other branch.
|
|
47
|
+
*/
|
|
48
|
+
createBranch(id, name, parentBranchId) {
|
|
49
|
+
const parent = this.requireBranch(parentBranchId);
|
|
50
|
+
const parentDoc = this.requireSceneDoc(parentBranchId);
|
|
51
|
+
const snapshot = parentDoc.snapshot();
|
|
52
|
+
const subdoc = new Y.Doc();
|
|
53
|
+
const sceneDoc = new SceneDoc(subdoc);
|
|
54
|
+
sceneDoc.replace(snapshot, "branch-fork");
|
|
55
|
+
const meta = {
|
|
56
|
+
id,
|
|
57
|
+
name,
|
|
58
|
+
parentBranchId: parent.id,
|
|
59
|
+
subdocGuid: subdoc.guid,
|
|
60
|
+
ancestorScene: cloneScene(snapshot),
|
|
61
|
+
};
|
|
62
|
+
this.branches.set(id, meta);
|
|
63
|
+
this.subdocs.set(id, subdoc);
|
|
64
|
+
this.sceneDocs.set(id, sceneDoc);
|
|
65
|
+
return { id, name, parentVersionId: parent.id };
|
|
66
|
+
}
|
|
67
|
+
/** SceneDoc for the named branch (throws when unknown). */
|
|
68
|
+
sceneDocFor(branchId) {
|
|
69
|
+
return this.requireSceneDoc(branchId);
|
|
70
|
+
}
|
|
71
|
+
/** Y.Doc subdoc passthrough — required by `BranchMergeAPI`. */
|
|
72
|
+
branchToDoc(branchId) {
|
|
73
|
+
return this.requireSubdoc(branchId.id);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Merge `source` into `target`. Uses the source branch's stored
|
|
77
|
+
* ancestor as the three-way merge base. Auto-applied changes
|
|
78
|
+
* land in the returned `autoMerged` scene; conflicts wait for
|
|
79
|
+
* `applyConflictResolution`.
|
|
80
|
+
*/
|
|
81
|
+
mergeBranch(source, target) {
|
|
82
|
+
const sMeta = this.requireBranch(source.id);
|
|
83
|
+
const sourceScene = this.requireSceneDoc(source.id).snapshot();
|
|
84
|
+
const targetScene = this.requireSceneDoc(target.id).snapshot();
|
|
85
|
+
// Yjs snapshot allocates fresh shape objects every call — reference
|
|
86
|
+
// equality (the algorithm's default) can't tell "unchanged" from
|
|
87
|
+
// "edited" across two snapshots. JSON.stringify is good enough for
|
|
88
|
+
// plain-object shapes, which is what the kernel ships.
|
|
89
|
+
const report = mergeScenesThreeWay(sMeta.ancestorScene, sourceScene, targetScene, {
|
|
90
|
+
compareElements: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
|
91
|
+
});
|
|
92
|
+
const applied = [];
|
|
93
|
+
for (const [id, shape] of report.autoMerged.elements) {
|
|
94
|
+
const prev = targetScene.elements.get(id);
|
|
95
|
+
if (prev !== shape)
|
|
96
|
+
applied.push(id);
|
|
97
|
+
}
|
|
98
|
+
const conflicts = report.conflicts.map((c) => ({
|
|
99
|
+
elementId: c.elementId,
|
|
100
|
+
base: c.base,
|
|
101
|
+
source: c.source,
|
|
102
|
+
target: c.target,
|
|
103
|
+
}));
|
|
104
|
+
return Promise.resolve({
|
|
105
|
+
applied,
|
|
106
|
+
conflicts,
|
|
107
|
+
autoMerged: report.autoMerged,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolve a merge report and return the final scene. Pure —
|
|
112
|
+
* does NOT push the result back into any branch's subdoc; the
|
|
113
|
+
* host commits it by calling `sceneDocFor(targetId).replace(
|
|
114
|
+
* finalScene)` when it wants to materialise the merge.
|
|
115
|
+
*/
|
|
116
|
+
applyConflictResolution(report, resolutions) {
|
|
117
|
+
return Promise.resolve(applyConflictResolutions({
|
|
118
|
+
autoMerged: report.autoMerged,
|
|
119
|
+
conflicts: report.conflicts.map((c) => ({
|
|
120
|
+
elementId: c.elementId,
|
|
121
|
+
base: c.base,
|
|
122
|
+
source: c.source,
|
|
123
|
+
target: c.target,
|
|
124
|
+
})),
|
|
125
|
+
}, resolutions.map((r) => ({
|
|
126
|
+
elementId: r.elementId,
|
|
127
|
+
choice: r.choice === "ours" ? "ours" : r.choice === "theirs" ? "theirs" : "both",
|
|
128
|
+
}))));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Commit `scene` into `targetBranchId`'s subdoc and re-baseline
|
|
132
|
+
* the source branch's ancestor so a follow-up merge starts from
|
|
133
|
+
* the post-merge state. Call after `applyConflictResolution`
|
|
134
|
+
* when the host is happy with the result.
|
|
135
|
+
*/
|
|
136
|
+
commitMerge(sourceBranchId, targetBranchId, mergedScene) {
|
|
137
|
+
const targetDoc = this.requireSceneDoc(targetBranchId);
|
|
138
|
+
targetDoc.replace(mergedScene, "branch-merge");
|
|
139
|
+
const sMeta = this.requireBranch(sourceBranchId);
|
|
140
|
+
this.branches.set(sourceBranchId, {
|
|
141
|
+
...sMeta,
|
|
142
|
+
ancestorScene: cloneScene(mergedScene),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
requireBranch(id) {
|
|
146
|
+
const m = this.branches.get(id);
|
|
147
|
+
if (!m)
|
|
148
|
+
throw new Error(`BranchDoc: unknown branch "${id}"`);
|
|
149
|
+
return m;
|
|
150
|
+
}
|
|
151
|
+
requireSceneDoc(id) {
|
|
152
|
+
const cached = this.sceneDocs.get(id);
|
|
153
|
+
if (cached)
|
|
154
|
+
return cached;
|
|
155
|
+
// Subdoc was created in a different session — rehydrate.
|
|
156
|
+
const subdoc = this.requireSubdoc(id);
|
|
157
|
+
const sceneDoc = new SceneDoc(subdoc);
|
|
158
|
+
this.sceneDocs.set(id, sceneDoc);
|
|
159
|
+
return sceneDoc;
|
|
160
|
+
}
|
|
161
|
+
requireSubdoc(id) {
|
|
162
|
+
const cached = this.subdocs.get(id);
|
|
163
|
+
if (cached)
|
|
164
|
+
return cached;
|
|
165
|
+
const meta = this.requireBranch(id);
|
|
166
|
+
// Create the subdoc shell with the recorded guid so peers
|
|
167
|
+
// converge on the same doc id across sessions.
|
|
168
|
+
const subdoc = new Y.Doc({ guid: meta.subdocGuid });
|
|
169
|
+
this.subdocs.set(id, subdoc);
|
|
170
|
+
return subdoc;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Deep clone a scene. The Yjs subdoc owns its own copies of
|
|
175
|
+
* shapes / edges / layers, so the ancestor snapshot has to be a
|
|
176
|
+
* detached deep clone — otherwise editor mutations would silently
|
|
177
|
+
* mutate the ancestor and the three-way merge would always see
|
|
178
|
+
* "no changes" on the source side.
|
|
179
|
+
*/
|
|
180
|
+
const cloneScene = (scene) => ({
|
|
181
|
+
elements: new Map([...scene.elements].map(([id, shape]) => [id, structuredClone(shape)])),
|
|
182
|
+
links: new Map([...scene.links].map(([id, edge]) => [id, structuredClone(edge)])),
|
|
183
|
+
layers: new Map([...scene.layers].map(([id, layer]) => [id, structuredClone(layer)])),
|
|
184
|
+
annotations: new Map([...scene.annotations].map(([id, ann]) => [id, structuredClone(ann)])),
|
|
185
|
+
files: new Map([...scene.files].map(([id, file]) => [id, structuredClone(file)])),
|
|
186
|
+
viewport: structuredClone(scene.viewport),
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=branch-doc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-doc.js","sourceRoot":"","sources":["../src/branch-doc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tunable thresholds for the collab package.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Length of the public room identifier in bytes (hex string =
|
|
6
|
+
* 2× chars). 10 bytes = 80 bits = ~1 in 2^80 collision odds at
|
|
7
|
+
* realistic concurrency; matches standard's 20-hex format.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ROOM_ID_BYTES = 10;
|
|
10
|
+
/**
|
|
11
|
+
* AES key length in bits. AES-128 is the modern default for
|
|
12
|
+
* symmetric session keys — fast on every platform and
|
|
13
|
+
* cryptographically equivalent to AES-256 against any practical
|
|
14
|
+
* attacker (the 256-bit version's marginal extra security
|
|
15
|
+
* matters only for offline attacks against a long-lived key,
|
|
16
|
+
* not for ephemeral session keys).
|
|
17
|
+
*/
|
|
18
|
+
export declare const ENCRYPTION_KEY_BITS = 128;
|
|
19
|
+
/**
|
|
20
|
+
* Initialisation-vector length for AES-GCM. NIST SP 800-38D
|
|
21
|
+
* recommends 96 bits (12 bytes) — the construction is most
|
|
22
|
+
* efficient at that length, longer IVs go through an extra
|
|
23
|
+
* hashing step.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ENCRYPTION_IV_BYTES = 12;
|
|
26
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tunable thresholds for the collab package.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Length of the public room identifier in bytes (hex string =
|
|
6
|
+
* 2× chars). 10 bytes = 80 bits = ~1 in 2^80 collision odds at
|
|
7
|
+
* realistic concurrency; matches standard's 20-hex format.
|
|
8
|
+
*/
|
|
9
|
+
export const ROOM_ID_BYTES = 10;
|
|
10
|
+
/**
|
|
11
|
+
* AES key length in bits. AES-128 is the modern default for
|
|
12
|
+
* symmetric session keys — fast on every platform and
|
|
13
|
+
* cryptographically equivalent to AES-256 against any practical
|
|
14
|
+
* attacker (the 256-bit version's marginal extra security
|
|
15
|
+
* matters only for offline attacks against a long-lived key,
|
|
16
|
+
* not for ephemeral session keys).
|
|
17
|
+
*/
|
|
18
|
+
export const ENCRYPTION_KEY_BITS = 128;
|
|
19
|
+
/**
|
|
20
|
+
* Initialisation-vector length for AES-GCM. NIST SP 800-38D
|
|
21
|
+
* recommends 96 bits (12 bytes) — the construction is most
|
|
22
|
+
* efficient at that length, longer IVs go through an extra
|
|
23
|
+
* hashing step.
|
|
24
|
+
*/
|
|
25
|
+
export const ENCRYPTION_IV_BYTES = 12;
|
|
26
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC;AAEhC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Transport } from "@oh-just-another/network";
|
|
2
|
+
/**
|
|
3
|
+
* Client-side AES-GCM encryption for collab transports. Mirrors
|
|
4
|
+
* standard's "URL fragment carries the key" model: the secret
|
|
5
|
+
* never reaches the relay, so a blind fan-out server can route
|
|
6
|
+
* payloads without ever decrypting them.
|
|
7
|
+
*
|
|
8
|
+
* URL convention (set by the host app, parsed in `apps/diagram/src/collab.ts`):
|
|
9
|
+
*
|
|
10
|
+
* /#room=<roomId>,<keyBase64>
|
|
11
|
+
*
|
|
12
|
+
* `roomId` goes in the WebSocket pathname (`/<roomId>`) so the
|
|
13
|
+
* relay can route by room. `keyBase64` stays in the URL fragment
|
|
14
|
+
* which the browser does not transmit on any request — including
|
|
15
|
+
* the WebSocket upgrade — so the server never sees it.
|
|
16
|
+
*
|
|
17
|
+
* Wire format per frame:
|
|
18
|
+
*
|
|
19
|
+
* [ 12-byte IV ][ AES-GCM ciphertext + 16-byte auth tag ]
|
|
20
|
+
*
|
|
21
|
+
* IV is random per frame (WebCrypto / NIST guidance — never reuse
|
|
22
|
+
* an IV with the same key). The auth tag is part of the ciphertext
|
|
23
|
+
* tail produced by AES-GCM; tampered frames throw on decrypt.
|
|
24
|
+
*/
|
|
25
|
+
/** Strongly-typed result of {@link generateRoomKey}. */
|
|
26
|
+
export interface RoomCredentials {
|
|
27
|
+
/** Public room identifier — appears in the WS pathname, visible to relay. */
|
|
28
|
+
readonly roomId: string;
|
|
29
|
+
/**
|
|
30
|
+
* Base64url-encoded raw AES key. Goes into the URL fragment; share
|
|
31
|
+
* the full URL to invite peers. Never log this value.
|
|
32
|
+
*/
|
|
33
|
+
readonly keyBase64: string;
|
|
34
|
+
/** The imported `CryptoKey` ready for AES-GCM operations. */
|
|
35
|
+
readonly key: CryptoKey;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Mint a fresh credentials pair for a new collab session. Uses
|
|
39
|
+
* `crypto.getRandomValues` for the roomId and `crypto.subtle` for
|
|
40
|
+
* the AES key.
|
|
41
|
+
*/
|
|
42
|
+
export declare const generateRoomKey: () => Promise<RoomCredentials>;
|
|
43
|
+
/**
|
|
44
|
+
* Re-import a key the second peer parsed out of the URL fragment.
|
|
45
|
+
* Throws on malformed base64 / wrong length so the caller can
|
|
46
|
+
* surface "broken invite link" cleanly.
|
|
47
|
+
*/
|
|
48
|
+
export declare const importRoomKey: (keyBase64: string) => Promise<CryptoKey>;
|
|
49
|
+
/**
|
|
50
|
+
* Wrap any {@link Transport} so every outgoing payload is AES-GCM
|
|
51
|
+
* encrypted and every incoming payload is decrypted before reaching
|
|
52
|
+
* subscribers. The relay sees nothing but binary blobs.
|
|
53
|
+
*
|
|
54
|
+
* Decryption errors are silently dropped — a tampered or
|
|
55
|
+
* wrong-key frame should not surface as a noisy console error
|
|
56
|
+
* (that would let a malicious peer flood the log). Callers can opt
|
|
57
|
+
* into observability by passing `onDecryptError`.
|
|
58
|
+
*/
|
|
59
|
+
export declare class EncryptedTransport implements Transport {
|
|
60
|
+
private readonly inner;
|
|
61
|
+
private readonly key;
|
|
62
|
+
private readonly options;
|
|
63
|
+
private readonly handlers;
|
|
64
|
+
private unsubscribe;
|
|
65
|
+
constructor(inner: Transport, key: CryptoKey, options?: {
|
|
66
|
+
readonly onDecryptError?: (err: unknown) => void;
|
|
67
|
+
});
|
|
68
|
+
send(payload: Uint8Array): void;
|
|
69
|
+
onMessage(handler: (payload: Uint8Array) => void): () => void;
|
|
70
|
+
close(): void;
|
|
71
|
+
private encryptAndSend;
|
|
72
|
+
private decryptAndDispatch;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=encryption.d.ts.map
|