@dxos/client-services 0.1.23 → 0.1.24

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 (115) hide show
  1. package/dist/lib/browser/chunk-R2FEBB66.mjs +3865 -0
  2. package/dist/lib/browser/chunk-R2FEBB66.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +56 -3066
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +198 -0
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +7 -0
  8. package/dist/lib/node/index.cjs +1401 -645
  9. package/dist/lib/node/index.cjs.map +4 -4
  10. package/dist/lib/node/meta.json +1 -1
  11. package/dist/lib/node/packlets/testing/index.cjs +3688 -0
  12. package/dist/lib/node/packlets/testing/index.cjs.map +7 -0
  13. package/dist/types/src/packlets/deprecated/profile.d.ts +3 -1
  14. package/dist/types/src/packlets/deprecated/profile.d.ts.map +1 -1
  15. package/dist/types/src/packlets/deprecated/space.d.ts.map +1 -1
  16. package/dist/types/src/packlets/deprecated/system.d.ts +15 -9
  17. package/dist/types/src/packlets/deprecated/system.d.ts.map +1 -1
  18. package/dist/types/src/packlets/devtools/devtools.d.ts +2 -2
  19. package/dist/types/src/packlets/devtools/devtools.d.ts.map +1 -1
  20. package/dist/types/src/packlets/devtools/feeds.d.ts.map +1 -1
  21. package/dist/types/src/packlets/devtools/network.d.ts +3 -3
  22. package/dist/types/src/packlets/devtools/network.d.ts.map +1 -1
  23. package/dist/types/src/packlets/devtools/spaces.d.ts.map +1 -1
  24. package/dist/types/src/packlets/identity/authenticator.d.ts +1 -1
  25. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  26. package/dist/types/src/packlets/identity/identity-manager.d.ts +7 -1
  27. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  28. package/dist/types/src/packlets/identity/identity.d.ts +1 -0
  29. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  30. package/dist/types/src/packlets/invitations/halo-invitations-handler.d.ts +11 -3
  31. package/dist/types/src/packlets/invitations/halo-invitations-handler.d.ts.map +1 -1
  32. package/dist/types/src/packlets/invitations/invitations.d.ts +1 -1
  33. package/dist/types/src/packlets/invitations/invitations.d.ts.map +1 -1
  34. package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
  35. package/dist/types/src/packlets/services/service-context.d.ts +2 -0
  36. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  37. package/dist/types/src/packlets/services/service-context.test.d.ts +2 -0
  38. package/dist/types/src/packlets/services/service-context.test.d.ts.map +1 -0
  39. package/dist/types/src/packlets/services/service-definitions.d.ts +1 -1
  40. package/dist/types/src/packlets/services/service-definitions.d.ts.map +1 -1
  41. package/dist/types/src/packlets/services/service-host.d.ts +9 -6
  42. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  43. package/dist/types/src/packlets/services/service-registry.d.ts +4 -3
  44. package/dist/types/src/packlets/services/service-registry.d.ts.map +1 -1
  45. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +9 -2
  46. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  47. package/dist/types/src/packlets/spaces/data-space.d.ts +18 -4
  48. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  49. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +50 -0
  50. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -0
  51. package/dist/types/src/packlets/spaces/notarization-plugin.test.d.ts +2 -0
  52. package/dist/types/src/packlets/spaces/notarization-plugin.test.d.ts.map +1 -0
  53. package/dist/types/src/packlets/spaces/spaces-service.d.ts +2 -1
  54. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  55. package/dist/types/src/packlets/testing/credential-utils.d.ts +8 -0
  56. package/dist/types/src/packlets/testing/credential-utils.d.ts.map +1 -0
  57. package/dist/types/src/packlets/testing/index.d.ts +2 -0
  58. package/dist/types/src/packlets/testing/index.d.ts.map +1 -1
  59. package/dist/types/src/packlets/testing/invitation-utils.d.ts +3 -0
  60. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -0
  61. package/dist/types/src/packlets/testing/test-builder.d.ts +2 -2
  62. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  63. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts +28 -0
  64. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts.map +1 -0
  65. package/dist/types/src/packlets/vault/{iframe-runtime.d.ts → iframe-proxy-runtime.d.ts} +4 -4
  66. package/dist/types/src/packlets/vault/iframe-proxy-runtime.d.ts.map +1 -0
  67. package/dist/types/src/packlets/vault/index.d.ts +4 -1
  68. package/dist/types/src/packlets/vault/index.d.ts.map +1 -1
  69. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts +19 -0
  70. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts.map +1 -0
  71. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  72. package/dist/types/src/packlets/vault/worker-session.d.ts +4 -4
  73. package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
  74. package/package.json +29 -29
  75. package/src/packlets/deprecated/profile.ts +20 -2
  76. package/src/packlets/deprecated/space.ts +41 -37
  77. package/src/packlets/deprecated/system.ts +26 -13
  78. package/src/packlets/devtools/devtools.ts +4 -4
  79. package/src/packlets/devtools/feeds.ts +11 -6
  80. package/src/packlets/devtools/network.ts +20 -9
  81. package/src/packlets/devtools/spaces.ts +7 -6
  82. package/src/packlets/identity/authenticator.ts +11 -6
  83. package/src/packlets/identity/identity-manager.test.ts +24 -15
  84. package/src/packlets/identity/identity-manager.ts +22 -8
  85. package/src/packlets/identity/identity.test.ts +21 -22
  86. package/src/packlets/identity/identity.ts +10 -5
  87. package/src/packlets/invitations/halo-invitations-handler.test.ts +4 -45
  88. package/src/packlets/invitations/halo-invitations-handler.ts +33 -17
  89. package/src/packlets/invitations/invitations.ts +2 -1
  90. package/src/packlets/invitations/space-invitations-handler.test.ts +43 -26
  91. package/src/packlets/invitations/space-invitations-handler.ts +8 -11
  92. package/src/packlets/services/service-context.test.ts +56 -0
  93. package/src/packlets/services/service-context.ts +60 -4
  94. package/src/packlets/services/service-definitions.ts +1 -1
  95. package/src/packlets/services/service-host.test.ts +74 -5
  96. package/src/packlets/services/service-host.ts +85 -23
  97. package/src/packlets/services/service-registry.test.ts +2 -1
  98. package/src/packlets/services/service-registry.ts +5 -1
  99. package/src/packlets/spaces/data-space-manager.test.ts +71 -14
  100. package/src/packlets/spaces/data-space-manager.ts +49 -8
  101. package/src/packlets/spaces/data-space.ts +113 -14
  102. package/src/packlets/spaces/notarization-plugin.test.ts +81 -0
  103. package/src/packlets/spaces/notarization-plugin.ts +211 -0
  104. package/src/packlets/spaces/spaces-service.ts +9 -1
  105. package/src/packlets/testing/credential-utils.ts +28 -0
  106. package/src/packlets/testing/index.ts +2 -0
  107. package/src/packlets/testing/invitation-utils.ts +44 -0
  108. package/src/packlets/testing/test-builder.ts +14 -18
  109. package/src/packlets/vault/iframe-host-runtime.ts +112 -0
  110. package/src/packlets/vault/{iframe-runtime.ts → iframe-proxy-runtime.ts} +3 -3
  111. package/src/packlets/vault/index.ts +4 -1
  112. package/src/packlets/vault/vault-resource-lock.ts +76 -0
  113. package/src/packlets/vault/worker-runtime.ts +13 -3
  114. package/src/packlets/vault/worker-session.ts +17 -17
  115. package/dist/types/src/packlets/vault/iframe-runtime.d.ts.map +0 -1
@@ -7,14 +7,15 @@ import assert from 'node:assert';
7
7
 
8
8
  import { asyncChain, latch, Trigger } from '@dxos/async';
9
9
  import { raise } from '@dxos/debug';
10
+ import { testLocalDatabase } from '@dxos/echo-db/testing';
10
11
  import { PublicKey } from '@dxos/keys';
11
- import { ObjectModel } from '@dxos/object-model';
12
12
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
13
- import { describe, test, afterTest } from '@dxos/test';
13
+ import { afterTest, describe, test } from '@dxos/test';
14
14
  import { range } from '@dxos/util';
15
15
 
16
16
  import { ServiceContext } from '../services';
17
17
  import { createIdentity, createPeers, syncItems } from '../testing';
18
+ import { performInvitation } from '../testing/invitation-utils';
18
19
 
19
20
  const closeAfterTest = async (peer: ServiceContext) => {
20
21
  afterTest(() => peer.close());
@@ -26,7 +27,6 @@ describe('services/space-invitations-handler', () => {
26
27
  const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
27
28
 
28
29
  const space = await peer.dataSpaceManager!.createSpace();
29
- expect(space.database).not.to.be.undefined;
30
30
  expect(peer.dataSpaceManager!.spaces.has(space.key)).to.be.true;
31
31
  await space.close();
32
32
  });
@@ -34,18 +34,30 @@ describe('services/space-invitations-handler', () => {
34
34
  test('genesis with database mutations', async () => {
35
35
  const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
36
36
  const space = await peer.dataSpaceManager!.createSpace();
37
+ afterTest(() => space.close());
37
38
 
38
- {
39
- const item = await space.database!.createItem<ObjectModel>({ type: 'test' });
40
- await item.model.set('name', 'test');
41
- }
39
+ await testLocalDatabase(space.dataPipelineController);
40
+ });
41
+
42
+ test('invitation with no auth', async () => {
43
+ const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
44
+
45
+ const space1 = await host.dataSpaceManager!.createSpace();
46
+ const spaceKey = space1.key;
47
+
48
+ await performInvitation(host.spaceInvitations!, guest.spaceInvitations!, space1);
42
49
 
43
50
  {
44
- const [item] = space.database!.select({ type: 'test' }).exec().entities;
45
- expect(item.model.get('name')).to.eq('test');
46
- }
51
+ const space1 = host.dataSpaceManager!.spaces.get(spaceKey)!;
52
+ const space2 = guest.dataSpaceManager!.spaces.get(spaceKey)!;
53
+ expect(space1).not.to.be.undefined;
54
+ expect(space2).not.to.be.undefined;
47
55
 
48
- await space.close();
56
+ await syncItems(space1.dataPipelineController, space2.dataPipelineController);
57
+
58
+ await space1.close();
59
+ await space2.close();
60
+ }
49
61
  });
50
62
 
51
63
  test('creates and accepts invitation with retry', async () => {
@@ -79,8 +91,8 @@ describe('services/space-invitations-handler', () => {
79
91
  complete2.wake(invitation.spaceKey!);
80
92
  },
81
93
  onCancelled: () => raise(new Error()),
82
- onTimeout: (err: Error) => raise(new Error(err.message)),
83
- onError: (err: Error) => raise(new Error(err.message))
94
+ onTimeout: (err: Error) => raise(err),
95
+ onError: (err: Error) => raise(err)
84
96
  });
85
97
  },
86
98
  onConnected: (invitation: Invitation) => {
@@ -91,8 +103,8 @@ describe('services/space-invitations-handler', () => {
91
103
  complete1.wake(invitation.spaceKey!);
92
104
  },
93
105
  onCancelled: () => raise(new Error()),
94
- onTimeout: (err: Error) => raise(new Error(err.message)),
95
- onError: (err: Error) => raise(new Error(err.message))
106
+ onTimeout: (err: Error) => raise(err),
107
+ onError: (err: Error) => raise(err)
96
108
  });
97
109
 
98
110
  const [spaceKey1, spaceKey2] = await Promise.all([complete1.wait(), complete2.wait()]);
@@ -104,11 +116,15 @@ describe('services/space-invitations-handler', () => {
104
116
  expect(space1).not.to.be.undefined;
105
117
  expect(space2).not.to.be.undefined;
106
118
 
107
- await syncItems(space1, space2);
119
+ await syncItems(space1.dataPipelineController, space2.dataPipelineController);
108
120
 
109
121
  await space1.close();
110
122
  await space2.close();
111
123
  }
124
+
125
+ expect(
126
+ guest.identityManager.identity?.space.spaceState.getCredentialsOfType('dxos.halo.credentials.SpaceMember').length
127
+ ).to.equal(2); // own halo + newly joined space.
112
128
  });
113
129
 
114
130
  test('cancels invitation', async () => {
@@ -133,8 +149,8 @@ describe('services/space-invitations-handler', () => {
133
149
  onConnected: async (invitation2: Invitation) => {},
134
150
  onSuccess: () => {},
135
151
  onCancelled: () => raise(new Error()),
136
- onTimeout: (err: Error) => raise(new Error(err.message)),
137
- onError: (err: Error) => raise(new Error(err.message))
152
+ onTimeout: (err: Error) => raise(err),
153
+ onError: (err: Error) => raise(err)
138
154
  });
139
155
  },
140
156
  onConnected: async (invitation1: Invitation) => {},
@@ -142,13 +158,13 @@ describe('services/space-invitations-handler', () => {
142
158
  cancelled.wake();
143
159
  },
144
160
  onSuccess: () => raise(new Error()),
145
- onTimeout: (err: Error) => raise(new Error(err.message)),
146
- onError: (err: Error) => raise(new Error(err.message))
161
+ onTimeout: (err: Error) => raise(err),
162
+ onError: (err: Error) => raise(err)
147
163
  });
148
164
 
149
165
  const invitation1 = await connecting1.wait();
150
166
  const invitation2 = await connecting2.wait();
151
- expect(invitation1.swarmKey).to.eq(invitation2.swarmKey);
167
+ expect(invitation1.swarmKey).to.eq(invitation2.swarmKey); // TODO(burdon): Normalize to use to.deep.eq.
152
168
 
153
169
  // TODO(burdon): Simulate network latency.
154
170
  setTimeout(async () => {
@@ -159,7 +175,8 @@ describe('services/space-invitations-handler', () => {
159
175
  await space1.close();
160
176
  });
161
177
 
162
- test('test multi-use invitation', async () => {
178
+ // TODO(burdon): Flaky.
179
+ test.skip('test multi-use invitation', async () => {
163
180
  const GUEST_COUNT = 3;
164
181
  const [host, ...guests] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(
165
182
  createPeers(GUEST_COUNT + 1)
@@ -180,8 +197,8 @@ describe('services/space-invitations-handler', () => {
180
197
  count();
181
198
  },
182
199
  onCancelled: () => {},
183
- onTimeout: (err: Error) => raise(new Error(err.message)),
184
- onError: (err: Error) => raise(new Error(err.message))
200
+ onTimeout: (err: Error) => raise(err),
201
+ onError: (err: Error) => raise(err)
185
202
  });
186
203
 
187
204
  await Promise.all(
@@ -198,8 +215,8 @@ describe('services/space-invitations-handler', () => {
198
215
  success.wake();
199
216
  },
200
217
  onCancelled: () => raise(new Error()),
201
- onTimeout: (err: Error) => raise(new Error(err.message)),
202
- onError: (err: Error) => raise(new Error(err.message))
218
+ onTimeout: (err: Error) => raise(err),
219
+ onError: (err: Error) => raise(err)
203
220
  });
204
221
  await success.wait({ timeout: 300 });
205
222
  })
@@ -15,6 +15,7 @@ import { log } from '@dxos/log';
15
15
  import { createTeleportProtocolFactory, NetworkManager, StarTopology, SwarmConnection } from '@dxos/network-manager';
16
16
  import { schema } from '@dxos/protocols';
17
17
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
18
+ import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
18
19
  import { ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
19
20
  import {
20
21
  AuthenticationRequest,
@@ -137,20 +138,17 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
137
138
 
138
139
  log('writing guest credentials', { host: this._signingContext.deviceKey, guest: deviceKey });
139
140
  // TODO(burdon): Check if already admitted.
140
- const credentials = await createAdmissionCredentials(
141
+ const credentials: FeedMessage.Payload[] = await createAdmissionCredentials(
141
142
  this._signingContext.credentialSigner,
142
143
  identityKey,
143
- deviceKey,
144
144
  space.key,
145
- controlFeedKey,
146
- dataFeedKey,
147
145
  space.inner.genesisFeedKey,
148
146
  guestProfile
149
147
  );
150
148
 
151
149
  // TODO(dmaretskyi): Refactor.
152
- assert(credentials[0]['@type'] === 'dxos.echo.feed.CredentialsMessage');
153
- const spaceMemberCredential = credentials[0].credential;
150
+ assert(credentials[0].credential);
151
+ const spaceMemberCredential = credentials[0].credential.credential;
154
152
  assert(getCredentialAssertion(spaceMemberCredential)['@type'] === 'dxos.halo.credentials.SpaceMember');
155
153
 
156
154
  await writeMessages(space.inner.controlPipeline.writer, credentials);
@@ -295,8 +293,6 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
295
293
  dataFeedKey
296
294
  });
297
295
 
298
- // TODO(dmaretskyi): Record credential in our HALO.
299
-
300
296
  // 4. Create local space.
301
297
  const assertion = getCredentialAssertion(credential);
302
298
  assert(assertion['@type'] === 'dxos.halo.credentials.SpaceMember', 'Invalid credential');
@@ -304,11 +300,12 @@ export class SpaceInvitationsHandler extends AbstractInvitationsHandler<DataSpac
304
300
 
305
301
  const space = await this._spaceManager.acceptSpace({
306
302
  spaceKey: assertion.spaceKey,
307
- genesisFeedKey: assertion.genesisFeedKey,
308
- controlFeedKey,
309
- dataFeedKey
303
+ genesisFeedKey: assertion.genesisFeedKey
310
304
  });
311
305
 
306
+ // Record credential in our HALO.
307
+ await this._signingContext.recordCredential(credential);
308
+
312
309
  // 5. Success.
313
310
  log('admitted by host', { guest: this._signingContext.deviceKey, spaceKey: space.key });
314
311
  complete.wake();
@@ -0,0 +1,56 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { MemorySignalManagerContext } from '@dxos/messaging';
6
+ import { describe, test } from '@dxos/test';
7
+
8
+ import { createServiceContext, syncItems } from '../testing';
9
+ import { performInvitation } from '../testing/invitation-utils';
10
+
11
+ describe('services/ServiceContext', () => {
12
+ test('existing space is synchronized on device invitations', async () => {
13
+ const networkContext = new MemorySignalManagerContext();
14
+ const device1 = createServiceContext({ signalContext: networkContext });
15
+ await device1.createIdentity();
16
+ const space1 = await device1.dataSpaceManager!.createSpace();
17
+
18
+ const device2 = createServiceContext({ signalContext: networkContext });
19
+ await performInvitation(device1.haloInvitations, device2.haloInvitations, undefined);
20
+
21
+ const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
22
+ await syncItems(space1.dataPipelineController, space2!.dataPipelineController);
23
+ });
24
+
25
+ test('new space is synchronized on device invitations', async () => {
26
+ const networkContext = new MemorySignalManagerContext();
27
+ const device1 = createServiceContext({ signalContext: networkContext });
28
+ await device1.createIdentity();
29
+
30
+ const device2 = createServiceContext({ signalContext: networkContext });
31
+ await performInvitation(device1.haloInvitations, device2.haloInvitations, undefined);
32
+
33
+ const space1 = await device1.dataSpaceManager!.createSpace();
34
+ await device2.dataSpaceManager?.updated.waitForCondition(() => !!device2.dataSpaceManager!.spaces.get(space1.key));
35
+ const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
36
+ await syncItems(space1.dataPipelineController, space2!.dataPipelineController);
37
+ });
38
+
39
+ test('joined space is synchronized on device invitations', async () => {
40
+ const networkContext = new MemorySignalManagerContext();
41
+ const device1 = createServiceContext({ signalContext: networkContext });
42
+ await device1.createIdentity();
43
+
44
+ const device2 = createServiceContext({ signalContext: networkContext });
45
+ await performInvitation(device1.haloInvitations, device2.haloInvitations, undefined);
46
+
47
+ const identity2 = createServiceContext({ signalContext: networkContext });
48
+ await identity2.createIdentity();
49
+ const space1 = await identity2.dataSpaceManager!.createSpace();
50
+ await performInvitation(identity2.spaceInvitations!, device1.spaceInvitations!, space1);
51
+
52
+ await device2.dataSpaceManager?.updated.waitForCondition(() => !!device2.dataSpaceManager!.spaces.get(space1.key));
53
+ const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
54
+ await syncItems(space1.dataPipelineController, space2!.dataPipelineController);
55
+ });
56
+ });
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Trigger } from '@dxos/async';
6
+ import { CredentialConsumer, getCredentialAssertion } from '@dxos/credentials';
6
7
  import { failUndefined } from '@dxos/debug';
7
8
  import {
8
9
  valueEncoding,
@@ -18,16 +19,16 @@ import { log } from '@dxos/log';
18
19
  import { ModelFactory } from '@dxos/model-factory';
19
20
  import { NetworkManager } from '@dxos/network-manager';
20
21
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
+ import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
21
23
  import { Storage } from '@dxos/random-access-storage';
22
24
 
23
- import { CreateIdentityOptions, IdentityManager } from '../identity';
25
+ import { CreateIdentityOptions, IdentityManager, JoinIdentityParams } from '../identity';
24
26
  import { HaloInvitationsHandler, SpaceInvitationsHandler } from '../invitations';
25
27
  import { DataSpaceManager } from '../spaces/data-space-manager';
26
28
 
27
29
  /**
28
30
  * Shared backend for all client services.
29
31
  */
30
-
31
32
  // TODO(burdon): Rename/break-up into smaller components. And/or make members private.
32
33
  export class ServiceContext {
33
34
  public readonly initialized = new Trigger();
@@ -40,6 +41,8 @@ export class ServiceContext {
40
41
  public readonly identityManager: IdentityManager;
41
42
  public readonly haloInvitations: HaloInvitationsHandler;
42
43
 
44
+ private _deviceSpaceSync?: CredentialConsumer<any>;
45
+
43
46
  // Initialized after identity is initialized.
44
47
  public dataSpaceManager?: DataSpaceManager;
45
48
  public spaceInvitations?: SpaceInvitationsHandler;
@@ -64,19 +67,27 @@ export class ServiceContext {
64
67
  }
65
68
  })
66
69
  });
70
+
67
71
  this.spaceManager = new SpaceManager({
68
72
  feedStore: this.feedStore,
69
73
  networkManager: this.networkManager
70
74
  });
75
+
71
76
  this.identityManager = new IdentityManager(
72
77
  this.metadataStore,
73
78
  this.keyring,
79
+ this.feedStore,
74
80
  this.spaceManager
75
81
  );
76
82
 
77
83
  // TODO(burdon): _initialize called in multiple places.
78
84
  // TODO(burdon): Call _initialize on success.
79
- this.haloInvitations = new HaloInvitationsHandler(this.networkManager, this.identityManager);
85
+ this.haloInvitations = new HaloInvitationsHandler({
86
+ networkManager: this.networkManager,
87
+ getIdentity: () => this.identityManager.identity ?? failUndefined(),
88
+ acceptIdentity: this._acceptIdentity.bind(this),
89
+ keyring: this.keyring
90
+ });
80
91
  }
81
92
 
82
93
  async open() {
@@ -92,6 +103,7 @@ export class ServiceContext {
92
103
 
93
104
  async close() {
94
105
  log('closing...');
106
+ await this._deviceSpaceSync?.close();
95
107
  await this.dataSpaceManager?.close();
96
108
  await this.identityManager.close();
97
109
  await this.spaceManager.close();
@@ -115,6 +127,13 @@ export class ServiceContext {
115
127
  return identity;
116
128
  }
117
129
 
130
+ private async _acceptIdentity(params: JoinIdentityParams) {
131
+ const identity = await this.identityManager.acceptIdentity(params);
132
+
133
+ await this._initialize();
134
+ return identity;
135
+ }
136
+
118
137
  // Called when identity is created.
119
138
  private async _initialize() {
120
139
  log('initializing spaces...');
@@ -123,7 +142,10 @@ export class ServiceContext {
123
142
  credentialSigner: identity.getIdentityCredentialSigner(),
124
143
  identityKey: identity.identityKey,
125
144
  deviceKey: identity.deviceKey,
126
- profile: identity.profileDocument
145
+ profile: identity.profileDocument,
146
+ recordCredential: async (credential) => {
147
+ await identity.controlPipeline.writer.write({ credential: { credential } });
148
+ }
127
149
  };
128
150
 
129
151
  this.dataSpaceManager = new DataSpaceManager(
@@ -133,9 +155,11 @@ export class ServiceContext {
133
155
  this.keyring,
134
156
  signingContext,
135
157
  this.modelFactory,
158
+ this.feedStore,
136
159
  this.snapshotStore
137
160
  );
138
161
  await this.dataSpaceManager.open();
162
+
139
163
  this.spaceInvitations = new SpaceInvitationsHandler(
140
164
  this.networkManager,
141
165
  this.dataSpaceManager,
@@ -143,5 +167,37 @@ export class ServiceContext {
143
167
  this.keyring
144
168
  );
145
169
  this.initialized.wake();
170
+
171
+ this._deviceSpaceSync = identity.space.spaceState.registerProcessor({
172
+ process: async (credential: Credential) => {
173
+ const assertion = getCredentialAssertion(credential);
174
+ if (assertion['@type'] !== 'dxos.halo.credentials.SpaceMember') {
175
+ return;
176
+ }
177
+ if (assertion.spaceKey.equals(identity.space.key)) {
178
+ // ignore halo space
179
+ return;
180
+ }
181
+ if (!this.dataSpaceManager) {
182
+ log('dataSpaceManager not initialized yet, ignoring space admission', { details: assertion });
183
+ return;
184
+ }
185
+ if (this.dataSpaceManager.spaces.has(assertion.spaceKey)) {
186
+ log('space already exists, ignoring space admission', { details: assertion });
187
+ return;
188
+ }
189
+
190
+ try {
191
+ log('accepting space recorded in halo', { details: assertion });
192
+ await this.dataSpaceManager.acceptSpace({
193
+ spaceKey: assertion.spaceKey,
194
+ genesisFeedKey: assertion.genesisFeedKey
195
+ });
196
+ } catch (err) {
197
+ log.catch(err);
198
+ }
199
+ }
200
+ });
201
+ await this._deviceSpaceSync.open();
146
202
  }
147
203
  }
@@ -42,7 +42,7 @@ export type ClientServices = {
42
42
  */
43
43
  export interface ClientServicesProvider {
44
44
  descriptors: ServiceBundle<ClientServices>;
45
- services: ClientServices;
45
+ services: Partial<ClientServices>;
46
46
 
47
47
  open(): Promise<void>;
48
48
  close(): Promise<void>;
@@ -2,12 +2,17 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { latch } from '@dxos/async';
5
+ import { expect } from 'chai';
6
+
7
+ import { latch, Trigger } from '@dxos/async';
6
8
  import { Config } from '@dxos/config';
9
+ import { verifyPresentation } from '@dxos/credentials';
10
+ import { PublicKey } from '@dxos/keys';
7
11
  import { MemorySignalManagerContext } from '@dxos/messaging';
12
+ import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
8
13
  import { afterTest, describe, test } from '@dxos/test';
9
14
 
10
- import { createServiceHost } from '../testing';
15
+ import { createMockCredential, createServiceHost } from '../testing';
11
16
 
12
17
  describe('ClientServicesHost', () => {
13
18
  test('queryCredentials', async () => {
@@ -15,10 +20,10 @@ describe('ClientServicesHost', () => {
15
20
  await host.open();
16
21
  afterTest(() => host.close());
17
22
 
18
- await host.services.ProfileService.createProfile({});
19
- const { publicKey: spaceKey } = await host.services.SpaceService.createSpace();
23
+ await host.services.ProfileService!.createProfile({});
24
+ const { publicKey: spaceKey } = await host.services.SpaceService!.createSpace();
20
25
 
21
- const stream = host.services.SpacesService.queryCredentials({ spaceKey });
26
+ const stream = host.services.SpacesService!.queryCredentials({ spaceKey });
22
27
  const [done, tick] = latch({ count: 3 });
23
28
  stream.subscribe((credential) => {
24
29
  tick();
@@ -28,4 +33,68 @@ describe('ClientServicesHost', () => {
28
33
 
29
34
  await done();
30
35
  });
36
+
37
+ test('write and query credentials', async () => {
38
+ const host = createServiceHost(new Config(), new MemorySignalManagerContext());
39
+ await host.open();
40
+ afterTest(() => host.close());
41
+
42
+ await host.services.ProfileService!.createProfile({});
43
+
44
+ const testCredential = await createMockCredential({
45
+ signer: host._serviceContext.keyring,
46
+ issuer: host._serviceContext.identityManager.identity!.deviceKey
47
+ });
48
+
49
+ // Test if Profile exposes haloSpace key.
50
+ const haloSpace = new Trigger<PublicKey>();
51
+ host.services.ProfileService!.subscribeProfile()!.subscribe(({ profile }) => {
52
+ if (profile?.haloSpace) {
53
+ haloSpace.wake(profile.haloSpace);
54
+ }
55
+ });
56
+
57
+ await host.services.SpacesService?.writeCredentials({
58
+ spaceKey: await haloSpace.wait(),
59
+ credentials: [testCredential]
60
+ });
61
+
62
+ const credentials = host.services.SpacesService!.queryCredentials({ spaceKey: await haloSpace.wait() });
63
+ const queriedCredential = new Trigger<Credential>();
64
+ credentials.subscribe((credential) => {
65
+ if (credential.subject.id.equals(testCredential.subject.id)) {
66
+ queriedCredential.wake(credential);
67
+ }
68
+ });
69
+ afterTest(() => credentials.close());
70
+
71
+ await queriedCredential.wait();
72
+ });
73
+
74
+ test('sign presentation', async () => {
75
+ const host = createServiceHost(new Config(), new MemorySignalManagerContext());
76
+ await host.open();
77
+ afterTest(() => host.close());
78
+
79
+ await host.services.ProfileService!.createProfile({});
80
+
81
+ const testCredential = await createMockCredential({
82
+ signer: host._serviceContext.keyring,
83
+ issuer: host._serviceContext.identityManager.identity!.deviceKey
84
+ });
85
+
86
+ const nonce = new Uint8Array([0, 0, 0, 0]);
87
+
88
+ const presentation = await host.services.ProfileService!.signPresentation({
89
+ presentation: {
90
+ credentials: [testCredential]
91
+ },
92
+ nonce
93
+ });
94
+
95
+ expect(presentation.proofs?.[0].nonce).to.deep.equal(nonce);
96
+ expect(await verifyPresentation(presentation)).to.deep.equal({
97
+ kind: 'pass'
98
+ });
99
+ });
31
100
  });