@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
|
@@ -1,463 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2022 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import assert from 'node:assert';
|
|
6
|
-
|
|
7
|
-
import { scheduleTask, sleep, Trigger } from '@dxos/async';
|
|
8
|
-
import {
|
|
9
|
-
AuthenticatingInvitationProvider,
|
|
10
|
-
AUTHENTICATION_CODE_LENGTH,
|
|
11
|
-
CancellableInvitationObservable,
|
|
12
|
-
InvitationObservableProvider,
|
|
13
|
-
INVITATION_TIMEOUT,
|
|
14
|
-
ON_CLOSE_DELAY,
|
|
15
|
-
AbstractInvitationsHandler,
|
|
16
|
-
InvitationsOptions
|
|
17
|
-
} from '@dxos/client';
|
|
18
|
-
import { Context } from '@dxos/context';
|
|
19
|
-
import { createAdmissionCredentials, generatePasscode, getCredentialAssertion } from '@dxos/credentials';
|
|
20
|
-
import { SigningContext } from '@dxos/echo-pipeline';
|
|
21
|
-
import { writeMessages } from '@dxos/feed-store';
|
|
22
|
-
import { Keyring } from '@dxos/keyring';
|
|
23
|
-
import { PublicKey } from '@dxos/keys';
|
|
24
|
-
import { log } from '@dxos/log';
|
|
25
|
-
import { createTeleportProtocolFactory, NetworkManager, StarTopology, SwarmConnection } from '@dxos/network-manager';
|
|
26
|
-
import { schema } from '@dxos/protocols';
|
|
27
|
-
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
28
|
-
import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
29
|
-
import { ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
30
|
-
import {
|
|
31
|
-
AuthenticationRequest,
|
|
32
|
-
AuthenticationResponse,
|
|
33
|
-
AuthMethod,
|
|
34
|
-
Introduction,
|
|
35
|
-
IntroductionResponse,
|
|
36
|
-
SpaceAdmissionCredentials,
|
|
37
|
-
SpaceAdmissionRequest,
|
|
38
|
-
SpaceHostService
|
|
39
|
-
} from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
40
|
-
import { ExtensionContext, RpcExtension } from '@dxos/teleport';
|
|
41
|
-
|
|
42
|
-
import { DataSpace, DataSpaceManager } from '../spaces';
|
|
43
|
-
|
|
44
|
-
const MAX_OTP_ATTEMPTS = 3;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Handles the life-cycle of Space invitations between peers.
|
|
48
|
-
*/
|
|
49
|
-
// TODO(dmaretskyi): Split into Host and Guest parts.
|
|
50
|
-
export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpace> {
|
|
51
|
-
constructor(
|
|
52
|
-
networkManager: NetworkManager,
|
|
53
|
-
private readonly _spaceManager: DataSpaceManager,
|
|
54
|
-
private readonly _signingContext: SigningContext,
|
|
55
|
-
private readonly _keyring: Keyring
|
|
56
|
-
) {
|
|
57
|
-
super(networkManager);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Creates an invitation and listens for a join request from the invited (guest) peer.
|
|
62
|
-
*/
|
|
63
|
-
createInvitation(space: DataSpace, options?: InvitationsOptions): CancellableInvitationObservable {
|
|
64
|
-
let swarmConnection: SwarmConnection | undefined;
|
|
65
|
-
const { type, timeout = INVITATION_TIMEOUT, swarmKey } = options ?? {};
|
|
66
|
-
assert(type !== Invitation.Type.OFFLINE);
|
|
67
|
-
assert(space);
|
|
68
|
-
|
|
69
|
-
// TODO(dmaretskyi): Add invitation kind: halo/space.
|
|
70
|
-
const invitation: Invitation = {
|
|
71
|
-
type,
|
|
72
|
-
invitationId: PublicKey.random().toHex(),
|
|
73
|
-
swarmKey: swarmKey ?? PublicKey.random(),
|
|
74
|
-
spaceKey: space.key,
|
|
75
|
-
authMethod: options?.authMethod ?? AuthMethod.SHARED_SECRET,
|
|
76
|
-
authenticationCode: generatePasscode(AUTHENTICATION_CODE_LENGTH) // TODO(dmaretskyi): Don't generate if authMethod === NONE .
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const ctx = new Context({
|
|
80
|
-
onError: (err) => {
|
|
81
|
-
void ctx.dispose();
|
|
82
|
-
observable.callback.onError(err);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// TODO(burdon): Stop anything pending.
|
|
87
|
-
const observable = new InvitationObservableProvider(async () => {
|
|
88
|
-
await swarmConnection?.close();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
let authenticationPassed = false;
|
|
92
|
-
let authenticationRetry = 0;
|
|
93
|
-
|
|
94
|
-
// Called for every connecting peer.
|
|
95
|
-
const createExtension = (): HostSpaceInvitationExtension => {
|
|
96
|
-
const complete = new Trigger<PublicKey>();
|
|
97
|
-
let guestProfile: ProfileDocument | undefined;
|
|
98
|
-
|
|
99
|
-
const hostInvitationExtension = new HostSpaceInvitationExtension({
|
|
100
|
-
introduce: async ({ profile }) => {
|
|
101
|
-
log('guest introduced itself', {
|
|
102
|
-
guestProfile: profile,
|
|
103
|
-
host: this._signingContext.deviceKey,
|
|
104
|
-
spaceKey: space.key
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
guestProfile = profile;
|
|
108
|
-
|
|
109
|
-
// TODO(burdon): Is this the right place to set this state?
|
|
110
|
-
// TODO(dmaretskyi): Should we expose guest's profile in this callback?
|
|
111
|
-
observable.callback.onAuthenticating?.(invitation);
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
spaceKey:
|
|
115
|
-
type === Invitation.Type.INTERACTIVE_TESTING || type === Invitation.Type.MULTIUSE_TESTING
|
|
116
|
-
? space.key
|
|
117
|
-
: undefined,
|
|
118
|
-
authMethod: invitation.authMethod!
|
|
119
|
-
};
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
authenticate: async ({ authenticationCode: code }) => {
|
|
123
|
-
log('received authentication request', { authenticationCode: code });
|
|
124
|
-
let status = AuthenticationResponse.Status.OK;
|
|
125
|
-
|
|
126
|
-
switch (invitation.authMethod) {
|
|
127
|
-
case AuthMethod.NONE: {
|
|
128
|
-
log('authentication not required');
|
|
129
|
-
return { status: AuthenticationResponse.Status.OK };
|
|
130
|
-
}
|
|
131
|
-
case AuthMethod.SHARED_SECRET: {
|
|
132
|
-
if (invitation.authenticationCode) {
|
|
133
|
-
if (authenticationRetry++ > MAX_OTP_ATTEMPTS) {
|
|
134
|
-
status = AuthenticationResponse.Status.INVALID_OPT_ATTEMPTS;
|
|
135
|
-
} else if (code !== invitation.authenticationCode) {
|
|
136
|
-
status = AuthenticationResponse.Status.INVALID_OTP;
|
|
137
|
-
} else {
|
|
138
|
-
authenticationPassed = true;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
default: {
|
|
144
|
-
log.error('invalid authentication method', { authMethod: invitation.authMethod });
|
|
145
|
-
status = AuthenticationResponse.Status.INTERNAL_ERROR;
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return { status };
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
requestAdmission: async ({ identityKey, deviceKey, controlFeedKey, dataFeedKey }) => {
|
|
154
|
-
try {
|
|
155
|
-
// Check authenticated.
|
|
156
|
-
if (isAuthenticationRequired(invitation) && !authenticationPassed) {
|
|
157
|
-
throw new Error('Not authenticated');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
log('writing guest credentials', { host: this._signingContext.deviceKey, guest: deviceKey });
|
|
161
|
-
// TODO(burdon): Check if already admitted.
|
|
162
|
-
const credentials: FeedMessage.Payload[] = await createAdmissionCredentials(
|
|
163
|
-
this._signingContext.credentialSigner,
|
|
164
|
-
identityKey,
|
|
165
|
-
space.key,
|
|
166
|
-
space.inner.genesisFeedKey,
|
|
167
|
-
guestProfile
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
// TODO(dmaretskyi): Refactor.
|
|
171
|
-
assert(credentials[0].credential);
|
|
172
|
-
const spaceMemberCredential = credentials[0].credential.credential;
|
|
173
|
-
assert(getCredentialAssertion(spaceMemberCredential)['@type'] === 'dxos.halo.credentials.SpaceMember');
|
|
174
|
-
|
|
175
|
-
await writeMessages(space.inner.controlPipeline.writer, credentials);
|
|
176
|
-
|
|
177
|
-
// Updating credentials complete.
|
|
178
|
-
complete.wake(deviceKey);
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
credential: spaceMemberCredential,
|
|
182
|
-
controlTimeframe: space.inner.controlPipeline.state.timeframe,
|
|
183
|
-
dataTimeframe: space.dataPipeline.pipelineState?.timeframe
|
|
184
|
-
};
|
|
185
|
-
} catch (err) {
|
|
186
|
-
// TODO(burdon): Generic RPC callback to report error to client.
|
|
187
|
-
observable.callback.onError(err);
|
|
188
|
-
throw err; // Propagate error to guest.
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
onOpen: () => {
|
|
193
|
-
scheduleTask(ctx, async () => {
|
|
194
|
-
try {
|
|
195
|
-
log('connected', { host: this._signingContext.deviceKey });
|
|
196
|
-
observable.callback.onConnected?.(invitation);
|
|
197
|
-
const deviceKey = await complete.wait({ timeout });
|
|
198
|
-
log('admitted guest', { host: this._signingContext.deviceKey, guest: deviceKey });
|
|
199
|
-
observable.callback.onSuccess(invitation);
|
|
200
|
-
} catch (err) {
|
|
201
|
-
if (!observable.cancelled) {
|
|
202
|
-
log.error('failed', err);
|
|
203
|
-
observable.callback.onError(err);
|
|
204
|
-
}
|
|
205
|
-
} finally {
|
|
206
|
-
if (type !== Invitation.Type.MULTIUSE_TESTING) {
|
|
207
|
-
await sleep(ON_CLOSE_DELAY);
|
|
208
|
-
await ctx.dispose();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
return hostInvitationExtension;
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
scheduleTask(ctx, async () => {
|
|
218
|
-
const topic = invitation.swarmKey!;
|
|
219
|
-
const swarmConnection = await this._networkManager.joinSwarm({
|
|
220
|
-
topic,
|
|
221
|
-
peerId: topic,
|
|
222
|
-
protocolProvider: createTeleportProtocolFactory(async (teleport) => {
|
|
223
|
-
teleport.addExtension('dxos.halo.invitations', createExtension());
|
|
224
|
-
}),
|
|
225
|
-
topology: new StarTopology(topic)
|
|
226
|
-
});
|
|
227
|
-
ctx.onDispose(() => swarmConnection.close());
|
|
228
|
-
|
|
229
|
-
observable.callback.onConnecting?.(invitation);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
return observable;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Waits for the host peer (inviter) to accept our join request.
|
|
237
|
-
* The local guest peer (invitee) then sends the local space invitation to the host,
|
|
238
|
-
* which then writes the guest's credentials to the space.
|
|
239
|
-
*/
|
|
240
|
-
acceptInvitation(invitation: Invitation, options?: InvitationsOptions): AuthenticatingInvitationProvider {
|
|
241
|
-
const { timeout = INVITATION_TIMEOUT } = options ?? {};
|
|
242
|
-
|
|
243
|
-
const ctx = new Context({
|
|
244
|
-
onError: (err) => {
|
|
245
|
-
void ctx.dispose();
|
|
246
|
-
observable.callback.onError(err);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
const authenticated = new Trigger<string>();
|
|
251
|
-
const observable = new AuthenticatingInvitationProvider({
|
|
252
|
-
onCancel: async () => {
|
|
253
|
-
await ctx.dispose();
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
onAuthenticate: async (code: string) => {
|
|
257
|
-
// TODO(burdon): Reset creates a race condition? Event?
|
|
258
|
-
authenticated.wake(code);
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
let connectionCount = 0;
|
|
263
|
-
const complete = new Trigger();
|
|
264
|
-
|
|
265
|
-
const createExtension = (): GuestSpaceInvitationExtension => {
|
|
266
|
-
const extension = new GuestSpaceInvitationExtension({
|
|
267
|
-
onOpen: () => {
|
|
268
|
-
scheduleTask(ctx, async () => {
|
|
269
|
-
try {
|
|
270
|
-
// TODO(burdon): Bug where guest may create multiple connections.
|
|
271
|
-
if (++connectionCount > 1) {
|
|
272
|
-
throw new Error(`multiple connections detected: ${connectionCount}`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
log('connected', { guest: this._signingContext.deviceKey });
|
|
276
|
-
observable.callback.onConnected?.(invitation);
|
|
277
|
-
|
|
278
|
-
// 1. Introduce guest to host.
|
|
279
|
-
log('introduce', { guest: this._signingContext.deviceKey });
|
|
280
|
-
const introductionResponse = await extension.rpc.SpaceHostService.introduce({
|
|
281
|
-
profile: this._signingContext.profile
|
|
282
|
-
});
|
|
283
|
-
log('introduce response', { guest: this._signingContext.deviceKey, response: introductionResponse });
|
|
284
|
-
invitation.authMethod = introductionResponse.authMethod;
|
|
285
|
-
if (introductionResponse.spaceKey) {
|
|
286
|
-
invitation.spaceKey = introductionResponse.spaceKey;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// TODO(dmaretskyi): Add a callback here to notify that we have introduced ourselves to the host (regradless of auth requirements).
|
|
290
|
-
|
|
291
|
-
// 2. Get authentication code.
|
|
292
|
-
// TODO(burdon): Test timeout (options for timeouts at different steps).
|
|
293
|
-
if (isAuthenticationRequired(invitation)) {
|
|
294
|
-
observable.callback.onAuthenticating?.(invitation);
|
|
295
|
-
|
|
296
|
-
for (let attempt = 1; attempt <= MAX_OTP_ATTEMPTS; attempt++) {
|
|
297
|
-
log('guest waiting for authentication code...');
|
|
298
|
-
observable.callback.onAuthenticating?.(invitation);
|
|
299
|
-
const authenticationCode = await authenticated.wait({ timeout });
|
|
300
|
-
|
|
301
|
-
log('sending authentication request');
|
|
302
|
-
const response = await extension.rpc.SpaceHostService.authenticate({ authenticationCode });
|
|
303
|
-
if (response.status === undefined || response.status === AuthenticationResponse.Status.OK) {
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (response.status === AuthenticationResponse.Status.INVALID_OTP) {
|
|
308
|
-
if (attempt === MAX_OTP_ATTEMPTS) {
|
|
309
|
-
throw new Error(`Maximum retry attempts: ${MAX_OTP_ATTEMPTS}`);
|
|
310
|
-
} else {
|
|
311
|
-
log('retrying invalid code', { attempt });
|
|
312
|
-
authenticated.reset();
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// 3. Generate a pair of keys for our feeds.
|
|
318
|
-
const controlFeedKey = await this._keyring.createKey();
|
|
319
|
-
const dataFeedKey = await this._keyring.createKey();
|
|
320
|
-
|
|
321
|
-
// 4. Send admission credentials to host (with local space keys).
|
|
322
|
-
log('request admission', { guest: this._signingContext.deviceKey });
|
|
323
|
-
const { credential, controlTimeframe, dataTimeframe } =
|
|
324
|
-
await extension.rpc.SpaceHostService.requestAdmission({
|
|
325
|
-
identityKey: this._signingContext.identityKey,
|
|
326
|
-
deviceKey: this._signingContext.deviceKey,
|
|
327
|
-
controlFeedKey,
|
|
328
|
-
dataFeedKey
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// 4. Create local space.
|
|
332
|
-
const assertion = getCredentialAssertion(credential);
|
|
333
|
-
assert(assertion['@type'] === 'dxos.halo.credentials.SpaceMember', 'Invalid credential');
|
|
334
|
-
assert(credential.subject.id.equals(this._signingContext.identityKey));
|
|
335
|
-
|
|
336
|
-
const space = await this._spaceManager.acceptSpace({
|
|
337
|
-
spaceKey: assertion.spaceKey,
|
|
338
|
-
genesisFeedKey: assertion.genesisFeedKey,
|
|
339
|
-
controlTimeframe,
|
|
340
|
-
dataTimeframe
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// Record credential in our HALO.
|
|
344
|
-
await this._signingContext.recordCredential(credential);
|
|
345
|
-
|
|
346
|
-
// 5. Success.
|
|
347
|
-
log('admitted by host', { guest: this._signingContext.deviceKey, spaceKey: space.key });
|
|
348
|
-
complete.wake();
|
|
349
|
-
} catch (err) {
|
|
350
|
-
if (!observable.cancelled) {
|
|
351
|
-
log.warn('auth failed', err);
|
|
352
|
-
observable.callback.onError(err);
|
|
353
|
-
}
|
|
354
|
-
} finally {
|
|
355
|
-
await ctx.dispose();
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
return extension;
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
scheduleTask(ctx, async () => {
|
|
364
|
-
assert(invitation.swarmKey);
|
|
365
|
-
const topic = invitation.swarmKey;
|
|
366
|
-
const swarmConnection = await this._networkManager.joinSwarm({
|
|
367
|
-
topic,
|
|
368
|
-
peerId: PublicKey.random(),
|
|
369
|
-
protocolProvider: createTeleportProtocolFactory(async (teleport) => {
|
|
370
|
-
teleport.addExtension('dxos.halo.invitations', createExtension());
|
|
371
|
-
}),
|
|
372
|
-
topology: new StarTopology(topic)
|
|
373
|
-
});
|
|
374
|
-
ctx.onDispose(() => swarmConnection.close());
|
|
375
|
-
|
|
376
|
-
observable.callback.onConnecting?.(invitation);
|
|
377
|
-
await complete.wait();
|
|
378
|
-
observable.callback.onSuccess(invitation);
|
|
379
|
-
await ctx.dispose();
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
return observable;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const isAuthenticationRequired = (invitation: Invitation) =>
|
|
387
|
-
invitation.authMethod !== AuthMethod.NONE &&
|
|
388
|
-
invitation.type !== Invitation.Type.INTERACTIVE_TESTING &&
|
|
389
|
-
invitation.type !== Invitation.Type.MULTIUSE_TESTING;
|
|
390
|
-
|
|
391
|
-
type HostSpaceInvitationExtensionCallbacks = {
|
|
392
|
-
// Deliberately not async to not block the extensions opening.
|
|
393
|
-
onOpen: () => void;
|
|
394
|
-
|
|
395
|
-
introduce: (introduction: Introduction) => Promise<IntroductionResponse>;
|
|
396
|
-
authenticate: (request: AuthenticationRequest) => Promise<AuthenticationResponse>;
|
|
397
|
-
requestAdmission: (request: SpaceAdmissionRequest) => Promise<SpaceAdmissionCredentials>;
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Host's side for a connection to a concrete peer in p2p network during invitation.
|
|
402
|
-
*/
|
|
403
|
-
class HostSpaceInvitationExtension extends RpcExtension<{}, { SpaceHostService: SpaceHostService }> {
|
|
404
|
-
constructor(private readonly _callbacks: HostSpaceInvitationExtensionCallbacks) {
|
|
405
|
-
super({
|
|
406
|
-
exposed: {
|
|
407
|
-
SpaceHostService: schema.getService('dxos.halo.invitations.SpaceHostService')
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
protected override async getHandlers(): Promise<{ SpaceHostService: SpaceHostService }> {
|
|
413
|
-
return {
|
|
414
|
-
// TODO(dmaretskyi): For now this is just forwarding the data to callbacks since we don't have session-specific logic.
|
|
415
|
-
// Perhaps in the future we will have more complex logic here.
|
|
416
|
-
SpaceHostService: {
|
|
417
|
-
introduce: async (params) => {
|
|
418
|
-
return this._callbacks.introduce(params);
|
|
419
|
-
},
|
|
420
|
-
|
|
421
|
-
authenticate: async (credentials) => {
|
|
422
|
-
return this._callbacks.authenticate(credentials);
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
requestAdmission: async (credentials) => {
|
|
426
|
-
return this._callbacks.requestAdmission(credentials);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
override async onOpen(context: ExtensionContext) {
|
|
433
|
-
await super.onOpen(context);
|
|
434
|
-
this._callbacks.onOpen();
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
type GuestSpaceInvitationExtensionCallbacks = {
|
|
439
|
-
// Deliberately not async to not block the extensions opening.
|
|
440
|
-
onOpen: () => void;
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Guest's side for a connection to a concrete peer in p2p network during invitation.
|
|
445
|
-
*/
|
|
446
|
-
class GuestSpaceInvitationExtension extends RpcExtension<{ SpaceHostService: SpaceHostService }, {}> {
|
|
447
|
-
constructor(private readonly _callbacks: GuestSpaceInvitationExtensionCallbacks) {
|
|
448
|
-
super({
|
|
449
|
-
requested: {
|
|
450
|
-
SpaceHostService: schema.getService('dxos.halo.invitations.SpaceHostService')
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
protected override async getHandlers() {
|
|
456
|
-
return {};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
override async onOpen(context: ExtensionContext) {
|
|
460
|
-
await super.onOpen(context);
|
|
461
|
-
this._callbacks.onOpen();
|
|
462
|
-
}
|
|
463
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2022 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { expect } from 'chai';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
|
|
8
|
-
import { asyncChain, Trigger } from '@dxos/async';
|
|
9
|
-
import { SpaceInvitationsProxy } from '@dxos/client';
|
|
10
|
-
import { raise } from '@dxos/debug';
|
|
11
|
-
import { Invitation, SpaceInvitationsService } from '@dxos/protocols/proto/dxos/client/services';
|
|
12
|
-
import { describe, test, afterTest } from '@dxos/test';
|
|
13
|
-
|
|
14
|
-
import { ServiceContext } from '../services';
|
|
15
|
-
import { createIdentity, createPeers } from '../testing';
|
|
16
|
-
import { SpaceInvitationsServiceImpl } from './space-invitations-service';
|
|
17
|
-
|
|
18
|
-
const closeAfterTest = async (peer: ServiceContext) => {
|
|
19
|
-
afterTest(() => peer.close());
|
|
20
|
-
return peer;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
describe('services/space-invitation-service', () => {
|
|
24
|
-
test('creates space and invites peer', async () => {
|
|
25
|
-
const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
|
|
26
|
-
|
|
27
|
-
assert(host.dataSpaceManager);
|
|
28
|
-
assert(host.spaceInvitations);
|
|
29
|
-
const service1: SpaceInvitationsService = new SpaceInvitationsServiceImpl(
|
|
30
|
-
host.identityManager,
|
|
31
|
-
() => host.spaceInvitations!,
|
|
32
|
-
() => host.dataSpaceManager!
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
assert(guest.dataSpaceManager);
|
|
36
|
-
assert(guest.spaceInvitations);
|
|
37
|
-
const service2: SpaceInvitationsService = new SpaceInvitationsServiceImpl(
|
|
38
|
-
guest.identityManager,
|
|
39
|
-
() => guest.spaceInvitations!,
|
|
40
|
-
() => guest.dataSpaceManager!
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const space1 = await host.dataSpaceManager.createSpace();
|
|
44
|
-
|
|
45
|
-
const success1 = new Trigger<Invitation>();
|
|
46
|
-
const success2 = new Trigger<Invitation>();
|
|
47
|
-
|
|
48
|
-
const authenticationCode = new Trigger<string>();
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
const proxy1 = new SpaceInvitationsProxy(service1);
|
|
52
|
-
const observable1 = proxy1.createInvitation(space1.key);
|
|
53
|
-
observable1.subscribe({
|
|
54
|
-
onConnecting: (invitation: Invitation) => {
|
|
55
|
-
const proxy2 = new SpaceInvitationsProxy(service2);
|
|
56
|
-
const observable2 = proxy2.acceptInvitation(invitation);
|
|
57
|
-
observable2.subscribe({
|
|
58
|
-
onAuthenticating: async (invitation2: Invitation) => {
|
|
59
|
-
await observable2.authenticate(await authenticationCode.wait());
|
|
60
|
-
},
|
|
61
|
-
onSuccess: (invitation: Invitation) => {
|
|
62
|
-
success2.wake(invitation);
|
|
63
|
-
},
|
|
64
|
-
onError: (err: Error) => raise(new Error(err.message))
|
|
65
|
-
});
|
|
66
|
-
},
|
|
67
|
-
onConnected: (invitation: Invitation) => {
|
|
68
|
-
assert(invitation.authenticationCode);
|
|
69
|
-
authenticationCode.wake(invitation.authenticationCode);
|
|
70
|
-
},
|
|
71
|
-
onSuccess: (invitation: Invitation) => {
|
|
72
|
-
success1.wake(invitation);
|
|
73
|
-
},
|
|
74
|
-
onCancelled: () => raise(new Error()),
|
|
75
|
-
onTimeout: (err: Error) => raise(new Error(err.message)),
|
|
76
|
-
onError: (err: Error) => raise(new Error(err.message))
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const [invitation1, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
|
|
81
|
-
expect(invitation1.spaceKey).to.deep.eq(invitation2.spaceKey);
|
|
82
|
-
expect(invitation1.state).to.eq(Invitation.State.SUCCESS);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('creates space and cancels invitation', async () => {
|
|
86
|
-
const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
|
|
87
|
-
|
|
88
|
-
assert(host.dataSpaceManager);
|
|
89
|
-
assert(host.spaceInvitations);
|
|
90
|
-
const service1: SpaceInvitationsService = new SpaceInvitationsServiceImpl(
|
|
91
|
-
host.identityManager,
|
|
92
|
-
() => host.spaceInvitations!,
|
|
93
|
-
() => host.dataSpaceManager!
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
assert(guest.dataSpaceManager);
|
|
97
|
-
assert(guest.spaceInvitations);
|
|
98
|
-
const service2: SpaceInvitationsService = new SpaceInvitationsServiceImpl(
|
|
99
|
-
guest.identityManager,
|
|
100
|
-
() => guest.spaceInvitations!,
|
|
101
|
-
() => guest.dataSpaceManager!
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const space1 = await host.dataSpaceManager.createSpace();
|
|
105
|
-
const cancelled = new Trigger();
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
const proxy = new SpaceInvitationsProxy(service1);
|
|
109
|
-
const observable = proxy.createInvitation(space1.key);
|
|
110
|
-
observable.subscribe({
|
|
111
|
-
onConnecting: (invitation: Invitation) => {
|
|
112
|
-
const proxy2 = new SpaceInvitationsProxy(service2);
|
|
113
|
-
proxy2.acceptInvitation(invitation);
|
|
114
|
-
},
|
|
115
|
-
onConnected: (invitation: Invitation) => {
|
|
116
|
-
void observable.cancel();
|
|
117
|
-
},
|
|
118
|
-
onCancelled: () => {
|
|
119
|
-
cancelled.wake();
|
|
120
|
-
},
|
|
121
|
-
onSuccess: () => raise(new Error()),
|
|
122
|
-
onTimeout: (err: Error) => raise(new Error(err.message)),
|
|
123
|
-
onError: (err: Error) => raise(new Error(err.message))
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
await cancelled.wait();
|
|
128
|
-
});
|
|
129
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2022 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import assert from 'node:assert';
|
|
6
|
-
|
|
7
|
-
import { InvitationsHandler } from '@dxos/client';
|
|
8
|
-
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
9
|
-
import { Provider } from '@dxos/util';
|
|
10
|
-
|
|
11
|
-
import { IdentityManager } from '../identity';
|
|
12
|
-
import { DataSpace, DataSpaceManager } from '../spaces';
|
|
13
|
-
import { AbstractInvitationsService } from './invitations-service';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Adapts invitation service observable to client/service stream.
|
|
17
|
-
*/
|
|
18
|
-
export class SpaceInvitationsServiceImpl extends AbstractInvitationsService<DataSpace> {
|
|
19
|
-
// prettier-ignore
|
|
20
|
-
constructor (
|
|
21
|
-
identityManager: IdentityManager,
|
|
22
|
-
private readonly invitationsHandler: Provider<InvitationsHandler<DataSpace>>,
|
|
23
|
-
private readonly _getSpaceManager: Provider<DataSpaceManager>
|
|
24
|
-
) {
|
|
25
|
-
super(identityManager, invitationsHandler);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
override getContext(invitation: Invitation): DataSpace {
|
|
29
|
-
assert(invitation.spaceKey);
|
|
30
|
-
const spaceManager = this._getSpaceManager();
|
|
31
|
-
const space = spaceManager.spaces.get(invitation.spaceKey!);
|
|
32
|
-
assert(space);
|
|
33
|
-
return space;
|
|
34
|
-
}
|
|
35
|
-
}
|