@dxos/client-services 0.1.35 → 0.1.36
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-H5IXSSI7.mjs → chunk-2AYYVPWN.mjs} +835 -950
- package/dist/lib/browser/chunk-2AYYVPWN.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +9 -11
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +212 -74
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/index.cjs +942 -1058
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +1089 -1067
- package/dist/lib/node/packlets/testing/index.cjs.map +4 -4
- package/dist/types/src/packlets/identity/identity-manager.d.ts +1 -0
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +3 -3
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +18 -0
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/device-invitation-protocol.test.d.ts +2 -0
- package/dist/types/src/packlets/invitations/device-invitation-protocol.test.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/index.d.ts +4 -4
- package/dist/types/src/packlets/invitations/index.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +12 -0
- package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +38 -0
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/invitations-service.d.ts +13 -15
- package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +22 -0
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/space-invitation-protocol.test.d.ts +2 -0
- package/dist/types/src/packlets/invitations/space-invitation-protocol.test.d.ts.map +1 -0
- package/dist/types/src/packlets/services/service-context.d.ts +6 -4
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +2 -0
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts +31 -2
- package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +3 -3
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/tests/invitations.test.d.ts +2 -0
- package/dist/types/src/packlets/tests/invitations.test.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/worker-runtime.d.ts +2 -0
- package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
- package/package.json +30 -30
- package/src/packlets/identity/identity-manager.ts +2 -1
- package/src/packlets/identity/identity.ts +3 -3
- package/src/packlets/invitations/{device-invitations-handler.test.ts → device-invitation-protocol.test.ts} +3 -3
- package/src/packlets/invitations/device-invitation-protocol.ts +83 -0
- package/src/packlets/invitations/index.ts +4 -4
- package/src/packlets/invitations/invitation-protocol.ts +21 -0
- package/src/packlets/invitations/invitations-handler.ts +462 -0
- package/src/packlets/invitations/invitations-service.ts +127 -104
- package/src/packlets/invitations/space-invitation-protocol.test.ts +221 -0
- package/src/packlets/invitations/space-invitation-protocol.ts +117 -0
- package/src/packlets/services/service-context.test.ts +11 -4
- package/src/packlets/services/service-context.ts +40 -19
- package/src/packlets/services/service-host.ts +4 -12
- package/src/packlets/services/service-registry.test.ts +23 -25
- package/src/packlets/spaces/data-space-manager.ts +5 -0
- package/src/packlets/spaces/spaces-service.ts +7 -2
- package/src/packlets/storage/storage.ts +2 -0
- package/src/packlets/testing/invitation-utils.ts +219 -31
- package/src/packlets/testing/test-builder.ts +33 -22
- package/src/packlets/tests/client-services.test.ts +33 -76
- package/src/packlets/tests/halo-profile.test.ts +2 -28
- package/src/packlets/tests/invitations.test.ts +389 -0
- package/src/packlets/tests/spaces-invitations.test.ts +12 -30
- package/src/packlets/tests/spaces.test.ts +30 -28
- package/src/packlets/vault/worker-runtime.ts +4 -0
- package/dist/lib/browser/chunk-H5IXSSI7.mjs.map +0 -7
- package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts +0 -30
- package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/device-invitations-handler.test.d.ts +0 -2
- package/dist/types/src/packlets/invitations/device-invitations-handler.test.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/device-invitations-service.d.ts +0 -13
- package/dist/types/src/packlets/invitations/device-invitations-service.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/device-invitations-service.test.d.ts +0 -2
- package/dist/types/src/packlets/invitations/device-invitations-service.test.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts +0 -26
- package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/space-invitations-handler.test.d.ts +0 -2
- package/dist/types/src/packlets/invitations/space-invitations-handler.test.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/space-invitations-service.d.ts +0 -16
- package/dist/types/src/packlets/invitations/space-invitations-service.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/space-invitations-service.test.d.ts +0 -2
- package/dist/types/src/packlets/invitations/space-invitations-service.test.d.ts.map +0 -1
- package/src/packlets/invitations/device-invitations-handler.ts +0 -370
- package/src/packlets/invitations/device-invitations-service.test.ts +0 -96
- package/src/packlets/invitations/device-invitations-service.ts +0 -24
- package/src/packlets/invitations/space-invitations-handler.test.ts +0 -246
- package/src/packlets/invitations/space-invitations-handler.ts +0 -463
- package/src/packlets/invitations/space-invitations-service.test.ts +0 -129
- package/src/packlets/invitations/space-invitations-service.ts +0 -35
|
@@ -4,92 +4,98 @@
|
|
|
4
4
|
|
|
5
5
|
import assert from 'node:assert';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
AuthenticatingInvitationObservable,
|
|
10
|
-
CancellableInvitationObservable,
|
|
11
|
-
InvitationsService,
|
|
12
|
-
InvitationsHandler,
|
|
13
|
-
InvitationsOptions
|
|
14
|
-
} from '@dxos/client';
|
|
7
|
+
import { AuthenticatingInvitationObservable, CancellableInvitationObservable } from '@dxos/client';
|
|
15
8
|
import { Stream } from '@dxos/codec-protobuf';
|
|
16
9
|
import { log } from '@dxos/log';
|
|
17
|
-
import { AuthenticationRequest, Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
18
|
-
import { Provider } from '@dxos/util';
|
|
10
|
+
import { AuthenticationRequest, Invitation, InvitationsService } from '@dxos/protocols/proto/dxos/client/services';
|
|
19
11
|
|
|
20
|
-
import {
|
|
12
|
+
import { InvitationProtocol } from './invitation-protocol';
|
|
13
|
+
import { InvitationsHandler } from './invitations-handler';
|
|
21
14
|
|
|
22
15
|
/**
|
|
23
16
|
* Adapts invitation service observable to client/service stream.
|
|
24
17
|
*/
|
|
25
|
-
export
|
|
18
|
+
export class InvitationsServiceImpl implements InvitationsService {
|
|
26
19
|
private readonly _createInvitations = new Map<string, CancellableInvitationObservable>();
|
|
27
20
|
private readonly _acceptInvitations = new Map<string, AuthenticatingInvitationObservable>();
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
private readonly
|
|
32
|
-
private readonly _getInvitationsHandler: Provider<InvitationsHandler<T>>
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly _invitationsHandler: InvitationsHandler,
|
|
24
|
+
private readonly _getHandler: (invitation: Invitation) => InvitationProtocol
|
|
33
25
|
) {}
|
|
34
26
|
|
|
35
27
|
// TODO(burdon): Guest/host label.
|
|
36
28
|
getLoggingContext() {
|
|
37
29
|
return {
|
|
38
|
-
deviceKey: this._identityManager.identity?.deviceKey
|
|
30
|
+
// deviceKey: this._identityManager.identity?.deviceKey
|
|
39
31
|
};
|
|
40
32
|
}
|
|
41
33
|
|
|
42
|
-
abstract getContext(invitation: Invitation): T;
|
|
43
|
-
|
|
44
34
|
createInvitation(invitation: Invitation): Stream<Invitation> {
|
|
45
35
|
return new Stream<Invitation>(({ next, close }) => {
|
|
46
|
-
const
|
|
47
|
-
const context = this.getContext(invitation);
|
|
36
|
+
const handler = this._getHandler(invitation);
|
|
48
37
|
log('stream opened', this.getLoggingContext());
|
|
49
38
|
|
|
50
39
|
let invitationId: string;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
40
|
+
const observable = this._invitationsHandler.createInvitation(handler, invitation);
|
|
41
|
+
observable.subscribe(
|
|
42
|
+
(invitation) => {
|
|
43
|
+
switch (invitation.state) {
|
|
44
|
+
case Invitation.State.CONNECTING: {
|
|
45
|
+
assert(invitation.invitationId);
|
|
46
|
+
invitationId = invitation.invitationId;
|
|
47
|
+
this._createInvitations.set(invitation.invitationId, observable);
|
|
48
|
+
invitation.state = Invitation.State.CONNECTING;
|
|
49
|
+
next(invitation);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case Invitation.State.CONNECTED: {
|
|
53
|
+
assert(invitation.invitationId);
|
|
54
|
+
invitation.state = Invitation.State.CONNECTED;
|
|
55
|
+
next(invitation);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case Invitation.State.READY_FOR_AUTHENTICATION: {
|
|
59
|
+
assert(invitation.invitationId);
|
|
60
|
+
invitation.state = Invitation.State.READY_FOR_AUTHENTICATION;
|
|
61
|
+
next(invitation);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case Invitation.State.AUTHENTICATING: {
|
|
65
|
+
assert(invitation.invitationId);
|
|
66
|
+
invitation.state = Invitation.State.AUTHENTICATING;
|
|
67
|
+
next(invitation);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case Invitation.State.SUCCESS: {
|
|
71
|
+
assert(invitation.invitationId);
|
|
72
|
+
invitation.state = Invitation.State.SUCCESS;
|
|
73
|
+
next(invitation);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case Invitation.State.CANCELLED: {
|
|
77
|
+
assert(invitationId);
|
|
78
|
+
invitation.invitationId = invitationId;
|
|
79
|
+
invitation.state = Invitation.State.CANCELLED;
|
|
80
|
+
next(invitation);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case Invitation.State.TIMEOUT: {
|
|
84
|
+
assert(invitationId);
|
|
85
|
+
invitation.invitationId = invitationId;
|
|
86
|
+
invitation.state = Invitation.State.TIMEOUT;
|
|
87
|
+
next(invitation);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
76
91
|
},
|
|
77
|
-
|
|
78
|
-
assert(invitationId);
|
|
79
|
-
invitation.invitationId = invitationId;
|
|
80
|
-
invitation.state = Invitation.State.CANCELLED;
|
|
81
|
-
next(invitation);
|
|
82
|
-
close();
|
|
83
|
-
},
|
|
84
|
-
onTimeout: (err: TimeoutError) => {
|
|
85
|
-
invitation.state = Invitation.State.TIMEOUT;
|
|
92
|
+
(err: Error) => {
|
|
86
93
|
close(err);
|
|
87
94
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
close(err);
|
|
95
|
+
() => {
|
|
96
|
+
close();
|
|
91
97
|
}
|
|
92
|
-
|
|
98
|
+
);
|
|
93
99
|
|
|
94
100
|
return (err?: Error) => {
|
|
95
101
|
const context = this.getLoggingContext();
|
|
@@ -104,52 +110,70 @@ export abstract class AbstractInvitationsService<T = void> implements Invitation
|
|
|
104
110
|
});
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
acceptInvitation(invitation: Invitation
|
|
113
|
+
acceptInvitation(invitation: Invitation): Stream<Invitation> {
|
|
108
114
|
return new Stream<Invitation>(({ next, close }) => {
|
|
109
115
|
log('stream opened', this.getLoggingContext());
|
|
110
|
-
const
|
|
116
|
+
const handler = this._getHandler(invitation);
|
|
111
117
|
|
|
112
118
|
let invitationId: string;
|
|
113
|
-
const observable =
|
|
114
|
-
observable.subscribe(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
const observable = this._invitationsHandler.acceptInvitation(handler, invitation);
|
|
120
|
+
observable.subscribe(
|
|
121
|
+
(invitation) => {
|
|
122
|
+
switch (invitation.state) {
|
|
123
|
+
case Invitation.State.CONNECTING: {
|
|
124
|
+
assert(invitation.invitationId);
|
|
125
|
+
invitationId = invitation.invitationId;
|
|
126
|
+
this._acceptInvitations.set(invitation.invitationId, observable);
|
|
127
|
+
invitation.state = Invitation.State.CONNECTING;
|
|
128
|
+
next(invitation);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case Invitation.State.CONNECTED: {
|
|
132
|
+
assert(invitation.invitationId);
|
|
133
|
+
invitation.state = Invitation.State.CONNECTED;
|
|
134
|
+
next(invitation);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case Invitation.State.READY_FOR_AUTHENTICATION: {
|
|
138
|
+
assert(invitation.invitationId);
|
|
139
|
+
invitation.state = Invitation.State.READY_FOR_AUTHENTICATION;
|
|
140
|
+
next(invitation);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case Invitation.State.AUTHENTICATING: {
|
|
144
|
+
assert(invitation.invitationId);
|
|
145
|
+
invitation.state = Invitation.State.AUTHENTICATING;
|
|
146
|
+
next(invitation);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case Invitation.State.SUCCESS: {
|
|
150
|
+
invitation.state = Invitation.State.SUCCESS;
|
|
151
|
+
next(invitation);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case Invitation.State.CANCELLED: {
|
|
155
|
+
assert(invitationId);
|
|
156
|
+
invitation.invitationId = invitationId;
|
|
157
|
+
invitation.state = Invitation.State.CANCELLED;
|
|
158
|
+
next(invitation);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case Invitation.State.TIMEOUT: {
|
|
162
|
+
assert(invitationId);
|
|
163
|
+
invitation.invitationId = invitationId;
|
|
164
|
+
invitation.state = Invitation.State.TIMEOUT;
|
|
165
|
+
next(invitation);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
126
169
|
},
|
|
127
|
-
|
|
128
|
-
assert(invitation.invitationId);
|
|
129
|
-
invitation.state = Invitation.State.AUTHENTICATING;
|
|
130
|
-
next(invitation);
|
|
131
|
-
},
|
|
132
|
-
onSuccess: (invitation) => {
|
|
133
|
-
invitation.state = Invitation.State.SUCCESS;
|
|
134
|
-
next(invitation);
|
|
135
|
-
close();
|
|
136
|
-
},
|
|
137
|
-
onCancelled: () => {
|
|
138
|
-
assert(invitationId);
|
|
139
|
-
invitation.invitationId = invitationId;
|
|
140
|
-
invitation.state = Invitation.State.CANCELLED;
|
|
141
|
-
next(invitation);
|
|
142
|
-
close();
|
|
143
|
-
},
|
|
144
|
-
onTimeout: (err: TimeoutError) => {
|
|
145
|
-
invitation.state = Invitation.State.TIMEOUT;
|
|
170
|
+
(err: Error) => {
|
|
146
171
|
close(err);
|
|
147
172
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
close(err);
|
|
173
|
+
() => {
|
|
174
|
+
close();
|
|
151
175
|
}
|
|
152
|
-
|
|
176
|
+
);
|
|
153
177
|
|
|
154
178
|
return (err?: Error) => {
|
|
155
179
|
const context = this.getLoggingContext();
|
|
@@ -164,24 +188,23 @@ export abstract class AbstractInvitationsService<T = void> implements Invitation
|
|
|
164
188
|
});
|
|
165
189
|
}
|
|
166
190
|
|
|
167
|
-
async authenticate({ invitationId,
|
|
191
|
+
async authenticate({ invitationId, authCode }: AuthenticationRequest): Promise<void> {
|
|
168
192
|
log('authenticating...');
|
|
169
193
|
assert(invitationId);
|
|
170
194
|
const observable = this._acceptInvitations.get(invitationId);
|
|
171
195
|
if (!observable) {
|
|
172
196
|
log.warn('invalid invitation', { invitationId });
|
|
173
197
|
} else {
|
|
174
|
-
await observable.authenticate(
|
|
198
|
+
await observable.authenticate(authCode);
|
|
175
199
|
}
|
|
176
200
|
}
|
|
177
201
|
|
|
178
|
-
async cancelInvitation(
|
|
202
|
+
async cancelInvitation({ invitationId }: { invitationId: string }): Promise<void> {
|
|
179
203
|
log('cancelling...');
|
|
180
|
-
assert(
|
|
181
|
-
const observable =
|
|
182
|
-
this._createInvitations.get(invitation.invitationId) ?? this._acceptInvitations.get(invitation.invitationId);
|
|
204
|
+
assert(invitationId);
|
|
205
|
+
const observable = this._createInvitations.get(invitationId) ?? this._acceptInvitations.get(invitationId);
|
|
183
206
|
if (!observable) {
|
|
184
|
-
log.warn('invalid invitation', { invitationId
|
|
207
|
+
log.warn('invalid invitation', { invitationId });
|
|
185
208
|
} else {
|
|
186
209
|
await observable?.cancel();
|
|
187
210
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { asyncChain, Trigger } from '@dxos/async';
|
|
8
|
+
import { raise } from '@dxos/debug';
|
|
9
|
+
import { testLocalDatabase } from '@dxos/echo-pipeline/testing';
|
|
10
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
|
+
import { afterTest, describe, test } from '@dxos/test';
|
|
12
|
+
|
|
13
|
+
import { ServiceContext } from '../services';
|
|
14
|
+
import { createIdentity, createPeers, syncItemsLocal } from '../testing';
|
|
15
|
+
import { performInvitation } from '../testing/invitation-utils';
|
|
16
|
+
|
|
17
|
+
const closeAfterTest = async (peer: ServiceContext) => {
|
|
18
|
+
afterTest(() => peer.close());
|
|
19
|
+
return peer;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('services/space-invitations-protocol', () => {
|
|
23
|
+
test('genesis', async () => {
|
|
24
|
+
const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
|
|
25
|
+
|
|
26
|
+
const space = await peer.dataSpaceManager!.createSpace();
|
|
27
|
+
expect(peer.dataSpaceManager!.spaces.has(space.key)).to.be.true;
|
|
28
|
+
|
|
29
|
+
await space.close();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('genesis & ready', async () => {
|
|
33
|
+
const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
|
|
34
|
+
|
|
35
|
+
const space = await peer.dataSpaceManager!.createSpace();
|
|
36
|
+
expect(peer.dataSpaceManager!.spaces.has(space.key)).to.be.true;
|
|
37
|
+
|
|
38
|
+
await peer.dataSpaceManager?.waitUntilSpaceReady(space.key);
|
|
39
|
+
await space.close();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('genesis with database mutations', async () => {
|
|
43
|
+
const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
|
|
44
|
+
const space = await peer.dataSpaceManager!.createSpace();
|
|
45
|
+
afterTest(() => space.close());
|
|
46
|
+
|
|
47
|
+
await testLocalDatabase(space.dataPipeline);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('invitation with no auth', async () => {
|
|
51
|
+
const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
|
|
52
|
+
|
|
53
|
+
const space1 = await host.dataSpaceManager!.createSpace();
|
|
54
|
+
const spaceKey = space1.key;
|
|
55
|
+
|
|
56
|
+
await Promise.all(performInvitation({ host, guest, options: { kind: Invitation.Kind.SPACE, spaceKey } }));
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
const space1 = host.dataSpaceManager!.spaces.get(spaceKey)!;
|
|
60
|
+
const space2 = guest.dataSpaceManager!.spaces.get(spaceKey)!;
|
|
61
|
+
expect(space1).not.to.be.undefined;
|
|
62
|
+
expect(space2).not.to.be.undefined;
|
|
63
|
+
|
|
64
|
+
await host.dataSpaceManager?.waitUntilSpaceReady(space1.key);
|
|
65
|
+
await guest.dataSpaceManager?.waitUntilSpaceReady(space2.key);
|
|
66
|
+
|
|
67
|
+
await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
|
|
68
|
+
|
|
69
|
+
await space1.close();
|
|
70
|
+
await space2.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('creates and accepts invitation with retry', async () => {
|
|
75
|
+
const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
|
|
76
|
+
|
|
77
|
+
let attempt = 0;
|
|
78
|
+
|
|
79
|
+
const space1 = await host.dataSpaceManager!.createSpace();
|
|
80
|
+
|
|
81
|
+
const [{ invitation: invitation1 }, { invitation: invitation2 }] = await Promise.all(
|
|
82
|
+
performInvitation({
|
|
83
|
+
host,
|
|
84
|
+
guest,
|
|
85
|
+
options: { kind: Invitation.Kind.SPACE, spaceKey: space1.key },
|
|
86
|
+
hooks: {
|
|
87
|
+
guest: {
|
|
88
|
+
onReady: (invitation) => {
|
|
89
|
+
if (attempt === 0) {
|
|
90
|
+
// Force retry.
|
|
91
|
+
void invitation.authenticate('000000');
|
|
92
|
+
attempt++;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(attempt).to.eq(1);
|
|
104
|
+
expect(invitation1?.spaceKey).to.exist;
|
|
105
|
+
expect(invitation2?.spaceKey).to.exist;
|
|
106
|
+
expect(invitation1?.spaceKey).to.deep.eq(invitation2?.spaceKey);
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
const space1 = host.dataSpaceManager!.spaces.get(invitation1!.spaceKey!)!;
|
|
110
|
+
const space2 = guest.dataSpaceManager!.spaces.get(invitation2!.spaceKey!)!;
|
|
111
|
+
expect(space1).not.to.be.undefined;
|
|
112
|
+
expect(space2).not.to.be.undefined;
|
|
113
|
+
|
|
114
|
+
await host.dataSpaceManager?.waitUntilSpaceReady(space1.key);
|
|
115
|
+
await guest.dataSpaceManager?.waitUntilSpaceReady(space2.key);
|
|
116
|
+
|
|
117
|
+
await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
|
|
118
|
+
|
|
119
|
+
await space1.close();
|
|
120
|
+
await space2.close();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
expect(
|
|
124
|
+
guest.identityManager.identity?.space.spaceState.getCredentialsOfType('dxos.halo.credentials.SpaceMember').length
|
|
125
|
+
).to.equal(2); // own halo + newly joined space.
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('cancels invitation', async () => {
|
|
129
|
+
const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
|
|
130
|
+
|
|
131
|
+
const hostConnected = new Trigger<Invitation>();
|
|
132
|
+
const guestConnected = new Trigger<Invitation>();
|
|
133
|
+
|
|
134
|
+
const space1 = await host.dataSpaceManager!.createSpace();
|
|
135
|
+
|
|
136
|
+
const invitationPromises = performInvitation({
|
|
137
|
+
host,
|
|
138
|
+
guest,
|
|
139
|
+
options: { kind: Invitation.Kind.SPACE, spaceKey: space1.key },
|
|
140
|
+
hooks: {
|
|
141
|
+
host: {
|
|
142
|
+
onConnecting: (invitation) => {
|
|
143
|
+
hostConnected.wake(invitation.get());
|
|
144
|
+
},
|
|
145
|
+
onConnected: (invitation) => {
|
|
146
|
+
void invitation.cancel();
|
|
147
|
+
return true;
|
|
148
|
+
},
|
|
149
|
+
onSuccess: () => raise(new Error('invitation success'))
|
|
150
|
+
},
|
|
151
|
+
guest: {
|
|
152
|
+
onConnecting: (invitation) => {
|
|
153
|
+
guestConnected.wake(invitation.get());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const { swarmKey: swarmKey1 } = await hostConnected.wait();
|
|
160
|
+
const { swarmKey: swarmKey2 } = await guestConnected.wait();
|
|
161
|
+
expect(swarmKey1).to.deep.eq(swarmKey2);
|
|
162
|
+
|
|
163
|
+
const [{ invitation: invitation1 }, { error }] = await Promise.all(invitationPromises);
|
|
164
|
+
expect(invitation1?.state).to.eq(Invitation.State.CANCELLED);
|
|
165
|
+
expect(error).to.exist;
|
|
166
|
+
|
|
167
|
+
await space1.close();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// TODO(burdon): Flaky.
|
|
171
|
+
// test.skip('test multi-use invitation', async () => {
|
|
172
|
+
// const GUEST_COUNT = 3;
|
|
173
|
+
// const [host, ...guests] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(
|
|
174
|
+
// createPeers(GUEST_COUNT + 1)
|
|
175
|
+
// );
|
|
176
|
+
|
|
177
|
+
// const hostSpace = await host.dataSpaceManager!.createSpace();
|
|
178
|
+
// const swarmKey = PublicKey.random();
|
|
179
|
+
// const hostObservable = await host.spaceInvitations!.createInvitation(hostSpace, {
|
|
180
|
+
// swarmKey,
|
|
181
|
+
// type: Invitation.Type.MULTIUSE_TESTING
|
|
182
|
+
// });
|
|
183
|
+
|
|
184
|
+
// const [done, count] = latch({ count: GUEST_COUNT });
|
|
185
|
+
// hostObservable.subscribe({
|
|
186
|
+
// onConnecting: async (invitation2: Invitation) => {},
|
|
187
|
+
// onConnected: async (invitation2: Invitation) => {},
|
|
188
|
+
// onSuccess: () => {
|
|
189
|
+
// count();
|
|
190
|
+
// },
|
|
191
|
+
// onCancelled: () => {},
|
|
192
|
+
// onTimeout: (err: Error) => raise(err),
|
|
193
|
+
// onError: (err: Error) => raise(err)
|
|
194
|
+
// });
|
|
195
|
+
|
|
196
|
+
// await Promise.all(
|
|
197
|
+
// range(GUEST_COUNT).map(async (idx) => {
|
|
198
|
+
// const observable = await guests[idx].spaceInvitations!.acceptInvitation({
|
|
199
|
+
// swarmKey,
|
|
200
|
+
// type: Invitation.Type.MULTIUSE_TESTING
|
|
201
|
+
// });
|
|
202
|
+
// const success = new Trigger();
|
|
203
|
+
// observable.subscribe({
|
|
204
|
+
// onConnecting: async (invitation2: Invitation) => {},
|
|
205
|
+
// onConnected: async (invitation2: Invitation) => {},
|
|
206
|
+
// onSuccess: () => {
|
|
207
|
+
// success.wake();
|
|
208
|
+
// },
|
|
209
|
+
// onCancelled: () => raise(new Error()),
|
|
210
|
+
// onTimeout: (err: Error) => raise(err),
|
|
211
|
+
// onError: (err: Error) => raise(err)
|
|
212
|
+
// });
|
|
213
|
+
// await success.wait({ timeout: 300 });
|
|
214
|
+
// })
|
|
215
|
+
// );
|
|
216
|
+
// await done();
|
|
217
|
+
|
|
218
|
+
// await hostObservable.cancel();
|
|
219
|
+
// await hostSpace.close();
|
|
220
|
+
// });
|
|
221
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { createAdmissionCredentials, getCredentialAssertion } from '@dxos/credentials';
|
|
8
|
+
import { SigningContext } from '@dxos/echo-pipeline';
|
|
9
|
+
import { writeMessages } from '@dxos/feed-store';
|
|
10
|
+
import { Keyring } from '@dxos/keyring';
|
|
11
|
+
import { PublicKey } from '@dxos/keys';
|
|
12
|
+
import { log } from '@dxos/log';
|
|
13
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
14
|
+
import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
15
|
+
import { ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
16
|
+
import { AdmissionRequest, AdmissionResponse, IntroductionRequest } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
17
|
+
|
|
18
|
+
import { DataSpaceManager } from '../spaces';
|
|
19
|
+
import { InvitationProtocol } from './invitation-protocol';
|
|
20
|
+
|
|
21
|
+
export class SpaceInvitationProtocol implements InvitationProtocol {
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly _spaceManager: DataSpaceManager,
|
|
24
|
+
private readonly _signingContext: SigningContext,
|
|
25
|
+
private readonly _keyring: Keyring,
|
|
26
|
+
private readonly _spaceKey?: PublicKey
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
toJSON(): object {
|
|
30
|
+
return {
|
|
31
|
+
deviceKey: this._signingContext.deviceKey,
|
|
32
|
+
spaceKey: this._spaceKey
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getInvitationContext(): Partial<Invitation> & Pick<Invitation, 'kind'> {
|
|
37
|
+
return {
|
|
38
|
+
kind: Invitation.Kind.SPACE,
|
|
39
|
+
spaceKey: this._spaceKey
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async admit(request: AdmissionRequest, guestProfile?: ProfileDocument | undefined): Promise<AdmissionResponse> {
|
|
44
|
+
assert(this._spaceKey);
|
|
45
|
+
const space = await this._spaceManager.spaces.get(this._spaceKey);
|
|
46
|
+
assert(space);
|
|
47
|
+
|
|
48
|
+
assert(request.space);
|
|
49
|
+
const { identityKey, deviceKey } = request.space;
|
|
50
|
+
|
|
51
|
+
log('writing guest credentials', { host: this._signingContext.deviceKey, guest: deviceKey });
|
|
52
|
+
// TODO(burdon): Check if already admitted.
|
|
53
|
+
const credentials: FeedMessage.Payload[] = await createAdmissionCredentials(
|
|
54
|
+
this._signingContext.credentialSigner,
|
|
55
|
+
identityKey,
|
|
56
|
+
space.key,
|
|
57
|
+
space.inner.genesisFeedKey,
|
|
58
|
+
guestProfile
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// TODO(dmaretskyi): Refactor.
|
|
62
|
+
assert(credentials[0].credential);
|
|
63
|
+
const spaceMemberCredential = credentials[0].credential.credential;
|
|
64
|
+
assert(getCredentialAssertion(spaceMemberCredential)['@type'] === 'dxos.halo.credentials.SpaceMember');
|
|
65
|
+
|
|
66
|
+
await writeMessages(space.inner.controlPipeline.writer, credentials);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
space: {
|
|
70
|
+
credential: spaceMemberCredential,
|
|
71
|
+
controlTimeframe: space.inner.controlPipeline.state.timeframe,
|
|
72
|
+
dataTimeframe: space.dataPipeline.pipelineState?.timeframe
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
createIntroduction(): IntroductionRequest {
|
|
78
|
+
return {
|
|
79
|
+
profile: this._signingContext.profile
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async createAdmissionRequest(): Promise<AdmissionRequest> {
|
|
84
|
+
// Generate a pair of keys for our feeds.
|
|
85
|
+
const controlFeedKey = await this._keyring.createKey();
|
|
86
|
+
const dataFeedKey = await this._keyring.createKey();
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
space: {
|
|
90
|
+
identityKey: this._signingContext.identityKey,
|
|
91
|
+
deviceKey: this._signingContext.deviceKey,
|
|
92
|
+
controlFeedKey,
|
|
93
|
+
dataFeedKey
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async accept(response: AdmissionResponse): Promise<Partial<Invitation>> {
|
|
99
|
+
assert(response.space);
|
|
100
|
+
const { credential, controlTimeframe, dataTimeframe } = response.space;
|
|
101
|
+
const assertion = getCredentialAssertion(credential);
|
|
102
|
+
assert(assertion['@type'] === 'dxos.halo.credentials.SpaceMember', 'Invalid credential');
|
|
103
|
+
assert(credential.subject.id.equals(this._signingContext.identityKey));
|
|
104
|
+
|
|
105
|
+
// Create local space.
|
|
106
|
+
await this._spaceManager.acceptSpace({
|
|
107
|
+
spaceKey: assertion.spaceKey,
|
|
108
|
+
genesisFeedKey: assertion.genesisFeedKey,
|
|
109
|
+
controlTimeframe,
|
|
110
|
+
dataTimeframe
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await this._signingContext.recordCredential(credential);
|
|
114
|
+
|
|
115
|
+
return { spaceKey: assertion.spaceKey };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Invitation } from '@dxos/client';
|
|
5
6
|
import { MemorySignalManagerContext } from '@dxos/messaging';
|
|
6
7
|
import { describe, test } from '@dxos/test';
|
|
7
8
|
|
|
@@ -16,7 +17,7 @@ describe('services/ServiceContext', () => {
|
|
|
16
17
|
const space1 = await device1.dataSpaceManager!.createSpace();
|
|
17
18
|
|
|
18
19
|
const device2 = createServiceContext({ signalContext: networkContext });
|
|
19
|
-
await performInvitation(device1
|
|
20
|
+
await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
|
|
20
21
|
|
|
21
22
|
await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
|
|
22
23
|
const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
|
|
@@ -29,7 +30,7 @@ describe('services/ServiceContext', () => {
|
|
|
29
30
|
await device1.createIdentity();
|
|
30
31
|
|
|
31
32
|
const device2 = createServiceContext({ signalContext: networkContext });
|
|
32
|
-
await performInvitation(device1
|
|
33
|
+
await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
|
|
33
34
|
|
|
34
35
|
const space1 = await device1.dataSpaceManager!.createSpace();
|
|
35
36
|
await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
|
|
@@ -43,12 +44,18 @@ describe('services/ServiceContext', () => {
|
|
|
43
44
|
await device1.createIdentity();
|
|
44
45
|
|
|
45
46
|
const device2 = createServiceContext({ signalContext: networkContext });
|
|
46
|
-
await performInvitation(device1
|
|
47
|
+
await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
|
|
47
48
|
|
|
48
49
|
const identity2 = createServiceContext({ signalContext: networkContext });
|
|
49
50
|
await identity2.createIdentity();
|
|
50
51
|
const space1 = await identity2.dataSpaceManager!.createSpace();
|
|
51
|
-
await
|
|
52
|
+
await Promise.all(
|
|
53
|
+
performInvitation({
|
|
54
|
+
host: identity2,
|
|
55
|
+
guest: device1,
|
|
56
|
+
options: { kind: Invitation.Kind.SPACE, spaceKey: space1.key }
|
|
57
|
+
})
|
|
58
|
+
);
|
|
52
59
|
|
|
53
60
|
await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
|
|
54
61
|
const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
|