@dxos/client-services 0.1.23 → 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 (126) 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 +66 -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 +1655 -669
  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 +2 -2
  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 +3 -2
  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.d.ts +18 -4
  49. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  50. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +50 -0
  51. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -0
  52. package/dist/types/src/packlets/spaces/notarization-plugin.test.d.ts +2 -0
  53. package/dist/types/src/packlets/spaces/notarization-plugin.test.d.ts.map +1 -0
  54. package/dist/types/src/packlets/spaces/spaces-service.d.ts +3 -2
  55. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  56. package/dist/types/src/packlets/testing/credential-utils.d.ts +8 -0
  57. package/dist/types/src/packlets/testing/credential-utils.d.ts.map +1 -0
  58. package/dist/types/src/packlets/testing/index.d.ts +2 -0
  59. package/dist/types/src/packlets/testing/index.d.ts.map +1 -1
  60. package/dist/types/src/packlets/testing/invitation-utils.d.ts +3 -0
  61. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -0
  62. package/dist/types/src/packlets/testing/test-builder.d.ts +2 -2
  63. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  64. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts +35 -0
  65. package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts.map +1 -0
  66. package/dist/types/src/packlets/vault/{iframe-runtime.d.ts → iframe-proxy-runtime.d.ts} +9 -4
  67. package/dist/types/src/packlets/vault/iframe-proxy-runtime.d.ts.map +1 -0
  68. package/dist/types/src/packlets/vault/index.d.ts +6 -1
  69. package/dist/types/src/packlets/vault/index.d.ts.map +1 -1
  70. package/dist/types/src/packlets/vault/services.d.ts +9 -1
  71. package/dist/types/src/packlets/vault/services.d.ts.map +1 -1
  72. package/dist/types/src/packlets/vault/shell-controller.d.ts +24 -0
  73. package/dist/types/src/packlets/vault/shell-controller.d.ts.map +1 -0
  74. package/dist/types/src/packlets/vault/shell-runtime.d.ts +45 -0
  75. package/dist/types/src/packlets/vault/shell-runtime.d.ts.map +1 -0
  76. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts +19 -0
  77. package/dist/types/src/packlets/vault/vault-resource-lock.d.ts.map +1 -0
  78. package/dist/types/src/packlets/vault/worker-runtime.d.ts +2 -1
  79. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  80. package/dist/types/src/packlets/vault/worker-session.d.ts +7 -4
  81. package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
  82. package/package.json +30 -29
  83. package/src/packlets/deprecated/profile.ts +20 -2
  84. package/src/packlets/deprecated/space.ts +41 -37
  85. package/src/packlets/deprecated/system.ts +26 -13
  86. package/src/packlets/devtools/devtools.ts +4 -4
  87. package/src/packlets/devtools/feeds.ts +11 -6
  88. package/src/packlets/devtools/network.ts +20 -9
  89. package/src/packlets/devtools/spaces.ts +8 -7
  90. package/src/packlets/identity/authenticator.ts +12 -7
  91. package/src/packlets/identity/identity-manager.test.ts +25 -16
  92. package/src/packlets/identity/identity-manager.ts +23 -9
  93. package/src/packlets/identity/identity.test.ts +22 -23
  94. package/src/packlets/identity/identity.ts +11 -6
  95. package/src/packlets/invitations/halo-invitations-handler.test.ts +4 -45
  96. package/src/packlets/invitations/halo-invitations-handler.ts +33 -17
  97. package/src/packlets/invitations/invitations.ts +2 -1
  98. package/src/packlets/invitations/space-invitations-handler.test.ts +43 -26
  99. package/src/packlets/invitations/space-invitations-handler.ts +9 -12
  100. package/src/packlets/services/service-context.test.ts +56 -0
  101. package/src/packlets/services/service-context.ts +62 -6
  102. package/src/packlets/services/service-definitions.ts +1 -1
  103. package/src/packlets/services/service-host.test.ts +74 -5
  104. package/src/packlets/services/service-host.ts +86 -24
  105. package/src/packlets/services/service-registry.test.ts +2 -1
  106. package/src/packlets/services/service-registry.ts +5 -1
  107. package/src/packlets/spaces/data-space-manager.test.ts +72 -15
  108. package/src/packlets/spaces/data-space-manager.ts +53 -12
  109. package/src/packlets/spaces/data-space.ts +113 -14
  110. package/src/packlets/spaces/notarization-plugin.test.ts +81 -0
  111. package/src/packlets/spaces/notarization-plugin.ts +211 -0
  112. package/src/packlets/spaces/spaces-service.ts +10 -2
  113. package/src/packlets/testing/credential-utils.ts +28 -0
  114. package/src/packlets/testing/index.ts +2 -0
  115. package/src/packlets/testing/invitation-utils.ts +44 -0
  116. package/src/packlets/testing/test-builder.ts +14 -18
  117. package/src/packlets/vault/iframe-host-runtime.ts +131 -0
  118. package/src/packlets/vault/{iframe-runtime.ts → iframe-proxy-runtime.ts} +18 -3
  119. package/src/packlets/vault/index.ts +6 -1
  120. package/src/packlets/vault/services.ts +17 -1
  121. package/src/packlets/vault/shell-controller.ts +77 -0
  122. package/src/packlets/vault/shell-runtime.ts +133 -0
  123. package/src/packlets/vault/vault-resource-lock.ts +76 -0
  124. package/src/packlets/vault/worker-runtime.ts +16 -4
  125. package/src/packlets/vault/worker-session.ts +36 -26
  126. package/dist/types/src/packlets/vault/iframe-runtime.d.ts.map +0 -1
@@ -6,21 +6,23 @@ import assert from 'node:assert';
6
6
 
7
7
  import { Event, synchronized, trackLeaks } from '@dxos/async';
8
8
  import { Context } from '@dxos/context';
9
+ import { getCredentialAssertion } from '@dxos/credentials';
9
10
  import {
10
- AcceptSpaceOptions,
11
- DataServiceSubscriptions,
12
11
  MetadataStore,
13
12
  SigningContext,
14
- SnapshotStore,
15
13
  Space,
16
14
  spaceGenesis,
17
15
  SpaceManager,
18
- SnapshotManager
19
- } from '@dxos/echo-db';
16
+ DataServiceSubscriptions,
17
+ SnapshotManager,
18
+ SnapshotStore
19
+ } from '@dxos/echo-pipeline';
20
+ import { FeedStore } from '@dxos/feed-store';
20
21
  import { Keyring } from '@dxos/keyring';
21
22
  import { PublicKey } from '@dxos/keys';
22
23
  import { log } from '@dxos/log';
23
24
  import { ModelFactory } from '@dxos/model-factory';
25
+ import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
24
26
  import { SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
25
27
  import { Presence } from '@dxos/teleport-extension-presence';
26
28
  import { ComplexMap, deferFunction } from '@dxos/util';
@@ -28,6 +30,13 @@ import { ComplexMap, deferFunction } from '@dxos/util';
28
30
  import { createAuthProvider } from '../identity';
29
31
  import { DataSpace } from './data-space';
30
32
 
33
+ const DATA_PIPELINE_READY_TIMEOUT = 3_000;
34
+
35
+ export type AcceptSpaceOptions = {
36
+ spaceKey: PublicKey;
37
+ genesisFeedKey: PublicKey;
38
+ };
39
+
31
40
  @trackLeaks('open', 'close')
32
41
  export class DataSpaceManager {
33
42
  private readonly _ctx = new Context();
@@ -43,6 +52,7 @@ export class DataSpaceManager {
43
52
  private readonly _keyring: Keyring,
44
53
  private readonly _signingContext: SigningContext,
45
54
  private readonly _modelFactory: ModelFactory,
55
+ private readonly _feedStore: FeedStore<FeedMessage>,
46
56
  private readonly _snapshotStore: SnapshotStore
47
57
  ) {}
48
58
 
@@ -59,10 +69,18 @@ export class DataSpaceManager {
59
69
  for (const spaceMetadata of this._metadataStore.spaces) {
60
70
  log('load space', { spaceMetadata });
61
71
  const space = await this._constructSpace(spaceMetadata);
62
- if (spaceMetadata.latestTimeframe) {
72
+ await space.initializeDataPipeline();
73
+ if (spaceMetadata.dataTimeframe) {
63
74
  log('waiting for latest timeframe', { spaceMetadata });
64
- await space.dataPipelineController.waitUntilTimeframe(spaceMetadata.latestTimeframe);
75
+ await space.dataPipelineController.pipelineState!.setTargetTimeframe(spaceMetadata.dataTimeframe);
65
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
+ );
66
84
  }
67
85
  }
68
86
 
@@ -92,9 +110,19 @@ export class DataSpaceManager {
92
110
  log('creating space...', { spaceKey });
93
111
  const space = await this._constructSpace(metadata);
94
112
 
95
- await spaceGenesis(this._keyring, this._signingContext, space.inner);
113
+ const credentials = await spaceGenesis(this._keyring, this._signingContext, space.inner);
96
114
  await this._metadataStore.addSpace(metadata);
97
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
+
98
126
  this.updated.emit();
99
127
  return space;
100
128
  }
@@ -106,13 +134,17 @@ export class DataSpaceManager {
106
134
 
107
135
  const metadata: SpaceMetadata = {
108
136
  key: opts.spaceKey,
109
- genesisFeedKey: opts.genesisFeedKey,
110
- controlFeedKey: opts.controlFeedKey,
111
- dataFeedKey: opts.dataFeedKey
137
+ genesisFeedKey: opts.genesisFeedKey
112
138
  };
113
139
 
114
140
  const space = await this._constructSpace(metadata);
115
141
  await this._metadataStore.addSpace(metadata);
142
+ await space.initializeDataPipeline();
143
+ this._dataServiceSubscriptions.registerSpace(
144
+ space.key,
145
+ space.dataPipelineController.databaseBackend!.createDataServiceHost()
146
+ );
147
+
116
148
  this.updated.emit();
117
149
  return space;
118
150
  }
@@ -126,6 +158,10 @@ export class DataSpaceManager {
126
158
  });
127
159
  const snapshotManager = new SnapshotManager(this._snapshotStore);
128
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
+
129
165
  const space: Space = await this._spaceManager.constructSpace({
130
166
  metadata,
131
167
  swarmIdentity: {
@@ -139,8 +175,11 @@ export class DataSpaceManager {
139
175
  presence.createExtension({ remotePeerId: session.remotePeerId })
140
176
  );
141
177
  session.addExtension('dxos.mesh.teleport.objectsync', snapshotManager.objectSync.createExtension());
178
+ session.addExtension('dxos.mesh.teleport.notarization', dataSpace.notarizationPlugin.createExtension());
142
179
  }
143
180
  });
181
+ controlFeed && space.setControlFeed(controlFeed);
182
+ dataFeed && space.setDataFeed(dataFeed);
144
183
 
145
184
  const dataSpace = new DataSpace({
146
185
  inner: space,
@@ -149,11 +188,13 @@ export class DataSpaceManager {
149
188
  snapshotManager,
150
189
  presence,
151
190
  memberKey: this._signingContext.identityKey,
191
+ keyring: this._keyring,
192
+ feedStore: this._feedStore,
193
+ signingContext: this._signingContext,
152
194
  snapshotId: metadata.snapshot
153
195
  });
154
196
 
155
197
  await dataSpace.open();
156
- this._dataServiceSubscriptions.registerSpace(space.key, dataSpace.database.createDataServiceHost());
157
198
  this._spaces.set(metadata.key, dataSpace);
158
199
  return dataSpace;
159
200
  }
@@ -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
+ 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';
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
+ };
@@ -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
  }
@@ -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
+ });