@dxos/client-services 0.1.23 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/chunk-R2FEBB66.mjs +3865 -0
- package/dist/lib/browser/chunk-R2FEBB66.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +56 -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 +1401 -645
- 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 +3688 -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 +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-manager.d.ts +7 -1
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +1 -0
- 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.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +2 -0
- 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 +2 -1
- 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 +28 -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} +4 -4
- package/dist/types/src/packlets/vault/iframe-proxy-runtime.d.ts.map +1 -0
- package/dist/types/src/packlets/vault/index.d.ts +4 -1
- package/dist/types/src/packlets/vault/index.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/types/src/packlets/vault/worker-session.d.ts +4 -4
- package/dist/types/src/packlets/vault/worker-session.d.ts.map +1 -1
- package/package.json +29 -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 +7 -6
- package/src/packlets/identity/authenticator.ts +11 -6
- package/src/packlets/identity/identity-manager.test.ts +24 -15
- package/src/packlets/identity/identity-manager.ts +22 -8
- package/src/packlets/identity/identity.test.ts +21 -22
- package/src/packlets/identity/identity.ts +10 -5
- 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 +8 -11
- package/src/packlets/services/service-context.test.ts +56 -0
- package/src/packlets/services/service-context.ts +60 -4
- 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 +85 -23
- 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 +71 -14
- package/src/packlets/spaces/data-space-manager.ts +49 -8
- 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 +9 -1
- 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 +112 -0
- package/src/packlets/vault/{iframe-runtime.ts → iframe-proxy-runtime.ts} +3 -3
- package/src/packlets/vault/index.ts +4 -1
- package/src/packlets/vault/vault-resource-lock.ts +76 -0
- package/src/packlets/vault/worker-runtime.ts +13 -3
- package/src/packlets/vault/worker-session.ts +17 -17
- package/dist/types/src/packlets/vault/iframe-runtime.d.ts.map +0 -1
|
@@ -2,17 +2,29 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import assert from 'node:assert';
|
|
6
|
-
|
|
7
5
|
import { trackLeaks } from '@dxos/async';
|
|
8
6
|
import { Context } from '@dxos/context';
|
|
9
|
-
import {
|
|
7
|
+
import { CredentialConsumer } from '@dxos/credentials';
|
|
8
|
+
import { timed } from '@dxos/debug';
|
|
9
|
+
import {
|
|
10
|
+
DataPipelineControllerImpl,
|
|
11
|
+
MetadataStore,
|
|
12
|
+
Space,
|
|
13
|
+
SnapshotManager,
|
|
14
|
+
SigningContext,
|
|
15
|
+
createMappedFeedWriter
|
|
16
|
+
} from '@dxos/echo-db';
|
|
17
|
+
import { FeedStore } from '@dxos/feed-store';
|
|
18
|
+
import { Keyring } from '@dxos/keyring';
|
|
10
19
|
import { PublicKey } from '@dxos/keys';
|
|
11
20
|
import { ModelFactory } from '@dxos/model-factory';
|
|
21
|
+
import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
22
|
+
import { AdmittedFeed, Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
12
23
|
import { Presence } from '@dxos/teleport-extension-presence';
|
|
13
24
|
import { ComplexSet } from '@dxos/util';
|
|
14
25
|
|
|
15
26
|
import { TrustedKeySetAuthVerifier } from '../identity';
|
|
27
|
+
import { NotarizationPlugin } from './notarization-plugin';
|
|
16
28
|
|
|
17
29
|
const AUTH_TIMEOUT = 30000;
|
|
18
30
|
|
|
@@ -22,21 +34,35 @@ export type DataSpaceParams = {
|
|
|
22
34
|
metadataStore: MetadataStore;
|
|
23
35
|
snapshotManager: SnapshotManager;
|
|
24
36
|
presence: Presence;
|
|
37
|
+
keyring: Keyring;
|
|
38
|
+
feedStore: FeedStore<FeedMessage>;
|
|
39
|
+
signingContext: SigningContext;
|
|
25
40
|
memberKey: PublicKey;
|
|
26
41
|
snapshotId?: string | undefined;
|
|
27
42
|
};
|
|
28
43
|
|
|
44
|
+
const CONTROL_PIPELINE_READY_TIMEFRAME = 3000;
|
|
29
45
|
@trackLeaks('open', 'close')
|
|
30
|
-
export class DataSpace
|
|
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
|
+
};
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
QueryMembersRequest,
|
|
12
12
|
QueryMembersResponse,
|
|
13
13
|
Space,
|
|
14
|
-
SpacesService
|
|
14
|
+
SpacesService,
|
|
15
|
+
WriteCredentialsRequest
|
|
15
16
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
16
17
|
import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
17
18
|
|
|
@@ -46,4 +47,11 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
46
47
|
scheduleTask(ctx, () => processor.open());
|
|
47
48
|
});
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
async writeCredentials({ spaceKey, credentials }: WriteCredentialsRequest) {
|
|
52
|
+
const space = this._spaceManager.spaces.get(spaceKey) ?? raise(new SpaceNotFoundError(spaceKey));
|
|
53
|
+
for (const credential of credentials ?? []) {
|
|
54
|
+
await space.controlPipeline.writer.write({ credential: { credential } });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
49
57
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createCredential } from '@dxos/credentials';
|
|
6
|
+
import { Signer } from '@dxos/crypto';
|
|
7
|
+
import { PublicKey } from '@dxos/keys';
|
|
8
|
+
import { Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
9
|
+
|
|
10
|
+
export const createMockCredential = async ({
|
|
11
|
+
signer,
|
|
12
|
+
issuer
|
|
13
|
+
}: {
|
|
14
|
+
signer: Signer;
|
|
15
|
+
issuer: PublicKey;
|
|
16
|
+
}): Promise<Credential> =>
|
|
17
|
+
createCredential({
|
|
18
|
+
signer,
|
|
19
|
+
issuer,
|
|
20
|
+
subject: new PublicKey(Buffer.from('test')),
|
|
21
|
+
assertion: {
|
|
22
|
+
'@type': 'example.testing.rpc.MessageWithAny',
|
|
23
|
+
payload: {
|
|
24
|
+
'@type': 'google.protobuf.Any',
|
|
25
|
+
value: Buffer.from('test')
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
|
|
7
|
+
import { Trigger } from '@dxos/async';
|
|
8
|
+
import { raise } from '@dxos/debug';
|
|
9
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
10
|
+
|
|
11
|
+
import { InvitationsHandler } from '../invitations';
|
|
12
|
+
|
|
13
|
+
export const performInvitation = async <T>(host: InvitationsHandler<T>, guest: InvitationsHandler<T>, context: T) => {
|
|
14
|
+
const done1 = new Trigger<Invitation>(); // peer 1 connected.
|
|
15
|
+
const done2 = new Trigger<Invitation>(); // peer 2 connected.
|
|
16
|
+
const observable1 = await host.createInvitation(context, { type: Invitation.Type.INTERACTIVE_TESTING });
|
|
17
|
+
observable1.subscribe({
|
|
18
|
+
onConnecting: async (invitation1: Invitation) => {
|
|
19
|
+
const observable2 = await guest.acceptInvitation(invitation1, { type: Invitation.Type.INTERACTIVE_TESTING });
|
|
20
|
+
observable2.subscribe({
|
|
21
|
+
onConnecting: async (invitation2: Invitation) => {
|
|
22
|
+
assert(invitation1.swarmKey!.equals(invitation2.swarmKey!));
|
|
23
|
+
},
|
|
24
|
+
onConnected: async (invitation2: Invitation) => {},
|
|
25
|
+
onSuccess: (invitation) => {
|
|
26
|
+
done2.wake(invitation);
|
|
27
|
+
},
|
|
28
|
+
onCancelled: () => raise(new Error()),
|
|
29
|
+
onTimeout: (err: Error) => raise(err),
|
|
30
|
+
onError: (err: Error) => raise(err)
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
onConnected: async (invitation1: Invitation) => {},
|
|
34
|
+
onCancelled: () => raise(new Error('cancelled')),
|
|
35
|
+
onSuccess: (invitation) => {
|
|
36
|
+
done1.wake(invitation);
|
|
37
|
+
},
|
|
38
|
+
onTimeout: (err: Error) => raise(err),
|
|
39
|
+
onError: (err: Error) => raise(err)
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await done1.wait();
|
|
43
|
+
await done2.wait();
|
|
44
|
+
};
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from 'chai';
|
|
6
|
-
|
|
7
5
|
import { Config } from '@dxos/config';
|
|
8
6
|
import { createCredentialSignerWithChain, CredentialGenerator } from '@dxos/credentials';
|
|
9
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DataPipelineControllerImpl,
|
|
9
|
+
MetadataStore,
|
|
10
|
+
SigningContext,
|
|
11
|
+
SnapshotStore,
|
|
12
|
+
SpaceManager,
|
|
13
|
+
valueEncoding
|
|
14
|
+
} from '@dxos/echo-db';
|
|
15
|
+
import { testLocalDatabase } from '@dxos/echo-db/testing';
|
|
10
16
|
import { FeedFactory, FeedStore } from '@dxos/feed-store';
|
|
11
17
|
import { Keyring } from '@dxos/keyring';
|
|
12
18
|
import { MemorySignalManager, MemorySignalManagerContext } from '@dxos/messaging';
|
|
@@ -66,20 +72,9 @@ export const createIdentity = async (peer: ServiceContext) => {
|
|
|
66
72
|
|
|
67
73
|
// TODO(burdon): Remove @dxos/client-testing.
|
|
68
74
|
// TODO(burdon): Create builder and make configurable.
|
|
69
|
-
export const syncItems = async (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const item1 = await space1.database!.createItem({ type: 'type-1' });
|
|
73
|
-
const item2 = await space2.database!.waitForItem({ type: 'type-1' });
|
|
74
|
-
expect(item1.id).to.eq(item2.id);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
// Check item replicated from 2 => 1.
|
|
79
|
-
const item1 = await space2.database!.createItem({ type: 'type-2' });
|
|
80
|
-
const item2 = await space1.database!.waitForItem({ type: 'type-2' });
|
|
81
|
-
expect(item1.id).to.eq(item2.id);
|
|
82
|
-
}
|
|
75
|
+
export const syncItems = async (db1: DataPipelineControllerImpl, db2: DataPipelineControllerImpl) => {
|
|
76
|
+
await testLocalDatabase(db1, db2);
|
|
77
|
+
await testLocalDatabase(db2, db1);
|
|
83
78
|
};
|
|
84
79
|
|
|
85
80
|
export class TestBuilder {
|
|
@@ -161,6 +156,7 @@ export const createSigningContext = async (keyring: Keyring): Promise<SigningCon
|
|
|
161
156
|
credential: await new CredentialGenerator(keyring, identityKey, deviceKey).createDeviceAuthorization(deviceKey)
|
|
162
157
|
},
|
|
163
158
|
deviceKey
|
|
164
|
-
)
|
|
159
|
+
),
|
|
160
|
+
recordCredential: async () => {} // No-op.
|
|
165
161
|
};
|
|
166
162
|
};
|