@dxos/client-services 0.1.35 → 0.1.36

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 (95) hide show
  1. package/dist/lib/browser/{chunk-H5IXSSI7.mjs → chunk-2AYYVPWN.mjs} +835 -950
  2. package/dist/lib/browser/chunk-2AYYVPWN.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +9 -11
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/packlets/testing/index.mjs +212 -74
  6. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  7. package/dist/lib/node/index.cjs +942 -1058
  8. package/dist/lib/node/index.cjs.map +4 -4
  9. package/dist/lib/node/meta.json +1 -1
  10. package/dist/lib/node/packlets/testing/index.cjs +1089 -1067
  11. package/dist/lib/node/packlets/testing/index.cjs.map +4 -4
  12. package/dist/types/src/packlets/identity/identity-manager.d.ts +1 -0
  13. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  14. package/dist/types/src/packlets/identity/identity.d.ts +3 -3
  15. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  16. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +18 -0
  17. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -0
  18. package/dist/types/src/packlets/invitations/device-invitation-protocol.test.d.ts +2 -0
  19. package/dist/types/src/packlets/invitations/device-invitation-protocol.test.d.ts.map +1 -0
  20. package/dist/types/src/packlets/invitations/index.d.ts +4 -4
  21. package/dist/types/src/packlets/invitations/index.d.ts.map +1 -1
  22. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +12 -0
  23. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -0
  24. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +38 -0
  25. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -0
  26. package/dist/types/src/packlets/invitations/invitations-service.d.ts +13 -15
  27. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  28. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +22 -0
  29. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -0
  30. package/dist/types/src/packlets/invitations/space-invitation-protocol.test.d.ts +2 -0
  31. package/dist/types/src/packlets/invitations/space-invitation-protocol.test.d.ts.map +1 -0
  32. package/dist/types/src/packlets/services/service-context.d.ts +6 -4
  33. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  34. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  35. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +2 -0
  36. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  37. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  38. package/dist/types/src/packlets/testing/invitation-utils.d.ts +31 -2
  39. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  40. package/dist/types/src/packlets/testing/test-builder.d.ts +3 -3
  41. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  42. package/dist/types/src/packlets/tests/invitations.test.d.ts +2 -0
  43. package/dist/types/src/packlets/tests/invitations.test.d.ts.map +1 -0
  44. package/dist/types/src/packlets/vault/worker-runtime.d.ts +2 -0
  45. package/dist/types/src/packlets/vault/worker-runtime.d.ts.map +1 -1
  46. package/package.json +30 -30
  47. package/src/packlets/identity/identity-manager.ts +2 -1
  48. package/src/packlets/identity/identity.ts +3 -3
  49. package/src/packlets/invitations/{device-invitations-handler.test.ts → device-invitation-protocol.test.ts} +3 -3
  50. package/src/packlets/invitations/device-invitation-protocol.ts +83 -0
  51. package/src/packlets/invitations/index.ts +4 -4
  52. package/src/packlets/invitations/invitation-protocol.ts +21 -0
  53. package/src/packlets/invitations/invitations-handler.ts +462 -0
  54. package/src/packlets/invitations/invitations-service.ts +127 -104
  55. package/src/packlets/invitations/space-invitation-protocol.test.ts +221 -0
  56. package/src/packlets/invitations/space-invitation-protocol.ts +117 -0
  57. package/src/packlets/services/service-context.test.ts +11 -4
  58. package/src/packlets/services/service-context.ts +40 -19
  59. package/src/packlets/services/service-host.ts +4 -12
  60. package/src/packlets/services/service-registry.test.ts +23 -25
  61. package/src/packlets/spaces/data-space-manager.ts +5 -0
  62. package/src/packlets/spaces/spaces-service.ts +7 -2
  63. package/src/packlets/storage/storage.ts +2 -0
  64. package/src/packlets/testing/invitation-utils.ts +219 -31
  65. package/src/packlets/testing/test-builder.ts +33 -22
  66. package/src/packlets/tests/client-services.test.ts +33 -76
  67. package/src/packlets/tests/halo-profile.test.ts +2 -28
  68. package/src/packlets/tests/invitations.test.ts +389 -0
  69. package/src/packlets/tests/spaces-invitations.test.ts +12 -30
  70. package/src/packlets/tests/spaces.test.ts +30 -28
  71. package/src/packlets/vault/worker-runtime.ts +4 -0
  72. package/dist/lib/browser/chunk-H5IXSSI7.mjs.map +0 -7
  73. package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts +0 -30
  74. package/dist/types/src/packlets/invitations/device-invitations-handler.d.ts.map +0 -1
  75. package/dist/types/src/packlets/invitations/device-invitations-handler.test.d.ts +0 -2
  76. package/dist/types/src/packlets/invitations/device-invitations-handler.test.d.ts.map +0 -1
  77. package/dist/types/src/packlets/invitations/device-invitations-service.d.ts +0 -13
  78. package/dist/types/src/packlets/invitations/device-invitations-service.d.ts.map +0 -1
  79. package/dist/types/src/packlets/invitations/device-invitations-service.test.d.ts +0 -2
  80. package/dist/types/src/packlets/invitations/device-invitations-service.test.d.ts.map +0 -1
  81. package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts +0 -26
  82. package/dist/types/src/packlets/invitations/space-invitations-handler.d.ts.map +0 -1
  83. package/dist/types/src/packlets/invitations/space-invitations-handler.test.d.ts +0 -2
  84. package/dist/types/src/packlets/invitations/space-invitations-handler.test.d.ts.map +0 -1
  85. package/dist/types/src/packlets/invitations/space-invitations-service.d.ts +0 -16
  86. package/dist/types/src/packlets/invitations/space-invitations-service.d.ts.map +0 -1
  87. package/dist/types/src/packlets/invitations/space-invitations-service.test.d.ts +0 -2
  88. package/dist/types/src/packlets/invitations/space-invitations-service.test.d.ts.map +0 -1
  89. package/src/packlets/invitations/device-invitations-handler.ts +0 -370
  90. package/src/packlets/invitations/device-invitations-service.test.ts +0 -96
  91. package/src/packlets/invitations/device-invitations-service.ts +0 -24
  92. package/src/packlets/invitations/space-invitations-handler.test.ts +0 -246
  93. package/src/packlets/invitations/space-invitations-handler.ts +0 -463
  94. package/src/packlets/invitations/space-invitations-service.test.ts +0 -129
  95. package/src/packlets/invitations/space-invitations-service.ts +0 -35
@@ -4,92 +4,98 @@
4
4
 
5
5
  import assert from 'node:assert';
6
6
 
7
- import { TimeoutError } from '@dxos/async';
8
- import {
9
- AuthenticatingInvitationObservable,
10
- CancellableInvitationObservable,
11
- InvitationsService,
12
- InvitationsHandler,
13
- InvitationsOptions
14
- } from '@dxos/client';
7
+ import { AuthenticatingInvitationObservable, CancellableInvitationObservable } from '@dxos/client';
15
8
  import { Stream } from '@dxos/codec-protobuf';
16
9
  import { log } from '@dxos/log';
17
- import { AuthenticationRequest, Invitation } from '@dxos/protocols/proto/dxos/client/services';
18
- import { Provider } from '@dxos/util';
10
+ import { AuthenticationRequest, Invitation, InvitationsService } from '@dxos/protocols/proto/dxos/client/services';
19
11
 
20
- import { IdentityManager } from '../identity';
12
+ import { InvitationProtocol } from './invitation-protocol';
13
+ import { InvitationsHandler } from './invitations-handler';
21
14
 
22
15
  /**
23
16
  * Adapts invitation service observable to client/service stream.
24
17
  */
25
- export abstract class AbstractInvitationsService<T = void> implements InvitationsService {
18
+ export class InvitationsServiceImpl implements InvitationsService {
26
19
  private readonly _createInvitations = new Map<string, CancellableInvitationObservable>();
27
20
  private readonly _acceptInvitations = new Map<string, AuthenticatingInvitationObservable>();
28
21
 
29
- // prettier-ignore
30
- protected constructor (
31
- private readonly _identityManager: IdentityManager,
32
- private readonly _getInvitationsHandler: Provider<InvitationsHandler<T>>
22
+ constructor(
23
+ private readonly _invitationsHandler: InvitationsHandler,
24
+ private readonly _getHandler: (invitation: Invitation) => InvitationProtocol
33
25
  ) {}
34
26
 
35
27
  // TODO(burdon): Guest/host label.
36
28
  getLoggingContext() {
37
29
  return {
38
- deviceKey: this._identityManager.identity?.deviceKey
30
+ // deviceKey: this._identityManager.identity?.deviceKey
39
31
  };
40
32
  }
41
33
 
42
- abstract getContext(invitation: Invitation): T;
43
-
44
34
  createInvitation(invitation: Invitation): Stream<Invitation> {
45
35
  return new Stream<Invitation>(({ next, close }) => {
46
- const invitationsHandler: InvitationsHandler<T> = this._getInvitationsHandler();
47
- const context = this.getContext(invitation);
36
+ const handler = this._getHandler(invitation);
48
37
  log('stream opened', this.getLoggingContext());
49
38
 
50
39
  let invitationId: string;
51
- const { type, swarmKey, authMethod } = invitation;
52
- const observable = invitationsHandler.createInvitation(context, { type, swarmKey, authMethod });
53
- observable.subscribe({
54
- onConnecting: (invitation) => {
55
- assert(invitation.invitationId);
56
- invitationId = invitation.invitationId;
57
- this._createInvitations.set(invitation.invitationId, observable);
58
- invitation.state = Invitation.State.CONNECTING;
59
- next(invitation);
60
- },
61
- onConnected: (invitation) => {
62
- assert(invitation.invitationId);
63
- invitation.state = Invitation.State.CONNECTED;
64
- next(invitation);
65
- },
66
- onAuthenticating: (invitation) => {
67
- assert(invitation.invitationId);
68
- invitation.state = Invitation.State.AUTHENTICATING;
69
- next(invitation);
70
- },
71
- onSuccess: (invitation) => {
72
- assert(invitation.invitationId);
73
- invitation.state = Invitation.State.SUCCESS;
74
- next(invitation);
75
- close();
40
+ const observable = this._invitationsHandler.createInvitation(handler, invitation);
41
+ observable.subscribe(
42
+ (invitation) => {
43
+ switch (invitation.state) {
44
+ case Invitation.State.CONNECTING: {
45
+ assert(invitation.invitationId);
46
+ invitationId = invitation.invitationId;
47
+ this._createInvitations.set(invitation.invitationId, observable);
48
+ invitation.state = Invitation.State.CONNECTING;
49
+ next(invitation);
50
+ break;
51
+ }
52
+ case Invitation.State.CONNECTED: {
53
+ assert(invitation.invitationId);
54
+ invitation.state = Invitation.State.CONNECTED;
55
+ next(invitation);
56
+ break;
57
+ }
58
+ case Invitation.State.READY_FOR_AUTHENTICATION: {
59
+ assert(invitation.invitationId);
60
+ invitation.state = Invitation.State.READY_FOR_AUTHENTICATION;
61
+ next(invitation);
62
+ break;
63
+ }
64
+ case Invitation.State.AUTHENTICATING: {
65
+ assert(invitation.invitationId);
66
+ invitation.state = Invitation.State.AUTHENTICATING;
67
+ next(invitation);
68
+ break;
69
+ }
70
+ case Invitation.State.SUCCESS: {
71
+ assert(invitation.invitationId);
72
+ invitation.state = Invitation.State.SUCCESS;
73
+ next(invitation);
74
+ break;
75
+ }
76
+ case Invitation.State.CANCELLED: {
77
+ assert(invitationId);
78
+ invitation.invitationId = invitationId;
79
+ invitation.state = Invitation.State.CANCELLED;
80
+ next(invitation);
81
+ break;
82
+ }
83
+ case Invitation.State.TIMEOUT: {
84
+ assert(invitationId);
85
+ invitation.invitationId = invitationId;
86
+ invitation.state = Invitation.State.TIMEOUT;
87
+ next(invitation);
88
+ break;
89
+ }
90
+ }
76
91
  },
77
- onCancelled: () => {
78
- assert(invitationId);
79
- invitation.invitationId = invitationId;
80
- invitation.state = Invitation.State.CANCELLED;
81
- next(invitation);
82
- close();
83
- },
84
- onTimeout: (err: TimeoutError) => {
85
- invitation.state = Invitation.State.TIMEOUT;
92
+ (err: Error) => {
86
93
  close(err);
87
94
  },
88
- onError: (err: any) => {
89
- invitation.state = Invitation.State.ERROR;
90
- close(err);
95
+ () => {
96
+ close();
91
97
  }
92
- });
98
+ );
93
99
 
94
100
  return (err?: Error) => {
95
101
  const context = this.getLoggingContext();
@@ -104,52 +110,70 @@ export abstract class AbstractInvitationsService<T = void> implements Invitation
104
110
  });
105
111
  }
106
112
 
107
- acceptInvitation(invitation: Invitation, options?: InvitationsOptions): Stream<Invitation> {
113
+ acceptInvitation(invitation: Invitation): Stream<Invitation> {
108
114
  return new Stream<Invitation>(({ next, close }) => {
109
115
  log('stream opened', this.getLoggingContext());
110
- const invitationsHandler = this._getInvitationsHandler();
116
+ const handler = this._getHandler(invitation);
111
117
 
112
118
  let invitationId: string;
113
- const observable = invitationsHandler.acceptInvitation(invitation, options);
114
- observable.subscribe({
115
- onConnecting: (invitation) => {
116
- assert(invitation.invitationId);
117
- invitationId = invitation.invitationId;
118
- this._acceptInvitations.set(invitation.invitationId, observable);
119
- invitation.state = Invitation.State.CONNECTING;
120
- next(invitation);
121
- },
122
- onConnected: (invitation) => {
123
- assert(invitation.invitationId);
124
- invitation.state = Invitation.State.CONNECTED;
125
- next(invitation);
119
+ const observable = this._invitationsHandler.acceptInvitation(handler, invitation);
120
+ observable.subscribe(
121
+ (invitation) => {
122
+ switch (invitation.state) {
123
+ case Invitation.State.CONNECTING: {
124
+ assert(invitation.invitationId);
125
+ invitationId = invitation.invitationId;
126
+ this._acceptInvitations.set(invitation.invitationId, observable);
127
+ invitation.state = Invitation.State.CONNECTING;
128
+ next(invitation);
129
+ break;
130
+ }
131
+ case Invitation.State.CONNECTED: {
132
+ assert(invitation.invitationId);
133
+ invitation.state = Invitation.State.CONNECTED;
134
+ next(invitation);
135
+ break;
136
+ }
137
+ case Invitation.State.READY_FOR_AUTHENTICATION: {
138
+ assert(invitation.invitationId);
139
+ invitation.state = Invitation.State.READY_FOR_AUTHENTICATION;
140
+ next(invitation);
141
+ break;
142
+ }
143
+ case Invitation.State.AUTHENTICATING: {
144
+ assert(invitation.invitationId);
145
+ invitation.state = Invitation.State.AUTHENTICATING;
146
+ next(invitation);
147
+ break;
148
+ }
149
+ case Invitation.State.SUCCESS: {
150
+ invitation.state = Invitation.State.SUCCESS;
151
+ next(invitation);
152
+ break;
153
+ }
154
+ case Invitation.State.CANCELLED: {
155
+ assert(invitationId);
156
+ invitation.invitationId = invitationId;
157
+ invitation.state = Invitation.State.CANCELLED;
158
+ next(invitation);
159
+ break;
160
+ }
161
+ case Invitation.State.TIMEOUT: {
162
+ assert(invitationId);
163
+ invitation.invitationId = invitationId;
164
+ invitation.state = Invitation.State.TIMEOUT;
165
+ next(invitation);
166
+ break;
167
+ }
168
+ }
126
169
  },
127
- onAuthenticating: (invitation) => {
128
- assert(invitation.invitationId);
129
- invitation.state = Invitation.State.AUTHENTICATING;
130
- next(invitation);
131
- },
132
- onSuccess: (invitation) => {
133
- invitation.state = Invitation.State.SUCCESS;
134
- next(invitation);
135
- close();
136
- },
137
- onCancelled: () => {
138
- assert(invitationId);
139
- invitation.invitationId = invitationId;
140
- invitation.state = Invitation.State.CANCELLED;
141
- next(invitation);
142
- close();
143
- },
144
- onTimeout: (err: TimeoutError) => {
145
- invitation.state = Invitation.State.TIMEOUT;
170
+ (err: Error) => {
146
171
  close(err);
147
172
  },
148
- onError: (err: any) => {
149
- invitation.state = Invitation.State.ERROR;
150
- close(err);
173
+ () => {
174
+ close();
151
175
  }
152
- });
176
+ );
153
177
 
154
178
  return (err?: Error) => {
155
179
  const context = this.getLoggingContext();
@@ -164,24 +188,23 @@ export abstract class AbstractInvitationsService<T = void> implements Invitation
164
188
  });
165
189
  }
166
190
 
167
- async authenticate({ invitationId, authenticationCode }: AuthenticationRequest): Promise<void> {
191
+ async authenticate({ invitationId, authCode }: AuthenticationRequest): Promise<void> {
168
192
  log('authenticating...');
169
193
  assert(invitationId);
170
194
  const observable = this._acceptInvitations.get(invitationId);
171
195
  if (!observable) {
172
196
  log.warn('invalid invitation', { invitationId });
173
197
  } else {
174
- await observable.authenticate(authenticationCode);
198
+ await observable.authenticate(authCode);
175
199
  }
176
200
  }
177
201
 
178
- async cancelInvitation(invitation: Invitation): Promise<void> {
202
+ async cancelInvitation({ invitationId }: { invitationId: string }): Promise<void> {
179
203
  log('cancelling...');
180
- assert(invitation.invitationId);
181
- const observable =
182
- this._createInvitations.get(invitation.invitationId) ?? this._acceptInvitations.get(invitation.invitationId);
204
+ assert(invitationId);
205
+ const observable = this._createInvitations.get(invitationId) ?? this._acceptInvitations.get(invitationId);
183
206
  if (!observable) {
184
- log.warn('invalid invitation', { invitationId: invitation.invitationId });
207
+ log.warn('invalid invitation', { invitationId });
185
208
  } else {
186
209
  await observable?.cancel();
187
210
  }
@@ -0,0 +1,221 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { asyncChain, Trigger } from '@dxos/async';
8
+ import { raise } from '@dxos/debug';
9
+ import { testLocalDatabase } from '@dxos/echo-pipeline/testing';
10
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
11
+ import { afterTest, describe, test } from '@dxos/test';
12
+
13
+ import { ServiceContext } from '../services';
14
+ import { createIdentity, createPeers, syncItemsLocal } from '../testing';
15
+ import { performInvitation } from '../testing/invitation-utils';
16
+
17
+ const closeAfterTest = async (peer: ServiceContext) => {
18
+ afterTest(() => peer.close());
19
+ return peer;
20
+ };
21
+
22
+ describe('services/space-invitations-protocol', () => {
23
+ test('genesis', async () => {
24
+ const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
25
+
26
+ const space = await peer.dataSpaceManager!.createSpace();
27
+ expect(peer.dataSpaceManager!.spaces.has(space.key)).to.be.true;
28
+
29
+ await space.close();
30
+ });
31
+
32
+ test('genesis & ready', async () => {
33
+ const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
34
+
35
+ const space = await peer.dataSpaceManager!.createSpace();
36
+ expect(peer.dataSpaceManager!.spaces.has(space.key)).to.be.true;
37
+
38
+ await peer.dataSpaceManager?.waitUntilSpaceReady(space.key);
39
+ await space.close();
40
+ });
41
+
42
+ test('genesis with database mutations', async () => {
43
+ const [peer] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(1));
44
+ const space = await peer.dataSpaceManager!.createSpace();
45
+ afterTest(() => space.close());
46
+
47
+ await testLocalDatabase(space.dataPipeline);
48
+ });
49
+
50
+ test('invitation with no auth', async () => {
51
+ const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
52
+
53
+ const space1 = await host.dataSpaceManager!.createSpace();
54
+ const spaceKey = space1.key;
55
+
56
+ await Promise.all(performInvitation({ host, guest, options: { kind: Invitation.Kind.SPACE, spaceKey } }));
57
+
58
+ {
59
+ const space1 = host.dataSpaceManager!.spaces.get(spaceKey)!;
60
+ const space2 = guest.dataSpaceManager!.spaces.get(spaceKey)!;
61
+ expect(space1).not.to.be.undefined;
62
+ expect(space2).not.to.be.undefined;
63
+
64
+ await host.dataSpaceManager?.waitUntilSpaceReady(space1.key);
65
+ await guest.dataSpaceManager?.waitUntilSpaceReady(space2.key);
66
+
67
+ await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
68
+
69
+ await space1.close();
70
+ await space2.close();
71
+ }
72
+ });
73
+
74
+ test('creates and accepts invitation with retry', async () => {
75
+ const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
76
+
77
+ let attempt = 0;
78
+
79
+ const space1 = await host.dataSpaceManager!.createSpace();
80
+
81
+ const [{ invitation: invitation1 }, { invitation: invitation2 }] = await Promise.all(
82
+ performInvitation({
83
+ host,
84
+ guest,
85
+ options: { kind: Invitation.Kind.SPACE, spaceKey: space1.key },
86
+ hooks: {
87
+ guest: {
88
+ onReady: (invitation) => {
89
+ if (attempt === 0) {
90
+ // Force retry.
91
+ void invitation.authenticate('000000');
92
+ attempt++;
93
+ return true;
94
+ }
95
+
96
+ return false;
97
+ }
98
+ }
99
+ }
100
+ })
101
+ );
102
+
103
+ expect(attempt).to.eq(1);
104
+ expect(invitation1?.spaceKey).to.exist;
105
+ expect(invitation2?.spaceKey).to.exist;
106
+ expect(invitation1?.spaceKey).to.deep.eq(invitation2?.spaceKey);
107
+
108
+ {
109
+ const space1 = host.dataSpaceManager!.spaces.get(invitation1!.spaceKey!)!;
110
+ const space2 = guest.dataSpaceManager!.spaces.get(invitation2!.spaceKey!)!;
111
+ expect(space1).not.to.be.undefined;
112
+ expect(space2).not.to.be.undefined;
113
+
114
+ await host.dataSpaceManager?.waitUntilSpaceReady(space1.key);
115
+ await guest.dataSpaceManager?.waitUntilSpaceReady(space2.key);
116
+
117
+ await syncItemsLocal(space1.dataPipeline, space2.dataPipeline);
118
+
119
+ await space1.close();
120
+ await space2.close();
121
+ }
122
+
123
+ expect(
124
+ guest.identityManager.identity?.space.spaceState.getCredentialsOfType('dxos.halo.credentials.SpaceMember').length
125
+ ).to.equal(2); // own halo + newly joined space.
126
+ });
127
+
128
+ test('cancels invitation', async () => {
129
+ const [host, guest] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(createPeers(2));
130
+
131
+ const hostConnected = new Trigger<Invitation>();
132
+ const guestConnected = new Trigger<Invitation>();
133
+
134
+ const space1 = await host.dataSpaceManager!.createSpace();
135
+
136
+ const invitationPromises = performInvitation({
137
+ host,
138
+ guest,
139
+ options: { kind: Invitation.Kind.SPACE, spaceKey: space1.key },
140
+ hooks: {
141
+ host: {
142
+ onConnecting: (invitation) => {
143
+ hostConnected.wake(invitation.get());
144
+ },
145
+ onConnected: (invitation) => {
146
+ void invitation.cancel();
147
+ return true;
148
+ },
149
+ onSuccess: () => raise(new Error('invitation success'))
150
+ },
151
+ guest: {
152
+ onConnecting: (invitation) => {
153
+ guestConnected.wake(invitation.get());
154
+ }
155
+ }
156
+ }
157
+ });
158
+
159
+ const { swarmKey: swarmKey1 } = await hostConnected.wait();
160
+ const { swarmKey: swarmKey2 } = await guestConnected.wait();
161
+ expect(swarmKey1).to.deep.eq(swarmKey2);
162
+
163
+ const [{ invitation: invitation1 }, { error }] = await Promise.all(invitationPromises);
164
+ expect(invitation1?.state).to.eq(Invitation.State.CANCELLED);
165
+ expect(error).to.exist;
166
+
167
+ await space1.close();
168
+ });
169
+
170
+ // TODO(burdon): Flaky.
171
+ // test.skip('test multi-use invitation', async () => {
172
+ // const GUEST_COUNT = 3;
173
+ // const [host, ...guests] = await asyncChain<ServiceContext>([createIdentity, closeAfterTest])(
174
+ // createPeers(GUEST_COUNT + 1)
175
+ // );
176
+
177
+ // const hostSpace = await host.dataSpaceManager!.createSpace();
178
+ // const swarmKey = PublicKey.random();
179
+ // const hostObservable = await host.spaceInvitations!.createInvitation(hostSpace, {
180
+ // swarmKey,
181
+ // type: Invitation.Type.MULTIUSE_TESTING
182
+ // });
183
+
184
+ // const [done, count] = latch({ count: GUEST_COUNT });
185
+ // hostObservable.subscribe({
186
+ // onConnecting: async (invitation2: Invitation) => {},
187
+ // onConnected: async (invitation2: Invitation) => {},
188
+ // onSuccess: () => {
189
+ // count();
190
+ // },
191
+ // onCancelled: () => {},
192
+ // onTimeout: (err: Error) => raise(err),
193
+ // onError: (err: Error) => raise(err)
194
+ // });
195
+
196
+ // await Promise.all(
197
+ // range(GUEST_COUNT).map(async (idx) => {
198
+ // const observable = await guests[idx].spaceInvitations!.acceptInvitation({
199
+ // swarmKey,
200
+ // type: Invitation.Type.MULTIUSE_TESTING
201
+ // });
202
+ // const success = new Trigger();
203
+ // observable.subscribe({
204
+ // onConnecting: async (invitation2: Invitation) => {},
205
+ // onConnected: async (invitation2: Invitation) => {},
206
+ // onSuccess: () => {
207
+ // success.wake();
208
+ // },
209
+ // onCancelled: () => raise(new Error()),
210
+ // onTimeout: (err: Error) => raise(err),
211
+ // onError: (err: Error) => raise(err)
212
+ // });
213
+ // await success.wait({ timeout: 300 });
214
+ // })
215
+ // );
216
+ // await done();
217
+
218
+ // await hostObservable.cancel();
219
+ // await hostSpace.close();
220
+ // });
221
+ });
@@ -0,0 +1,117 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import assert from 'node:assert';
6
+
7
+ import { createAdmissionCredentials, getCredentialAssertion } from '@dxos/credentials';
8
+ import { SigningContext } from '@dxos/echo-pipeline';
9
+ import { writeMessages } from '@dxos/feed-store';
10
+ import { Keyring } from '@dxos/keyring';
11
+ import { PublicKey } from '@dxos/keys';
12
+ import { log } from '@dxos/log';
13
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
14
+ import { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
15
+ import { ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
16
+ import { AdmissionRequest, AdmissionResponse, IntroductionRequest } from '@dxos/protocols/proto/dxos/halo/invitations';
17
+
18
+ import { DataSpaceManager } from '../spaces';
19
+ import { InvitationProtocol } from './invitation-protocol';
20
+
21
+ export class SpaceInvitationProtocol implements InvitationProtocol {
22
+ constructor(
23
+ private readonly _spaceManager: DataSpaceManager,
24
+ private readonly _signingContext: SigningContext,
25
+ private readonly _keyring: Keyring,
26
+ private readonly _spaceKey?: PublicKey
27
+ ) {}
28
+
29
+ toJSON(): object {
30
+ return {
31
+ deviceKey: this._signingContext.deviceKey,
32
+ spaceKey: this._spaceKey
33
+ };
34
+ }
35
+
36
+ getInvitationContext(): Partial<Invitation> & Pick<Invitation, 'kind'> {
37
+ return {
38
+ kind: Invitation.Kind.SPACE,
39
+ spaceKey: this._spaceKey
40
+ };
41
+ }
42
+
43
+ async admit(request: AdmissionRequest, guestProfile?: ProfileDocument | undefined): Promise<AdmissionResponse> {
44
+ assert(this._spaceKey);
45
+ const space = await this._spaceManager.spaces.get(this._spaceKey);
46
+ assert(space);
47
+
48
+ assert(request.space);
49
+ const { identityKey, deviceKey } = request.space;
50
+
51
+ log('writing guest credentials', { host: this._signingContext.deviceKey, guest: deviceKey });
52
+ // TODO(burdon): Check if already admitted.
53
+ const credentials: FeedMessage.Payload[] = await createAdmissionCredentials(
54
+ this._signingContext.credentialSigner,
55
+ identityKey,
56
+ space.key,
57
+ space.inner.genesisFeedKey,
58
+ guestProfile
59
+ );
60
+
61
+ // TODO(dmaretskyi): Refactor.
62
+ assert(credentials[0].credential);
63
+ const spaceMemberCredential = credentials[0].credential.credential;
64
+ assert(getCredentialAssertion(spaceMemberCredential)['@type'] === 'dxos.halo.credentials.SpaceMember');
65
+
66
+ await writeMessages(space.inner.controlPipeline.writer, credentials);
67
+
68
+ return {
69
+ space: {
70
+ credential: spaceMemberCredential,
71
+ controlTimeframe: space.inner.controlPipeline.state.timeframe,
72
+ dataTimeframe: space.dataPipeline.pipelineState?.timeframe
73
+ }
74
+ };
75
+ }
76
+
77
+ createIntroduction(): IntroductionRequest {
78
+ return {
79
+ profile: this._signingContext.profile
80
+ };
81
+ }
82
+
83
+ async createAdmissionRequest(): Promise<AdmissionRequest> {
84
+ // Generate a pair of keys for our feeds.
85
+ const controlFeedKey = await this._keyring.createKey();
86
+ const dataFeedKey = await this._keyring.createKey();
87
+
88
+ return {
89
+ space: {
90
+ identityKey: this._signingContext.identityKey,
91
+ deviceKey: this._signingContext.deviceKey,
92
+ controlFeedKey,
93
+ dataFeedKey
94
+ }
95
+ };
96
+ }
97
+
98
+ async accept(response: AdmissionResponse): Promise<Partial<Invitation>> {
99
+ assert(response.space);
100
+ const { credential, controlTimeframe, dataTimeframe } = response.space;
101
+ const assertion = getCredentialAssertion(credential);
102
+ assert(assertion['@type'] === 'dxos.halo.credentials.SpaceMember', 'Invalid credential');
103
+ assert(credential.subject.id.equals(this._signingContext.identityKey));
104
+
105
+ // Create local space.
106
+ await this._spaceManager.acceptSpace({
107
+ spaceKey: assertion.spaceKey,
108
+ genesisFeedKey: assertion.genesisFeedKey,
109
+ controlTimeframe,
110
+ dataTimeframe
111
+ });
112
+
113
+ await this._signingContext.recordCredential(credential);
114
+
115
+ return { spaceKey: assertion.spaceKey };
116
+ }
117
+ }
@@ -2,6 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { Invitation } from '@dxos/client';
5
6
  import { MemorySignalManagerContext } from '@dxos/messaging';
6
7
  import { describe, test } from '@dxos/test';
7
8
 
@@ -16,7 +17,7 @@ describe('services/ServiceContext', () => {
16
17
  const space1 = await device1.dataSpaceManager!.createSpace();
17
18
 
18
19
  const device2 = createServiceContext({ signalContext: networkContext });
19
- await performInvitation(device1.deviceInvitations, device2.deviceInvitations, undefined);
20
+ await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
20
21
 
21
22
  await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
22
23
  const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);
@@ -29,7 +30,7 @@ describe('services/ServiceContext', () => {
29
30
  await device1.createIdentity();
30
31
 
31
32
  const device2 = createServiceContext({ signalContext: networkContext });
32
- await performInvitation(device1.deviceInvitations, device2.deviceInvitations, undefined);
33
+ await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
33
34
 
34
35
  const space1 = await device1.dataSpaceManager!.createSpace();
35
36
  await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
@@ -43,12 +44,18 @@ describe('services/ServiceContext', () => {
43
44
  await device1.createIdentity();
44
45
 
45
46
  const device2 = createServiceContext({ signalContext: networkContext });
46
- await performInvitation(device1.deviceInvitations, device2.deviceInvitations, undefined);
47
+ await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
47
48
 
48
49
  const identity2 = createServiceContext({ signalContext: networkContext });
49
50
  await identity2.createIdentity();
50
51
  const space1 = await identity2.dataSpaceManager!.createSpace();
51
- await performInvitation(identity2.spaceInvitations!, device1.spaceInvitations!, space1);
52
+ await Promise.all(
53
+ performInvitation({
54
+ host: identity2,
55
+ guest: device1,
56
+ options: { kind: Invitation.Kind.SPACE, spaceKey: space1.key }
57
+ })
58
+ );
52
59
 
53
60
  await device2.dataSpaceManager!.waitUntilSpaceReady(space1!.key);
54
61
  const space2 = await device2.dataSpaceManager!.spaces.get(space1.key);