@dxos/echo-pipeline 0.3.11-main.cc2fe2c → 0.3.11-main.ccc0ca3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{chunk-MPBRK5OV.mjs → chunk-PB5T4DLC.mjs} +142 -17
- package/dist/lib/browser/{chunk-MPBRK5OV.mjs.map → chunk-PB5T4DLC.mjs.map} +3 -3
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +2 -2
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-GJQNRSA3.cjs → chunk-WGNVVL2H.cjs} +141 -19
- package/dist/lib/node/{chunk-GJQNRSA3.cjs.map → chunk-WGNVVL2H.cjs.map} +3 -3
- package/dist/lib/node/index.cjs +26 -26
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +17 -17
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/automerge/automerge-host.d.ts +9 -1
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +2 -2
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/package.json +33 -33
- package/src/automerge/automerge-host.test.ts +111 -2
- package/src/automerge/automerge-host.ts +99 -8
- package/src/space/space-manager.ts +3 -3
- package/src/testing/test-agent-builder.ts +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Trigger } from '@dxos/async';
|
|
5
6
|
import {
|
|
6
7
|
Repo,
|
|
7
8
|
NetworkAdapter,
|
|
@@ -15,12 +16,13 @@ import {
|
|
|
15
16
|
import { IndexedDBStorageAdapter } from '@dxos/automerge/automerge-repo-storage-indexeddb';
|
|
16
17
|
import { Stream } from '@dxos/codec-protobuf';
|
|
17
18
|
import { invariant } from '@dxos/invariant';
|
|
19
|
+
import { PublicKey } from '@dxos/keys';
|
|
18
20
|
import { log } from '@dxos/log';
|
|
19
21
|
import { type HostInfo, type SyncRepoRequest, type SyncRepoResponse } from '@dxos/protocols/proto/dxos/echo/service';
|
|
20
22
|
import { type PeerInfo } from '@dxos/protocols/proto/dxos/mesh/teleport/automerge';
|
|
21
23
|
import { StorageType, type Directory } from '@dxos/random-access-storage';
|
|
22
24
|
import { AutomergeReplicator } from '@dxos/teleport-extension-automerge-replicator';
|
|
23
|
-
import { arrayToBuffer, bufferToArray } from '@dxos/util';
|
|
25
|
+
import { ComplexMap, ComplexSet, arrayToBuffer, bufferToArray, defaultMap } from '@dxos/util';
|
|
24
26
|
|
|
25
27
|
export class AutomergeHost {
|
|
26
28
|
private readonly _repo: Repo;
|
|
@@ -28,6 +30,11 @@ export class AutomergeHost {
|
|
|
28
30
|
private readonly _clientNetwork: LocalHostNetworkAdapter;
|
|
29
31
|
private readonly _storage: StorageAdapter;
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* spaceKey -> deviceKey[]
|
|
35
|
+
*/
|
|
36
|
+
private readonly _authorizedDevices = new ComplexMap<PublicKey, ComplexSet<PublicKey>>(PublicKey.hash);
|
|
37
|
+
|
|
31
38
|
constructor(storageDirectory: Directory) {
|
|
32
39
|
this._meshNetwork = new MeshNetworkAdapter();
|
|
33
40
|
this._clientNetwork = new LocalHostNetworkAdapter();
|
|
@@ -38,11 +45,52 @@ export class AutomergeHost {
|
|
|
38
45
|
? new IndexedDBStorageAdapter(storageDirectory.path, 'data')
|
|
39
46
|
: new AutomergeStorageAdapter(storageDirectory);
|
|
40
47
|
this._repo = new Repo({
|
|
48
|
+
peerId: `host-${PublicKey.random().toHex()}` as PeerId,
|
|
41
49
|
network: [this._clientNetwork, this._meshNetwork],
|
|
42
50
|
storage: this._storage,
|
|
43
51
|
|
|
44
52
|
// TODO(dmaretskyi): Share based on HALO permissions and space affinity.
|
|
45
|
-
|
|
53
|
+
// Hosts, running in the worker, don't share documents unless requested by other peers.
|
|
54
|
+
sharePolicy: async (peerId /* device key */, documentId /* space key */) => {
|
|
55
|
+
if (peerId.startsWith('client-')) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!documentId) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const doc = this._repo.handles[documentId]?.docSync();
|
|
64
|
+
if (!doc) {
|
|
65
|
+
log('doc not found for share policy check', { peerId, documentId });
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (!doc.experimental_spaceKey) {
|
|
71
|
+
log.warn('space key not found for share policy check', { peerId, documentId });
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const spaceKey = PublicKey.from(doc.experimental_spaceKey);
|
|
76
|
+
const authorizedDevices = this._authorizedDevices.get(spaceKey);
|
|
77
|
+
|
|
78
|
+
// TODO(mykola): Hack, stop abusing `peerMetadata` field.
|
|
79
|
+
const deviceKeyHex = (this.repo.peerMetadataByPeerId[peerId] as any)?.dxos_deviceKey;
|
|
80
|
+
if (!deviceKeyHex) {
|
|
81
|
+
log.warn('device key not found for share policy check', { peerId, documentId });
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const deviceKey = PublicKey.from(deviceKeyHex);
|
|
85
|
+
|
|
86
|
+
const isAuthorized = authorizedDevices?.has(deviceKey) ?? false;
|
|
87
|
+
log.info('share policy check', { peerId, documentId, deviceKey, spaceKey, isAuthorized });
|
|
88
|
+
return isAuthorized;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log.catch(err);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}, // Share everything.
|
|
46
94
|
});
|
|
47
95
|
this._clientNetwork.ready();
|
|
48
96
|
this._meshNetwork.ready();
|
|
@@ -53,6 +101,7 @@ export class AutomergeHost {
|
|
|
53
101
|
}
|
|
54
102
|
|
|
55
103
|
async close() {
|
|
104
|
+
this._storage instanceof AutomergeStorageAdapter && (await this._storage.close());
|
|
56
105
|
await this._clientNetwork.close();
|
|
57
106
|
}
|
|
58
107
|
|
|
@@ -68,7 +117,7 @@ export class AutomergeHost {
|
|
|
68
117
|
return this._clientNetwork.sendSyncMessage(request);
|
|
69
118
|
}
|
|
70
119
|
|
|
71
|
-
getHostInfo(): HostInfo {
|
|
120
|
+
async getHostInfo(): Promise<HostInfo> {
|
|
72
121
|
return this._clientNetwork.getHostInfo();
|
|
73
122
|
}
|
|
74
123
|
|
|
@@ -79,6 +128,10 @@ export class AutomergeHost {
|
|
|
79
128
|
createExtension(): AutomergeReplicator {
|
|
80
129
|
return this._meshNetwork.createExtension();
|
|
81
130
|
}
|
|
131
|
+
|
|
132
|
+
authorizeDevice(spaceKey: PublicKey, deviceKey: PublicKey) {
|
|
133
|
+
defaultMap(this._authorizedDevices, spaceKey, () => new ComplexSet(PublicKey.hash)).add(deviceKey);
|
|
134
|
+
}
|
|
82
135
|
}
|
|
83
136
|
|
|
84
137
|
type ClientSyncState = {
|
|
@@ -104,8 +157,11 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
104
157
|
});
|
|
105
158
|
}
|
|
106
159
|
|
|
160
|
+
private _connected = new Trigger();
|
|
161
|
+
|
|
107
162
|
override connect(peerId: PeerId): void {
|
|
108
163
|
this.peerId = peerId;
|
|
164
|
+
this._connected.wake();
|
|
109
165
|
// No-op. Client always connects first
|
|
110
166
|
}
|
|
111
167
|
|
|
@@ -146,18 +202,26 @@ class LocalHostNetworkAdapter extends NetworkAdapter {
|
|
|
146
202
|
},
|
|
147
203
|
});
|
|
148
204
|
|
|
149
|
-
this.
|
|
150
|
-
|
|
151
|
-
|
|
205
|
+
this._connected
|
|
206
|
+
.wait({ timeout: 1_000 })
|
|
207
|
+
.then(() => {
|
|
208
|
+
this.emit('peer-candidate', {
|
|
209
|
+
peerMetadata: {},
|
|
210
|
+
peerId,
|
|
211
|
+
});
|
|
212
|
+
})
|
|
213
|
+
.catch((err) => log.catch(err));
|
|
152
214
|
});
|
|
153
215
|
}
|
|
154
216
|
|
|
155
217
|
async sendSyncMessage({ id, syncMessage }: SyncRepoRequest): Promise<void> {
|
|
218
|
+
await this._connected.wait({ timeout: 1_000 });
|
|
156
219
|
const message = cbor.decode(syncMessage!) as Message;
|
|
157
220
|
this.emit('message', message);
|
|
158
221
|
}
|
|
159
222
|
|
|
160
|
-
getHostInfo(): HostInfo {
|
|
223
|
+
async getHostInfo(): Promise<HostInfo> {
|
|
224
|
+
await this._connected.wait({ timeout: 1_000 });
|
|
161
225
|
invariant(this.peerId, 'Peer id not set.');
|
|
162
226
|
return {
|
|
163
227
|
peerId: this.peerId,
|
|
@@ -210,7 +274,7 @@ export class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
210
274
|
peerId: this.peerId,
|
|
211
275
|
},
|
|
212
276
|
{
|
|
213
|
-
onStartReplication: async (info) => {
|
|
277
|
+
onStartReplication: async (info, remotePeerId /** Teleport ID */) => {
|
|
214
278
|
// Note: We store only one extension per peer.
|
|
215
279
|
// There can be a case where two connected peers have more than one teleport connection between them
|
|
216
280
|
// and each of them uses different teleport connections to send messages.
|
|
@@ -224,6 +288,10 @@ export class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
224
288
|
// TODO(mykola): Fix race condition?
|
|
225
289
|
this._extensions.set(info.id, extension);
|
|
226
290
|
this.emit('peer-candidate', {
|
|
291
|
+
// TODO(mykola): Hack, stop abusing `peerMetadata` field.
|
|
292
|
+
peerMetadata: {
|
|
293
|
+
dxos_deviceKey: remotePeerId.toHex(),
|
|
294
|
+
} as any,
|
|
227
295
|
peerId: info.id as PeerId,
|
|
228
296
|
});
|
|
229
297
|
},
|
|
@@ -248,11 +316,18 @@ export class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
248
316
|
}
|
|
249
317
|
|
|
250
318
|
export class AutomergeStorageAdapter extends StorageAdapter {
|
|
319
|
+
// TODO(mykola): Hack for restricting automerge Repo to access storage if Host is `closed`.
|
|
320
|
+
// Automerge Repo do not have any lifetime management.
|
|
321
|
+
private _state: 'opened' | 'closed' = 'opened';
|
|
322
|
+
|
|
251
323
|
constructor(private readonly _directory: Directory) {
|
|
252
324
|
super();
|
|
253
325
|
}
|
|
254
326
|
|
|
255
327
|
override async load(key: StorageKey): Promise<Uint8Array | undefined> {
|
|
328
|
+
if (this._state !== 'opened') {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
256
331
|
const filename = this._getFilename(key);
|
|
257
332
|
const file = this._directory.getOrCreateFile(filename);
|
|
258
333
|
const { size } = await file.stat();
|
|
@@ -264,6 +339,9 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
264
339
|
}
|
|
265
340
|
|
|
266
341
|
override async save(key: StorageKey, data: Uint8Array): Promise<void> {
|
|
342
|
+
if (this._state !== 'opened') {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
267
345
|
const filename = this._getFilename(key);
|
|
268
346
|
const file = this._directory.getOrCreateFile(filename);
|
|
269
347
|
await file.write(0, arrayToBuffer(data));
|
|
@@ -273,6 +351,9 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
273
351
|
}
|
|
274
352
|
|
|
275
353
|
override async remove(key: StorageKey): Promise<void> {
|
|
354
|
+
if (this._state !== 'opened') {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
276
357
|
// TODO(dmaretskyi): Better deletion.
|
|
277
358
|
const filename = this._getFilename(key);
|
|
278
359
|
const file = this._directory.getOrCreateFile(filename);
|
|
@@ -280,6 +361,9 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
280
361
|
}
|
|
281
362
|
|
|
282
363
|
override async loadRange(keyPrefix: StorageKey): Promise<Chunk[]> {
|
|
364
|
+
if (this._state !== 'opened') {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
283
367
|
const filename = this._getFilename(keyPrefix);
|
|
284
368
|
const entries = await this._directory.list();
|
|
285
369
|
return Promise.all(
|
|
@@ -298,6 +382,9 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
298
382
|
}
|
|
299
383
|
|
|
300
384
|
override async removeRange(keyPrefix: StorageKey): Promise<void> {
|
|
385
|
+
if (this._state !== 'opened') {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
301
388
|
const filename = this._getFilename(keyPrefix);
|
|
302
389
|
const entries = await this._directory.list();
|
|
303
390
|
await Promise.all(
|
|
@@ -310,6 +397,10 @@ export class AutomergeStorageAdapter extends StorageAdapter {
|
|
|
310
397
|
);
|
|
311
398
|
}
|
|
312
399
|
|
|
400
|
+
async close(): Promise<void> {
|
|
401
|
+
this._state = 'closed';
|
|
402
|
+
}
|
|
403
|
+
|
|
313
404
|
private _getFilename(key: StorageKey): string {
|
|
314
405
|
return key.map((k) => k.replaceAll('%', '%25').replaceAll('-', '%2D')).join('-');
|
|
315
406
|
}
|
|
@@ -42,7 +42,7 @@ export type ConstructSpaceParams = {
|
|
|
42
42
|
/**
|
|
43
43
|
* Called when connection auth passed successful.
|
|
44
44
|
*/
|
|
45
|
-
|
|
45
|
+
onAuthorizedConnection: (session: Teleport) => void;
|
|
46
46
|
onAuthFailure?: (session: Teleport) => void;
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -93,7 +93,7 @@ export class SpaceManager {
|
|
|
93
93
|
async constructSpace({
|
|
94
94
|
metadata,
|
|
95
95
|
swarmIdentity,
|
|
96
|
-
|
|
96
|
+
onAuthorizedConnection,
|
|
97
97
|
onAuthFailure,
|
|
98
98
|
memberKey,
|
|
99
99
|
}: ConstructSpaceParams) {
|
|
@@ -108,7 +108,7 @@ export class SpaceManager {
|
|
|
108
108
|
topic: spaceKey,
|
|
109
109
|
swarmIdentity,
|
|
110
110
|
networkManager: this._networkManager,
|
|
111
|
-
onSessionAuth:
|
|
111
|
+
onSessionAuth: onAuthorizedConnection,
|
|
112
112
|
onAuthFailure,
|
|
113
113
|
blobStore: this._blobStore,
|
|
114
114
|
});
|
|
@@ -189,7 +189,7 @@ export class TestAgent {
|
|
|
189
189
|
credentialAuthenticator: MOCK_AUTH_VERIFIER,
|
|
190
190
|
},
|
|
191
191
|
memberKey: identityKey,
|
|
192
|
-
|
|
192
|
+
onAuthorizedConnection: (session) => {
|
|
193
193
|
session.addExtension(
|
|
194
194
|
'dxos.mesh.teleport.gossip',
|
|
195
195
|
this.createGossip().createExtension({ remotePeerId: session.remotePeerId }),
|