@dxos/client-services 0.4.10-main.e96274c → 0.4.10-main.e99db3e
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-UVEPMM4Z.mjs → chunk-KFDIAZLU.mjs} +355 -287
- package/dist/lib/browser/chunk-KFDIAZLU.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +125 -112
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-GZWBW7OC.cjs → chunk-GAS53XAH.cjs} +445 -377
- package/dist/lib/node/chunk-GAS53XAH.cjs.map +7 -0
- package/dist/lib/node/index.cjs +43 -43
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +125 -115
- package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/indexing/util.d.ts +0 -5
- package/dist/types/src/packlets/indexing/util.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +3 -1
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +6 -1
- package/dist/types/src/packlets/invitations/invitation-protocol.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 +9 -7
- package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +5 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +3 -0
- 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 +34 -34
- package/src/packlets/identity/identity-manager.ts +1 -0
- package/src/packlets/identity/identity.test.ts +3 -0
- package/src/packlets/indexing/util.ts +1 -65
- package/src/packlets/invitations/device-invitation-protocol.ts +6 -1
- package/src/packlets/invitations/invitation-protocol.ts +7 -1
- package/src/packlets/invitations/invitations-handler.ts +10 -71
- package/src/packlets/invitations/invitations-manager.ts +114 -40
- package/src/packlets/invitations/invitations-service.ts +4 -2
- package/src/packlets/invitations/space-invitation-protocol.ts +45 -3
- package/src/packlets/services/service-context.ts +3 -2
- package/src/packlets/spaces/data-space-manager.ts +48 -2
- package/src/packlets/testing/invitation-utils.ts +100 -97
- package/src/packlets/testing/test-builder.ts +19 -1
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-UVEPMM4Z.mjs.map +0 -7
- package/dist/lib/node/chunk-GZWBW7OC.cjs.map +0 -7
|
@@ -2,20 +2,27 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Event } from '@dxos/async';
|
|
6
|
-
import
|
|
7
|
-
|
|
5
|
+
import { Event, PushStream } from '@dxos/async';
|
|
6
|
+
import {
|
|
7
|
+
type AuthenticatingInvitation,
|
|
8
|
+
AUTHENTICATION_CODE_LENGTH,
|
|
9
|
+
CancellableInvitation,
|
|
10
|
+
INVITATION_TIMEOUT,
|
|
11
|
+
} from '@dxos/client-protocol';
|
|
12
|
+
import { Context } from '@dxos/context';
|
|
13
|
+
import { generatePasscode } from '@dxos/credentials';
|
|
8
14
|
import { hasInvitationExpired, type MetadataStore } from '@dxos/echo-pipeline';
|
|
9
15
|
import { invariant } from '@dxos/invariant';
|
|
16
|
+
import { PublicKey } from '@dxos/keys';
|
|
10
17
|
import { log } from '@dxos/log';
|
|
11
18
|
import {
|
|
12
19
|
type AcceptInvitationRequest,
|
|
13
|
-
type Invitation,
|
|
14
20
|
type AuthenticationRequest,
|
|
21
|
+
Invitation,
|
|
15
22
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
16
23
|
|
|
17
24
|
import type { InvitationProtocol } from './invitation-protocol';
|
|
18
|
-
import type
|
|
25
|
+
import { createAdmissionKeypair, type InvitationsHandler } from './invitations-handler';
|
|
19
26
|
|
|
20
27
|
/**
|
|
21
28
|
* Entry point for creating and accepting invitations, keeps track of existing invitation set and
|
|
@@ -36,36 +43,44 @@ export class InvitationsManager {
|
|
|
36
43
|
|
|
37
44
|
constructor(
|
|
38
45
|
private readonly _invitationsHandler: InvitationsHandler,
|
|
39
|
-
private readonly _getHandler: (invitation: Invitation) => InvitationProtocol,
|
|
46
|
+
private readonly _getHandler: (invitation: Partial<Invitation> & Pick<Invitation, 'kind'>) => InvitationProtocol,
|
|
40
47
|
private readonly _metadataStore: MetadataStore,
|
|
41
48
|
) {}
|
|
42
49
|
|
|
43
|
-
createInvitation(options: Invitation): CancellableInvitation {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
async createInvitation(options: Partial<Invitation> & Pick<Invitation, 'kind'>): Promise<CancellableInvitation> {
|
|
51
|
+
if (options.invitationId) {
|
|
52
|
+
const existingInvitation = this._createInvitations.get(options.invitationId);
|
|
53
|
+
if (existingInvitation) {
|
|
54
|
+
return existingInvitation;
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
const handler = this._getHandler(options);
|
|
50
|
-
const invitation = this.
|
|
51
|
-
this.
|
|
52
|
-
this.invitationCreated.emit(invitation.get());
|
|
53
|
-
|
|
54
|
-
const saveInvitationTask = invitation.get().persistent
|
|
55
|
-
? this._safePersistInBackground(invitation)
|
|
56
|
-
: Promise.resolve();
|
|
59
|
+
const invitation = this._createInvitation(handler, options);
|
|
60
|
+
const { ctx, stream, observableInvitation } = this._createObservableInvitation(handler, invitation);
|
|
57
61
|
|
|
62
|
+
this._createInvitations.set(invitation.invitationId, observableInvitation);
|
|
63
|
+
this.invitationCreated.emit(invitation);
|
|
58
64
|
// onComplete is called on cancel, expiration, or redemption of a single-use invitation
|
|
59
|
-
this._onInvitationComplete(
|
|
60
|
-
this._createInvitations.delete(
|
|
61
|
-
this.removedCreated.emit(
|
|
62
|
-
if (
|
|
63
|
-
await
|
|
64
|
-
await this._safeDeleteInvitation(invitation.get());
|
|
65
|
+
this._onInvitationComplete(observableInvitation, async () => {
|
|
66
|
+
this._createInvitations.delete(observableInvitation.get().invitationId);
|
|
67
|
+
this.removedCreated.emit(observableInvitation.get());
|
|
68
|
+
if (observableInvitation.get().persistent) {
|
|
69
|
+
await this._safeDeleteInvitation(observableInvitation.get());
|
|
65
70
|
}
|
|
66
71
|
});
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
try {
|
|
74
|
+
await this._persistIfRequired(handler, stream, invitation);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log.catch(err);
|
|
77
|
+
await observableInvitation.cancel();
|
|
78
|
+
return observableInvitation;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this._invitationsHandler.handleInvitationFlow(ctx, stream, handler, observableInvitation.get());
|
|
82
|
+
|
|
83
|
+
return observableInvitation;
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
async loadPersistentInvitations(): Promise<{ invitations: Invitation[] }> {
|
|
@@ -78,12 +93,13 @@ export class InvitationsManager {
|
|
|
78
93
|
// get saved persistent invitations, filter and remove from storage those that have expired.
|
|
79
94
|
const freshInvitations = persistentInvitations.filter((invitation) => !hasInvitationExpired(invitation));
|
|
80
95
|
|
|
81
|
-
const
|
|
96
|
+
const loadTasks = freshInvitations.map((persistentInvitation) => {
|
|
82
97
|
invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
|
|
83
|
-
return this.createInvitation({ ...persistentInvitation, persistent: false })
|
|
98
|
+
return this.createInvitation({ ...persistentInvitation, persistent: false });
|
|
84
99
|
});
|
|
100
|
+
const cInvitations = await Promise.all(loadTasks);
|
|
85
101
|
|
|
86
|
-
return { invitations: cInvitations };
|
|
102
|
+
return { invitations: cInvitations.map((invitation) => invitation.get()) };
|
|
87
103
|
} catch (err) {
|
|
88
104
|
log.catch(err);
|
|
89
105
|
return { invitations: [] };
|
|
@@ -163,20 +179,78 @@ export class InvitationsManager {
|
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
private _createInvitation(protocol: InvitationProtocol, options?: Partial<Invitation>): Invitation {
|
|
183
|
+
const {
|
|
184
|
+
invitationId = PublicKey.random().toHex(),
|
|
185
|
+
type = Invitation.Type.INTERACTIVE,
|
|
186
|
+
authMethod = Invitation.AuthMethod.SHARED_SECRET,
|
|
187
|
+
state = Invitation.State.INIT,
|
|
188
|
+
timeout = INVITATION_TIMEOUT,
|
|
189
|
+
swarmKey = PublicKey.random(),
|
|
190
|
+
persistent = options?.authMethod !== Invitation.AuthMethod.KNOWN_PUBLIC_KEY, // default no not storing keypairs
|
|
191
|
+
created = new Date(),
|
|
192
|
+
guestKeypair = undefined,
|
|
193
|
+
lifetime = 86400, // 1 day,
|
|
194
|
+
multiUse = false,
|
|
195
|
+
} = options ?? {};
|
|
196
|
+
const authCode =
|
|
197
|
+
options?.authCode ??
|
|
198
|
+
(authMethod === Invitation.AuthMethod.SHARED_SECRET ? generatePasscode(AUTHENTICATION_CODE_LENGTH) : undefined);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
invitationId,
|
|
202
|
+
type,
|
|
203
|
+
authMethod,
|
|
204
|
+
state,
|
|
205
|
+
swarmKey,
|
|
206
|
+
authCode,
|
|
207
|
+
timeout,
|
|
208
|
+
persistent: persistent && type !== Invitation.Type.DELEGATED, // delegated invitations are persisted in control feed
|
|
209
|
+
guestKeypair:
|
|
210
|
+
guestKeypair ?? (authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? createAdmissionKeypair() : undefined),
|
|
211
|
+
created,
|
|
212
|
+
lifetime,
|
|
213
|
+
multiUse,
|
|
214
|
+
delegationCredentialId: options?.delegationCredentialId,
|
|
215
|
+
...protocol.getInvitationContext(),
|
|
216
|
+
} satisfies Invitation;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private _createObservableInvitation(handler: InvitationProtocol, invitation: Invitation) {
|
|
220
|
+
const stream = new PushStream<Invitation>();
|
|
221
|
+
const ctx = new Context({
|
|
222
|
+
onError: (err) => {
|
|
223
|
+
stream.error(err);
|
|
224
|
+
void ctx.dispose();
|
|
225
|
+
},
|
|
179
226
|
});
|
|
227
|
+
ctx.onDispose(() => {
|
|
228
|
+
log('complete', { ...handler.toJSON() });
|
|
229
|
+
stream.complete();
|
|
230
|
+
});
|
|
231
|
+
const observableInvitation = new CancellableInvitation({
|
|
232
|
+
initialInvitation: invitation,
|
|
233
|
+
subscriber: stream.observable,
|
|
234
|
+
onCancel: async () => {
|
|
235
|
+
stream.next({ ...invitation, state: Invitation.State.CANCELLED });
|
|
236
|
+
await ctx.dispose();
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
return { ctx, stream, observableInvitation };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private async _persistIfRequired(
|
|
243
|
+
handler: InvitationProtocol,
|
|
244
|
+
changeStream: PushStream<Invitation>,
|
|
245
|
+
invitation: Invitation,
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
if (invitation.type === Invitation.Type.DELEGATED && invitation.delegationCredentialId == null) {
|
|
248
|
+
const delegationCredentialId = await handler.delegate(invitation);
|
|
249
|
+
changeStream.next({ ...invitation, delegationCredentialId });
|
|
250
|
+
} else if (invitation.persistent) {
|
|
251
|
+
await this._metadataStore.addInvitation(invitation);
|
|
252
|
+
this.saved.emit(invitation);
|
|
253
|
+
}
|
|
180
254
|
}
|
|
181
255
|
|
|
182
256
|
private async _safeDeleteInvitation(invitation: Invitation): Promise<void> {
|
|
@@ -27,9 +27,11 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
createInvitation(options: Invitation): Stream<Invitation> {
|
|
30
|
-
const invitation = this._invitationsManager.createInvitation(options);
|
|
31
30
|
return new Stream<Invitation>(({ next, close }) => {
|
|
32
|
-
|
|
31
|
+
void this._invitationsManager
|
|
32
|
+
.createInvitation(options)
|
|
33
|
+
.then((invitation) => invitation.subscribe(next, close, close))
|
|
34
|
+
.catch(close);
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createAdmissionCredentials,
|
|
7
|
+
createDelegatedSpaceInvitationCredential,
|
|
8
|
+
getCredentialAssertion,
|
|
9
|
+
} from '@dxos/credentials';
|
|
6
10
|
import { writeMessages } from '@dxos/feed-store';
|
|
7
11
|
import { invariant } from '@dxos/invariant';
|
|
8
12
|
import { type Keyring } from '@dxos/keyring';
|
|
@@ -11,7 +15,7 @@ import { log } from '@dxos/log';
|
|
|
11
15
|
import { AlreadyJoinedError } from '@dxos/protocols';
|
|
12
16
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
13
17
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
14
|
-
import { type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
18
|
+
import { SpaceMember, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
15
19
|
import {
|
|
16
20
|
type AdmissionRequest,
|
|
17
21
|
type AdmissionResponse,
|
|
@@ -43,7 +47,11 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
|
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
async admit(
|
|
50
|
+
async admit(
|
|
51
|
+
invitation: Invitation,
|
|
52
|
+
request: AdmissionRequest,
|
|
53
|
+
guestProfile?: ProfileDocument | undefined,
|
|
54
|
+
): Promise<AdmissionResponse> {
|
|
47
55
|
invariant(this._spaceKey);
|
|
48
56
|
const space = await this._spaceManager.spaces.get(this._spaceKey);
|
|
49
57
|
invariant(space);
|
|
@@ -59,6 +67,7 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
|
|
|
59
67
|
space.key,
|
|
60
68
|
space.inner.genesisFeedKey,
|
|
61
69
|
guestProfile,
|
|
70
|
+
invitation.delegationCredentialId,
|
|
62
71
|
);
|
|
63
72
|
|
|
64
73
|
// TODO(dmaretskyi): Refactor.
|
|
@@ -76,6 +85,39 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
|
|
|
76
85
|
};
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
async delegate(invitation: Invitation): Promise<PublicKey> {
|
|
89
|
+
invariant(this._spaceKey);
|
|
90
|
+
const space = await this._spaceManager.spaces.get(this._spaceKey);
|
|
91
|
+
invariant(space);
|
|
92
|
+
if (invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY) {
|
|
93
|
+
invariant(invitation.guestKeypair?.publicKey);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
log('writing delegate space invitation', { host: this._signingContext.deviceKey, id: invitation.invitationId });
|
|
97
|
+
const credential = await createDelegatedSpaceInvitationCredential(
|
|
98
|
+
this._signingContext.credentialSigner,
|
|
99
|
+
space.key,
|
|
100
|
+
{
|
|
101
|
+
invitationId: invitation.invitationId,
|
|
102
|
+
authMethod: invitation.authMethod,
|
|
103
|
+
swarmKey: invitation.swarmKey,
|
|
104
|
+
role: SpaceMember.Role.ADMIN,
|
|
105
|
+
expiresOn: invitation.lifetime
|
|
106
|
+
? new Date((invitation.created?.getTime() ?? Date.now()) + invitation.lifetime)
|
|
107
|
+
: undefined,
|
|
108
|
+
multiUse: invitation.multiUse ?? false,
|
|
109
|
+
guestKey:
|
|
110
|
+
invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY
|
|
111
|
+
? invitation.guestKeypair!.publicKey
|
|
112
|
+
: undefined,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
invariant(credential.credential);
|
|
117
|
+
await writeMessages(space.inner.controlPipeline.writer, [credential]);
|
|
118
|
+
return credential.credential.credential.id!;
|
|
119
|
+
}
|
|
120
|
+
|
|
79
121
|
checkInvitation(invitation: Partial<Invitation>) {
|
|
80
122
|
if (invitation.spaceKey && this._spaceManager.spaces.has(invitation.spaceKey)) {
|
|
81
123
|
return new AlreadyJoinedError('Already joined space.');
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type IdentityManagerRuntimeParams,
|
|
33
33
|
type JoinIdentityParams,
|
|
34
34
|
} from '../identity';
|
|
35
|
-
import {
|
|
35
|
+
import { createSelectedDocumentsIterator } from '../indexing';
|
|
36
36
|
import {
|
|
37
37
|
DeviceInvitationProtocol,
|
|
38
38
|
InvitationsHandler,
|
|
@@ -131,10 +131,10 @@ export class ServiceContext extends Resource {
|
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
this.indexer = new Indexer({
|
|
134
|
+
db: this.level,
|
|
134
135
|
indexStore: new IndexStore({ db: level.sublevel('index-storage') }),
|
|
135
136
|
metadataStore: this.indexMetadata,
|
|
136
137
|
loadDocuments: createSelectedDocumentsIterator(this.automergeHost),
|
|
137
|
-
getAllDocuments: createDocumentsIterator(this.automergeHost),
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
this.invitations = new InvitationsHandler(this.networkManager);
|
|
@@ -256,6 +256,7 @@ export class ServiceContext extends Resource {
|
|
|
256
256
|
signingContext,
|
|
257
257
|
this.feedStore,
|
|
258
258
|
this.automergeHost,
|
|
259
|
+
this.invitationsManager,
|
|
259
260
|
this._runtimeParams as DataSpaceManagerRuntimeParams,
|
|
260
261
|
);
|
|
261
262
|
await this.dataSpaceManager.open();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { Event, synchronized, trackLeaks } from '@dxos/async';
|
|
6
6
|
import { Context, cancelWithContext } from '@dxos/context';
|
|
7
|
-
import { getCredentialAssertion, type CredentialSigner } from '@dxos/credentials';
|
|
7
|
+
import { getCredentialAssertion, type CredentialSigner, type DelegateInvitationCredential } from '@dxos/credentials';
|
|
8
8
|
import { type AutomergeHost, type MetadataStore, type Space, type SpaceManager } from '@dxos/echo-pipeline';
|
|
9
9
|
import { type FeedStore } from '@dxos/feed-store';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
@@ -12,10 +12,11 @@ import { type Keyring } from '@dxos/keyring';
|
|
|
12
12
|
import { PublicKey } from '@dxos/keys';
|
|
13
13
|
import { log } from '@dxos/log';
|
|
14
14
|
import { trace } from '@dxos/protocols';
|
|
15
|
-
import { SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
15
|
+
import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
16
16
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
17
17
|
import { type SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
18
18
|
import { type Credential, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
19
|
+
import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
19
20
|
import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
|
|
20
21
|
import { type Timeframe } from '@dxos/timeframe';
|
|
21
22
|
import { ComplexMap, deferFunction, forEachAsync } from '@dxos/util';
|
|
@@ -23,6 +24,7 @@ import { ComplexMap, deferFunction, forEachAsync } from '@dxos/util';
|
|
|
23
24
|
import { DataSpace } from './data-space';
|
|
24
25
|
import { spaceGenesis } from './genesis';
|
|
25
26
|
import { createAuthProvider } from '../identity';
|
|
27
|
+
import { type InvitationsManager } from '../invitations';
|
|
26
28
|
|
|
27
29
|
const PRESENCE_ANNOUNCE_INTERVAL = 10_000;
|
|
28
30
|
const PRESENCE_OFFLINE_TIMEOUT = 20_000;
|
|
@@ -78,6 +80,7 @@ export class DataSpaceManager {
|
|
|
78
80
|
private readonly _signingContext: SigningContext,
|
|
79
81
|
private readonly _feedStore: FeedStore<FeedMessage>,
|
|
80
82
|
private readonly _automergeHost: AutomergeHost,
|
|
83
|
+
private readonly _invitationsManager: InvitationsManager,
|
|
81
84
|
params?: DataSpaceManagerRuntimeParams,
|
|
82
85
|
) {
|
|
83
86
|
const {
|
|
@@ -247,6 +250,9 @@ export class DataSpaceManager {
|
|
|
247
250
|
log.warn('auth failure');
|
|
248
251
|
},
|
|
249
252
|
memberKey: this._signingContext.identityKey,
|
|
253
|
+
onDelegatedInvitationStatusChange: (invitation, isActive) => {
|
|
254
|
+
return this._handleInvitationStatusChange(dataSpace, invitation, isActive);
|
|
255
|
+
},
|
|
250
256
|
});
|
|
251
257
|
controlFeed && (await space.setControlFeed(controlFeed));
|
|
252
258
|
dataFeed && (await space.setDataFeed(dataFeed));
|
|
@@ -267,6 +273,7 @@ export class DataSpaceManager {
|
|
|
267
273
|
afterReady: async () => {
|
|
268
274
|
log('after space ready', { space: space.key, open: this._isOpen });
|
|
269
275
|
if (this._isOpen) {
|
|
276
|
+
await this._createDelegatedInvitations(dataSpace, [...space.spaceState.invitations.entries()]);
|
|
270
277
|
this.updated.emit();
|
|
271
278
|
}
|
|
272
279
|
},
|
|
@@ -289,4 +296,43 @@ export class DataSpaceManager {
|
|
|
289
296
|
this._spaces.set(metadata.key, dataSpace);
|
|
290
297
|
return dataSpace;
|
|
291
298
|
}
|
|
299
|
+
|
|
300
|
+
private async _handleInvitationStatusChange(
|
|
301
|
+
dataSpace: DataSpace | undefined,
|
|
302
|
+
delegatedInvitation: DelegateInvitationCredential,
|
|
303
|
+
isActive: boolean,
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
if (dataSpace?.state !== SpaceState.READY) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (isActive) {
|
|
309
|
+
await this._createDelegatedInvitations(dataSpace, [
|
|
310
|
+
[delegatedInvitation.credentialId, delegatedInvitation.invitation],
|
|
311
|
+
]);
|
|
312
|
+
} else {
|
|
313
|
+
await this._invitationsManager.cancelInvitation(delegatedInvitation.invitation);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async _createDelegatedInvitations(
|
|
318
|
+
space: DataSpace,
|
|
319
|
+
invitations: Array<[PublicKey, DelegateSpaceInvitation]>,
|
|
320
|
+
): Promise<void> {
|
|
321
|
+
const tasks = invitations.map(([credentialId, invitation]) => {
|
|
322
|
+
return this._invitationsManager.createInvitation({
|
|
323
|
+
type: Invitation.Type.DELEGATED,
|
|
324
|
+
kind: Invitation.Kind.SPACE,
|
|
325
|
+
spaceKey: space.key,
|
|
326
|
+
authMethod: invitation.authMethod,
|
|
327
|
+
invitationId: invitation.invitationId,
|
|
328
|
+
swarmKey: invitation.swarmKey,
|
|
329
|
+
guestKeypair: invitation.guestKey ? { publicKey: invitation.guestKey } : undefined,
|
|
330
|
+
lifetime: invitation.expiresOn ? invitation.expiresOn.getTime() - Date.now() : undefined,
|
|
331
|
+
multiUse: invitation.multiUse,
|
|
332
|
+
delegationCredentialId: credentialId,
|
|
333
|
+
persistent: false,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
await Promise.all(tasks);
|
|
337
|
+
}
|
|
292
338
|
}
|
|
@@ -60,146 +60,149 @@ export const performInvitation = ({
|
|
|
60
60
|
const guestComplete = new Trigger<Result>();
|
|
61
61
|
const authCode = new Trigger<string>();
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
void createInvitation(host, options).then((hostObservable) => {
|
|
64
|
+
hostObservable.subscribe(
|
|
65
|
+
async (hostInvitation: Invitation) => {
|
|
66
|
+
switch (hostInvitation.state) {
|
|
67
|
+
case Invitation.State.CONNECTING: {
|
|
68
|
+
if (hooks?.host?.onConnecting?.(hostObservable)) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
const guestObservable = acceptInvitation(guest, hostInvitation, guestDeviceProfile);
|
|
72
|
+
guestObservable.subscribe(
|
|
73
|
+
async (guestInvitation: Invitation) => {
|
|
74
|
+
switch (guestInvitation.state) {
|
|
75
|
+
case Invitation.State.CONNECTING: {
|
|
76
|
+
if (hooks?.guest?.onConnecting?.(guestObservable)) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
invariant(hostInvitation.swarmKey!.equals(guestInvitation.swarmKey!));
|
|
77
80
|
break;
|
|
78
81
|
}
|
|
79
|
-
invariant(hostInvitation.swarmKey!.equals(guestInvitation.swarmKey!));
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
case Invitation.State.CONNECTED: {
|
|
84
|
+
hooks?.guest?.onConnected?.(guestObservable);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
case Invitation.State.READY_FOR_AUTHENTICATION: {
|
|
89
|
+
if (hooks?.guest?.onReady?.(guestObservable)) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
await guestObservable.authenticate(await authCode.wait());
|
|
90
93
|
break;
|
|
91
94
|
}
|
|
92
|
-
await guestObservable.authenticate(await authCode.wait());
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
case Invitation.State.AUTHENTICATING: {
|
|
97
|
+
hooks?.guest?.onAuthenticating?.(guestObservable);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
case Invitation.State.SUCCESS: {
|
|
102
|
+
if (hooks?.guest?.onSuccess?.(guestObservable)) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
guestComplete.wake({ invitation: guestInvitation });
|
|
103
106
|
break;
|
|
104
107
|
}
|
|
105
|
-
guestComplete.wake({ invitation: guestInvitation });
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
case Invitation.State.CANCELLED: {
|
|
110
|
+
if (hooks?.guest?.onCancelled?.(guestObservable)) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
guestComplete.wake({ invitation: guestInvitation });
|
|
111
114
|
break;
|
|
112
115
|
}
|
|
113
|
-
guestComplete.wake({ invitation: guestInvitation });
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
case Invitation.State.TIMEOUT: {
|
|
118
|
+
if (hooks?.guest?.onTimeout?.(guestObservable)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
guestComplete.wake({ invitation: guestInvitation });
|
|
120
122
|
}
|
|
121
|
-
guestComplete.wake({ invitation: guestInvitation });
|
|
122
123
|
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
case Invitation.State.CONNECTED: {
|
|
136
|
-
hooks?.host?.onConnected?.(hostObservable);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
124
|
+
},
|
|
125
|
+
(error: Error) => {
|
|
126
|
+
if (hooks?.guest?.onError?.(guestObservable)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
guestComplete.wake({ error });
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
139
134
|
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
case Invitation.State.CONNECTED: {
|
|
136
|
+
hooks?.host?.onConnected?.(hostObservable);
|
|
142
137
|
break;
|
|
143
138
|
}
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
|
|
140
|
+
case Invitation.State.READY_FOR_AUTHENTICATION: {
|
|
141
|
+
if (hooks?.host?.onReady?.(hostObservable)) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (hostInvitation.authCode) {
|
|
145
|
+
authCode.wake(hostInvitation.authCode);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
146
148
|
}
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
case Invitation.State.AUTHENTICATING: {
|
|
151
|
+
hooks?.host?.onAuthenticating?.(hostObservable);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
case Invitation.State.SUCCESS: {
|
|
156
|
+
if (hooks?.host?.onSuccess?.(hostObservable)) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
hostComplete.wake({ invitation: hostInvitation });
|
|
157
160
|
break;
|
|
158
161
|
}
|
|
159
|
-
hostComplete.wake({ invitation: hostInvitation });
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
case Invitation.State.CANCELLED: {
|
|
164
|
+
if (hooks?.host?.onCancelled?.(hostObservable)) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
hostComplete.wake({ invitation: hostInvitation });
|
|
165
168
|
break;
|
|
166
169
|
}
|
|
167
|
-
hostComplete.wake({ invitation: hostInvitation });
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
case Invitation.State.TIMEOUT: {
|
|
172
|
+
if (hooks?.host?.onTimeout?.(hostObservable)) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
hostComplete.wake({ invitation: hostInvitation });
|
|
173
176
|
break;
|
|
174
177
|
}
|
|
175
|
-
hostComplete.wake({ invitation: hostInvitation });
|
|
176
|
-
break;
|
|
177
178
|
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
);
|
|
179
|
+
},
|
|
180
|
+
(error: Error) => {
|
|
181
|
+
if (hooks?.host?.onError?.(hostObservable)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
hostComplete.wake({ error });
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
});
|
|
187
188
|
|
|
188
189
|
return [hostComplete.wait(), guestComplete.wait()];
|
|
189
190
|
};
|
|
190
191
|
|
|
191
|
-
const createInvitation = (
|
|
192
|
+
const createInvitation = async (
|
|
192
193
|
host: ServiceContext | InvitationHost,
|
|
193
194
|
options?: Partial<Invitation>,
|
|
194
|
-
): CancellableInvitation => {
|
|
195
|
+
): Promise<CancellableInvitation> => {
|
|
195
196
|
options ??= {
|
|
196
197
|
authMethod: Invitation.AuthMethod.NONE,
|
|
197
198
|
...(options ?? {}),
|
|
198
199
|
};
|
|
199
200
|
|
|
200
201
|
if (host instanceof ServiceContext) {
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
return host.invitationsManager.createInvitation({
|
|
203
|
+
kind: Invitation.Kind.SPACE,
|
|
204
|
+
...options,
|
|
205
|
+
});
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
return host.share(options);
|