@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.
- package/dist/lib/browser/chunk-ZPZBLOQW.mjs +4086 -0
- package/dist/lib/browser/chunk-ZPZBLOQW.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +66 -3066
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +198 -0
- package/dist/lib/browser/packlets/testing/index.mjs.map +7 -0
- package/dist/lib/node/index.cjs +1655 -669
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +3806 -0
- package/dist/lib/node/packlets/testing/index.cjs.map +7 -0
- package/dist/types/src/packlets/deprecated/profile.d.ts +3 -1
- package/dist/types/src/packlets/deprecated/profile.d.ts.map +1 -1
- package/dist/types/src/packlets/deprecated/space.d.ts.map +1 -1
- package/dist/types/src/packlets/deprecated/system.d.ts +15 -9
- package/dist/types/src/packlets/deprecated/system.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/devtools.d.ts +2 -2
- package/dist/types/src/packlets/devtools/devtools.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/feeds.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/network.d.ts +3 -3
- package/dist/types/src/packlets/devtools/network.d.ts.map +1 -1
- package/dist/types/src/packlets/devtools/spaces.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts +2 -2
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-manager.d.ts +8 -2
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +3 -2
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/halo-invitations-handler.d.ts +11 -3
- package/dist/types/src/packlets/invitations/halo-invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations.d.ts +1 -1
- package/dist/types/src/packlets/invitations/invitations.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts +1 -1
- package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +3 -1
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.test.d.ts +2 -0
- package/dist/types/src/packlets/services/service-context.test.d.ts.map +1 -0
- package/dist/types/src/packlets/services/service-definitions.d.ts +1 -1
- package/dist/types/src/packlets/services/service-definitions.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +9 -6
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-registry.d.ts +4 -3
- package/dist/types/src/packlets/services/service-registry.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +9 -2
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +18 -4
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +50 -0
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/notarization-plugin.test.d.ts +2 -0
- package/dist/types/src/packlets/spaces/notarization-plugin.test.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +3 -2
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/credential-utils.d.ts +8 -0
- package/dist/types/src/packlets/testing/credential-utils.d.ts.map +1 -0
- package/dist/types/src/packlets/testing/index.d.ts +2 -0
- package/dist/types/src/packlets/testing/index.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/invitation-utils.d.ts +3 -0
- package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -0
- package/dist/types/src/packlets/testing/test-builder.d.ts +2 -2
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts +35 -0
- package/dist/types/src/packlets/vault/iframe-host-runtime.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/{iframe-runtime.d.ts → iframe-proxy-runtime.d.ts} +9 -4
- package/dist/types/src/packlets/vault/iframe-proxy-runtime.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/index.d.ts +6 -1
- package/dist/types/src/packlets/vault/index.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/services.d.ts +9 -1
- package/dist/types/src/packlets/vault/services.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/shell-controller.d.ts +24 -0
- package/dist/types/src/packlets/vault/shell-controller.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/shell-runtime.d.ts +45 -0
- package/dist/types/src/packlets/vault/shell-runtime.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/vault-resource-lock.d.ts +19 -0
- package/dist/types/src/packlets/vault/vault-resource-lock.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/worker-runtime.d.ts +2 -1
- package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/packlets/vault/worker-session.d.ts +7 -4
- package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
- package/package.json +30 -29
- package/src/packlets/deprecated/profile.ts +20 -2
- package/src/packlets/deprecated/space.ts +41 -37
- package/src/packlets/deprecated/system.ts +26 -13
- package/src/packlets/devtools/devtools.ts +4 -4
- package/src/packlets/devtools/feeds.ts +11 -6
- package/src/packlets/devtools/network.ts +20 -9
- package/src/packlets/devtools/spaces.ts +8 -7
- package/src/packlets/identity/authenticator.ts +12 -7
- package/src/packlets/identity/identity-manager.test.ts +25 -16
- package/src/packlets/identity/identity-manager.ts +23 -9
- package/src/packlets/identity/identity.test.ts +22 -23
- package/src/packlets/identity/identity.ts +11 -6
- package/src/packlets/invitations/halo-invitations-handler.test.ts +4 -45
- package/src/packlets/invitations/halo-invitations-handler.ts +33 -17
- package/src/packlets/invitations/invitations.ts +2 -1
- package/src/packlets/invitations/space-invitations-handler.test.ts +43 -26
- package/src/packlets/invitations/space-invitations-handler.ts +9 -12
- package/src/packlets/services/service-context.test.ts +56 -0
- package/src/packlets/services/service-context.ts +62 -6
- package/src/packlets/services/service-definitions.ts +1 -1
- package/src/packlets/services/service-host.test.ts +74 -5
- package/src/packlets/services/service-host.ts +86 -24
- package/src/packlets/services/service-registry.test.ts +2 -1
- package/src/packlets/services/service-registry.ts +5 -1
- package/src/packlets/spaces/data-space-manager.test.ts +72 -15
- package/src/packlets/spaces/data-space-manager.ts +53 -12
- package/src/packlets/spaces/data-space.ts +113 -14
- package/src/packlets/spaces/notarization-plugin.test.ts +81 -0
- package/src/packlets/spaces/notarization-plugin.ts +211 -0
- package/src/packlets/spaces/spaces-service.ts +10 -2
- package/src/packlets/testing/credential-utils.ts +28 -0
- package/src/packlets/testing/index.ts +2 -0
- package/src/packlets/testing/invitation-utils.ts +44 -0
- package/src/packlets/testing/test-builder.ts +14 -18
- package/src/packlets/vault/iframe-host-runtime.ts +131 -0
- package/src/packlets/vault/{iframe-runtime.ts → iframe-proxy-runtime.ts} +18 -3
- package/src/packlets/vault/index.ts +6 -1
- package/src/packlets/vault/services.ts +17 -1
- package/src/packlets/vault/shell-controller.ts +77 -0
- package/src/packlets/vault/shell-runtime.ts +133 -0
- package/src/packlets/vault/vault-resource-lock.ts +76 -0
- package/src/packlets/vault/worker-runtime.ts +16 -4
- package/src/packlets/vault/worker-session.ts +36 -26
- 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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
72
|
+
await space.initializeDataPipeline();
|
|
73
|
+
if (spaceMetadata.dataTimeframe) {
|
|
63
74
|
log('waiting for latest timeframe', { spaceMetadata });
|
|
64
|
-
await space.dataPipelineController.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
93
|
+
// TODO(burdon): Can we mark this for debugging only?
|
|
94
|
+
get inner() {
|
|
95
|
+
return this._inner;
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
get
|
|
73
|
-
|
|
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-
|
|
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
|
+
});
|