@dxos/client-services 0.5.0 → 0.5.1-main.0ba1ecb

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 (95) hide show
  1. package/dist/lib/browser/{chunk-ESEYLOPB.mjs → chunk-UIRDNXVF.mjs} +1437 -1119
  2. package/dist/lib/browser/chunk-UIRDNXVF.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +36 -5
  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 +36 -14
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-GA7JFIXK.cjs → chunk-A6NCS4DT.cjs} +1552 -1238
  9. package/dist/lib/node/chunk-A6NCS4DT.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +78 -47
  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 +40 -18
  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 +12 -11
  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 -2
  40. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  41. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +4 -3
  42. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  43. package/dist/types/src/packlets/spaces/data-space.d.ts +4 -3
  44. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  45. package/dist/types/src/packlets/spaces/spaces-service.d.ts +2 -1
  46. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  47. package/dist/types/src/packlets/storage/level.d.ts +1 -2
  48. package/dist/types/src/packlets/storage/level.d.ts.map +1 -1
  49. package/dist/types/src/packlets/testing/invitation-utils.d.ts +2 -1
  50. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  51. package/dist/types/src/packlets/testing/test-builder.d.ts +5 -3
  52. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  53. package/dist/types/src/packlets/vault/shell-runtime.d.ts +10 -2
  54. package/dist/types/src/packlets/vault/shell-runtime.d.ts.map +1 -1
  55. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  56. package/dist/types/src/version.d.ts +1 -1
  57. package/dist/types/src/version.d.ts.map +1 -1
  58. package/package.json +36 -35
  59. package/src/packlets/diagnostics/diagnostics-collector.ts +14 -9
  60. package/src/packlets/diagnostics/diagnostics.ts +78 -68
  61. package/src/packlets/invitations/device-invitation-protocol.ts +5 -1
  62. package/src/packlets/invitations/invitation-guest-extenstion.ts +126 -0
  63. package/src/packlets/invitations/{invitation-extension.ts → invitation-host-extension.ts} +99 -105
  64. package/src/packlets/invitations/invitation-protocol.ts +7 -1
  65. package/src/packlets/invitations/invitation-topology.ts +87 -0
  66. package/src/packlets/invitations/invitations-handler.test.ts +361 -0
  67. package/src/packlets/invitations/invitations-handler.ts +246 -149
  68. package/src/packlets/invitations/invitations-manager.ts +45 -3
  69. package/src/packlets/invitations/space-invitation-protocol.ts +23 -3
  70. package/src/packlets/invitations/utils.ts +27 -0
  71. package/src/packlets/services/automerge-host.test.ts +6 -4
  72. package/src/packlets/services/service-context.test.ts +3 -3
  73. package/src/packlets/services/service-context.ts +18 -31
  74. package/src/packlets/services/service-host.test.ts +6 -0
  75. package/src/packlets/services/service-host.ts +11 -28
  76. package/src/packlets/spaces/data-space-manager.test.ts +4 -4
  77. package/src/packlets/spaces/data-space-manager.ts +8 -11
  78. package/src/packlets/spaces/data-space.ts +16 -19
  79. package/src/packlets/spaces/genesis.ts +1 -1
  80. package/src/packlets/spaces/spaces-service.ts +12 -6
  81. package/src/packlets/storage/level.ts +3 -2
  82. package/src/packlets/testing/invitation-utils.ts +23 -3
  83. package/src/packlets/testing/test-builder.ts +13 -15
  84. package/src/packlets/vault/shell-runtime.ts +40 -2
  85. package/src/packlets/vault/worker-runtime.ts +3 -1
  86. package/src/version.ts +1 -5
  87. package/dist/lib/browser/chunk-ESEYLOPB.mjs.map +0 -7
  88. package/dist/lib/node/chunk-GA7JFIXK.cjs.map +0 -7
  89. package/dist/types/src/packlets/indexing/index.d.ts +0 -2
  90. package/dist/types/src/packlets/indexing/index.d.ts.map +0 -1
  91. package/dist/types/src/packlets/indexing/util.d.ts +0 -11
  92. package/dist/types/src/packlets/indexing/util.d.ts.map +0 -1
  93. package/dist/types/src/packlets/invitations/invitation-extension.d.ts.map +0 -1
  94. package/src/packlets/indexing/index.ts +0 -5
  95. package/src/packlets/indexing/util.ts +0 -32
@@ -0,0 +1,87 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { invariant } from '@dxos/invariant';
6
+ import { PublicKey } from '@dxos/keys';
7
+ import { log } from '@dxos/log';
8
+ import type { SwarmController, Topology } from '@dxos/network-manager';
9
+ import { Options } from '@dxos/protocols/proto/dxos/halo/invitations';
10
+ import { ComplexSet } from '@dxos/util';
11
+
12
+ /**
13
+ * Hosts are listening on an invitation topic.
14
+ * They initiate a connection with any new peer if they are not currently in the invitation flow
15
+ * with another peer (connected.length > 0).
16
+ * When the invitation flow ends guest leaves the swarm and topology is updated once again,
17
+ * so we can connect to the next peer we haven't tried yet.
18
+ * If the peer turns out to be a host or a malicious guest their ID is remembered so that we don't try
19
+ * to establish a connection with them again.
20
+ *
21
+ * Guests don't initiate connections. They accept all connections because if we reject,
22
+ * the host won't retry their offer.
23
+ * Even if we started an invitation flow with one host we might want to try other hosts in case
24
+ * the first one failed due to a network error, so multiple connections are accepted.
25
+ */
26
+ export class InvitationTopology implements Topology {
27
+ private _controller?: SwarmController;
28
+
29
+ /**
30
+ * Peers we tried to establish a connection with.
31
+ * In invitation flow peers are assigned random ids when they join the swarm, so we'll retry
32
+ * a peer if they reload an invitation.
33
+ *
34
+ * Consider keeping a separate set for peers we know are hosts and have some retry timeout
35
+ * for guests we failed an invitation flow with (potentially due to a network error).
36
+ */
37
+ private _seenPeers = new ComplexSet<PublicKey>(PublicKey.hash);
38
+
39
+ constructor(private readonly _role: Options.Role) {}
40
+
41
+ init(controller: SwarmController): void {
42
+ invariant(!this._controller, 'Already initialized.');
43
+ this._controller = controller;
44
+ }
45
+
46
+ update(): void {
47
+ invariant(this._controller, 'Not initialized.');
48
+ const { ownPeerId, candidates, connected, allPeers } = this._controller.getState();
49
+
50
+ // guests don't initiate connections
51
+ if (this._role === Options.Role.GUEST) {
52
+ return;
53
+ }
54
+
55
+ // don't start a connection while we have an active invitation flow
56
+ if (connected.length > 0) {
57
+ // update seenPeers here as well in case another host initiated a connection with us
58
+ connected.forEach((c) => this._seenPeers.add(c));
59
+ return;
60
+ }
61
+
62
+ const firstUnknownPeer = candidates.find((peerId) => !this._seenPeers.has(peerId));
63
+ // cleanup
64
+ this._seenPeers = new ComplexSet<PublicKey>(
65
+ PublicKey.hash,
66
+ allPeers.filter((peerId) => this._seenPeers.has(peerId)),
67
+ );
68
+ if (firstUnknownPeer != null) {
69
+ log('invitation connect', { ownPeerId, remotePeerId: firstUnknownPeer });
70
+ this._controller.connect(firstUnknownPeer);
71
+ this._seenPeers.add(firstUnknownPeer);
72
+ }
73
+ }
74
+
75
+ async onOffer(peer: PublicKey): Promise<boolean> {
76
+ invariant(this._controller, 'Not initialized.');
77
+ return !this._seenPeers.has(peer);
78
+ }
79
+
80
+ async destroy(): Promise<void> {
81
+ this._seenPeers.clear();
82
+ }
83
+
84
+ toString() {
85
+ return `InvitationTopology(${this._role === Options.Role.GUEST ? 'guest' : 'host'})`;
86
+ }
87
+ }
@@ -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
+ });