@dxos/client-services 0.6.13 → 0.6.14-main.69511f5
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-CRXXOI45.mjs → chunk-PK5RMXEO.mjs} +6462 -5230
- package/dist/lib/browser/chunk-PK5RMXEO.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +7 -3
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +12 -8
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-PZ3JJJ3K.cjs → chunk-XDE6WELX.cjs} +6287 -5057
- package/dist/lib/node/chunk-XDE6WELX.cjs.map +7 -0
- package/dist/lib/node/index.cjs +50 -46
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +18 -13
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/lib/node-esm/chunk-S5DTGWTU.mjs +8956 -0
- package/dist/lib/node-esm/chunk-S5DTGWTU.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +420 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/testing/index.mjs +424 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +35 -0
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -0
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts +10 -0
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -0
- package/dist/types/src/packlets/agents/index.d.ts +3 -0
- package/dist/types/src/packlets/agents/index.d.ts.map +1 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +2 -0
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-manager.d.ts +28 -9
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +18 -0
- package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/identity-service.d.ts +7 -2
- package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +12 -3
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +30 -0
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +2 -1
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +2 -1
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-state.d.ts +19 -0
- package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +14 -9
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +2 -0
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +7 -3
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +5 -3
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +3 -0
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +2 -0
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +1 -0
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +35 -6
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +1 -2
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/testing/setup.d.ts +3 -0
- package/dist/types/src/testing/setup.d.ts.map +1 -0
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/package.json +44 -45
- package/src/index.ts +1 -0
- package/src/packlets/agents/edge-agent-manager.ts +163 -0
- package/src/packlets/agents/edge-agent-service.ts +42 -0
- package/src/packlets/agents/index.ts +6 -0
- package/src/packlets/devices/devices-service.test.ts +4 -5
- package/src/packlets/diagnostics/diagnostics-broadcast.ts +1 -0
- package/src/packlets/identity/{authenticator.test.ts → authenticator.node.test.ts} +2 -3
- package/src/packlets/identity/authenticator.ts +5 -2
- package/src/packlets/identity/contacts-service.ts +1 -1
- package/src/packlets/identity/identity-manager.test.ts +31 -16
- package/src/packlets/identity/identity-manager.ts +76 -32
- package/src/packlets/identity/identity-recovery-manager.ts +95 -0
- package/src/packlets/identity/identity-service.test.ts +5 -8
- package/src/packlets/identity/identity-service.ts +11 -5
- package/src/packlets/identity/identity.test.ts +130 -239
- package/src/packlets/identity/identity.ts +60 -17
- package/src/packlets/invitations/device-invitation-protocol.test.ts +7 -4
- package/src/packlets/invitations/device-invitation-protocol.ts +8 -2
- package/src/packlets/invitations/edge-invitation-handler.ts +185 -0
- package/src/packlets/invitations/invitation-guest-extenstion.ts +8 -4
- package/src/packlets/invitations/invitation-host-extension.ts +8 -7
- package/src/packlets/invitations/invitation-state.ts +112 -0
- package/src/packlets/invitations/invitations-handler.test.ts +16 -9
- package/src/packlets/invitations/invitations-handler.ts +57 -98
- package/src/packlets/invitations/space-invitation-protocol.test.ts +4 -3
- package/src/packlets/invitations/space-invitation-protocol.ts +5 -0
- package/src/packlets/logging/logging.test.ts +1 -2
- package/src/packlets/network/network-service.test.ts +2 -3
- package/src/packlets/services/service-context.test.ts +3 -1
- package/src/packlets/services/service-context.ts +113 -35
- package/src/packlets/services/service-host.test.ts +8 -12
- package/src/packlets/services/service-host.ts +25 -7
- package/src/packlets/services/service-registry.test.ts +1 -2
- package/src/packlets/spaces/data-space-manager.test.ts +2 -2
- package/src/packlets/spaces/data-space-manager.ts +44 -7
- package/src/packlets/spaces/data-space.ts +37 -6
- package/src/packlets/spaces/edge-feed-replicator.test.ts +252 -0
- package/src/packlets/spaces/edge-feed-replicator.ts +80 -22
- package/src/packlets/spaces/epoch-migrations.ts +2 -2
- package/src/packlets/spaces/notarization-plugin.test.ts +10 -7
- package/src/packlets/spaces/notarization-plugin.ts +196 -29
- package/src/packlets/spaces/spaces-service.test.ts +5 -9
- package/src/packlets/spaces/spaces-service.ts +6 -1
- package/src/packlets/storage/storage.ts +0 -1
- package/src/packlets/system/system-service.test.ts +1 -2
- package/src/packlets/testing/test-builder.ts +7 -4
- package/src/packlets/worker/worker-runtime.ts +2 -2
- package/src/testing/setup.ts +11 -0
- package/src/version.ts +1 -5
- package/dist/lib/browser/chunk-CRXXOI45.mjs.map +0 -7
- package/dist/lib/node/chunk-PZ3JJJ3K.cjs.map +0 -7
- package/dist/types/src/packlets/identity/authenticator.test.d.ts +0 -2
- package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +0 -1
- package/dist/types/src/packlets/services/automerge-host.test.d.ts +0 -2
- package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +0 -1
- package/src/packlets/services/automerge-host.test.ts +0 -60
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { afterEach, onTestFinished, beforeEach, describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { Trigger } from '@dxos/async';
|
|
8
8
|
import { Context } from '@dxos/context';
|
|
9
9
|
import { log } from '@dxos/log';
|
|
10
10
|
import { type DevicesService, type Device } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
|
-
import { afterEach, afterTest, beforeEach, describe, test } from '@dxos/test';
|
|
12
11
|
|
|
13
12
|
import { DevicesServiceImpl } from './devices-service';
|
|
14
13
|
import { type ServiceContext } from '../services';
|
|
@@ -36,7 +35,7 @@ describe('DevicesService', () => {
|
|
|
36
35
|
query.subscribe(({ devices }) => {
|
|
37
36
|
result.wake(devices);
|
|
38
37
|
});
|
|
39
|
-
|
|
38
|
+
onTestFinished(() => query.close());
|
|
40
39
|
expect(device.profile?.label).to.equal('test-device');
|
|
41
40
|
});
|
|
42
41
|
});
|
|
@@ -51,7 +50,7 @@ describe('DevicesService', () => {
|
|
|
51
50
|
},
|
|
52
51
|
(err) => log.catch(err),
|
|
53
52
|
);
|
|
54
|
-
|
|
53
|
+
onTestFinished(() => query.close().catch((err) => log.catch(err)));
|
|
55
54
|
expect(await result.wait()).to.be.length(0);
|
|
56
55
|
});
|
|
57
56
|
|
|
@@ -61,7 +60,7 @@ describe('DevicesService', () => {
|
|
|
61
60
|
query.subscribe(({ devices }) => {
|
|
62
61
|
result.wake(devices);
|
|
63
62
|
});
|
|
64
|
-
|
|
63
|
+
onTestFinished(() => query.close());
|
|
65
64
|
expect(await result.wait()).to.be.length(0);
|
|
66
65
|
|
|
67
66
|
result = new Trigger<Device[] | undefined>();
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import expect from '
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { Event } from '@dxos/async';
|
|
8
8
|
import { createCredentialSignerWithKey } from '@dxos/credentials';
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
import { Keyring } from '@dxos/keyring';
|
|
11
11
|
import { PublicKey } from '@dxos/keys';
|
|
12
|
-
import { describe, test } from '@dxos/test';
|
|
13
12
|
import { ComplexSet } from '@dxos/util';
|
|
14
13
|
|
|
15
14
|
import { createAuthProvider, TrustedKeySetAuthVerifier } from './authenticator';
|
|
@@ -30,5 +29,5 @@ describe('identity/authenticator', () => {
|
|
|
30
29
|
const credential = await authProvider(nonce);
|
|
31
30
|
invariant(credential);
|
|
32
31
|
expect(await authVerifier.verifier(nonce, credential)).toBeTruthy();
|
|
33
|
-
})
|
|
32
|
+
});
|
|
34
33
|
});
|
|
@@ -67,7 +67,7 @@ export class TrustedKeySetAuthVerifier {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (this._isTrustedKey(credential.issuer)) {
|
|
70
|
-
log('key is
|
|
70
|
+
log('key is trusted -- auth success', { key: credential.issuer });
|
|
71
71
|
return true;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -81,7 +81,10 @@ export class TrustedKeySetAuthVerifier {
|
|
|
81
81
|
log('auth success', { key: credential.issuer });
|
|
82
82
|
trigger.wake(true);
|
|
83
83
|
} else {
|
|
84
|
-
log('key is not currently in trusted set, waiting...', {
|
|
84
|
+
log('key is not currently in trusted set, waiting...', {
|
|
85
|
+
key: credential.issuer,
|
|
86
|
+
trusted: [...this._params.trustedKeysProvider()],
|
|
87
|
+
});
|
|
85
88
|
}
|
|
86
89
|
});
|
|
87
90
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { EventSubscriptions, scheduleTask, UpdateScheduler } from '@dxos/async';
|
|
6
6
|
import { Stream } from '@dxos/codec-protobuf';
|
|
7
7
|
import { type MemberInfo } from '@dxos/credentials';
|
|
8
|
-
import type
|
|
8
|
+
import { type SpaceManager } from '@dxos/echo-pipeline';
|
|
9
9
|
import { PublicKey } from '@dxos/keys';
|
|
10
10
|
import { type Contact, type ContactBook, type ContactsService } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
11
|
import { ComplexMap, ComplexSet } from '@dxos/util';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { expect } from '
|
|
5
|
+
import { describe, expect, test, onTestFinished } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { Context } from '@dxos/context';
|
|
8
8
|
import { valueEncoding, MetadataStore, SpaceManager, AuthStatus } from '@dxos/echo-pipeline';
|
|
@@ -13,7 +13,6 @@ import { MemoryTransportFactory, SwarmNetworkManager } from '@dxos/network-manag
|
|
|
13
13
|
import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
14
14
|
import { createStorage, type Storage, StorageType } from '@dxos/random-access-storage';
|
|
15
15
|
import { BlobStore } from '@dxos/teleport-extension-object-sync';
|
|
16
|
-
import { describe, test, afterTest } from '@dxos/test';
|
|
17
16
|
|
|
18
17
|
import { IdentityManager } from './identity-manager';
|
|
19
18
|
|
|
@@ -39,7 +38,7 @@ describe('identity/identity-manager', () => {
|
|
|
39
38
|
}),
|
|
40
39
|
});
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
onTestFinished(() => feedStore.close());
|
|
43
42
|
|
|
44
43
|
const networkManager = new SwarmNetworkManager({
|
|
45
44
|
signalManager: new MemorySignalManager(signalContext),
|
|
@@ -51,9 +50,10 @@ describe('identity/identity-manager', () => {
|
|
|
51
50
|
blobStore,
|
|
52
51
|
metadataStore,
|
|
53
52
|
});
|
|
54
|
-
const identityManager = new IdentityManager(metadataStore, keyring, feedStore, spaceManager);
|
|
53
|
+
const identityManager = new IdentityManager({ metadataStore, keyring, feedStore, spaceManager });
|
|
55
54
|
|
|
56
55
|
return {
|
|
56
|
+
networkManager,
|
|
57
57
|
metadataStore,
|
|
58
58
|
identityManager,
|
|
59
59
|
feedStore,
|
|
@@ -64,7 +64,7 @@ describe('identity/identity-manager', () => {
|
|
|
64
64
|
test('creates identity', async () => {
|
|
65
65
|
const { identityManager } = await setupPeer();
|
|
66
66
|
await identityManager.open(new Context());
|
|
67
|
-
|
|
67
|
+
onTestFinished(() => identityManager.close());
|
|
68
68
|
|
|
69
69
|
const identity = await identityManager.createIdentity();
|
|
70
70
|
expect(identity).to.exist;
|
|
@@ -95,7 +95,7 @@ describe('identity/identity-manager', () => {
|
|
|
95
95
|
test('update profile', async () => {
|
|
96
96
|
const { identityManager } = await setupPeer();
|
|
97
97
|
await identityManager.open(new Context());
|
|
98
|
-
|
|
98
|
+
onTestFinished(() => identityManager.close());
|
|
99
99
|
|
|
100
100
|
const identity = await identityManager.createIdentity();
|
|
101
101
|
expect(identity.profileDocument?.displayName).to.be.undefined;
|
|
@@ -108,6 +108,11 @@ describe('identity/identity-manager', () => {
|
|
|
108
108
|
|
|
109
109
|
const peer1 = await setupPeer({ signalContext });
|
|
110
110
|
const identity1 = await peer1.identityManager.createIdentity();
|
|
111
|
+
peer1.networkManager.setPeerInfo({
|
|
112
|
+
peerKey: identity1.deviceKey.toHex(),
|
|
113
|
+
identityKey: identity1.identityKey.toHex(),
|
|
114
|
+
});
|
|
115
|
+
await identity1.joinNetwork();
|
|
111
116
|
|
|
112
117
|
const peer2 = await setupPeer({ signalContext });
|
|
113
118
|
|
|
@@ -115,28 +120,38 @@ describe('identity/identity-manager', () => {
|
|
|
115
120
|
const controlFeedKey = await peer2.keyring.createKey();
|
|
116
121
|
const dataFeedKey = await peer2.keyring.createKey();
|
|
117
122
|
|
|
123
|
+
const credential = await identity1.getIdentityCredentialSigner().createCredential({
|
|
124
|
+
subject: deviceKey,
|
|
125
|
+
assertion: {
|
|
126
|
+
'@type': 'dxos.halo.credentials.AuthorizedDevice',
|
|
127
|
+
identityKey: identity1.identityKey,
|
|
128
|
+
deviceKey,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
118
132
|
await identity1.controlPipeline.writer.write({
|
|
119
133
|
credential: {
|
|
120
|
-
credential
|
|
121
|
-
subject: deviceKey,
|
|
122
|
-
assertion: {
|
|
123
|
-
'@type': 'dxos.halo.credentials.AuthorizedDevice',
|
|
124
|
-
identityKey: identity1.identityKey,
|
|
125
|
-
deviceKey,
|
|
126
|
-
},
|
|
127
|
-
}),
|
|
134
|
+
credential,
|
|
128
135
|
},
|
|
129
136
|
});
|
|
130
137
|
|
|
131
|
-
|
|
132
|
-
const identity2 = await peer2.identityManager.acceptIdentity({
|
|
138
|
+
const { identity: identity2, identityRecord } = await peer2.identityManager.prepareIdentity({
|
|
133
139
|
identityKey: identity1.identityKey,
|
|
134
140
|
deviceKey,
|
|
135
141
|
haloSpaceKey: identity1.haloSpaceKey,
|
|
136
142
|
haloGenesisFeedKey: identity1.haloGenesisFeedKey,
|
|
137
143
|
controlFeedKey,
|
|
138
144
|
dataFeedKey,
|
|
145
|
+
authorizedDeviceCredential: credential,
|
|
146
|
+
});
|
|
147
|
+
peer2.networkManager.setPeerInfo({
|
|
148
|
+
peerKey: identity2.deviceKey.toHex(),
|
|
149
|
+
identityKey: identity2.identityKey.toHex(),
|
|
139
150
|
});
|
|
151
|
+
await identity2.joinNetwork();
|
|
152
|
+
|
|
153
|
+
// Identity2 is not yet ready at this point. Peer1 needs to admit peer2 device key and feed keys.
|
|
154
|
+
await peer2.identityManager.acceptIdentity(identity2, identityRecord);
|
|
140
155
|
|
|
141
156
|
// TODO(dmaretskyi): We'd also need to admit device2's feeds otherwise messages from them won't be processed by the pipeline.
|
|
142
157
|
// This would mean that peer2 has replicated it's device credential chain from peer1 and is ready to issue credentials.
|
|
@@ -5,8 +5,14 @@ import platform from 'platform';
|
|
|
5
5
|
|
|
6
6
|
import { Event } from '@dxos/async';
|
|
7
7
|
import { Context } from '@dxos/context';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createCredentialSignerWithKey,
|
|
10
|
+
CredentialGenerator,
|
|
11
|
+
generateSeedPhrase,
|
|
12
|
+
keyPairFromSeedPhrase,
|
|
13
|
+
} from '@dxos/credentials';
|
|
9
14
|
import { type MetadataStore, type SpaceManager, type SwarmIdentity } from '@dxos/echo-pipeline';
|
|
15
|
+
import { type EdgeConnection } from '@dxos/edge-client';
|
|
10
16
|
import { type FeedStore } from '@dxos/feed-store';
|
|
11
17
|
import { invariant } from '@dxos/invariant';
|
|
12
18
|
import { type Keyring } from '@dxos/keyring';
|
|
@@ -14,6 +20,7 @@ import { PublicKey } from '@dxos/keys';
|
|
|
14
20
|
import { log } from '@dxos/log';
|
|
15
21
|
import { trace } from '@dxos/protocols';
|
|
16
22
|
import { Device, DeviceKind } from '@dxos/protocols/proto/dxos/client/services';
|
|
23
|
+
import { type Runtime } from '@dxos/protocols/proto/dxos/config';
|
|
17
24
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
18
25
|
import { type IdentityRecord, type SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
19
26
|
import {
|
|
@@ -21,6 +28,7 @@ import {
|
|
|
21
28
|
type DeviceProfileDocument,
|
|
22
29
|
DeviceType,
|
|
23
30
|
type ProfileDocument,
|
|
31
|
+
type Credential,
|
|
24
32
|
} from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
25
33
|
import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
|
|
26
34
|
import { Timeframe } from '@dxos/timeframe';
|
|
@@ -47,6 +55,7 @@ export type JoinIdentityParams = {
|
|
|
47
55
|
haloGenesisFeedKey: PublicKey;
|
|
48
56
|
controlFeedKey: PublicKey;
|
|
49
57
|
dataFeedKey: PublicKey;
|
|
58
|
+
authorizedDeviceCredential: Credential;
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
61
|
* Latest known timeframe for the control pipeline.
|
|
@@ -63,7 +72,13 @@ export type CreateIdentityOptions = {
|
|
|
63
72
|
deviceProfile?: DeviceProfileDocument;
|
|
64
73
|
};
|
|
65
74
|
|
|
66
|
-
export type
|
|
75
|
+
export type IdentityManagerParams = {
|
|
76
|
+
metadataStore: MetadataStore;
|
|
77
|
+
keyring: Keyring;
|
|
78
|
+
feedStore: FeedStore<FeedMessage>;
|
|
79
|
+
spaceManager: SpaceManager;
|
|
80
|
+
edgeConnection?: EdgeConnection;
|
|
81
|
+
edgeFeatures?: Runtime.Client.EdgeFeatures;
|
|
67
82
|
devicePresenceAnnounceInterval?: number;
|
|
68
83
|
devicePresenceOfflineTimeout?: number;
|
|
69
84
|
};
|
|
@@ -73,28 +88,27 @@ export type IdentityManagerRuntimeParams = {
|
|
|
73
88
|
export class IdentityManager {
|
|
74
89
|
readonly stateUpdate = new Event();
|
|
75
90
|
|
|
76
|
-
private
|
|
91
|
+
private readonly _metadataStore: MetadataStore;
|
|
92
|
+
private readonly _keyring: Keyring;
|
|
93
|
+
private readonly _feedStore: FeedStore<FeedMessage>;
|
|
94
|
+
private readonly _spaceManager: SpaceManager;
|
|
77
95
|
private readonly _devicePresenceAnnounceInterval: number;
|
|
78
96
|
private readonly _devicePresenceOfflineTimeout: number;
|
|
97
|
+
private readonly _edgeConnection: EdgeConnection | undefined;
|
|
98
|
+
private readonly _edgeFeatures: Runtime.Client.EdgeFeatures | undefined;
|
|
99
|
+
|
|
100
|
+
private _identity?: Identity;
|
|
79
101
|
|
|
80
|
-
// TODO(burdon): IdentityManagerParams.
|
|
81
102
|
// TODO(dmaretskyi): Perhaps this should take/generate the peerKey outside of an initialized identity.
|
|
82
|
-
constructor(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
params
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
) {
|
|
92
|
-
const {
|
|
93
|
-
devicePresenceAnnounceInterval = DEVICE_PRESENCE_ANNOUNCE_INTERVAL,
|
|
94
|
-
devicePresenceOfflineTimeout = DEVICE_PRESENCE_OFFLINE_TIMEOUT,
|
|
95
|
-
} = params ?? {};
|
|
96
|
-
this._devicePresenceAnnounceInterval = devicePresenceAnnounceInterval;
|
|
97
|
-
this._devicePresenceOfflineTimeout = devicePresenceOfflineTimeout;
|
|
103
|
+
constructor(params: IdentityManagerParams) {
|
|
104
|
+
this._metadataStore = params.metadataStore;
|
|
105
|
+
this._keyring = params.keyring;
|
|
106
|
+
this._feedStore = params.feedStore;
|
|
107
|
+
this._spaceManager = params.spaceManager;
|
|
108
|
+
this._edgeConnection = params.edgeConnection;
|
|
109
|
+
this._edgeFeatures = params.edgeFeatures;
|
|
110
|
+
this._devicePresenceAnnounceInterval = params.devicePresenceAnnounceInterval ?? DEVICE_PRESENCE_ANNOUNCE_INTERVAL;
|
|
111
|
+
this._devicePresenceOfflineTimeout = params.devicePresenceOfflineTimeout ?? DEVICE_PRESENCE_OFFLINE_TIMEOUT;
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
get identity() {
|
|
@@ -184,10 +198,6 @@ export class IdentityManager {
|
|
|
184
198
|
}
|
|
185
199
|
}
|
|
186
200
|
|
|
187
|
-
// TODO(burdon): ???
|
|
188
|
-
// await this._keyring.deleteKey(identityRecord.identity_key);
|
|
189
|
-
// await this._keyring.deleteKey(identityRecord.halo_space.space_key);
|
|
190
|
-
|
|
191
201
|
await this._metadataStore.setIdentityRecord(identityRecord);
|
|
192
202
|
this._identity = identity;
|
|
193
203
|
await this._identity.ready();
|
|
@@ -202,6 +212,7 @@ export class IdentityManager {
|
|
|
202
212
|
deviceKey: identity.deviceKey,
|
|
203
213
|
profile: identity.profileDocument,
|
|
204
214
|
});
|
|
215
|
+
|
|
205
216
|
return identity;
|
|
206
217
|
}
|
|
207
218
|
|
|
@@ -232,9 +243,9 @@ export class IdentityManager {
|
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
/**
|
|
235
|
-
*
|
|
246
|
+
* Prepare an identity object as the first step of acceptIdentity flow.
|
|
236
247
|
*/
|
|
237
|
-
async
|
|
248
|
+
async prepareIdentity(params: JoinIdentityParams) {
|
|
238
249
|
log('accepting identity', { params });
|
|
239
250
|
invariant(!this._identity, 'Identity already exists.');
|
|
240
251
|
|
|
@@ -249,24 +260,33 @@ export class IdentityManager {
|
|
|
249
260
|
controlTimeframe: params.controlTimeframe,
|
|
250
261
|
},
|
|
251
262
|
};
|
|
252
|
-
|
|
253
263
|
const identity = await this._constructIdentity(identityRecord);
|
|
254
264
|
await identity.open(new Context());
|
|
265
|
+
return { identity, identityRecord };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Accept an existing identity. Expects its device key to be authorized (now or later).
|
|
270
|
+
*/
|
|
271
|
+
public async acceptIdentity(identity: Identity, identityRecord: IdentityRecord, profile?: DeviceProfileDocument) {
|
|
255
272
|
this._identity = identity;
|
|
256
|
-
|
|
273
|
+
|
|
274
|
+
// Identity becomes ready after device chain is replicated. Wait for it before storing the record.
|
|
257
275
|
await this._identity.ready();
|
|
276
|
+
await this._metadataStore.setIdentityRecord(identityRecord);
|
|
277
|
+
|
|
258
278
|
log.trace('dxos.halo.identity', {
|
|
259
|
-
identityKey:
|
|
279
|
+
identityKey: this._identity!.identityKey,
|
|
260
280
|
displayName: this._identity.profileDocument?.displayName,
|
|
261
281
|
});
|
|
262
282
|
|
|
263
283
|
await this.updateDeviceProfile({
|
|
264
284
|
...this.createDefaultDeviceProfile(),
|
|
265
|
-
...
|
|
285
|
+
...profile,
|
|
266
286
|
});
|
|
267
287
|
this.stateUpdate.emit();
|
|
288
|
+
|
|
268
289
|
log('accepted identity', { identityKey: identity.identityKey, deviceKey: identity.deviceKey });
|
|
269
|
-
return identity;
|
|
270
290
|
}
|
|
271
291
|
|
|
272
292
|
/**
|
|
@@ -315,6 +335,29 @@ export class IdentityManager {
|
|
|
315
335
|
};
|
|
316
336
|
}
|
|
317
337
|
|
|
338
|
+
async createRecoveryPhrase() {
|
|
339
|
+
const identity = this._identity;
|
|
340
|
+
invariant(identity);
|
|
341
|
+
|
|
342
|
+
const seedphrase = generateSeedPhrase();
|
|
343
|
+
const keypair = keyPairFromSeedPhrase(seedphrase);
|
|
344
|
+
const recoveryKey = PublicKey.from(keypair.publicKey);
|
|
345
|
+
const identityKey = identity.identityKey;
|
|
346
|
+
const credential = await identity.getIdentityCredentialSigner().createCredential({
|
|
347
|
+
subject: identityKey,
|
|
348
|
+
assertion: {
|
|
349
|
+
'@type': 'dxos.halo.credentials.IdentityRecovery',
|
|
350
|
+
recoveryKey,
|
|
351
|
+
identityKey,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const receipt = await identity.controlPipeline.writer.write({ credential: { credential } });
|
|
356
|
+
await identity.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
|
|
357
|
+
|
|
358
|
+
return { seedphrase };
|
|
359
|
+
}
|
|
360
|
+
|
|
318
361
|
private async _constructIdentity(identityRecord: IdentityRecord) {
|
|
319
362
|
invariant(!this._identity);
|
|
320
363
|
log('constructing identity', { identityRecord });
|
|
@@ -360,9 +403,10 @@ export class IdentityManager {
|
|
|
360
403
|
signer: this._keyring,
|
|
361
404
|
identityKey: identityRecord.identityKey,
|
|
362
405
|
deviceKey: identityRecord.deviceKey,
|
|
406
|
+
edgeConnection: this._edgeConnection,
|
|
407
|
+
edgeFeatures: this._edgeFeatures,
|
|
363
408
|
});
|
|
364
409
|
log('done', { identityKey: identityRecord.identityKey });
|
|
365
|
-
this._callbacks?.onIdentityConstruction?.(identity);
|
|
366
410
|
|
|
367
411
|
// TODO(mykola): Set new timeframe on a write to a feed.
|
|
368
412
|
if (identityRecord.haloSpace.controlTimeframe) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { generateSeedPhrase, keyPairFromSeedPhrase } from '@dxos/credentials';
|
|
6
|
+
import { sign } from '@dxos/crypto';
|
|
7
|
+
import { type EdgeHttpClient } from '@dxos/edge-client';
|
|
8
|
+
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { type Keyring } from '@dxos/keyring';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { EdgeAuthChallengeError, type RecoverIdentityRequest, type RecoverIdentityResponseBody } from '@dxos/protocols';
|
|
13
|
+
import { schema } from '@dxos/protocols/proto';
|
|
14
|
+
import { Timeframe } from '@dxos/timeframe';
|
|
15
|
+
|
|
16
|
+
import { type Identity } from './identity';
|
|
17
|
+
import { type JoinIdentityParams } from './identity-manager';
|
|
18
|
+
|
|
19
|
+
export class EdgeIdentityRecoveryManager {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly _keyring: Keyring,
|
|
22
|
+
private readonly _edgeClient: EdgeHttpClient | undefined,
|
|
23
|
+
private readonly _identityProvider: () => Identity | undefined,
|
|
24
|
+
private readonly _acceptRecoveredIdentity: (params: JoinIdentityParams) => Promise<Identity>,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
public async createRecoveryPhrase() {
|
|
28
|
+
const identity = this._identityProvider();
|
|
29
|
+
invariant(identity);
|
|
30
|
+
|
|
31
|
+
const seedphrase = generateSeedPhrase();
|
|
32
|
+
const keypair = keyPairFromSeedPhrase(seedphrase);
|
|
33
|
+
const recoveryKey = PublicKey.from(keypair.publicKey);
|
|
34
|
+
const identityKey = identity.identityKey;
|
|
35
|
+
const credential = await identity.getIdentityCredentialSigner().createCredential({
|
|
36
|
+
subject: identityKey,
|
|
37
|
+
assertion: {
|
|
38
|
+
'@type': 'dxos.halo.credentials.IdentityRecovery',
|
|
39
|
+
recoveryKey,
|
|
40
|
+
identityKey,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const receipt = await identity.controlPipeline.writer.write({ credential: { credential } });
|
|
45
|
+
await identity.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
|
|
46
|
+
|
|
47
|
+
return { seedphrase };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async recoverIdentity(args: { seedphrase: string }) {
|
|
51
|
+
invariant(this._edgeClient, 'Not connected to EDGE.');
|
|
52
|
+
|
|
53
|
+
const recoveryKeypair = keyPairFromSeedPhrase(args.seedphrase);
|
|
54
|
+
const recoveryKey = PublicKey.from(recoveryKeypair.publicKey);
|
|
55
|
+
const deviceKey = await this._keyring.createKey();
|
|
56
|
+
const controlFeedKey = await this._keyring.createKey();
|
|
57
|
+
const request: RecoverIdentityRequest = {
|
|
58
|
+
recoveryKey: recoveryKey.toHex(),
|
|
59
|
+
deviceKey: deviceKey.toHex(),
|
|
60
|
+
controlFeedKey: controlFeedKey.toHex(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let response: RecoverIdentityResponseBody;
|
|
64
|
+
try {
|
|
65
|
+
response = await this._edgeClient.recoverIdentity(request);
|
|
66
|
+
} catch (error: any) {
|
|
67
|
+
if (!(error instanceof EdgeAuthChallengeError)) {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
const signature = sign(Buffer.from(error.challenge, 'base64'), recoveryKeypair.secretKey);
|
|
71
|
+
response = await this._edgeClient.recoverIdentity({
|
|
72
|
+
...request,
|
|
73
|
+
signature: Buffer.from(signature).toString('base64'),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
log.info('recovering identity', response);
|
|
78
|
+
|
|
79
|
+
await this._acceptRecoveredIdentity({
|
|
80
|
+
authorizedDeviceCredential: decodeCredential(response.deviceAuthCredential),
|
|
81
|
+
haloGenesisFeedKey: PublicKey.fromHex(response.genesisFeedKey),
|
|
82
|
+
haloSpaceKey: PublicKey.fromHex(response.haloSpaceKey),
|
|
83
|
+
identityKey: PublicKey.fromHex(response.identityKey),
|
|
84
|
+
deviceKey,
|
|
85
|
+
controlFeedKey,
|
|
86
|
+
dataFeedKey: await this._keyring.createKey(),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const decodeCredential = (credentialBase64: string) => {
|
|
92
|
+
const credentialBytes = Buffer.from(credentialBase64, 'base64');
|
|
93
|
+
const codec = schema.getCodecForType('dxos.halo.credentials.Credential');
|
|
94
|
+
return codec.decode(credentialBytes);
|
|
95
|
+
};
|
|
@@ -2,21 +2,17 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import chaiAsPromised from 'chai-as-promised';
|
|
5
|
+
import { afterEach, onTestFinished, beforeEach, describe, expect, test } from 'vitest';
|
|
7
6
|
|
|
8
7
|
import { Trigger } from '@dxos/async';
|
|
9
8
|
import { Context } from '@dxos/context';
|
|
10
9
|
import { PublicKey } from '@dxos/keys';
|
|
11
10
|
import { type Identity, type IdentityService } from '@dxos/protocols/proto/dxos/client/services';
|
|
12
|
-
import { afterEach, afterTest, beforeEach, describe, test } from '@dxos/test';
|
|
13
11
|
|
|
14
12
|
import { IdentityServiceImpl } from './identity-service';
|
|
15
13
|
import { type ServiceContext } from '../services';
|
|
16
14
|
import { createServiceContext } from '../testing';
|
|
17
15
|
|
|
18
|
-
chai.use(chaiAsPromised);
|
|
19
|
-
|
|
20
16
|
describe('IdentityService', () => {
|
|
21
17
|
let serviceContext: ServiceContext;
|
|
22
18
|
let identityService: IdentityService;
|
|
@@ -49,7 +45,7 @@ describe('IdentityService', () => {
|
|
|
49
45
|
|
|
50
46
|
test('fails to create identity if one already exists', async () => {
|
|
51
47
|
await identityService.createIdentity({});
|
|
52
|
-
await expect(identityService.createIdentity({})).
|
|
48
|
+
await expect(identityService.createIdentity({})).rejects.toThrowError('Identity already exists');
|
|
53
49
|
});
|
|
54
50
|
});
|
|
55
51
|
|
|
@@ -72,7 +68,7 @@ describe('IdentityService', () => {
|
|
|
72
68
|
query.subscribe(({ identity }) => {
|
|
73
69
|
result.wake(identity);
|
|
74
70
|
});
|
|
75
|
-
|
|
71
|
+
onTestFinished(() => query.close());
|
|
76
72
|
expect(await result.wait()).to.be.undefined;
|
|
77
73
|
});
|
|
78
74
|
|
|
@@ -82,7 +78,7 @@ describe('IdentityService', () => {
|
|
|
82
78
|
query.subscribe(({ identity }) => {
|
|
83
79
|
result.wake(identity);
|
|
84
80
|
});
|
|
85
|
-
|
|
81
|
+
onTestFinished(() => query.close());
|
|
86
82
|
expect(await result.wait()).to.be.undefined;
|
|
87
83
|
|
|
88
84
|
result = new Trigger<Identity | undefined>();
|
|
@@ -120,6 +116,7 @@ describe('open', () => {
|
|
|
120
116
|
const createIdentityService = (serviceContext: ServiceContext) => {
|
|
121
117
|
return new IdentityServiceImpl(
|
|
122
118
|
serviceContext.identityManager,
|
|
119
|
+
serviceContext.recoveryManager,
|
|
123
120
|
serviceContext.keyring,
|
|
124
121
|
() => serviceContext.dataSpaceManager!,
|
|
125
122
|
(options) => serviceContext.createIdentity(options),
|
|
@@ -6,7 +6,6 @@ import { Trigger, sleep } from '@dxos/async';
|
|
|
6
6
|
import { Stream } from '@dxos/codec-protobuf';
|
|
7
7
|
import { Resource } from '@dxos/context';
|
|
8
8
|
import { signPresentation } from '@dxos/credentials';
|
|
9
|
-
import { todo } from '@dxos/debug';
|
|
10
9
|
import { invariant } from '@dxos/invariant';
|
|
11
10
|
import { type Keyring } from '@dxos/keyring';
|
|
12
11
|
import { log } from '@dxos/log';
|
|
@@ -24,6 +23,7 @@ import { safeAwaitAll } from '@dxos/util';
|
|
|
24
23
|
|
|
25
24
|
import { type Identity } from './identity';
|
|
26
25
|
import { type CreateIdentityOptions, type IdentityManager } from './identity-manager';
|
|
26
|
+
import { type EdgeIdentityRecoveryManager } from './identity-recovery-manager';
|
|
27
27
|
import { type DataSpaceManager } from '../spaces';
|
|
28
28
|
|
|
29
29
|
const DEFAULT_SPACE_SEARCH_TIMEOUT = 10_000;
|
|
@@ -31,6 +31,7 @@ const DEFAULT_SPACE_SEARCH_TIMEOUT = 10_000;
|
|
|
31
31
|
export class IdentityServiceImpl extends Resource implements IdentityService {
|
|
32
32
|
constructor(
|
|
33
33
|
private readonly _identityManager: IdentityManager,
|
|
34
|
+
private readonly _recoveryManager: EdgeIdentityRecoveryManager,
|
|
34
35
|
private readonly _keyring: Keyring,
|
|
35
36
|
private readonly _dataSpaceManagerProvider: () => DataSpaceManager,
|
|
36
37
|
private readonly _createIdentity: (params: CreateIdentityOptions) => Promise<Identity>,
|
|
@@ -60,10 +61,6 @@ export class IdentityServiceImpl extends Resource implements IdentityService {
|
|
|
60
61
|
await identity.updateDefaultSpace(space.id);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
async recoverIdentity(request: RecoverIdentityRequest): Promise<IdentityProto> {
|
|
64
|
-
return todo();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
64
|
queryIdentity(): Stream<QueryIdentityResponse> {
|
|
68
65
|
return new Stream(({ next }) => {
|
|
69
66
|
const emitNext = () => next({ identity: this._getIdentity() });
|
|
@@ -92,6 +89,15 @@ export class IdentityServiceImpl extends Resource implements IdentityService {
|
|
|
92
89
|
return this._getIdentity()!;
|
|
93
90
|
}
|
|
94
91
|
|
|
92
|
+
async createRecoveryPhrase() {
|
|
93
|
+
return this._recoveryManager.createRecoveryPhrase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async recoverIdentity(request: RecoverIdentityRequest): Promise<IdentityProto> {
|
|
97
|
+
await this._recoveryManager.recoverIdentity(request);
|
|
98
|
+
return this._getIdentity()!;
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
// TODO(burdon): Rename createPresentation?
|
|
96
102
|
async signPresentation({ presentation, nonce }: SignPresentationRequest): Promise<Presentation> {
|
|
97
103
|
invariant(this._identityManager.identity, 'Identity not initialized.');
|