@dxos/client-services 0.1.18 → 0.1.19

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 (46) hide show
  1. package/dist/lib/browser/index.mjs +460 -314
  2. package/dist/lib/browser/index.mjs.map +7 -0
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +498 -352
  5. package/dist/lib/node/index.cjs.map +7 -0
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/packlets/deprecated/space.d.ts.map +1 -1
  8. package/dist/types/src/packlets/identity/authenticator.d.ts +21 -1
  9. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  10. package/dist/types/src/packlets/identity/identity-manager.d.ts +3 -9
  11. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  12. package/dist/types/src/packlets/identity/identity.d.ts +5 -4
  13. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  14. package/dist/types/src/packlets/invitations/halo-invitations-handler.d.ts.map +1 -1
  15. package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts +6 -4
  16. package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
  17. package/dist/types/src/packlets/invitations/space-invitations-service.d.ts +5 -4
  18. package/dist/types/src/packlets/invitations/space-invitations-service.d.ts.map +1 -1
  19. package/dist/types/src/packlets/services/service-context.d.ts +3 -1
  20. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  21. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +28 -0
  22. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -0
  23. package/dist/types/src/packlets/spaces/data-space.d.ts +23 -0
  24. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -0
  25. package/package.json +28 -27
  26. package/src/packlets/deprecated/space.ts +16 -8
  27. package/src/packlets/devtools/spaces.ts +1 -1
  28. package/src/packlets/identity/authenticator.test.ts +8 -3
  29. package/src/packlets/identity/authenticator.ts +68 -24
  30. package/src/packlets/identity/identity-manager.test.ts +11 -9
  31. package/src/packlets/identity/identity-manager.ts +33 -46
  32. package/src/packlets/identity/identity.test.ts +40 -17
  33. package/src/packlets/identity/identity.ts +26 -16
  34. package/src/packlets/invitations/halo-invitations-handler.ts +1 -2
  35. package/src/packlets/invitations/space-invitations-handler.test.ts +7 -7
  36. package/src/packlets/invitations/space-invitations-handler.ts +9 -8
  37. package/src/packlets/invitations/space-invitations-service.test.ts +10 -10
  38. package/src/packlets/invitations/space-invitations-service.ts +6 -5
  39. package/src/packlets/services/service-context.ts +21 -21
  40. package/src/packlets/services/service-host.ts +1 -1
  41. package/src/packlets/services/service-registry.test.ts +3 -3
  42. package/src/packlets/spaces/data-space-manager.ts +118 -0
  43. package/src/packlets/spaces/data-space.ts +66 -0
  44. package/dist/types/src/packlets/invitations/rpc-extension.d.ts +0 -17
  45. package/dist/types/src/packlets/invitations/rpc-extension.d.ts.map +0 -1
  46. package/src/packlets/invitations/rpc-extension.ts +0 -56
@@ -5,13 +5,14 @@
5
5
  import expect from 'expect';
6
6
  import assert from 'node:assert';
7
7
 
8
+ import { Event } from '@dxos/async';
8
9
  import { createCredentialSignerWithKey } from '@dxos/credentials';
9
10
  import { Keyring } from '@dxos/keyring';
10
11
  import { PublicKey } from '@dxos/keys';
11
12
  import { describe, test } from '@dxos/test';
12
13
  import { ComplexSet } from '@dxos/util';
13
14
 
14
- import { createHaloAuthProvider, createHaloAuthVerifier } from './authenticator';
15
+ import { createHaloAuthProvider, HaloAuthVerifier } from './authenticator';
15
16
 
16
17
  describe('identity/authenticator', () => {
17
18
  test('verifies credentials', async () => {
@@ -19,11 +20,15 @@ describe('identity/authenticator', () => {
19
20
  const deviceKey = await keyring.createKey();
20
21
  const signer = createCredentialSignerWithKey(keyring, deviceKey);
21
22
  const authProvider = createHaloAuthProvider(signer);
22
- const authVerifier = createHaloAuthVerifier(() => new ComplexSet(PublicKey.hash, [deviceKey]));
23
+ const authVerifier = new HaloAuthVerifier({
24
+ trustedDevicesProvider: () => new ComplexSet(PublicKey.hash, [deviceKey]),
25
+ update: new Event(),
26
+ authTimeout: 10
27
+ });
23
28
 
24
29
  const nonce = new Uint8Array([2, 1, 3, 7]);
25
30
  const credential = await authProvider(nonce);
26
31
  assert(credential);
27
- expect(await authVerifier(nonce, credential)).toBeTruthy();
32
+ expect(await authVerifier.verifier(nonce, credential)).toBeTruthy();
28
33
  }).onlyEnvironments('nodejs');
29
34
  });
@@ -2,6 +2,8 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import { Event, Trigger } from '@dxos/async';
6
+ import { Context } from '@dxos/context';
5
7
  import { verifyCredential, CredentialSigner } from '@dxos/credentials';
6
8
  import { AuthProvider, AuthVerifier } from '@dxos/echo-db';
7
9
  import { PublicKey } from '@dxos/keys';
@@ -23,27 +25,69 @@ export const createHaloAuthProvider =
23
25
  return schema.getCodecForType('dxos.halo.credentials.Credential').encode(credential);
24
26
  };
25
27
 
26
- export const createHaloAuthVerifier =
27
- (getDeviceSet: () => ComplexSet<PublicKey>): AuthVerifier =>
28
- async (nonce, auth) => {
29
- const credential = schema.getCodecForType('dxos.halo.credentials.Credential').decode(auth);
30
- const deviceSet = getDeviceSet();
31
-
32
- const result = await verifyCredential(credential);
33
- if (result.kind === 'fail') {
34
- log('Invalid credential', { result });
35
- return false;
36
- }
37
-
38
- if (!credential.proof.nonce || !Buffer.from(nonce).equals(credential.proof.nonce)) {
39
- log('Invalid nonce', { nonce, credential });
40
- return false;
41
- }
42
-
43
- if (!deviceSet.has(credential.issuer)) {
44
- log('Device not in allowed set');
45
- return false;
46
- }
47
-
48
- return true;
49
- };
28
+ export type HaloAuthVerifierParams = {
29
+ trustedDevicesProvider: () => ComplexSet<PublicKey>;
30
+ update: Event<void>;
31
+ /**
32
+ * Timeout to wait for the device key to be added to the trusted set.
33
+ */
34
+ authTimeout: number;
35
+ };
36
+
37
+ /**
38
+ * Verifies credentials of another device of the same identity in the HALO space.
39
+ * Will wait up to `authTimeout` for the device key to be added to the trusted set.
40
+ */
41
+ export class HaloAuthVerifier {
42
+ private _ctx = new Context();
43
+
44
+ constructor(private readonly _params: HaloAuthVerifierParams) {}
45
+
46
+ async close() {
47
+ await this._ctx.dispose();
48
+ }
49
+
50
+ private _isTrustedDevice(deviceKey: PublicKey) {
51
+ const deviceSet = this._params.trustedDevicesProvider();
52
+ return deviceSet.has(deviceKey);
53
+ }
54
+
55
+ get verifier(): AuthVerifier {
56
+ return async (nonce, auth) => {
57
+ const credential = schema.getCodecForType('dxos.halo.credentials.Credential').decode(auth);
58
+
59
+ const result = await verifyCredential(credential);
60
+ if (result.kind === 'fail') {
61
+ log('Invalid credential', { result });
62
+ return false;
63
+ }
64
+
65
+ if (!credential.proof.nonce || !Buffer.from(nonce).equals(credential.proof.nonce)) {
66
+ log('Invalid nonce', { nonce, credential });
67
+ return false;
68
+ }
69
+
70
+ if (this._isTrustedDevice(credential.issuer)) {
71
+ return true;
72
+ }
73
+
74
+ const trigger = new Trigger<boolean>();
75
+ this._ctx.onDispose(() => {
76
+ trigger.wake(false);
77
+ });
78
+ const clear = this._params.update.on(this._ctx, () => {
79
+ if (this._isTrustedDevice(credential.issuer)) {
80
+ trigger.wake(true);
81
+ }
82
+ });
83
+ try {
84
+ await trigger.wait({ timeout: this._params.authTimeout });
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ } finally {
89
+ clear();
90
+ }
91
+ };
92
+ }
93
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { expect } from 'chai';
6
6
 
7
- import { valueEncoding, MetadataStore } from '@dxos/echo-db';
7
+ import { valueEncoding, MetadataStore, SpaceManager, AuthStatus } from '@dxos/echo-db';
8
8
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
9
9
  import { Keyring } from '@dxos/keyring';
10
10
  import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
@@ -13,7 +13,6 @@ import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
13
13
  import { createStorage, Storage, StorageType } from '@dxos/random-access-storage';
14
14
  import { describe, test, afterTest } from '@dxos/test';
15
15
 
16
- import { createDefaultModelFactory } from '../services';
17
16
  import { IdentityManager } from './identity-manager';
18
17
 
19
18
  describe('identity/identity-manager', () => {
@@ -43,14 +42,11 @@ describe('identity/identity-manager', () => {
43
42
  signalManager: new MemorySignalManager(signalContext),
44
43
  transportFactory: MemoryTransportFactory
45
44
  });
46
-
47
- const identityManager = new IdentityManager(
48
- metadataStore,
45
+ const spaceManager = new SpaceManager({
49
46
  feedStore,
50
- keyring,
51
- networkManager,
52
- createDefaultModelFactory()
53
- );
47
+ networkManager
48
+ });
49
+ const identityManager = new IdentityManager(metadataStore, keyring, spaceManager);
54
50
 
55
51
  return {
56
52
  identityManager,
@@ -116,5 +112,11 @@ describe('identity/identity-manager', () => {
116
112
  // TODO(dmaretskyi): We'd also need to admit device2's feeds otherwise messages from them won't be processed by the pipeline.
117
113
  // This would mean that peer2 has replicated it's device credential chain from peer1 and is ready to issue credentials.
118
114
  await identity2.ready();
115
+
116
+ // Connection is authenticated.
117
+ expect(identity1.space.protocol.sessions.get(identity2.deviceKey)).to.exist;
118
+ expect(identity1.space.protocol.sessions.get(identity2.deviceKey)?.authStatus).to.equal(AuthStatus.SUCCESS);
119
+ expect(identity2.space.protocol.sessions.get(identity1.deviceKey)).to.exist;
120
+ expect(identity2.space.protocol.sessions.get(identity1.deviceKey)?.authStatus).to.equal(AuthStatus.SUCCESS);
119
121
  });
120
122
  });
@@ -5,30 +5,22 @@
5
5
  import assert from 'node:assert';
6
6
 
7
7
  import { Event } from '@dxos/async';
8
- import { CredentialGenerator } from '@dxos/credentials';
9
- import {
10
- MOCK_AUTH_PROVIDER,
11
- MOCK_AUTH_VERIFIER,
12
- MetadataStore,
13
- Space,
14
- SwarmIdentity,
15
- Database,
16
- SpaceProtocol
17
- } from '@dxos/echo-db';
18
- import { FeedStore } from '@dxos/feed-store';
8
+ import { createCredentialSignerWithKey, CredentialGenerator } from '@dxos/credentials';
9
+ import { MetadataStore, NoopDataPipelineController, SpaceManager, SwarmIdentity } from '@dxos/echo-db';
19
10
  import { Keyring } from '@dxos/keyring';
20
11
  import { PublicKey } from '@dxos/keys';
21
12
  import { log } from '@dxos/log';
22
- import { ModelFactory } from '@dxos/model-factory';
23
- import { NetworkManager } from '@dxos/network-manager';
24
- import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
25
13
  import { AdmittedFeed, IdentityRecord, SpaceRecord } from '@dxos/protocols/proto/dxos/halo/credentials';
14
+ import { Presence } from '@dxos/teleport-extension-presence';
15
+ import { deferFunction } from '@dxos/util';
26
16
 
27
17
  import { Identity } from '../identity';
18
+ import { createHaloAuthProvider } from './authenticator';
28
19
 
29
20
  interface ConstructSpaceParams {
30
21
  spaceRecord: SpaceRecord;
31
22
  swarmIdentity: SwarmIdentity;
23
+ identityKey: PublicKey;
32
24
  }
33
25
 
34
26
  export type JoinIdentityParams = {
@@ -51,10 +43,8 @@ export class IdentityManager {
51
43
  // TODO(dmaretskyi): Perhaps this should take/generate the peerKey outside of an initialized identity.
52
44
  constructor(
53
45
  private readonly _metadataStore: MetadataStore,
54
- private readonly _feedStore: FeedStore<FeedMessage>,
55
46
  private readonly _keyring: Keyring,
56
- private readonly _networkManager: NetworkManager,
57
- private readonly _modelFactory: ModelFactory
47
+ private readonly _spaceManager: SpaceManager
58
48
  ) {}
59
49
 
60
50
  get identity() {
@@ -176,43 +166,40 @@ export class IdentityManager {
176
166
  spaceRecord: identityRecord.haloSpace,
177
167
  swarmIdentity: {
178
168
  peerKey: identityRecord.deviceKey,
179
- credentialProvider: MOCK_AUTH_PROVIDER,
180
- credentialAuthenticator: MOCK_AUTH_VERIFIER
181
- }
169
+ credentialProvider: createHaloAuthProvider(
170
+ createCredentialSignerWithKey(this._keyring, identityRecord.deviceKey)
171
+ ),
172
+ credentialAuthenticator: deferFunction(() => identity.authVerifier.verifier)
173
+ },
174
+ identityKey: identityRecord.identityKey
182
175
  });
183
-
184
- log('done', { identityKey: identityRecord.identityKey });
185
- return new Identity({
176
+ const identity: Identity = new Identity({
186
177
  space,
187
178
  signer: this._keyring,
188
179
  identityKey: identityRecord.identityKey,
189
180
  deviceKey: identityRecord.deviceKey
190
181
  });
191
- }
192
-
193
- private async _constructSpace({ spaceRecord, swarmIdentity }: ConstructSpaceParams) {
194
- const controlFeed = await this._feedStore.openFeed(spaceRecord.writeControlFeedKey, { writable: true });
195
- const dataFeed = await this._feedStore.openFeed(spaceRecord.writeDataFeedKey, { writable: true });
196
-
197
- // The genesis feed will be the same as the control feed if the space was created by the local agent.
198
- // NOTE: Must be initialized after writable feeds so that it is in a writable state.
199
- const genesisFeed = await this._feedStore.openFeed(spaceRecord.genesisFeedKey);
182
+ log('done', { identityKey: identityRecord.identityKey });
200
183
 
201
- const protocol = new SpaceProtocol({
202
- topic: spaceRecord.spaceKey,
203
- identity: swarmIdentity,
204
- networkManager: this._networkManager
205
- });
184
+ return identity;
185
+ }
206
186
 
207
- return new Space({
208
- spaceKey: spaceRecord.spaceKey,
209
- protocol,
210
- genesisFeed,
211
- controlFeed,
212
- dataFeed,
213
- feedProvider: (feedKey) => this._feedStore.openFeed(feedKey),
214
- databaseFactory: async ({ databaseBackend }) =>
215
- new Database(this._modelFactory, databaseBackend, swarmIdentity.peerKey)
187
+ private async _constructSpace({ spaceRecord, swarmIdentity, identityKey }: ConstructSpaceParams) {
188
+ return this._spaceManager.constructSpace({
189
+ metadata: {
190
+ key: spaceRecord.spaceKey,
191
+ genesisFeedKey: spaceRecord.genesisFeedKey,
192
+ controlFeedKey: spaceRecord.writeControlFeedKey,
193
+ dataFeedKey: spaceRecord.writeDataFeedKey
194
+ },
195
+ dataPipelineControllerProvider: () => new NoopDataPipelineController(),
196
+ swarmIdentity,
197
+ presence: new Presence({
198
+ localPeerId: swarmIdentity.peerKey,
199
+ announceInterval: 1_000,
200
+ offlineTimeout: 30_000,
201
+ identityKey
202
+ })
216
203
  });
217
204
  }
218
205
  }
@@ -4,8 +4,15 @@
4
4
 
5
5
  import expect from 'expect';
6
6
 
7
- import { CredentialGenerator, verifyCredential, createCredentialSignerWithKey } from '@dxos/credentials';
8
- import { valueEncoding, Database, MOCK_AUTH_PROVIDER, MOCK_AUTH_VERIFIER, Space, SpaceProtocol } from '@dxos/echo-db';
7
+ import { CredentialGenerator, verifyCredential } from '@dxos/credentials';
8
+ import {
9
+ MOCK_AUTH_PROVIDER,
10
+ MOCK_AUTH_VERIFIER,
11
+ NoopDataPipelineController,
12
+ Space,
13
+ SpaceProtocol,
14
+ valueEncoding
15
+ } from '@dxos/echo-db';
9
16
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
10
17
  import { Keyring } from '@dxos/keyring';
11
18
  import { PublicKey } from '@dxos/keys';
@@ -14,14 +21,11 @@ import { MemoryTransportFactory, NetworkManager } from '@dxos/network-manager';
14
21
  import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
15
22
  import { AdmittedFeed } from '@dxos/protocols/proto/dxos/halo/credentials';
16
23
  import { createStorage, StorageType } from '@dxos/random-access-storage';
17
- import { describe, test, afterTest } from '@dxos/test';
24
+ import { Presence } from '@dxos/teleport-extension-presence';
25
+ import { afterTest, describe, test } from '@dxos/test';
18
26
 
19
- import { createDefaultModelFactory } from '../services';
20
- import { createHaloAuthProvider, createHaloAuthVerifier } from './authenticator';
21
27
  import { Identity } from './identity';
22
28
 
23
- const modelFactory = createDefaultModelFactory();
24
-
25
29
  describe('identity/identity', () => {
26
30
  test('create', async () => {
27
31
  const keyring = new Keyring();
@@ -49,14 +53,20 @@ describe('identity/identity', () => {
49
53
 
50
54
  const protocol = new SpaceProtocol({
51
55
  topic: spaceKey,
52
- identity: {
53
- peerKey: identityKey,
54
- credentialProvider: createHaloAuthProvider(createCredentialSignerWithKey(keyring, deviceKey)),
55
- credentialAuthenticator: createHaloAuthVerifier(() => identity.authorizedDeviceKeys)
56
+ swarmIdentity: {
57
+ peerKey: deviceKey,
58
+ credentialProvider: MOCK_AUTH_PROVIDER,
59
+ credentialAuthenticator: MOCK_AUTH_VERIFIER
56
60
  },
57
61
  networkManager: new NetworkManager({
58
62
  signalManager: new MemorySignalManager(new MemorySignalManagerContext()),
59
63
  transportFactory: MemoryTransportFactory
64
+ }),
65
+ presence: new Presence({
66
+ localPeerId: deviceKey,
67
+ announceInterval: 30,
68
+ offlineTimeout: 200,
69
+ identityKey
60
70
  })
61
71
  });
62
72
 
@@ -67,7 +77,7 @@ describe('identity/identity', () => {
67
77
  controlFeed,
68
78
  dataFeed,
69
79
  feedProvider: (feedKey) => feedStore.openFeed(feedKey),
70
- databaseFactory: async ({ databaseBackend }) => new Database(modelFactory, databaseBackend, deviceKey)
80
+ dataPipelineControllerProvider: () => new NoopDataPipelineController()
71
81
  });
72
82
 
73
83
  const identity = new Identity({
@@ -120,6 +130,7 @@ describe('identity/identity', () => {
120
130
  const signalContext = new MemorySignalManagerContext();
121
131
 
122
132
  let spaceKey: PublicKey;
133
+ let identityKey: PublicKey;
123
134
  let genesisFeedKey: PublicKey;
124
135
  let identity1: Identity;
125
136
  let identity2: Identity;
@@ -129,7 +140,7 @@ describe('identity/identity', () => {
129
140
  //
130
141
  {
131
142
  const keyring = new Keyring();
132
- const identityKey = await keyring.createKey();
143
+ identityKey = await keyring.createKey();
133
144
  const deviceKey = await keyring.createKey();
134
145
  spaceKey = await keyring.createKey();
135
146
 
@@ -155,7 +166,7 @@ describe('identity/identity', () => {
155
166
 
156
167
  const protocol = new SpaceProtocol({
157
168
  topic: spaceKey,
158
- identity: {
169
+ swarmIdentity: {
159
170
  peerKey: deviceKey,
160
171
  credentialProvider: MOCK_AUTH_PROVIDER, // createHaloAuthProvider(createCredentialSignerWithKey(keyring, device_key)),
161
172
  credentialAuthenticator: MOCK_AUTH_VERIFIER // createHaloAuthVerifier(() => identity.authorizedDeviceKeys),
@@ -163,6 +174,12 @@ describe('identity/identity', () => {
163
174
  networkManager: new NetworkManager({
164
175
  signalManager: new MemorySignalManager(signalContext),
165
176
  transportFactory: MemoryTransportFactory
177
+ }),
178
+ presence: new Presence({
179
+ localPeerId: deviceKey,
180
+ announceInterval: 30,
181
+ offlineTimeout: 200,
182
+ identityKey
166
183
  })
167
184
  });
168
185
 
@@ -173,7 +190,7 @@ describe('identity/identity', () => {
173
190
  controlFeed,
174
191
  dataFeed,
175
192
  feedProvider: (feedKey) => feedStore.openFeed(feedKey),
176
- databaseFactory: async ({ databaseBackend }) => new Database(modelFactory, databaseBackend, deviceKey)
193
+ dataPipelineControllerProvider: () => new NoopDataPipelineController()
177
194
  });
178
195
 
179
196
  const identity = (identity1 = new Identity({
@@ -236,7 +253,7 @@ describe('identity/identity', () => {
236
253
 
237
254
  const protocol = new SpaceProtocol({
238
255
  topic: spaceKey,
239
- identity: {
256
+ swarmIdentity: {
240
257
  peerKey: deviceKey,
241
258
  credentialProvider: MOCK_AUTH_PROVIDER, // createHaloAuthProvider(createCredentialSignerWithKey(keyring, device_key)),
242
259
  credentialAuthenticator: MOCK_AUTH_VERIFIER // createHaloAuthVerifier(() => identity.authorizedDeviceKeys),
@@ -244,6 +261,12 @@ describe('identity/identity', () => {
244
261
  networkManager: new NetworkManager({
245
262
  signalManager: new MemorySignalManager(signalContext),
246
263
  transportFactory: MemoryTransportFactory
264
+ }),
265
+ presence: new Presence({
266
+ localPeerId: deviceKey,
267
+ announceInterval: 30,
268
+ offlineTimeout: 200,
269
+ identityKey
247
270
  })
248
271
  });
249
272
 
@@ -254,7 +277,7 @@ describe('identity/identity', () => {
254
277
  controlFeed,
255
278
  dataFeed,
256
279
  feedProvider: (feedKey) => feedStore.openFeed(feedKey),
257
- databaseFactory: async ({ databaseBackend }) => new Database(modelFactory, databaseBackend, deviceKey)
280
+ dataPipelineControllerProvider: () => new NoopDataPipelineController()
258
281
  });
259
282
 
260
283
  const identity = (identity2 = new Identity({
@@ -13,8 +13,7 @@ import {
13
13
  ProfileStateMachine
14
14
  } from '@dxos/credentials';
15
15
  import { Signer } from '@dxos/crypto';
16
- import { failUndefined } from '@dxos/debug';
17
- import { Database, Space } from '@dxos/echo-db';
16
+ import { Space } from '@dxos/echo-db';
18
17
  import { writeMessages } from '@dxos/feed-store';
19
18
  import { PublicKey } from '@dxos/keys';
20
19
  import { log } from '@dxos/log';
@@ -23,6 +22,13 @@ import { AdmittedFeed, ProfileDocument } from '@dxos/protocols/proto/dxos/halo/c
23
22
  import { HaloAdmissionCredentials } from '@dxos/protocols/proto/dxos/halo/invitations';
24
23
  import { ComplexSet } from '@dxos/util';
25
24
 
25
+ import { HaloAuthVerifier } from './authenticator';
26
+
27
+ /**
28
+ * Timeout for the device to be added to the trusted set during auth.
29
+ */
30
+ const AUTH_TIMEOUT = 30000;
31
+
26
32
  export type IdentityParams = {
27
33
  identityKey: PublicKey;
28
34
  deviceKey: PublicKey;
@@ -34,10 +40,11 @@ export type IdentityParams = {
34
40
  * Agent identity manager, which includes the agent's Halo space.
35
41
  */
36
42
  export class Identity {
37
- private readonly _space: Space;
43
+ public readonly space: Space;
38
44
  private readonly _signer: Signer;
39
45
  private readonly _deviceStateMachine: DeviceStateMachine;
40
46
  private readonly _profileStateMachine: ProfileStateMachine;
47
+ public readonly authVerifier: HaloAuthVerifier;
41
48
 
42
49
  public readonly identityKey: PublicKey;
43
50
  public readonly deviceKey: PublicKey;
@@ -45,7 +52,7 @@ export class Identity {
45
52
  public readonly stateUpdate = new Event();
46
53
 
47
54
  constructor({ space, signer, identityKey, deviceKey }: IdentityParams) {
48
- this._space = space;
55
+ this.space = space;
49
56
  this._signer = signer;
50
57
 
51
58
  this.identityKey = identityKey;
@@ -55,12 +62,18 @@ export class Identity {
55
62
  this._profileStateMachine = new ProfileStateMachine(this.identityKey);
56
63
 
57
64
  // Process halo-specific credentials.
58
- this._space.onCredentialProcessed.set(async (credential) => {
65
+ this.space.onCredentialProcessed.set(async (credential) => {
59
66
  // Save device keychain credential when processed by the space state machine.
60
67
  await this._deviceStateMachine.process(credential);
61
68
  await this._profileStateMachine.process(credential);
62
69
  this.stateUpdate.emit();
63
70
  });
71
+
72
+ this.authVerifier = new HaloAuthVerifier({
73
+ trustedDevicesProvider: () => this.authorizedDeviceKeys,
74
+ update: this.stateUpdate,
75
+ authTimeout: AUTH_TIMEOUT
76
+ });
64
77
  }
65
78
 
66
79
  // TODO(burdon): Expose state object?
@@ -69,11 +82,12 @@ export class Identity {
69
82
  }
70
83
 
71
84
  async open() {
72
- await this._space.open();
85
+ await this.space.open();
73
86
  }
74
87
 
75
88
  async close() {
76
- await this._space.close();
89
+ await this.authVerifier.close();
90
+ await this.space.close();
77
91
  }
78
92
 
79
93
  async ready() {
@@ -90,26 +104,22 @@ export class Identity {
90
104
  * @test-only
91
105
  */
92
106
  get controlPipeline() {
93
- return this._space.controlPipeline;
107
+ return this.space.controlPipeline;
94
108
  }
95
109
 
96
110
  get haloSpaceKey() {
97
- return this._space.key;
111
+ return this.space.key;
98
112
  }
99
113
 
100
114
  get haloGenesisFeedKey() {
101
- return this._space.genesisFeedKey;
102
- }
103
-
104
- get haloDatabase(): Database {
105
- return this._space.database ?? failUndefined();
115
+ return this.space.genesisFeedKey;
106
116
  }
107
117
 
108
118
  getAdmissionCredentials(): HaloAdmissionCredentials {
109
119
  return {
110
120
  deviceKey: this.deviceKey,
111
- controlFeedKey: this._space.controlFeedKey,
112
- dataFeedKey: this._space.dataFeedKey
121
+ controlFeedKey: this.space.controlFeedKey,
122
+ dataFeedKey: this.space.dataFeedKey
113
123
  };
114
124
  }
115
125
 
@@ -20,7 +20,7 @@ import {
20
20
  HaloAdmissionOffer,
21
21
  HaloHostService
22
22
  } from '@dxos/protocols/proto/dxos/halo/invitations';
23
- import { ExtensionContext } from '@dxos/teleport';
23
+ import { ExtensionContext, RpcExtension } from '@dxos/teleport';
24
24
 
25
25
  import { IdentityManager } from '../identity';
26
26
  import {
@@ -32,7 +32,6 @@ import {
32
32
  ON_CLOSE_DELAY
33
33
  } from './invitations';
34
34
  import { AbstractInvitationsHandler, InvitationsOptions } from './invitations-handler';
35
- import { RpcExtension } from './rpc-extension';
36
35
 
37
36
  /**
38
37
  * Handles the life-cycle of Halo invitations between peers.
@@ -24,15 +24,15 @@ describe('services/space-invitations-handler', () => {
24
24
  test('genesis', async () => {
25
25
  const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
26
26
 
27
- const space = await peer.spaceManager!.createSpace();
27
+ const space = await peer.dataSpaceManager!.createSpace();
28
28
  expect(space.database).not.to.be.undefined;
29
- expect(peer.spaceManager!.spaces.has(space.key)).to.be.true;
29
+ expect(peer.dataSpaceManager!.spaces.has(space.key)).to.be.true;
30
30
  await space.close();
31
31
  });
32
32
 
33
33
  test('genesis with database mutations', async () => {
34
34
  const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
35
- const space = await peer.spaceManager!.createSpace();
35
+ const space = await peer.dataSpaceManager!.createSpace();
36
36
 
37
37
  {
38
38
  const item = await space.database!.createItem<ObjectModel>({ type: 'test' });
@@ -56,7 +56,7 @@ describe('services/space-invitations-handler', () => {
56
56
  let attempt = 0;
57
57
  const authenticationCode = new Trigger<string>();
58
58
 
59
- const space1 = await host.spaceManager!.createSpace();
59
+ const space1 = await host.dataSpaceManager!.createSpace();
60
60
  const observable1 = host.spaceInvitations!.createInvitation(space1);
61
61
  observable1.subscribe({
62
62
  onConnecting: async (invitation1: Invitation) => {
@@ -98,8 +98,8 @@ describe('services/space-invitations-handler', () => {
98
98
  expect(spaceKey1).to.deep.eq(spaceKey2);
99
99
 
100
100
  {
101
- const space1 = host.spaceManager!.spaces.get(spaceKey1)!;
102
- const space2 = guest.spaceManager!.spaces.get(spaceKey2)!;
101
+ const space1 = host.dataSpaceManager!.spaces.get(spaceKey1)!;
102
+ const space2 = guest.dataSpaceManager!.spaces.get(spaceKey2)!;
103
103
  expect(space1).not.to.be.undefined;
104
104
  expect(space2).not.to.be.undefined;
105
105
 
@@ -117,7 +117,7 @@ describe('services/space-invitations-handler', () => {
117
117
  const connecting1 = new Trigger<Invitation>(); // peer 1 connected.
118
118
  const connecting2 = new Trigger<Invitation>(); // peer 2 connected.
119
119
 
120
- const space1 = await host.spaceManager!.createSpace();
120
+ const space1 = await host.dataSpaceManager!.createSpace();
121
121
  const observable1 = await host.spaceInvitations!.createInvitation(space1);
122
122
  observable1.subscribe({
123
123
  onConnecting: async (invitation1: Invitation) => {