@dxos/client-services 0.6.13 → 0.6.14-main.69511f5

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 (138) hide show
  1. package/dist/lib/browser/{chunk-CRXXOI45.mjs → chunk-PK5RMXEO.mjs} +6462 -5230
  2. package/dist/lib/browser/chunk-PK5RMXEO.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +7 -3
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +12 -8
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-PZ3JJJ3K.cjs → chunk-XDE6WELX.cjs} +6287 -5057
  9. package/dist/lib/node/chunk-XDE6WELX.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +50 -46
  11. package/dist/lib/node/index.cjs.map +3 -3
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +18 -13
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/lib/node-esm/chunk-S5DTGWTU.mjs +8956 -0
  16. package/dist/lib/node-esm/chunk-S5DTGWTU.mjs.map +7 -0
  17. package/dist/lib/node-esm/index.mjs +420 -0
  18. package/dist/lib/node-esm/index.mjs.map +7 -0
  19. package/dist/lib/node-esm/meta.json +1 -0
  20. package/dist/lib/node-esm/testing/index.mjs +424 -0
  21. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  22. package/dist/types/src/index.d.ts +1 -0
  23. package/dist/types/src/index.d.ts.map +1 -1
  24. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +35 -0
  25. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -0
  26. package/dist/types/src/packlets/agents/edge-agent-service.d.ts +10 -0
  27. package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -0
  28. package/dist/types/src/packlets/agents/index.d.ts +3 -0
  29. package/dist/types/src/packlets/agents/index.d.ts.map +1 -0
  30. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  31. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  32. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +2 -0
  33. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +1 -0
  34. package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
  35. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  36. package/dist/types/src/packlets/identity/identity-manager.d.ts +28 -9
  37. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  38. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +18 -0
  39. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -0
  40. package/dist/types/src/packlets/identity/identity-service.d.ts +7 -2
  41. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  42. package/dist/types/src/packlets/identity/identity.d.ts +12 -3
  43. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  44. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  45. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +30 -0
  46. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -0
  47. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +2 -1
  48. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  49. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +2 -1
  50. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  51. package/dist/types/src/packlets/invitations/invitation-state.d.ts +19 -0
  52. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -0
  53. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
  54. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  55. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  56. package/dist/types/src/packlets/services/service-context.d.ts +14 -9
  57. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  58. package/dist/types/src/packlets/services/service-host.d.ts +2 -0
  59. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  60. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +7 -3
  61. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  62. package/dist/types/src/packlets/spaces/data-space.d.ts +5 -3
  63. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  64. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +3 -0
  65. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  66. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +2 -0
  67. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +1 -0
  68. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
  69. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  70. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +35 -6
  71. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  72. package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
  73. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  74. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  75. package/dist/types/src/packlets/testing/test-builder.d.ts +1 -2
  76. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  77. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  78. package/dist/types/src/testing/setup.d.ts +3 -0
  79. package/dist/types/src/testing/setup.d.ts.map +1 -0
  80. package/dist/types/src/version.d.ts +1 -1
  81. package/dist/types/src/version.d.ts.map +1 -1
  82. package/package.json +44 -45
  83. package/src/index.ts +1 -0
  84. package/src/packlets/agents/edge-agent-manager.ts +163 -0
  85. package/src/packlets/agents/edge-agent-service.ts +42 -0
  86. package/src/packlets/agents/index.ts +6 -0
  87. package/src/packlets/devices/devices-service.test.ts +4 -5
  88. package/src/packlets/diagnostics/diagnostics-broadcast.ts +1 -0
  89. package/src/packlets/identity/{authenticator.test.ts → authenticator.node.test.ts} +2 -3
  90. package/src/packlets/identity/authenticator.ts +5 -2
  91. package/src/packlets/identity/contacts-service.ts +1 -1
  92. package/src/packlets/identity/identity-manager.test.ts +31 -16
  93. package/src/packlets/identity/identity-manager.ts +76 -32
  94. package/src/packlets/identity/identity-recovery-manager.ts +95 -0
  95. package/src/packlets/identity/identity-service.test.ts +5 -8
  96. package/src/packlets/identity/identity-service.ts +11 -5
  97. package/src/packlets/identity/identity.test.ts +130 -239
  98. package/src/packlets/identity/identity.ts +60 -17
  99. package/src/packlets/invitations/device-invitation-protocol.test.ts +7 -4
  100. package/src/packlets/invitations/device-invitation-protocol.ts +8 -2
  101. package/src/packlets/invitations/edge-invitation-handler.ts +185 -0
  102. package/src/packlets/invitations/invitation-guest-extenstion.ts +8 -4
  103. package/src/packlets/invitations/invitation-host-extension.ts +8 -7
  104. package/src/packlets/invitations/invitation-state.ts +112 -0
  105. package/src/packlets/invitations/invitations-handler.test.ts +16 -9
  106. package/src/packlets/invitations/invitations-handler.ts +57 -98
  107. package/src/packlets/invitations/space-invitation-protocol.test.ts +4 -3
  108. package/src/packlets/invitations/space-invitation-protocol.ts +5 -0
  109. package/src/packlets/logging/logging.test.ts +1 -2
  110. package/src/packlets/network/network-service.test.ts +2 -3
  111. package/src/packlets/services/service-context.test.ts +3 -1
  112. package/src/packlets/services/service-context.ts +113 -35
  113. package/src/packlets/services/service-host.test.ts +8 -12
  114. package/src/packlets/services/service-host.ts +25 -7
  115. package/src/packlets/services/service-registry.test.ts +1 -2
  116. package/src/packlets/spaces/data-space-manager.test.ts +2 -2
  117. package/src/packlets/spaces/data-space-manager.ts +44 -7
  118. package/src/packlets/spaces/data-space.ts +37 -6
  119. package/src/packlets/spaces/edge-feed-replicator.test.ts +252 -0
  120. package/src/packlets/spaces/edge-feed-replicator.ts +80 -22
  121. package/src/packlets/spaces/epoch-migrations.ts +2 -2
  122. package/src/packlets/spaces/notarization-plugin.test.ts +10 -7
  123. package/src/packlets/spaces/notarization-plugin.ts +196 -29
  124. package/src/packlets/spaces/spaces-service.test.ts +5 -9
  125. package/src/packlets/spaces/spaces-service.ts +6 -1
  126. package/src/packlets/storage/storage.ts +0 -1
  127. package/src/packlets/system/system-service.test.ts +1 -2
  128. package/src/packlets/testing/test-builder.ts +7 -4
  129. package/src/packlets/worker/worker-runtime.ts +2 -2
  130. package/src/testing/setup.ts +11 -0
  131. package/src/version.ts +1 -5
  132. package/dist/lib/browser/chunk-CRXXOI45.mjs.map +0 -7
  133. package/dist/lib/node/chunk-PZ3JJJ3K.cjs.map +0 -7
  134. package/dist/types/src/packlets/identity/authenticator.test.d.ts +0 -2
  135. package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +0 -1
  136. package/dist/types/src/packlets/services/automerge-host.test.d.ts +0 -2
  137. package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +0 -1
  138. package/src/packlets/services/automerge-host.test.ts +0 -60
@@ -2,10 +2,11 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Mutex, type PushStream, scheduleTask, TimeoutError, type Trigger } from '@dxos/async';
5
+ import { type PushStream, scheduleTask, TimeoutError, type Trigger } from '@dxos/async';
6
6
  import { INVITATION_TIMEOUT } from '@dxos/client-protocol';
7
7
  import { type Context, ContextDisposedError } from '@dxos/context';
8
8
  import { createKeyPair, sign } from '@dxos/crypto';
9
+ import { type EdgeHttpClient } from '@dxos/edge-client';
9
10
  import { invariant } from '@dxos/invariant';
10
11
  import { PublicKey } from '@dxos/keys';
11
12
  import { log } from '@dxos/log';
@@ -19,17 +20,21 @@ import { type ExtensionContext, type TeleportExtension, type TeleportParams } fr
19
20
  import { trace as _trace } from '@dxos/tracing';
20
21
  import { ComplexSet } from '@dxos/util';
21
22
 
23
+ import { type EdgeInvitationConfig, EdgeInvitationHandler } from './edge-invitation-handler';
22
24
  import { InvitationGuestExtension } from './invitation-guest-extenstion';
23
25
  import { InvitationHostExtension, isAuthenticationRequired, MAX_OTP_ATTEMPTS } from './invitation-host-extension';
24
26
  import { type InvitationProtocol } from './invitation-protocol';
27
+ import { createGuardedInvitationState } from './invitation-state';
25
28
  import { InvitationTopology } from './invitation-topology';
26
- import { stateToString } from './utils';
27
29
 
28
30
  const metrics = _trace.metrics;
29
31
 
30
32
  const MAX_DELEGATED_INVITATION_HOST_TRIES = 3;
31
33
 
32
- type InvitationExtension = InvitationHostExtension | InvitationGuestExtension;
34
+ export type InvitationConnectionParams = {
35
+ teleport: Partial<TeleportParams>;
36
+ edgeInvitations?: EdgeInvitationConfig;
37
+ };
33
38
 
34
39
  /**
35
40
  * Generic handler for Halo and Space invitations.
@@ -65,7 +70,8 @@ export class InvitationsHandler {
65
70
  */
66
71
  constructor(
67
72
  private readonly _networkManager: SwarmNetworkManager,
68
- private readonly _defaultTeleportParams?: Partial<TeleportParams>,
73
+ private readonly _edgeClient?: EdgeHttpClient,
74
+ private readonly _connectionParams?: InvitationConnectionParams,
69
75
  ) {}
70
76
 
71
77
  handleInvitationFlow(
@@ -74,8 +80,14 @@ export class InvitationsHandler {
74
80
  protocol: InvitationProtocol,
75
81
  invitation: Invitation,
76
82
  ): void {
83
+ log.verbose('dxos.sdk.invitations-handler.handleInvitationFlow', {
84
+ state: invitation.state,
85
+ invitationId: invitation.invitationId,
86
+ kind: invitation.kind,
87
+ type: invitation.type,
88
+ });
77
89
  metrics.increment('dxos.invitation.created');
78
- const guardedState = this._createGuardedState(ctx, invitation, stream);
90
+ const guardedState = createGuardedInvitationState(ctx, invitation, stream);
79
91
  // Called for every connecting peer.
80
92
  const createExtension = (): InvitationHostExtension => {
81
93
  const extension = new InvitationHostExtension(guardedState.mutex, {
@@ -90,6 +102,10 @@ export class InvitationsHandler {
90
102
 
91
103
  admit: async (admissionRequest) => {
92
104
  try {
105
+ log.verbose('dxos.sdk.invitations-handler.host.admit', {
106
+ invitationId: invitation.invitationId,
107
+ ...protocol.toJSON(),
108
+ });
93
109
  const deviceKey = admissionRequest.device?.deviceKey ?? admissionRequest.space?.deviceKey;
94
110
  invariant(deviceKey);
95
111
  const admissionResponse = await protocol.admit(invitation, admissionRequest, extension.guestProfile);
@@ -201,6 +217,12 @@ export class InvitationsHandler {
201
217
  otpEnteredTrigger: Trigger<string>,
202
218
  deviceProfile?: DeviceProfileDocument,
203
219
  ): void {
220
+ log.verbose('dxos.sdk.invitations-handler.acceptInvitation', {
221
+ state: invitation.state,
222
+ invitationId: invitation.invitationId,
223
+ kind: invitation.kind,
224
+ type: invitation.type,
225
+ });
204
226
  const { timeout = INVITATION_TIMEOUT } = invitation;
205
227
 
206
228
  if (deviceProfile) {
@@ -208,7 +230,7 @@ export class InvitationsHandler {
208
230
  }
209
231
 
210
232
  const triedPeersIds = new ComplexSet(PublicKey.hash);
211
- const guardedState = this._createGuardedState(ctx, invitation, stream);
233
+ const guardedState = createGuardedInvitationState(ctx, invitation, stream);
212
234
 
213
235
  const shouldCancelInvitationFlow = (extension: InvitationGuestExtension) => {
214
236
  const isLockedByAnotherConnection = guardedState.mutex.isLocked() && !extension.hasFlowLock();
@@ -263,16 +285,23 @@ export class InvitationsHandler {
263
285
  timeout,
264
286
  );
265
287
 
266
- log('connected', { ...protocol.toJSON() });
288
+ log.verbose('dxos.sdk.invitations-handler.guest.connected', { ...protocol.toJSON() });
267
289
  guardedState.set(extension, Invitation.State.CONNECTED);
268
290
 
269
291
  // 1. Introduce guest to host.
270
- log('introduce', { ...protocol.toJSON() });
292
+ log.verbose('dxos.sdk.invitations-handler.guest.introduce', {
293
+ invitationId: invitation.invitationId,
294
+ ...protocol.toJSON(),
295
+ });
271
296
  const introductionResponse = await extension.rpc.InvitationHostService.introduce({
272
297
  invitationId: invitation.invitationId,
273
298
  ...protocol.createIntroduction(),
274
299
  });
275
- log('introduce response', { ...protocol.toJSON(), response: introductionResponse });
300
+ log.verbose('dxos.sdk.invitations-handler.guest.introduce-response', {
301
+ invitationId: invitation.invitationId,
302
+ ...protocol.toJSON(),
303
+ authMethod: introductionResponse.authMethod,
304
+ });
276
305
  invitation.authMethod = introductionResponse.authMethod;
277
306
 
278
307
  // 2. Get authentication code.
@@ -298,7 +327,10 @@ export class InvitationsHandler {
298
327
  }
299
328
 
300
329
  // 3. Send admission credentials to host (with local space keys).
301
- log('request admission', { ...protocol.toJSON() });
330
+ log.verbose('dxos.sdk.invitations-handler.guest.request-admission', {
331
+ invitationId: invitation.invitationId,
332
+ ...protocol.toJSON(),
333
+ });
302
334
  const admissionRequest = await protocol.createAdmissionRequest(deviceProfile);
303
335
  const admissionResponse = await extension.rpc.InvitationHostService.admit(admissionRequest);
304
336
 
@@ -309,8 +341,11 @@ export class InvitationsHandler {
309
341
  const result = await protocol.accept(admissionResponse, admissionRequest);
310
342
 
311
343
  // 5. Success.
312
- log('admitted by host', { ...protocol.toJSON() });
313
- await guardedState.complete({
344
+ log.verbose('dxos.sdk.invitations-handler.guest.admitted-by-host', {
345
+ invitationId: invitation.invitationId,
346
+ ...protocol.toJSON(),
347
+ });
348
+ guardedState.complete({
314
349
  ...guardedState.current,
315
350
  ...result,
316
351
  state: Invitation.State.SUCCESS,
@@ -346,6 +381,15 @@ export class InvitationsHandler {
346
381
  return extension;
347
382
  };
348
383
 
384
+ const edgeInvitationHandler = new EdgeInvitationHandler(this._connectionParams?.edgeInvitations, this._edgeClient, {
385
+ onInvitationSuccess: async (admissionResponse, admissionRequest) => {
386
+ const result = await protocol.accept(admissionResponse, admissionRequest);
387
+ log.info('admitted by edge', { ...protocol.toJSON() });
388
+ guardedState.complete({ ...guardedState.current, ...result, state: Invitation.State.SUCCESS });
389
+ },
390
+ });
391
+ edgeInvitationHandler.handle(ctx, guardedState, protocol, deviceProfile);
392
+
349
393
  scheduleTask(ctx, async () => {
350
394
  const error = protocol.checkInvitation(invitation);
351
395
  if (error) {
@@ -389,7 +433,7 @@ export class InvitationsHandler {
389
433
  topic: invitation.swarmKey,
390
434
  protocolProvider: createTeleportProtocolFactory(async (teleport) => {
391
435
  teleport.addExtension('dxos.halo.invitations', extensionFactory());
392
- }, this._defaultTeleportParams),
436
+ }, this._connectionParams?.teleport),
393
437
  topology: new InvitationTopology(role),
394
438
  label,
395
439
  });
@@ -397,90 +441,6 @@ export class InvitationsHandler {
397
441
  return swarmConnection;
398
442
  }
399
443
 
400
- /**
401
- * A utility object for serializing invitation state changes by multiple concurrent
402
- * invitation flow connections.
403
- */
404
- private _createGuardedState(ctx: Context, invitation: Invitation, stream: PushStream<Invitation>) {
405
- // the mutex guards invitation flow on host and guest side, making sure only one flow is currently active
406
- // deadlocks seem very unlikely because hosts don't initiate multiple connections
407
- // even if this somehow happens that there are 2 guests (A, B) and 2 hosts (1, 2) and:
408
- // A has lock for flow with 1, B has lock for flow with 2
409
- // 1 has lock for flow with B, 2 has lock for flow with A
410
- // there'll be a 10-second introduction timeout after which connection will be closed and deadlock broken
411
- const mutex = new Mutex();
412
- let lastActiveExtension: any = null;
413
- let currentInvitation = { ...invitation };
414
- const isStateChangeAllowed = (extension: InvitationExtension | null) => {
415
- if (ctx.disposed || (extension !== null && mutex.isLocked() && !extension.hasFlowLock())) {
416
- return false;
417
- }
418
- // don't allow transitions from a terminal state unless a new extension acquired mutex
419
- // handles a case when error occurs (e.g. connection is closed) after we completed the flow
420
- // successfully or already reported another error
421
- return extension == null || lastActiveExtension !== extension || this._isNotTerminal(currentInvitation.state);
422
- };
423
- return {
424
- mutex,
425
- get current() {
426
- return currentInvitation;
427
- },
428
- // disposing context prevents any further state updates
429
- complete: (newState: Partial<Invitation>) => {
430
- currentInvitation = { ...currentInvitation, ...newState };
431
- stream.next(currentInvitation);
432
- return ctx.dispose();
433
- },
434
- set: (extension: InvitationExtension | null, newState: Invitation.State): boolean => {
435
- if (isStateChangeAllowed(extension)) {
436
- this._logStateUpdate(currentInvitation, extension, newState);
437
- currentInvitation = { ...currentInvitation, state: newState };
438
- stream.next(currentInvitation);
439
- lastActiveExtension = extension;
440
- return true;
441
- }
442
- return false;
443
- },
444
- error: (extension: InvitationExtension | null, error: any): boolean => {
445
- if (isStateChangeAllowed(extension)) {
446
- this._logStateUpdate(currentInvitation, extension, Invitation.State.ERROR);
447
- currentInvitation = { ...currentInvitation, state: Invitation.State.ERROR };
448
- stream.next(currentInvitation);
449
- stream.error(error);
450
- lastActiveExtension = extension;
451
- return true;
452
- }
453
- return false;
454
- },
455
- };
456
- }
457
-
458
- private _logStateUpdate(invitation: Invitation, actor: any, newState: Invitation.State) {
459
- if (this._isNotTerminal(newState)) {
460
- log('invitation state update', {
461
- actor: actor?.constructor.name,
462
- newState: stateToString(newState),
463
- oldState: stateToString(invitation.state),
464
- });
465
- } else {
466
- log.info('invitation state update', {
467
- actor: actor?.constructor.name,
468
- newState: stateToString(newState),
469
- oldState: stateToString(invitation.state),
470
- });
471
- }
472
- }
473
-
474
- private _isNotTerminal(currentState: Invitation.State): boolean {
475
- return ![
476
- Invitation.State.SUCCESS,
477
- Invitation.State.ERROR,
478
- Invitation.State.CANCELLED,
479
- Invitation.State.TIMEOUT,
480
- Invitation.State.EXPIRED,
481
- ].includes(currentState);
482
- }
483
-
484
444
  private async _handleGuestOtpAuth(
485
445
  extension: InvitationGuestExtension,
486
446
  setState: (newState: Invitation.State) => void,
@@ -523,7 +483,6 @@ export class InvitationsHandler {
523
483
  throw new Error('challenge missing in the introduction');
524
484
  }
525
485
  log('sending authentication request');
526
- setState(Invitation.State.AUTHENTICATING);
527
486
  const signature = sign(Buffer.from(introductionResponse.challenge), invitation.guestKeypair.privateKey);
528
487
  const response = await extension.rpc.InvitationHostService.authenticate({
529
488
  signedChallenge: signature,
@@ -2,20 +2,21 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
5
+ import { onTestFinished, describe, expect, test } from 'vitest';
6
6
 
7
7
  import { asyncChain, Trigger } from '@dxos/async';
8
8
  import { raise } from '@dxos/debug';
9
9
  import { AlreadyJoinedError } from '@dxos/protocols';
10
10
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
11
- import { afterTest, describe, test } from '@dxos/test';
12
11
 
13
12
  import { type ServiceContext } from '../services';
14
13
  import { createIdentity, createPeers } from '../testing';
15
14
  import { acceptInvitation, createInvitation, performInvitation } from '../testing/invitation-utils';
16
15
 
17
16
  const closeAfterTest = async (peer: ServiceContext) => {
18
- afterTest(() => peer.close());
17
+ onTestFinished(async () => {
18
+ await peer.close();
19
+ });
19
20
  return peer;
20
21
  };
21
22
 
@@ -40,6 +40,7 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
40
40
 
41
41
  toJSON(): object {
42
42
  return {
43
+ kind: 'space',
43
44
  deviceKey: this._signingContext.deviceKey,
44
45
  spaceKey: this._spaceKey,
45
46
  };
@@ -60,9 +61,13 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
60
61
  }
61
62
 
62
63
  getInvitationContext(): Partial<Invitation> & Pick<Invitation, 'kind'> {
64
+ invariant(this._spaceKey);
65
+ const space = this._spaceManager.spaces.get(this._spaceKey);
66
+ invariant(space);
63
67
  return {
64
68
  kind: Invitation.Kind.SPACE,
65
69
  spaceKey: this._spaceKey,
70
+ spaceId: space.id,
66
71
  };
67
72
  }
68
73
 
@@ -2,12 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
5
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
6
6
 
7
7
  import { Trigger } from '@dxos/async';
8
8
  import { log, LogLevel } from '@dxos/log';
9
9
  import { type LogEntry } from '@dxos/protocols/proto/dxos/client/services';
10
- import { beforeEach, describe, test } from '@dxos/test';
11
10
 
12
11
  import { LoggingServiceImpl } from './logging-service';
13
12
 
@@ -2,12 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
5
+ import { afterEach, onTestFinished, beforeEach, describe, expect, test } from 'vitest';
6
6
 
7
7
  import { Trigger } from '@dxos/async';
8
8
  import { Context } from '@dxos/context';
9
9
  import { type NetworkService, ConnectionState } from '@dxos/protocols/proto/dxos/client/services';
10
- import { afterEach, afterTest, beforeEach, describe, test } from '@dxos/test';
11
10
 
12
11
  import { NetworkServiceImpl } from './network-service';
13
12
  import { type ServiceContext } from '../services';
@@ -41,7 +40,7 @@ describe('NetworkService', () => {
41
40
  query.subscribe(({ swarm }) => {
42
41
  result.wake(swarm);
43
42
  });
44
- afterTest(() => query.close());
43
+ onTestFinished(() => query.close());
45
44
  expect(await result.wait()).to.equal(ConnectionState.ONLINE);
46
45
 
47
46
  result = new Trigger<ConnectionState | undefined>();
@@ -2,9 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { describe, test } from 'vitest';
6
+
5
7
  import { MemorySignalManagerContext, MemorySignalManager } from '@dxos/messaging';
6
8
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
7
- import { describe, openAndClose, test } from '@dxos/test';
9
+ import { openAndClose } from '@dxos/test-utils';
8
10
 
9
11
  import { createServiceContext, performInvitation } from '../testing';
10
12
 
@@ -2,13 +2,20 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { Trigger } from '@dxos/async';
5
+ import { Mutex, Trigger } from '@dxos/async';
6
6
  import { Context, Resource } from '@dxos/context';
7
7
  import { getCredentialAssertion, type CredentialProcessor } from '@dxos/credentials';
8
- import { failUndefined } from '@dxos/debug';
9
- import { EchoEdgeReplicator, EchoHost } from '@dxos/echo-db';
10
- import { MeshEchoReplicator, MetadataStore, SpaceManager, valueEncoding } from '@dxos/echo-pipeline';
11
- import type { EdgeConnection } from '@dxos/edge-client';
8
+ import { failUndefined, warnAfterTimeout } from '@dxos/debug';
9
+ import {
10
+ EchoEdgeReplicator,
11
+ EchoHost,
12
+ MeshEchoReplicator,
13
+ MetadataStore,
14
+ SpaceManager,
15
+ valueEncoding,
16
+ } from '@dxos/echo-pipeline';
17
+ import { createChainEdgeIdentity, createEphemeralEdgeIdentity } from '@dxos/edge-client';
18
+ import type { EdgeHttpClient, EdgeConnection, EdgeIdentity } from '@dxos/edge-client';
12
19
  import { FeedFactory, FeedStore } from '@dxos/feed-store';
13
20
  import { invariant } from '@dxos/invariant';
14
21
  import { Keyring } from '@dxos/keyring';
@@ -23,19 +30,21 @@ import { type Runtime } from '@dxos/protocols/proto/dxos/config';
23
30
  import type { FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
24
31
  import { type Credential, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
25
32
  import { type Storage } from '@dxos/random-access-storage';
26
- import type { TeleportParams } from '@dxos/teleport';
27
33
  import { BlobStore } from '@dxos/teleport-extension-object-sync';
28
34
  import { trace as Trace } from '@dxos/tracing';
29
35
  import { safeInstanceof } from '@dxos/util';
30
36
 
37
+ import { EdgeAgentManager } from '../agents';
31
38
  import {
32
39
  IdentityManager,
33
40
  type CreateIdentityOptions,
34
- type IdentityManagerRuntimeParams,
41
+ type IdentityManagerParams,
35
42
  type JoinIdentityParams,
36
43
  } from '../identity';
44
+ import { EdgeIdentityRecoveryManager } from '../identity/identity-recovery-manager';
37
45
  import {
38
46
  DeviceInvitationProtocol,
47
+ type InvitationConnectionParams,
39
48
  InvitationsHandler,
40
49
  InvitationsManager,
41
50
  SpaceInvitationProtocol,
@@ -43,9 +52,12 @@ import {
43
52
  } from '../invitations';
44
53
  import { DataSpaceManager, type DataSpaceManagerRuntimeParams, type SigningContext } from '../spaces';
45
54
 
46
- export type ServiceContextRuntimeParams = IdentityManagerRuntimeParams &
55
+ export type ServiceContextRuntimeParams = Pick<
56
+ IdentityManagerParams,
57
+ 'devicePresenceOfflineTimeout' | 'devicePresenceAnnounceInterval'
58
+ > &
47
59
  DataSpaceManagerRuntimeParams & {
48
- invitationConnectionDefaultParams?: Partial<TeleportParams>;
60
+ invitationConnectionDefaultParams?: InvitationConnectionParams;
49
61
  disableP2pReplication?: boolean;
50
62
  };
51
63
  /**
@@ -56,6 +68,8 @@ export type ServiceContextRuntimeParams = IdentityManagerRuntimeParams &
56
68
  @safeInstanceof('dxos.client-services.ServiceContext')
57
69
  @Trace.resource()
58
70
  export class ServiceContext extends Resource {
71
+ private readonly _edgeIdentityUpdateMutex = new Mutex();
72
+
59
73
  public readonly initialized = new Trigger();
60
74
  public readonly metadataStore: MetadataStore;
61
75
  public readonly blobStore: BlobStore;
@@ -63,6 +77,7 @@ export class ServiceContext extends Resource {
63
77
  public readonly keyring: Keyring;
64
78
  public readonly spaceManager: SpaceManager;
65
79
  public readonly identityManager: IdentityManager;
80
+ public readonly recoveryManager: EdgeIdentityRecoveryManager;
66
81
  public readonly invitations: InvitationsHandler;
67
82
  public readonly invitationsManager: InvitationsManager;
68
83
  public readonly echoHost: EchoHost;
@@ -71,6 +86,7 @@ export class ServiceContext extends Resource {
71
86
 
72
87
  // Initialized after identity is initialized.
73
88
  public dataSpaceManager?: DataSpaceManager;
89
+ public edgeAgentManager?: EdgeAgentManager;
74
90
 
75
91
  private readonly _handlerFactories = new Map<
76
92
  Invitation.Kind,
@@ -87,6 +103,7 @@ export class ServiceContext extends Resource {
87
103
  public readonly networkManager: SwarmNetworkManager,
88
104
  public readonly signalManager: SignalManager,
89
105
  private readonly _edgeConnection: EdgeConnection | undefined,
106
+ private readonly _edgeHttpClient: EdgeHttpClient | undefined,
90
107
  public readonly _runtimeParams?: ServiceContextRuntimeParams,
91
108
  private readonly _edgeFeatures?: Runtime.Client.EdgeFeatures,
92
109
  ) {
@@ -116,31 +133,22 @@ export class ServiceContext extends Resource {
116
133
  disableP2pReplication: this._runtimeParams?.disableP2pReplication,
117
134
  });
118
135
 
119
- this.identityManager = new IdentityManager(
120
- this.metadataStore,
136
+ this.identityManager = new IdentityManager({
137
+ metadataStore: this.metadataStore,
138
+ keyring: this.keyring,
139
+ feedStore: this.feedStore,
140
+ spaceManager: this.spaceManager,
141
+ devicePresenceOfflineTimeout: this._runtimeParams?.devicePresenceOfflineTimeout,
142
+ devicePresenceAnnounceInterval: this._runtimeParams?.devicePresenceAnnounceInterval,
143
+ edgeConnection: this._edgeConnection,
144
+ edgeFeatures: this._edgeFeatures,
145
+ });
146
+
147
+ this.recoveryManager = new EdgeIdentityRecoveryManager(
121
148
  this.keyring,
122
- this.feedStore,
123
- this.spaceManager,
124
- this._runtimeParams as IdentityManagerRuntimeParams,
125
- {
126
- onIdentityConstruction: (identity) => {
127
- if (this._edgeConnection) {
128
- log.info('Setting identity on edge connection', {
129
- identity: identity.identityKey.toHex(),
130
- oldIdentity: this._edgeConnection.identityKey,
131
- swarms: this.networkManager.topics,
132
- });
133
- this._edgeConnection.setIdentity({
134
- peerKey: identity.deviceKey.toHex(),
135
- identityKey: identity.identityKey.toHex(),
136
- });
137
- this.networkManager.setPeerInfo({
138
- identityKey: identity.identityKey.toHex(),
139
- peerKey: identity.deviceKey.toHex(),
140
- });
141
- }
142
- },
143
- },
149
+ this._edgeHttpClient,
150
+ () => this.identityManager.identity,
151
+ this._acceptIdentity.bind(this),
144
152
  );
145
153
 
146
154
  this.echoHost = new EchoHost({ kv: this.level });
@@ -149,6 +157,7 @@ export class ServiceContext extends Resource {
149
157
 
150
158
  this.invitations = new InvitationsHandler(
151
159
  this.networkManager, //
160
+ this._edgeHttpClient,
152
161
  _runtimeParams?.invitationConnectionDefaultParams,
153
162
  );
154
163
  this.invitationsManager = new InvitationsManager(
@@ -185,6 +194,11 @@ export class ServiceContext extends Resource {
185
194
 
186
195
  log('opening...');
187
196
  log.trace('dxos.sdk.service-context.open', trace.begin({ id: this._instanceId }));
197
+
198
+ await this.identityManager.open(ctx);
199
+
200
+ await this._setNetworkIdentity();
201
+
188
202
  await this._edgeConnection?.open();
189
203
  await this.signalManager.open();
190
204
  await this.networkManager.open();
@@ -200,9 +214,9 @@ export class ServiceContext extends Resource {
200
214
 
201
215
  await this.metadataStore.load();
202
216
  await this.spaceManager.open();
203
- await this.identityManager.open(ctx);
204
217
 
205
218
  if (this.identityManager.identity) {
219
+ await this.identityManager.identity.joinNetwork();
206
220
  await this._initialize(ctx);
207
221
  }
208
222
 
@@ -219,6 +233,7 @@ export class ServiceContext extends Resource {
219
233
  await this.identityManager.identity.space.spaceState.removeCredentialProcessor(this._deviceSpaceSync);
220
234
  }
221
235
  await this.dataSpaceManager?.close();
236
+ await this.edgeAgentManager?.close();
222
237
  await this.identityManager.close();
223
238
  await this.spaceManager.close();
224
239
  await this.feedStore.close();
@@ -234,11 +249,16 @@ export class ServiceContext extends Resource {
234
249
 
235
250
  async createIdentity(params: CreateIdentityOptions = {}) {
236
251
  const identity = await this.identityManager.createIdentity(params);
252
+ await this._setNetworkIdentity();
253
+ await identity.joinNetwork();
237
254
  await this._initialize(new Context());
238
255
  return identity;
239
256
  }
240
257
 
241
258
  getInvitationHandler(invitation: Partial<Invitation> & Pick<Invitation, 'kind'>): InvitationProtocol {
259
+ if (this.identityManager.identity == null && invitation.kind === Invitation.Kind.SPACE) {
260
+ throw new Error('Identity must be created before joining a space.');
261
+ }
242
262
  const factory = this._handlerFactories.get(invitation.kind);
243
263
  invariant(factory, `Unknown invitation kind: ${invitation.kind}`);
244
264
  return factory(invitation);
@@ -255,7 +275,10 @@ export class ServiceContext extends Resource {
255
275
  }
256
276
 
257
277
  private async _acceptIdentity(params: JoinIdentityParams) {
258
- const identity = await this.identityManager.acceptIdentity(params);
278
+ const { identity, identityRecord } = await this.identityManager.prepareIdentity(params);
279
+ await this._setNetworkIdentity({ deviceCredential: params.authorizedDeviceCredential! });
280
+ await identity.joinNetwork();
281
+ await this.identityManager.acceptIdentity(identity, identityRecord, params.deviceProfile);
259
282
  await this._initialize(new Context());
260
283
  return identity;
261
284
  }
@@ -292,6 +315,7 @@ export class ServiceContext extends Resource {
292
315
  echoHost: this.echoHost,
293
316
  invitationsManager: this.invitationsManager,
294
317
  edgeConnection: this._edgeConnection,
318
+ edgeHttpClient: this._edgeHttpClient,
295
319
  echoEdgeReplicator: this._echoEdgeReplicator,
296
320
  meshReplicator: this._meshReplicator,
297
321
  runtimeParams: this._runtimeParams as DataSpaceManagerRuntimeParams,
@@ -299,6 +323,14 @@ export class ServiceContext extends Resource {
299
323
  });
300
324
  await this.dataSpaceManager.open();
301
325
 
326
+ this.edgeAgentManager = new EdgeAgentManager(
327
+ this._edgeFeatures,
328
+ this._edgeHttpClient,
329
+ this.dataSpaceManager,
330
+ identity,
331
+ );
332
+ await this.edgeAgentManager.open();
333
+
302
334
  this._handlerFactories.set(Invitation.Kind.SPACE, (invitation) => {
303
335
  invariant(this.dataSpaceManager, 'dataSpaceManager not initialized yet');
304
336
  return new SpaceInvitationProtocol(this.dataSpaceManager, signingContext, this.keyring, invitation.spaceKey);
@@ -338,4 +370,50 @@ export class ServiceContext extends Resource {
338
370
 
339
371
  await identity.space.spaceState.addCredentialProcessor(this._deviceSpaceSync);
340
372
  }
373
+
374
+ private async _setNetworkIdentity(params?: { deviceCredential: Credential }) {
375
+ using _ = await this._edgeIdentityUpdateMutex.acquire();
376
+
377
+ let edgeIdentity: EdgeIdentity;
378
+ const identity = this.identityManager.identity;
379
+ if (identity) {
380
+ log.info('Setting identity on edge connection', {
381
+ identity: identity.identityKey.toHex(),
382
+ swarms: this.networkManager.topics,
383
+ });
384
+ if (params?.deviceCredential) {
385
+ edgeIdentity = await createChainEdgeIdentity(
386
+ identity.signer,
387
+ identity.identityKey,
388
+ identity.deviceKey,
389
+ { credential: params.deviceCredential },
390
+ [], // TODO(dmaretskyi): Service access credentials.
391
+ );
392
+ } else {
393
+ // TODO: throw here or from identity if device chain can't be loaded, to avoid indefinite hangup
394
+ await warnAfterTimeout(10_000, 'Waiting for identity to be ready for edge connection', async () => {
395
+ await identity.ready();
396
+ });
397
+
398
+ invariant(identity.deviceCredentialChain);
399
+
400
+ edgeIdentity = await createChainEdgeIdentity(
401
+ identity.signer,
402
+ identity.identityKey,
403
+ identity.deviceKey,
404
+ identity.deviceCredentialChain,
405
+ [], // TODO(dmaretskyi): Service access credentials.
406
+ );
407
+ }
408
+ } else {
409
+ edgeIdentity = await createEphemeralEdgeIdentity();
410
+ }
411
+
412
+ this._edgeConnection?.setIdentity(edgeIdentity);
413
+ this._edgeHttpClient?.setIdentity(edgeIdentity);
414
+ this.networkManager.setPeerInfo({
415
+ identityKey: edgeIdentity.identityKey,
416
+ peerKey: edgeIdentity.peerKey,
417
+ });
418
+ }
341
419
  }