@dxos/client-services 0.1.0
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/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/src/errors.d.ts +8 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +21 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +26 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/packlets/devtools/devtools-host-events.d.ts +5 -0
- package/dist/src/packlets/devtools/devtools-host-events.d.ts.map +1 -0
- package/dist/src/packlets/devtools/devtools-host-events.js +14 -0
- package/dist/src/packlets/devtools/devtools-host-events.js.map +1 -0
- package/dist/src/packlets/devtools/feeds.d.ts +11 -0
- package/dist/src/packlets/devtools/feeds.d.ts.map +1 -0
- package/dist/src/packlets/devtools/feeds.js +72 -0
- package/dist/src/packlets/devtools/feeds.js.map +1 -0
- package/dist/src/packlets/devtools/index.d.ts +5 -0
- package/dist/src/packlets/devtools/index.d.ts.map +1 -0
- package/dist/src/packlets/devtools/index.js +24 -0
- package/dist/src/packlets/devtools/index.js.map +1 -0
- package/dist/src/packlets/devtools/network.d.ts +19 -0
- package/dist/src/packlets/devtools/network.d.ts.map +1 -0
- package/dist/src/packlets/devtools/network.js +79 -0
- package/dist/src/packlets/devtools/network.js.map +1 -0
- package/dist/src/packlets/devtools/spaces.d.ts +5 -0
- package/dist/src/packlets/devtools/spaces.d.ts.map +1 -0
- package/dist/src/packlets/devtools/spaces.js +38 -0
- package/dist/src/packlets/devtools/spaces.js.map +1 -0
- package/dist/src/packlets/identity/authenticator.d.ts +7 -0
- package/dist/src/packlets/identity/authenticator.d.ts.map +1 -0
- package/dist/src/packlets/identity/authenticator.js +40 -0
- package/dist/src/packlets/identity/authenticator.js.map +1 -0
- package/dist/src/packlets/identity/authenticator.test.d.ts +2 -0
- package/dist/src/packlets/identity/authenticator.test.d.ts.map +1 -0
- package/dist/src/packlets/identity/authenticator.test.js +32 -0
- package/dist/src/packlets/identity/authenticator.test.js.map +1 -0
- package/dist/src/packlets/identity/identity-manager.d.ts +35 -0
- package/dist/src/packlets/identity/identity-manager.d.ts.map +1 -0
- package/dist/src/packlets/identity/identity-manager.js +151 -0
- package/dist/src/packlets/identity/identity-manager.js.map +1 -0
- package/dist/src/packlets/identity/identity-manager.test.d.ts +2 -0
- package/dist/src/packlets/identity/identity-manager.test.d.ts.map +1 -0
- package/dist/src/packlets/identity/identity-manager.test.js +92 -0
- package/dist/src/packlets/identity/identity-manager.test.js.map +1 -0
- package/dist/src/packlets/identity/identity.d.ts +43 -0
- package/dist/src/packlets/identity/identity.d.ts.map +1 -0
- package/dist/src/packlets/identity/identity.js +74 -0
- package/dist/src/packlets/identity/identity.js.map +1 -0
- package/dist/src/packlets/identity/identity.test.d.ts +2 -0
- package/dist/src/packlets/identity/identity.test.d.ts.map +1 -0
- package/dist/src/packlets/identity/identity.test.js +258 -0
- package/dist/src/packlets/identity/identity.test.js.map +1 -0
- package/dist/src/packlets/identity/index.d.ts +4 -0
- package/dist/src/packlets/identity/index.d.ts.map +1 -0
- package/dist/src/packlets/identity/index.js +23 -0
- package/dist/src/packlets/identity/index.js.map +1 -0
- package/dist/src/packlets/invitations/common.d.ts +35 -0
- package/dist/src/packlets/invitations/common.d.ts.map +1 -0
- package/dist/src/packlets/invitations/common.js +12 -0
- package/dist/src/packlets/invitations/common.js.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations.d.ts +23 -0
- package/dist/src/packlets/invitations/halo-invitations.d.ts.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations.js +150 -0
- package/dist/src/packlets/invitations/halo-invitations.js.map +1 -0
- package/dist/src/packlets/invitations/index.d.ts +5 -0
- package/dist/src/packlets/invitations/index.d.ts.map +1 -0
- package/dist/src/packlets/invitations/index.js +24 -0
- package/dist/src/packlets/invitations/index.js.map +1 -0
- package/dist/src/packlets/invitations/invitation-wrapper.d.ts +36 -0
- package/dist/src/packlets/invitations/invitation-wrapper.d.ts.map +1 -0
- package/dist/src/packlets/invitations/invitation-wrapper.js +104 -0
- package/dist/src/packlets/invitations/invitation-wrapper.js.map +1 -0
- package/dist/src/packlets/invitations/observable.test.d.ts +2 -0
- package/dist/src/packlets/invitations/observable.test.d.ts.map +1 -0
- package/dist/src/packlets/invitations/observable.test.js +43 -0
- package/dist/src/packlets/invitations/observable.test.js.map +1 -0
- package/dist/src/packlets/invitations/space-invitations.d.ts +25 -0
- package/dist/src/packlets/invitations/space-invitations.d.ts.map +1 -0
- package/dist/src/packlets/invitations/space-invitations.js +136 -0
- package/dist/src/packlets/invitations/space-invitations.js.map +1 -0
- package/dist/src/packlets/services/impl/devtools.d.ts +34 -0
- package/dist/src/packlets/services/impl/devtools.d.ts.map +1 -0
- package/dist/src/packlets/services/impl/devtools.js +77 -0
- package/dist/src/packlets/services/impl/devtools.js.map +1 -0
- package/dist/src/packlets/services/impl/halo.d.ts +20 -0
- package/dist/src/packlets/services/impl/halo.d.ts.map +1 -0
- package/dist/src/packlets/services/impl/halo.js +93 -0
- package/dist/src/packlets/services/impl/halo.js.map +1 -0
- package/dist/src/packlets/services/impl/index.d.ts +6 -0
- package/dist/src/packlets/services/impl/index.d.ts.map +1 -0
- package/dist/src/packlets/services/impl/index.js +26 -0
- package/dist/src/packlets/services/impl/index.js.map +1 -0
- package/dist/src/packlets/services/impl/party.d.ts +25 -0
- package/dist/src/packlets/services/impl/party.d.ts.map +1 -0
- package/dist/src/packlets/services/impl/party.js +265 -0
- package/dist/src/packlets/services/impl/party.js.map +1 -0
- package/dist/src/packlets/services/impl/profile.d.ts +19 -0
- package/dist/src/packlets/services/impl/profile.d.ts.map +1 -0
- package/dist/src/packlets/services/impl/profile.js +120 -0
- package/dist/src/packlets/services/impl/profile.js.map +1 -0
- package/dist/src/packlets/services/impl/system.d.ts +9 -0
- package/dist/src/packlets/services/impl/system.d.ts.map +1 -0
- package/dist/src/packlets/services/impl/system.js +20 -0
- package/dist/src/packlets/services/impl/system.js.map +1 -0
- package/dist/src/packlets/services/impl/tracing.d.ts +12 -0
- package/dist/src/packlets/services/impl/tracing.d.ts.map +1 -0
- package/dist/src/packlets/services/impl/tracing.js +22 -0
- package/dist/src/packlets/services/impl/tracing.js.map +1 -0
- package/dist/src/packlets/services/index.d.ts +6 -0
- package/dist/src/packlets/services/index.d.ts.map +1 -0
- package/dist/src/packlets/services/index.js +25 -0
- package/dist/src/packlets/services/index.js.map +1 -0
- package/dist/src/packlets/services/service-context.d.ts +36 -0
- package/dist/src/packlets/services/service-context.d.ts.map +1 -0
- package/dist/src/packlets/services/service-context.js +112 -0
- package/dist/src/packlets/services/service-context.js.map +1 -0
- package/dist/src/packlets/services/service-factory.d.ts +15 -0
- package/dist/src/packlets/services/service-factory.d.ts.map +1 -0
- package/dist/src/packlets/services/service-factory.js +25 -0
- package/dist/src/packlets/services/service-factory.js.map +1 -0
- package/dist/src/packlets/services/service-host.d.ts +27 -0
- package/dist/src/packlets/services/service-host.d.ts.map +1 -0
- package/dist/src/packlets/services/service-host.js +78 -0
- package/dist/src/packlets/services/service-host.js.map +1 -0
- package/dist/src/packlets/services/service-host.test.d.ts +2 -0
- package/dist/src/packlets/services/service-host.test.d.ts.map +1 -0
- package/dist/src/packlets/services/service-host.test.js +78 -0
- package/dist/src/packlets/services/service-host.test.js.map +1 -0
- package/dist/src/packlets/services/services.d.ts +19 -0
- package/dist/src/packlets/services/services.d.ts.map +1 -0
- package/dist/src/packlets/services/services.js +19 -0
- package/dist/src/packlets/services/services.js.map +1 -0
- package/dist/src/packlets/services/signer.d.ts +9 -0
- package/dist/src/packlets/services/signer.d.ts.map +1 -0
- package/dist/src/packlets/services/signer.js +6 -0
- package/dist/src/packlets/services/signer.js.map +1 -0
- package/dist/src/packlets/services/testing/halo.test.d.ts +2 -0
- package/dist/src/packlets/services/testing/halo.test.d.ts.map +1 -0
- package/dist/src/packlets/services/testing/halo.test.js +34 -0
- package/dist/src/packlets/services/testing/halo.test.js.map +1 -0
- package/dist/src/packlets/services/testing/spaces.test.d.ts +2 -0
- package/dist/src/packlets/services/testing/spaces.test.d.ts.map +1 -0
- package/dist/src/packlets/services/testing/spaces.test.js +68 -0
- package/dist/src/packlets/services/testing/spaces.test.js.map +1 -0
- package/dist/src/packlets/services/testing/testing.d.ts +8 -0
- package/dist/src/packlets/services/testing/testing.d.ts.map +1 -0
- package/dist/src/packlets/services/testing/testing.js +23 -0
- package/dist/src/packlets/services/testing/testing.js.map +1 -0
- package/dist/src/packlets/storage/index.d.ts +2 -0
- package/dist/src/packlets/storage/index.d.ts.map +1 -0
- package/dist/src/packlets/storage/index.js +21 -0
- package/dist/src/packlets/storage/index.js.map +1 -0
- package/dist/src/packlets/storage/storage.d.ts +7 -0
- package/dist/src/packlets/storage/storage.d.ts.map +1 -0
- package/dist/src/packlets/storage/storage.js +89 -0
- package/dist/src/packlets/storage/storage.js.map +1 -0
- package/package.json +61 -0
- package/src/errors.ts +19 -0
- package/src/index.ts +11 -0
- package/src/packlets/devtools/devtools-host-events.ts +8 -0
- package/src/packlets/devtools/feeds.ts +91 -0
- package/src/packlets/devtools/index.ts +8 -0
- package/src/packlets/devtools/network.ts +89 -0
- package/src/packlets/devtools/spaces.ts +50 -0
- package/src/packlets/identity/authenticator.test.ts +32 -0
- package/src/packlets/identity/authenticator.ts +49 -0
- package/src/packlets/identity/identity-manager.test.ts +121 -0
- package/src/packlets/identity/identity-manager.ts +207 -0
- package/src/packlets/identity/identity.test.ts +295 -0
- package/src/packlets/identity/identity.ts +109 -0
- package/src/packlets/identity/index.ts +7 -0
- package/src/packlets/invitations/common.ts +48 -0
- package/src/packlets/invitations/halo-invitations.ts +162 -0
- package/src/packlets/invitations/index.ts +8 -0
- package/src/packlets/invitations/invitation-wrapper.ts +140 -0
- package/src/packlets/invitations/observable.test.ts +67 -0
- package/src/packlets/invitations/space-invitations.ts +159 -0
- package/src/packlets/services/impl/devtools.ts +141 -0
- package/src/packlets/services/impl/halo.ts +107 -0
- package/src/packlets/services/impl/index.ts +11 -0
- package/src/packlets/services/impl/party.ts +305 -0
- package/src/packlets/services/impl/profile.ts +141 -0
- package/src/packlets/services/impl/system.ts +19 -0
- package/src/packlets/services/impl/tracing.ts +21 -0
- package/src/packlets/services/index.ts +9 -0
- package/src/packlets/services/service-context.ts +151 -0
- package/src/packlets/services/service-factory.ts +35 -0
- package/src/packlets/services/service-host.test.ts +88 -0
- package/src/packlets/services/service-host.ts +101 -0
- package/src/packlets/services/services.ts +39 -0
- package/src/packlets/services/signer.ts +13 -0
- package/src/packlets/services/testing/halo.test.ts +40 -0
- package/src/packlets/services/testing/spaces.test.ts +87 -0
- package/src/packlets/services/testing/testing.ts +28 -0
- package/src/packlets/storage/index.ts +5 -0
- package/src/packlets/storage/storage.ts +97 -0
- package/src/typings.d.ts +5 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'assert';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DeviceStateMachine,
|
|
9
|
+
CredentialSigner,
|
|
10
|
+
createCredentialSignerWithKey,
|
|
11
|
+
createCredentialSignerWithChain
|
|
12
|
+
} from '@dxos/credentials';
|
|
13
|
+
import { Signer } from '@dxos/crypto';
|
|
14
|
+
import { failUndefined } from '@dxos/debug';
|
|
15
|
+
import { Database, Space } from '@dxos/echo-db';
|
|
16
|
+
import { PublicKey } from '@dxos/keys';
|
|
17
|
+
import { ComplexSet } from '@dxos/util';
|
|
18
|
+
|
|
19
|
+
export type IdentityParams = {
|
|
20
|
+
identityKey: PublicKey;
|
|
21
|
+
deviceKey: PublicKey;
|
|
22
|
+
signer: Signer;
|
|
23
|
+
space: Space;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Agent identity manager, which includes the agent's Halo space.
|
|
28
|
+
*/
|
|
29
|
+
export class Identity {
|
|
30
|
+
private readonly _space: Space;
|
|
31
|
+
private readonly _signer: Signer;
|
|
32
|
+
private readonly _deviceStateMachine: DeviceStateMachine;
|
|
33
|
+
|
|
34
|
+
public readonly identityKey: PublicKey;
|
|
35
|
+
public readonly deviceKey: PublicKey;
|
|
36
|
+
|
|
37
|
+
constructor({ space, signer, identityKey, deviceKey }: IdentityParams) {
|
|
38
|
+
this._space = space;
|
|
39
|
+
this._signer = signer;
|
|
40
|
+
|
|
41
|
+
this.identityKey = identityKey;
|
|
42
|
+
this.deviceKey = deviceKey;
|
|
43
|
+
|
|
44
|
+
this._deviceStateMachine = new DeviceStateMachine(this.identityKey, this.deviceKey);
|
|
45
|
+
|
|
46
|
+
// Save device key chain credential when processed by the party state machine.
|
|
47
|
+
this._space.onCredentialProcessed.set(async (credential) => {
|
|
48
|
+
await this._deviceStateMachine.process(credential);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// TODO(burdon): Expose state object?
|
|
53
|
+
get authorizedDeviceKeys(): ComplexSet<PublicKey> {
|
|
54
|
+
return this._deviceStateMachine.authorizedDeviceKeys;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async open() {
|
|
58
|
+
await this._space.open();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async close() {
|
|
62
|
+
await this._space.close();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async ready() {
|
|
66
|
+
await this._deviceStateMachine.deviceChainReady.wait();
|
|
67
|
+
|
|
68
|
+
// TODO(dmaretskyi): Should we also wait for our feeds to be admitted?
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @test-only
|
|
73
|
+
*/
|
|
74
|
+
get controlPipeline() {
|
|
75
|
+
return this._space.controlPipeline;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get haloSpaceKey() {
|
|
79
|
+
return this._space.key;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get haloGenesisFeedKey() {
|
|
83
|
+
return this._space.genesisFeedKey;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get haloDatabase(): Database {
|
|
87
|
+
return this._space.database ?? failUndefined();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Issues credentials as identity.
|
|
92
|
+
* Requires identity to be ready.
|
|
93
|
+
*/
|
|
94
|
+
getIdentityCredentialSigner(): CredentialSigner {
|
|
95
|
+
assert(this._deviceStateMachine.deviceCredentialChain, 'Device credential chain is not ready.');
|
|
96
|
+
return createCredentialSignerWithChain(
|
|
97
|
+
this._signer,
|
|
98
|
+
this._deviceStateMachine.deviceCredentialChain,
|
|
99
|
+
this.deviceKey
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Issues credentials as device.
|
|
105
|
+
*/
|
|
106
|
+
getDeviceCredentialSigner(): CredentialSigner {
|
|
107
|
+
return createCredentialSignerWithKey(this._signer, this.deviceKey);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { defaultSecretProvider, defaultSecretValidator, SecretProvider, SecretValidator } from '@dxos/credentials';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Defines a way for peers to authenticate each other through a side channel.
|
|
9
|
+
*/
|
|
10
|
+
export interface InvitationAuthenticator {
|
|
11
|
+
secretProvider?: SecretProvider;
|
|
12
|
+
secretValidator: SecretValidator;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const defaultInvitationAuthenticator: Required<InvitationAuthenticator> = {
|
|
16
|
+
secretProvider: defaultSecretProvider,
|
|
17
|
+
secretValidator: defaultSecretValidator
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Additional set of callbacks and options used in the invitation process.
|
|
22
|
+
*/
|
|
23
|
+
export interface InvitationOptions {
|
|
24
|
+
/**
|
|
25
|
+
* A function to be called when the invitation is closed (successfully or not).
|
|
26
|
+
*/
|
|
27
|
+
onFinish?: ({ expired }: { expired?: boolean }) => void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Date.now()-style timestamp of when this invitation should expire.
|
|
31
|
+
*/
|
|
32
|
+
expiration?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// TODO(burdon): Remove/move to API.
|
|
36
|
+
export type InviterInvitation = {
|
|
37
|
+
invitationCode: string;
|
|
38
|
+
secret: Uint8Array | undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// TODO(burdon): Remove/move to API.
|
|
42
|
+
export type InviteeInvitation = {
|
|
43
|
+
secret?: Uint8Array | undefined; // Can be undefined initially, then set after receiving secret from the inviter.
|
|
44
|
+
secretTrigger?: () => void; // Is triggered after supplying the secret.
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type InviterInvitations = InviterInvitation[];
|
|
48
|
+
export type InviteeInvitations = Map<string, InviteeInvitation>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Trigger } from '@dxos/async';
|
|
6
|
+
import { failUndefined } from '@dxos/debug';
|
|
7
|
+
import { PublicKey } from '@dxos/keys';
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
import { createProtocolFactory, NetworkManager, StarTopology } from '@dxos/network-manager';
|
|
10
|
+
import { createRpcPlugin } from '@dxos/protocol-plugin-rpc';
|
|
11
|
+
import { schema } from '@dxos/protocols';
|
|
12
|
+
import { InvitationDescriptor } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
13
|
+
import { createProtoRpcPeer } from '@dxos/rpc';
|
|
14
|
+
|
|
15
|
+
import { Identity, IdentityManager } from '../identity';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates and processes Halo invitations between devices.
|
|
19
|
+
*/
|
|
20
|
+
export class HaloInvitations {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly _identityManager: IdentityManager,
|
|
23
|
+
private readonly _networkManager: NetworkManager,
|
|
24
|
+
private readonly _onInitialize: () => Promise<void> // TODO(burdon): ???
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sends a HALO admission offer and waits for guest to accept.
|
|
29
|
+
*/
|
|
30
|
+
async createInvitation({ onFinish }: { onFinish?: () => void } = {}): Promise<InvitationDescriptor> {
|
|
31
|
+
const identity = this._identityManager.identity ?? failUndefined();
|
|
32
|
+
|
|
33
|
+
const plugin = createRpcPlugin(async (port) => {
|
|
34
|
+
const peer = createProtoRpcPeer({
|
|
35
|
+
port,
|
|
36
|
+
requested: {
|
|
37
|
+
HaloGuestService: schema.getService('dxos.halo.invitations.HaloGuestService')
|
|
38
|
+
},
|
|
39
|
+
exposed: {
|
|
40
|
+
HaloHostService: schema.getService('dxos.halo.invitations.HaloHostService')
|
|
41
|
+
},
|
|
42
|
+
handlers: {
|
|
43
|
+
HaloHostService: {
|
|
44
|
+
presentAdmissionCredentials: async ({ deviceKey, controlFeedKey, dataFeedKey }) => {
|
|
45
|
+
log('processing admission request', { identityKey: identity.identityKey, deviceKey });
|
|
46
|
+
|
|
47
|
+
// TODO(burdon): Use CredentialGenerator.
|
|
48
|
+
const signer = identity.getIdentityCredentialSigner();
|
|
49
|
+
await identity.controlPipeline.writer.write({
|
|
50
|
+
'@type': 'dxos.echo.feed.CredentialsMessage',
|
|
51
|
+
credential: await signer.createCredential({
|
|
52
|
+
subject: deviceKey,
|
|
53
|
+
assertion: {
|
|
54
|
+
'@type': 'dxos.halo.credentials.AuthorizedDevice',
|
|
55
|
+
identityKey: identity.identityKey,
|
|
56
|
+
deviceKey
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// TODO(dmaretskyi): Admit guest's feeds otherwise messages from them won't be processed by the pipeline.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await peer.open();
|
|
68
|
+
{
|
|
69
|
+
log('sending admission offer', { identityKey: identity.identityKey });
|
|
70
|
+
await peer.rpc.HaloGuestService.presentAdmissionOffer({
|
|
71
|
+
identityKey: identity.identityKey,
|
|
72
|
+
haloSpaceKey: identity.haloSpaceKey,
|
|
73
|
+
genesisFeedKey: identity.haloGenesisFeedKey
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
onFinish?.();
|
|
77
|
+
}
|
|
78
|
+
await peer.close();
|
|
79
|
+
await connection.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const topic = PublicKey.random();
|
|
83
|
+
const peerId = PublicKey.random(); // TODO(burdon): Fails if using the actual peer key.
|
|
84
|
+
const connection = await this._networkManager.openSwarmConnection({
|
|
85
|
+
topic,
|
|
86
|
+
peerId: topic, // TODO(burdon): Why???
|
|
87
|
+
// peerId,
|
|
88
|
+
protocol: createProtocolFactory(topic, peerId, [plugin]),
|
|
89
|
+
topology: new StarTopology(topic)
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
type: InvitationDescriptor.Type.INTERACTIVE,
|
|
94
|
+
swarmKey: topic.asUint8Array(),
|
|
95
|
+
invitation: new Uint8Array() // TODO(burdon): Remove.
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Joins an existing identity HALO by invitation.
|
|
101
|
+
*/
|
|
102
|
+
async acceptInvitation(invitation: InvitationDescriptor): Promise<Identity> {
|
|
103
|
+
const admitted = new Trigger<Identity>(); // TODO(burdon): Must not share across multiple connections.
|
|
104
|
+
|
|
105
|
+
const plugin = createRpcPlugin(async (port) => {
|
|
106
|
+
const peer = createProtoRpcPeer({
|
|
107
|
+
port,
|
|
108
|
+
requested: {
|
|
109
|
+
// TODO(burdon): Rename in-bound.
|
|
110
|
+
HaloHostService: schema.getService('dxos.halo.invitations.HaloHostService')
|
|
111
|
+
},
|
|
112
|
+
exposed: {
|
|
113
|
+
// TODO(burdon): Rename out-bound?
|
|
114
|
+
HaloGuestService: schema.getService('dxos.halo.invitations.HaloGuestService')
|
|
115
|
+
},
|
|
116
|
+
handlers: {
|
|
117
|
+
// TODO(burdon): Factory to create handlers and open/close handlers.
|
|
118
|
+
HaloGuestService: {
|
|
119
|
+
presentAdmissionOffer: async ({ identityKey, haloSpaceKey, genesisFeedKey }) => {
|
|
120
|
+
log('processing admission offer', { identityKey });
|
|
121
|
+
const identity = await this._identityManager.acceptIdentity({
|
|
122
|
+
identityKey,
|
|
123
|
+
haloSpaceKey,
|
|
124
|
+
haloGenesisFeedKey: genesisFeedKey
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
log('sending admission request', { identityKey: invitation.identityKey });
|
|
128
|
+
await peer.rpc.HaloHostService.presentAdmissionCredentials({
|
|
129
|
+
deviceKey: identity.deviceKey,
|
|
130
|
+
controlFeedKey: PublicKey.random(),
|
|
131
|
+
dataFeedKey: PublicKey.random()
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// TODO(burdon): Document.
|
|
135
|
+
await identity.ready();
|
|
136
|
+
admitted.wake(identity);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await peer.open();
|
|
143
|
+
await admitted.wait();
|
|
144
|
+
await peer.close();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const topic = PublicKey.from(invitation.swarmKey);
|
|
148
|
+
const peerId = PublicKey.random(); // TODO(burdon): Fails if using the actual peer key.
|
|
149
|
+
const connection = await this._networkManager.openSwarmConnection({
|
|
150
|
+
topic,
|
|
151
|
+
peerId: PublicKey.random(),
|
|
152
|
+
// peerId,
|
|
153
|
+
protocol: createProtocolFactory(topic, peerId, [plugin]),
|
|
154
|
+
topology: new StarTopology(topic)
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const identity = await admitted.wait();
|
|
158
|
+
await this._onInitialize();
|
|
159
|
+
await connection.close();
|
|
160
|
+
return identity;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import base from 'base-x';
|
|
6
|
+
import stableStringify from 'json-stable-stringify';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
|
|
9
|
+
import { ripemd160 } from '@dxos/crypto';
|
|
10
|
+
import { InvalidInvitationError } from '@dxos/echo-db';
|
|
11
|
+
import { PublicKey } from '@dxos/keys';
|
|
12
|
+
import { InvitationDescriptor } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
13
|
+
|
|
14
|
+
// TODO(burdon): Move to Client API.
|
|
15
|
+
|
|
16
|
+
// Encode with only alpha-numeric characters.
|
|
17
|
+
const base62 = base('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A serialized version of InvitationWrapper that's suitable to be encoded as an URL query string.
|
|
21
|
+
*/
|
|
22
|
+
export interface InvitationQueryParameters {
|
|
23
|
+
hash: string;
|
|
24
|
+
swarmKey: string;
|
|
25
|
+
invitation: string;
|
|
26
|
+
identityKey?: string;
|
|
27
|
+
type: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Describes an issued invitation.
|
|
32
|
+
*
|
|
33
|
+
* Can be serialized to protobuf or JSON.
|
|
34
|
+
* Invitations can be interactive or offline.
|
|
35
|
+
*
|
|
36
|
+
* This descriptor might also have a bundled secret for authentication in interactive mode.
|
|
37
|
+
*/
|
|
38
|
+
// TODO(burdon): Move to Client API (and/or remove).
|
|
39
|
+
export class InvitationWrapper {
|
|
40
|
+
static decode(code: string): InvitationWrapper {
|
|
41
|
+
const json = base62.decode(code).toString();
|
|
42
|
+
return InvitationWrapper.fromQueryParameters(JSON.parse(json));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static encode(invitation: InvitationWrapper): string {
|
|
46
|
+
const buffer = Buffer.from(JSON.stringify(invitation.toQueryParameters()));
|
|
47
|
+
return base62.encode(buffer);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static fromQueryParameters(queryParameters: InvitationQueryParameters): InvitationWrapper {
|
|
51
|
+
const { hash, swarmKey, invitation, identityKey, type } = queryParameters;
|
|
52
|
+
const descriptor = new InvitationWrapper(
|
|
53
|
+
parseInvitationType(type),
|
|
54
|
+
PublicKey.from(swarmKey),
|
|
55
|
+
PublicKey.bufferize(invitation),
|
|
56
|
+
identityKey ? PublicKey.from(identityKey) : undefined
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (hash !== descriptor.hash) {
|
|
60
|
+
throw new InvalidInvitationError();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return descriptor;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static fromProto(invitation: InvitationDescriptor): InvitationWrapper {
|
|
67
|
+
assert(invitation.type !== undefined);
|
|
68
|
+
assert(invitation.swarmKey, 'Missing swarm key');
|
|
69
|
+
assert(invitation.invitation);
|
|
70
|
+
|
|
71
|
+
return new InvitationWrapper(
|
|
72
|
+
invitation.type,
|
|
73
|
+
PublicKey.from(invitation.swarmKey),
|
|
74
|
+
Buffer.from(invitation.invitation),
|
|
75
|
+
invitation.identityKey ? PublicKey.from(invitation.identityKey) : undefined,
|
|
76
|
+
invitation.secret ? Buffer.from(invitation.secret) : undefined
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
public readonly type: InvitationDescriptor.Type,
|
|
82
|
+
public readonly swarmKey: PublicKey,
|
|
83
|
+
public readonly invitation: Uint8Array,
|
|
84
|
+
public readonly identityKey?: PublicKey,
|
|
85
|
+
public secret?: Uint8Array
|
|
86
|
+
) {
|
|
87
|
+
assert(type !== undefined);
|
|
88
|
+
assert(PublicKey.isPublicKey(swarmKey));
|
|
89
|
+
assert(invitation instanceof Uint8Array);
|
|
90
|
+
if (identityKey) {
|
|
91
|
+
PublicKey.assertValidPublicKey(identityKey);
|
|
92
|
+
}
|
|
93
|
+
if (secret) {
|
|
94
|
+
assert(secret instanceof Uint8Array);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get hash() {
|
|
99
|
+
const query = this.toQueryParameters();
|
|
100
|
+
return query.hash;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
toProto(): InvitationDescriptor {
|
|
104
|
+
return {
|
|
105
|
+
type: this.type,
|
|
106
|
+
swarmKey: this.swarmKey.asUint8Array(),
|
|
107
|
+
invitation: this.invitation,
|
|
108
|
+
identityKey: this.identityKey?.asUint8Array(),
|
|
109
|
+
secret: this.secret
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
toQueryParameters(): InvitationQueryParameters {
|
|
114
|
+
const query: Partial<InvitationQueryParameters> = {
|
|
115
|
+
swarmKey: this.swarmKey.toHex(),
|
|
116
|
+
invitation: PublicKey.stringify(this.invitation),
|
|
117
|
+
type: stringifyInvitationType(this.type)
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (this.identityKey) {
|
|
121
|
+
query.identityKey = this.identityKey.toHex();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
query.hash = ripemd160(stableStringify(query));
|
|
125
|
+
return query as InvitationQueryParameters;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// TODO(burdon): Move to client API.
|
|
130
|
+
const parseInvitationType = (str: string): InvitationDescriptor.Type => {
|
|
131
|
+
const type = parseInt(str);
|
|
132
|
+
assert(
|
|
133
|
+
type === InvitationDescriptor.Type.INTERACTIVE || type === InvitationDescriptor.Type.OFFLINE,
|
|
134
|
+
'Invalid invitation type'
|
|
135
|
+
);
|
|
136
|
+
return type;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// TODO(burdon): Move to client API.
|
|
140
|
+
const stringifyInvitationType = (type: InvitationDescriptor.Type): string => type.toString();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { asyncCatch, AsyncCallbacks, Observable, ObservableImpl } from '@dxos/async';
|
|
8
|
+
import { Space } from '@dxos/echo-db';
|
|
9
|
+
import { InvitationDescriptor } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
10
|
+
|
|
11
|
+
// TODO(burdon): Timeout.
|
|
12
|
+
// TODO(burdon): Objective: Service impl pattern with clean open/close semantics.
|
|
13
|
+
// TODO(burdon): Isolate deps on protocol throughout echo-db.
|
|
14
|
+
|
|
15
|
+
import { SpaceInvitations } from './space-invitations';
|
|
16
|
+
|
|
17
|
+
interface InvitationEvents extends AsyncCallbacks {
|
|
18
|
+
onConnect(invitation: InvitationDescriptor): void;
|
|
19
|
+
onComplete(): void;
|
|
20
|
+
onReject(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type InvitationObservable = Observable<InvitationEvents>;
|
|
24
|
+
|
|
25
|
+
describe('observable', function () {
|
|
26
|
+
it('sanity', function () {
|
|
27
|
+
// TODO(burdon): Testing.
|
|
28
|
+
const _ = async (space: Space, spaceInvitations: SpaceInvitations) => {
|
|
29
|
+
// Impl.
|
|
30
|
+
const doTest = (): InvitationObservable => {
|
|
31
|
+
const observable = new ObservableImpl<InvitationEvents>();
|
|
32
|
+
|
|
33
|
+
asyncCatch(
|
|
34
|
+
async () => {
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
observable.callbacks?.onConnect({} as InvitationDescriptor);
|
|
37
|
+
}, 100);
|
|
38
|
+
},
|
|
39
|
+
observable,
|
|
40
|
+
30_000
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return observable;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// TODO(burdon): Ability to cancel.
|
|
47
|
+
|
|
48
|
+
// Caller.
|
|
49
|
+
const observable = doTest();
|
|
50
|
+
observable.subscribe({
|
|
51
|
+
onConnect: (invitation) => {
|
|
52
|
+
console.log(invitation);
|
|
53
|
+
},
|
|
54
|
+
onComplete: () => {},
|
|
55
|
+
onReject: () => {},
|
|
56
|
+
onTimeout: (err) => {
|
|
57
|
+
console.log(err);
|
|
58
|
+
},
|
|
59
|
+
onError: (err: Error) => {
|
|
60
|
+
console.log(err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
expect(true).to.be.true;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Trigger } from '@dxos/async';
|
|
6
|
+
import { createAdmissionCredentials } from '@dxos/credentials';
|
|
7
|
+
import { SigningContext, Space, SpaceManager } from '@dxos/echo-db';
|
|
8
|
+
import { writeMessages } from '@dxos/feed-store';
|
|
9
|
+
import { PublicKey } from '@dxos/keys';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
import { createProtocolFactory, NetworkManager, StarTopology } from '@dxos/network-manager';
|
|
12
|
+
import { createRpcPlugin } from '@dxos/protocol-plugin-rpc';
|
|
13
|
+
import { schema } from '@dxos/protocols';
|
|
14
|
+
import { InvitationDescriptor } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
15
|
+
import { createProtoRpcPeer } from '@dxos/rpc';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Manages the life-cycle of Space invitations between peers.
|
|
19
|
+
*/
|
|
20
|
+
// TODO(burdon): Rename factory.
|
|
21
|
+
export class SpaceInvitations {
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly _spaceManager: SpaceManager,
|
|
24
|
+
private readonly _networkManager: NetworkManager,
|
|
25
|
+
private readonly _signingContext: SigningContext
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates an invitation and listens for a join request from the invited (guest) peer.
|
|
30
|
+
*/
|
|
31
|
+
// TODO(burdon): Replace callback with cancelable observable.
|
|
32
|
+
async createInvitation(space: Space, { onFinish }: { onFinish?: () => void } = {}): Promise<InvitationDescriptor> {
|
|
33
|
+
const admitted = new Trigger();
|
|
34
|
+
|
|
35
|
+
const plugin = createRpcPlugin(async (port) => {
|
|
36
|
+
const peer = createProtoRpcPeer({
|
|
37
|
+
port,
|
|
38
|
+
requested: {
|
|
39
|
+
SpaceGuestService: schema.getService('dxos.halo.invitations.SpaceGuestService')
|
|
40
|
+
},
|
|
41
|
+
exposed: {
|
|
42
|
+
SpaceHostService: schema.getService('dxos.halo.invitations.SpaceHostService')
|
|
43
|
+
},
|
|
44
|
+
handlers: {
|
|
45
|
+
SpaceHostService: {
|
|
46
|
+
presentAdmissionCredentials: async ({ identityKey, deviceKey, controlFeedKey, dataFeedKey }) => {
|
|
47
|
+
log('processing admission request', { identityKey, deviceKey });
|
|
48
|
+
|
|
49
|
+
await writeMessages(
|
|
50
|
+
space.controlPipeline.writer,
|
|
51
|
+
await createAdmissionCredentials(
|
|
52
|
+
this._signingContext.credentialSigner,
|
|
53
|
+
identityKey,
|
|
54
|
+
deviceKey,
|
|
55
|
+
space.key,
|
|
56
|
+
controlFeedKey,
|
|
57
|
+
dataFeedKey
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
admitted.wake();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// TODO(burdon): Error handling and timeout.
|
|
68
|
+
await peer.open();
|
|
69
|
+
{
|
|
70
|
+
log('sending admission offer', { spaceKey: space.key });
|
|
71
|
+
await peer.rpc.SpaceGuestService.presentAdmissionOffer({
|
|
72
|
+
spaceKey: space.key,
|
|
73
|
+
genesisFeedKey: space.genesisFeedKey
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
onFinish?.();
|
|
77
|
+
}
|
|
78
|
+
await peer.close();
|
|
79
|
+
await connection.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const topic = PublicKey.random();
|
|
83
|
+
const peerId = PublicKey.random(); // TODO(burdon): Use actual key.
|
|
84
|
+
const connection = await this._networkManager.openSwarmConnection({
|
|
85
|
+
topic,
|
|
86
|
+
peerId: topic, // TODO(burdon): Why???
|
|
87
|
+
// peerId,
|
|
88
|
+
protocol: createProtocolFactory(topic, peerId, [plugin]),
|
|
89
|
+
topology: new StarTopology(topic)
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
type: InvitationDescriptor.Type.INTERACTIVE, // TODO(burdon): Remove (default).
|
|
94
|
+
swarmKey: topic.asUint8Array(),
|
|
95
|
+
invitation: new Uint8Array() // TODO(burdon): Remove.
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Waits for the host peer (inviter) to accept our join request.
|
|
101
|
+
* The local guest peer (invitee) then sends the local party invitation to the host,
|
|
102
|
+
* which then writes the guest's credentials to the space.
|
|
103
|
+
*/
|
|
104
|
+
async acceptInvitation(invitation: InvitationDescriptor): Promise<Space> {
|
|
105
|
+
const admitted = new Trigger<Space>();
|
|
106
|
+
|
|
107
|
+
const plugin = createRpcPlugin(async (port) => {
|
|
108
|
+
const peer = createProtoRpcPeer({
|
|
109
|
+
port,
|
|
110
|
+
requested: {
|
|
111
|
+
SpaceHostService: schema.getService('dxos.halo.invitations.SpaceHostService')
|
|
112
|
+
},
|
|
113
|
+
exposed: {
|
|
114
|
+
SpaceGuestService: schema.getService('dxos.halo.invitations.SpaceGuestService')
|
|
115
|
+
},
|
|
116
|
+
handlers: {
|
|
117
|
+
SpaceGuestService: {
|
|
118
|
+
presentAdmissionOffer: async ({ spaceKey, genesisFeedKey }) => {
|
|
119
|
+
log('processing admission offer', { spaceKey });
|
|
120
|
+
// Create local space.
|
|
121
|
+
const space = await this._spaceManager.acceptSpace({ spaceKey, genesisFeedKey });
|
|
122
|
+
|
|
123
|
+
// Send local space's details to host (inviter).
|
|
124
|
+
// TODO(burdon): Space is orphaned if we crash before other side ACKs. Retry from cold start possible?
|
|
125
|
+
log('sending admission request', { identityKey: invitation.identityKey });
|
|
126
|
+
await peer.rpc.SpaceHostService.presentAdmissionCredentials({
|
|
127
|
+
identityKey: this._signingContext.identityKey,
|
|
128
|
+
deviceKey: this._signingContext.deviceKey,
|
|
129
|
+
controlFeedKey: space.controlFeedKey,
|
|
130
|
+
dataFeedKey: space.dataFeedKey
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// TODO(burdon): Error: Writable stream closed prematurely
|
|
134
|
+
admitted.wake(space);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await peer.open();
|
|
141
|
+
await admitted.wait();
|
|
142
|
+
await peer.close();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const topic = PublicKey.from(invitation.swarmKey);
|
|
146
|
+
const peerId = PublicKey.random(); // TODO(burdon): Use actual key.
|
|
147
|
+
const connection = await this._networkManager.openSwarmConnection({
|
|
148
|
+
topic,
|
|
149
|
+
peerId: PublicKey.random(), // TODO(burdon): Why???
|
|
150
|
+
// peerId,
|
|
151
|
+
protocol: createProtocolFactory(topic, peerId, [plugin]),
|
|
152
|
+
topology: new StarTopology(topic)
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const space = await admitted.wait();
|
|
156
|
+
await connection.close();
|
|
157
|
+
return space;
|
|
158
|
+
}
|
|
159
|
+
}
|