@dxos/client-services 0.4.10-main.fe71b4c → 0.4.10-next.169e4e3

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 (63) hide show
  1. package/dist/lib/browser/{chunk-7S34JE6M.mjs → chunk-KCCL73B5.mjs} +625 -508
  2. package/dist/lib/browser/chunk-KCCL73B5.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3 -1
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/packlets/testing/index.mjs +125 -112
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-DQMGKBOV.cjs → chunk-2EUUFY7P.cjs} +694 -578
  9. package/dist/lib/node/chunk-2EUUFY7P.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +44 -42
  11. package/dist/lib/node/index.cjs.map +1 -1
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/packlets/testing/index.cjs +125 -115
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  16. package/dist/types/src/packlets/indexing/util.d.ts +0 -5
  17. package/dist/types/src/packlets/indexing/util.d.ts.map +1 -1
  18. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +3 -1
  19. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  20. package/dist/types/src/packlets/invitations/index.d.ts +1 -0
  21. package/dist/types/src/packlets/invitations/index.d.ts.map +1 -1
  22. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +6 -1
  23. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  24. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +4 -2
  25. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  26. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +44 -0
  27. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -0
  28. package/dist/types/src/packlets/invitations/invitations-service.d.ts +7 -23
  29. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  30. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -1
  31. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  32. package/dist/types/src/packlets/services/service-context.d.ts +2 -0
  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 +1 -0
  35. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  36. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +5 -1
  37. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  38. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  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 -0
  41. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  42. package/dist/types/src/version.d.ts +1 -1
  43. package/package.json +34 -34
  44. package/src/packlets/identity/identity-manager.ts +1 -0
  45. package/src/packlets/identity/identity.test.ts +3 -0
  46. package/src/packlets/indexing/util.ts +7 -64
  47. package/src/packlets/invitations/device-invitation-protocol.ts +6 -1
  48. package/src/packlets/invitations/index.ts +1 -0
  49. package/src/packlets/invitations/invitation-protocol.ts +7 -1
  50. package/src/packlets/invitations/invitations-handler.ts +11 -73
  51. package/src/packlets/invitations/invitations-manager.ts +271 -0
  52. package/src/packlets/invitations/invitations-service.ts +23 -168
  53. package/src/packlets/invitations/space-invitation-protocol.ts +45 -3
  54. package/src/packlets/services/automerge-host.test.ts +1 -1
  55. package/src/packlets/services/service-context.ts +14 -2
  56. package/src/packlets/services/service-host.ts +11 -15
  57. package/src/packlets/spaces/data-space-manager.ts +48 -2
  58. package/src/packlets/spaces/data-space.ts +1 -1
  59. package/src/packlets/testing/invitation-utils.ts +100 -97
  60. package/src/packlets/testing/test-builder.ts +19 -1
  61. package/src/version.ts +1 -1
  62. package/dist/lib/browser/chunk-7S34JE6M.mjs.map +0 -7
  63. package/dist/lib/node/chunk-DQMGKBOV.cjs.map +0 -7
@@ -3,14 +3,8 @@
3
3
  //
4
4
 
5
5
  import { PushStream, scheduleTask, TimeoutError, Trigger } from '@dxos/async';
6
- import {
7
- AuthenticatingInvitation,
8
- AUTHENTICATION_CODE_LENGTH,
9
- CancellableInvitation,
10
- INVITATION_TIMEOUT,
11
- } from '@dxos/client-protocol';
6
+ import { AuthenticatingInvitation, INVITATION_TIMEOUT } from '@dxos/client-protocol';
12
7
  import { Context } from '@dxos/context';
13
- import { generatePasscode } from '@dxos/credentials';
14
8
  import { createKeyPair, sign } from '@dxos/crypto';
15
9
  import { invariant } from '@dxos/invariant';
16
10
  import { PublicKey } from '@dxos/keys';
@@ -67,55 +61,12 @@ export class InvitationsHandler {
67
61
  */
68
62
  constructor(private readonly _networkManager: NetworkManager) {}
69
63
 
70
- createInvitation(protocol: InvitationProtocol, options?: Partial<Invitation>): CancellableInvitation {
71
- const {
72
- invitationId = PublicKey.random().toHex(),
73
- type = Invitation.Type.INTERACTIVE,
74
- authMethod = Invitation.AuthMethod.SHARED_SECRET,
75
- state = Invitation.State.INIT,
76
- timeout = INVITATION_TIMEOUT,
77
- swarmKey = PublicKey.random(),
78
- persistent = options?.authMethod !== Invitation.AuthMethod.KNOWN_PUBLIC_KEY, // default no not storing keypairs
79
- created = new Date(),
80
- guestKeypair = undefined,
81
- lifetime = 86400, // 1 day,
82
- multiUse = false,
83
- } = options ?? {};
84
- const authCode =
85
- options?.authCode ??
86
- (authMethod === Invitation.AuthMethod.SHARED_SECRET ? generatePasscode(AUTHENTICATION_CODE_LENGTH) : undefined);
87
- invariant(protocol);
88
-
89
- const invitation: Invitation = {
90
- invitationId,
91
- type,
92
- authMethod,
93
- state,
94
- swarmKey,
95
- authCode,
96
- timeout,
97
- persistent: persistent && type !== Invitation.Type.DELEGATED, // delegated invitations are persisted in control feed
98
- guestKeypair:
99
- guestKeypair ?? (authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? createAdmissionKeypair() : undefined),
100
- created,
101
- lifetime,
102
- multiUse,
103
- ...protocol.getInvitationContext(),
104
- };
105
-
106
- const stream = new PushStream<Invitation>();
107
- const ctx = new Context({
108
- onError: (err) => {
109
- stream.error(err);
110
- void ctx.dispose();
111
- },
112
- });
113
-
114
- ctx.onDispose(() => {
115
- log('complete', { ...protocol.toJSON() });
116
- stream.complete();
117
- });
118
-
64
+ handleInvitationFlow(
65
+ ctx: Context,
66
+ stream: PushStream<Invitation>,
67
+ protocol: InvitationProtocol,
68
+ invitation: Invitation,
69
+ ): void {
119
70
  // Called for every connecting peer.
120
71
  const createExtension = (): InvitationHostExtension => {
121
72
  const extension = new InvitationHostExtension({
@@ -134,7 +85,7 @@ export class InvitationsHandler {
134
85
  try {
135
86
  const deviceKey = admissionRequest.device?.deviceKey ?? admissionRequest.space?.deviceKey;
136
87
  invariant(deviceKey);
137
- const admissionResponse = await protocol.admit(admissionRequest, extension.guestProfile);
88
+ const admissionResponse = await protocol.admit(invitation, admissionRequest, extension.guestProfile);
138
89
 
139
90
  // Updating credentials complete.
140
91
  extension.completedTrigger.wake(deviceKey);
@@ -154,7 +105,7 @@ export class InvitationsHandler {
154
105
  log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.begin({ id: traceId }));
155
106
  log('connected', { ...protocol.toJSON() });
156
107
  stream.next({ ...invitation, state: Invitation.State.CONNECTED });
157
- const deviceKey = await extension.completedTrigger.wait({ timeout });
108
+ const deviceKey = await extension.completedTrigger.wait({ timeout: invitation.timeout });
158
109
  log('admitted guest', { guest: deviceKey, ...protocol.toJSON() });
159
110
  stream.next({ ...invitation, state: Invitation.State.SUCCESS });
160
111
  log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.end({ id: traceId }));
@@ -168,7 +119,7 @@ export class InvitationsHandler {
168
119
  }
169
120
  log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.error({ id: traceId, error: err }));
170
121
  } finally {
171
- if (!multiUse) {
122
+ if (!invitation.multiUse) {
172
123
  // Wait for graceful close before disposing.
173
124
  await swarmConnection.close();
174
125
  await ctx.dispose();
@@ -193,7 +144,7 @@ export class InvitationsHandler {
193
144
  return extension;
194
145
  };
195
146
 
196
- if (invitation.lifetime && invitation.created && invitation.lifetime !== 0) {
147
+ if (invitation.lifetime && invitation.created) {
197
148
  if (invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()) {
198
149
  log.warn('invitation has already expired');
199
150
  } else {
@@ -229,18 +180,6 @@ export class InvitationsHandler {
229
180
 
230
181
  stream.next({ ...invitation, state: Invitation.State.CONNECTING });
231
182
  });
232
-
233
- // TODO(burdon): Stop anything pending.
234
- const observable = new CancellableInvitation({
235
- initialInvitation: invitation,
236
- subscriber: stream.observable,
237
- onCancel: async () => {
238
- stream.next({ ...invitation, state: Invitation.State.CANCELLED });
239
- await ctx.dispose();
240
- },
241
- });
242
-
243
- return observable;
244
183
  }
245
184
 
246
185
  acceptInvitation(
@@ -251,7 +190,6 @@ export class InvitationsHandler {
251
190
  const { timeout = INVITATION_TIMEOUT } = invitation;
252
191
  invariant(protocol);
253
192
 
254
- // TODO(nf): duplicate check in InvitationsService
255
193
  if (deviceProfile) {
256
194
  invariant(invitation.kind === Invitation.Kind.DEVICE, 'deviceProfile provided for non-device invitation');
257
195
  }
@@ -0,0 +1,271 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Event, PushStream } from '@dxos/async';
6
+ import {
7
+ type AuthenticatingInvitation,
8
+ AUTHENTICATION_CODE_LENGTH,
9
+ CancellableInvitation,
10
+ INVITATION_TIMEOUT,
11
+ } from '@dxos/client-protocol';
12
+ import { Context } from '@dxos/context';
13
+ import { generatePasscode } from '@dxos/credentials';
14
+ import { hasInvitationExpired, type MetadataStore } from '@dxos/echo-pipeline';
15
+ import { invariant } from '@dxos/invariant';
16
+ import { PublicKey } from '@dxos/keys';
17
+ import { log } from '@dxos/log';
18
+ import {
19
+ type AcceptInvitationRequest,
20
+ type AuthenticationRequest,
21
+ Invitation,
22
+ } from '@dxos/protocols/proto/dxos/client/services';
23
+
24
+ import type { InvitationProtocol } from './invitation-protocol';
25
+ import { createAdmissionKeypair, type InvitationsHandler } from './invitations-handler';
26
+
27
+ /**
28
+ * Entry point for creating and accepting invitations, keeps track of existing invitation set and
29
+ * emits events when the set changes.
30
+ */
31
+ export class InvitationsManager {
32
+ private readonly _createInvitations = new Map<string, CancellableInvitation>();
33
+ private readonly _acceptInvitations = new Map<string, AuthenticatingInvitation>();
34
+
35
+ public readonly invitationCreated = new Event<Invitation>();
36
+ public readonly invitationAccepted = new Event<Invitation>();
37
+ public readonly removedCreated = new Event<Invitation>();
38
+ public readonly removedAccepted = new Event<Invitation>();
39
+ public readonly saved = new Event<Invitation>();
40
+
41
+ private readonly _persistentInvitationsLoadedEvent = new Event();
42
+ private _persistentInvitationsLoaded = false;
43
+
44
+ constructor(
45
+ private readonly _invitationsHandler: InvitationsHandler,
46
+ private readonly _getHandler: (invitation: Partial<Invitation> & Pick<Invitation, 'kind'>) => InvitationProtocol,
47
+ private readonly _metadataStore: MetadataStore,
48
+ ) {}
49
+
50
+ async createInvitation(options: Partial<Invitation> & Pick<Invitation, 'kind'>): Promise<CancellableInvitation> {
51
+ if (options.invitationId) {
52
+ const existingInvitation = this._createInvitations.get(options.invitationId);
53
+ if (existingInvitation) {
54
+ return existingInvitation;
55
+ }
56
+ }
57
+
58
+ const handler = this._getHandler(options);
59
+ const invitation = this._createInvitation(handler, options);
60
+ const { ctx, stream, observableInvitation } = this._createObservableInvitation(handler, invitation);
61
+
62
+ this._createInvitations.set(invitation.invitationId, observableInvitation);
63
+ this.invitationCreated.emit(invitation);
64
+ // onComplete is called on cancel, expiration, or redemption of a single-use invitation
65
+ this._onInvitationComplete(observableInvitation, async () => {
66
+ this._createInvitations.delete(observableInvitation.get().invitationId);
67
+ this.removedCreated.emit(observableInvitation.get());
68
+ if (observableInvitation.get().persistent) {
69
+ await this._safeDeleteInvitation(observableInvitation.get());
70
+ }
71
+ });
72
+
73
+ try {
74
+ await this._persistIfRequired(handler, stream, invitation);
75
+ } catch (err) {
76
+ log.catch(err);
77
+ await observableInvitation.cancel();
78
+ return observableInvitation;
79
+ }
80
+
81
+ this._invitationsHandler.handleInvitationFlow(ctx, stream, handler, observableInvitation.get());
82
+
83
+ return observableInvitation;
84
+ }
85
+
86
+ async loadPersistentInvitations(): Promise<{ invitations: Invitation[] }> {
87
+ if (this._persistentInvitationsLoaded) {
88
+ const invitations = this.getCreatedInvitations().filter((i) => i.persistent);
89
+ return { invitations };
90
+ }
91
+ try {
92
+ const persistentInvitations = this._metadataStore.getInvitations();
93
+ // get saved persistent invitations, filter and remove from storage those that have expired.
94
+ const freshInvitations = persistentInvitations.filter((invitation) => !hasInvitationExpired(invitation));
95
+
96
+ const loadTasks = freshInvitations.map((persistentInvitation) => {
97
+ invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
98
+ return this.createInvitation({ ...persistentInvitation, persistent: false });
99
+ });
100
+ const cInvitations = await Promise.all(loadTasks);
101
+
102
+ return { invitations: cInvitations.map((invitation) => invitation.get()) };
103
+ } catch (err) {
104
+ log.catch(err);
105
+ return { invitations: [] };
106
+ } finally {
107
+ this._persistentInvitationsLoadedEvent.emit();
108
+ this._persistentInvitationsLoaded = true;
109
+ }
110
+ }
111
+
112
+ acceptInvitation(request: AcceptInvitationRequest): AuthenticatingInvitation {
113
+ const options = request.invitation;
114
+ const existingInvitation = this._acceptInvitations.get(options.invitationId);
115
+ if (existingInvitation) {
116
+ return existingInvitation;
117
+ }
118
+
119
+ const handler = this._getHandler(options);
120
+ const invitation = this._invitationsHandler.acceptInvitation(handler, options, request.deviceProfile);
121
+ this._acceptInvitations.set(invitation.get().invitationId, invitation);
122
+ this.invitationAccepted.emit(invitation.get());
123
+
124
+ this._onInvitationComplete(invitation, () => {
125
+ this._acceptInvitations.delete(invitation.get().invitationId);
126
+ this.removedAccepted.emit(invitation.get());
127
+ });
128
+
129
+ return invitation;
130
+ }
131
+
132
+ async authenticate({ invitationId, authCode }: AuthenticationRequest): Promise<void> {
133
+ log('authenticating...');
134
+ invariant(invitationId);
135
+ const observable = this._acceptInvitations.get(invitationId);
136
+ if (!observable) {
137
+ log.warn('invalid invitation', { invitationId });
138
+ } else {
139
+ await observable.authenticate(authCode);
140
+ }
141
+ }
142
+
143
+ async cancelInvitation({ invitationId }: { invitationId: string }): Promise<void> {
144
+ log('cancelInvitation...', { invitationId });
145
+ invariant(invitationId);
146
+ const created = this._createInvitations.get(invitationId);
147
+ if (created) {
148
+ // remove from storage before modifying in-memory state, higher chance of failing
149
+ if (created.get().persistent) {
150
+ await this._metadataStore.removeInvitation(invitationId);
151
+ }
152
+ await created.cancel();
153
+ this._createInvitations.delete(invitationId);
154
+ this.removedCreated.emit(created.get());
155
+ return;
156
+ }
157
+
158
+ const accepted = this._acceptInvitations.get(invitationId);
159
+ if (accepted) {
160
+ await accepted.cancel();
161
+ this._acceptInvitations.delete(invitationId);
162
+ this.removedAccepted.emit(accepted.get());
163
+ }
164
+ }
165
+
166
+ getCreatedInvitations(): Invitation[] {
167
+ return [...this._createInvitations.values()].map((i) => i.get());
168
+ }
169
+
170
+ getAcceptedInvitations(): Invitation[] {
171
+ return [...this._acceptInvitations.values()].map((i) => i.get());
172
+ }
173
+
174
+ onPersistentInvitationsLoaded(ctx: Context, callback: () => void) {
175
+ if (this._persistentInvitationsLoaded) {
176
+ callback();
177
+ } else {
178
+ this._persistentInvitationsLoadedEvent.once(ctx, () => callback());
179
+ }
180
+ }
181
+
182
+ private _createInvitation(protocol: InvitationProtocol, options?: Partial<Invitation>): Invitation {
183
+ const {
184
+ invitationId = PublicKey.random().toHex(),
185
+ type = Invitation.Type.INTERACTIVE,
186
+ authMethod = Invitation.AuthMethod.SHARED_SECRET,
187
+ state = Invitation.State.INIT,
188
+ timeout = INVITATION_TIMEOUT,
189
+ swarmKey = PublicKey.random(),
190
+ persistent = options?.authMethod !== Invitation.AuthMethod.KNOWN_PUBLIC_KEY, // default no not storing keypairs
191
+ created = new Date(),
192
+ guestKeypair = undefined,
193
+ lifetime = 86400, // 1 day,
194
+ multiUse = false,
195
+ } = options ?? {};
196
+ const authCode =
197
+ options?.authCode ??
198
+ (authMethod === Invitation.AuthMethod.SHARED_SECRET ? generatePasscode(AUTHENTICATION_CODE_LENGTH) : undefined);
199
+
200
+ return {
201
+ invitationId,
202
+ type,
203
+ authMethod,
204
+ state,
205
+ swarmKey,
206
+ authCode,
207
+ timeout,
208
+ persistent: persistent && type !== Invitation.Type.DELEGATED, // delegated invitations are persisted in control feed
209
+ guestKeypair:
210
+ guestKeypair ?? (authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? createAdmissionKeypair() : undefined),
211
+ created,
212
+ lifetime,
213
+ multiUse,
214
+ delegationCredentialId: options?.delegationCredentialId,
215
+ ...protocol.getInvitationContext(),
216
+ } satisfies Invitation;
217
+ }
218
+
219
+ private _createObservableInvitation(handler: InvitationProtocol, invitation: Invitation) {
220
+ const stream = new PushStream<Invitation>();
221
+ const ctx = new Context({
222
+ onError: (err) => {
223
+ stream.error(err);
224
+ void ctx.dispose();
225
+ },
226
+ });
227
+ ctx.onDispose(() => {
228
+ log('complete', { ...handler.toJSON() });
229
+ stream.complete();
230
+ });
231
+ const observableInvitation = new CancellableInvitation({
232
+ initialInvitation: invitation,
233
+ subscriber: stream.observable,
234
+ onCancel: async () => {
235
+ stream.next({ ...invitation, state: Invitation.State.CANCELLED });
236
+ await ctx.dispose();
237
+ },
238
+ });
239
+ return { ctx, stream, observableInvitation };
240
+ }
241
+
242
+ private async _persistIfRequired(
243
+ handler: InvitationProtocol,
244
+ changeStream: PushStream<Invitation>,
245
+ invitation: Invitation,
246
+ ): Promise<void> {
247
+ if (invitation.type === Invitation.Type.DELEGATED && invitation.delegationCredentialId == null) {
248
+ const delegationCredentialId = await handler.delegate(invitation);
249
+ changeStream.next({ ...invitation, delegationCredentialId });
250
+ } else if (invitation.persistent) {
251
+ await this._metadataStore.addInvitation(invitation);
252
+ this.saved.emit(invitation);
253
+ }
254
+ }
255
+
256
+ private async _safeDeleteInvitation(invitation: Invitation): Promise<void> {
257
+ try {
258
+ await this._metadataStore.removeInvitation(invitation.invitationId);
259
+ } catch (err) {
260
+ log.catch(err);
261
+ }
262
+ }
263
+
264
+ private _onInvitationComplete(invitation: CancellableInvitation, callback: () => void) {
265
+ invitation.subscribe(
266
+ () => {},
267
+ () => {},
268
+ callback,
269
+ );
270
+ }
271
+ }
@@ -2,43 +2,22 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Event, scheduleTask } from '@dxos/async';
6
- import { type AuthenticatingInvitation, type CancellableInvitation } from '@dxos/client-protocol';
7
5
  import { Stream } from '@dxos/codec-protobuf';
8
- import { Context } from '@dxos/context';
9
- import { hasInvitationExpired, type MetadataStore } from '@dxos/echo-pipeline';
10
- import { invariant } from '@dxos/invariant';
11
- import { log } from '@dxos/log';
12
6
  import {
13
7
  type AuthenticationRequest,
14
8
  type AcceptInvitationRequest,
15
- Invitation,
9
+ type Invitation,
16
10
  type InvitationsService,
17
11
  QueryInvitationsResponse,
18
12
  } from '@dxos/protocols/proto/dxos/client/services';
19
13
 
20
- import { type InvitationProtocol } from './invitation-protocol';
21
- import { type InvitationsHandler } from './invitations-handler';
14
+ import { type InvitationsManager } from './invitations-manager';
22
15
 
23
16
  /**
24
17
  * Adapts invitation service observable to client/service stream.
25
18
  */
26
19
  export class InvitationsServiceImpl implements InvitationsService {
27
- private readonly _createInvitations = new Map<string, CancellableInvitation>();
28
- private readonly _acceptInvitations = new Map<string, AuthenticatingInvitation>();
29
- private readonly _invitationCreated = new Event<Invitation>();
30
- private readonly _invitationAccepted = new Event<Invitation>();
31
- private readonly _removedCreated = new Event<Invitation>();
32
- private readonly _removedAccepted = new Event<Invitation>();
33
- private readonly _saved = new Event<Invitation>();
34
- private readonly _persistentInvitationsLoadedEvent = new Event();
35
- private _persistentInvitationsLoaded = false;
36
-
37
- constructor(
38
- private readonly _invitationsHandler: InvitationsHandler,
39
- private readonly _getHandler: (invitation: Invitation) => InvitationProtocol,
40
- private readonly _metadataStore: MetadataStore,
41
- ) {}
20
+ constructor(private readonly _invitationsManager: InvitationsManager) {}
42
21
 
43
22
  // TODO(burdon): Guest/host label.
44
23
  getLoggingContext() {
@@ -48,148 +27,33 @@ export class InvitationsServiceImpl implements InvitationsService {
48
27
  }
49
28
 
50
29
  createInvitation(options: Invitation): Stream<Invitation> {
51
- let invitation: CancellableInvitation;
52
-
53
- const savePersistentInvitationCtx = new Context();
54
- const existingInvitation = this._createInvitations.get(options.invitationId);
55
- if (existingInvitation) {
56
- invitation = existingInvitation;
57
- } else {
58
- const handler = this._getHandler(options);
59
- invitation = this._invitationsHandler.createInvitation(handler, options);
60
- this._createInvitations.set(invitation.get().invitationId, invitation);
61
- this._invitationCreated.emit(invitation.get());
62
- }
63
-
64
30
  return new Stream<Invitation>(({ next, close }) => {
65
- if (invitation.get().persistent) {
66
- scheduleTask(savePersistentInvitationCtx, async () => {
67
- try {
68
- await this._metadataStore.addInvitation(invitation.get());
69
- this._saved.emit(invitation.get());
70
- } catch (err: any) {
71
- close(err);
72
- }
73
- });
74
- }
75
- invitation.subscribe(
76
- (invitation) => {
77
- next(invitation);
78
- },
79
- async (err: Error) => {
80
- await savePersistentInvitationCtx.dispose();
81
-
82
- // TODO(nf): also remove from storage?
83
- close(err);
84
- },
85
- async () => {
86
- close();
87
- if (invitation.get().persistent) {
88
- await savePersistentInvitationCtx.dispose();
89
- // TODO(nf): remove on all complete conditions?
90
- await this._metadataStore.removeInvitation(invitation.get().invitationId);
91
- }
92
-
93
- this._createInvitations.delete(invitation.get().invitationId);
94
- if (!invitation.get().multiUse) {
95
- this._removedCreated.emit(invitation.get());
96
- }
97
- },
98
- );
99
- });
100
- }
101
-
102
- async loadPersistentInvitations() {
103
- const persistentInvitations = this._metadataStore.getInvitations();
104
-
105
- // get saved persistent invitations, filter and remove from storage those that have expired.
106
- const freshInvitations = persistentInvitations.filter(async (invitation) => !hasInvitationExpired(invitation));
107
-
108
- const cInvitations = freshInvitations.map((persistentInvitation) => {
109
- invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
110
-
111
- const handler = this._getHandler(persistentInvitation);
112
- const invitation = this._invitationsHandler.createInvitation(handler, persistentInvitation);
113
- this._createInvitations.set(invitation.get().invitationId, invitation);
114
- this._invitationCreated.emit(invitation.get());
115
- return persistentInvitation;
31
+ void this._invitationsManager
32
+ .createInvitation(options)
33
+ .then((invitation) => invitation.subscribe(next, close, close))
34
+ .catch(close);
116
35
  });
117
- this._persistentInvitationsLoadedEvent.emit();
118
- this._persistentInvitationsLoaded = true;
119
- return { invitations: cInvitations };
120
36
  }
121
37
 
122
- acceptInvitation({ invitation: options, deviceProfile }: AcceptInvitationRequest): Stream<Invitation> {
123
- let invitation: AuthenticatingInvitation;
124
-
125
- // TODO(nf): duplicate check in InvitationHandler
126
- if (deviceProfile) {
127
- invariant(options.kind === Invitation.Kind.DEVICE, 'deviceProfile provided for non-device invitation');
128
- }
129
-
130
- const existingInvitation = this._acceptInvitations.get(options.invitationId);
131
- if (existingInvitation) {
132
- invitation = existingInvitation;
133
- } else {
134
- const handler = this._getHandler(options);
135
- invitation = this._invitationsHandler.acceptInvitation(handler, options, deviceProfile);
136
- this._acceptInvitations.set(invitation.get().invitationId, invitation);
137
- this._invitationAccepted.emit(invitation.get());
138
- }
139
-
38
+ acceptInvitation(request: AcceptInvitationRequest): Stream<Invitation> {
39
+ const invitation = this._invitationsManager.acceptInvitation(request);
140
40
  return new Stream<Invitation>(({ next, close }) => {
141
- invitation.subscribe(
142
- (invitation) => {
143
- next(invitation);
144
- },
145
- (err: Error) => {
146
- close(err);
147
- },
148
- () => {
149
- close();
150
- this._acceptInvitations.delete(invitation.get().invitationId);
151
- if (!invitation.get().multiUse) {
152
- this._removedAccepted.emit(invitation.get());
153
- }
154
- },
155
- );
41
+ invitation.subscribe(next, close, close);
156
42
  });
157
43
  }
158
44
 
159
- async authenticate({ invitationId, authCode }: AuthenticationRequest): Promise<void> {
160
- log('authenticating...');
161
- invariant(invitationId);
162
- const observable = this._acceptInvitations.get(invitationId);
163
- if (!observable) {
164
- log.warn('invalid invitation', { invitationId });
165
- } else {
166
- await observable.authenticate(authCode);
167
- }
45
+ async authenticate(request: AuthenticationRequest): Promise<void> {
46
+ return this._invitationsManager.authenticate(request);
168
47
  }
169
48
 
170
- async cancelInvitation({ invitationId }: { invitationId: string }): Promise<void> {
171
- log('cancelInvitation...', { invitationId });
172
- invariant(invitationId);
173
- const created = this._createInvitations.get(invitationId);
174
- const accepted = this._acceptInvitations.get(invitationId);
175
- if (created) {
176
- await created.cancel();
177
- this._createInvitations.delete(invitationId);
178
- this._removedCreated.emit(created.get());
179
- if (created.get().persistent) {
180
- await this._metadataStore.removeInvitation(created.get().invitationId);
181
- }
182
- } else if (accepted) {
183
- await accepted.cancel();
184
- this._acceptInvitations.delete(invitationId);
185
- this._removedAccepted.emit(accepted.get());
186
- }
49
+ async cancelInvitation(request: { invitationId: string }): Promise<void> {
50
+ return this._invitationsManager.cancelInvitation(request);
187
51
  }
188
52
 
189
53
  queryInvitations(): Stream<QueryInvitationsResponse> {
190
54
  return new Stream<QueryInvitationsResponse>(({ next, ctx }) => {
191
55
  // Push added invitations to the stream.
192
- this._invitationCreated.on(ctx, (invitation) => {
56
+ this._invitationsManager.invitationCreated.on(ctx, (invitation) => {
193
57
  next({
194
58
  action: QueryInvitationsResponse.Action.ADDED,
195
59
  type: QueryInvitationsResponse.Type.CREATED,
@@ -197,7 +61,7 @@ export class InvitationsServiceImpl implements InvitationsService {
197
61
  });
198
62
  });
199
63
 
200
- this._invitationAccepted.on(ctx, (invitation) => {
64
+ this._invitationsManager.invitationAccepted.on(ctx, (invitation) => {
201
65
  next({
202
66
  action: QueryInvitationsResponse.Action.ADDED,
203
67
  type: QueryInvitationsResponse.Type.ACCEPTED,
@@ -206,7 +70,7 @@ export class InvitationsServiceImpl implements InvitationsService {
206
70
  });
207
71
 
208
72
  // Push removed invitations to the stream.
209
- this._removedCreated.on(ctx, (invitation) => {
73
+ this._invitationsManager.removedCreated.on(ctx, (invitation) => {
210
74
  next({
211
75
  action: QueryInvitationsResponse.Action.REMOVED,
212
76
  type: QueryInvitationsResponse.Type.CREATED,
@@ -214,7 +78,7 @@ export class InvitationsServiceImpl implements InvitationsService {
214
78
  });
215
79
  });
216
80
 
217
- this._removedAccepted.on(ctx, (invitation) => {
81
+ this._invitationsManager.removedAccepted.on(ctx, (invitation) => {
218
82
  next({
219
83
  action: QueryInvitationsResponse.Action.REMOVED,
220
84
  type: QueryInvitationsResponse.Type.ACCEPTED,
@@ -223,7 +87,7 @@ export class InvitationsServiceImpl implements InvitationsService {
223
87
  });
224
88
 
225
89
  // used only for testing
226
- this._saved.on(ctx, (invitation) => {
90
+ this._invitationsManager.saved.on(ctx, (invitation) => {
227
91
  next({
228
92
  action: QueryInvitationsResponse.Action.SAVED,
229
93
  type: QueryInvitationsResponse.Type.CREATED,
@@ -235,33 +99,24 @@ export class InvitationsServiceImpl implements InvitationsService {
235
99
  next({
236
100
  action: QueryInvitationsResponse.Action.ADDED,
237
101
  type: QueryInvitationsResponse.Type.CREATED,
238
- invitations: Array.from(this._createInvitations.values()).map((invitation) => invitation.get()),
102
+ invitations: this._invitationsManager.getCreatedInvitations(),
239
103
  existing: true,
240
104
  });
241
105
 
242
106
  next({
243
107
  action: QueryInvitationsResponse.Action.ADDED,
244
108
  type: QueryInvitationsResponse.Type.ACCEPTED,
245
- invitations: Array.from(this._acceptInvitations.values()).map((invitation) => invitation.get()),
109
+ invitations: this._invitationsManager.getAcceptedInvitations(),
246
110
  existing: true,
247
111
  });
248
112
 
249
- if (this._persistentInvitationsLoaded) {
113
+ this._invitationsManager.onPersistentInvitationsLoaded(ctx, () => {
250
114
  next({
251
115
  action: QueryInvitationsResponse.Action.LOAD_COMPLETE,
252
116
  type: QueryInvitationsResponse.Type.CREATED,
253
117
  // TODO(nf): populate with invitations
254
118
  });
255
- } else {
256
- this._persistentInvitationsLoadedEvent.on(ctx, () => {
257
- next({
258
- action: QueryInvitationsResponse.Action.LOAD_COMPLETE,
259
- type: QueryInvitationsResponse.Type.CREATED,
260
- // TODO(nf): populate with invitations
261
- });
262
- });
263
- }
264
-
119
+ });
265
120
  // TODO(nf): expired invitations?
266
121
  });
267
122
  }