@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
@@ -2,17 +2,29 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import assert from 'node:assert';
6
-
7
5
  import { trackLeaks } from '@dxos/async';
8
6
  import { Context } from '@dxos/context';
9
- import { Database, DataPipelineControllerImpl, ISpace, MetadataStore, Space, SnapshotManager } from '@dxos/echo-db';
7
+ import { CredentialConsumer } from '@dxos/credentials';
8
+ import { timed } from '@dxos/debug';
9
+ import {
10
+ DataPipelineControllerImpl,
11
+ MetadataStore,
12
+ Space,
13
+ SnapshotManager,
14
+ SigningContext,
15
+ createMappedFeedWriter
16
+ } from '@dxos/echo-db';
17
+ import { FeedStore } from '@dxos/feed-store';
18
+ import { Keyring } from '@dxos/keyring';
10
19
  import { PublicKey } from '@dxos/keys';
11
20
  import { ModelFactory } from '@dxos/model-factory';
21
+ import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
+ import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
12
23
  import { Presence } from '@dxos/teleport-extension-presence';
13
24
  import { ComplexSet } from '@dxos/util';
14
25
 
15
26
  import { TrustedKeySetAuthVerifier } from '../identity';
27
+ import { NotarizationPlugin } from './notarization-plugin';
16
28
 
17
29
  const AUTH_TIMEOUT = 30000;
18
30
 
@@ -22,21 +34,35 @@ export type DataSpaceParams = {
22
34
  metadataStore: MetadataStore;
23
35
  snapshotManager: SnapshotManager;
24
36
  presence: Presence;
37
+ keyring: Keyring;
38
+ feedStore: FeedStore<FeedMessage>;
39
+ signingContext: SigningContext;
25
40
  memberKey: PublicKey;
26
41
  snapshotId?: string | undefined;
27
42
  };
28
43
 
44
+ const CONTROL_PIPELINE_READY_TIMEFRAME = 3000;
29
45
  @trackLeaks('open', 'close')
30
- export class DataSpace implements ISpace {
46
+ export class DataSpace {
31
47
  private readonly _ctx = new Context();
32
48
  private readonly _dataPipelineController: DataPipelineControllerImpl;
33
49
  private readonly _inner: Space;
34
50
  private readonly _presence: Presence;
51
+ private readonly _keyring: Keyring;
52
+ private readonly _feedStore: FeedStore<FeedMessage>;
53
+ private readonly _metadataStore: MetadataStore;
54
+ private readonly _signingContext: SigningContext;
55
+ private readonly _notarizationPluginConsumer: CredentialConsumer<NotarizationPlugin>;
56
+
35
57
  public readonly authVerifier: TrustedKeySetAuthVerifier;
36
58
 
37
59
  constructor(params: DataSpaceParams) {
38
60
  this._inner = params.inner;
39
61
  this._presence = params.presence;
62
+ this._keyring = params.keyring;
63
+ this._feedStore = params.feedStore;
64
+ this._metadataStore = params.metadataStore;
65
+ this._signingContext = params.signingContext;
40
66
  this._dataPipelineController = new DataPipelineControllerImpl({
41
67
  modelFactory: params.modelFactory,
42
68
  metadataStore: params.metadataStore,
@@ -46,32 +72,31 @@ export class DataSpace implements ISpace {
46
72
  feedInfoProvider: (feedKey) => this._inner.spaceState.feeds.get(feedKey),
47
73
  snapshotId: params.snapshotId
48
74
  });
75
+
49
76
  this.authVerifier = new TrustedKeySetAuthVerifier({
50
77
  trustedKeysProvider: () => new ComplexSet(PublicKey.hash, Array.from(this._inner.spaceState.members.keys())),
51
78
  update: this._inner.stateUpdate,
52
79
  authTimeout: AUTH_TIMEOUT
53
80
  });
81
+
82
+ this._notarizationPluginConsumer = this._inner.spaceState.registerProcessor(new NotarizationPlugin());
54
83
  }
55
84
 
56
85
  get key() {
57
86
  return this._inner.key;
58
87
  }
59
88
 
60
- get inner() {
61
- return this._inner;
62
- }
63
-
64
89
  get isOpen() {
65
90
  return this._inner.isOpen;
66
91
  }
67
92
 
68
- get dataPipelineController(): DataPipelineControllerImpl {
69
- return this._dataPipelineController;
93
+ // TODO(burdon): Can we mark this for debugging only?
94
+ get inner() {
95
+ return this._inner;
70
96
  }
71
97
 
72
- get database(): Database {
73
- assert(this._dataPipelineController.database);
74
- return this._dataPipelineController.database;
98
+ get dataPipelineController(): DataPipelineControllerImpl {
99
+ return this._dataPipelineController;
75
100
  }
76
101
 
77
102
  get stateUpdate() {
@@ -82,15 +107,89 @@ export class DataSpace implements ISpace {
82
107
  return this._presence;
83
108
  }
84
109
 
110
+ get notarizationPlugin() {
111
+ return this._notarizationPluginConsumer.processor;
112
+ }
113
+
85
114
  async open() {
115
+ await this.notarizationPlugin.open();
116
+ await this._notarizationPluginConsumer.open();
86
117
  await this._inner.open();
87
- await this._inner.initDataPipeline(this._dataPipelineController);
88
118
  }
89
119
 
90
120
  async close() {
91
121
  await this._ctx.dispose();
122
+
92
123
  await this.authVerifier.close();
124
+
93
125
  await this._inner.close();
126
+ await this._notarizationPluginConsumer.close();
127
+ await this.notarizationPlugin.close();
128
+
94
129
  await this._presence.destroy();
95
130
  }
131
+
132
+ async initializeDataPipeline() {
133
+ await this._inner.controlPipeline.state.waitUntilReachedTargetTimeframe({
134
+ timeout: CONTROL_PIPELINE_READY_TIMEFRAME
135
+ });
136
+
137
+ await this._createWritableFeeds();
138
+ this.notarizationPlugin.setWriter(
139
+ createMappedFeedWriter<Credential, FeedMessage.Payload>(
140
+ (credential) => ({
141
+ credential: { credential }
142
+ }),
143
+ this._inner.controlPipeline.writer
144
+ )
145
+ );
146
+
147
+ await this._inner.initDataPipeline(this._dataPipelineController);
148
+ }
149
+
150
+ @timed(10_000)
151
+ private async _createWritableFeeds() {
152
+ const credentials: Credential[] = [];
153
+ if (!this.inner.controlFeedKey) {
154
+ const controlFeed = await this._feedStore.openFeed(await this._keyring.createKey(), { writable: true });
155
+ this.inner.setControlFeed(controlFeed);
156
+
157
+ credentials.push(
158
+ await this._signingContext.credentialSigner.createCredential({
159
+ subject: controlFeed.key,
160
+ assertion: {
161
+ '@type': 'dxos.halo.credentials.AdmittedFeed',
162
+ spaceKey: this.key,
163
+ deviceKey: this._signingContext.deviceKey,
164
+ identityKey: this._signingContext.identityKey,
165
+ designation: AdmittedFeed.Designation.CONTROL
166
+ }
167
+ })
168
+ );
169
+ }
170
+ if (!this.inner.dataFeedKey) {
171
+ const dataFeed = await this._feedStore.openFeed(await this._keyring.createKey(), { writable: true });
172
+ this.inner.setDataFeed(dataFeed);
173
+
174
+ credentials.push(
175
+ await this._signingContext.credentialSigner.createCredential({
176
+ subject: dataFeed.key,
177
+ assertion: {
178
+ '@type': 'dxos.halo.credentials.AdmittedFeed',
179
+ spaceKey: this.key,
180
+ deviceKey: this._signingContext.deviceKey,
181
+ identityKey: this._signingContext.identityKey,
182
+ designation: AdmittedFeed.Designation.DATA
183
+ }
184
+ })
185
+ );
186
+ }
187
+
188
+ if (credentials.length > 0) {
189
+ await this.notarizationPlugin.notarize(credentials);
190
+ }
191
+
192
+ // Set this after credentials are notarized so that on failure we will retry.
193
+ await this._metadataStore.setWritableFeedKeys(this.key, this.inner.controlFeedKey!, this.inner.dataFeedKey!);
194
+ }
96
195
  }
@@ -0,0 +1,81 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { Context } from '@dxos/context';
8
+ import { CredentialGenerator } from '@dxos/credentials';
9
+ import { MockFeedWriter } from '@dxos/feed-store/testing';
10
+ import { Keyring } from '@dxos/keyring';
11
+ import { log } from '@dxos/log';
12
+ import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
13
+ import { TestBuilder, TestConnection, TestPeer } from '@dxos/teleport/testing';
14
+ import { afterTest, describe, test } from '@dxos/test';
15
+
16
+ import { NotarizationPlugin } from './notarization-plugin';
17
+
18
+ class TestAgent extends TestPeer {
19
+ private readonly _ctx = new Context();
20
+
21
+ feed = new MockFeedWriter<Credential>();
22
+ notarizationPlugin = new NotarizationPlugin();
23
+
24
+ constructor() {
25
+ super();
26
+ this.feed.written.on(this._ctx, async ([credential]) => {
27
+ log('written to feed', { credential });
28
+ await this.notarizationPlugin.process(credential);
29
+ });
30
+ }
31
+
32
+ enableWriting() {
33
+ this.notarizationPlugin.setWriter(this.feed);
34
+ }
35
+
36
+ protected override onOpen(connection: TestConnection): Promise<void> {
37
+ log('onOpen');
38
+ connection.teleport.addExtension('dxos.mesh.teleport.notarization', this.notarizationPlugin.createExtension());
39
+ return super.onOpen(connection);
40
+ }
41
+
42
+ override async destroy(): Promise<void> {
43
+ await this._ctx.dispose();
44
+ await super.destroy();
45
+ await this.notarizationPlugin.close();
46
+ }
47
+ }
48
+
49
+ describe('NotarizationPlugin', () => {
50
+ test('notarize single credential', async () => {
51
+ const testBuilder = new TestBuilder();
52
+ afterTest(() => testBuilder.destroy());
53
+
54
+ // peer0 is there to test retries.
55
+ const [_peer0, peer1, peer2] = await testBuilder.createPeers({ factory: () => new TestAgent() });
56
+ peer1.enableWriting();
57
+
58
+ peer1.feed.written.on(async ([credential]) => {
59
+ log('mock replication', { credential });
60
+ await peer2.feed.write(credential);
61
+ });
62
+
63
+ await testBuilder.connect(_peer0, peer2);
64
+
65
+ const keyring = new Keyring();
66
+ const generator = new CredentialGenerator(keyring, await keyring.createKey(), await keyring.createKey());
67
+ const credential = await generator.createFeedAdmission(
68
+ await keyring.createKey(),
69
+ await keyring.createKey(),
70
+ AdmittedFeed.Designation.CONTROL
71
+ );
72
+
73
+ const notarized = peer2.notarizationPlugin.notarize([credential]);
74
+
75
+ await testBuilder.connect(peer1, peer2);
76
+ await notarized;
77
+
78
+ expect(peer1.feed.messages.map((c) => c.id)).to.deep.eq([credential.id]);
79
+ expect(peer2.feed.messages.map((c) => c.id)).to.deep.eq([credential.id]);
80
+ });
81
+ });
@@ -0,0 +1,211 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import assert from 'node:assert';
6
+
7
+ import { DeferredTask, Event, scheduleTask, TimeoutError, Trigger } from '@dxos/async';
8
+ import { Context } from '@dxos/context';
9
+ import { CredentialProcessor } from '@dxos/credentials';
10
+ import { FeedWriter } from '@dxos/feed-store';
11
+ import { PublicKey } from '@dxos/keys';
12
+ import { log } from '@dxos/log';
13
+ import { schema } from '@dxos/protocols';
14
+ import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
15
+ import { NotarizationService, NotarizeRequest } from '@dxos/protocols/proto/dxos/mesh/teleport/notarization';
16
+ import { ExtensionContext, RpcExtension } from '@dxos/teleport';
17
+ import { ComplexMap, ComplexSet, entry } from '@dxos/util';
18
+
19
+ const RETRY_TIMEOUT = 1000;
20
+ const NOTARIZE_TIMEOUT = 10_000;
21
+
22
+ /**
23
+ * See NotarizationService proto.
24
+ */
25
+ export class NotarizationPlugin implements CredentialProcessor {
26
+ private readonly _ctx = new Context();
27
+ private readonly _extensionOpened = new Event();
28
+
29
+ private _writer: FeedWriter<Credential> | undefined;
30
+ private readonly _extensions = new Set<NotarizationTeleportExtension>();
31
+ private readonly _processedCredentials = new ComplexSet<PublicKey>(PublicKey.hash);
32
+ private readonly _processCredentialsTriggers = new ComplexMap<PublicKey, Trigger>(PublicKey.hash);
33
+
34
+ async open() {}
35
+
36
+ async close() {
37
+ await this._ctx.dispose();
38
+ }
39
+
40
+ /**
41
+ * Request credentials to be notarized.
42
+ */
43
+ async notarize(credentials: Credential[]) {
44
+ log('notarize', { credentials });
45
+ assert(
46
+ credentials.every((credential) => credential.id),
47
+ 'Credentials must have an id'
48
+ );
49
+
50
+ const errors = new Trigger();
51
+ const ctx = this._ctx.derive({
52
+ onError: (err) => {
53
+ log.warn('Notarization error', { err });
54
+ void ctx.dispose();
55
+ errors.throw(err);
56
+ }
57
+ });
58
+ scheduleTask(
59
+ ctx,
60
+ () => {
61
+ log.warn('Notarization timeout');
62
+ void ctx.dispose();
63
+ errors.throw(new TimeoutError(NOTARIZE_TIMEOUT, 'Notarization timed out'));
64
+ },
65
+ NOTARIZE_TIMEOUT
66
+ );
67
+
68
+ const allNotarized = Promise.all(credentials.map((credential) => this._waitUntilProcessed(credential.id!)));
69
+
70
+ const peersTried = new Set<NotarizationTeleportExtension>();
71
+
72
+ // Repeatable task that tries to notarize credentials with one of the available peers.
73
+ const notarizeTask = new DeferredTask(ctx, async () => {
74
+ try {
75
+ if (this._extensions.size === 0) {
76
+ log.warn('No peers to notarize with');
77
+ return; // No peers to try.
78
+ }
79
+
80
+ // Pick a peer that we haven't tried yet.
81
+ const peer = [...this._extensions].find((peer) => !peersTried.has(peer));
82
+ if (!peer) {
83
+ log.warn('Exhausted all peers to notarize with', { retryIn: RETRY_TIMEOUT });
84
+ peersTried.clear();
85
+ scheduleTask(ctx, () => notarizeTask.schedule(), RETRY_TIMEOUT); // retry with all peers again
86
+ return;
87
+ }
88
+
89
+ peersTried.add(peer);
90
+ log('try notarizing', { peer: peer.localPeerId, credentialId: credentials.map((credential) => credential.id) });
91
+ await peer.rpc.NotarizationService.notarize({
92
+ credentials: credentials.filter((credential) => !this._processedCredentials.has(credential.id!))
93
+ });
94
+ log('success');
95
+ } catch (err) {
96
+ log.warn('error notarizing (recoverable)', err);
97
+ notarizeTask.schedule(); // retry immediately with next peer
98
+ }
99
+ });
100
+
101
+ notarizeTask.schedule();
102
+ this._extensionOpened.on(ctx, () => notarizeTask.schedule());
103
+
104
+ try {
105
+ // TODO(dmaretskyi): Abort (context) & timeout.
106
+ await Promise.race([allNotarized, errors.wait()]);
107
+ log('done');
108
+ } finally {
109
+ await ctx.dispose();
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Called with credentials arriving from the control pipeline.
115
+ */
116
+ async process(credential: Credential) {
117
+ if (!credential.id) {
118
+ return;
119
+ }
120
+ this._processCredentialsTriggers.get(credential.id)?.wake();
121
+ this._processedCredentials.add(credential.id);
122
+ this._processCredentialsTriggers.delete(credential.id);
123
+ }
124
+
125
+ setWriter(writer: FeedWriter<Credential>) {
126
+ assert(!this._writer, 'Writer already set.');
127
+ this._writer = writer;
128
+ }
129
+
130
+ private async _waitUntilProcessed(id: PublicKey) {
131
+ if (this._processedCredentials.has(id)) {
132
+ return;
133
+ }
134
+ await entry(this._processCredentialsTriggers, id).orInsert(new Trigger()).value.wait();
135
+ }
136
+
137
+ /**
138
+ * Requests from other peers to notarize credentials.
139
+ */
140
+ private async _onNotarize(request: NotarizeRequest) {
141
+ if (!this._writer) {
142
+ throw new Error('Writer not set');
143
+ }
144
+ for (const credential of request.credentials ?? []) {
145
+ assert(credential.id, 'Credential must have an id');
146
+ if (this._processedCredentials.has(credential.id)) {
147
+ continue;
148
+ }
149
+ await this._writer.write(credential);
150
+ }
151
+ }
152
+
153
+ createExtension() {
154
+ const extension = new NotarizationTeleportExtension({
155
+ onOpen: async () => {
156
+ log('extension opened', { peer: extension.localPeerId });
157
+ this._extensions.add(extension);
158
+ this._extensionOpened.emit();
159
+ },
160
+ onClose: async () => {
161
+ log('extension closed', { peer: extension.localPeerId });
162
+ this._extensions.delete(extension);
163
+ },
164
+ onNotarize: this._onNotarize.bind(this)
165
+ });
166
+ return extension;
167
+ }
168
+ }
169
+
170
+ export type NotarizationTeleportExtensionParams = {
171
+ onOpen: () => Promise<void>;
172
+ onClose: () => Promise<void>;
173
+ onNotarize: (request: NotarizeRequest) => Promise<void>;
174
+ };
175
+
176
+ export class NotarizationTeleportExtension extends RpcExtension<Services, Services> {
177
+ constructor(private readonly _params: NotarizationTeleportExtensionParams) {
178
+ super({
179
+ requested: {
180
+ NotarizationService: schema.getService('dxos.mesh.teleport.notarization.NotarizationService')
181
+ },
182
+ exposed: {
183
+ NotarizationService: schema.getService('dxos.mesh.teleport.notarization.NotarizationService')
184
+ }
185
+ });
186
+ }
187
+
188
+ protected async getHandlers(): Promise<Services> {
189
+ return {
190
+ NotarizationService: {
191
+ notarize: async (request) => {
192
+ await this._params.onNotarize(request);
193
+ }
194
+ }
195
+ };
196
+ }
197
+
198
+ override async onOpen(ctx: ExtensionContext) {
199
+ await super.onOpen(ctx);
200
+ await this._params.onOpen();
201
+ }
202
+
203
+ override async onClose(err?: Error | undefined) {
204
+ await this._params.onClose();
205
+ await super.onClose(err);
206
+ }
207
+ }
208
+
209
+ type Services = {
210
+ NotarizationService: NotarizationService;
211
+ };
@@ -11,7 +11,8 @@ import {
11
11
  QueryMembersRequest,
12
12
  QueryMembersResponse,
13
13
  Space,
14
- SpacesService
14
+ SpacesService,
15
+ WriteCredentialsRequest
15
16
  } from '@dxos/protocols/proto/dxos/client/services';
16
17
  import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
17
18
 
@@ -46,4 +47,11 @@ export class SpacesServiceImpl implements SpacesService {
46
47
  scheduleTask(ctx, () => processor.open());
47
48
  });
48
49
  }
50
+
51
+ async writeCredentials({ spaceKey, credentials }: WriteCredentialsRequest) {
52
+ const space = this._spaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
53
+ for (const credential of credentials ?? []) {
54
+ await space.controlPipeline.writer.write({ credential: { credential } });
55
+ }
56
+ }
49
57
  }
@@ -0,0 +1,28 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { createCredential } from '@dxos/credentials';
6
+ import { Signer } from '@dxos/crypto';
7
+ import { PublicKey } from '@dxos/keys';
8
+ import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
9
+
10
+ export const createMockCredential = async ({
11
+ signer,
12
+ issuer
13
+ }: {
14
+ signer: Signer;
15
+ issuer: PublicKey;
16
+ }): Promise<Credential> =>
17
+ createCredential({
18
+ signer,
19
+ issuer,
20
+ subject: new PublicKey(Buffer.from('test')),
21
+ assertion: {
22
+ '@type': 'example.testing.rpc.MessageWithAny',
23
+ payload: {
24
+ '@type': 'google.protobuf.Any',
25
+ value: Buffer.from('test')
26
+ }
27
+ }
28
+ });
@@ -2,4 +2,6 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ export * from './credential-utils';
5
6
  export * from './test-builder';
7
+ export * from './invitation-utils';
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import assert from 'node:assert';
6
+
7
+ import { Trigger } from '@dxos/async';
8
+ import { raise } from '@dxos/debug';
9
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
10
+
11
+ import { InvitationsHandler } from '../invitations';
12
+
13
+ export const performInvitation = async <T>(host: InvitationsHandler<T>, guest: InvitationsHandler<T>, context: T) => {
14
+ const done1 = new Trigger<Invitation>(); // peer 1 connected.
15
+ const done2 = new Trigger<Invitation>(); // peer 2 connected.
16
+ const observable1 = await host.createInvitation(context, { type: Invitation.Type.INTERACTIVE_TESTING });
17
+ observable1.subscribe({
18
+ onConnecting: async (invitation1: Invitation) => {
19
+ const observable2 = await guest.acceptInvitation(invitation1, { type: Invitation.Type.INTERACTIVE_TESTING });
20
+ observable2.subscribe({
21
+ onConnecting: async (invitation2: Invitation) => {
22
+ assert(invitation1.swarmKey!.equals(invitation2.swarmKey!));
23
+ },
24
+ onConnected: async (invitation2: Invitation) => {},
25
+ onSuccess: (invitation) => {
26
+ done2.wake(invitation);
27
+ },
28
+ onCancelled: () => raise(new Error()),
29
+ onTimeout: (err: Error) => raise(err),
30
+ onError: (err: Error) => raise(err)
31
+ });
32
+ },
33
+ onConnected: async (invitation1: Invitation) => {},
34
+ onCancelled: () => raise(new Error('cancelled')),
35
+ onSuccess: (invitation) => {
36
+ done1.wake(invitation);
37
+ },
38
+ onTimeout: (err: Error) => raise(err),
39
+ onError: (err: Error) => raise(err)
40
+ });
41
+
42
+ await done1.wait();
43
+ await done2.wait();
44
+ };
@@ -2,11 +2,17 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
6
-
7
5
  import { Config } from '@dxos/config';
8
6
  import { createCredentialSignerWithChain, CredentialGenerator } from '@dxos/credentials';
9
- import { ISpace, MetadataStore, SigningContext, SnapshotStore, SpaceManager, valueEncoding } from '@dxos/echo-db';
7
+ import {
8
+ DataPipelineControllerImpl,
9
+ MetadataStore,
10
+ SigningContext,
11
+ SnapshotStore,
12
+ SpaceManager,
13
+ valueEncoding
14
+ } from '@dxos/echo-db';
15
+ import { testLocalDatabase } from '@dxos/echo-db/testing';
10
16
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
11
17
  import { Keyring } from '@dxos/keyring';
12
18
  import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
@@ -66,20 +72,9 @@ export const createIdentity = async (peer: ServiceContext) => {
66
72
 
67
73
  // TODO(burdon): Remove @dxos/client-testing.
68
74
  // TODO(burdon): Create builder and make configurable.
69
- export const syncItems = async (space1: ISpace, space2: ISpace) => {
70
- {
71
- // Check item replicated from 1 => 2.
72
- const item1 = await space1.database!.createItem({ type: 'type-1' });
73
- const item2 = await space2.database!.waitForItem({ type: 'type-1' });
74
- expect(item1.id).to.eq(item2.id);
75
- }
76
-
77
- {
78
- // Check item replicated from 2 => 1.
79
- const item1 = await space2.database!.createItem({ type: 'type-2' });
80
- const item2 = await space1.database!.waitForItem({ type: 'type-2' });
81
- expect(item1.id).to.eq(item2.id);
82
- }
75
+ export const syncItems = async (db1: DataPipelineControllerImpl, db2: DataPipelineControllerImpl) => {
76
+ await testLocalDatabase(db1, db2);
77
+ await testLocalDatabase(db2, db1);
83
78
  };
84
79
 
85
80
  export class TestBuilder {
@@ -161,6 +156,7 @@ export const createSigningContext = async (keyring: Keyring): Promise<SigningCon
161
156
  credential: await new CredentialGenerator(keyring, identityKey, deviceKey).createDeviceAuthorization(deviceKey)
162
157
  },
163
158
  deviceKey
164
- )
159
+ ),
160
+ recordCredential: async () => {} // No-op.
165
161
  };
166
162
  };