@dxos/client-services 0.4.10-main.4c7b3fa → 0.4.10-main.4d26ea7
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-UWSCLXQ5.mjs → chunk-X462P3GQ.mjs} +425 -413
- package/dist/lib/browser/chunk-X462P3GQ.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +6 -4
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +131 -116
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-G6YGTBEV.cjs → chunk-LE3INNLG.cjs} +527 -511
- package/dist/lib/node/chunk-LE3INNLG.cjs.map +7 -0
- package/dist/lib/node/index.cjs +48 -46
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +130 -118
- 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/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 +4 -6
- 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 +8 -3
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +4 -3
- package/dist/types/src/packlets/spaces/data-space.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 +7 -3
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/worker-runtime.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/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/automerge-host.test.ts +1 -1
- package/src/packlets/services/service-context.test.ts +3 -3
- package/src/packlets/services/service-context.ts +12 -25
- package/src/packlets/services/service-host.test.ts +6 -0
- package/src/packlets/services/service-host.ts +5 -16
- package/src/packlets/spaces/data-space-manager.test.ts +4 -4
- package/src/packlets/spaces/data-space-manager.ts +56 -13
- package/src/packlets/spaces/data-space.ts +14 -19
- package/src/packlets/testing/invitation-utils.ts +100 -97
- package/src/packlets/testing/test-builder.ts +27 -14
- package/src/packlets/vault/worker-runtime.ts +3 -1
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-UWSCLXQ5.mjs.map +0 -7
- package/dist/lib/node/chunk-G6YGTBEV.cjs.map +0 -7
- package/dist/types/src/packlets/indexing/index.d.ts +0 -2
- package/dist/types/src/packlets/indexing/index.d.ts.map +0 -1
- package/dist/types/src/packlets/indexing/util.d.ts +0 -16
- package/dist/types/src/packlets/indexing/util.d.ts.map +0 -1
- package/src/packlets/indexing/index.ts +0 -5
- package/src/packlets/indexing/util.ts +0 -94
|
@@ -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.');
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import { expect } from 'chai';
|
|
6
6
|
|
|
7
7
|
import { asyncTimeout, sleep } from '@dxos/async';
|
|
8
|
+
import { AutomergeContext } from '@dxos/echo-db';
|
|
8
9
|
import { AutomergeHost, DataServiceImpl } from '@dxos/echo-pipeline';
|
|
9
10
|
import { createTestLevel } from '@dxos/echo-pipeline/testing';
|
|
10
|
-
import { AutomergeContext } from '@dxos/echo-schema';
|
|
11
11
|
import { afterTest, describe, test } from '@dxos/test';
|
|
12
12
|
|
|
13
13
|
describe('AutomergeHost', () => {
|
|
@@ -27,15 +27,15 @@ describe('services/ServiceContext', () => {
|
|
|
27
27
|
test('joined space is synchronized on device invitations', async () => {
|
|
28
28
|
const networkContext = new MemorySignalManagerContext();
|
|
29
29
|
const device1 = await createServiceContext({ signalContext: networkContext });
|
|
30
|
-
await openAndClose(device1.
|
|
30
|
+
await openAndClose(device1.echoHost);
|
|
31
31
|
await device1.createIdentity();
|
|
32
32
|
|
|
33
33
|
const device2 = await createServiceContext({ signalContext: networkContext });
|
|
34
|
-
await openAndClose(device2.
|
|
34
|
+
await openAndClose(device2.echoHost);
|
|
35
35
|
await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
|
|
36
36
|
|
|
37
37
|
const identity2 = await createServiceContext({ signalContext: networkContext });
|
|
38
|
-
await openAndClose(identity2.
|
|
38
|
+
await openAndClose(identity2.echoHost);
|
|
39
39
|
await identity2.createIdentity();
|
|
40
40
|
const space1 = await identity2.dataSpaceManager!.createSpace();
|
|
41
41
|
await Promise.all(
|
|
@@ -8,9 +8,9 @@ import { Trigger } from '@dxos/async';
|
|
|
8
8
|
import { Context, Resource } from '@dxos/context';
|
|
9
9
|
import { getCredentialAssertion, type CredentialProcessor } from '@dxos/credentials';
|
|
10
10
|
import { failUndefined } from '@dxos/debug';
|
|
11
|
-
import {
|
|
11
|
+
import { EchoHost } from '@dxos/echo-db';
|
|
12
|
+
import { MetadataStore, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-pipeline';
|
|
12
13
|
import { FeedFactory, FeedStore } from '@dxos/feed-store';
|
|
13
|
-
import { IndexMetadataStore, IndexStore, Indexer, createStorageCallbacks } from '@dxos/indexing';
|
|
14
14
|
import { invariant } from '@dxos/invariant';
|
|
15
15
|
import { Keyring } from '@dxos/keyring';
|
|
16
16
|
import { PublicKey } from '@dxos/keys';
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
type IdentityManagerRuntimeParams,
|
|
33
33
|
type JoinIdentityParams,
|
|
34
34
|
} from '../identity';
|
|
35
|
-
import { createDocumentsIterator, createSelectedDocumentsIterator } from '../indexing';
|
|
36
35
|
import {
|
|
37
36
|
DeviceInvitationProtocol,
|
|
38
37
|
InvitationsHandler,
|
|
@@ -64,9 +63,7 @@ export class ServiceContext extends Resource {
|
|
|
64
63
|
public readonly identityManager: IdentityManager;
|
|
65
64
|
public readonly invitations: InvitationsHandler;
|
|
66
65
|
public readonly invitationsManager: InvitationsManager;
|
|
67
|
-
public readonly
|
|
68
|
-
public readonly indexMetadata: IndexMetadataStore;
|
|
69
|
-
public readonly indexer: Indexer;
|
|
66
|
+
public readonly echoHost: EchoHost;
|
|
70
67
|
|
|
71
68
|
// Initialized after identity is initialized.
|
|
72
69
|
public dataSpaceManager?: DataSpaceManager;
|
|
@@ -122,19 +119,9 @@ export class ServiceContext extends Resource {
|
|
|
122
119
|
this._runtimeParams as IdentityManagerRuntimeParams,
|
|
123
120
|
);
|
|
124
121
|
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
directory: storage.createDirectory('automerge'),
|
|
129
|
-
db: level.sublevel('automerge'),
|
|
130
|
-
storageCallbacks: createStorageCallbacks({ host: () => this.automergeHost, metadata: this.indexMetadata }),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
this.indexer = new Indexer({
|
|
134
|
-
indexStore: new IndexStore({ db: level.sublevel('index-storage') }),
|
|
135
|
-
metadataStore: this.indexMetadata,
|
|
136
|
-
loadDocuments: createSelectedDocumentsIterator(this.automergeHost),
|
|
137
|
-
getAllDocuments: createDocumentsIterator(this.automergeHost),
|
|
122
|
+
this.echoHost = new EchoHost({
|
|
123
|
+
kv: this.level,
|
|
124
|
+
storage: this.storage,
|
|
138
125
|
});
|
|
139
126
|
|
|
140
127
|
this.invitations = new InvitationsHandler(this.networkManager);
|
|
@@ -166,7 +153,7 @@ export class ServiceContext extends Resource {
|
|
|
166
153
|
await this.signalManager.open();
|
|
167
154
|
await this.networkManager.open();
|
|
168
155
|
|
|
169
|
-
await this.
|
|
156
|
+
await this.echoHost.open(ctx);
|
|
170
157
|
await this.metadataStore.load();
|
|
171
158
|
await this.spaceManager.open();
|
|
172
159
|
await this.identityManager.open(ctx);
|
|
@@ -181,20 +168,19 @@ export class ServiceContext extends Resource {
|
|
|
181
168
|
log('opened');
|
|
182
169
|
}
|
|
183
170
|
|
|
184
|
-
protected override async _close() {
|
|
171
|
+
protected override async _close(ctx: Context) {
|
|
185
172
|
log('closing...');
|
|
186
173
|
if (this._deviceSpaceSync && this.identityManager.identity) {
|
|
187
174
|
await this.identityManager.identity.space.spaceState.removeCredentialProcessor(this._deviceSpaceSync);
|
|
188
175
|
}
|
|
189
|
-
await this.automergeHost.close();
|
|
190
176
|
await this.dataSpaceManager?.close();
|
|
191
177
|
await this.identityManager.close();
|
|
192
178
|
await this.spaceManager.close();
|
|
193
179
|
await this.feedStore.close();
|
|
180
|
+
await this.metadataStore.close();
|
|
181
|
+
await this.echoHost.close(ctx);
|
|
194
182
|
await this.networkManager.close();
|
|
195
183
|
await this.signalManager.close();
|
|
196
|
-
await this.metadataStore.close();
|
|
197
|
-
await this.indexer.destroy();
|
|
198
184
|
log('closed');
|
|
199
185
|
}
|
|
200
186
|
|
|
@@ -255,7 +241,8 @@ export class ServiceContext extends Resource {
|
|
|
255
241
|
this.keyring,
|
|
256
242
|
signingContext,
|
|
257
243
|
this.feedStore,
|
|
258
|
-
this.
|
|
244
|
+
this.echoHost,
|
|
245
|
+
this.invitationsManager,
|
|
259
246
|
this._runtimeParams as DataSpaceManagerRuntimeParams,
|
|
260
247
|
);
|
|
261
248
|
await this.dataSpaceManager.open();
|
|
@@ -29,6 +29,12 @@ describe('ClientServicesHost', () => {
|
|
|
29
29
|
isNode() && rmSync(dataRoot, { recursive: true, force: true });
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
test('open and close', async () => {
|
|
33
|
+
const host = createServiceHost(new Config(), new MemorySignalManagerContext());
|
|
34
|
+
await host.open(new Context());
|
|
35
|
+
await host.close();
|
|
36
|
+
});
|
|
37
|
+
|
|
32
38
|
test('queryCredentials', async () => {
|
|
33
39
|
const host = createServiceHost(new Config(), new MemorySignalManagerContext());
|
|
34
40
|
await host.open(new Context());
|
|
@@ -8,15 +8,8 @@ import { Event, synchronized } from '@dxos/async';
|
|
|
8
8
|
import { clientServiceBundle, defaultKey, type ClientServices, Properties } from '@dxos/client-protocol';
|
|
9
9
|
import { type Config } from '@dxos/config';
|
|
10
10
|
import { Context } from '@dxos/context';
|
|
11
|
-
import {
|
|
12
|
-
DataServiceImpl,
|
|
13
|
-
type ObjectStructure,
|
|
14
|
-
encodeReference,
|
|
15
|
-
type SpaceDoc,
|
|
16
|
-
type LevelDB,
|
|
17
|
-
} from '@dxos/echo-pipeline';
|
|
11
|
+
import { type ObjectStructure, encodeReference, type SpaceDoc, type LevelDB } from '@dxos/echo-pipeline';
|
|
18
12
|
import { getTypeReference } from '@dxos/echo-schema';
|
|
19
|
-
import { IndexServiceImpl } from '@dxos/indexing';
|
|
20
13
|
import { invariant } from '@dxos/invariant';
|
|
21
14
|
import { PublicKey } from '@dxos/keys';
|
|
22
15
|
import { log } from '@dxos/log';
|
|
@@ -285,12 +278,8 @@ export class ClientServicesHost {
|
|
|
285
278
|
},
|
|
286
279
|
),
|
|
287
280
|
|
|
288
|
-
DataService:
|
|
289
|
-
|
|
290
|
-
IndexService: new IndexServiceImpl({
|
|
291
|
-
indexer: this._serviceContext.indexer,
|
|
292
|
-
automergeHost: this._serviceContext.automergeHost,
|
|
293
|
-
}),
|
|
281
|
+
DataService: this._serviceContext.echoHost.dataService,
|
|
282
|
+
QueryService: this._serviceContext.echoHost.queryService,
|
|
294
283
|
|
|
295
284
|
NetworkService: new NetworkServiceImpl(this._serviceContext.networkManager, this._serviceContext.signalManager),
|
|
296
285
|
|
|
@@ -368,7 +357,7 @@ export class ClientServicesHost {
|
|
|
368
357
|
|
|
369
358
|
const automergeIndex = space.automergeSpaceState.rootUrl;
|
|
370
359
|
invariant(automergeIndex);
|
|
371
|
-
const document = await this._serviceContext.
|
|
360
|
+
const document = await this._serviceContext.echoHost.automergeRepo.find<SpaceDoc>(automergeIndex as any);
|
|
372
361
|
await document.whenReady();
|
|
373
362
|
|
|
374
363
|
// TODO(dmaretskyi): Better API for low-level data access.
|
|
@@ -388,7 +377,7 @@ export class ClientServicesHost {
|
|
|
388
377
|
assignDeep(doc, ['objects', propertiesId], properties);
|
|
389
378
|
});
|
|
390
379
|
|
|
391
|
-
await this._serviceContext.
|
|
380
|
+
await this._serviceContext.echoHost.flush();
|
|
392
381
|
|
|
393
382
|
return identity;
|
|
394
383
|
}
|
|
@@ -20,7 +20,7 @@ describe('DataSpaceManager', () => {
|
|
|
20
20
|
|
|
21
21
|
const peer = builder.createPeer();
|
|
22
22
|
await peer.createIdentity();
|
|
23
|
-
await openAndClose(peer.
|
|
23
|
+
await openAndClose(peer.echoHost, peer.dataSpaceManager);
|
|
24
24
|
|
|
25
25
|
const space = await peer.dataSpaceManager.createSpace();
|
|
26
26
|
|
|
@@ -42,7 +42,7 @@ describe('DataSpaceManager', () => {
|
|
|
42
42
|
const peer2 = builder.createPeer();
|
|
43
43
|
await peer2.createIdentity();
|
|
44
44
|
|
|
45
|
-
await openAndClose(peer1.
|
|
45
|
+
await openAndClose(peer1.echoHost, peer1.dataSpaceManager, peer2.echoHost, peer2.dataSpaceManager);
|
|
46
46
|
|
|
47
47
|
const space1 = await peer1.dataSpaceManager.createSpace();
|
|
48
48
|
await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
|
|
@@ -111,7 +111,7 @@ describe('DataSpaceManager', () => {
|
|
|
111
111
|
await peer2.createIdentity();
|
|
112
112
|
await peer2.dataSpaceManager.open();
|
|
113
113
|
|
|
114
|
-
await openAndClose(peer1.
|
|
114
|
+
await openAndClose(peer1.echoHost, peer1.dataSpaceManager, peer2.echoHost, peer2.dataSpaceManager);
|
|
115
115
|
|
|
116
116
|
const space1 = await peer1.dataSpaceManager.createSpace();
|
|
117
117
|
await space1.inner.controlPipeline.state.waitUntilTimeframe(space1.inner.controlPipeline.state.endTimeframe);
|
|
@@ -153,7 +153,7 @@ describe('DataSpaceManager', () => {
|
|
|
153
153
|
|
|
154
154
|
const peer = builder.createPeer();
|
|
155
155
|
await peer.createIdentity();
|
|
156
|
-
await openAndClose(peer.
|
|
156
|
+
await openAndClose(peer.echoHost, peer.dataSpaceManager);
|
|
157
157
|
|
|
158
158
|
const space = await peer.dataSpaceManager.createSpace();
|
|
159
159
|
await space.inner.controlPipeline.state.waitUntilTimeframe(space.inner.controlPipeline.state.endTimeframe);
|
|
@@ -4,18 +4,20 @@
|
|
|
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';
|
|
8
|
-
import { type
|
|
7
|
+
import { getCredentialAssertion, type CredentialSigner, type DelegateInvitationCredential } from '@dxos/credentials';
|
|
8
|
+
import { type EchoHost } from '@dxos/echo-db';
|
|
9
|
+
import { type MetadataStore, type Space, type SpaceManager } from '@dxos/echo-pipeline';
|
|
9
10
|
import { type FeedStore } from '@dxos/feed-store';
|
|
10
11
|
import { invariant } from '@dxos/invariant';
|
|
11
12
|
import { type Keyring } from '@dxos/keyring';
|
|
12
13
|
import { PublicKey } from '@dxos/keys';
|
|
13
14
|
import { log } from '@dxos/log';
|
|
14
15
|
import { trace } from '@dxos/protocols';
|
|
15
|
-
import { SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
16
|
+
import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
16
17
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
17
18
|
import { type SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
18
19
|
import { type Credential, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
20
|
+
import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
19
21
|
import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
|
|
20
22
|
import { type Timeframe } from '@dxos/timeframe';
|
|
21
23
|
import { ComplexMap, deferFunction, forEachAsync } from '@dxos/util';
|
|
@@ -23,6 +25,7 @@ import { ComplexMap, deferFunction, forEachAsync } from '@dxos/util';
|
|
|
23
25
|
import { DataSpace } from './data-space';
|
|
24
26
|
import { spaceGenesis } from './genesis';
|
|
25
27
|
import { createAuthProvider } from '../identity';
|
|
28
|
+
import { type InvitationsManager } from '../invitations';
|
|
26
29
|
|
|
27
30
|
const PRESENCE_ANNOUNCE_INTERVAL = 10_000;
|
|
28
31
|
const PRESENCE_OFFLINE_TIMEOUT = 20_000;
|
|
@@ -77,7 +80,8 @@ export class DataSpaceManager {
|
|
|
77
80
|
private readonly _keyring: Keyring,
|
|
78
81
|
private readonly _signingContext: SigningContext,
|
|
79
82
|
private readonly _feedStore: FeedStore<FeedMessage>,
|
|
80
|
-
private readonly
|
|
83
|
+
private readonly _echoHost: EchoHost,
|
|
84
|
+
private readonly _invitationsManager: InvitationsManager,
|
|
81
85
|
params?: DataSpaceManagerRuntimeParams,
|
|
82
86
|
) {
|
|
83
87
|
const {
|
|
@@ -149,14 +153,10 @@ export class DataSpaceManager {
|
|
|
149
153
|
|
|
150
154
|
log('creating space...', { spaceKey });
|
|
151
155
|
|
|
152
|
-
const
|
|
153
|
-
automergeRoot.change((doc: any) => {
|
|
154
|
-
doc.access = { spaceKey: spaceKey.toHex() };
|
|
155
|
-
});
|
|
156
|
-
|
|
156
|
+
const automergeRootUrl = await this._echoHost.createSpaceRoot(spaceKey);
|
|
157
157
|
const space = await this._constructSpace(metadata);
|
|
158
158
|
|
|
159
|
-
const credentials = await spaceGenesis(this._keyring, this._signingContext, space.inner,
|
|
159
|
+
const credentials = await spaceGenesis(this._keyring, this._signingContext, space.inner, automergeRootUrl);
|
|
160
160
|
await this._metadataStore.addSpace(metadata);
|
|
161
161
|
|
|
162
162
|
const memberCredential = credentials[1];
|
|
@@ -240,13 +240,16 @@ export class DataSpaceManager {
|
|
|
240
240
|
gossip.createExtension({ remotePeerId: session.remotePeerId }),
|
|
241
241
|
);
|
|
242
242
|
session.addExtension('dxos.mesh.teleport.notarization', dataSpace.notarizationPlugin.createExtension());
|
|
243
|
-
this.
|
|
244
|
-
session.addExtension('dxos.mesh.teleport.automerge', this.
|
|
243
|
+
this._echoHost.authorizeDevice(space.key, session.remotePeerId);
|
|
244
|
+
session.addExtension('dxos.mesh.teleport.automerge', this._echoHost.createReplicationExtension());
|
|
245
245
|
},
|
|
246
246
|
onAuthFailure: () => {
|
|
247
247
|
log.warn('auth failure');
|
|
248
248
|
},
|
|
249
249
|
memberKey: this._signingContext.identityKey,
|
|
250
|
+
onDelegatedInvitationStatusChange: (invitation, isActive) => {
|
|
251
|
+
return this._handleInvitationStatusChange(dataSpace, invitation, isActive);
|
|
252
|
+
},
|
|
250
253
|
});
|
|
251
254
|
controlFeed && (await space.setControlFeed(controlFeed));
|
|
252
255
|
dataFeed && (await space.setDataFeed(dataFeed));
|
|
@@ -259,6 +262,7 @@ export class DataSpaceManager {
|
|
|
259
262
|
presence,
|
|
260
263
|
keyring: this._keyring,
|
|
261
264
|
feedStore: this._feedStore,
|
|
265
|
+
echoHost: this._echoHost,
|
|
262
266
|
signingContext: this._signingContext,
|
|
263
267
|
callbacks: {
|
|
264
268
|
beforeReady: async () => {
|
|
@@ -267,6 +271,7 @@ export class DataSpaceManager {
|
|
|
267
271
|
afterReady: async () => {
|
|
268
272
|
log('after space ready', { space: space.key, open: this._isOpen });
|
|
269
273
|
if (this._isOpen) {
|
|
274
|
+
await this._createDelegatedInvitations(dataSpace, [...space.spaceState.invitations.entries()]);
|
|
270
275
|
this.updated.emit();
|
|
271
276
|
}
|
|
272
277
|
},
|
|
@@ -275,7 +280,6 @@ export class DataSpaceManager {
|
|
|
275
280
|
},
|
|
276
281
|
},
|
|
277
282
|
cache: metadata.cache,
|
|
278
|
-
automergeHost: this._automergeHost,
|
|
279
283
|
});
|
|
280
284
|
|
|
281
285
|
if (metadata.state !== SpaceState.INACTIVE) {
|
|
@@ -289,4 +293,43 @@ export class DataSpaceManager {
|
|
|
289
293
|
this._spaces.set(metadata.key, dataSpace);
|
|
290
294
|
return dataSpace;
|
|
291
295
|
}
|
|
296
|
+
|
|
297
|
+
private async _handleInvitationStatusChange(
|
|
298
|
+
dataSpace: DataSpace | undefined,
|
|
299
|
+
delegatedInvitation: DelegateInvitationCredential,
|
|
300
|
+
isActive: boolean,
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
if (dataSpace?.state !== SpaceState.READY) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (isActive) {
|
|
306
|
+
await this._createDelegatedInvitations(dataSpace, [
|
|
307
|
+
[delegatedInvitation.credentialId, delegatedInvitation.invitation],
|
|
308
|
+
]);
|
|
309
|
+
} else {
|
|
310
|
+
await this._invitationsManager.cancelInvitation(delegatedInvitation.invitation);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async _createDelegatedInvitations(
|
|
315
|
+
space: DataSpace,
|
|
316
|
+
invitations: Array<[PublicKey, DelegateSpaceInvitation]>,
|
|
317
|
+
): Promise<void> {
|
|
318
|
+
const tasks = invitations.map(([credentialId, invitation]) => {
|
|
319
|
+
return this._invitationsManager.createInvitation({
|
|
320
|
+
type: Invitation.Type.DELEGATED,
|
|
321
|
+
kind: Invitation.Kind.SPACE,
|
|
322
|
+
spaceKey: space.key,
|
|
323
|
+
authMethod: invitation.authMethod,
|
|
324
|
+
invitationId: invitation.invitationId,
|
|
325
|
+
swarmKey: invitation.swarmKey,
|
|
326
|
+
guestKeypair: invitation.guestKey ? { publicKey: invitation.guestKey } : undefined,
|
|
327
|
+
lifetime: invitation.expiresOn ? invitation.expiresOn.getTime() - Date.now() : undefined,
|
|
328
|
+
multiUse: invitation.multiUse,
|
|
329
|
+
delegationCredentialId: credentialId,
|
|
330
|
+
persistent: false,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
await Promise.all(tasks);
|
|
334
|
+
}
|
|
292
335
|
}
|