@dxos/client-services 0.1.4 → 0.1.6
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 +5 -18
- package/README.md +14 -10
- package/README.yml +5 -0
- package/dist/src/packlets/deprecated/profile.d.ts +2 -5
- package/dist/src/packlets/deprecated/profile.d.ts.map +1 -1
- package/dist/src/packlets/deprecated/profile.js +1 -73
- package/dist/src/packlets/deprecated/profile.js.map +1 -1
- package/dist/src/packlets/deprecated/system.d.ts +4 -0
- package/dist/src/packlets/deprecated/system.d.ts.map +1 -1
- package/dist/src/packlets/deprecated/system.js +6 -0
- package/dist/src/packlets/deprecated/system.js.map +1 -1
- package/dist/src/packlets/deprecated/tracing.d.ts +1 -1
- package/dist/src/packlets/deprecated/tracing.js +1 -1
- package/dist/src/packlets/devtools/devtools.d.ts +3 -0
- package/dist/src/packlets/devtools/devtools.d.ts.map +1 -1
- package/dist/src/packlets/devtools/devtools.js +3 -1
- package/dist/src/packlets/devtools/devtools.js.map +1 -1
- package/dist/src/packlets/identity/devices-service-impl.d.ts.map +1 -1
- package/dist/src/packlets/identity/devices-service-impl.js.map +1 -1
- package/dist/src/packlets/identity/identity-manager.d.ts +2 -2
- package/dist/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/src/packlets/identity/identity-manager.js +44 -43
- package/dist/src/packlets/identity/identity-manager.js.map +1 -1
- package/dist/src/packlets/identity/identity.d.ts +3 -0
- package/dist/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/src/packlets/identity/identity.js +51 -1
- package/dist/src/packlets/identity/identity.js.map +1 -1
- package/dist/src/packlets/invitations/halo-invitations-handler.d.ts +23 -0
- package/dist/src/packlets/invitations/halo-invitations-handler.d.ts.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-handler.js +231 -0
- package/dist/src/packlets/invitations/halo-invitations-handler.js.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-handler.test.d.ts +2 -0
- package/dist/src/packlets/invitations/halo-invitations-handler.test.d.ts.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-handler.test.js +69 -0
- package/dist/src/packlets/invitations/halo-invitations-handler.test.js.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-proxy.d.ts +8 -0
- package/dist/src/packlets/invitations/halo-invitations-proxy.d.ts.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-proxy.js +17 -0
- package/dist/src/packlets/invitations/halo-invitations-proxy.js.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-service.d.ts +13 -0
- package/dist/src/packlets/invitations/halo-invitations-service.d.ts.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-service.js +20 -0
- package/dist/src/packlets/invitations/halo-invitations-service.js.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-service.test.d.ts +2 -0
- package/dist/src/packlets/invitations/halo-invitations-service.test.d.ts.map +1 -0
- package/dist/src/packlets/invitations/halo-invitations-service.test.js +83 -0
- package/dist/src/packlets/invitations/halo-invitations-service.test.js.map +1 -0
- package/dist/src/packlets/invitations/index.d.ts +5 -1
- package/dist/src/packlets/invitations/index.d.ts.map +1 -1
- package/dist/src/packlets/invitations/index.js +5 -1
- package/dist/src/packlets/invitations/index.js.map +1 -1
- package/dist/src/packlets/invitations/invitations-handler.d.ts +48 -0
- package/dist/src/packlets/invitations/invitations-handler.d.ts.map +1 -0
- package/dist/src/packlets/invitations/invitations-handler.js +18 -0
- package/dist/src/packlets/invitations/invitations-handler.js.map +1 -0
- package/dist/src/packlets/invitations/invitations-proxy.d.ts +11 -7
- package/dist/src/packlets/invitations/invitations-proxy.d.ts.map +1 -1
- package/dist/src/packlets/invitations/invitations-proxy.js +5 -10
- package/dist/src/packlets/invitations/invitations-proxy.js.map +1 -1
- package/dist/src/packlets/invitations/invitations-service.d.ts +25 -0
- package/dist/src/packlets/invitations/invitations-service.d.ts.map +1 -0
- package/dist/src/packlets/invitations/invitations-service.js +174 -0
- package/dist/src/packlets/invitations/invitations-service.js.map +1 -0
- package/dist/src/packlets/invitations/invitations.d.ts +10 -42
- package/dist/src/packlets/invitations/invitations.d.ts.map +1 -1
- package/dist/src/packlets/invitations/invitations.js +5 -2
- package/dist/src/packlets/invitations/invitations.js.map +1 -1
- package/dist/src/packlets/invitations/space-invitations-handler.d.ts +6 -6
- package/dist/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
- package/dist/src/packlets/invitations/space-invitations-handler.js +18 -19
- package/dist/src/packlets/invitations/space-invitations-handler.js.map +1 -1
- package/dist/src/packlets/invitations/space-invitations-handler.test.js +5 -5
- package/dist/src/packlets/invitations/space-invitations-handler.test.js.map +1 -1
- package/dist/src/packlets/invitations/space-invitations-proxy.d.ts +1 -1
- package/dist/src/packlets/invitations/space-invitations-proxy.d.ts.map +1 -1
- package/dist/src/packlets/invitations/space-invitations-proxy.js +2 -2
- package/dist/src/packlets/invitations/space-invitations-proxy.js.map +1 -1
- package/dist/src/packlets/invitations/space-invitations-service.d.ts +8 -14
- package/dist/src/packlets/invitations/space-invitations-service.d.ts.map +1 -1
- package/dist/src/packlets/invitations/space-invitations-service.js +11 -169
- package/dist/src/packlets/invitations/space-invitations-service.js.map +1 -1
- package/dist/src/packlets/invitations/space-invitations-service.test.js +4 -4
- package/dist/src/packlets/invitations/space-invitations-service.test.js.map +1 -1
- package/dist/src/packlets/services/service-context.d.ts +2 -2
- package/dist/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/src/packlets/services/service-context.js +8 -8
- package/dist/src/packlets/services/service-context.js.map +1 -1
- package/dist/src/packlets/services/service-definitions.d.ts +5 -4
- package/dist/src/packlets/services/service-definitions.d.ts.map +1 -1
- package/dist/src/packlets/services/service-definitions.js +6 -4
- package/dist/src/packlets/services/service-definitions.js.map +1 -1
- package/dist/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/src/packlets/services/service-host.js +10 -8
- package/dist/src/packlets/services/service-host.js.map +1 -1
- package/dist/src/packlets/services/service-proxy.js +1 -1
- package/dist/src/packlets/services/service-registry.test.js +2 -2
- package/dist/src/packlets/services/service-registry.test.js.map +1 -1
- package/dist/src/packlets/vault/iframe-runtime.d.ts +5 -4
- package/dist/src/packlets/vault/iframe-runtime.d.ts.map +1 -1
- package/dist/src/packlets/vault/iframe-runtime.js +4 -12
- package/dist/src/packlets/vault/iframe-runtime.js.map +1 -1
- package/dist/src/packlets/vault/worker-runtime.d.ts +4 -3
- package/dist/src/packlets/vault/worker-runtime.d.ts.map +1 -1
- package/dist/src/packlets/vault/worker-runtime.js +29 -24
- package/dist/src/packlets/vault/worker-runtime.js.map +1 -1
- package/dist/src/packlets/vault/worker-session.d.ts +5 -2
- package/dist/src/packlets/vault/worker-session.d.ts.map +1 -1
- package/dist/src/packlets/vault/worker-session.js +23 -10
- package/dist/src/packlets/vault/worker-session.js.map +1 -1
- package/package.json +26 -26
- package/src/packlets/deprecated/profile.ts +2 -84
- package/src/packlets/deprecated/system.ts +7 -0
- package/src/packlets/deprecated/tracing.ts +1 -1
- package/src/packlets/devtools/devtools.ts +3 -1
- package/src/packlets/identity/devices-service-impl.ts +0 -1
- package/src/packlets/identity/identity-manager.ts +50 -49
- package/src/packlets/identity/identity.ts +58 -1
- package/src/packlets/invitations/halo-invitations-handler.test.ts +79 -0
- package/src/packlets/invitations/halo-invitations-handler.ts +255 -0
- package/src/packlets/invitations/halo-invitations-proxy.ts +14 -0
- package/src/packlets/invitations/halo-invitations-service.test.ts +96 -0
- package/src/packlets/invitations/halo-invitations-service.ts +24 -0
- package/src/packlets/invitations/index.ts +5 -1
- package/src/packlets/invitations/invitations-handler.ts +59 -0
- package/src/packlets/invitations/invitations-proxy.ts +15 -13
- package/src/packlets/invitations/invitations-service.ts +184 -0
- package/src/packlets/invitations/invitations.ts +14 -46
- package/src/packlets/invitations/space-invitations-handler.test.ts +6 -6
- package/src/packlets/invitations/space-invitations-handler.ts +16 -16
- package/src/packlets/invitations/space-invitations-proxy.ts +2 -2
- package/src/packlets/invitations/space-invitations-service.test.ts +13 -13
- package/src/packlets/invitations/space-invitations-service.ts +16 -177
- package/src/packlets/services/service-context.ts +6 -6
- package/src/packlets/services/service-definitions.ts +20 -10
- package/src/packlets/services/service-host.ts +15 -7
- package/src/packlets/services/service-proxy.ts +1 -1
- package/src/packlets/services/service-registry.test.ts +5 -5
- package/src/packlets/vault/iframe-runtime.ts +8 -15
- package/src/packlets/vault/worker-runtime.ts +36 -30
- package/src/packlets/vault/worker-session.ts +20 -6
- package/dist/src/packlets/invitations/halo-invitations.d.ts +0 -23
- package/dist/src/packlets/invitations/halo-invitations.d.ts.map +0 -1
- package/dist/src/packlets/invitations/halo-invitations.js +0 -154
- package/dist/src/packlets/invitations/halo-invitations.js.map +0 -1
- package/dist/src/packlets/invitations/halo-invitations.test.d.ts +0 -2
- package/dist/src/packlets/invitations/halo-invitations.test.d.ts.map +0 -1
- package/dist/src/packlets/invitations/halo-invitations.test.js +0 -34
- package/dist/src/packlets/invitations/halo-invitations.test.js.map +0 -1
- package/dist/src/packlets/services/iframe-service-proxy.d.ts +0 -23
- package/dist/src/packlets/services/iframe-service-proxy.d.ts.map +0 -1
- package/dist/src/packlets/services/iframe-service-proxy.js +0 -53
- package/dist/src/packlets/services/iframe-service-proxy.js.map +0 -1
- package/dist/src/packlets/services/service-host.test.d.ts +0 -2
- package/dist/src/packlets/services/service-host.test.d.ts.map +0 -1
- package/dist/src/packlets/services/service-host.test.js +0 -68
- package/dist/src/packlets/services/service-host.test.js.map +0 -1
- package/src/packlets/invitations/halo-invitations.test.ts +0 -40
- package/src/packlets/invitations/halo-invitations.ts +0 -164
- package/src/packlets/services/iframe-service-proxy.ts +0 -71
- package/src/packlets/services/service-host.test.ts +0 -76
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'assert';
|
|
6
|
+
|
|
7
|
+
import { sleep, Trigger } from '@dxos/async';
|
|
8
|
+
import { generatePasscode } from '@dxos/credentials';
|
|
9
|
+
import { failUndefined } from '@dxos/debug';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { createProtocolFactory, NetworkManager, StarTopology, SwarmConnection } from '@dxos/network-manager';
|
|
13
|
+
import { createRpcPlugin, RpcPlugin } from '@dxos/protocol-plugin-rpc';
|
|
14
|
+
import { schema } from '@dxos/protocols';
|
|
15
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
16
|
+
import { createProtoRpcPeer } from '@dxos/rpc';
|
|
17
|
+
|
|
18
|
+
import { IdentityManager } from '../identity';
|
|
19
|
+
import {
|
|
20
|
+
AuthenticatingInvitationProvider,
|
|
21
|
+
InvitationObservable,
|
|
22
|
+
InvitationObservableProvider,
|
|
23
|
+
AUTHENTICATION_CODE_LENGTH,
|
|
24
|
+
INVITATION_TIMEOUT,
|
|
25
|
+
ON_CLOSE_DELAY
|
|
26
|
+
} from './invitations';
|
|
27
|
+
import { AbstractInvitationsHandler, InvitationsOptions } from './invitations-handler';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handles the life-cycle of Halo invitations between peers.
|
|
31
|
+
*/
|
|
32
|
+
// TODO(dmaretskyi): Split into Host and Guest parts.
|
|
33
|
+
export class HaloInvitationsHandler extends AbstractInvitationsHandler {
|
|
34
|
+
// prettier-ignore
|
|
35
|
+
constructor(
|
|
36
|
+
networkManager: NetworkManager,
|
|
37
|
+
private readonly _identityManager: IdentityManager
|
|
38
|
+
) {
|
|
39
|
+
super(networkManager);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates an invitation and listens for a join request from the invited (guest) peer.
|
|
44
|
+
*/
|
|
45
|
+
createInvitation(context: void, options?: InvitationsOptions): InvitationObservable {
|
|
46
|
+
let swarmConnection: SwarmConnection | undefined;
|
|
47
|
+
const { type, timeout = INVITATION_TIMEOUT } = options ?? {};
|
|
48
|
+
assert(type !== Invitation.Type.OFFLINE);
|
|
49
|
+
const identity = this._identityManager.identity ?? failUndefined();
|
|
50
|
+
|
|
51
|
+
const invitation: Invitation = {
|
|
52
|
+
type,
|
|
53
|
+
invitationId: PublicKey.random().toHex(),
|
|
54
|
+
swarmKey: PublicKey.random(),
|
|
55
|
+
authenticationCode: generatePasscode(AUTHENTICATION_CODE_LENGTH)
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// TODO(burdon): Stop anything pending.
|
|
59
|
+
const observable = new InvitationObservableProvider(async () => {
|
|
60
|
+
await swarmConnection?.close();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let authenticationCode: string;
|
|
64
|
+
const complete = new Trigger<PublicKey>();
|
|
65
|
+
const plugin = new RpcPlugin(async (port) => {
|
|
66
|
+
const peer = createProtoRpcPeer({
|
|
67
|
+
exposed: {
|
|
68
|
+
HaloHostService: schema.getService('dxos.halo.invitations.HaloHostService')
|
|
69
|
+
},
|
|
70
|
+
handlers: {
|
|
71
|
+
HaloHostService: {
|
|
72
|
+
requestAdmission: async () => {
|
|
73
|
+
log('responding with admission offer', {
|
|
74
|
+
host: identity.deviceKey
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// TODO(burdon): Is this the right place to set this state?
|
|
78
|
+
observable.callback.onAuthenticating?.(invitation);
|
|
79
|
+
return {
|
|
80
|
+
identityKey: identity.identityKey,
|
|
81
|
+
haloSpaceKey: identity.haloSpaceKey,
|
|
82
|
+
genesisFeedKey: identity.haloGenesisFeedKey
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
authenticate: async ({ authenticationCode: code }) => {
|
|
87
|
+
log('received authentication request', { authenticationCode: code });
|
|
88
|
+
authenticationCode = code;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// TODO(burdon): Not used: controlFeedKey, dataFeedKey.
|
|
92
|
+
presentAdmissionCredentials: async (credentials) => {
|
|
93
|
+
try {
|
|
94
|
+
// Check authenticated.
|
|
95
|
+
if (invitation.type === undefined || invitation.type === Invitation.Type.INTERACTIVE) {
|
|
96
|
+
if (
|
|
97
|
+
invitation.authenticationCode === undefined ||
|
|
98
|
+
authenticationCode !== invitation.authenticationCode
|
|
99
|
+
) {
|
|
100
|
+
throw new Error('authentication code not set');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
log('writing guest credentials', { host: identity.deviceKey, guest: credentials.deviceKey });
|
|
105
|
+
// TODO(burdon): Check if already admitted.
|
|
106
|
+
await identity.admitDevice(credentials);
|
|
107
|
+
|
|
108
|
+
// Updating credentials complete.
|
|
109
|
+
complete.wake(credentials.deviceKey);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// TODO(burdon): Generic RPC callback to report error to client.
|
|
112
|
+
observable.callback.onError(err);
|
|
113
|
+
throw err; // Propagate error to guest.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
port
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await peer.open();
|
|
123
|
+
log('connected', { host: identity.deviceKey });
|
|
124
|
+
observable.callback.onConnected?.(invitation);
|
|
125
|
+
const deviceKey = await complete.wait({ timeout });
|
|
126
|
+
log('admitted guest', { host: identity.deviceKey, guest: deviceKey });
|
|
127
|
+
observable.callback.onSuccess(invitation);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (!observable.cancelled) {
|
|
130
|
+
log.error('failed', err);
|
|
131
|
+
observable.callback.onError(err);
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
await sleep(ON_CLOSE_DELAY);
|
|
135
|
+
await peer.close();
|
|
136
|
+
await swarmConnection!.close();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
setTimeout(async () => {
|
|
141
|
+
const topic = invitation.swarmKey!;
|
|
142
|
+
const peerId = PublicKey.random(); // Use anonymous key.
|
|
143
|
+
swarmConnection = await this._networkManager.openSwarmConnection({
|
|
144
|
+
topic,
|
|
145
|
+
peerId: topic,
|
|
146
|
+
protocol: createProtocolFactory(topic, peerId, [plugin]),
|
|
147
|
+
topology: new StarTopology(topic)
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
observable.callback.onConnecting?.(invitation);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return observable;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Waits for the host peer (inviter) to accept our join request.
|
|
158
|
+
* The local guest peer (invitee) then sends the local halo invitation to the host,
|
|
159
|
+
* which then writes the guest's credentials to the halo.
|
|
160
|
+
*/
|
|
161
|
+
acceptInvitation(invitation: Invitation, options?: InvitationsOptions): AuthenticatingInvitationProvider {
|
|
162
|
+
const { timeout = INVITATION_TIMEOUT } = options ?? {};
|
|
163
|
+
let swarmConnection: SwarmConnection | undefined;
|
|
164
|
+
|
|
165
|
+
const authenticated = new Trigger<string>();
|
|
166
|
+
const observable = new AuthenticatingInvitationProvider({
|
|
167
|
+
onCancel: async () => {
|
|
168
|
+
await swarmConnection?.close();
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// TODO(burdon): Consider retry.
|
|
172
|
+
onAuthenticate: async (code: string) => {
|
|
173
|
+
authenticated.wake(code);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let connectionCount = 0;
|
|
178
|
+
const complete = new Trigger<PublicKey>();
|
|
179
|
+
const plugin = createRpcPlugin(async (port) => {
|
|
180
|
+
const peer = createProtoRpcPeer({
|
|
181
|
+
requested: {
|
|
182
|
+
HaloHostService: schema.getService('dxos.halo.invitations.HaloHostService')
|
|
183
|
+
},
|
|
184
|
+
port
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await peer.open();
|
|
189
|
+
// TODO(burdon): Bug where guest may create multiple connections.
|
|
190
|
+
if (++connectionCount > 1) {
|
|
191
|
+
throw new Error(`multiple connections detected: ${connectionCount}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
log('connected');
|
|
195
|
+
observable.callback.onConnected?.(invitation);
|
|
196
|
+
|
|
197
|
+
// 1. Send request.
|
|
198
|
+
log('sending admission request');
|
|
199
|
+
const { identityKey, haloSpaceKey, genesisFeedKey } = await peer.rpc.HaloHostService.requestAdmission();
|
|
200
|
+
|
|
201
|
+
// 2. Get authentication code.
|
|
202
|
+
// TODO(burdon): Test timeout (options for timeouts at different steps).
|
|
203
|
+
if (invitation.type === undefined || invitation.type === Invitation.Type.INTERACTIVE) {
|
|
204
|
+
log('guest waiting for authentication code...');
|
|
205
|
+
observable.callback.onAuthenticating?.(invitation);
|
|
206
|
+
const authenticationCode = await authenticated.wait({ timeout });
|
|
207
|
+
log('sending authentication request');
|
|
208
|
+
await peer.rpc.HaloHostService.authenticate({ authenticationCode });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 3. Create local identity.
|
|
212
|
+
// TODO(burdon): Abandon if does not complete (otherwise retry will fail).
|
|
213
|
+
const identity = await this._identityManager.acceptIdentity({
|
|
214
|
+
identityKey,
|
|
215
|
+
haloSpaceKey,
|
|
216
|
+
haloGenesisFeedKey: genesisFeedKey
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// 4. Send admission credentials to host (with local identity keys).
|
|
220
|
+
log('presenting admission credentials', { guest: identity.deviceKey, identityKey });
|
|
221
|
+
await peer.rpc.HaloHostService.presentAdmissionCredentials(identity.getAdmissionCredentials());
|
|
222
|
+
|
|
223
|
+
// 5. Success.
|
|
224
|
+
log('admitted by host', { guest: identity.deviceKey, identityKey });
|
|
225
|
+
complete.wake(identityKey);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (!observable.cancelled) {
|
|
228
|
+
log.warn('failed', err);
|
|
229
|
+
observable.callback.onError(err);
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
await peer.close();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
setTimeout(async () => {
|
|
237
|
+
assert(invitation.swarmKey);
|
|
238
|
+
const topic = invitation.swarmKey;
|
|
239
|
+
const peerId = PublicKey.random(); // Use anonymous key.
|
|
240
|
+
swarmConnection = await this._networkManager.openSwarmConnection({
|
|
241
|
+
topic,
|
|
242
|
+
peerId: PublicKey.random(),
|
|
243
|
+
protocol: createProtocolFactory(topic, peerId, [plugin]),
|
|
244
|
+
topology: new StarTopology(topic)
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
observable.callback.onConnecting?.(invitation);
|
|
248
|
+
invitation.identityKey = await complete.wait();
|
|
249
|
+
observable.callback.onSuccess(invitation);
|
|
250
|
+
await swarmConnection.close();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return observable;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { AbstractInvitationsProxy } from './invitations-proxy';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Adapts invitation service observable to client/service stream.
|
|
9
|
+
*/
|
|
10
|
+
export class HaloInvitationsProxy extends AbstractInvitationsProxy {
|
|
11
|
+
getInvitationOptions() {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'assert';
|
|
6
|
+
import { expect } from 'chai';
|
|
7
|
+
import waitForExpect from 'wait-for-expect';
|
|
8
|
+
|
|
9
|
+
import { asyncChain, Trigger } from '@dxos/async';
|
|
10
|
+
import { raise } from '@dxos/debug';
|
|
11
|
+
import { Invitation, HaloInvitationsService } from '@dxos/protocols/proto/dxos/client/services';
|
|
12
|
+
import { afterTest } from '@dxos/testutils';
|
|
13
|
+
|
|
14
|
+
import { ServiceContext } from '../services';
|
|
15
|
+
import { createPeers } from '../testing';
|
|
16
|
+
import { HaloInvitationsProxy } from './halo-invitations-proxy';
|
|
17
|
+
import { HaloInvitationsServiceImpl } from './halo-invitations-service';
|
|
18
|
+
|
|
19
|
+
const closeAfterTest = async (peer: ServiceContext) => {
|
|
20
|
+
afterTest(() => peer.close());
|
|
21
|
+
return peer;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('services/halo-invitation-service', function () {
|
|
25
|
+
it('creates identity and invites peer', async function () {
|
|
26
|
+
const [host, guest] = await asyncChain<ServiceContext>([closeAfterTest])(createPeers(2));
|
|
27
|
+
|
|
28
|
+
await host.identityManager.createIdentity();
|
|
29
|
+
expect(host.identityManager.identity!.authorizedDeviceKeys.size).to.eq(1);
|
|
30
|
+
|
|
31
|
+
// prettier-ignore
|
|
32
|
+
const service1: HaloInvitationsService = new HaloInvitationsServiceImpl(
|
|
33
|
+
host.identityManager,
|
|
34
|
+
host.haloInvitations
|
|
35
|
+
);
|
|
36
|
+
// prettier-ignore
|
|
37
|
+
const service2: HaloInvitationsService = new HaloInvitationsServiceImpl(
|
|
38
|
+
guest.identityManager,
|
|
39
|
+
guest.haloInvitations
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const success1 = new Trigger<Invitation>();
|
|
43
|
+
const success2 = new Trigger<Invitation>();
|
|
44
|
+
|
|
45
|
+
const authenticationCode = new Trigger<string>();
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
const proxy1 = new HaloInvitationsProxy(service1);
|
|
49
|
+
const observable1 = proxy1.createInvitation();
|
|
50
|
+
observable1.subscribe({
|
|
51
|
+
onConnecting: (invitation: Invitation) => {
|
|
52
|
+
const proxy2 = new HaloInvitationsProxy(service2);
|
|
53
|
+
const observable2 = proxy2.acceptInvitation(invitation);
|
|
54
|
+
observable2.subscribe({
|
|
55
|
+
onAuthenticating: async () => {
|
|
56
|
+
await observable2.authenticate(await authenticationCode.wait());
|
|
57
|
+
},
|
|
58
|
+
onSuccess: (invitation: Invitation) => {
|
|
59
|
+
// TODO(burdon): No device.
|
|
60
|
+
// expect(guest.identityManager.identity!.authorizedDeviceKeys.size).to.eq(1);
|
|
61
|
+
success2.wake(invitation);
|
|
62
|
+
},
|
|
63
|
+
onError: (err: Error) => raise(new Error(err.message))
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
onConnected: (invitation: Invitation) => {
|
|
67
|
+
assert(invitation.authenticationCode);
|
|
68
|
+
authenticationCode.wake(invitation.authenticationCode);
|
|
69
|
+
},
|
|
70
|
+
onSuccess: (invitation: Invitation) => {
|
|
71
|
+
success1.wake(invitation);
|
|
72
|
+
},
|
|
73
|
+
onCancelled: () => raise(new Error()),
|
|
74
|
+
onTimeout: (err: Error) => raise(new Error(err.message)),
|
|
75
|
+
onError: (err: Error) => raise(new Error(err.message))
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check same identity.
|
|
80
|
+
const [invitation1, invitation2] = await Promise.all([success1.wait(), success2.wait()]);
|
|
81
|
+
expect(invitation1.identityKey).not.to.exist;
|
|
82
|
+
expect(invitation2.identityKey).to.deep.eq(host.identityManager.identity!.identityKey);
|
|
83
|
+
expect(invitation2.identityKey).to.deep.eq(guest.identityManager.identity!.identityKey);
|
|
84
|
+
expect(invitation1.state).to.eq(Invitation.State.SUCCESS);
|
|
85
|
+
expect(invitation2.state).to.eq(Invitation.State.SUCCESS);
|
|
86
|
+
|
|
87
|
+
// Check devices.
|
|
88
|
+
// TODO(burdon): Incorrect number of devices.
|
|
89
|
+
await waitForExpect(() => {
|
|
90
|
+
expect(host.identityManager.identity!.authorizedDeviceKeys.size).to.eq(2);
|
|
91
|
+
expect(guest.identityManager.identity!.authorizedDeviceKeys.size).to.eq(2);
|
|
92
|
+
});
|
|
93
|
+
// console.log(host.identityManager.identity!.authorizedDeviceKeys.size);
|
|
94
|
+
// console.log(guest.identityManager.identity!.authorizedDeviceKeys.size);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
6
|
+
|
|
7
|
+
import { IdentityManager } from '../identity';
|
|
8
|
+
import { InvitationsHandler } from './invitations-handler';
|
|
9
|
+
import { AbstractInvitationsService } from './invitations-service';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Adapts invitation service observable to client/service stream.
|
|
13
|
+
*/
|
|
14
|
+
export class HaloInvitationsServiceImpl extends AbstractInvitationsService {
|
|
15
|
+
// prettier-ignore
|
|
16
|
+
constructor (
|
|
17
|
+
identityManager: IdentityManager,
|
|
18
|
+
private readonly invitationsHandler: InvitationsHandler
|
|
19
|
+
) {
|
|
20
|
+
super(identityManager, () => invitationsHandler);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override getContext(invitation: Invitation) {}
|
|
24
|
+
}
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './encoder';
|
|
6
|
-
export * from './halo-invitations';
|
|
6
|
+
export * from './halo-invitations-handler';
|
|
7
|
+
export * from './halo-invitations-proxy';
|
|
8
|
+
export * from './halo-invitations-service';
|
|
7
9
|
export * from './invitations';
|
|
10
|
+
export * from './invitations-handler';
|
|
8
11
|
export * from './invitations-proxy';
|
|
12
|
+
export * from './invitations-service';
|
|
9
13
|
export * from './space-invitations-handler';
|
|
10
14
|
export * from './space-invitations-proxy';
|
|
11
15
|
export * from './space-invitations-service';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { NetworkManager } from '@dxos/network-manager';
|
|
6
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
7
|
+
|
|
8
|
+
import { AuthenticatingInvitationObservable, InvitationObservable } from './invitations';
|
|
9
|
+
|
|
10
|
+
export type InvitationsOptions = {
|
|
11
|
+
type?: Invitation.Type;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Common interface for Halo and Space invitation proxies and handlers.
|
|
17
|
+
* Handles the life-cycle of Space invitations between peers.
|
|
18
|
+
*
|
|
19
|
+
* Host
|
|
20
|
+
* - Creates an invitation containing a swarm topic.
|
|
21
|
+
* - Joins the swarm with the topic and waits for guest's admission request.
|
|
22
|
+
* - Responds with admission offer then waits for guest's credentials.
|
|
23
|
+
* - Writes credentials to control feed then exits.
|
|
24
|
+
*
|
|
25
|
+
* Guest
|
|
26
|
+
* - Joins the swarm with the topic.
|
|
27
|
+
* - NOTE: The topic is transmitted out-of-band (e.g., via a QR code).
|
|
28
|
+
* - Sends an admission request.
|
|
29
|
+
* - If Space handler then creates a local cloned space (with genesis block).
|
|
30
|
+
* - Sends admission credentials (containing local device and feed keys).
|
|
31
|
+
*
|
|
32
|
+
* TODO(burdon): Show proxy/service relationship and reference design doc/diagram.
|
|
33
|
+
*
|
|
34
|
+
* ```
|
|
35
|
+
* [Guest] [Host]
|
|
36
|
+
* |-------------------------------------RequestAdmission-->|
|
|
37
|
+
* |<--AdmissionOffer---------------------------------------|
|
|
38
|
+
* |
|
|
39
|
+
* |--------------------------PresentAdmissionCredentials-->|
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export interface InvitationsHandler<T = void> {
|
|
43
|
+
createInvitation(context: T, options?: InvitationsOptions): InvitationObservable;
|
|
44
|
+
acceptInvitation(invitation: Invitation, options?: InvitationsOptions): AuthenticatingInvitationObservable;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Base class for Halo/Space invitations handlers.
|
|
49
|
+
*/
|
|
50
|
+
// TODO(burdon): Extract common functionality.
|
|
51
|
+
export abstract class AbstractInvitationsHandler<T = void> implements InvitationsHandler<T> {
|
|
52
|
+
// prettier-ignore
|
|
53
|
+
protected constructor(
|
|
54
|
+
protected readonly _networkManager: NetworkManager
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
abstract createInvitation(context: T, options?: InvitationsOptions): InvitationObservable;
|
|
58
|
+
abstract acceptInvitation(invitation: Invitation, options?: InvitationsOptions): AuthenticatingInvitationObservable;
|
|
59
|
+
}
|
|
@@ -7,32 +7,34 @@ import assert from 'assert';
|
|
|
7
7
|
import { observableError } from '@dxos/async';
|
|
8
8
|
import { Stream } from '@dxos/codec-protobuf';
|
|
9
9
|
import { log } from '@dxos/log';
|
|
10
|
-
import { Invitation
|
|
10
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
AuthenticatingInvitationObservable,
|
|
14
14
|
AuthenticatingInvitationProvider,
|
|
15
|
-
CreateInvitationsOptions,
|
|
16
|
-
InvitationsProxy,
|
|
17
15
|
InvitationObservable,
|
|
18
|
-
InvitationObservableProvider
|
|
16
|
+
InvitationObservableProvider,
|
|
17
|
+
InvitationsService
|
|
19
18
|
} from './invitations';
|
|
19
|
+
import { InvitationsOptions, InvitationsHandler } from './invitations-handler';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Adapts invitations service observable to client/service stream.
|
|
23
|
-
*
|
|
23
|
+
* Common base class for HALO and Spaces implementations.
|
|
24
24
|
*/
|
|
25
|
-
export
|
|
25
|
+
export interface InvitationsProxy<T = void> extends InvitationsHandler<T> {
|
|
26
|
+
getInvitationOptions(context: T): Invitation;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export abstract class AbstractInvitationsProxy<T = void> implements InvitationsProxy<T> {
|
|
26
30
|
// prettier-ignore
|
|
27
31
|
constructor(
|
|
28
32
|
private readonly _invitationsService: InvitationsService
|
|
29
33
|
) {}
|
|
30
34
|
|
|
31
|
-
abstract
|
|
32
|
-
|
|
33
|
-
createInvitation(context: T, options?: CreateInvitationsOptions): InvitationObservable {
|
|
34
|
-
assert(context);
|
|
35
|
+
abstract getInvitationOptions(context: T): Invitation;
|
|
35
36
|
|
|
37
|
+
createInvitation(context: T, options?: InvitationsOptions): InvitationObservable {
|
|
36
38
|
let invitationId: string;
|
|
37
39
|
const observable = new InvitationObservableProvider(async () => {
|
|
38
40
|
if (invitationId) {
|
|
@@ -40,7 +42,7 @@ export abstract class AbstractInvitationsProxy<T> implements InvitationsProxy<T>
|
|
|
40
42
|
}
|
|
41
43
|
});
|
|
42
44
|
|
|
43
|
-
const invitation = { ...this.
|
|
45
|
+
const invitation: Invitation = { ...this.getInvitationOptions(context), ...options };
|
|
44
46
|
const stream: Stream<Invitation> = this._invitationsService.createInvitation(invitation);
|
|
45
47
|
|
|
46
48
|
stream.subscribe(
|
|
@@ -90,7 +92,7 @@ export abstract class AbstractInvitationsProxy<T> implements InvitationsProxy<T>
|
|
|
90
92
|
return observable;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
acceptInvitation(invitation: Invitation): AuthenticatingInvitationObservable {
|
|
95
|
+
acceptInvitation(invitation: Invitation, options?: InvitationsOptions): AuthenticatingInvitationObservable {
|
|
94
96
|
assert(invitation && invitation.swarmKey);
|
|
95
97
|
|
|
96
98
|
const observable = new AuthenticatingInvitationProvider({
|
|
@@ -107,7 +109,7 @@ export abstract class AbstractInvitationsProxy<T> implements InvitationsProxy<T>
|
|
|
107
109
|
}
|
|
108
110
|
});
|
|
109
111
|
|
|
110
|
-
const stream: Stream<Invitation> = this._invitationsService.acceptInvitation(invitation);
|
|
112
|
+
const stream: Stream<Invitation> = this._invitationsService.acceptInvitation({ ...invitation, ...options });
|
|
111
113
|
|
|
112
114
|
stream.subscribe(
|
|
113
115
|
(invitation: Invitation) => {
|