@dxos/client-services 0.6.13 → 0.6.14-main.1366248
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-CRXXOI45.mjs → chunk-KQURG5PQ.mjs} +6537 -5279
- package/dist/lib/browser/chunk-KQURG5PQ.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +7 -3
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +20 -9
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-PZ3JJJ3K.cjs → chunk-WNXJ7EP5.cjs} +6324 -5068
- package/dist/lib/node/chunk-WNXJ7EP5.cjs.map +7 -0
- package/dist/lib/node/index.cjs +50 -46
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +26 -14
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/lib/node-esm/chunk-OQSP5ADY.mjs +8982 -0
- package/dist/lib/node-esm/chunk-OQSP5ADY.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +420 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/testing/index.mjs +431 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +35 -0
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -0
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts +10 -0
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -0
- package/dist/types/src/packlets/agents/index.d.ts +3 -0
- package/dist/types/src/packlets/agents/index.d.ts.map +1 -0
- package/dist/types/src/packlets/devices/devices-service.d.ts +3 -1
- package/dist/types/src/packlets/devices/devices-service.d.ts.map +1 -1
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +2 -0
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-manager.d.ts +28 -9
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +18 -0
- package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/identity-service.d.ts +7 -2
- package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +12 -3
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +31 -0
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +2 -1
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +2 -1
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-state.d.ts +19 -0
- package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +14 -9
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +2 -0
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +7 -3
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +5 -3
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +4 -0
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +2 -0
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +35 -6
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +1 -2
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/testing/setup.d.ts +3 -0
- package/dist/types/src/testing/setup.d.ts.map +1 -0
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/package.json +44 -45
- package/src/index.ts +1 -0
- package/src/packlets/agents/edge-agent-manager.ts +163 -0
- package/src/packlets/agents/edge-agent-service.ts +42 -0
- package/src/packlets/agents/index.ts +6 -0
- package/src/packlets/devices/devices-service.test.ts +4 -5
- package/src/packlets/devices/devices-service.ts +18 -7
- package/src/packlets/diagnostics/diagnostics-broadcast.ts +1 -0
- package/src/packlets/identity/{authenticator.test.ts → authenticator.node.test.ts} +2 -3
- package/src/packlets/identity/authenticator.ts +5 -2
- package/src/packlets/identity/contacts-service.ts +1 -1
- package/src/packlets/identity/identity-manager.test.ts +31 -16
- package/src/packlets/identity/identity-manager.ts +76 -32
- package/src/packlets/identity/identity-recovery-manager.ts +95 -0
- package/src/packlets/identity/identity-service.test.ts +5 -8
- package/src/packlets/identity/identity-service.ts +11 -5
- package/src/packlets/identity/identity.test.ts +141 -239
- package/src/packlets/identity/identity.ts +60 -17
- package/src/packlets/invitations/device-invitation-protocol.test.ts +7 -4
- package/src/packlets/invitations/device-invitation-protocol.ts +8 -2
- package/src/packlets/invitations/edge-invitation-handler.ts +188 -0
- package/src/packlets/invitations/invitation-guest-extenstion.ts +13 -9
- package/src/packlets/invitations/invitation-host-extension.ts +15 -17
- package/src/packlets/invitations/invitation-state.ts +112 -0
- package/src/packlets/invitations/invitations-handler.test.ts +19 -15
- package/src/packlets/invitations/invitations-handler.ts +91 -126
- package/src/packlets/invitations/invitations-service.ts +5 -1
- package/src/packlets/invitations/space-invitation-protocol.test.ts +4 -3
- package/src/packlets/invitations/space-invitation-protocol.ts +5 -0
- package/src/packlets/logging/logging.test.ts +1 -2
- package/src/packlets/network/network-service.test.ts +2 -3
- package/src/packlets/services/service-context.test.ts +3 -1
- package/src/packlets/services/service-context.ts +113 -35
- package/src/packlets/services/service-host.test.ts +8 -12
- package/src/packlets/services/service-host.ts +26 -8
- package/src/packlets/services/service-registry.test.ts +1 -2
- package/src/packlets/spaces/data-space-manager.test.ts +2 -2
- package/src/packlets/spaces/data-space-manager.ts +44 -7
- package/src/packlets/spaces/data-space.ts +37 -6
- package/src/packlets/spaces/edge-feed-replicator.test.ts +257 -0
- package/src/packlets/spaces/edge-feed-replicator.ts +84 -22
- package/src/packlets/spaces/epoch-migrations.ts +2 -2
- package/src/packlets/spaces/notarization-plugin.test.ts +10 -7
- package/src/packlets/spaces/notarization-plugin.ts +196 -29
- package/src/packlets/spaces/spaces-service.test.ts +5 -9
- package/src/packlets/spaces/spaces-service.ts +6 -1
- package/src/packlets/storage/storage.ts +0 -1
- package/src/packlets/system/system-service.test.ts +1 -2
- package/src/packlets/testing/invitation-utils.ts +7 -0
- package/src/packlets/testing/test-builder.ts +7 -4
- package/src/packlets/worker/worker-runtime.ts +2 -2
- package/src/testing/setup.ts +11 -0
- package/src/version.ts +1 -5
- package/dist/lib/browser/chunk-CRXXOI45.mjs.map +0 -7
- package/dist/lib/node/chunk-PZ3JJJ3K.cjs.map +0 -7
- package/dist/types/src/packlets/identity/authenticator.test.d.ts +0 -2
- package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +0 -1
- package/dist/types/src/packlets/services/automerge-host.test.d.ts +0 -2
- package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +0 -1
- package/src/packlets/services/automerge-host.test.ts +0 -60
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A utility object for serializing invitation state changes by multiple concurrent
|
|
3
|
+
* invitation flow connections.
|
|
4
|
+
*/
|
|
5
|
+
//
|
|
6
|
+
// Copyright 2024 DXOS.org
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import { Mutex, type PushStream } from '@dxos/async';
|
|
10
|
+
import { type Context } from '@dxos/context';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
13
|
+
|
|
14
|
+
import { stateToString } from './utils';
|
|
15
|
+
|
|
16
|
+
export interface FlowLockHolder {
|
|
17
|
+
hasFlowLock(): boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GuardedInvitationState {
|
|
21
|
+
mutex: Mutex;
|
|
22
|
+
current: Invitation;
|
|
23
|
+
|
|
24
|
+
complete(newState: Partial<Invitation>): void;
|
|
25
|
+
set(lockHolder: FlowLockHolder | null, newState: Invitation.State): boolean;
|
|
26
|
+
error(lockHolder: FlowLockHolder | null, error: any): boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const createGuardedInvitationState = (
|
|
30
|
+
ctx: Context,
|
|
31
|
+
invitation: Invitation,
|
|
32
|
+
stream: PushStream<Invitation>,
|
|
33
|
+
): GuardedInvitationState => {
|
|
34
|
+
// the mutex guards invitation flow on host and guest side, making sure only one flow is currently active
|
|
35
|
+
// deadlocks seem very unlikely because hosts don't initiate multiple connections
|
|
36
|
+
// even if this somehow happens that there are 2 guests (A, B) and 2 hosts (1, 2) and:
|
|
37
|
+
// A has lock for flow with 1, B has lock for flow with 2
|
|
38
|
+
// 1 has lock for flow with B, 2 has lock for flow with A
|
|
39
|
+
// there'll be a 10-second introduction timeout after which connection will be closed and deadlock broken
|
|
40
|
+
const mutex = new Mutex();
|
|
41
|
+
let lastActiveLockHolder: FlowLockHolder | null = null;
|
|
42
|
+
let currentInvitation = { ...invitation };
|
|
43
|
+
const isStateChangeAllowed = (lockHolder: FlowLockHolder | null) => {
|
|
44
|
+
if (ctx.disposed || (lockHolder !== null && mutex.isLocked() && !lockHolder.hasFlowLock())) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// don't allow transitions from a terminal state unless a new extension acquired mutex
|
|
48
|
+
// handles a case when error occurs (e.g. connection is closed) after we completed the flow
|
|
49
|
+
// successfully or already reported another error
|
|
50
|
+
return lockHolder == null || lastActiveLockHolder !== lockHolder || isNonTerminalState(currentInvitation.state);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
mutex,
|
|
54
|
+
get current() {
|
|
55
|
+
return currentInvitation;
|
|
56
|
+
},
|
|
57
|
+
// disposing context prevents any further state updates
|
|
58
|
+
complete: (newState: Partial<Invitation>) => {
|
|
59
|
+
logStateUpdate(currentInvitation, undefined, invitation.state);
|
|
60
|
+
currentInvitation = { ...currentInvitation, ...newState };
|
|
61
|
+
stream.next(currentInvitation);
|
|
62
|
+
return ctx.dispose();
|
|
63
|
+
},
|
|
64
|
+
set: (lockHolder: FlowLockHolder | null, newState: Invitation.State): boolean => {
|
|
65
|
+
if (isStateChangeAllowed(lockHolder)) {
|
|
66
|
+
logStateUpdate(currentInvitation, lockHolder, newState);
|
|
67
|
+
currentInvitation = { ...currentInvitation, state: newState };
|
|
68
|
+
stream.next(currentInvitation);
|
|
69
|
+
lastActiveLockHolder = lockHolder;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
},
|
|
74
|
+
error: (lockHolder: FlowLockHolder | null, error: any): boolean => {
|
|
75
|
+
if (isStateChangeAllowed(lockHolder)) {
|
|
76
|
+
logStateUpdate(currentInvitation, lockHolder, Invitation.State.ERROR, error);
|
|
77
|
+
currentInvitation = { ...currentInvitation, state: Invitation.State.ERROR };
|
|
78
|
+
stream.next(currentInvitation);
|
|
79
|
+
stream.error(error);
|
|
80
|
+
lastActiveLockHolder = lockHolder;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const logStateUpdate = (invitation: Invitation, actor: any, newState: Invitation.State, error?: Error) => {
|
|
89
|
+
const logContext = {
|
|
90
|
+
invitationId: invitation.invitationId,
|
|
91
|
+
actor: actor?.constructor.name,
|
|
92
|
+
newState: stateToString(newState),
|
|
93
|
+
oldState: stateToString(invitation.state),
|
|
94
|
+
error: error?.message,
|
|
95
|
+
errorStack: error?.stack,
|
|
96
|
+
};
|
|
97
|
+
if (isNonTerminalState(newState)) {
|
|
98
|
+
log.verbose('dxos.sdk.invitations-handler.state.update', logContext);
|
|
99
|
+
} else {
|
|
100
|
+
log.info('dxos.sdk.invitations-handler.state.update', logContext);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const isNonTerminalState = (currentState: Invitation.State): boolean => {
|
|
105
|
+
return ![
|
|
106
|
+
Invitation.State.SUCCESS,
|
|
107
|
+
Invitation.State.ERROR,
|
|
108
|
+
Invitation.State.CANCELLED,
|
|
109
|
+
Invitation.State.TIMEOUT,
|
|
110
|
+
Invitation.State.EXPIRED,
|
|
111
|
+
].includes(currentState);
|
|
112
|
+
};
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
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 {
|
|
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
|
});
|
|
@@ -70,7 +71,7 @@ describe('InvitationHandler', () => {
|
|
|
70
71
|
|
|
71
72
|
await guest.sink.waitFor(Invitation.State.READY_FOR_AUTHENTICATION);
|
|
72
73
|
await sleep(200);
|
|
73
|
-
await host.sink.waitFor(Invitation.State.
|
|
74
|
+
await host.sink.waitFor(Invitation.State.CONNECTING);
|
|
74
75
|
await guest.sink.waitFor(Invitation.State.TIMEOUT);
|
|
75
76
|
|
|
76
77
|
await sleep(10);
|
|
@@ -90,7 +91,7 @@ describe('InvitationHandler', () => {
|
|
|
90
91
|
|
|
91
92
|
await guest.sink.waitFor(Invitation.State.READY_FOR_AUTHENTICATION);
|
|
92
93
|
await guest.peer.networkManager.close();
|
|
93
|
-
await host.sink.waitFor(Invitation.State.
|
|
94
|
+
await host.sink.waitFor(Invitation.State.CONNECTING);
|
|
94
95
|
|
|
95
96
|
await sleep(10);
|
|
96
97
|
expect(host.ctx.disposed).to.be.false;
|
|
@@ -133,7 +134,7 @@ describe('InvitationHandler', () => {
|
|
|
133
134
|
const host = await createPeer();
|
|
134
135
|
const invitation = await createInvitation(host, { multiUse: true });
|
|
135
136
|
await hostInvitation(host, invitation);
|
|
136
|
-
|
|
137
|
+
await createNewHost(invitation);
|
|
137
138
|
|
|
138
139
|
const guest = await createPeer(host.spaceKey);
|
|
139
140
|
const codeInput = await failAuth(guest, invitation);
|
|
@@ -142,9 +143,6 @@ describe('InvitationHandler', () => {
|
|
|
142
143
|
await sleep(10);
|
|
143
144
|
}
|
|
144
145
|
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
146
|
});
|
|
149
147
|
|
|
150
148
|
test('single guest - many hosts', async () => {
|
|
@@ -163,7 +161,7 @@ describe('InvitationHandler', () => {
|
|
|
163
161
|
expect(guest.ctx.disposed).to.be.true;
|
|
164
162
|
});
|
|
165
163
|
|
|
166
|
-
test('guest gives up after trying with three hosts', async () => {
|
|
164
|
+
test('guest gives up after trying with three hosts', { timeout: 20_000 }, async () => {
|
|
167
165
|
const hosts: PeerSetup[] = [await createPeer()];
|
|
168
166
|
const [host] = hosts;
|
|
169
167
|
const invitation = await createInvitation(host, { multiUse: true });
|
|
@@ -181,7 +179,7 @@ describe('InvitationHandler', () => {
|
|
|
181
179
|
|
|
182
180
|
await sleep(10);
|
|
183
181
|
expect(guest.sink.lastState).to.eq(Invitation.State.ERROR);
|
|
184
|
-
})
|
|
182
|
+
});
|
|
185
183
|
|
|
186
184
|
test('single host - many guests', async () => {
|
|
187
185
|
const hosts: PeerSetup[] = [await createPeer()];
|
|
@@ -256,12 +254,14 @@ describe('InvitationHandler', () => {
|
|
|
256
254
|
const space = await peer.dataSpaceManager.createSpace();
|
|
257
255
|
spaceKey = space.key;
|
|
258
256
|
}
|
|
259
|
-
const invitationHandler = new InvitationsHandler(peer.networkManager, {
|
|
260
|
-
controlHeartbeatInterval: 250, // faster peer failure detection
|
|
257
|
+
const invitationHandler = new InvitationsHandler(peer.networkManager, undefined, {
|
|
258
|
+
teleport: { controlHeartbeatInterval: 250 }, // faster peer failure detection
|
|
261
259
|
});
|
|
262
260
|
const protocol = new SpaceInvitationProtocol(peer.dataSpaceManager, peer.identity, peer.keyring, spaceKey);
|
|
263
261
|
const ctx = new Context();
|
|
264
|
-
|
|
262
|
+
onTestFinished(async () => {
|
|
263
|
+
await ctx.dispose();
|
|
264
|
+
});
|
|
265
265
|
const sink = newStateUpdateSink();
|
|
266
266
|
return { ctx, sink, peer, protocol, handler: invitationHandler, spaceKey };
|
|
267
267
|
};
|
|
@@ -269,14 +269,18 @@ describe('InvitationHandler', () => {
|
|
|
269
269
|
const hostInvitation = async (setup: PeerSetup, invitation: Invitation) => {
|
|
270
270
|
await setup.ctx.dispose();
|
|
271
271
|
setup.ctx = new Context();
|
|
272
|
-
|
|
272
|
+
onTestFinished(async () => {
|
|
273
|
+
await setup.ctx.dispose();
|
|
274
|
+
});
|
|
273
275
|
setup.handler.handleInvitationFlow(setup.ctx, setup.sink, setup.protocol, invitation);
|
|
274
276
|
};
|
|
275
277
|
|
|
276
278
|
const acceptInvitation = async (setup: PeerSetup, invitation: Invitation): Promise<Trigger<string>> => {
|
|
277
279
|
await setup.ctx.dispose();
|
|
278
280
|
setup.ctx = new Context();
|
|
279
|
-
|
|
281
|
+
onTestFinished(async () => {
|
|
282
|
+
await setup.ctx.dispose();
|
|
283
|
+
});
|
|
280
284
|
const authCodeInput = new Trigger<string>();
|
|
281
285
|
setup.handler.acceptInvitation(setup.ctx, setup.sink, setup.protocol, invitation, authCodeInput);
|
|
282
286
|
return authCodeInput;
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type PushStream, scheduleTask, TimeoutError, type Trigger } from '@dxos/async';
|
|
6
6
|
import { INVITATION_TIMEOUT } from '@dxos/client-protocol';
|
|
7
7
|
import { type Context, ContextDisposedError } from '@dxos/context';
|
|
8
8
|
import { createKeyPair, sign } from '@dxos/crypto';
|
|
9
|
+
import { type EdgeHttpClient } from '@dxos/edge-client';
|
|
9
10
|
import { invariant } from '@dxos/invariant';
|
|
10
11
|
import { PublicKey } from '@dxos/keys';
|
|
11
12
|
import { log } from '@dxos/log';
|
|
@@ -19,17 +20,21 @@ import { type ExtensionContext, type TeleportExtension, type TeleportParams } fr
|
|
|
19
20
|
import { trace as _trace } from '@dxos/tracing';
|
|
20
21
|
import { ComplexSet } from '@dxos/util';
|
|
21
22
|
|
|
23
|
+
import { type EdgeInvitationConfig, EdgeInvitationHandler } from './edge-invitation-handler';
|
|
22
24
|
import { InvitationGuestExtension } from './invitation-guest-extenstion';
|
|
23
25
|
import { InvitationHostExtension, isAuthenticationRequired, MAX_OTP_ATTEMPTS } from './invitation-host-extension';
|
|
24
26
|
import { type InvitationProtocol } from './invitation-protocol';
|
|
27
|
+
import { createGuardedInvitationState } from './invitation-state';
|
|
25
28
|
import { InvitationTopology } from './invitation-topology';
|
|
26
|
-
import { stateToString } from './utils';
|
|
27
29
|
|
|
28
30
|
const metrics = _trace.metrics;
|
|
29
31
|
|
|
30
32
|
const MAX_DELEGATED_INVITATION_HOST_TRIES = 3;
|
|
31
33
|
|
|
32
|
-
type
|
|
34
|
+
export type InvitationConnectionParams = {
|
|
35
|
+
teleport: Partial<TeleportParams>;
|
|
36
|
+
edgeInvitations?: EdgeInvitationConfig;
|
|
37
|
+
};
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
40
|
* Generic handler for Halo and Space invitations.
|
|
@@ -65,7 +70,8 @@ export class InvitationsHandler {
|
|
|
65
70
|
*/
|
|
66
71
|
constructor(
|
|
67
72
|
private readonly _networkManager: SwarmNetworkManager,
|
|
68
|
-
private readonly
|
|
73
|
+
private readonly _edgeClient?: EdgeHttpClient,
|
|
74
|
+
private readonly _connectionParams?: InvitationConnectionParams,
|
|
69
75
|
) {}
|
|
70
76
|
|
|
71
77
|
handleInvitationFlow(
|
|
@@ -74,8 +80,14 @@ export class InvitationsHandler {
|
|
|
74
80
|
protocol: InvitationProtocol,
|
|
75
81
|
invitation: Invitation,
|
|
76
82
|
): void {
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
log.verbose('dxos.sdk.invitations-handler.handleInvitationFlow', {
|
|
84
|
+
state: invitation.state,
|
|
85
|
+
invitationId: invitation.invitationId,
|
|
86
|
+
kind: invitation.kind,
|
|
87
|
+
type: invitation.type,
|
|
88
|
+
});
|
|
89
|
+
metrics.increment('dxos.invitation.host');
|
|
90
|
+
const guardedState = createGuardedInvitationState(ctx, invitation, stream);
|
|
79
91
|
// Called for every connecting peer.
|
|
80
92
|
const createExtension = (): InvitationHostExtension => {
|
|
81
93
|
const extension = new InvitationHostExtension(guardedState.mutex, {
|
|
@@ -84,12 +96,18 @@ export class InvitationsHandler {
|
|
|
84
96
|
},
|
|
85
97
|
|
|
86
98
|
onStateUpdate: (newState: Invitation.State): Invitation => {
|
|
87
|
-
|
|
99
|
+
if (newState !== Invitation.State.ERROR && newState !== Invitation.State.TIMEOUT) {
|
|
100
|
+
guardedState.set(extension, newState);
|
|
101
|
+
}
|
|
88
102
|
return guardedState.current;
|
|
89
103
|
},
|
|
90
104
|
|
|
91
105
|
admit: async (admissionRequest) => {
|
|
92
106
|
try {
|
|
107
|
+
log.verbose('dxos.sdk.invitations-handler.host.admit', {
|
|
108
|
+
invitationId: invitation.invitationId,
|
|
109
|
+
...protocol.toJSON(),
|
|
110
|
+
});
|
|
93
111
|
const deviceKey = admissionRequest.device?.deviceKey ?? admissionRequest.space?.deviceKey;
|
|
94
112
|
invariant(deviceKey);
|
|
95
113
|
const admissionResponse = await protocol.admit(invitation, admissionRequest, extension.guestProfile);
|
|
@@ -109,7 +127,7 @@ export class InvitationsHandler {
|
|
|
109
127
|
let admitted = false;
|
|
110
128
|
connectionCtx.onDispose(() => {
|
|
111
129
|
if (!admitted) {
|
|
112
|
-
guardedState.
|
|
130
|
+
guardedState.set(extension, Invitation.State.CONNECTING);
|
|
113
131
|
}
|
|
114
132
|
});
|
|
115
133
|
|
|
@@ -117,9 +135,9 @@ export class InvitationsHandler {
|
|
|
117
135
|
const traceId = PublicKey.random().toHex();
|
|
118
136
|
try {
|
|
119
137
|
log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.begin({ id: traceId }));
|
|
120
|
-
log('connected', { ...protocol.toJSON() });
|
|
138
|
+
log.verbose('connected', { ...protocol.toJSON() });
|
|
121
139
|
const deviceKey = await extension.completedTrigger.wait({ timeout: invitation.timeout });
|
|
122
|
-
log('admitted guest', { guest: deviceKey, ...protocol.toJSON() });
|
|
140
|
+
log.verbose('admitted guest', { guest: deviceKey, ...protocol.toJSON() });
|
|
123
141
|
guardedState.set(extension, Invitation.State.SUCCESS);
|
|
124
142
|
metrics.increment('dxos.invitation.success');
|
|
125
143
|
log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.end({ id: traceId }));
|
|
@@ -129,13 +147,14 @@ export class InvitationsHandler {
|
|
|
129
147
|
await ctx.dispose();
|
|
130
148
|
}
|
|
131
149
|
} catch (err: any) {
|
|
150
|
+
const stateChanged = guardedState.set(extension, Invitation.State.CONNECTING);
|
|
132
151
|
if (err instanceof TimeoutError) {
|
|
133
|
-
if (
|
|
152
|
+
if (stateChanged) {
|
|
134
153
|
metrics.increment('dxos.invitation.timeout');
|
|
135
|
-
log('timeout', { ...protocol.toJSON() });
|
|
154
|
+
log.verbose('timeout', { ...protocol.toJSON() });
|
|
136
155
|
}
|
|
137
156
|
} else {
|
|
138
|
-
if (
|
|
157
|
+
if (stateChanged) {
|
|
139
158
|
metrics.increment('dxos.invitation.failed');
|
|
140
159
|
log.error('failed', err);
|
|
141
160
|
}
|
|
@@ -147,17 +166,18 @@ export class InvitationsHandler {
|
|
|
147
166
|
});
|
|
148
167
|
},
|
|
149
168
|
onError: (err) => {
|
|
169
|
+
const stateChanged = guardedState.set(extension, Invitation.State.CONNECTING);
|
|
150
170
|
if (err instanceof InvalidInvitationExtensionRoleError) {
|
|
151
171
|
log('invalid role', { ...err.context });
|
|
152
172
|
return;
|
|
153
173
|
}
|
|
154
174
|
if (err instanceof TimeoutError) {
|
|
155
|
-
if (
|
|
175
|
+
if (stateChanged) {
|
|
156
176
|
metrics.increment('dxos.invitation.timeout');
|
|
157
|
-
log('timeout', { err });
|
|
177
|
+
log.verbose('timeout', { err });
|
|
158
178
|
}
|
|
159
179
|
} else {
|
|
160
|
-
if (
|
|
180
|
+
if (stateChanged) {
|
|
161
181
|
metrics.increment('dxos.invitation.failed');
|
|
162
182
|
log.error('failed', err);
|
|
163
183
|
}
|
|
@@ -171,19 +191,21 @@ export class InvitationsHandler {
|
|
|
171
191
|
if (invitation.lifetime && invitation.created) {
|
|
172
192
|
if (invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()) {
|
|
173
193
|
log.warn('invitation has already expired');
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
async () => {
|
|
178
|
-
// ensure the swarm is closed before changing state and closing the stream.
|
|
179
|
-
await swarmConnection.close();
|
|
180
|
-
guardedState.set(null, Invitation.State.EXPIRED);
|
|
181
|
-
metrics.increment('dxos.invitation.expired');
|
|
182
|
-
await ctx.dispose();
|
|
183
|
-
},
|
|
184
|
-
invitation.created.getTime() + invitation.lifetime * 1000 - Date.now(),
|
|
185
|
-
);
|
|
194
|
+
guardedState.set(null, Invitation.State.EXPIRED);
|
|
195
|
+
void ctx.dispose().catch((err) => log.catch(err));
|
|
196
|
+
return;
|
|
186
197
|
}
|
|
198
|
+
scheduleTask(
|
|
199
|
+
ctx,
|
|
200
|
+
async () => {
|
|
201
|
+
// ensure the swarm is closed before changing state and closing the stream.
|
|
202
|
+
await swarmConnection.close();
|
|
203
|
+
guardedState.set(null, Invitation.State.EXPIRED);
|
|
204
|
+
metrics.increment('dxos.invitation.expired');
|
|
205
|
+
await ctx.dispose();
|
|
206
|
+
},
|
|
207
|
+
invitation.created.getTime() + invitation.lifetime * 1000 - Date.now(),
|
|
208
|
+
);
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
let swarmConnection: SwarmConnection;
|
|
@@ -201,6 +223,12 @@ export class InvitationsHandler {
|
|
|
201
223
|
otpEnteredTrigger: Trigger<string>,
|
|
202
224
|
deviceProfile?: DeviceProfileDocument,
|
|
203
225
|
): void {
|
|
226
|
+
log.verbose('dxos.sdk.invitations-handler.acceptInvitation', {
|
|
227
|
+
state: invitation.state,
|
|
228
|
+
invitationId: invitation.invitationId,
|
|
229
|
+
kind: invitation.kind,
|
|
230
|
+
type: invitation.type,
|
|
231
|
+
});
|
|
204
232
|
const { timeout = INVITATION_TIMEOUT } = invitation;
|
|
205
233
|
|
|
206
234
|
if (deviceProfile) {
|
|
@@ -208,7 +236,7 @@ export class InvitationsHandler {
|
|
|
208
236
|
}
|
|
209
237
|
|
|
210
238
|
const triedPeersIds = new ComplexSet(PublicKey.hash);
|
|
211
|
-
const guardedState =
|
|
239
|
+
const guardedState = createGuardedInvitationState(ctx, invitation, stream);
|
|
212
240
|
|
|
213
241
|
const shouldCancelInvitationFlow = (extension: InvitationGuestExtension) => {
|
|
214
242
|
const isLockedByAnotherConnection = guardedState.mutex.isLocked() && !extension.hasFlowLock();
|
|
@@ -240,7 +268,7 @@ export class InvitationsHandler {
|
|
|
240
268
|
}
|
|
241
269
|
|
|
242
270
|
connectionCtx.onDispose(async () => {
|
|
243
|
-
log('extension disposed', { admitted, currentState: guardedState.current.state });
|
|
271
|
+
log.verbose('extension disposed', { admitted, currentState: guardedState.current.state });
|
|
244
272
|
if (!admitted) {
|
|
245
273
|
guardedState.error(extension, new ContextDisposedError());
|
|
246
274
|
if (shouldCancelInvitationFlow(extension)) {
|
|
@@ -263,16 +291,23 @@ export class InvitationsHandler {
|
|
|
263
291
|
timeout,
|
|
264
292
|
);
|
|
265
293
|
|
|
266
|
-
log('connected', { ...protocol.toJSON() });
|
|
294
|
+
log.verbose('dxos.sdk.invitations-handler.guest.connected', { ...protocol.toJSON() });
|
|
267
295
|
guardedState.set(extension, Invitation.State.CONNECTED);
|
|
268
296
|
|
|
269
297
|
// 1. Introduce guest to host.
|
|
270
|
-
log('introduce', {
|
|
298
|
+
log.verbose('dxos.sdk.invitations-handler.guest.introduce', {
|
|
299
|
+
invitationId: invitation.invitationId,
|
|
300
|
+
...protocol.toJSON(),
|
|
301
|
+
});
|
|
271
302
|
const introductionResponse = await extension.rpc.InvitationHostService.introduce({
|
|
272
303
|
invitationId: invitation.invitationId,
|
|
273
304
|
...protocol.createIntroduction(),
|
|
274
305
|
});
|
|
275
|
-
log('introduce
|
|
306
|
+
log.verbose('dxos.sdk.invitations-handler.guest.introduce-response', {
|
|
307
|
+
invitationId: invitation.invitationId,
|
|
308
|
+
...protocol.toJSON(),
|
|
309
|
+
authMethod: introductionResponse.authMethod,
|
|
310
|
+
});
|
|
276
311
|
invitation.authMethod = introductionResponse.authMethod;
|
|
277
312
|
|
|
278
313
|
// 2. Get authentication code.
|
|
@@ -298,7 +333,10 @@ export class InvitationsHandler {
|
|
|
298
333
|
}
|
|
299
334
|
|
|
300
335
|
// 3. Send admission credentials to host (with local space keys).
|
|
301
|
-
log('request
|
|
336
|
+
log.verbose('dxos.sdk.invitations-handler.guest.request-admission', {
|
|
337
|
+
invitationId: invitation.invitationId,
|
|
338
|
+
...protocol.toJSON(),
|
|
339
|
+
});
|
|
302
340
|
const admissionRequest = await protocol.createAdmissionRequest(deviceProfile);
|
|
303
341
|
const admissionResponse = await extension.rpc.InvitationHostService.admit(admissionRequest);
|
|
304
342
|
|
|
@@ -309,8 +347,11 @@ export class InvitationsHandler {
|
|
|
309
347
|
const result = await protocol.accept(admissionResponse, admissionRequest);
|
|
310
348
|
|
|
311
349
|
// 5. Success.
|
|
312
|
-
log('admitted
|
|
313
|
-
|
|
350
|
+
log.verbose('dxos.sdk.invitations-handler.guest.admitted-by-host', {
|
|
351
|
+
invitationId: invitation.invitationId,
|
|
352
|
+
...protocol.toJSON(),
|
|
353
|
+
});
|
|
354
|
+
guardedState.complete({
|
|
314
355
|
...guardedState.current,
|
|
315
356
|
...result,
|
|
316
357
|
state: Invitation.State.SUCCESS,
|
|
@@ -318,10 +359,10 @@ export class InvitationsHandler {
|
|
|
318
359
|
log.trace('dxos.sdk.invitations-handler.guest.onOpen', trace.end({ id: traceId }));
|
|
319
360
|
} catch (err: any) {
|
|
320
361
|
if (err instanceof TimeoutError) {
|
|
321
|
-
log('timeout', { ...protocol.toJSON() });
|
|
362
|
+
log.verbose('timeout', { ...protocol.toJSON() });
|
|
322
363
|
guardedState.set(extension, Invitation.State.TIMEOUT);
|
|
323
364
|
} else {
|
|
324
|
-
log('auth failed', err);
|
|
365
|
+
log.verbose('auth failed', err);
|
|
325
366
|
guardedState.error(extension, err);
|
|
326
367
|
}
|
|
327
368
|
extensionCtx.close(err);
|
|
@@ -334,10 +375,10 @@ export class InvitationsHandler {
|
|
|
334
375
|
return;
|
|
335
376
|
}
|
|
336
377
|
if (err instanceof TimeoutError) {
|
|
337
|
-
log('timeout', { ...protocol.toJSON() });
|
|
378
|
+
log.verbose('timeout', { ...protocol.toJSON() });
|
|
338
379
|
guardedState.set(extension, Invitation.State.TIMEOUT);
|
|
339
380
|
} else {
|
|
340
|
-
log('auth failed', err);
|
|
381
|
+
log.verbose('auth failed', err);
|
|
341
382
|
guardedState.error(extension, err);
|
|
342
383
|
}
|
|
343
384
|
},
|
|
@@ -346,6 +387,15 @@ export class InvitationsHandler {
|
|
|
346
387
|
return extension;
|
|
347
388
|
};
|
|
348
389
|
|
|
390
|
+
const edgeInvitationHandler = new EdgeInvitationHandler(this._connectionParams?.edgeInvitations, this._edgeClient, {
|
|
391
|
+
onInvitationSuccess: async (admissionResponse, admissionRequest) => {
|
|
392
|
+
const result = await protocol.accept(admissionResponse, admissionRequest);
|
|
393
|
+
log.info('admitted by edge', { ...protocol.toJSON() });
|
|
394
|
+
guardedState.complete({ ...guardedState.current, ...result, state: Invitation.State.SUCCESS });
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
edgeInvitationHandler.handle(ctx, guardedState, protocol, deviceProfile);
|
|
398
|
+
|
|
349
399
|
scheduleTask(ctx, async () => {
|
|
350
400
|
const error = protocol.checkInvitation(invitation);
|
|
351
401
|
if (error) {
|
|
@@ -389,7 +439,7 @@ export class InvitationsHandler {
|
|
|
389
439
|
topic: invitation.swarmKey,
|
|
390
440
|
protocolProvider: createTeleportProtocolFactory(async (teleport) => {
|
|
391
441
|
teleport.addExtension('dxos.halo.invitations', extensionFactory());
|
|
392
|
-
}, this.
|
|
442
|
+
}, this._connectionParams?.teleport),
|
|
393
443
|
topology: new InvitationTopology(role),
|
|
394
444
|
label,
|
|
395
445
|
});
|
|
@@ -397,90 +447,6 @@ export class InvitationsHandler {
|
|
|
397
447
|
return swarmConnection;
|
|
398
448
|
}
|
|
399
449
|
|
|
400
|
-
/**
|
|
401
|
-
* A utility object for serializing invitation state changes by multiple concurrent
|
|
402
|
-
* invitation flow connections.
|
|
403
|
-
*/
|
|
404
|
-
private _createGuardedState(ctx: Context, invitation: Invitation, stream: PushStream<Invitation>) {
|
|
405
|
-
// the mutex guards invitation flow on host and guest side, making sure only one flow is currently active
|
|
406
|
-
// deadlocks seem very unlikely because hosts don't initiate multiple connections
|
|
407
|
-
// even if this somehow happens that there are 2 guests (A, B) and 2 hosts (1, 2) and:
|
|
408
|
-
// A has lock for flow with 1, B has lock for flow with 2
|
|
409
|
-
// 1 has lock for flow with B, 2 has lock for flow with A
|
|
410
|
-
// there'll be a 10-second introduction timeout after which connection will be closed and deadlock broken
|
|
411
|
-
const mutex = new Mutex();
|
|
412
|
-
let lastActiveExtension: any = null;
|
|
413
|
-
let currentInvitation = { ...invitation };
|
|
414
|
-
const isStateChangeAllowed = (extension: InvitationExtension | null) => {
|
|
415
|
-
if (ctx.disposed || (extension !== null && mutex.isLocked() && !extension.hasFlowLock())) {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
// don't allow transitions from a terminal state unless a new extension acquired mutex
|
|
419
|
-
// handles a case when error occurs (e.g. connection is closed) after we completed the flow
|
|
420
|
-
// successfully or already reported another error
|
|
421
|
-
return extension == null || lastActiveExtension !== extension || this._isNotTerminal(currentInvitation.state);
|
|
422
|
-
};
|
|
423
|
-
return {
|
|
424
|
-
mutex,
|
|
425
|
-
get current() {
|
|
426
|
-
return currentInvitation;
|
|
427
|
-
},
|
|
428
|
-
// disposing context prevents any further state updates
|
|
429
|
-
complete: (newState: Partial<Invitation>) => {
|
|
430
|
-
currentInvitation = { ...currentInvitation, ...newState };
|
|
431
|
-
stream.next(currentInvitation);
|
|
432
|
-
return ctx.dispose();
|
|
433
|
-
},
|
|
434
|
-
set: (extension: InvitationExtension | null, newState: Invitation.State): boolean => {
|
|
435
|
-
if (isStateChangeAllowed(extension)) {
|
|
436
|
-
this._logStateUpdate(currentInvitation, extension, newState);
|
|
437
|
-
currentInvitation = { ...currentInvitation, state: newState };
|
|
438
|
-
stream.next(currentInvitation);
|
|
439
|
-
lastActiveExtension = extension;
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
return false;
|
|
443
|
-
},
|
|
444
|
-
error: (extension: InvitationExtension | null, error: any): boolean => {
|
|
445
|
-
if (isStateChangeAllowed(extension)) {
|
|
446
|
-
this._logStateUpdate(currentInvitation, extension, Invitation.State.ERROR);
|
|
447
|
-
currentInvitation = { ...currentInvitation, state: Invitation.State.ERROR };
|
|
448
|
-
stream.next(currentInvitation);
|
|
449
|
-
stream.error(error);
|
|
450
|
-
lastActiveExtension = extension;
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
return false;
|
|
454
|
-
},
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
private _logStateUpdate(invitation: Invitation, actor: any, newState: Invitation.State) {
|
|
459
|
-
if (this._isNotTerminal(newState)) {
|
|
460
|
-
log('invitation state update', {
|
|
461
|
-
actor: actor?.constructor.name,
|
|
462
|
-
newState: stateToString(newState),
|
|
463
|
-
oldState: stateToString(invitation.state),
|
|
464
|
-
});
|
|
465
|
-
} else {
|
|
466
|
-
log.info('invitation state update', {
|
|
467
|
-
actor: actor?.constructor.name,
|
|
468
|
-
newState: stateToString(newState),
|
|
469
|
-
oldState: stateToString(invitation.state),
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private _isNotTerminal(currentState: Invitation.State): boolean {
|
|
475
|
-
return ![
|
|
476
|
-
Invitation.State.SUCCESS,
|
|
477
|
-
Invitation.State.ERROR,
|
|
478
|
-
Invitation.State.CANCELLED,
|
|
479
|
-
Invitation.State.TIMEOUT,
|
|
480
|
-
Invitation.State.EXPIRED,
|
|
481
|
-
].includes(currentState);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
450
|
private async _handleGuestOtpAuth(
|
|
485
451
|
extension: InvitationGuestExtension,
|
|
486
452
|
setState: (newState: Invitation.State) => void,
|
|
@@ -523,7 +489,6 @@ export class InvitationsHandler {
|
|
|
523
489
|
throw new Error('challenge missing in the introduction');
|
|
524
490
|
}
|
|
525
491
|
log('sending authentication request');
|
|
526
|
-
setState(Invitation.State.AUTHENTICATING);
|
|
527
492
|
const signature = sign(Buffer.from(introductionResponse.challenge), invitation.guestKeypair.privateKey);
|
|
528
493
|
const response = await extension.rpc.InvitationHostService.authenticate({
|
|
529
494
|
signedChallenge: signature,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type InvitationsService,
|
|
11
11
|
QueryInvitationsResponse,
|
|
12
12
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
13
|
+
import { trace } from '@dxos/tracing';
|
|
13
14
|
|
|
14
15
|
import { type InvitationsManager } from './invitations-manager';
|
|
15
16
|
|
|
@@ -30,7 +31,10 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
30
31
|
return new Stream<Invitation>(({ next, close }) => {
|
|
31
32
|
void this._invitationsManager
|
|
32
33
|
.createInvitation(options)
|
|
33
|
-
.then((invitation) =>
|
|
34
|
+
.then((invitation) => {
|
|
35
|
+
trace.metrics.increment('dxos.invitation.created');
|
|
36
|
+
invitation.subscribe(next, close, close);
|
|
37
|
+
})
|
|
34
38
|
.catch(close);
|
|
35
39
|
});
|
|
36
40
|
}
|
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { onTestFinished, describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { asyncChain, Trigger } from '@dxos/async';
|
|
8
8
|
import { raise } from '@dxos/debug';
|
|
9
9
|
import { AlreadyJoinedError } from '@dxos/protocols';
|
|
10
10
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
|
-
import { afterTest, describe, test } from '@dxos/test';
|
|
12
11
|
|
|
13
12
|
import { type ServiceContext } from '../services';
|
|
14
13
|
import { createIdentity, createPeers } from '../testing';
|
|
15
14
|
import { acceptInvitation, createInvitation, performInvitation } from '../testing/invitation-utils';
|
|
16
15
|
|
|
17
16
|
const closeAfterTest = async (peer: ServiceContext) => {
|
|
18
|
-
|
|
17
|
+
onTestFinished(async () => {
|
|
18
|
+
await peer.close();
|
|
19
|
+
});
|
|
19
20
|
return peer;
|
|
20
21
|
};
|
|
21
22
|
|