@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.
Files changed (67) hide show
  1. package/dist/lib/browser/{chunk-UWSCLXQ5.mjs → chunk-X462P3GQ.mjs} +425 -413
  2. package/dist/lib/browser/chunk-X462P3GQ.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +6 -4
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +131 -116
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-G6YGTBEV.cjs → chunk-LE3INNLG.cjs} +527 -511
  9. package/dist/lib/node/chunk-LE3INNLG.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +48 -46
  11. package/dist/lib/node/index.cjs.map +3 -3
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/packlets/testing/index.cjs +130 -118
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  16. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +3 -1
  17. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  18. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +6 -1
  19. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  20. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +4 -2
  21. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  22. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +9 -7
  23. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -1
  24. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  25. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -1
  26. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  27. package/dist/types/src/packlets/services/service-context.d.ts +4 -6
  28. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  29. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  30. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +8 -3
  31. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  32. package/dist/types/src/packlets/spaces/data-space.d.ts +4 -3
  33. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  34. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  35. package/dist/types/src/packlets/testing/test-builder.d.ts +7 -3
  36. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  37. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  38. package/dist/types/src/version.d.ts +1 -1
  39. package/package.json +34 -34
  40. package/src/packlets/identity/identity-manager.ts +1 -0
  41. package/src/packlets/identity/identity.test.ts +3 -0
  42. package/src/packlets/invitations/device-invitation-protocol.ts +6 -1
  43. package/src/packlets/invitations/invitation-protocol.ts +7 -1
  44. package/src/packlets/invitations/invitations-handler.ts +10 -71
  45. package/src/packlets/invitations/invitations-manager.ts +114 -40
  46. package/src/packlets/invitations/invitations-service.ts +4 -2
  47. package/src/packlets/invitations/space-invitation-protocol.ts +45 -3
  48. package/src/packlets/services/automerge-host.test.ts +1 -1
  49. package/src/packlets/services/service-context.test.ts +3 -3
  50. package/src/packlets/services/service-context.ts +12 -25
  51. package/src/packlets/services/service-host.test.ts +6 -0
  52. package/src/packlets/services/service-host.ts +5 -16
  53. package/src/packlets/spaces/data-space-manager.test.ts +4 -4
  54. package/src/packlets/spaces/data-space-manager.ts +56 -13
  55. package/src/packlets/spaces/data-space.ts +14 -19
  56. package/src/packlets/testing/invitation-utils.ts +100 -97
  57. package/src/packlets/testing/test-builder.ts +27 -14
  58. package/src/packlets/vault/worker-runtime.ts +3 -1
  59. package/src/version.ts +1 -1
  60. package/dist/lib/browser/chunk-UWSCLXQ5.mjs.map +0 -7
  61. package/dist/lib/node/chunk-G6YGTBEV.cjs.map +0 -7
  62. package/dist/types/src/packlets/indexing/index.d.ts +0 -2
  63. package/dist/types/src/packlets/indexing/index.d.ts.map +0 -1
  64. package/dist/types/src/packlets/indexing/util.d.ts +0 -16
  65. package/dist/types/src/packlets/indexing/util.d.ts.map +0 -1
  66. package/src/packlets/indexing/index.ts +0 -5
  67. 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 type { AuthenticatingInvitation, CancellableInvitation } from '@dxos/client-protocol';
7
- import { type Context } from '@dxos/context';
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 { InvitationsHandler } from './invitations-handler';
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
- const existingInvitation = this._createInvitations.get(options.invitationId);
45
- if (existingInvitation) {
46
- return existingInvitation;
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._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();
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(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
+ 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
- return invitation;
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 cInvitations = freshInvitations.map((persistentInvitation) => {
96
+ const loadTasks = freshInvitations.map((persistentInvitation) => {
82
97
  invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
83
- return this.createInvitation({ ...persistentInvitation, persistent: false }).get();
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 _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
- });
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
- invitation.subscribe(next, close, close);
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 { createAdmissionCredentials, getCredentialAssertion } from '@dxos/credentials';
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(request: AdmissionRequest, guestProfile?: ProfileDocument | undefined): Promise<AdmissionResponse> {
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.automergeHost);
30
+ await openAndClose(device1.echoHost);
31
31
  await device1.createIdentity();
32
32
 
33
33
  const device2 = await createServiceContext({ signalContext: networkContext });
34
- await openAndClose(device2.automergeHost);
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.automergeHost);
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 { AutomergeHost, MetadataStore, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-pipeline';
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 automergeHost: AutomergeHost;
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.indexMetadata = new IndexMetadataStore({ db: level.sublevel('index-metadata') });
126
-
127
- this.automergeHost = new AutomergeHost({
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.automergeHost.open();
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.automergeHost,
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: new DataServiceImpl(this._serviceContext.automergeHost),
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.automergeHost.repo.find<SpaceDoc>(automergeIndex as any);
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.automergeHost.repo.flush();
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.automergeHost, peer.dataSpaceManager);
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.automergeHost, peer1.dataSpaceManager, peer2.automergeHost, peer2.dataSpaceManager);
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.automergeHost, peer1.dataSpaceManager, peer2.automergeHost, peer2.dataSpaceManager);
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.automergeHost, peer.dataSpaceManager);
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 AutomergeHost, type MetadataStore, type Space, type SpaceManager } from '@dxos/echo-pipeline';
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 _automergeHost: AutomergeHost,
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 automergeRoot = this._automergeHost.repo.create();
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, automergeRoot.url);
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._automergeHost.authorizeDevice(space.key, session.remotePeerId);
244
- session.addExtension('dxos.mesh.teleport.automerge', this._automergeHost.createExtension());
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
  }