@dxos/client-services 0.4.10-main.ef6fbc2 → 0.4.10-main.f15a546

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 (97) hide show
  1. package/dist/lib/browser/{chunk-2B66DLAB.mjs → chunk-MPDRQNRM.mjs} +1402 -1074
  2. package/dist/lib/browser/chunk-MPDRQNRM.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +13 -3
  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 +133 -115
  7. package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-Y7UTCQRW.cjs → chunk-YOYDU2WU.cjs} +1308 -1063
  9. package/dist/lib/node/chunk-YOYDU2WU.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +49 -39
  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 +133 -118
  14. package/dist/lib/node/packlets/testing/index.cjs.map +3 -3
  15. package/dist/types/src/index.d.ts +1 -0
  16. package/dist/types/src/index.d.ts.map +1 -1
  17. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts +5 -0
  18. package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts.map +1 -0
  19. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts +5 -0
  20. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -0
  21. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts +15 -0
  22. package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -0
  23. package/dist/types/src/packlets/{services → diagnostics}/diagnostics.d.ts +1 -1
  24. package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -0
  25. package/dist/types/src/packlets/diagnostics/index.d.ts +4 -0
  26. package/dist/types/src/packlets/diagnostics/index.d.ts.map +1 -0
  27. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  28. package/dist/types/src/packlets/indexing/util.d.ts +2 -6
  29. package/dist/types/src/packlets/indexing/util.d.ts.map +1 -1
  30. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts +3 -1
  31. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  32. package/dist/types/src/packlets/invitations/index.d.ts +1 -0
  33. package/dist/types/src/packlets/invitations/index.d.ts.map +1 -1
  34. package/dist/types/src/packlets/invitations/invitation-extension.d.ts +1 -0
  35. package/dist/types/src/packlets/invitations/invitation-extension.d.ts.map +1 -1
  36. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts +6 -1
  37. package/dist/types/src/packlets/invitations/invitation-protocol.d.ts.map +1 -1
  38. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -4
  39. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  40. package/dist/types/src/packlets/invitations/invitations-manager.d.ts +44 -0
  41. package/dist/types/src/packlets/invitations/invitations-manager.d.ts.map +1 -0
  42. package/dist/types/src/packlets/invitations/invitations-service.d.ts +7 -23
  43. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  44. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts +2 -1
  45. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  46. package/dist/types/src/packlets/services/index.d.ts +1 -1
  47. package/dist/types/src/packlets/services/index.d.ts.map +1 -1
  48. package/dist/types/src/packlets/services/service-context.d.ts +2 -0
  49. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  50. package/dist/types/src/packlets/services/service-host.d.ts +5 -1
  51. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  52. package/dist/types/src/packlets/services/util.d.ts +1 -0
  53. package/dist/types/src/packlets/services/util.d.ts.map +1 -1
  54. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +5 -1
  55. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  56. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  57. package/dist/types/src/packlets/system/system-service.d.ts +1 -1
  58. package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
  59. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  60. package/dist/types/src/packlets/testing/test-builder.d.ts +6 -1
  61. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  62. package/dist/types/src/version.d.ts +1 -1
  63. package/package.json +35 -34
  64. package/src/index.ts +1 -0
  65. package/src/packlets/diagnostics/browser-diagnostics-broadcast.ts +94 -0
  66. package/src/packlets/diagnostics/diagnostics-broadcast.ts +20 -0
  67. package/src/packlets/diagnostics/diagnostics-collector.ts +65 -0
  68. package/src/packlets/{services → diagnostics}/diagnostics.ts +2 -2
  69. package/src/packlets/diagnostics/index.ts +7 -0
  70. package/src/packlets/identity/identity-manager.ts +1 -0
  71. package/src/packlets/identity/identity.test.ts +3 -0
  72. package/src/packlets/indexing/util.ts +9 -66
  73. package/src/packlets/invitations/device-invitation-protocol.ts +6 -1
  74. package/src/packlets/invitations/index.ts +1 -0
  75. package/src/packlets/invitations/invitation-extension.ts +28 -1
  76. package/src/packlets/invitations/invitation-protocol.ts +7 -1
  77. package/src/packlets/invitations/invitations-handler.ts +75 -96
  78. package/src/packlets/invitations/invitations-manager.ts +271 -0
  79. package/src/packlets/invitations/invitations-service.ts +23 -168
  80. package/src/packlets/invitations/space-invitation-protocol.ts +45 -3
  81. package/src/packlets/services/automerge-host.test.ts +10 -4
  82. package/src/packlets/services/index.ts +1 -1
  83. package/src/packlets/services/service-context.test.ts +4 -1
  84. package/src/packlets/services/service-context.ts +19 -5
  85. package/src/packlets/services/service-host.ts +34 -19
  86. package/src/packlets/services/util.ts +2 -0
  87. package/src/packlets/spaces/data-space-manager.test.ts +4 -4
  88. package/src/packlets/spaces/data-space-manager.ts +48 -2
  89. package/src/packlets/spaces/data-space.ts +1 -1
  90. package/src/packlets/storage/level.ts +1 -1
  91. package/src/packlets/system/system-service.ts +1 -1
  92. package/src/packlets/testing/invitation-utils.ts +100 -97
  93. package/src/packlets/testing/test-builder.ts +39 -5
  94. package/src/version.ts +1 -1
  95. package/dist/lib/browser/chunk-2B66DLAB.mjs.map +0 -7
  96. package/dist/lib/node/chunk-Y7UTCQRW.cjs.map +0 -7
  97. package/dist/types/src/packlets/services/diagnostics.d.ts.map +0 -1
@@ -3,14 +3,9 @@
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';
8
+ import { createKeyPair, sign } from '@dxos/crypto';
14
9
  import { invariant } from '@dxos/invariant';
15
10
  import { PublicKey } from '@dxos/keys';
16
11
  import { log } from '@dxos/log';
@@ -21,9 +16,9 @@ import {
21
16
  type SwarmConnection,
22
17
  } from '@dxos/network-manager';
23
18
  import { InvalidInvitationExtensionRoleError, trace } from '@dxos/protocols';
24
- import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
19
+ import { type AdmissionKeypair, Invitation } from '@dxos/protocols/proto/dxos/client/services';
25
20
  import { type DeviceProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
26
- import { AuthenticationResponse } from '@dxos/protocols/proto/dxos/halo/invitations';
21
+ import { AuthenticationResponse, type IntroductionResponse } from '@dxos/protocols/proto/dxos/halo/invitations';
27
22
 
28
23
  import {
29
24
  InvitationGuestExtension,
@@ -66,50 +61,12 @@ export class InvitationsHandler {
66
61
  */
67
62
  constructor(private readonly _networkManager: NetworkManager) {}
68
63
 
69
- createInvitation(protocol: InvitationProtocol, options?: Partial<Invitation>): CancellableInvitation {
70
- const {
71
- invitationId = PublicKey.random().toHex(),
72
- type = Invitation.Type.INTERACTIVE,
73
- authMethod = Invitation.AuthMethod.SHARED_SECRET,
74
- state = Invitation.State.INIT,
75
- timeout = INVITATION_TIMEOUT,
76
- swarmKey = PublicKey.random(),
77
- persistent = true,
78
- created = new Date(),
79
- lifetime = 86400, // 1 day
80
- } = options ?? {};
81
- const authCode =
82
- options?.authCode ??
83
- (authMethod === Invitation.AuthMethod.SHARED_SECRET ? generatePasscode(AUTHENTICATION_CODE_LENGTH) : undefined);
84
- invariant(protocol);
85
-
86
- const invitation: Invitation = {
87
- invitationId,
88
- type,
89
- authMethod,
90
- state,
91
- swarmKey,
92
- authCode,
93
- timeout,
94
- persistent,
95
- created,
96
- lifetime,
97
- ...protocol.getInvitationContext(),
98
- };
99
-
100
- const stream = new PushStream<Invitation>();
101
- const ctx = new Context({
102
- onError: (err) => {
103
- stream.error(err);
104
- void ctx.dispose();
105
- },
106
- });
107
-
108
- ctx.onDispose(() => {
109
- log('complete', { ...protocol.toJSON() });
110
- stream.complete();
111
- });
112
-
64
+ handleInvitationFlow(
65
+ ctx: Context,
66
+ stream: PushStream<Invitation>,
67
+ protocol: InvitationProtocol,
68
+ invitation: Invitation,
69
+ ): void {
113
70
  // Called for every connecting peer.
114
71
  const createExtension = (): InvitationHostExtension => {
115
72
  const extension = new InvitationHostExtension({
@@ -128,7 +85,7 @@ export class InvitationsHandler {
128
85
  try {
129
86
  const deviceKey = admissionRequest.device?.deviceKey ?? admissionRequest.space?.deviceKey;
130
87
  invariant(deviceKey);
131
- const admissionResponse = await protocol.admit(admissionRequest, extension.guestProfile);
88
+ const admissionResponse = await protocol.admit(invitation, admissionRequest, extension.guestProfile);
132
89
 
133
90
  // Updating credentials complete.
134
91
  extension.completedTrigger.wake(deviceKey);
@@ -148,7 +105,7 @@ export class InvitationsHandler {
148
105
  log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.begin({ id: traceId }));
149
106
  log('connected', { ...protocol.toJSON() });
150
107
  stream.next({ ...invitation, state: Invitation.State.CONNECTED });
151
- const deviceKey = await extension.completedTrigger.wait({ timeout });
108
+ const deviceKey = await extension.completedTrigger.wait({ timeout: invitation.timeout });
152
109
  log('admitted guest', { guest: deviceKey, ...protocol.toJSON() });
153
110
  stream.next({ ...invitation, state: Invitation.State.SUCCESS });
154
111
  log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.end({ id: traceId }));
@@ -162,7 +119,7 @@ export class InvitationsHandler {
162
119
  }
163
120
  log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.error({ id: traceId, error: err }));
164
121
  } finally {
165
- if (type !== Invitation.Type.MULTIUSE) {
122
+ if (!invitation.multiUse) {
166
123
  // Wait for graceful close before disposing.
167
124
  await swarmConnection.close();
168
125
  await ctx.dispose();
@@ -187,7 +144,7 @@ export class InvitationsHandler {
187
144
  return extension;
188
145
  };
189
146
 
190
- if (invitation.lifetime && invitation.created && invitation.lifetime !== 0) {
147
+ if (invitation.lifetime && invitation.created) {
191
148
  if (invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()) {
192
149
  log.warn('invitation has already expired');
193
150
  } else {
@@ -223,18 +180,6 @@ export class InvitationsHandler {
223
180
 
224
181
  stream.next({ ...invitation, state: Invitation.State.CONNECTING });
225
182
  });
226
-
227
- // TODO(burdon): Stop anything pending.
228
- const observable = new CancellableInvitation({
229
- initialInvitation: invitation,
230
- subscriber: stream.observable,
231
- onCancel: async () => {
232
- stream.next({ ...invitation, state: Invitation.State.CANCELLED });
233
- await ctx.dispose();
234
- },
235
- });
236
-
237
- return observable;
238
183
  }
239
184
 
240
185
  acceptInvitation(
@@ -245,7 +190,6 @@ export class InvitationsHandler {
245
190
  const { timeout = INVITATION_TIMEOUT } = invitation;
246
191
  invariant(protocol);
247
192
 
248
- // TODO(nf): duplicate check in InvitationsService
249
193
  if (deviceProfile) {
250
194
  invariant(invitation.kind === Invitation.Kind.DEVICE, 'deviceProfile provided for non-device invitation');
251
195
  }
@@ -317,26 +261,13 @@ export class InvitationsHandler {
317
261
 
318
262
  // 2. Get authentication code.
319
263
  if (isAuthenticationRequired(invitation)) {
320
- for (let attempt = 1; attempt <= MAX_OTP_ATTEMPTS; attempt++) {
321
- log('guest waiting for authentication code...');
322
- setState({ state: Invitation.State.READY_FOR_AUTHENTICATION });
323
- const authCode = await authenticated.wait({ timeout });
324
-
325
- log('sending authentication request');
326
- setState({ state: Invitation.State.AUTHENTICATING });
327
- const response = await extension.rpc.InvitationHostService.authenticate({ authCode });
328
- if (response.status === undefined || response.status === AuthenticationResponse.Status.OK) {
264
+ switch (invitation.authMethod) {
265
+ case Invitation.AuthMethod.SHARED_SECRET:
266
+ await this._handleGuestOtpAuth(extension, setState, authenticated, { timeout });
267
+ break;
268
+ case Invitation.AuthMethod.KNOWN_PUBLIC_KEY:
269
+ await this._handleGuestKpkAuth(extension, setState, invitation, introductionResponse);
329
270
  break;
330
- }
331
-
332
- if (response.status === AuthenticationResponse.Status.INVALID_OTP) {
333
- if (attempt === MAX_OTP_ATTEMPTS) {
334
- throw new Error(`Maximum retry attempts: ${MAX_OTP_ATTEMPTS}`);
335
- } else {
336
- log('retrying invalid code', { attempt });
337
- authenticated.reset();
338
- }
339
- }
340
271
  }
341
272
  }
342
273
 
@@ -423,13 +354,61 @@ export class InvitationsHandler {
423
354
 
424
355
  return observable;
425
356
  }
357
+
358
+ private async _handleGuestOtpAuth(
359
+ extension: InvitationGuestExtension,
360
+ setState: (newState: Partial<Invitation>) => void,
361
+ authenticated: Trigger<string>,
362
+ options: { timeout: number },
363
+ ) {
364
+ for (let attempt = 1; attempt <= MAX_OTP_ATTEMPTS; attempt++) {
365
+ log('guest waiting for authentication code...');
366
+ setState({ state: Invitation.State.READY_FOR_AUTHENTICATION });
367
+ const authCode = await authenticated.wait(options);
368
+
369
+ log('sending authentication request');
370
+ setState({ state: Invitation.State.AUTHENTICATING });
371
+ const response = await extension.rpc.InvitationHostService.authenticate({ authCode });
372
+ if (response.status === undefined || response.status === AuthenticationResponse.Status.OK) {
373
+ break;
374
+ }
375
+
376
+ if (response.status === AuthenticationResponse.Status.INVALID_OTP) {
377
+ if (attempt === MAX_OTP_ATTEMPTS) {
378
+ throw new Error(`Maximum retry attempts: ${MAX_OTP_ATTEMPTS}`);
379
+ } else {
380
+ log('retrying invalid code', { attempt });
381
+ authenticated.reset();
382
+ }
383
+ }
384
+ }
385
+ }
386
+
387
+ private async _handleGuestKpkAuth(
388
+ extension: InvitationGuestExtension,
389
+ setState: (newState: Partial<Invitation>) => void,
390
+ invitation: Invitation,
391
+ introductionResponse: IntroductionResponse,
392
+ ) {
393
+ if (invitation.guestKeypair?.privateKey == null) {
394
+ throw new Error('keypair missing in the invitation');
395
+ }
396
+ if (introductionResponse.challenge == null) {
397
+ throw new Error('challenge missing in the introduction');
398
+ }
399
+ log('sending authentication request');
400
+ setState({ state: Invitation.State.AUTHENTICATING });
401
+ const signature = sign(Buffer.from(introductionResponse.challenge), invitation.guestKeypair.privateKey);
402
+ const response = await extension.rpc.InvitationHostService.authenticate({
403
+ signedChallenge: signature,
404
+ });
405
+ if (response.status !== AuthenticationResponse.Status.OK) {
406
+ throw new Error(`Authentication failed with code: ${response.status}`);
407
+ }
408
+ }
426
409
  }
427
410
 
428
- export const invitationExpired = (invitation: Invitation) => {
429
- return (
430
- invitation.created &&
431
- invitation.lifetime &&
432
- invitation.lifetime !== 0 &&
433
- invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()
434
- );
411
+ export const createAdmissionKeypair = (): AdmissionKeypair => {
412
+ const keypair = createKeyPair();
413
+ return { publicKey: PublicKey.from(keypair.publicKey), privateKey: keypair.secretKey };
435
414
  };
@@ -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
+ }