@dxos/client-services 0.1.21 → 0.1.23

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 (29) hide show
  1. package/dist/lib/browser/index.mjs +79 -38
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +82 -41
  5. package/dist/lib/node/index.cjs.map +3 -3
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/types/src/packlets/identity/authenticator.d.ts +8 -8
  8. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  9. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  10. package/dist/types/src/packlets/identity/identity.d.ts +2 -2
  11. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  12. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  13. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  14. package/dist/types/src/packlets/spaces/data-space-manager.test.d.ts +2 -0
  15. package/dist/types/src/packlets/spaces/data-space-manager.test.d.ts.map +1 -0
  16. package/dist/types/src/packlets/spaces/data-space.d.ts +2 -0
  17. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  18. package/dist/types/src/packlets/testing/test-builder.d.ts +30 -1
  19. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  20. package/package.json +28 -28
  21. package/src/packlets/identity/authenticator.test.ts +4 -4
  22. package/src/packlets/identity/authenticator.ts +18 -13
  23. package/src/packlets/identity/identity-manager.ts +2 -4
  24. package/src/packlets/identity/identity.ts +4 -4
  25. package/src/packlets/services/service-context.ts +0 -4
  26. package/src/packlets/spaces/data-space-manager.test.ts +74 -0
  27. package/src/packlets/spaces/data-space-manager.ts +8 -3
  28. package/src/packlets/spaces/data-space.ts +12 -0
  29. package/src/packlets/testing/test-builder.ts +87 -1
@@ -11,7 +11,7 @@ import { log } from '@dxos/log';
11
11
  import { schema } from '@dxos/protocols';
12
12
  import { ComplexSet } from '@dxos/util';
13
13
 
14
- export const createHaloAuthProvider =
14
+ export const createAuthProvider =
15
15
  (signer: CredentialSigner): AuthProvider =>
16
16
  async (nonce) => {
17
17
  const credential = await signer.createCredential({
@@ -25,8 +25,9 @@ export const createHaloAuthProvider =
25
25
  return schema.getCodecForType('dxos.halo.credentials.Credential').encode(credential);
26
26
  };
27
27
 
28
- export type HaloAuthVerifierParams = {
29
- trustedDevicesProvider: () => ComplexSet<PublicKey>;
28
+ export type TrustedKeySetAuthVerifierParams = {
29
+ // TODO(dmaretskyi): Change to `isTrusted: (key) => bool`.
30
+ trustedKeysProvider: () => ComplexSet<PublicKey>;
30
31
  update: Event<void>;
31
32
  /**
32
33
  * Timeout to wait for the device key to be added to the trusted set.
@@ -35,26 +36,27 @@ export type HaloAuthVerifierParams = {
35
36
  };
36
37
 
37
38
  /**
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.
39
+ * Verifies credentials of another member in the space based on a set of trusted key.
40
+ * Will wait up to `authTimeout` for the key to be added to the trusted set.
40
41
  */
41
- export class HaloAuthVerifier {
42
+ export class TrustedKeySetAuthVerifier {
42
43
  private _ctx = new Context();
43
44
 
44
- constructor(private readonly _params: HaloAuthVerifierParams) {}
45
+ constructor(private readonly _params: TrustedKeySetAuthVerifierParams) {}
45
46
 
46
47
  async close() {
47
48
  await this._ctx.dispose();
48
49
  }
49
50
 
50
- private _isTrustedDevice(deviceKey: PublicKey) {
51
- const deviceSet = this._params.trustedDevicesProvider();
51
+ private _isTrustedKey(deviceKey: PublicKey) {
52
+ const deviceSet = this._params.trustedKeysProvider();
52
53
  return deviceSet.has(deviceKey);
53
54
  }
54
55
 
55
56
  get verifier(): AuthVerifier {
56
57
  return async (nonce, auth) => {
57
58
  const credential = schema.getCodecForType('dxos.halo.credentials.Credential').decode(auth);
59
+ log('authenticating...', { credential });
58
60
 
59
61
  const result = await verifyCredential(credential);
60
62
  if (result.kind === 'fail') {
@@ -67,7 +69,8 @@ export class HaloAuthVerifier {
67
69
  return false;
68
70
  }
69
71
 
70
- if (this._isTrustedDevice(credential.issuer)) {
72
+ if (this._isTrustedKey(credential.issuer)) {
73
+ log('key is not currently in trusted set, waiting...', { key: credential.issuer });
71
74
  return true;
72
75
  }
73
76
 
@@ -76,13 +79,15 @@ export class HaloAuthVerifier {
76
79
  trigger.wake(false);
77
80
  });
78
81
  const clear = this._params.update.on(this._ctx, () => {
79
- if (this._isTrustedDevice(credential.issuer)) {
82
+ if (this._isTrustedKey(credential.issuer)) {
83
+ log('auth success', { key: credential.issuer });
80
84
  trigger.wake(true);
85
+ } else {
86
+ log('key is not currently in trusted set, waiting...', { key: credential.issuer });
81
87
  }
82
88
  });
83
89
  try {
84
- await trigger.wait({ timeout: this._params.authTimeout });
85
- return true;
90
+ return await trigger.wait({ timeout: this._params.authTimeout });
86
91
  } catch {
87
92
  return false;
88
93
  } finally {
@@ -14,7 +14,7 @@ import { AdmittedFeed, IdentityRecord, SpaceRecord } from '@dxos/protocols/proto
14
14
  import { deferFunction } from '@dxos/util';
15
15
 
16
16
  import { Identity } from '../identity';
17
- import { createHaloAuthProvider } from './authenticator';
17
+ import { createAuthProvider } from './authenticator';
18
18
 
19
19
  interface ConstructSpaceParams {
20
20
  spaceRecord: SpaceRecord;
@@ -166,9 +166,7 @@ export class IdentityManager {
166
166
  spaceRecord: identityRecord.haloSpace,
167
167
  swarmIdentity: {
168
168
  peerKey: identityRecord.deviceKey,
169
- credentialProvider: createHaloAuthProvider(
170
- createCredentialSignerWithKey(this._keyring, identityRecord.deviceKey)
171
- ),
169
+ credentialProvider: createAuthProvider(createCredentialSignerWithKey(this._keyring, identityRecord.deviceKey)),
172
170
  credentialAuthenticator: deferFunction(() => identity.authVerifier.verifier)
173
171
  },
174
172
  identityKey: identityRecord.identityKey
@@ -23,7 +23,7 @@ import { AdmittedFeed, ProfileDocument } from '@dxos/protocols/proto/dxos/halo/c
23
23
  import { HaloAdmissionCredentials } from '@dxos/protocols/proto/dxos/halo/invitations';
24
24
  import { ComplexSet } from '@dxos/util';
25
25
 
26
- import { HaloAuthVerifier } from './authenticator';
26
+ import { TrustedKeySetAuthVerifier } from './authenticator';
27
27
 
28
28
  /**
29
29
  * Timeout for the device to be added to the trusted set during auth.
@@ -45,7 +45,7 @@ export class Identity {
45
45
  private readonly _signer: Signer;
46
46
  private readonly _deviceStateMachine: CredentialConsumer<DeviceStateMachine>;
47
47
  private readonly _profileStateMachine: CredentialConsumer<ProfileStateMachine>;
48
- public readonly authVerifier: HaloAuthVerifier;
48
+ public readonly authVerifier: TrustedKeySetAuthVerifier;
49
49
 
50
50
  public readonly identityKey: PublicKey;
51
51
  public readonly deviceKey: PublicKey;
@@ -73,8 +73,8 @@ export class Identity {
73
73
  })
74
74
  );
75
75
 
76
- this.authVerifier = new HaloAuthVerifier({
77
- trustedDevicesProvider: () => this.authorizedDeviceKeys,
76
+ this.authVerifier = new TrustedKeySetAuthVerifier({
77
+ trustedKeysProvider: () => this.authorizedDeviceKeys,
78
78
  update: this.stateUpdate,
79
79
  authTimeout: AUTH_TIMEOUT
80
80
  });
@@ -5,8 +5,6 @@
5
5
  import { Trigger } from '@dxos/async';
6
6
  import { failUndefined } from '@dxos/debug';
7
7
  import {
8
- MOCK_AUTH_PROVIDER,
9
- MOCK_AUTH_VERIFIER,
10
8
  valueEncoding,
11
9
  DataServiceSubscriptions,
12
10
  MetadataStore,
@@ -122,8 +120,6 @@ export class ServiceContext {
122
120
  log('initializing spaces...');
123
121
  const identity = this.identityManager.identity ?? failUndefined();
124
122
  const signingContext: SigningContext = {
125
- credentialProvider: MOCK_AUTH_PROVIDER,
126
- credentialAuthenticator: MOCK_AUTH_VERIFIER,
127
123
  credentialSigner: identity.getIdentityCredentialSigner(),
128
124
  identityKey: identity.identityKey,
129
125
  deviceKey: identity.deviceKey,
@@ -0,0 +1,74 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { createAdmissionCredentials } from '@dxos/credentials';
8
+ import { AuthStatus, DataServiceSubscriptions } from '@dxos/echo-db';
9
+ import { writeMessages } from '@dxos/feed-store';
10
+ import { ModelFactory } from '@dxos/model-factory';
11
+ import { ObjectModel } from '@dxos/object-model';
12
+ import { test, describe } from '@dxos/test';
13
+
14
+ import { createSigningContext, syncItems, TestBuilder } from '../testing';
15
+ import { DataSpaceManager } from './data-space-manager';
16
+
17
+ describe('DataSpaceManager', () => {
18
+ test('sync between peers', async () => {
19
+ const builder = new TestBuilder();
20
+
21
+ const peer1 = builder.createPeer();
22
+ const identity1 = await createSigningContext(peer1.keyring);
23
+ const dataSpaceManager1 = new DataSpaceManager(
24
+ peer1.spaceManager,
25
+ peer1.metadataStore,
26
+ new DataServiceSubscriptions(),
27
+ peer1.keyring,
28
+ identity1,
29
+ new ModelFactory().registerModel(ObjectModel),
30
+ peer1.snapshotStore
31
+ );
32
+
33
+ const peer2 = builder.createPeer();
34
+ const identity2 = await createSigningContext(peer2.keyring);
35
+ const dataSpaceManager2 = new DataSpaceManager(
36
+ peer2.spaceManager,
37
+ peer2.metadataStore,
38
+ new DataServiceSubscriptions(),
39
+ peer2.keyring,
40
+ identity2,
41
+ new ModelFactory().registerModel(ObjectModel),
42
+ peer1.snapshotStore
43
+ );
44
+
45
+ const space1 = await dataSpaceManager1.createSpace();
46
+ const space2 = await dataSpaceManager2.acceptSpace({
47
+ spaceKey: space1.key,
48
+ genesisFeedKey: space1.inner.genesisFeedKey,
49
+ controlFeedKey: await peer2.keyring.createKey(),
50
+ dataFeedKey: await peer2.keyring.createKey()
51
+ });
52
+
53
+ // Admit peer2 to space1.
54
+ await writeMessages(
55
+ space1.inner.controlPipeline.writer,
56
+ await createAdmissionCredentials(
57
+ identity1.credentialSigner,
58
+ identity2.identityKey,
59
+ identity2.deviceKey,
60
+ space1.key,
61
+ space2.inner.controlFeedKey,
62
+ space2.inner.dataFeedKey,
63
+ space2.inner.genesisFeedKey
64
+ )
65
+ );
66
+
67
+ await syncItems(space1, space2);
68
+
69
+ expect(space1.inner.protocol.sessions.get(identity2.deviceKey)).to.exist;
70
+ expect(space1.inner.protocol.sessions.get(identity2.deviceKey)?.authStatus).to.equal(AuthStatus.SUCCESS);
71
+ expect(space2.inner.protocol.sessions.get(identity1.deviceKey)).to.exist;
72
+ expect(space2.inner.protocol.sessions.get(identity1.deviceKey)?.authStatus).to.equal(AuthStatus.SUCCESS);
73
+ });
74
+ });
@@ -2,6 +2,8 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ import assert from 'node:assert';
6
+
5
7
  import { Event, synchronized, trackLeaks } from '@dxos/async';
6
8
  import { Context } from '@dxos/context';
7
9
  import {
@@ -21,8 +23,9 @@ import { log } from '@dxos/log';
21
23
  import { ModelFactory } from '@dxos/model-factory';
22
24
  import { SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
23
25
  import { Presence } from '@dxos/teleport-extension-presence';
24
- import { ComplexMap } from '@dxos/util';
26
+ import { ComplexMap, deferFunction } from '@dxos/util';
25
27
 
28
+ import { createAuthProvider } from '../identity';
26
29
  import { DataSpace } from './data-space';
27
30
 
28
31
  @trackLeaks('open', 'close')
@@ -99,6 +102,8 @@ export class DataSpaceManager {
99
102
  // TODO(burdon): Rename join space.
100
103
  @synchronized
101
104
  async acceptSpace(opts: AcceptSpaceOptions): Promise<DataSpace> {
105
+ assert(!this._spaces.has(opts.spaceKey), 'Space already exists.');
106
+
102
107
  const metadata: SpaceMetadata = {
103
108
  key: opts.spaceKey,
104
109
  genesisFeedKey: opts.genesisFeedKey,
@@ -125,8 +130,8 @@ export class DataSpaceManager {
125
130
  metadata,
126
131
  swarmIdentity: {
127
132
  peerKey: this._signingContext.deviceKey,
128
- credentialProvider: this._signingContext.credentialProvider,
129
- credentialAuthenticator: this._signingContext.credentialAuthenticator
133
+ credentialProvider: createAuthProvider(this._signingContext.credentialSigner),
134
+ credentialAuthenticator: deferFunction(() => dataSpace.authVerifier.verifier)
130
135
  },
131
136
  onNetworkConnection: (session) => {
132
137
  session.addExtension(
@@ -10,6 +10,11 @@ import { Database, DataPipelineControllerImpl, ISpace, MetadataStore, Space, Sna
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { ModelFactory } from '@dxos/model-factory';
12
12
  import { Presence } from '@dxos/teleport-extension-presence';
13
+ import { ComplexSet } from '@dxos/util';
14
+
15
+ import { TrustedKeySetAuthVerifier } from '../identity';
16
+
17
+ const AUTH_TIMEOUT = 30000;
13
18
 
14
19
  export type DataSpaceParams = {
15
20
  inner: Space;
@@ -27,6 +32,7 @@ export class DataSpace implements ISpace {
27
32
  private readonly _dataPipelineController: DataPipelineControllerImpl;
28
33
  private readonly _inner: Space;
29
34
  private readonly _presence: Presence;
35
+ public readonly authVerifier: TrustedKeySetAuthVerifier;
30
36
 
31
37
  constructor(params: DataSpaceParams) {
32
38
  this._inner = params.inner;
@@ -40,6 +46,11 @@ export class DataSpace implements ISpace {
40
46
  feedInfoProvider: (feedKey) => this._inner.spaceState.feeds.get(feedKey),
41
47
  snapshotId: params.snapshotId
42
48
  });
49
+ this.authVerifier = new TrustedKeySetAuthVerifier({
50
+ trustedKeysProvider: () => new ComplexSet(PublicKey.hash, Array.from(this._inner.spaceState.members.keys())),
51
+ update: this._inner.stateUpdate,
52
+ authTimeout: AUTH_TIMEOUT
53
+ });
43
54
  }
44
55
 
45
56
  get key() {
@@ -78,6 +89,7 @@ export class DataSpace implements ISpace {
78
89
 
79
90
  async close() {
80
91
  await this._ctx.dispose();
92
+ await this.authVerifier.close();
81
93
  await this._inner.close();
82
94
  await this._presence.destroy();
83
95
  }
@@ -5,7 +5,10 @@
5
5
  import { expect } from 'chai';
6
6
 
7
7
  import { Config } from '@dxos/config';
8
- import { ISpace } from '@dxos/echo-db';
8
+ import { createCredentialSignerWithChain, CredentialGenerator } from '@dxos/credentials';
9
+ import { ISpace, MetadataStore, SigningContext, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-db';
10
+ import { FeedFactory, FeedStore } from '@dxos/feed-store';
11
+ import { Keyring } from '@dxos/keyring';
9
12
  import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
10
13
  import { MemoryTransportFactory, NetworkManager } from '@dxos/network-manager';
11
14
  import { createStorage, Storage, StorageType } from '@dxos/random-access-storage';
@@ -78,3 +81,86 @@ export const syncItems = async (space1: ISpace, space2: ISpace) => {
78
81
  expect(item1.id).to.eq(item2.id);
79
82
  }
80
83
  };
84
+
85
+ export class TestBuilder {
86
+ public readonly signalContext = new MemorySignalManagerContext();
87
+
88
+ createPeer(): TestPeer {
89
+ return new TestPeer(this.signalContext);
90
+ }
91
+ }
92
+
93
+ export type TestPeerProps = {
94
+ storage?: Storage;
95
+ feedStore?: FeedStore<any>;
96
+ metadataStore?: MetadataStore;
97
+ keyring?: Keyring;
98
+ networkManager?: NetworkManager;
99
+ spaceManager?: SpaceManager;
100
+ snapshotStore?: SnapshotStore;
101
+ };
102
+
103
+ export class TestPeer {
104
+ private _props: TestPeerProps = {};
105
+
106
+ constructor(private readonly signalContext: MemorySignalManagerContext) {}
107
+
108
+ get storage() {
109
+ return (this._props.storage ??= createStorage({ type: StorageType.RAM }));
110
+ }
111
+
112
+ get keyring() {
113
+ return (this._props.keyring ??= new Keyring(this.storage.createDirectory('keyring')));
114
+ }
115
+
116
+ get feedStore() {
117
+ return (this._props.feedStore ??= new FeedStore({
118
+ factory: new FeedFactory({
119
+ root: this.storage.createDirectory('feeds'),
120
+ signer: this.keyring,
121
+ hypercore: {
122
+ valueEncoding
123
+ }
124
+ })
125
+ }));
126
+ }
127
+
128
+ get metadataStore() {
129
+ return (this._props.metadataStore ??= new MetadataStore(this.storage.createDirectory('metadata')));
130
+ }
131
+
132
+ get snapshotStore() {
133
+ return (this._props.snapshotStore ??= new SnapshotStore(this.storage.createDirectory('snapshots')));
134
+ }
135
+
136
+ get networkManager() {
137
+ return (this._props.networkManager ??= new NetworkManager({
138
+ signalManager: new MemorySignalManager(this.signalContext),
139
+ transportFactory: MemoryTransportFactory
140
+ }));
141
+ }
142
+
143
+ get spaceManager() {
144
+ return (this._props.spaceManager ??= new SpaceManager({
145
+ feedStore: this.feedStore,
146
+ networkManager: this.networkManager
147
+ }));
148
+ }
149
+ }
150
+
151
+ export const createSigningContext = async (keyring: Keyring): Promise<SigningContext> => {
152
+ const identityKey = await keyring.createKey();
153
+ const deviceKey = await keyring.createKey();
154
+
155
+ return {
156
+ identityKey,
157
+ deviceKey,
158
+ credentialSigner: createCredentialSignerWithChain(
159
+ keyring,
160
+ {
161
+ credential: await new CredentialGenerator(keyring, identityKey, deviceKey).createDeviceAuthorization(deviceKey)
162
+ },
163
+ deviceKey
164
+ )
165
+ };
166
+ };