@dxos/client-services 0.1.22 → 0.1.24-next.6b1f434

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 (129) hide show
  1. package/dist/lib/browser/chunk-ZPZBLOQW.mjs +4086 -0
  2. package/dist/lib/browser/chunk-ZPZBLOQW.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +68 -3027
  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 +1694 -667
  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 +3806 -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 +9 -9
  25. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  26. package/dist/types/src/packlets/identity/identity-manager.d.ts +8 -2
  27. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  28. package/dist/types/src/packlets/identity/identity.d.ts +5 -4
  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 +1 -1
  35. package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
  36. package/dist/types/src/packlets/services/service-context.d.ts +3 -1
  37. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  38. package/dist/types/src/packlets/services/service-context.test.d.ts +2 -0
  39. package/dist/types/src/packlets/services/service-context.test.d.ts.map +1 -0
  40. package/dist/types/src/packlets/services/service-definitions.d.ts +1 -1
  41. package/dist/types/src/packlets/services/service-definitions.d.ts.map +1 -1
  42. package/dist/types/src/packlets/services/service-host.d.ts +9 -6
  43. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  44. package/dist/types/src/packlets/services/service-registry.d.ts +4 -3
  45. package/dist/types/src/packlets/services/service-registry.d.ts.map +1 -1
  46. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +9 -2
  47. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  48. package/dist/types/src/packlets/spaces/data-space-manager.test.d.ts +2 -0
  49. package/dist/types/src/packlets/spaces/data-space-manager.test.d.ts.map +1 -0
  50. package/dist/types/src/packlets/spaces/data-space.d.ts +20 -4
  51. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  52. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +50 -0
  53. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -0
  54. package/dist/types/src/packlets/spaces/notarization-plugin.test.d.ts +2 -0
  55. package/dist/types/src/packlets/spaces/notarization-plugin.test.d.ts.map +1 -0
  56. package/dist/types/src/packlets/spaces/spaces-service.d.ts +3 -2
  57. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  58. package/dist/types/src/packlets/testing/credential-utils.d.ts +8 -0
  59. package/dist/types/src/packlets/testing/credential-utils.d.ts.map +1 -0
  60. package/dist/types/src/packlets/testing/index.d.ts +2 -0
  61. package/dist/types/src/packlets/testing/index.d.ts.map +1 -1
  62. package/dist/types/src/packlets/testing/invitation-utils.d.ts +3 -0
  63. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -0
  64. package/dist/types/src/packlets/testing/test-builder.d.ts +31 -2
  65. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  66. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts +35 -0
  67. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts.map +1 -0
  68. package/dist/types/src/packlets/vault/{iframe-runtime.d.ts → iframe-proxy-runtime.d.ts} +9 -4
  69. package/dist/types/src/packlets/vault/iframe-proxy-runtime.d.ts.map +1 -0
  70. package/dist/types/src/packlets/vault/index.d.ts +6 -1
  71. package/dist/types/src/packlets/vault/index.d.ts.map +1 -1
  72. package/dist/types/src/packlets/vault/services.d.ts +9 -1
  73. package/dist/types/src/packlets/vault/services.d.ts.map +1 -1
  74. package/dist/types/src/packlets/vault/shell-controller.d.ts +24 -0
  75. package/dist/types/src/packlets/vault/shell-controller.d.ts.map +1 -0
  76. package/dist/types/src/packlets/vault/shell-runtime.d.ts +45 -0
  77. package/dist/types/src/packlets/vault/shell-runtime.d.ts.map +1 -0
  78. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts +19 -0
  79. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts.map +1 -0
  80. package/dist/types/src/packlets/vault/worker-runtime.d.ts +2 -1
  81. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  82. package/dist/types/src/packlets/vault/worker-session.d.ts +7 -4
  83. package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
  84. package/package.json +30 -29
  85. package/src/packlets/deprecated/profile.ts +20 -2
  86. package/src/packlets/deprecated/space.ts +41 -37
  87. package/src/packlets/deprecated/system.ts +26 -13
  88. package/src/packlets/devtools/devtools.ts +4 -4
  89. package/src/packlets/devtools/feeds.ts +11 -6
  90. package/src/packlets/devtools/network.ts +20 -9
  91. package/src/packlets/devtools/spaces.ts +8 -7
  92. package/src/packlets/identity/authenticator.test.ts +4 -4
  93. package/src/packlets/identity/authenticator.ts +27 -17
  94. package/src/packlets/identity/identity-manager.test.ts +25 -16
  95. package/src/packlets/identity/identity-manager.ts +25 -13
  96. package/src/packlets/identity/identity.test.ts +22 -23
  97. package/src/packlets/identity/identity.ts +15 -10
  98. package/src/packlets/invitations/halo-invitations-handler.test.ts +4 -45
  99. package/src/packlets/invitations/halo-invitations-handler.ts +33 -17
  100. package/src/packlets/invitations/invitations.ts +2 -1
  101. package/src/packlets/invitations/space-invitations-handler.test.ts +43 -26
  102. package/src/packlets/invitations/space-invitations-handler.ts +9 -12
  103. package/src/packlets/services/service-context.test.ts +56 -0
  104. package/src/packlets/services/service-context.ts +62 -10
  105. package/src/packlets/services/service-definitions.ts +1 -1
  106. package/src/packlets/services/service-host.test.ts +74 -5
  107. package/src/packlets/services/service-host.ts +86 -24
  108. package/src/packlets/services/service-registry.test.ts +2 -1
  109. package/src/packlets/services/service-registry.ts +5 -1
  110. package/src/packlets/spaces/data-space-manager.test.ts +131 -0
  111. package/src/packlets/spaces/data-space-manager.ts +61 -15
  112. package/src/packlets/spaces/data-space.ts +125 -14
  113. package/src/packlets/spaces/notarization-plugin.test.ts +81 -0
  114. package/src/packlets/spaces/notarization-plugin.ts +211 -0
  115. package/src/packlets/spaces/spaces-service.ts +10 -2
  116. package/src/packlets/testing/credential-utils.ts +28 -0
  117. package/src/packlets/testing/index.ts +2 -0
  118. package/src/packlets/testing/invitation-utils.ts +44 -0
  119. package/src/packlets/testing/test-builder.ts +96 -14
  120. package/src/packlets/vault/iframe-host-runtime.ts +131 -0
  121. package/src/packlets/vault/{iframe-runtime.ts → iframe-proxy-runtime.ts} +18 -3
  122. package/src/packlets/vault/index.ts +6 -1
  123. package/src/packlets/vault/services.ts +17 -1
  124. package/src/packlets/vault/shell-controller.ts +77 -0
  125. package/src/packlets/vault/shell-runtime.ts +133 -0
  126. package/src/packlets/vault/vault-resource-lock.ts +76 -0
  127. package/src/packlets/vault/worker-runtime.ts +16 -4
  128. package/src/packlets/vault/worker-session.ts +36 -26
  129. package/dist/types/src/packlets/vault/iframe-runtime.d.ts.map +0 -1
@@ -2,29 +2,41 @@
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';
9
+ import { getCredentialAssertion } from '@dxos/credentials';
7
10
  import {
8
- AcceptSpaceOptions,
9
- DataServiceSubscriptions,
10
11
  MetadataStore,
11
12
  SigningContext,
12
- SnapshotStore,
13
13
  Space,
14
14
  spaceGenesis,
15
15
  SpaceManager,
16
- SnapshotManager
17
- } from '@dxos/echo-db';
16
+ DataServiceSubscriptions,
17
+ SnapshotManager,
18
+ SnapshotStore
19
+ } from '@dxos/echo-pipeline';
20
+ import { FeedStore } from '@dxos/feed-store';
18
21
  import { Keyring } from '@dxos/keyring';
19
22
  import { PublicKey } from '@dxos/keys';
20
23
  import { log } from '@dxos/log';
21
24
  import { ModelFactory } from '@dxos/model-factory';
25
+ import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
26
  import { SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
23
27
  import { Presence } from '@dxos/teleport-extension-presence';
24
- import { ComplexMap } from '@dxos/util';
28
+ import { ComplexMap, deferFunction } from '@dxos/util';
25
29
 
30
+ import { createAuthProvider } from '../identity';
26
31
  import { DataSpace } from './data-space';
27
32
 
33
+ const DATA_PIPELINE_READY_TIMEOUT = 3_000;
34
+
35
+ export type AcceptSpaceOptions = {
36
+ spaceKey: PublicKey;
37
+ genesisFeedKey: PublicKey;
38
+ };
39
+
28
40
  @trackLeaks('open', 'close')
29
41
  export class DataSpaceManager {
30
42
  private readonly _ctx = new Context();
@@ -40,6 +52,7 @@ export class DataSpaceManager {
40
52
  private readonly _keyring: Keyring,
41
53
  private readonly _signingContext: SigningContext,
42
54
  private readonly _modelFactory: ModelFactory,
55
+ private readonly _feedStore: FeedStore<FeedMessage>,
43
56
  private readonly _snapshotStore: SnapshotStore
44
57
  ) {}
45
58
 
@@ -56,10 +69,18 @@ export class DataSpaceManager {
56
69
  for (const spaceMetadata of this._metadataStore.spaces) {
57
70
  log('load space', { spaceMetadata });
58
71
  const space = await this._constructSpace(spaceMetadata);
59
- if (spaceMetadata.latestTimeframe) {
72
+ await space.initializeDataPipeline();
73
+ if (spaceMetadata.dataTimeframe) {
60
74
  log('waiting for latest timeframe', { spaceMetadata });
61
- await space.dataPipelineController.waitUntilTimeframe(spaceMetadata.latestTimeframe);
75
+ await space.dataPipelineController.pipelineState!.setTargetTimeframe(spaceMetadata.dataTimeframe);
62
76
  }
77
+ await space.dataPipelineController.pipelineState!.waitUntilReachedTargetTimeframe({
78
+ timeout: DATA_PIPELINE_READY_TIMEOUT
79
+ });
80
+ this._dataServiceSubscriptions.registerSpace(
81
+ space.key,
82
+ space.dataPipelineController.databaseBackend!.createDataServiceHost()
83
+ );
63
84
  }
64
85
  }
65
86
 
@@ -89,9 +110,19 @@ export class DataSpaceManager {
89
110
  log('creating space...', { spaceKey });
90
111
  const space = await this._constructSpace(metadata);
91
112
 
92
- await spaceGenesis(this._keyring, this._signingContext, space.inner);
113
+ const credentials = await spaceGenesis(this._keyring, this._signingContext, space.inner);
93
114
  await this._metadataStore.addSpace(metadata);
94
115
 
116
+ const memberCredential = credentials[1];
117
+ assert(getCredentialAssertion(memberCredential)['@type'] === 'dxos.halo.credentials.SpaceMember');
118
+ await this._signingContext.recordCredential(memberCredential);
119
+
120
+ await space.initializeDataPipeline();
121
+ this._dataServiceSubscriptions.registerSpace(
122
+ space.key,
123
+ space.dataPipelineController.databaseBackend!.createDataServiceHost()
124
+ );
125
+
95
126
  this.updated.emit();
96
127
  return space;
97
128
  }
@@ -99,15 +130,21 @@ export class DataSpaceManager {
99
130
  // TODO(burdon): Rename join space.
100
131
  @synchronized
101
132
  async acceptSpace(opts: AcceptSpaceOptions): Promise<DataSpace> {
133
+ assert(!this._spaces.has(opts.spaceKey), 'Space already exists.');
134
+
102
135
  const metadata: SpaceMetadata = {
103
136
  key: opts.spaceKey,
104
- genesisFeedKey: opts.genesisFeedKey,
105
- controlFeedKey: opts.controlFeedKey,
106
- dataFeedKey: opts.dataFeedKey
137
+ genesisFeedKey: opts.genesisFeedKey
107
138
  };
108
139
 
109
140
  const space = await this._constructSpace(metadata);
110
141
  await this._metadataStore.addSpace(metadata);
142
+ await space.initializeDataPipeline();
143
+ this._dataServiceSubscriptions.registerSpace(
144
+ space.key,
145
+ space.dataPipelineController.databaseBackend!.createDataServiceHost()
146
+ );
147
+
111
148
  this.updated.emit();
112
149
  return space;
113
150
  }
@@ -121,12 +158,16 @@ export class DataSpaceManager {
121
158
  });
122
159
  const snapshotManager = new SnapshotManager(this._snapshotStore);
123
160
 
161
+ const controlFeed =
162
+ metadata.controlFeedKey && (await this._feedStore.openFeed(metadata.controlFeedKey, { writable: true }));
163
+ const dataFeed = metadata.dataFeedKey && (await this._feedStore.openFeed(metadata.dataFeedKey, { writable: true }));
164
+
124
165
  const space: Space = await this._spaceManager.constructSpace({
125
166
  metadata,
126
167
  swarmIdentity: {
127
168
  peerKey: this._signingContext.deviceKey,
128
- credentialProvider: this._signingContext.credentialProvider,
129
- credentialAuthenticator: this._signingContext.credentialAuthenticator
169
+ credentialProvider: createAuthProvider(this._signingContext.credentialSigner),
170
+ credentialAuthenticator: deferFunction(() => dataSpace.authVerifier.verifier)
130
171
  },
131
172
  onNetworkConnection: (session) => {
132
173
  session.addExtension(
@@ -134,8 +175,11 @@ export class DataSpaceManager {
134
175
  presence.createExtension({ remotePeerId: session.remotePeerId })
135
176
  );
136
177
  session.addExtension('dxos.mesh.teleport.objectsync', snapshotManager.objectSync.createExtension());
178
+ session.addExtension('dxos.mesh.teleport.notarization', dataSpace.notarizationPlugin.createExtension());
137
179
  }
138
180
  });
181
+ controlFeed && space.setControlFeed(controlFeed);
182
+ dataFeed && space.setDataFeed(dataFeed);
139
183
 
140
184
  const dataSpace = new DataSpace({
141
185
  inner: space,
@@ -144,11 +188,13 @@ export class DataSpaceManager {
144
188
  snapshotManager,
145
189
  presence,
146
190
  memberKey: this._signingContext.identityKey,
191
+ keyring: this._keyring,
192
+ feedStore: this._feedStore,
193
+ signingContext: this._signingContext,
147
194
  snapshotId: metadata.snapshot
148
195
  });
149
196
 
150
197
  await dataSpace.open();
151
- this._dataServiceSubscriptions.registerSpace(space.key, dataSpace.database.createDataServiceHost());
152
198
  this._spaces.set(metadata.key, dataSpace);
153
199
  return dataSpace;
154
200
  }
@@ -2,14 +2,31 @@
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
+ SigningContext,
14
+ createMappedFeedWriter,
15
+ SnapshotManager
16
+ } from '@dxos/echo-pipeline';
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';
24
+ import { ComplexSet } from '@dxos/util';
25
+
26
+ import { TrustedKeySetAuthVerifier } from '../identity';
27
+ import { NotarizationPlugin } from './notarization-plugin';
28
+
29
+ const AUTH_TIMEOUT = 30000;
13
30
 
14
31
  export type DataSpaceParams = {
15
32
  inner: Space;
@@ -17,20 +34,35 @@ export type DataSpaceParams = {
17
34
  metadataStore: MetadataStore;
18
35
  snapshotManager: SnapshotManager;
19
36
  presence: Presence;
37
+ keyring: Keyring;
38
+ feedStore: FeedStore<FeedMessage>;
39
+ signingContext: SigningContext;
20
40
  memberKey: PublicKey;
21
41
  snapshotId?: string | undefined;
22
42
  };
23
43
 
44
+ const CONTROL_PIPELINE_READY_TIMEFRAME = 3000;
24
45
  @trackLeaks('open', 'close')
25
- export class DataSpace implements ISpace {
46
+ export class DataSpace {
26
47
  private readonly _ctx = new Context();
27
48
  private readonly _dataPipelineController: DataPipelineControllerImpl;
28
49
  private readonly _inner: Space;
29
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
+
57
+ public readonly authVerifier: TrustedKeySetAuthVerifier;
30
58
 
31
59
  constructor(params: DataSpaceParams) {
32
60
  this._inner = params.inner;
33
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;
34
66
  this._dataPipelineController = new DataPipelineControllerImpl({
35
67
  modelFactory: params.modelFactory,
36
68
  metadataStore: params.metadataStore,
@@ -40,27 +72,31 @@ export class DataSpace implements ISpace {
40
72
  feedInfoProvider: (feedKey) => this._inner.spaceState.feeds.get(feedKey),
41
73
  snapshotId: params.snapshotId
42
74
  });
75
+
76
+ this.authVerifier = new TrustedKeySetAuthVerifier({
77
+ trustedKeysProvider: () => new ComplexSet(PublicKey.hash, Array.from(this._inner.spaceState.members.keys())),
78
+ update: this._inner.stateUpdate,
79
+ authTimeout: AUTH_TIMEOUT
80
+ });
81
+
82
+ this._notarizationPluginConsumer = this._inner.spaceState.registerProcessor(new NotarizationPlugin());
43
83
  }
44
84
 
45
85
  get key() {
46
86
  return this._inner.key;
47
87
  }
48
88
 
49
- get inner() {
50
- return this._inner;
51
- }
52
-
53
89
  get isOpen() {
54
90
  return this._inner.isOpen;
55
91
  }
56
92
 
57
- get dataPipelineController(): DataPipelineControllerImpl {
58
- return this._dataPipelineController;
93
+ // TODO(burdon): Can we mark this for debugging only?
94
+ get inner() {
95
+ return this._inner;
59
96
  }
60
97
 
61
- get database(): Database {
62
- assert(this._dataPipelineController.database);
63
- return this._dataPipelineController.database;
98
+ get dataPipelineController(): DataPipelineControllerImpl {
99
+ return this._dataPipelineController;
64
100
  }
65
101
 
66
102
  get stateUpdate() {
@@ -71,14 +107,89 @@ export class DataSpace implements ISpace {
71
107
  return this._presence;
72
108
  }
73
109
 
110
+ get notarizationPlugin() {
111
+ return this._notarizationPluginConsumer.processor;
112
+ }
113
+
74
114
  async open() {
115
+ await this.notarizationPlugin.open();
116
+ await this._notarizationPluginConsumer.open();
75
117
  await this._inner.open();
76
- await this._inner.initDataPipeline(this._dataPipelineController);
77
118
  }
78
119
 
79
120
  async close() {
80
121
  await this._ctx.dispose();
122
+
123
+ await this.authVerifier.close();
124
+
81
125
  await this._inner.close();
126
+ await this._notarizationPluginConsumer.close();
127
+ await this.notarizationPlugin.close();
128
+
82
129
  await this._presence.destroy();
83
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
+ }
84
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
+ };
@@ -5,13 +5,14 @@
5
5
  import { scheduleTask } from '@dxos/async';
6
6
  import { Stream } from '@dxos/codec-protobuf';
7
7
  import { raise } from '@dxos/debug';
8
- import { SpaceManager, SpaceNotFoundError } from '@dxos/echo-db';
8
+ import { SpaceManager, SpaceNotFoundError } from '@dxos/echo-pipeline';
9
9
  import {
10
10
  QueryCredentialsRequest,
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
  }