@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.
Files changed (160) hide show
  1. package/LICENSE +5 -18
  2. package/README.md +14 -10
  3. package/README.yml +5 -0
  4. package/dist/src/packlets/deprecated/profile.d.ts +2 -5
  5. package/dist/src/packlets/deprecated/profile.d.ts.map +1 -1
  6. package/dist/src/packlets/deprecated/profile.js +1 -73
  7. package/dist/src/packlets/deprecated/profile.js.map +1 -1
  8. package/dist/src/packlets/deprecated/system.d.ts +4 -0
  9. package/dist/src/packlets/deprecated/system.d.ts.map +1 -1
  10. package/dist/src/packlets/deprecated/system.js +6 -0
  11. package/dist/src/packlets/deprecated/system.js.map +1 -1
  12. package/dist/src/packlets/deprecated/tracing.d.ts +1 -1
  13. package/dist/src/packlets/deprecated/tracing.js +1 -1
  14. package/dist/src/packlets/devtools/devtools.d.ts +3 -0
  15. package/dist/src/packlets/devtools/devtools.d.ts.map +1 -1
  16. package/dist/src/packlets/devtools/devtools.js +3 -1
  17. package/dist/src/packlets/devtools/devtools.js.map +1 -1
  18. package/dist/src/packlets/identity/devices-service-impl.d.ts.map +1 -1
  19. package/dist/src/packlets/identity/devices-service-impl.js.map +1 -1
  20. package/dist/src/packlets/identity/identity-manager.d.ts +2 -2
  21. package/dist/src/packlets/identity/identity-manager.d.ts.map +1 -1
  22. package/dist/src/packlets/identity/identity-manager.js +44 -43
  23. package/dist/src/packlets/identity/identity-manager.js.map +1 -1
  24. package/dist/src/packlets/identity/identity.d.ts +3 -0
  25. package/dist/src/packlets/identity/identity.d.ts.map +1 -1
  26. package/dist/src/packlets/identity/identity.js +51 -1
  27. package/dist/src/packlets/identity/identity.js.map +1 -1
  28. package/dist/src/packlets/invitations/halo-invitations-handler.d.ts +23 -0
  29. package/dist/src/packlets/invitations/halo-invitations-handler.d.ts.map +1 -0
  30. package/dist/src/packlets/invitations/halo-invitations-handler.js +231 -0
  31. package/dist/src/packlets/invitations/halo-invitations-handler.js.map +1 -0
  32. package/dist/src/packlets/invitations/halo-invitations-handler.test.d.ts +2 -0
  33. package/dist/src/packlets/invitations/halo-invitations-handler.test.d.ts.map +1 -0
  34. package/dist/src/packlets/invitations/halo-invitations-handler.test.js +69 -0
  35. package/dist/src/packlets/invitations/halo-invitations-handler.test.js.map +1 -0
  36. package/dist/src/packlets/invitations/halo-invitations-proxy.d.ts +8 -0
  37. package/dist/src/packlets/invitations/halo-invitations-proxy.d.ts.map +1 -0
  38. package/dist/src/packlets/invitations/halo-invitations-proxy.js +17 -0
  39. package/dist/src/packlets/invitations/halo-invitations-proxy.js.map +1 -0
  40. package/dist/src/packlets/invitations/halo-invitations-service.d.ts +13 -0
  41. package/dist/src/packlets/invitations/halo-invitations-service.d.ts.map +1 -0
  42. package/dist/src/packlets/invitations/halo-invitations-service.js +20 -0
  43. package/dist/src/packlets/invitations/halo-invitations-service.js.map +1 -0
  44. package/dist/src/packlets/invitations/halo-invitations-service.test.d.ts +2 -0
  45. package/dist/src/packlets/invitations/halo-invitations-service.test.d.ts.map +1 -0
  46. package/dist/src/packlets/invitations/halo-invitations-service.test.js +83 -0
  47. package/dist/src/packlets/invitations/halo-invitations-service.test.js.map +1 -0
  48. package/dist/src/packlets/invitations/index.d.ts +5 -1
  49. package/dist/src/packlets/invitations/index.d.ts.map +1 -1
  50. package/dist/src/packlets/invitations/index.js +5 -1
  51. package/dist/src/packlets/invitations/index.js.map +1 -1
  52. package/dist/src/packlets/invitations/invitations-handler.d.ts +48 -0
  53. package/dist/src/packlets/invitations/invitations-handler.d.ts.map +1 -0
  54. package/dist/src/packlets/invitations/invitations-handler.js +18 -0
  55. package/dist/src/packlets/invitations/invitations-handler.js.map +1 -0
  56. package/dist/src/packlets/invitations/invitations-proxy.d.ts +11 -7
  57. package/dist/src/packlets/invitations/invitations-proxy.d.ts.map +1 -1
  58. package/dist/src/packlets/invitations/invitations-proxy.js +5 -10
  59. package/dist/src/packlets/invitations/invitations-proxy.js.map +1 -1
  60. package/dist/src/packlets/invitations/invitations-service.d.ts +25 -0
  61. package/dist/src/packlets/invitations/invitations-service.d.ts.map +1 -0
  62. package/dist/src/packlets/invitations/invitations-service.js +174 -0
  63. package/dist/src/packlets/invitations/invitations-service.js.map +1 -0
  64. package/dist/src/packlets/invitations/invitations.d.ts +10 -42
  65. package/dist/src/packlets/invitations/invitations.d.ts.map +1 -1
  66. package/dist/src/packlets/invitations/invitations.js +5 -2
  67. package/dist/src/packlets/invitations/invitations.js.map +1 -1
  68. package/dist/src/packlets/invitations/space-invitations-handler.d.ts +6 -6
  69. package/dist/src/packlets/invitations/space-invitations-handler.d.ts.map +1 -1
  70. package/dist/src/packlets/invitations/space-invitations-handler.js +18 -19
  71. package/dist/src/packlets/invitations/space-invitations-handler.js.map +1 -1
  72. package/dist/src/packlets/invitations/space-invitations-handler.test.js +5 -5
  73. package/dist/src/packlets/invitations/space-invitations-handler.test.js.map +1 -1
  74. package/dist/src/packlets/invitations/space-invitations-proxy.d.ts +1 -1
  75. package/dist/src/packlets/invitations/space-invitations-proxy.d.ts.map +1 -1
  76. package/dist/src/packlets/invitations/space-invitations-proxy.js +2 -2
  77. package/dist/src/packlets/invitations/space-invitations-proxy.js.map +1 -1
  78. package/dist/src/packlets/invitations/space-invitations-service.d.ts +8 -14
  79. package/dist/src/packlets/invitations/space-invitations-service.d.ts.map +1 -1
  80. package/dist/src/packlets/invitations/space-invitations-service.js +11 -169
  81. package/dist/src/packlets/invitations/space-invitations-service.js.map +1 -1
  82. package/dist/src/packlets/invitations/space-invitations-service.test.js +4 -4
  83. package/dist/src/packlets/invitations/space-invitations-service.test.js.map +1 -1
  84. package/dist/src/packlets/services/service-context.d.ts +2 -2
  85. package/dist/src/packlets/services/service-context.d.ts.map +1 -1
  86. package/dist/src/packlets/services/service-context.js +8 -8
  87. package/dist/src/packlets/services/service-context.js.map +1 -1
  88. package/dist/src/packlets/services/service-definitions.d.ts +5 -4
  89. package/dist/src/packlets/services/service-definitions.d.ts.map +1 -1
  90. package/dist/src/packlets/services/service-definitions.js +6 -4
  91. package/dist/src/packlets/services/service-definitions.js.map +1 -1
  92. package/dist/src/packlets/services/service-host.d.ts.map +1 -1
  93. package/dist/src/packlets/services/service-host.js +10 -8
  94. package/dist/src/packlets/services/service-host.js.map +1 -1
  95. package/dist/src/packlets/services/service-proxy.js +1 -1
  96. package/dist/src/packlets/services/service-registry.test.js +2 -2
  97. package/dist/src/packlets/services/service-registry.test.js.map +1 -1
  98. package/dist/src/packlets/vault/iframe-runtime.d.ts +5 -4
  99. package/dist/src/packlets/vault/iframe-runtime.d.ts.map +1 -1
  100. package/dist/src/packlets/vault/iframe-runtime.js +4 -12
  101. package/dist/src/packlets/vault/iframe-runtime.js.map +1 -1
  102. package/dist/src/packlets/vault/worker-runtime.d.ts +4 -3
  103. package/dist/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  104. package/dist/src/packlets/vault/worker-runtime.js +29 -24
  105. package/dist/src/packlets/vault/worker-runtime.js.map +1 -1
  106. package/dist/src/packlets/vault/worker-session.d.ts +5 -2
  107. package/dist/src/packlets/vault/worker-session.d.ts.map +1 -1
  108. package/dist/src/packlets/vault/worker-session.js +23 -10
  109. package/dist/src/packlets/vault/worker-session.js.map +1 -1
  110. package/package.json +26 -26
  111. package/src/packlets/deprecated/profile.ts +2 -84
  112. package/src/packlets/deprecated/system.ts +7 -0
  113. package/src/packlets/deprecated/tracing.ts +1 -1
  114. package/src/packlets/devtools/devtools.ts +3 -1
  115. package/src/packlets/identity/devices-service-impl.ts +0 -1
  116. package/src/packlets/identity/identity-manager.ts +50 -49
  117. package/src/packlets/identity/identity.ts +58 -1
  118. package/src/packlets/invitations/halo-invitations-handler.test.ts +79 -0
  119. package/src/packlets/invitations/halo-invitations-handler.ts +255 -0
  120. package/src/packlets/invitations/halo-invitations-proxy.ts +14 -0
  121. package/src/packlets/invitations/halo-invitations-service.test.ts +96 -0
  122. package/src/packlets/invitations/halo-invitations-service.ts +24 -0
  123. package/src/packlets/invitations/index.ts +5 -1
  124. package/src/packlets/invitations/invitations-handler.ts +59 -0
  125. package/src/packlets/invitations/invitations-proxy.ts +15 -13
  126. package/src/packlets/invitations/invitations-service.ts +184 -0
  127. package/src/packlets/invitations/invitations.ts +14 -46
  128. package/src/packlets/invitations/space-invitations-handler.test.ts +6 -6
  129. package/src/packlets/invitations/space-invitations-handler.ts +16 -16
  130. package/src/packlets/invitations/space-invitations-proxy.ts +2 -2
  131. package/src/packlets/invitations/space-invitations-service.test.ts +13 -13
  132. package/src/packlets/invitations/space-invitations-service.ts +16 -177
  133. package/src/packlets/services/service-context.ts +6 -6
  134. package/src/packlets/services/service-definitions.ts +20 -10
  135. package/src/packlets/services/service-host.ts +15 -7
  136. package/src/packlets/services/service-proxy.ts +1 -1
  137. package/src/packlets/services/service-registry.test.ts +5 -5
  138. package/src/packlets/vault/iframe-runtime.ts +8 -15
  139. package/src/packlets/vault/worker-runtime.ts +36 -30
  140. package/src/packlets/vault/worker-session.ts +20 -6
  141. package/dist/src/packlets/invitations/halo-invitations.d.ts +0 -23
  142. package/dist/src/packlets/invitations/halo-invitations.d.ts.map +0 -1
  143. package/dist/src/packlets/invitations/halo-invitations.js +0 -154
  144. package/dist/src/packlets/invitations/halo-invitations.js.map +0 -1
  145. package/dist/src/packlets/invitations/halo-invitations.test.d.ts +0 -2
  146. package/dist/src/packlets/invitations/halo-invitations.test.d.ts.map +0 -1
  147. package/dist/src/packlets/invitations/halo-invitations.test.js +0 -34
  148. package/dist/src/packlets/invitations/halo-invitations.test.js.map +0 -1
  149. package/dist/src/packlets/services/iframe-service-proxy.d.ts +0 -23
  150. package/dist/src/packlets/services/iframe-service-proxy.d.ts.map +0 -1
  151. package/dist/src/packlets/services/iframe-service-proxy.js +0 -53
  152. package/dist/src/packlets/services/iframe-service-proxy.js.map +0 -1
  153. package/dist/src/packlets/services/service-host.test.d.ts +0 -2
  154. package/dist/src/packlets/services/service-host.test.d.ts.map +0 -1
  155. package/dist/src/packlets/services/service-host.test.js +0 -68
  156. package/dist/src/packlets/services/service-host.test.js.map +0 -1
  157. package/src/packlets/invitations/halo-invitations.test.ts +0 -40
  158. package/src/packlets/invitations/halo-invitations.ts +0 -164
  159. package/src/packlets/services/iframe-service-proxy.ts +0 -71
  160. 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, InvitationsService } from '@dxos/protocols/proto/dxos/client/services';
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
- * Used by both HALO and Spaces client/service interfaces.
23
+ * Common base class for HALO and Spaces implementations.
24
24
  */
25
- export abstract class AbstractInvitationsProxy<T> implements InvitationsProxy<T> {
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 createInvitationObject(context: T): Invitation;
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.createInvitationObject(context), ...options };
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) => {