@dxos/client-services 0.4.10-main.d560ca0 → 0.4.10-main.d6ef25d
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-BBMYNGYT.mjs → chunk-BASAWGAA.mjs} +1296 -993
- package/dist/lib/browser/chunk-BASAWGAA.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +15 -3
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +12 -5
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-UUUK4U6J.cjs → chunk-4PLPSVK3.cjs} +1125 -910
- package/dist/lib/node/chunk-4PLPSVK3.cjs.map +7 -0
- package/dist/lib/node/index.cjs +50 -38
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +16 -9
- package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts +5 -0
- package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts.map +1 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts +5 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts +15 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -0
- package/dist/types/src/packlets/{services → diagnostics}/diagnostics.d.ts +1 -1
- package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -0
- package/dist/types/src/packlets/diagnostics/index.d.ts +4 -0
- package/dist/types/src/packlets/diagnostics/index.d.ts.map +1 -0
- package/dist/types/src/packlets/indexing/util.d.ts +3 -2
- package/dist/types/src/packlets/indexing/util.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/index.d.ts +1 -0
- package/dist/types/src/packlets/invitations/index.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-extension.d.ts +1 -0
- package/dist/types/src/packlets/invitations/invitation-extension.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +4 -2
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-manager.d.ts +42 -0
- package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/invitations-service.d.ts +7 -23
- package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/services/index.d.ts +1 -1
- package/dist/types/src/packlets/services/index.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +9 -5
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +6 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/services/util.d.ts +1 -0
- package/dist/types/src/packlets/services/util.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/index.d.ts +1 -0
- package/dist/types/src/packlets/storage/index.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/level.d.ts +4 -0
- package/dist/types/src/packlets/storage/level.d.ts.map +1 -0
- package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/util.d.ts +4 -0
- package/dist/types/src/packlets/storage/util.d.ts.map +1 -0
- package/dist/types/src/packlets/system/system-service.d.ts +1 -1
- package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +4 -2
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/package.json +36 -34
- package/src/index.ts +1 -0
- package/src/packlets/devices/devices-service.test.ts +1 -1
- package/src/packlets/diagnostics/browser-diagnostics-broadcast.ts +94 -0
- package/src/packlets/diagnostics/diagnostics-broadcast.ts +20 -0
- package/src/packlets/diagnostics/diagnostics-collector.ts +65 -0
- package/src/packlets/{services → diagnostics}/diagnostics.ts +2 -2
- package/src/packlets/diagnostics/index.ts +7 -0
- package/src/packlets/identity/identity-service.test.ts +1 -1
- package/src/packlets/indexing/util.ts +19 -12
- package/src/packlets/invitations/device-invitation-protocol.test.ts +1 -1
- package/src/packlets/invitations/index.ts +1 -0
- package/src/packlets/invitations/invitation-extension.ts +28 -1
- package/src/packlets/invitations/invitations-handler.ts +74 -34
- package/src/packlets/invitations/invitations-manager.ts +197 -0
- package/src/packlets/invitations/invitations-service.ts +21 -168
- package/src/packlets/network/network-service.test.ts +1 -1
- package/src/packlets/services/automerge-host.test.ts +10 -4
- package/src/packlets/services/index.ts +1 -1
- package/src/packlets/services/service-context.test.ts +9 -6
- package/src/packlets/services/service-context.ts +29 -11
- package/src/packlets/services/service-host.ts +60 -24
- package/src/packlets/services/service-registry.test.ts +1 -1
- package/src/packlets/services/util.ts +2 -0
- package/src/packlets/spaces/data-space-manager.test.ts +4 -4
- package/src/packlets/spaces/data-space.ts +1 -1
- package/src/packlets/spaces/spaces-service.test.ts +1 -1
- package/src/packlets/storage/index.ts +1 -0
- package/src/packlets/storage/level.ts +19 -0
- package/src/packlets/storage/storage.ts +3 -9
- package/src/packlets/storage/util.ts +19 -0
- package/src/packlets/system/system-service.ts +1 -1
- package/src/packlets/testing/test-builder.ts +23 -5
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-BBMYNGYT.mjs.map +0 -7
- package/dist/lib/node/chunk-UUUK4U6J.cjs.map +0 -7
- package/dist/types/src/packlets/services/diagnostics.d.ts.map +0 -1
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@dxos/client-protocol';
|
|
12
12
|
import { Context } from '@dxos/context';
|
|
13
13
|
import { generatePasscode } from '@dxos/credentials';
|
|
14
|
+
import { createKeyPair, sign } from '@dxos/crypto';
|
|
14
15
|
import { invariant } from '@dxos/invariant';
|
|
15
16
|
import { PublicKey } from '@dxos/keys';
|
|
16
17
|
import { log } from '@dxos/log';
|
|
@@ -21,9 +22,9 @@ import {
|
|
|
21
22
|
type SwarmConnection,
|
|
22
23
|
} from '@dxos/network-manager';
|
|
23
24
|
import { InvalidInvitationExtensionRoleError, trace } from '@dxos/protocols';
|
|
24
|
-
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
25
|
+
import { type AdmissionKeypair, Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
25
26
|
import { type DeviceProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
26
|
-
import { AuthenticationResponse } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
27
|
+
import { AuthenticationResponse, type IntroductionResponse } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
27
28
|
|
|
28
29
|
import {
|
|
29
30
|
InvitationGuestExtension,
|
|
@@ -74,9 +75,11 @@ export class InvitationsHandler {
|
|
|
74
75
|
state = Invitation.State.INIT,
|
|
75
76
|
timeout = INVITATION_TIMEOUT,
|
|
76
77
|
swarmKey = PublicKey.random(),
|
|
77
|
-
persistent =
|
|
78
|
+
persistent = options?.authMethod !== Invitation.AuthMethod.KNOWN_PUBLIC_KEY, // default no not storing keypairs
|
|
78
79
|
created = new Date(),
|
|
79
|
-
|
|
80
|
+
guestKeypair = undefined,
|
|
81
|
+
lifetime = 86400, // 1 day,
|
|
82
|
+
multiUse = false,
|
|
80
83
|
} = options ?? {};
|
|
81
84
|
const authCode =
|
|
82
85
|
options?.authCode ??
|
|
@@ -91,9 +94,12 @@ export class InvitationsHandler {
|
|
|
91
94
|
swarmKey,
|
|
92
95
|
authCode,
|
|
93
96
|
timeout,
|
|
94
|
-
persistent,
|
|
97
|
+
persistent: persistent && type !== Invitation.Type.DELEGATED, // delegated invitations are persisted in control feed
|
|
98
|
+
guestKeypair:
|
|
99
|
+
guestKeypair ?? (authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? createAdmissionKeypair() : undefined),
|
|
95
100
|
created,
|
|
96
101
|
lifetime,
|
|
102
|
+
multiUse,
|
|
97
103
|
...protocol.getInvitationContext(),
|
|
98
104
|
};
|
|
99
105
|
|
|
@@ -162,7 +168,7 @@ export class InvitationsHandler {
|
|
|
162
168
|
}
|
|
163
169
|
log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.error({ id: traceId, error: err }));
|
|
164
170
|
} finally {
|
|
165
|
-
if (
|
|
171
|
+
if (!multiUse) {
|
|
166
172
|
// Wait for graceful close before disposing.
|
|
167
173
|
await swarmConnection.close();
|
|
168
174
|
await ctx.dispose();
|
|
@@ -187,7 +193,7 @@ export class InvitationsHandler {
|
|
|
187
193
|
return extension;
|
|
188
194
|
};
|
|
189
195
|
|
|
190
|
-
if (invitation.lifetime && invitation.created
|
|
196
|
+
if (invitation.lifetime && invitation.created) {
|
|
191
197
|
if (invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()) {
|
|
192
198
|
log.warn('invitation has already expired');
|
|
193
199
|
} else {
|
|
@@ -245,7 +251,6 @@ export class InvitationsHandler {
|
|
|
245
251
|
const { timeout = INVITATION_TIMEOUT } = invitation;
|
|
246
252
|
invariant(protocol);
|
|
247
253
|
|
|
248
|
-
// TODO(nf): duplicate check in InvitationsService
|
|
249
254
|
if (deviceProfile) {
|
|
250
255
|
invariant(invitation.kind === Invitation.Kind.DEVICE, 'deviceProfile provided for non-device invitation');
|
|
251
256
|
}
|
|
@@ -317,26 +322,13 @@ export class InvitationsHandler {
|
|
|
317
322
|
|
|
318
323
|
// 2. Get authentication code.
|
|
319
324
|
if (isAuthenticationRequired(invitation)) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
setState({ state: Invitation.State.AUTHENTICATING });
|
|
327
|
-
const response = await extension.rpc.InvitationHostService.authenticate({ authCode });
|
|
328
|
-
if (response.status === undefined || response.status === AuthenticationResponse.Status.OK) {
|
|
325
|
+
switch (invitation.authMethod) {
|
|
326
|
+
case Invitation.AuthMethod.SHARED_SECRET:
|
|
327
|
+
await this._handleGuestOtpAuth(extension, setState, authenticated, { timeout });
|
|
328
|
+
break;
|
|
329
|
+
case Invitation.AuthMethod.KNOWN_PUBLIC_KEY:
|
|
330
|
+
await this._handleGuestKpkAuth(extension, setState, invitation, introductionResponse);
|
|
329
331
|
break;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (response.status === AuthenticationResponse.Status.INVALID_OTP) {
|
|
333
|
-
if (attempt === MAX_OTP_ATTEMPTS) {
|
|
334
|
-
throw new Error(`Maximum retry attempts: ${MAX_OTP_ATTEMPTS}`);
|
|
335
|
-
} else {
|
|
336
|
-
log('retrying invalid code', { attempt });
|
|
337
|
-
authenticated.reset();
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
332
|
}
|
|
341
333
|
}
|
|
342
334
|
|
|
@@ -423,13 +415,61 @@ export class InvitationsHandler {
|
|
|
423
415
|
|
|
424
416
|
return observable;
|
|
425
417
|
}
|
|
418
|
+
|
|
419
|
+
private async _handleGuestOtpAuth(
|
|
420
|
+
extension: InvitationGuestExtension,
|
|
421
|
+
setState: (newState: Partial<Invitation>) => void,
|
|
422
|
+
authenticated: Trigger<string>,
|
|
423
|
+
options: { timeout: number },
|
|
424
|
+
) {
|
|
425
|
+
for (let attempt = 1; attempt <= MAX_OTP_ATTEMPTS; attempt++) {
|
|
426
|
+
log('guest waiting for authentication code...');
|
|
427
|
+
setState({ state: Invitation.State.READY_FOR_AUTHENTICATION });
|
|
428
|
+
const authCode = await authenticated.wait(options);
|
|
429
|
+
|
|
430
|
+
log('sending authentication request');
|
|
431
|
+
setState({ state: Invitation.State.AUTHENTICATING });
|
|
432
|
+
const response = await extension.rpc.InvitationHostService.authenticate({ authCode });
|
|
433
|
+
if (response.status === undefined || response.status === AuthenticationResponse.Status.OK) {
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (response.status === AuthenticationResponse.Status.INVALID_OTP) {
|
|
438
|
+
if (attempt === MAX_OTP_ATTEMPTS) {
|
|
439
|
+
throw new Error(`Maximum retry attempts: ${MAX_OTP_ATTEMPTS}`);
|
|
440
|
+
} else {
|
|
441
|
+
log('retrying invalid code', { attempt });
|
|
442
|
+
authenticated.reset();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private async _handleGuestKpkAuth(
|
|
449
|
+
extension: InvitationGuestExtension,
|
|
450
|
+
setState: (newState: Partial<Invitation>) => void,
|
|
451
|
+
invitation: Invitation,
|
|
452
|
+
introductionResponse: IntroductionResponse,
|
|
453
|
+
) {
|
|
454
|
+
if (invitation.guestKeypair?.privateKey == null) {
|
|
455
|
+
throw new Error('keypair missing in the invitation');
|
|
456
|
+
}
|
|
457
|
+
if (introductionResponse.challenge == null) {
|
|
458
|
+
throw new Error('challenge missing in the introduction');
|
|
459
|
+
}
|
|
460
|
+
log('sending authentication request');
|
|
461
|
+
setState({ state: Invitation.State.AUTHENTICATING });
|
|
462
|
+
const signature = sign(Buffer.from(introductionResponse.challenge), invitation.guestKeypair.privateKey);
|
|
463
|
+
const response = await extension.rpc.InvitationHostService.authenticate({
|
|
464
|
+
signedChallenge: signature,
|
|
465
|
+
});
|
|
466
|
+
if (response.status !== AuthenticationResponse.Status.OK) {
|
|
467
|
+
throw new Error(`Authentication failed with code: ${response.status}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
426
470
|
}
|
|
427
471
|
|
|
428
|
-
export const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
invitation.lifetime &&
|
|
432
|
-
invitation.lifetime !== 0 &&
|
|
433
|
-
invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()
|
|
434
|
-
);
|
|
472
|
+
export const createAdmissionKeypair = (): AdmissionKeypair => {
|
|
473
|
+
const keypair = createKeyPair();
|
|
474
|
+
return { publicKey: PublicKey.from(keypair.publicKey), privateKey: keypair.secretKey };
|
|
435
475
|
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Event } from '@dxos/async';
|
|
6
|
+
import type { AuthenticatingInvitation, CancellableInvitation } from '@dxos/client-protocol';
|
|
7
|
+
import { type Context } from '@dxos/context';
|
|
8
|
+
import { hasInvitationExpired, type MetadataStore } from '@dxos/echo-pipeline';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
import {
|
|
12
|
+
type AcceptInvitationRequest,
|
|
13
|
+
type Invitation,
|
|
14
|
+
type AuthenticationRequest,
|
|
15
|
+
} from '@dxos/protocols/proto/dxos/client/services';
|
|
16
|
+
|
|
17
|
+
import type { InvitationProtocol } from './invitation-protocol';
|
|
18
|
+
import type { InvitationsHandler } from './invitations-handler';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Entry point for creating and accepting invitations, keeps track of existing invitation set and
|
|
22
|
+
* emits events when the set changes.
|
|
23
|
+
*/
|
|
24
|
+
export class InvitationsManager {
|
|
25
|
+
private readonly _createInvitations = new Map<string, CancellableInvitation>();
|
|
26
|
+
private readonly _acceptInvitations = new Map<string, AuthenticatingInvitation>();
|
|
27
|
+
|
|
28
|
+
public readonly invitationCreated = new Event<Invitation>();
|
|
29
|
+
public readonly invitationAccepted = new Event<Invitation>();
|
|
30
|
+
public readonly removedCreated = new Event<Invitation>();
|
|
31
|
+
public readonly removedAccepted = new Event<Invitation>();
|
|
32
|
+
public readonly saved = new Event<Invitation>();
|
|
33
|
+
|
|
34
|
+
private readonly _persistentInvitationsLoadedEvent = new Event();
|
|
35
|
+
private _persistentInvitationsLoaded = false;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly _invitationsHandler: InvitationsHandler,
|
|
39
|
+
private readonly _getHandler: (invitation: Invitation) => InvitationProtocol,
|
|
40
|
+
private readonly _metadataStore: MetadataStore,
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
createInvitation(options: Invitation): CancellableInvitation {
|
|
44
|
+
const existingInvitation = this._createInvitations.get(options.invitationId);
|
|
45
|
+
if (existingInvitation) {
|
|
46
|
+
return existingInvitation;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handler = this._getHandler(options);
|
|
50
|
+
const invitation = this._invitationsHandler.createInvitation(handler, options);
|
|
51
|
+
this._createInvitations.set(invitation.get().invitationId, invitation);
|
|
52
|
+
this.invitationCreated.emit(invitation.get());
|
|
53
|
+
|
|
54
|
+
const saveInvitationTask = invitation.get().persistent
|
|
55
|
+
? this._safePersistInBackground(invitation)
|
|
56
|
+
: Promise.resolve();
|
|
57
|
+
|
|
58
|
+
// onComplete is called on cancel, expiration, or redemption of a single-use invitation
|
|
59
|
+
this._onInvitationComplete(invitation, async () => {
|
|
60
|
+
this._createInvitations.delete(invitation.get().invitationId);
|
|
61
|
+
this.removedCreated.emit(invitation.get());
|
|
62
|
+
if (invitation.get().persistent) {
|
|
63
|
+
await saveInvitationTask;
|
|
64
|
+
await this._safeDeleteInvitation(invitation.get());
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return invitation;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async loadPersistentInvitations(): Promise<{ invitations: Invitation[] }> {
|
|
72
|
+
if (this._persistentInvitationsLoaded) {
|
|
73
|
+
const invitations = this.getCreatedInvitations().filter((i) => i.persistent);
|
|
74
|
+
return { invitations };
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const persistentInvitations = this._metadataStore.getInvitations();
|
|
78
|
+
// get saved persistent invitations, filter and remove from storage those that have expired.
|
|
79
|
+
const freshInvitations = persistentInvitations.filter((invitation) => !hasInvitationExpired(invitation));
|
|
80
|
+
|
|
81
|
+
const cInvitations = freshInvitations.map((persistentInvitation) => {
|
|
82
|
+
invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
|
|
83
|
+
return this.createInvitation({ ...persistentInvitation, persistent: false }).get();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return { invitations: cInvitations };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
log.catch(err);
|
|
89
|
+
return { invitations: [] };
|
|
90
|
+
} finally {
|
|
91
|
+
this._persistentInvitationsLoadedEvent.emit();
|
|
92
|
+
this._persistentInvitationsLoaded = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
acceptInvitation(request: AcceptInvitationRequest): AuthenticatingInvitation {
|
|
97
|
+
const options = request.invitation;
|
|
98
|
+
const existingInvitation = this._acceptInvitations.get(options.invitationId);
|
|
99
|
+
if (existingInvitation) {
|
|
100
|
+
return existingInvitation;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const handler = this._getHandler(options);
|
|
104
|
+
const invitation = this._invitationsHandler.acceptInvitation(handler, options, request.deviceProfile);
|
|
105
|
+
this._acceptInvitations.set(invitation.get().invitationId, invitation);
|
|
106
|
+
this.invitationAccepted.emit(invitation.get());
|
|
107
|
+
|
|
108
|
+
this._onInvitationComplete(invitation, () => {
|
|
109
|
+
this._acceptInvitations.delete(invitation.get().invitationId);
|
|
110
|
+
this.removedAccepted.emit(invitation.get());
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return invitation;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async authenticate({ invitationId, authCode }: AuthenticationRequest): Promise<void> {
|
|
117
|
+
log('authenticating...');
|
|
118
|
+
invariant(invitationId);
|
|
119
|
+
const observable = this._acceptInvitations.get(invitationId);
|
|
120
|
+
if (!observable) {
|
|
121
|
+
log.warn('invalid invitation', { invitationId });
|
|
122
|
+
} else {
|
|
123
|
+
await observable.authenticate(authCode);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async cancelInvitation({ invitationId }: { invitationId: string }): Promise<void> {
|
|
128
|
+
log('cancelInvitation...', { invitationId });
|
|
129
|
+
invariant(invitationId);
|
|
130
|
+
const created = this._createInvitations.get(invitationId);
|
|
131
|
+
if (created) {
|
|
132
|
+
// remove from storage before modifying in-memory state, higher chance of failing
|
|
133
|
+
if (created.get().persistent) {
|
|
134
|
+
await this._metadataStore.removeInvitation(invitationId);
|
|
135
|
+
}
|
|
136
|
+
await created.cancel();
|
|
137
|
+
this._createInvitations.delete(invitationId);
|
|
138
|
+
this.removedCreated.emit(created.get());
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const accepted = this._acceptInvitations.get(invitationId);
|
|
143
|
+
if (accepted) {
|
|
144
|
+
await accepted.cancel();
|
|
145
|
+
this._acceptInvitations.delete(invitationId);
|
|
146
|
+
this.removedAccepted.emit(accepted.get());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getCreatedInvitations(): Invitation[] {
|
|
151
|
+
return [...this._createInvitations.values()].map((i) => i.get());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getAcceptedInvitations(): Invitation[] {
|
|
155
|
+
return [...this._acceptInvitations.values()].map((i) => i.get());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
onPersistentInvitationsLoaded(ctx: Context, callback: () => void) {
|
|
159
|
+
if (this._persistentInvitationsLoaded) {
|
|
160
|
+
callback();
|
|
161
|
+
} else {
|
|
162
|
+
this._persistentInvitationsLoadedEvent.once(ctx, () => callback());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private _safePersistInBackground(invitation: CancellableInvitation): Promise<void> {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
setTimeout(async () => {
|
|
169
|
+
try {
|
|
170
|
+
await this._metadataStore.addInvitation(invitation.get());
|
|
171
|
+
this.saved.emit(invitation.get());
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
log.catch(err);
|
|
174
|
+
await invitation.cancel();
|
|
175
|
+
} finally {
|
|
176
|
+
resolve();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async _safeDeleteInvitation(invitation: Invitation): Promise<void> {
|
|
183
|
+
try {
|
|
184
|
+
await this._metadataStore.removeInvitation(invitation.invitationId);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
log.catch(err);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private _onInvitationComplete(invitation: CancellableInvitation, callback: () => void) {
|
|
191
|
+
invitation.subscribe(
|
|
192
|
+
() => {},
|
|
193
|
+
() => {},
|
|
194
|
+
callback,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -2,43 +2,22 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Event, scheduleTask } from '@dxos/async';
|
|
6
|
-
import { type AuthenticatingInvitation, type CancellableInvitation } from '@dxos/client-protocol';
|
|
7
5
|
import { Stream } from '@dxos/codec-protobuf';
|
|
8
|
-
import { Context } from '@dxos/context';
|
|
9
|
-
import { type MetadataStore } from '@dxos/echo-pipeline';
|
|
10
|
-
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { log } from '@dxos/log';
|
|
12
6
|
import {
|
|
13
7
|
type AuthenticationRequest,
|
|
14
8
|
type AcceptInvitationRequest,
|
|
15
|
-
Invitation,
|
|
9
|
+
type Invitation,
|
|
16
10
|
type InvitationsService,
|
|
17
11
|
QueryInvitationsResponse,
|
|
18
12
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
19
13
|
|
|
20
|
-
import { type
|
|
21
|
-
import { invitationExpired, type InvitationsHandler } from './invitations-handler';
|
|
14
|
+
import { type InvitationsManager } from './invitations-manager';
|
|
22
15
|
|
|
23
16
|
/**
|
|
24
17
|
* Adapts invitation service observable to client/service stream.
|
|
25
18
|
*/
|
|
26
19
|
export class InvitationsServiceImpl implements InvitationsService {
|
|
27
|
-
private readonly
|
|
28
|
-
private readonly _acceptInvitations = new Map<string, AuthenticatingInvitation>();
|
|
29
|
-
private readonly _invitationCreated = new Event<Invitation>();
|
|
30
|
-
private readonly _invitationAccepted = new Event<Invitation>();
|
|
31
|
-
private readonly _removedCreated = new Event<Invitation>();
|
|
32
|
-
private readonly _removedAccepted = new Event<Invitation>();
|
|
33
|
-
private readonly _saved = new Event<Invitation>();
|
|
34
|
-
private readonly _persistentInvitationsLoadedEvent = new Event();
|
|
35
|
-
private _persistentInvitationsLoaded = false;
|
|
36
|
-
|
|
37
|
-
constructor(
|
|
38
|
-
private readonly _invitationsHandler: InvitationsHandler,
|
|
39
|
-
private readonly _getHandler: (invitation: Invitation) => InvitationProtocol,
|
|
40
|
-
private readonly _metadataStore: MetadataStore,
|
|
41
|
-
) {}
|
|
20
|
+
constructor(private readonly _invitationsManager: InvitationsManager) {}
|
|
42
21
|
|
|
43
22
|
// TODO(burdon): Guest/host label.
|
|
44
23
|
getLoggingContext() {
|
|
@@ -48,148 +27,31 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
48
27
|
}
|
|
49
28
|
|
|
50
29
|
createInvitation(options: Invitation): Stream<Invitation> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const savePersistentInvitationCtx = new Context();
|
|
54
|
-
const existingInvitation = this._createInvitations.get(options.invitationId);
|
|
55
|
-
if (existingInvitation) {
|
|
56
|
-
invitation = existingInvitation;
|
|
57
|
-
} else {
|
|
58
|
-
const handler = this._getHandler(options);
|
|
59
|
-
invitation = this._invitationsHandler.createInvitation(handler, options);
|
|
60
|
-
this._createInvitations.set(invitation.get().invitationId, invitation);
|
|
61
|
-
this._invitationCreated.emit(invitation.get());
|
|
62
|
-
}
|
|
63
|
-
|
|
30
|
+
const invitation = this._invitationsManager.createInvitation(options);
|
|
64
31
|
return new Stream<Invitation>(({ next, close }) => {
|
|
65
|
-
|
|
66
|
-
scheduleTask(savePersistentInvitationCtx, async () => {
|
|
67
|
-
try {
|
|
68
|
-
await this._metadataStore.addInvitation(invitation.get());
|
|
69
|
-
this._saved.emit(invitation.get());
|
|
70
|
-
} catch (err: any) {
|
|
71
|
-
close(err);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
invitation.subscribe(
|
|
76
|
-
(invitation) => {
|
|
77
|
-
next(invitation);
|
|
78
|
-
},
|
|
79
|
-
async (err: Error) => {
|
|
80
|
-
await savePersistentInvitationCtx.dispose();
|
|
81
|
-
|
|
82
|
-
// TODO(nf): also remove from storage?
|
|
83
|
-
close(err);
|
|
84
|
-
},
|
|
85
|
-
async () => {
|
|
86
|
-
close();
|
|
87
|
-
if (invitation.get().persistent) {
|
|
88
|
-
await savePersistentInvitationCtx.dispose();
|
|
89
|
-
// TODO(nf): remove on all complete conditions?
|
|
90
|
-
await this._metadataStore.removeInvitation(invitation.get().invitationId);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
this._createInvitations.delete(invitation.get().invitationId);
|
|
94
|
-
if (invitation.get().type !== Invitation.Type.MULTIUSE) {
|
|
95
|
-
this._removedCreated.emit(invitation.get());
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async loadPersistentInvitations() {
|
|
103
|
-
const persistentInvitations = this._metadataStore.getInvitations();
|
|
104
|
-
|
|
105
|
-
// get saved persistent invitations, filter and remove from storage those that have expired.
|
|
106
|
-
const freshInvitations = persistentInvitations.filter(async (invitation) => !invitationExpired(invitation));
|
|
107
|
-
|
|
108
|
-
const cInvitations = freshInvitations.map((persistentInvitation) => {
|
|
109
|
-
invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
|
|
110
|
-
|
|
111
|
-
const handler = this._getHandler(persistentInvitation);
|
|
112
|
-
const invitation = this._invitationsHandler.createInvitation(handler, persistentInvitation);
|
|
113
|
-
this._createInvitations.set(invitation.get().invitationId, invitation);
|
|
114
|
-
this._invitationCreated.emit(invitation.get());
|
|
115
|
-
return persistentInvitation;
|
|
32
|
+
invitation.subscribe(next, close, close);
|
|
116
33
|
});
|
|
117
|
-
this._persistentInvitationsLoadedEvent.emit();
|
|
118
|
-
this._persistentInvitationsLoaded = true;
|
|
119
|
-
return { invitations: cInvitations };
|
|
120
34
|
}
|
|
121
35
|
|
|
122
|
-
acceptInvitation(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// TODO(nf): duplicate check in InvitationHandler
|
|
126
|
-
if (deviceProfile) {
|
|
127
|
-
invariant(options.kind === Invitation.Kind.DEVICE, 'deviceProfile provided for non-device invitation');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const existingInvitation = this._acceptInvitations.get(options.invitationId);
|
|
131
|
-
if (existingInvitation) {
|
|
132
|
-
invitation = existingInvitation;
|
|
133
|
-
} else {
|
|
134
|
-
const handler = this._getHandler(options);
|
|
135
|
-
invitation = this._invitationsHandler.acceptInvitation(handler, options, deviceProfile);
|
|
136
|
-
this._acceptInvitations.set(invitation.get().invitationId, invitation);
|
|
137
|
-
this._invitationAccepted.emit(invitation.get());
|
|
138
|
-
}
|
|
139
|
-
|
|
36
|
+
acceptInvitation(request: AcceptInvitationRequest): Stream<Invitation> {
|
|
37
|
+
const invitation = this._invitationsManager.acceptInvitation(request);
|
|
140
38
|
return new Stream<Invitation>(({ next, close }) => {
|
|
141
|
-
invitation.subscribe(
|
|
142
|
-
(invitation) => {
|
|
143
|
-
next(invitation);
|
|
144
|
-
},
|
|
145
|
-
(err: Error) => {
|
|
146
|
-
close(err);
|
|
147
|
-
},
|
|
148
|
-
() => {
|
|
149
|
-
close();
|
|
150
|
-
this._acceptInvitations.delete(invitation.get().invitationId);
|
|
151
|
-
if (invitation.get().type !== Invitation.Type.MULTIUSE) {
|
|
152
|
-
this._removedAccepted.emit(invitation.get());
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
);
|
|
39
|
+
invitation.subscribe(next, close, close);
|
|
156
40
|
});
|
|
157
41
|
}
|
|
158
42
|
|
|
159
|
-
async authenticate(
|
|
160
|
-
|
|
161
|
-
invariant(invitationId);
|
|
162
|
-
const observable = this._acceptInvitations.get(invitationId);
|
|
163
|
-
if (!observable) {
|
|
164
|
-
log.warn('invalid invitation', { invitationId });
|
|
165
|
-
} else {
|
|
166
|
-
await observable.authenticate(authCode);
|
|
167
|
-
}
|
|
43
|
+
async authenticate(request: AuthenticationRequest): Promise<void> {
|
|
44
|
+
return this._invitationsManager.authenticate(request);
|
|
168
45
|
}
|
|
169
46
|
|
|
170
|
-
async cancelInvitation(
|
|
171
|
-
|
|
172
|
-
invariant(invitationId);
|
|
173
|
-
const created = this._createInvitations.get(invitationId);
|
|
174
|
-
const accepted = this._acceptInvitations.get(invitationId);
|
|
175
|
-
if (created) {
|
|
176
|
-
await created.cancel();
|
|
177
|
-
this._createInvitations.delete(invitationId);
|
|
178
|
-
this._removedCreated.emit(created.get());
|
|
179
|
-
if (created.get().persistent) {
|
|
180
|
-
await this._metadataStore.removeInvitation(created.get().invitationId);
|
|
181
|
-
}
|
|
182
|
-
} else if (accepted) {
|
|
183
|
-
await accepted.cancel();
|
|
184
|
-
this._acceptInvitations.delete(invitationId);
|
|
185
|
-
this._removedAccepted.emit(accepted.get());
|
|
186
|
-
}
|
|
47
|
+
async cancelInvitation(request: { invitationId: string }): Promise<void> {
|
|
48
|
+
return this._invitationsManager.cancelInvitation(request);
|
|
187
49
|
}
|
|
188
50
|
|
|
189
51
|
queryInvitations(): Stream<QueryInvitationsResponse> {
|
|
190
52
|
return new Stream<QueryInvitationsResponse>(({ next, ctx }) => {
|
|
191
53
|
// Push added invitations to the stream.
|
|
192
|
-
this.
|
|
54
|
+
this._invitationsManager.invitationCreated.on(ctx, (invitation) => {
|
|
193
55
|
next({
|
|
194
56
|
action: QueryInvitationsResponse.Action.ADDED,
|
|
195
57
|
type: QueryInvitationsResponse.Type.CREATED,
|
|
@@ -197,7 +59,7 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
197
59
|
});
|
|
198
60
|
});
|
|
199
61
|
|
|
200
|
-
this.
|
|
62
|
+
this._invitationsManager.invitationAccepted.on(ctx, (invitation) => {
|
|
201
63
|
next({
|
|
202
64
|
action: QueryInvitationsResponse.Action.ADDED,
|
|
203
65
|
type: QueryInvitationsResponse.Type.ACCEPTED,
|
|
@@ -206,7 +68,7 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
206
68
|
});
|
|
207
69
|
|
|
208
70
|
// Push removed invitations to the stream.
|
|
209
|
-
this.
|
|
71
|
+
this._invitationsManager.removedCreated.on(ctx, (invitation) => {
|
|
210
72
|
next({
|
|
211
73
|
action: QueryInvitationsResponse.Action.REMOVED,
|
|
212
74
|
type: QueryInvitationsResponse.Type.CREATED,
|
|
@@ -214,7 +76,7 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
214
76
|
});
|
|
215
77
|
});
|
|
216
78
|
|
|
217
|
-
this.
|
|
79
|
+
this._invitationsManager.removedAccepted.on(ctx, (invitation) => {
|
|
218
80
|
next({
|
|
219
81
|
action: QueryInvitationsResponse.Action.REMOVED,
|
|
220
82
|
type: QueryInvitationsResponse.Type.ACCEPTED,
|
|
@@ -223,7 +85,7 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
223
85
|
});
|
|
224
86
|
|
|
225
87
|
// used only for testing
|
|
226
|
-
this.
|
|
88
|
+
this._invitationsManager.saved.on(ctx, (invitation) => {
|
|
227
89
|
next({
|
|
228
90
|
action: QueryInvitationsResponse.Action.SAVED,
|
|
229
91
|
type: QueryInvitationsResponse.Type.CREATED,
|
|
@@ -235,33 +97,24 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
235
97
|
next({
|
|
236
98
|
action: QueryInvitationsResponse.Action.ADDED,
|
|
237
99
|
type: QueryInvitationsResponse.Type.CREATED,
|
|
238
|
-
invitations:
|
|
100
|
+
invitations: this._invitationsManager.getCreatedInvitations(),
|
|
239
101
|
existing: true,
|
|
240
102
|
});
|
|
241
103
|
|
|
242
104
|
next({
|
|
243
105
|
action: QueryInvitationsResponse.Action.ADDED,
|
|
244
106
|
type: QueryInvitationsResponse.Type.ACCEPTED,
|
|
245
|
-
invitations:
|
|
107
|
+
invitations: this._invitationsManager.getAcceptedInvitations(),
|
|
246
108
|
existing: true,
|
|
247
109
|
});
|
|
248
110
|
|
|
249
|
-
|
|
111
|
+
this._invitationsManager.onPersistentInvitationsLoaded(ctx, () => {
|
|
250
112
|
next({
|
|
251
113
|
action: QueryInvitationsResponse.Action.LOAD_COMPLETE,
|
|
252
114
|
type: QueryInvitationsResponse.Type.CREATED,
|
|
253
115
|
// TODO(nf): populate with invitations
|
|
254
116
|
});
|
|
255
|
-
}
|
|
256
|
-
this._persistentInvitationsLoadedEvent.on(ctx, () => {
|
|
257
|
-
next({
|
|
258
|
-
action: QueryInvitationsResponse.Action.LOAD_COMPLETE,
|
|
259
|
-
type: QueryInvitationsResponse.Type.CREATED,
|
|
260
|
-
// TODO(nf): populate with invitations
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
117
|
+
});
|
|
265
118
|
// TODO(nf): expired invitations?
|
|
266
119
|
});
|
|
267
120
|
}
|
|
@@ -18,7 +18,7 @@ describe('NetworkService', () => {
|
|
|
18
18
|
let networkService: NetworkService;
|
|
19
19
|
|
|
20
20
|
beforeEach(async () => {
|
|
21
|
-
serviceContext = createServiceContext();
|
|
21
|
+
serviceContext = await createServiceContext();
|
|
22
22
|
await serviceContext.open(new Context());
|
|
23
23
|
networkService = new NetworkServiceImpl(serviceContext.networkManager, serviceContext.signalManager);
|
|
24
24
|
});
|