@dxos/client-services 0.5.1-main.f02b2c7 → 0.5.1-main.f2d5836

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 (79) hide show
  1. package/dist/lib/browser/{chunk-T3ZIANWR.mjs → chunk-CZKKHM32.mjs} +1381 -1004
  2. package/dist/lib/browser/chunk-CZKKHM32.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +31 -2
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +29 -9
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-425TJE5X.cjs → chunk-T34IF5GF.cjs} +1576 -1207
  9. package/dist/lib/node/chunk-T34IF5GF.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +73 -44
  11. package/dist/lib/node/index.cjs.map +3 -3
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/packlets/testing/index.cjs +35 -15
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -1
  16. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -1
  17. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +2 -1
  18. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  19. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +39 -0
  20. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -0
  21. package/dist/types/src/packlets/invitations/{invitation-extension.d.ts → invitation-host-extension.d.ts} +17 -31
  22. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -0
  23. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +6 -1
  24. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  25. package/dist/types/src/packlets/invitations/invitation-topology.d.ts +37 -0
  26. package/dist/types/src/packlets/invitations/invitation-topology.d.ts.map +1 -0
  27. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +19 -10
  28. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  29. package/dist/types/src/packlets/invitations/invitations-handler.test.d.ts +2 -0
  30. package/dist/types/src/packlets/invitations/invitations-handler.test.d.ts.map +1 -0
  31. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +2 -1
  32. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
  33. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +1 -0
  34. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  35. package/dist/types/src/packlets/invitations/utils.d.ts +6 -0
  36. package/dist/types/src/packlets/invitations/utils.d.ts.map +1 -0
  37. package/dist/types/src/packlets/services/service-context.d.ts +8 -5
  38. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  39. package/dist/types/src/packlets/services/service-host.d.ts +1 -1
  40. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  41. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  42. package/dist/types/src/packlets/spaces/spaces-service.d.ts +2 -1
  43. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  44. package/dist/types/src/packlets/storage/level.d.ts +1 -2
  45. package/dist/types/src/packlets/storage/level.d.ts.map +1 -1
  46. package/dist/types/src/packlets/testing/invitation-utils.d.ts +2 -1
  47. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  48. package/dist/types/src/packlets/testing/test-builder.d.ts +2 -1
  49. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  50. package/dist/types/src/packlets/vault/shell-runtime.d.ts +10 -2
  51. package/dist/types/src/packlets/vault/shell-runtime.d.ts.map +1 -1
  52. package/dist/types/src/version.d.ts +1 -1
  53. package/package.json +36 -35
  54. package/src/packlets/diagnostics/diagnostics-collector.ts +14 -9
  55. package/src/packlets/diagnostics/diagnostics.ts +78 -68
  56. package/src/packlets/invitations/device-invitation-protocol.ts +5 -1
  57. package/src/packlets/invitations/invitation-guest-extenstion.ts +126 -0
  58. package/src/packlets/invitations/{invitation-extension.ts → invitation-host-extension.ts} +99 -105
  59. package/src/packlets/invitations/invitation-protocol.ts +7 -1
  60. package/src/packlets/invitations/invitation-topology.ts +87 -0
  61. package/src/packlets/invitations/invitations-handler.test.ts +361 -0
  62. package/src/packlets/invitations/invitations-handler.ts +246 -149
  63. package/src/packlets/invitations/invitations-manager.ts +45 -3
  64. package/src/packlets/invitations/space-invitation-protocol.ts +23 -3
  65. package/src/packlets/invitations/utils.ts +27 -0
  66. package/src/packlets/services/automerge-host.test.ts +3 -1
  67. package/src/packlets/services/service-context.ts +7 -6
  68. package/src/packlets/services/service-host.ts +5 -6
  69. package/src/packlets/spaces/data-space.ts +4 -2
  70. package/src/packlets/spaces/genesis.ts +1 -1
  71. package/src/packlets/spaces/spaces-service.ts +12 -6
  72. package/src/packlets/storage/level.ts +2 -2
  73. package/src/packlets/testing/invitation-utils.ts +23 -3
  74. package/src/packlets/testing/test-builder.ts +6 -3
  75. package/src/packlets/vault/shell-runtime.ts +40 -2
  76. package/src/version.ts +1 -1
  77. package/dist/lib/browser/chunk-T3ZIANWR.mjs.map +0 -7
  78. package/dist/lib/node/chunk-425TJE5X.cjs.map +0 -7
  79. package/dist/types/src/packlets/invitations/invitation-extension.d.ts.map +0 -1
@@ -0,0 +1,361 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { type PushStream, sleep, Trigger, waitForCondition } from '@dxos/async';
8
+ import { Context } from '@dxos/context';
9
+ import { PublicKey } from '@dxos/keys';
10
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
11
+ import { afterTest, describe, openAndClose, test } from '@dxos/test';
12
+ import { range } from '@dxos/util';
13
+
14
+ import { type InvitationProtocol } from './invitation-protocol';
15
+ import { InvitationsHandler } from './invitations-handler';
16
+ import { SpaceInvitationProtocol } from './space-invitation-protocol';
17
+ import { TestBuilder, type TestPeer } from '../testing';
18
+
19
+ interface PeerSetup {
20
+ ctx: Context;
21
+ peer: TestPeer;
22
+ sink: StateUpdateSink;
23
+ protocol: InvitationProtocol;
24
+ handler: InvitationsHandler;
25
+ spaceKey: PublicKey;
26
+ }
27
+
28
+ type StateUpdateSink = PushStream<Invitation> & {
29
+ sink: Invitation[];
30
+ lastState: Invitation.State | undefined;
31
+ hasState(startingFrom: number, state: Invitation.State): boolean;
32
+ waitFor(state: Invitation.State): Promise<void>;
33
+ };
34
+
35
+ describe('InvitationHandler', () => {
36
+ let testBuilder: TestBuilder;
37
+ beforeEach(() => {
38
+ testBuilder = new TestBuilder();
39
+ });
40
+
41
+ describe('delegated invitations', () => {
42
+ for (const multiUse of [false, true]) {
43
+ test(`base case success multiUse=${multiUse}`, async () => {
44
+ const host = await createPeer();
45
+ const invitation = await createInvitation(host, { multiUse });
46
+ await hostInvitation(host, invitation);
47
+
48
+ const guest = await createPeer(host.spaceKey);
49
+ await performAuth(guest, invitation);
50
+
51
+ await sleep(15);
52
+ expect(guest.ctx.disposed).to.be.true;
53
+ if (multiUse) {
54
+ expect(host.ctx.disposed).to.be.false;
55
+ } else {
56
+ expect(host.ctx.disposed).to.be.true;
57
+ }
58
+ });
59
+ }
60
+
61
+ test('invitation timeout', async () => {
62
+ const host = await createPeer();
63
+ const invitation = await createInvitation(host, {
64
+ timeout: 100,
65
+ });
66
+ await hostInvitation(host, invitation);
67
+
68
+ const guest = await createPeer(host.spaceKey);
69
+ await acceptInvitation(guest, invitation);
70
+
71
+ await guest.sink.waitFor(Invitation.State.READY_FOR_AUTHENTICATION);
72
+ await sleep(200);
73
+ await host.sink.waitFor(Invitation.State.TIMEOUT);
74
+ await guest.sink.waitFor(Invitation.State.TIMEOUT);
75
+
76
+ await sleep(10);
77
+ expect(host.ctx.disposed).to.be.false;
78
+ expect(guest.ctx.disposed).to.be.false;
79
+ });
80
+
81
+ test('host ctx active after guest disconnects', async () => {
82
+ const host = await createPeer();
83
+ const invitation = await createInvitation(host, {
84
+ timeout: 100,
85
+ });
86
+ await hostInvitation(host, invitation);
87
+
88
+ const guest = await createPeer(host.spaceKey);
89
+ await acceptInvitation(guest, invitation);
90
+
91
+ await guest.sink.waitFor(Invitation.State.READY_FOR_AUTHENTICATION);
92
+ await guest.peer.networkManager.close();
93
+ await host.sink.waitFor(Invitation.State.ERROR);
94
+
95
+ await sleep(10);
96
+ expect(host.ctx.disposed).to.be.false;
97
+ });
98
+
99
+ test('invitation success after first guest error', async () => {
100
+ const host = await createPeer();
101
+ const invitation = await createInvitation(host);
102
+ await hostInvitation(host, invitation);
103
+
104
+ const badGuest = await createPeer(host.spaceKey);
105
+ await failAuth(badGuest, invitation);
106
+ await badGuest.sink.waitFor(Invitation.State.ERROR);
107
+
108
+ const goodGuest = await createPeer(host.spaceKey);
109
+ await performAuth(goodGuest, invitation);
110
+ await host.sink.waitFor(Invitation.State.SUCCESS);
111
+
112
+ await sleep(10);
113
+ expect(goodGuest.ctx.disposed).to.be.true;
114
+ expect(host.ctx.disposed).to.be.true;
115
+ });
116
+
117
+ test('multiUse invitation with multiple guests', async () => {
118
+ const host = await createPeer();
119
+ const invitation = await createInvitation(host, { multiUse: true });
120
+ await hostInvitation(host, invitation);
121
+
122
+ const guest1 = await createPeer(host.spaceKey);
123
+ await performAuth(guest1, invitation);
124
+ const guest2 = await createPeer(host.spaceKey);
125
+ await performAuth(guest2, invitation);
126
+
127
+ await sleep(5);
128
+ [guest1, guest2].forEach((g) => expect(g.ctx.disposed).to.be.true);
129
+ expect(host.ctx.disposed).to.be.false;
130
+ });
131
+
132
+ test('invitation success after host error with another host', async () => {
133
+ const host = await createPeer();
134
+ const invitation = await createInvitation(host, { multiUse: true });
135
+ await hostInvitation(host, invitation);
136
+ const anotherHost = await createNewHost(invitation);
137
+
138
+ const guest = await createPeer(host.spaceKey);
139
+ const codeInput = await failAuth(guest, invitation);
140
+ while (!guest.ctx.disposed) {
141
+ codeInput.wake(invitation.authCode!);
142
+ await sleep(10);
143
+ }
144
+ await guest.sink.waitFor(Invitation.State.SUCCESS);
145
+
146
+ const hostFailed = [host, anotherHost].map((h) => h.sink.hasState(0, Invitation.State.ERROR));
147
+ expect(hostFailed.sort()).to.deep.eq([false, true]);
148
+ });
149
+
150
+ test('single guest - many hosts', async () => {
151
+ const hosts: PeerSetup[] = [await createPeer()];
152
+ const [host] = hosts;
153
+ const invitation = await createInvitation(host, { multiUse: true });
154
+ await hostInvitation(host, invitation);
155
+ for (let i = 0; i < 4; i++) {
156
+ hosts.push(await createNewHost(invitation));
157
+ }
158
+
159
+ const guest = await createPeer(host.spaceKey);
160
+ await performAuth(guest, invitation);
161
+ await guest.sink.waitFor(Invitation.State.SUCCESS);
162
+ await sleep(10);
163
+ expect(guest.ctx.disposed).to.be.true;
164
+ });
165
+
166
+ test('guest gives up after trying with three hosts', async () => {
167
+ const hosts: PeerSetup[] = [await createPeer()];
168
+ const [host] = hosts;
169
+ const invitation = await createInvitation(host, { multiUse: true });
170
+ await hostInvitation(host, invitation);
171
+ for (let i = 0; i < 2; i++) {
172
+ hosts.push(await createNewHost(invitation));
173
+ }
174
+
175
+ const guest = await createPeer(host.spaceKey);
176
+ const codeInput = await acceptInvitation(guest, invitation);
177
+ while (!guest.ctx.disposed) {
178
+ await failCodeInput(guest, codeInput, invitation);
179
+ await sleep(10);
180
+ }
181
+
182
+ await sleep(10);
183
+ expect(guest.sink.lastState).to.eq(Invitation.State.ERROR);
184
+ }).timeout(20_000);
185
+
186
+ test('single host - many guests', async () => {
187
+ const hosts: PeerSetup[] = [await createPeer()];
188
+ const [host] = hosts;
189
+ const invitation = await createInvitation(host, { multiUse: true });
190
+ await hostInvitation(host, invitation);
191
+ const guests = await Promise.all(
192
+ range(5).map(async () => {
193
+ const guest = await createPeer(host.spaceKey);
194
+ await performAuth(guest, invitation);
195
+ return guest;
196
+ }),
197
+ );
198
+
199
+ await sleep(10);
200
+ guests.forEach((g) => {
201
+ expect(g.ctx.disposed).to.be.true;
202
+ expect(g.sink.lastState).to.eq(Invitation.State.SUCCESS);
203
+ });
204
+ });
205
+
206
+ test('many guests - many hosts', async () => {
207
+ const hosts: PeerSetup[] = [await createPeer()];
208
+ const [host] = hosts;
209
+ const invitation = await createInvitation(host, { multiUse: true });
210
+ await hostInvitation(host, invitation);
211
+ for (let i = 0; i < 4; i++) {
212
+ hosts.push(await createNewHost(invitation));
213
+ }
214
+ const guests = await Promise.all(
215
+ range(5).map(async () => {
216
+ const guest = await createPeer(host.spaceKey);
217
+ await performAuth(guest, invitation);
218
+ return guest;
219
+ }),
220
+ );
221
+ await sleep(10);
222
+ guests.forEach((g) => {
223
+ expect(g.ctx.disposed).to.be.true;
224
+ expect(g.sink.lastState).to.eq(Invitation.State.SUCCESS);
225
+ });
226
+ });
227
+
228
+ test('single use invitation - many guests - only one admitted', async () => {
229
+ const host = await createPeer();
230
+ const invitation = await createInvitation(host);
231
+ await hostInvitation(host, invitation);
232
+ const guests = await Promise.all(
233
+ range(5).map(async () => {
234
+ const guest = await createPeer(invitation.spaceKey);
235
+ const authCodeInput2 = await acceptInvitation(guest, invitation);
236
+ authCodeInput2.wake(invitation.authCode!);
237
+ return guest;
238
+ }),
239
+ );
240
+
241
+ await waitForCondition({
242
+ condition: () => guests.find((g) => g.sink.lastState === Invitation.State.SUCCESS) != null,
243
+ });
244
+ await sleep(40);
245
+ const success = guests.filter((g) => g.sink.lastState === Invitation.State.SUCCESS);
246
+ expect(success.length).to.eq(1);
247
+ });
248
+ });
249
+
250
+ const createPeer = async (spaceKey: PublicKey | null = null): Promise<PeerSetup> => {
251
+ const peer = testBuilder.createPeer();
252
+ await peer.createIdentity();
253
+ await openAndClose(peer.echoHost, peer.dataSpaceManager);
254
+ if (spaceKey == null) {
255
+ const space = await peer.dataSpaceManager.createSpace();
256
+ spaceKey = space.key;
257
+ }
258
+ const invitationHandler = new InvitationsHandler(peer.networkManager, {
259
+ controlHeartbeatInterval: 250, // faster peer failure detection
260
+ });
261
+ const protocol = new SpaceInvitationProtocol(peer.dataSpaceManager, peer.identity, peer.keyring, spaceKey);
262
+ const ctx = new Context();
263
+ afterTest(() => ctx.dispose());
264
+ const sink = newStateUpdateSink();
265
+ return { ctx, sink, peer, protocol, handler: invitationHandler, spaceKey };
266
+ };
267
+
268
+ const hostInvitation = async (setup: PeerSetup, invitation: Invitation) => {
269
+ await setup.ctx.dispose();
270
+ setup.ctx = new Context();
271
+ afterTest(() => setup.ctx.dispose());
272
+ setup.handler.handleInvitationFlow(setup.ctx, setup.sink, setup.protocol, invitation);
273
+ };
274
+
275
+ const acceptInvitation = async (setup: PeerSetup, invitation: Invitation): Promise<Trigger<string>> => {
276
+ await setup.ctx.dispose();
277
+ setup.ctx = new Context();
278
+ afterTest(() => setup.ctx.dispose());
279
+ const authCodeInput = new Trigger<string>();
280
+ setup.handler.acceptInvitation(setup.ctx, setup.sink, setup.protocol, invitation, authCodeInput);
281
+ return authCodeInput;
282
+ };
283
+
284
+ const failAuth = async (setup: PeerSetup, invitation: Invitation) => {
285
+ const wrongAuthCodeInput = await acceptInvitation(setup, invitation);
286
+ await setup.sink.waitFor(Invitation.State.READY_FOR_AUTHENTICATION);
287
+ await failCodeInput(setup, wrongAuthCodeInput, invitation);
288
+ return wrongAuthCodeInput;
289
+ };
290
+
291
+ const failCodeInput = async (setup: PeerSetup, codeInput: Trigger<string>, invitation: Invitation): Promise<void> => {
292
+ const checkFrom = setup.sink.sink.length;
293
+ while (
294
+ !setup.ctx.disposed &&
295
+ !setup.sink.hasState(checkFrom, Invitation.State.ERROR) &&
296
+ !setup.sink.hasState(checkFrom, Invitation.State.CONNECTED)
297
+ ) {
298
+ codeInput.wake(invitation.authCode + '1');
299
+ await sleep(20);
300
+ }
301
+ };
302
+
303
+ const createNewHost = async (invitation: Invitation): Promise<PeerSetup> => {
304
+ const newHost = await createPeer(invitation.spaceKey!);
305
+ await performAuth(newHost, invitation);
306
+ await sleep(30);
307
+ await hostInvitation(newHost, invitation);
308
+ return newHost;
309
+ };
310
+
311
+ const performAuth = async (setup: PeerSetup, invitation: Invitation) => {
312
+ const authCodeInput2 = await acceptInvitation(setup, invitation);
313
+ await setup.sink.waitFor(Invitation.State.READY_FOR_AUTHENTICATION);
314
+ authCodeInput2.wake(invitation.authCode!);
315
+ await setup.sink.waitFor(Invitation.State.SUCCESS);
316
+ };
317
+
318
+ const newStateUpdateSink = (): StateUpdateSink => {
319
+ const sink: Invitation[] = [];
320
+ const hasState = (startingIndex: number, state: Invitation.State): boolean => {
321
+ return sink
322
+ .slice(startingIndex)
323
+ .map((i) => i.state)
324
+ .includes(state);
325
+ };
326
+ return {
327
+ sink,
328
+ next: sink.push.bind(sink),
329
+ error: () => {},
330
+ complete: () => {},
331
+ hasState,
332
+ get lastState() {
333
+ return sink[sink.length - 1]?.state;
334
+ },
335
+ waitFor: async (state: Invitation.State): Promise<void> => {
336
+ if (sink[sink.length - 1]?.state === state) {
337
+ return;
338
+ }
339
+ const sliceStart = sink.length;
340
+ await waitForCondition({
341
+ condition: () => hasState(sliceStart, state),
342
+ });
343
+ },
344
+ } as any;
345
+ };
346
+
347
+ const createInvitation = async (setup: PeerSetup, options?: Partial<Invitation>): Promise<Invitation> => {
348
+ const observable = await setup.peer.invitationsManager.createInvitation({
349
+ type: Invitation.Type.DELEGATED,
350
+ kind: Invitation.Kind.SPACE,
351
+ authMethod: Invitation.AuthMethod.SHARED_SECRET,
352
+ spaceKey: setup.spaceKey,
353
+ multiUse: false,
354
+ ...options,
355
+ });
356
+ // cancel to avoid interfering with invitations-handler direct invocations
357
+ const invitation = observable.get();
358
+ await setup.peer.invitationsManager.cancelInvitation(invitation);
359
+ return { ...invitation, swarmKey: PublicKey.random() };
360
+ };
361
+ });