@dxos/client-services 0.6.11 → 0.6.12-main.15a606f

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.
Files changed (88) hide show
  1. package/dist/lib/browser/{chunk-QYVLLBAA.mjs → chunk-BDGSOBZV.mjs} +5136 -5087
  2. package/dist/lib/browser/chunk-BDGSOBZV.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3 -3
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +4 -5
  7. package/dist/lib/browser/testing/index.mjs.map +2 -2
  8. package/dist/lib/node/{chunk-F7G2TXVG.cjs → chunk-EBZ7KFXS.cjs} +4908 -4859
  9. package/dist/lib/node/chunk-EBZ7KFXS.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +46 -46
  11. package/dist/lib/node/index.cjs.map +3 -3
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +11 -12
  14. package/dist/lib/node/testing/index.cjs.map +2 -2
  15. package/dist/lib/node-esm/chunk-LDBJYNPL.mjs +7765 -0
  16. package/dist/lib/node-esm/chunk-LDBJYNPL.mjs.map +7 -0
  17. package/dist/lib/node-esm/index.mjs +416 -0
  18. package/dist/lib/node-esm/index.mjs.map +7 -0
  19. package/dist/lib/node-esm/meta.json +1 -0
  20. package/dist/lib/node-esm/testing/index.mjs +418 -0
  21. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  22. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  23. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +2 -0
  24. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +1 -0
  25. package/dist/types/src/packlets/identity/identity-manager.d.ts +19 -7
  26. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  27. package/dist/types/src/packlets/identity/identity.d.ts +7 -1
  28. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  29. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  30. package/dist/types/src/packlets/services/service-context.d.ts +3 -4
  31. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  32. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  33. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +1 -2
  34. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  35. package/dist/types/src/packlets/spaces/data-space.d.ts +1 -2
  36. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  37. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  38. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
  39. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  40. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  41. package/dist/types/src/packlets/testing/test-builder.d.ts +1 -2
  42. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  43. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  44. package/dist/types/src/testing/setup.d.ts +3 -0
  45. package/dist/types/src/testing/setup.d.ts.map +1 -0
  46. package/dist/types/src/version.d.ts +1 -1
  47. package/dist/types/src/version.d.ts.map +1 -1
  48. package/package.json +42 -39
  49. package/src/packlets/devices/devices-service.test.ts +4 -5
  50. package/src/packlets/diagnostics/diagnostics-broadcast.ts +1 -0
  51. package/src/packlets/identity/{authenticator.test.ts → authenticator.node.test.ts} +2 -3
  52. package/src/packlets/identity/identity-manager.test.ts +5 -6
  53. package/src/packlets/identity/identity-manager.ts +35 -19
  54. package/src/packlets/identity/identity-service.test.ts +4 -8
  55. package/src/packlets/identity/identity.test.ts +124 -239
  56. package/src/packlets/identity/identity.ts +38 -8
  57. package/src/packlets/invitations/device-invitation-protocol.test.ts +7 -4
  58. package/src/packlets/invitations/invitation-host-extension.ts +0 -3
  59. package/src/packlets/invitations/invitations-handler.test.ts +14 -7
  60. package/src/packlets/invitations/invitations-handler.ts +1 -1
  61. package/src/packlets/invitations/space-invitation-protocol.test.ts +4 -3
  62. package/src/packlets/logging/logging.test.ts +1 -2
  63. package/src/packlets/network/network-service.test.ts +2 -3
  64. package/src/packlets/services/service-context.test.ts +3 -1
  65. package/src/packlets/services/service-context.ts +24 -12
  66. package/src/packlets/services/service-host.test.ts +8 -12
  67. package/src/packlets/services/service-host.ts +2 -2
  68. package/src/packlets/services/service-registry.test.ts +1 -2
  69. package/src/packlets/spaces/data-space-manager.test.ts +2 -2
  70. package/src/packlets/spaces/data-space-manager.ts +4 -1
  71. package/src/packlets/spaces/data-space.ts +7 -2
  72. package/src/packlets/spaces/edge-feed-replicator.ts +9 -4
  73. package/src/packlets/spaces/epoch-migrations.ts +2 -2
  74. package/src/packlets/spaces/notarization-plugin.test.ts +2 -3
  75. package/src/packlets/spaces/spaces-service.test.ts +5 -9
  76. package/src/packlets/storage/storage.ts +0 -1
  77. package/src/packlets/system/system-service.test.ts +1 -2
  78. package/src/packlets/testing/test-builder.ts +1 -2
  79. package/src/packlets/worker/worker-runtime.ts +2 -2
  80. package/src/testing/setup.ts +11 -0
  81. package/src/version.ts +1 -5
  82. package/dist/lib/browser/chunk-QYVLLBAA.mjs.map +0 -7
  83. package/dist/lib/node/chunk-F7G2TXVG.cjs.map +0 -7
  84. package/dist/types/src/packlets/identity/authenticator.test.d.ts +0 -2
  85. package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +0 -1
  86. package/dist/types/src/packlets/services/automerge-host.test.d.ts +0 -2
  87. package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +0 -1
  88. package/src/packlets/services/automerge-host.test.ts +0 -60
@@ -2,7 +2,7 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import expect from 'expect';
5
+ import { onTestFinished, describe, expect, test } from 'vitest';
6
6
 
7
7
  import { Context } from '@dxos/context';
8
8
  import { CredentialGenerator, verifyCredential } from '@dxos/credentials';
@@ -15,7 +15,9 @@ import {
15
15
  SpaceProtocol,
16
16
  valueEncoding,
17
17
  } from '@dxos/echo-pipeline';
18
+ import { type EdgeConnection, type MessageListener } from '@dxos/edge-client';
18
19
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
20
+ import { type FeedWrapper } from '@dxos/feed-store';
19
21
  import { Keyring } from '@dxos/keyring';
20
22
  import { type PublicKey } from '@dxos/keys';
21
23
  import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
@@ -24,7 +26,6 @@ import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
24
26
  import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
25
27
  import { createStorage, StorageType } from '@dxos/random-access-storage';
26
28
  import { BlobStore } from '@dxos/teleport-extension-object-sync';
27
- import { afterTest, describe, test } from '@dxos/test';
28
29
 
29
30
  import { Identity } from './identity';
30
31
 
@@ -42,16 +43,105 @@ const createStores = () => {
42
43
 
43
44
  describe('identity/identity', () => {
44
45
  test('create', async () => {
46
+ const setup = await setupIdentity();
47
+
48
+ await writeGenesisCredential(setup);
49
+
50
+ // Wait for identity to be ready.
51
+ await setup.identity.ready();
52
+ const identitySigner = setup.identity.getIdentityCredentialSigner();
53
+ const credential = await identitySigner.createCredential({
54
+ subject: setup.identityKey,
55
+ assertion: {
56
+ '@type': 'dxos.halo.credentials.IdentityProfile',
57
+ profile: {
58
+ displayName: 'Alice',
59
+ },
60
+ },
61
+ });
62
+
63
+ expect(credential.issuer).toEqual(setup.identityKey);
64
+ expect(await verifyCredential(credential)).toEqual({ kind: 'pass' });
65
+ });
66
+
67
+ test('two devices', async () => {
68
+ const signalContext = new MemorySignalManagerContext();
69
+
70
+ const owner = await setupIdentity({ signalContext });
71
+ await writeGenesisCredential(owner);
72
+ await owner.identity.ready();
73
+
74
+ const secondDevice = (
75
+ await setupIdentity({
76
+ signalContext,
77
+ spaceKey: owner.spaceKey,
78
+ identityKey: owner.identityKey,
79
+ genesisFeedKey: owner.controlFeed.key,
80
+ })
81
+ ).identity;
82
+
83
+ //
84
+ // Second device admission
85
+ //
86
+ {
87
+ const signer = owner.identity.getIdentityCredentialSigner();
88
+ void owner.identity.controlPipeline.writer.write({
89
+ credential: {
90
+ credential: await signer.createCredential({
91
+ subject: secondDevice.deviceKey,
92
+ assertion: {
93
+ '@type': 'dxos.halo.credentials.AuthorizedDevice',
94
+ identityKey: owner.identityKey,
95
+ deviceKey: secondDevice.deviceKey,
96
+ },
97
+ }),
98
+ },
99
+ });
100
+
101
+ await secondDevice.ready();
102
+ }
103
+
104
+ expect(Array.from(owner.identity.authorizedDeviceKeys.keys())).toEqual([owner.deviceKey, secondDevice.deviceKey]);
105
+ expect(Array.from(secondDevice.authorizedDeviceKeys.keys())).toEqual([owner.deviceKey, secondDevice.deviceKey]);
106
+ });
107
+
108
+ test('edge feed replicator', async () => {
109
+ let replicationStarted = false;
110
+ const setup = await setupIdentity({
111
+ edgeConnection: {
112
+ open: async () => {},
113
+ close: async () => {},
114
+ addListener: (_: MessageListener): (() => void) => {
115
+ return () => {};
116
+ },
117
+ send: async (_) => {
118
+ replicationStarted = true;
119
+ },
120
+ } as EdgeConnection,
121
+ });
122
+
123
+ await writeGenesisCredential(setup);
124
+
125
+ await expect.poll(() => replicationStarted).toBeTruthy();
126
+ });
127
+
128
+ const setupIdentity = async (args?: {
129
+ signalContext?: MemorySignalManagerContext;
130
+ spaceKey?: PublicKey;
131
+ identityKey?: PublicKey;
132
+ genesisFeedKey?: PublicKey;
133
+ edgeConnection?: EdgeConnection;
134
+ }): Promise<TestIdentitySetup> => {
45
135
  const { storage, metadataStore, blobStore } = createStores();
46
136
 
47
137
  const keyring = new Keyring();
48
- const identityKey = await keyring.createKey();
49
138
  const deviceKey = await keyring.createKey();
50
- const spaceKey = await keyring.createKey();
139
+ const identityKey = args?.identityKey ?? (await keyring.createKey());
140
+ const spaceKey = args?.spaceKey ?? (await keyring.createKey());
51
141
 
52
142
  const feedStore = new FeedStore<FeedMessage>({
53
143
  factory: new FeedFactory<FeedMessage>({
54
- root: storage.createDirectory('feeds'),
144
+ root: storage.createDirectory(),
55
145
  signer: keyring,
56
146
  hypercore: {
57
147
  valueEncoding,
@@ -77,7 +167,7 @@ describe('identity/identity', () => {
77
167
  },
78
168
  blobStore,
79
169
  networkManager: new SwarmNetworkManager({
80
- signalManager: new MemorySignalManager(new MemorySignalManagerContext()),
170
+ signalManager: new MemorySignalManager(args?.signalContext ?? new MemorySignalManagerContext()),
81
171
  transportFactory: MemoryTransportFactory,
82
172
  }),
83
173
  });
@@ -87,7 +177,7 @@ describe('identity/identity', () => {
87
177
  id: await createIdFromSpaceKey(spaceKey),
88
178
  spaceKey,
89
179
  protocol,
90
- genesisFeed: controlFeed,
180
+ genesisFeed: args?.genesisFeedKey ? await feedStore.openFeed(args.genesisFeedKey) : controlFeed,
91
181
  feedProvider: (feedKey) => feedStore.openFeed(feedKey),
92
182
  memberKey: identityKey,
93
183
  metadataStore,
@@ -103,242 +193,37 @@ describe('identity/identity', () => {
103
193
  identityKey,
104
194
  deviceKey,
105
195
  space,
196
+ edgeFeatures: args?.edgeConnection && { feedReplicator: true },
197
+ edgeConnection: args?.edgeConnection,
106
198
  });
107
199
 
108
200
  await identity.open(new Context());
109
- afterTest(() => identity.close(new Context()));
110
-
111
- //
112
- // Identity genesis
113
- //
114
- {
115
- const generator = new CredentialGenerator(keyring, identityKey, deviceKey);
116
- const credentials = [
117
- ...(await generator.createSpaceGenesis(spaceKey, controlFeed.key)),
118
- await generator.createDeviceAuthorization(deviceKey),
119
- await generator.createFeedAdmission(spaceKey, dataFeed.key, AdmittedFeed.Designation.DATA),
120
- ];
121
-
122
- for (const credential of credentials) {
123
- await identity.controlPipeline.writer.write({
124
- credential: { credential },
125
- });
126
- }
127
- }
128
-
129
- // Wait for identity to be ready.
130
- await identity.ready();
131
- const identitySigner = identity.getIdentityCredentialSigner();
132
- const credential = await identitySigner.createCredential({
133
- subject: identityKey,
134
- assertion: {
135
- '@type': 'dxos.halo.credentials.IdentityProfile',
136
- profile: {
137
- displayName: 'Alice',
138
- },
139
- },
140
- });
141
-
142
- expect(credential.issuer).toEqual(identityKey);
143
- expect(await verifyCredential(credential)).toEqual({ kind: 'pass' });
144
- });
145
-
146
- test('two devices', async () => {
147
- const signalContext = new MemorySignalManagerContext();
148
-
149
- let spaceKey: PublicKey;
150
- let identityKey: PublicKey;
151
- let genesisFeedKey: PublicKey;
152
- let identity1: Identity;
153
- let identity2: Identity;
154
-
155
- //
156
- // First device
157
- //
158
- {
159
- const { storage, metadataStore, blobStore } = createStores();
160
-
161
- const keyring = new Keyring();
162
- identityKey = await keyring.createKey();
163
- const deviceKey = await keyring.createKey();
164
- spaceKey = await keyring.createKey();
165
-
166
- const feedStore = new FeedStore<FeedMessage>({
167
- factory: new FeedFactory<FeedMessage>({
168
- root: storage.createDirectory(),
169
- signer: keyring,
170
- hypercore: {
171
- valueEncoding,
172
- },
173
- }),
174
- });
175
-
176
- const createFeed = async () => {
177
- const feedKey = await keyring.createKey();
178
- return feedStore.openFeed(feedKey, { writable: true });
179
- };
180
-
181
- const controlFeed = await createFeed();
182
- genesisFeedKey = controlFeed.key;
183
-
184
- const dataFeed = await createFeed();
185
-
186
- const protocol = new SpaceProtocol({
187
- topic: spaceKey,
188
- swarmIdentity: {
189
- peerKey: deviceKey,
190
- identityKey,
191
- credentialProvider: MOCK_AUTH_PROVIDER, // createHaloAuthProvider(createCredentialSignerWithKey(keyring, device_key)),
192
- credentialAuthenticator: MOCK_AUTH_VERIFIER, // createHaloAuthVerifier(() => identity.authorizedDeviceKeys),
193
- },
194
- blobStore,
195
- networkManager: new SwarmNetworkManager({
196
- signalManager: new MemorySignalManager(signalContext),
197
- transportFactory: MemoryTransportFactory,
198
- }),
199
- });
200
-
201
- await metadataStore.setIdentityRecord({ haloSpace: { key: spaceKey }, identityKey, deviceKey });
202
- const space = new Space({
203
- id: await createIdFromSpaceKey(spaceKey),
204
- spaceKey,
205
- protocol,
206
- genesisFeed: controlFeed,
207
- feedProvider: (feedKey) => feedStore.openFeed(feedKey),
208
- memberKey: identityKey,
209
- metadataStore,
210
- onDelegatedInvitationStatusChange: async () => {},
211
- onMemberRolesChanged: async () => {},
212
- });
213
- await space.setControlFeed(controlFeed);
214
- await space.setDataFeed(dataFeed);
215
-
216
- const identity = (identity1 = new Identity({
217
- signer: keyring,
218
- identityKey,
219
- deviceKey,
220
- space,
221
- }));
222
-
223
- await identity.open(new Context());
224
- afterTest(() => identity.close(new Context()));
225
-
226
- //
227
- // Identity genesis
228
- //
229
- {
230
- const generator = new CredentialGenerator(keyring, identityKey, deviceKey);
231
- const credentials = [
232
- ...(await generator.createSpaceGenesis(spaceKey, controlFeed.key)),
233
- await generator.createDeviceAuthorization(deviceKey),
234
- await generator.createFeedAdmission(spaceKey, dataFeed.key, AdmittedFeed.Designation.DATA),
235
- ];
236
-
237
- for (const credential of credentials) {
238
- await identity.controlPipeline.writer.write({
239
- credential: { credential },
240
- });
241
- }
242
- }
243
-
244
- // Wait for identity to be ready.
245
- await identity.ready();
246
- }
247
-
248
- //
249
- // Second device
250
- //
251
- {
252
- const { storage, metadataStore, blobStore } = createStores();
253
-
254
- const keyring = new Keyring();
255
- const deviceKey = await keyring.createKey();
256
-
257
- const feedStore = new FeedStore<FeedMessage>({
258
- factory: new FeedFactory<FeedMessage>({
259
- root: storage.createDirectory(),
260
- signer: keyring,
261
- hypercore: {
262
- valueEncoding,
263
- },
264
- }),
265
- });
266
-
267
- const createFeed = async () => {
268
- const feedKey = await keyring.createKey();
269
- return feedStore.openFeed(feedKey, { writable: true });
270
- };
271
-
272
- const controlFeed = await createFeed();
273
- const dataFeed = await createFeed();
274
-
275
- const protocol = new SpaceProtocol({
276
- topic: spaceKey,
277
- swarmIdentity: {
278
- peerKey: deviceKey,
279
- identityKey,
280
- credentialProvider: MOCK_AUTH_PROVIDER, // createHaloAuthProvider(createCredentialSignerWithKey(keyring, device_key)),
281
- credentialAuthenticator: MOCK_AUTH_VERIFIER, // createHaloAuthVerifier(() => identity.authorizedDeviceKeys),
282
- },
283
- blobStore,
284
- networkManager: new SwarmNetworkManager({
285
- signalManager: new MemorySignalManager(signalContext),
286
- transportFactory: MemoryTransportFactory,
287
- }),
288
- });
201
+ onTestFinished(() => identity.close(new Context()));
202
+ return { identity, identityKey, keyring, deviceKey, controlFeed, spaceKey, dataFeed };
203
+ };
289
204
 
290
- await metadataStore.setIdentityRecord({
291
- haloSpace: { key: spaceKey },
292
- identityKey: identity1.identityKey,
293
- deviceKey,
294
- });
295
- const space = new Space({
296
- id: await createIdFromSpaceKey(spaceKey),
297
- spaceKey,
298
- protocol,
299
- genesisFeed: await feedStore.openFeed(genesisFeedKey),
300
- feedProvider: (feedKey) => feedStore.openFeed(feedKey),
301
- memberKey: identityKey,
302
- metadataStore,
303
- onDelegatedInvitationStatusChange: async () => {},
304
- onMemberRolesChanged: async () => {},
205
+ const writeGenesisCredential = async (setup: TestIdentitySetup) => {
206
+ const generator = new CredentialGenerator(setup.keyring, setup.identityKey, setup.deviceKey);
207
+ const credentials = [
208
+ ...(await generator.createSpaceGenesis(setup.spaceKey, setup.controlFeed.key)),
209
+ await generator.createDeviceAuthorization(setup.deviceKey),
210
+ await generator.createFeedAdmission(setup.spaceKey, setup.dataFeed.key, AdmittedFeed.Designation.DATA),
211
+ ];
212
+
213
+ for (const credential of credentials) {
214
+ await setup.identity.controlPipeline.writer.write({
215
+ credential: { credential },
305
216
  });
306
- await space.setControlFeed(controlFeed);
307
- await space.setDataFeed(dataFeed);
308
-
309
- const identity = (identity2 = new Identity({
310
- signer: keyring,
311
- identityKey: identity1.identityKey,
312
- deviceKey,
313
- space,
314
- }));
315
-
316
- await identity.open(new Context());
317
- afterTest(() => identity.close(new Context()));
318
217
  }
319
-
320
- //
321
- // Second device admission
322
- //
323
- {
324
- const signer = identity1.getIdentityCredentialSigner();
325
- void identity1.controlPipeline.writer.write({
326
- credential: {
327
- credential: await signer.createCredential({
328
- subject: identity2.deviceKey,
329
- assertion: {
330
- '@type': 'dxos.halo.credentials.AuthorizedDevice',
331
- identityKey: identity1.identityKey,
332
- deviceKey: identity2.deviceKey,
333
- },
334
- }),
335
- },
336
- });
337
-
338
- await identity2.ready();
339
- }
340
-
341
- expect(Array.from(identity1.authorizedDeviceKeys.keys())).toEqual([identity1.deviceKey, identity2.deviceKey]);
342
- expect(Array.from(identity2.authorizedDeviceKeys.keys())).toEqual([identity1.deviceKey, identity2.deviceKey]);
343
- });
218
+ };
344
219
  });
220
+
221
+ type TestIdentitySetup = {
222
+ identity: Identity;
223
+ keyring: Keyring;
224
+ identityKey: PublicKey;
225
+ deviceKey: PublicKey;
226
+ spaceKey: PublicKey;
227
+ controlFeed: FeedWrapper<FeedMessage>;
228
+ dataFeed: FeedWrapper<FeedMessage>;
229
+ };
@@ -14,10 +14,12 @@ import {
14
14
  } from '@dxos/credentials';
15
15
  import { type Signer } from '@dxos/crypto';
16
16
  import { type Space } from '@dxos/echo-pipeline';
17
- import { writeMessages } from '@dxos/feed-store';
17
+ import { type EdgeConnection } from '@dxos/edge-client';
18
+ import { writeMessages, type FeedWrapper } from '@dxos/feed-store';
18
19
  import { invariant } from '@dxos/invariant';
19
20
  import { PublicKey, type SpaceId } from '@dxos/keys';
20
21
  import { log } from '@dxos/log';
22
+ import { type Runtime } from '@dxos/protocols/proto/dxos/config';
21
23
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
24
  import {
23
25
  AdmittedFeed,
@@ -32,6 +34,7 @@ import { type ComplexMap, ComplexSet } from '@dxos/util';
32
34
 
33
35
  import { TrustedKeySetAuthVerifier } from './authenticator';
34
36
  import { DefaultSpaceStateMachine } from './default-space-state-machine';
37
+ import { EdgeFeedReplicator } from '../spaces';
35
38
 
36
39
  export type IdentityParams = {
37
40
  identityKey: PublicKey;
@@ -39,6 +42,9 @@ export type IdentityParams = {
39
42
  signer: Signer;
40
43
  space: Space;
41
44
  presence?: Presence;
45
+
46
+ edgeConnection?: EdgeConnection;
47
+ edgeFeatures?: Runtime.Client.EdgeFeatures;
42
48
  };
43
49
 
44
50
  /**
@@ -52,6 +58,8 @@ export class Identity {
52
58
  private readonly _deviceStateMachine: DeviceStateMachine;
53
59
  private readonly _profileStateMachine: ProfileStateMachine;
54
60
  private readonly _defaultSpaceStateMachine: DefaultSpaceStateMachine;
61
+ private readonly _edgeFeedReplicator?: EdgeFeedReplicator = undefined;
62
+
55
63
  public readonly authVerifier: TrustedKeySetAuthVerifier;
56
64
 
57
65
  public readonly identityKey: PublicKey;
@@ -59,15 +67,15 @@ export class Identity {
59
67
 
60
68
  public readonly stateUpdate = new Event();
61
69
 
62
- constructor({ space, signer, identityKey, deviceKey, presence }: IdentityParams) {
63
- this.space = space;
64
- this._signer = signer;
65
- this._presence = presence;
70
+ constructor(params: IdentityParams) {
71
+ this.space = params.space;
72
+ this._signer = params.signer;
73
+ this._presence = params.presence;
66
74
 
67
- this.identityKey = identityKey;
68
- this.deviceKey = deviceKey;
75
+ this.identityKey = params.identityKey;
76
+ this.deviceKey = params.deviceKey;
69
77
 
70
- log.trace('dxos.halo.device', { deviceKey });
78
+ log.trace('dxos.halo.device', { deviceKey: params.deviceKey });
71
79
 
72
80
  this._deviceStateMachine = new DeviceStateMachine({
73
81
  identityKey: this.identityKey,
@@ -88,6 +96,10 @@ export class Identity {
88
96
  update: this.stateUpdate,
89
97
  authTimeout: AUTH_TIMEOUT,
90
98
  });
99
+
100
+ if (params.edgeConnection && params.edgeFeatures?.feedReplicator) {
101
+ this._edgeFeedReplicator = new EdgeFeedReplicator({ messenger: params.edgeConnection, spaceId: this.space.id });
102
+ }
91
103
  }
92
104
 
93
105
  // TODO(burdon): Expose state object?
@@ -105,7 +117,14 @@ export class Identity {
105
117
  await this.space.spaceState.addCredentialProcessor(this._deviceStateMachine);
106
118
  await this.space.spaceState.addCredentialProcessor(this._profileStateMachine);
107
119
  await this.space.spaceState.addCredentialProcessor(this._defaultSpaceStateMachine);
120
+
121
+ if (this._edgeFeedReplicator) {
122
+ this.space.protocol.feedAdded.append(this._onFeedAdded);
123
+ }
124
+
108
125
  await this.space.open(ctx);
126
+
127
+ await this._edgeFeedReplicator?.open();
109
128
  }
110
129
 
111
130
  @trace.span()
@@ -115,6 +134,13 @@ export class Identity {
115
134
  await this.space.spaceState.removeCredentialProcessor(this._defaultSpaceStateMachine);
116
135
  await this.space.spaceState.removeCredentialProcessor(this._profileStateMachine);
117
136
  await this.space.spaceState.removeCredentialProcessor(this._deviceStateMachine);
137
+
138
+ if (this._edgeFeedReplicator) {
139
+ this.space.protocol.feedAdded.remove(this._onFeedAdded);
140
+ }
141
+
142
+ await this._edgeFeedReplicator?.close();
143
+
118
144
  await this.space.close();
119
145
  }
120
146
 
@@ -223,4 +249,8 @@ export class Identity {
223
249
  ].map((credential): FeedMessage.Payload => ({ credential: { credential } })),
224
250
  );
225
251
  }
252
+
253
+ private _onFeedAdded = async (feed: FeedWrapper<any>) => {
254
+ await this._edgeFeedReplicator!.addFeed(feed);
255
+ };
226
256
  }
@@ -2,19 +2,20 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
5
+ import { describe, expect, test, onTestFinished } from 'vitest';
6
6
 
7
7
  import { asyncChain } from '@dxos/async';
8
8
  import { Context } from '@dxos/context';
9
9
  import { AlreadyJoinedError } from '@dxos/protocols';
10
10
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
11
- import { describe, test, afterTest } from '@dxos/test';
12
11
 
13
12
  import { type ServiceContext } from '../services';
14
13
  import { createPeers, createServiceContext, performInvitation } from '../testing';
15
14
 
16
15
  const closeAfterTest = async (peer: ServiceContext) => {
17
- afterTest(() => peer.close());
16
+ onTestFinished(async () => {
17
+ await peer.close();
18
+ });
18
19
  return peer;
19
20
  };
20
21
 
@@ -22,7 +23,9 @@ describe('services/device', () => {
22
23
  test('creates identity', async () => {
23
24
  const peer = await createServiceContext();
24
25
  await peer.open(new Context());
25
- afterTest(() => peer.close());
26
+ onTestFinished(async () => {
27
+ await peer.close();
28
+ });
26
29
 
27
30
  const identity = await peer.createIdentity();
28
31
  expect(identity).not.to.be.undefined;
@@ -106,13 +106,11 @@ export class InvitationHostExtension extends RpcExtension<
106
106
 
107
107
  introduce: async (request) => {
108
108
  const { profile, invitationId } = request;
109
-
110
109
  const traceId = PublicKey.random().toHex();
111
110
  log.trace('dxos.sdk.invitation-handler.host.introduce', trace.begin({ id: traceId }));
112
111
 
113
112
  const invitation = this._requireActiveInvitation();
114
113
  this._assertInvitationState(Invitation.State.CONNECTED);
115
-
116
114
  if (invitationId !== invitation?.invitationId) {
117
115
  log.warn('incorrect invitationId', { expected: invitation.invitationId, actual: invitationId });
118
116
  this._callbacks.onError(new Error('Incorrect invitationId.'));
@@ -126,7 +124,6 @@ export class InvitationHostExtension extends RpcExtension<
126
124
  log('guest introduced themselves', { guestProfile: profile });
127
125
  this.guestProfile = profile;
128
126
  this._callbacks.onStateUpdate(Invitation.State.READY_FOR_AUTHENTICATION);
129
-
130
127
  this._challenge =
131
128
  invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? randomBytes(32) : undefined;
132
129
 
@@ -2,13 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
5
+ import { beforeEach, onTestFinished, describe, expect, test } from 'vitest';
6
6
 
7
7
  import { type PushStream, sleep, Trigger, waitForCondition } from '@dxos/async';
8
8
  import { Context } from '@dxos/context';
9
9
  import { PublicKey } from '@dxos/keys';
10
10
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
11
- import { afterTest, describe, openAndClose, test } from '@dxos/test';
11
+ import { openAndClose } from '@dxos/test-utils';
12
12
  import { range } from '@dxos/util';
13
13
 
14
14
  import { type InvitationProtocol } from './invitation-protocol';
@@ -34,6 +34,7 @@ type StateUpdateSink = PushStream<Invitation> & {
34
34
 
35
35
  describe('InvitationHandler', () => {
36
36
  let testBuilder: TestBuilder;
37
+
37
38
  beforeEach(() => {
38
39
  testBuilder = new TestBuilder();
39
40
  });
@@ -163,7 +164,7 @@ describe('InvitationHandler', () => {
163
164
  expect(guest.ctx.disposed).to.be.true;
164
165
  });
165
166
 
166
- test('guest gives up after trying with three hosts', async () => {
167
+ test('guest gives up after trying with three hosts', { timeout: 20_000 }, async () => {
167
168
  const hosts: PeerSetup[] = [await createPeer()];
168
169
  const [host] = hosts;
169
170
  const invitation = await createInvitation(host, { multiUse: true });
@@ -181,7 +182,7 @@ describe('InvitationHandler', () => {
181
182
 
182
183
  await sleep(10);
183
184
  expect(guest.sink.lastState).to.eq(Invitation.State.ERROR);
184
- }).timeout(20_000);
185
+ });
185
186
 
186
187
  test('single host - many guests', async () => {
187
188
  const hosts: PeerSetup[] = [await createPeer()];
@@ -261,7 +262,9 @@ describe('InvitationHandler', () => {
261
262
  });
262
263
  const protocol = new SpaceInvitationProtocol(peer.dataSpaceManager, peer.identity, peer.keyring, spaceKey);
263
264
  const ctx = new Context();
264
- afterTest(() => ctx.dispose());
265
+ onTestFinished(async () => {
266
+ await ctx.dispose();
267
+ });
265
268
  const sink = newStateUpdateSink();
266
269
  return { ctx, sink, peer, protocol, handler: invitationHandler, spaceKey };
267
270
  };
@@ -269,14 +272,18 @@ describe('InvitationHandler', () => {
269
272
  const hostInvitation = async (setup: PeerSetup, invitation: Invitation) => {
270
273
  await setup.ctx.dispose();
271
274
  setup.ctx = new Context();
272
- afterTest(() => setup.ctx.dispose());
275
+ onTestFinished(async () => {
276
+ await setup.ctx.dispose();
277
+ });
273
278
  setup.handler.handleInvitationFlow(setup.ctx, setup.sink, setup.protocol, invitation);
274
279
  };
275
280
 
276
281
  const acceptInvitation = async (setup: PeerSetup, invitation: Invitation): Promise<Trigger<string>> => {
277
282
  await setup.ctx.dispose();
278
283
  setup.ctx = new Context();
279
- afterTest(() => setup.ctx.dispose());
284
+ onTestFinished(async () => {
285
+ await setup.ctx.dispose();
286
+ });
280
287
  const authCodeInput = new Trigger<string>();
281
288
  setup.handler.acceptInvitation(setup.ctx, setup.sink, setup.protocol, invitation, authCodeInput);
282
289
  return authCodeInput;
@@ -463,7 +463,7 @@ export class InvitationsHandler {
463
463
  oldState: stateToString(invitation.state),
464
464
  });
465
465
  } else {
466
- log.info('invitation state update', {
466
+ log('invitation state update', {
467
467
  actor: actor?.constructor.name,
468
468
  newState: stateToString(newState),
469
469
  oldState: stateToString(invitation.state),