@dxos/client-services 0.6.13 → 0.6.14-main.1366248

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 (145) hide show
  1. package/dist/lib/browser/{chunk-CRXXOI45.mjs → chunk-KQURG5PQ.mjs} +6537 -5279
  2. package/dist/lib/browser/chunk-KQURG5PQ.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 +20 -9
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-PZ3JJJ3K.cjs → chunk-WNXJ7EP5.cjs} +6324 -5068
  9. package/dist/lib/node/chunk-WNXJ7EP5.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 +26 -14
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/lib/node-esm/chunk-OQSP5ADY.mjs +8982 -0
  16. package/dist/lib/node-esm/chunk-OQSP5ADY.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 +431 -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/devices/devices-service.d.ts +3 -1
  31. package/dist/types/src/packlets/devices/devices-service.d.ts.map +1 -1
  32. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  33. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  34. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +2 -0
  35. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +1 -0
  36. package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
  37. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  38. package/dist/types/src/packlets/identity/identity-manager.d.ts +28 -9
  39. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  40. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts +18 -0
  41. package/dist/types/src/packlets/identity/identity-recovery-manager.d.ts.map +1 -0
  42. package/dist/types/src/packlets/identity/identity-service.d.ts +7 -2
  43. package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
  44. package/dist/types/src/packlets/identity/identity.d.ts +12 -3
  45. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  46. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  47. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +31 -0
  48. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -0
  49. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +2 -1
  50. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  51. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +2 -1
  52. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  53. package/dist/types/src/packlets/invitations/invitation-state.d.ts +19 -0
  54. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +1 -0
  55. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
  56. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  57. package/dist/types/src/packlets/invitations/invitations-service.d.ts.map +1 -1
  58. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  59. package/dist/types/src/packlets/services/service-context.d.ts +14 -9
  60. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  61. package/dist/types/src/packlets/services/service-host.d.ts +2 -0
  62. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  63. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +7 -3
  64. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  65. package/dist/types/src/packlets/spaces/data-space.d.ts +5 -3
  66. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  67. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +4 -0
  68. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  69. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +2 -0
  70. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +1 -0
  71. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
  72. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  73. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +35 -6
  74. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  75. package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
  76. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  77. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  78. package/dist/types/src/packlets/testing/invitation-utils.d.ts.map +1 -1
  79. package/dist/types/src/packlets/testing/test-builder.d.ts +1 -2
  80. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  81. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  82. package/dist/types/src/testing/setup.d.ts +3 -0
  83. package/dist/types/src/testing/setup.d.ts.map +1 -0
  84. package/dist/types/src/version.d.ts +1 -1
  85. package/dist/types/src/version.d.ts.map +1 -1
  86. package/package.json +44 -45
  87. package/src/index.ts +1 -0
  88. package/src/packlets/agents/edge-agent-manager.ts +163 -0
  89. package/src/packlets/agents/edge-agent-service.ts +42 -0
  90. package/src/packlets/agents/index.ts +6 -0
  91. package/src/packlets/devices/devices-service.test.ts +4 -5
  92. package/src/packlets/devices/devices-service.ts +18 -7
  93. package/src/packlets/diagnostics/diagnostics-broadcast.ts +1 -0
  94. package/src/packlets/identity/{authenticator.test.ts → authenticator.node.test.ts} +2 -3
  95. package/src/packlets/identity/authenticator.ts +5 -2
  96. package/src/packlets/identity/contacts-service.ts +1 -1
  97. package/src/packlets/identity/identity-manager.test.ts +31 -16
  98. package/src/packlets/identity/identity-manager.ts +76 -32
  99. package/src/packlets/identity/identity-recovery-manager.ts +95 -0
  100. package/src/packlets/identity/identity-service.test.ts +5 -8
  101. package/src/packlets/identity/identity-service.ts +11 -5
  102. package/src/packlets/identity/identity.test.ts +141 -239
  103. package/src/packlets/identity/identity.ts +60 -17
  104. package/src/packlets/invitations/device-invitation-protocol.test.ts +7 -4
  105. package/src/packlets/invitations/device-invitation-protocol.ts +8 -2
  106. package/src/packlets/invitations/edge-invitation-handler.ts +188 -0
  107. package/src/packlets/invitations/invitation-guest-extenstion.ts +13 -9
  108. package/src/packlets/invitations/invitation-host-extension.ts +15 -17
  109. package/src/packlets/invitations/invitation-state.ts +112 -0
  110. package/src/packlets/invitations/invitations-handler.test.ts +19 -15
  111. package/src/packlets/invitations/invitations-handler.ts +91 -126
  112. package/src/packlets/invitations/invitations-service.ts +5 -1
  113. package/src/packlets/invitations/space-invitation-protocol.test.ts +4 -3
  114. package/src/packlets/invitations/space-invitation-protocol.ts +5 -0
  115. package/src/packlets/logging/logging.test.ts +1 -2
  116. package/src/packlets/network/network-service.test.ts +2 -3
  117. package/src/packlets/services/service-context.test.ts +3 -1
  118. package/src/packlets/services/service-context.ts +113 -35
  119. package/src/packlets/services/service-host.test.ts +8 -12
  120. package/src/packlets/services/service-host.ts +26 -8
  121. package/src/packlets/services/service-registry.test.ts +1 -2
  122. package/src/packlets/spaces/data-space-manager.test.ts +2 -2
  123. package/src/packlets/spaces/data-space-manager.ts +44 -7
  124. package/src/packlets/spaces/data-space.ts +37 -6
  125. package/src/packlets/spaces/edge-feed-replicator.test.ts +257 -0
  126. package/src/packlets/spaces/edge-feed-replicator.ts +84 -22
  127. package/src/packlets/spaces/epoch-migrations.ts +2 -2
  128. package/src/packlets/spaces/notarization-plugin.test.ts +10 -7
  129. package/src/packlets/spaces/notarization-plugin.ts +196 -29
  130. package/src/packlets/spaces/spaces-service.test.ts +5 -9
  131. package/src/packlets/spaces/spaces-service.ts +6 -1
  132. package/src/packlets/storage/storage.ts +0 -1
  133. package/src/packlets/system/system-service.test.ts +1 -2
  134. package/src/packlets/testing/invitation-utils.ts +7 -0
  135. package/src/packlets/testing/test-builder.ts +7 -4
  136. package/src/packlets/worker/worker-runtime.ts +2 -2
  137. package/src/testing/setup.ts +11 -0
  138. package/src/version.ts +1 -5
  139. package/dist/lib/browser/chunk-CRXXOI45.mjs.map +0 -7
  140. package/dist/lib/node/chunk-PZ3JJJ3K.cjs.map +0 -7
  141. package/dist/types/src/packlets/identity/authenticator.test.d.ts +0 -2
  142. package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +0 -1
  143. package/dist/types/src/packlets/services/automerge-host.test.d.ts +0 -2
  144. package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +0 -1
  145. package/src/packlets/services/automerge-host.test.ts +0 -60
@@ -14,15 +14,18 @@ import {
14
14
  } from '@dxos/credentials';
15
15
  import { type Signer } from '@dxos/crypto';
16
16
  import { type Space } from '@dxos/echo-pipeline';
17
- import { writeMessages } from '@dxos/feed-store';
17
+ import { type EdgeConnection } from '@dxos/edge-client';
18
+ import { writeMessages, type FeedWrapper } from '@dxos/feed-store';
18
19
  import { invariant } from '@dxos/invariant';
19
20
  import { PublicKey, type SpaceId } from '@dxos/keys';
20
21
  import { log } from '@dxos/log';
22
+ import { type Runtime } from '@dxos/protocols/proto/dxos/config';
21
23
  import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
22
24
  import {
23
25
  AdmittedFeed,
24
26
  type DeviceProfileDocument,
25
27
  type ProfileDocument,
28
+ type Credential,
26
29
  } from '@dxos/protocols/proto/dxos/halo/credentials';
27
30
  import { type DeviceAdmissionRequest } from '@dxos/protocols/proto/dxos/halo/invitations';
28
31
  import { type Presence } from '@dxos/teleport-extension-gossip';
@@ -32,6 +35,7 @@ import { type ComplexMap, ComplexSet } from '@dxos/util';
32
35
 
33
36
  import { TrustedKeySetAuthVerifier } from './authenticator';
34
37
  import { DefaultSpaceStateMachine } from './default-space-state-machine';
38
+ import { EdgeFeedReplicator } from '../spaces';
35
39
 
36
40
  export type IdentityParams = {
37
41
  identityKey: PublicKey;
@@ -39,6 +43,9 @@ export type IdentityParams = {
39
43
  signer: Signer;
40
44
  space: Space;
41
45
  presence?: Presence;
46
+
47
+ edgeConnection?: EdgeConnection;
48
+ edgeFeatures?: Runtime.Client.EdgeFeatures;
42
49
  };
43
50
 
44
51
  /**
@@ -52,6 +59,8 @@ export class Identity {
52
59
  private readonly _deviceStateMachine: DeviceStateMachine;
53
60
  private readonly _profileStateMachine: ProfileStateMachine;
54
61
  private readonly _defaultSpaceStateMachine: DefaultSpaceStateMachine;
62
+ private readonly _edgeFeedReplicator?: EdgeFeedReplicator = undefined;
63
+
55
64
  public readonly authVerifier: TrustedKeySetAuthVerifier;
56
65
 
57
66
  public readonly identityKey: PublicKey;
@@ -59,15 +68,15 @@ export class Identity {
59
68
 
60
69
  public readonly stateUpdate = new Event();
61
70
 
62
- constructor({ space, signer, identityKey, deviceKey, presence }: IdentityParams) {
63
- this.space = space;
64
- this._signer = signer;
65
- this._presence = presence;
71
+ constructor(params: IdentityParams) {
72
+ this.space = params.space;
73
+ this._signer = params.signer;
74
+ this._presence = params.presence;
66
75
 
67
- this.identityKey = identityKey;
68
- this.deviceKey = deviceKey;
76
+ this.identityKey = params.identityKey;
77
+ this.deviceKey = params.deviceKey;
69
78
 
70
- log.trace('dxos.halo.device', { deviceKey });
79
+ log.trace('dxos.halo.device', { deviceKey: params.deviceKey });
71
80
 
72
81
  this._deviceStateMachine = new DeviceStateMachine({
73
82
  identityKey: this.identityKey,
@@ -88,6 +97,10 @@ export class Identity {
88
97
  update: this.stateUpdate,
89
98
  authTimeout: AUTH_TIMEOUT,
90
99
  });
100
+
101
+ if (params.edgeConnection && params.edgeFeatures?.feedReplicator) {
102
+ this._edgeFeedReplicator = new EdgeFeedReplicator({ messenger: params.edgeConnection, spaceId: this.space.id });
103
+ }
91
104
  }
92
105
 
93
106
  // TODO(burdon): Expose state object?
@@ -105,9 +118,17 @@ export class Identity {
105
118
  await this.space.spaceState.addCredentialProcessor(this._deviceStateMachine);
106
119
  await this.space.spaceState.addCredentialProcessor(this._profileStateMachine);
107
120
  await this.space.spaceState.addCredentialProcessor(this._defaultSpaceStateMachine);
121
+ if (this._edgeFeedReplicator) {
122
+ this.space.protocol.feedAdded.append(this._onFeedAdded);
123
+ }
108
124
  await this.space.open(ctx);
109
125
  }
110
126
 
127
+ public async joinNetwork() {
128
+ await this.space.startProtocol();
129
+ await this._edgeFeedReplicator?.open();
130
+ }
131
+
111
132
  @trace.span()
112
133
  async close(ctx: Context) {
113
134
  await this._presence?.close();
@@ -115,6 +136,13 @@ export class Identity {
115
136
  await this.space.spaceState.removeCredentialProcessor(this._defaultSpaceStateMachine);
116
137
  await this.space.spaceState.removeCredentialProcessor(this._profileStateMachine);
117
138
  await this.space.spaceState.removeCredentialProcessor(this._deviceStateMachine);
139
+
140
+ if (this._edgeFeedReplicator) {
141
+ this.space.protocol.feedAdded.remove(this._onFeedAdded);
142
+ }
143
+
144
+ await this._edgeFeedReplicator?.close();
145
+
118
146
  await this.space.close();
119
147
  }
120
148
 
@@ -135,6 +163,10 @@ export class Identity {
135
163
  return this.space.controlPipeline;
136
164
  }
137
165
 
166
+ get haloSpaceId() {
167
+ return this.space.id;
168
+ }
169
+
138
170
  get haloSpaceKey() {
139
171
  return this.space.key;
140
172
  }
@@ -151,6 +183,10 @@ export class Identity {
151
183
  return this._presence;
152
184
  }
153
185
 
186
+ get signer() {
187
+ return this._signer;
188
+ }
189
+
154
190
  /**
155
191
  * Issues credentials as identity.
156
192
  * Requires identity to be ready.
@@ -180,7 +216,7 @@ export class Identity {
180
216
  await this.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
181
217
  }
182
218
 
183
- async admitDevice({ deviceKey, controlFeedKey, dataFeedKey }: DeviceAdmissionRequest) {
219
+ async admitDevice({ deviceKey, controlFeedKey, dataFeedKey }: DeviceAdmissionRequest): Promise<Credential> {
184
220
  log('Admitting device:', {
185
221
  identityKey: this.identityKey,
186
222
  hostDevice: this.deviceKey,
@@ -189,17 +225,18 @@ export class Identity {
189
225
  dataFeedKey,
190
226
  });
191
227
  const signer = this.getIdentityCredentialSigner();
228
+ const deviceCredential = await signer.createCredential({
229
+ subject: deviceKey,
230
+ assertion: {
231
+ '@type': 'dxos.halo.credentials.AuthorizedDevice',
232
+ identityKey: this.identityKey,
233
+ deviceKey,
234
+ },
235
+ });
192
236
  await writeMessages(
193
237
  this.controlPipeline.writer,
194
238
  [
195
- await signer.createCredential({
196
- subject: deviceKey,
197
- assertion: {
198
- '@type': 'dxos.halo.credentials.AuthorizedDevice',
199
- identityKey: this.identityKey,
200
- deviceKey,
201
- },
202
- }),
239
+ deviceCredential,
203
240
  await signer.createCredential({
204
241
  subject: controlFeedKey,
205
242
  assertion: {
@@ -222,5 +259,11 @@ export class Identity {
222
259
  }),
223
260
  ].map((credential): FeedMessage.Payload => ({ credential: { credential } })),
224
261
  );
262
+
263
+ return deviceCredential;
225
264
  }
265
+
266
+ private _onFeedAdded = async (feed: FeedWrapper<any>) => {
267
+ await this._edgeFeedReplicator!.addFeed(feed);
268
+ };
226
269
  }
@@ -2,19 +2,20 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
- import { expect } from 'chai';
5
+ import { describe, expect, test, onTestFinished } from 'vitest';
6
6
 
7
7
  import { asyncChain } from '@dxos/async';
8
8
  import { Context } from '@dxos/context';
9
9
  import { AlreadyJoinedError } from '@dxos/protocols';
10
10
  import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
11
- import { describe, test, afterTest } from '@dxos/test';
12
11
 
13
12
  import { type ServiceContext } from '../services';
14
13
  import { createPeers, createServiceContext, performInvitation } from '../testing';
15
14
 
16
15
  const closeAfterTest = async (peer: ServiceContext) => {
17
- afterTest(() => peer.close());
16
+ onTestFinished(async () => {
17
+ await peer.close();
18
+ });
18
19
  return peer;
19
20
  };
20
21
 
@@ -22,7 +23,9 @@ describe('services/device', () => {
22
23
  test('creates identity', async () => {
23
24
  const peer = await createServiceContext();
24
25
  await peer.open(new Context());
25
- afterTest(() => peer.close());
26
+ onTestFinished(async () => {
27
+ await peer.close();
28
+ });
26
29
 
27
30
  const identity = await peer.createIdentity();
28
31
  expect(identity).not.to.be.undefined;
@@ -2,6 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { getCredentialAssertion } from '@dxos/credentials';
5
6
  import { invariant } from '@dxos/invariant';
6
7
  import { type Keyring } from '@dxos/keyring';
7
8
  import { type PublicKey } from '@dxos/keys';
@@ -25,7 +26,9 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
25
26
  ) {}
26
27
 
27
28
  toJSON(): object {
28
- return {};
29
+ return {
30
+ kind: 'device',
31
+ };
29
32
  }
30
33
 
31
34
  checkCanInviteNewMembers(): ApiError | undefined {
@@ -49,7 +52,8 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
49
52
  async admit(_: Invitation, request: AdmissionRequest): Promise<AdmissionResponse> {
50
53
  invariant(request.device);
51
54
  const identity = this._getIdentity();
52
- await identity.admitDevice(request.device);
55
+ const credential = await identity.admitDevice(request.device);
56
+ invariant(getCredentialAssertion(credential)['@type'] === 'dxos.halo.credentials.AuthorizedDevice');
53
57
 
54
58
  return {
55
59
  device: {
@@ -57,6 +61,7 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
57
61
  haloSpaceKey: identity.haloSpaceKey,
58
62
  genesisFeedKey: identity.haloGenesisFeedKey,
59
63
  controlTimeframe: identity.controlPipeline.state.timeframe,
64
+ credential,
60
65
  },
61
66
  };
62
67
  }
@@ -109,6 +114,7 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
109
114
  dataFeedKey,
110
115
  controlTimeframe,
111
116
  deviceProfile: profile,
117
+ authorizedDeviceCredential: response.device.credential,
112
118
  });
113
119
 
114
120
  return { identityKey };
@@ -0,0 +1,188 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type MutexGuard, scheduleMicroTask, scheduleTask } from '@dxos/async';
6
+ import { type Context } from '@dxos/context';
7
+ import { sign } from '@dxos/crypto';
8
+ import { type EdgeHttpClient } from '@dxos/edge-client';
9
+ import { invariant } from '@dxos/invariant';
10
+ import { SpaceId } from '@dxos/keys';
11
+ import { log } from '@dxos/log';
12
+ import {
13
+ EdgeAuthChallengeError,
14
+ EdgeCallFailedError,
15
+ type JoinSpaceRequest,
16
+ type JoinSpaceResponseBody,
17
+ } from '@dxos/protocols';
18
+ import { schema } from '@dxos/protocols/proto';
19
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
20
+ import { type DeviceProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
21
+ import {
22
+ type AdmissionResponse,
23
+ type AdmissionRequest,
24
+ type SpaceAdmissionRequest,
25
+ } from '@dxos/protocols/proto/dxos/halo/invitations';
26
+
27
+ import { type InvitationProtocol } from './invitation-protocol';
28
+ import { type FlowLockHolder, type GuardedInvitationState } from './invitation-state';
29
+ import { tryAcquireBeforeContextDisposed } from './utils';
30
+
31
+ export interface EdgeInvitationHandlerCallbacks {
32
+ onInvitationSuccess(response: AdmissionResponse, request: AdmissionRequest): Promise<void>;
33
+ }
34
+
35
+ export const MAX_RETRIES_PER_INVITATION = 5;
36
+ export const DEFAULT_REQUEST_RETRY_INTERVAL_MS = 3000;
37
+ export const DEFAULT_REQUEST_RETRY_JITTER_MS = 500;
38
+
39
+ export type EdgeInvitationConfig = {
40
+ retryInterval?: number;
41
+ retryJitter?: number;
42
+ };
43
+
44
+ export class EdgeInvitationHandler implements FlowLockHolder {
45
+ private _flowLock: MutexGuard | undefined;
46
+
47
+ private readonly _retryInterval: number;
48
+ private readonly _retryJitter: number;
49
+
50
+ constructor(
51
+ config: EdgeInvitationConfig | undefined,
52
+ private readonly _client: EdgeHttpClient | undefined,
53
+ private readonly _callbacks: EdgeInvitationHandlerCallbacks,
54
+ ) {
55
+ this._retryInterval = config?.retryInterval ?? DEFAULT_REQUEST_RETRY_INTERVAL_MS;
56
+ this._retryJitter = config?.retryJitter ?? DEFAULT_REQUEST_RETRY_JITTER_MS;
57
+ }
58
+
59
+ public handle(
60
+ ctx: Context,
61
+ guardedState: GuardedInvitationState,
62
+ protocol: InvitationProtocol,
63
+ deviceProfile?: DeviceProfileDocument,
64
+ ) {
65
+ if (!this._client) {
66
+ log('edge disabled');
67
+ return;
68
+ }
69
+
70
+ const invitation = guardedState.current;
71
+ const spaceId = invitation.spaceId;
72
+ const canBeHandledByEdge =
73
+ invitation.authMethod !== Invitation.AuthMethod.SHARED_SECRET &&
74
+ invitation.type === Invitation.Type.DELEGATED &&
75
+ invitation.kind === Invitation.Kind.SPACE &&
76
+ spaceId != null &&
77
+ SpaceId.isValid(spaceId);
78
+
79
+ if (!canBeHandledByEdge) {
80
+ log('invitation could not be handled by edge', { invitation });
81
+ return;
82
+ }
83
+
84
+ ctx.onDispose(() => {
85
+ this._flowLock?.release();
86
+ this._flowLock = undefined;
87
+ });
88
+
89
+ let requestCount = 0;
90
+ const tryHandleInvitation = async () => {
91
+ requestCount++;
92
+ const admissionRequest = await protocol.createAdmissionRequest(deviceProfile);
93
+ if (admissionRequest.space) {
94
+ try {
95
+ await this._handleSpaceInvitationFlow(ctx, guardedState, admissionRequest.space, spaceId);
96
+ } catch (error) {
97
+ if (error instanceof EdgeCallFailedError) {
98
+ log.info('join space with edge unsuccessful', {
99
+ reason: error.message,
100
+ retryable: error.isRetryable,
101
+ after: error.retryAfterMs ?? this._calculateNextRetryMs(),
102
+ });
103
+ if (error.isRetryable && requestCount < MAX_RETRIES_PER_INVITATION) {
104
+ scheduleTask(ctx, tryHandleInvitation, error.retryAfterMs ?? this._calculateNextRetryMs());
105
+ }
106
+ } else if (requestCount < MAX_RETRIES_PER_INVITATION) {
107
+ log.info('failed to handle invitation with edge', { error });
108
+ scheduleTask(ctx, tryHandleInvitation, this._calculateNextRetryMs());
109
+ }
110
+ }
111
+ }
112
+ };
113
+ scheduleMicroTask(ctx, tryHandleInvitation);
114
+ }
115
+
116
+ private async _handleSpaceInvitationFlow(
117
+ ctx: Context,
118
+ guardedState: GuardedInvitationState,
119
+ admissionRequest: SpaceAdmissionRequest,
120
+ spaceId: SpaceId,
121
+ ) {
122
+ try {
123
+ log('edge invitation flow');
124
+ this._flowLock = await tryAcquireBeforeContextDisposed(ctx, guardedState.mutex);
125
+ log.verbose('edge invitation flow acquired the lock');
126
+
127
+ guardedState.set(this, Invitation.State.CONNECTING);
128
+
129
+ const response = await this._joinSpaceByInvitation(guardedState, spaceId, {
130
+ identityKey: admissionRequest.identityKey.toHex(),
131
+ invitationId: guardedState.current.invitationId,
132
+ });
133
+
134
+ const admissionResponse = await this._mapToAdmissionResponse(response);
135
+ await this._callbacks.onInvitationSuccess(admissionResponse, { space: admissionRequest });
136
+ } catch (error) {
137
+ guardedState.set(this, Invitation.State.ERROR);
138
+ throw error;
139
+ } finally {
140
+ this._flowLock?.release();
141
+ this._flowLock = undefined;
142
+ }
143
+ }
144
+
145
+ private async _mapToAdmissionResponse(edgeResponse: JoinSpaceResponseBody): Promise<AdmissionResponse> {
146
+ const credentialBytes = Buffer.from(edgeResponse.spaceMemberCredential, 'base64');
147
+ const codec = schema.getCodecForType('dxos.halo.credentials.Credential');
148
+ return {
149
+ space: {
150
+ credential: codec.decode(credentialBytes),
151
+ },
152
+ };
153
+ }
154
+
155
+ private async _joinSpaceByInvitation(
156
+ guardedState: GuardedInvitationState,
157
+ spaceId: SpaceId,
158
+ request: JoinSpaceRequest,
159
+ ): Promise<JoinSpaceResponseBody> {
160
+ invariant(this._client);
161
+ try {
162
+ return await this._client.joinSpaceByInvitation(spaceId, request);
163
+ } catch (error: any) {
164
+ if (error instanceof EdgeAuthChallengeError) {
165
+ const publicKey = guardedState.current.guestKeypair?.publicKey;
166
+ const privateKey = guardedState.current.guestKeypair?.privateKey;
167
+ if (!privateKey || !publicKey) {
168
+ throw error;
169
+ }
170
+ const signature = sign(Buffer.from(error.challenge, 'base64'), privateKey);
171
+ return this._client.joinSpaceByInvitation(spaceId, {
172
+ ...request,
173
+ signature: Buffer.from(signature).toString('base64'),
174
+ });
175
+ } else {
176
+ throw error;
177
+ }
178
+ }
179
+ }
180
+
181
+ public hasFlowLock(): boolean {
182
+ return this._flowLock != null;
183
+ }
184
+
185
+ private _calculateNextRetryMs() {
186
+ return this._retryInterval + Math.random() * this._retryJitter;
187
+ }
188
+ }
@@ -12,6 +12,7 @@ import { type Invitation } from '@dxos/protocols/proto/dxos/client/services';
12
12
  import { type InvitationHostService, InvitationOptions } from '@dxos/protocols/proto/dxos/halo/invitations';
13
13
  import { type ExtensionContext, RpcExtension } from '@dxos/teleport';
14
14
 
15
+ import { type FlowLockHolder } from './invitation-state';
15
16
  import { tryAcquireBeforeContextDisposed } from './utils';
16
17
 
17
18
  const OPTIONS_TIMEOUT = 10_000;
@@ -27,10 +28,13 @@ type InvitationGuestExtensionCallbacks = {
27
28
  /**
28
29
  * Guest's side for a connection to a concrete peer in p2p network during invitation.
29
30
  */
30
- export class InvitationGuestExtension extends RpcExtension<
31
- { InvitationHostService: InvitationHostService },
32
- { InvitationHostService: InvitationHostService }
33
- > {
31
+ export class InvitationGuestExtension
32
+ extends RpcExtension<
33
+ { InvitationHostService: InvitationHostService },
34
+ { InvitationHostService: InvitationHostService }
35
+ >
36
+ implements FlowLockHolder
37
+ {
34
38
  private _ctx = new Context();
35
39
  private _remoteOptions?: InvitationOptions;
36
40
  private _remoteOptionsTrigger = new Trigger();
@@ -82,16 +86,16 @@ export class InvitationGuestExtension extends RpcExtension<
82
86
  await super.onOpen(context);
83
87
 
84
88
  try {
85
- log('guest acquire lock');
89
+ log.verbose('guest acquire lock');
86
90
  this._invitationFlowLock = await tryAcquireBeforeContextDisposed(this._ctx, this._invitationFlowMutex);
87
- log('guest lock acquired');
91
+ log.verbose('guest lock acquired');
88
92
  await cancelWithContext(
89
93
  this._ctx,
90
94
  this.rpc.InvitationHostService.options({ role: InvitationOptions.Role.GUEST }),
91
95
  );
92
- log('options sent');
96
+ log.verbose('options sent');
93
97
  await cancelWithContext(this._ctx, this._remoteOptionsTrigger.wait({ timeout: OPTIONS_TIMEOUT }));
94
- log('options received');
98
+ log.verbose('options received');
95
99
  if (this._remoteOptions?.role !== InvitationOptions.Role.HOST) {
96
100
  throw new InvalidInvitationExtensionRoleError(undefined, {
97
101
  expected: InvitationOptions.Role.HOST,
@@ -124,7 +128,7 @@ export class InvitationGuestExtension extends RpcExtension<
124
128
  if (this._invitationFlowLock != null) {
125
129
  this._invitationFlowLock.release();
126
130
  this._invitationFlowLock = null;
127
- log('invitation flow lock released');
131
+ log.verbose('invitation flow lock released');
128
132
  }
129
133
  }
130
134
  }
@@ -21,6 +21,7 @@ import {
21
21
  } from '@dxos/protocols/proto/dxos/halo/invitations';
22
22
  import { type ExtensionContext, RpcExtension } from '@dxos/teleport';
23
23
 
24
+ import type { FlowLockHolder } from './invitation-state';
24
25
  import { stateToString, tryAcquireBeforeContextDisposed } from './utils';
25
26
 
26
27
  /// Timeout for the options exchange.
@@ -43,10 +44,13 @@ type InvitationHostExtensionCallbacks = {
43
44
  /**
44
45
  * Host's side for a connection to a concrete peer in p2p network during invitation.
45
46
  */
46
- export class InvitationHostExtension extends RpcExtension<
47
- { InvitationHostService: InvitationHostService },
48
- { InvitationHostService: InvitationHostService }
49
- > {
47
+ export class InvitationHostExtension
48
+ extends RpcExtension<
49
+ { InvitationHostService: InvitationHostService },
50
+ { InvitationHostService: InvitationHostService }
51
+ >
52
+ implements FlowLockHolder
53
+ {
50
54
  /**
51
55
  * @internal
52
56
  */
@@ -106,13 +110,11 @@ export class InvitationHostExtension extends RpcExtension<
106
110
 
107
111
  introduce: async (request) => {
108
112
  const { profile, invitationId } = request;
109
-
110
113
  const traceId = PublicKey.random().toHex();
111
114
  log.trace('dxos.sdk.invitation-handler.host.introduce', trace.begin({ id: traceId }));
112
115
 
113
116
  const invitation = this._requireActiveInvitation();
114
117
  this._assertInvitationState(Invitation.State.CONNECTED);
115
-
116
118
  if (invitationId !== invitation?.invitationId) {
117
119
  log.warn('incorrect invitationId', { expected: invitation.invitationId, actual: invitationId });
118
120
  this._callbacks.onError(new Error('Incorrect invitationId.'));
@@ -123,10 +125,9 @@ export class InvitationHostExtension extends RpcExtension<
123
125
  };
124
126
  }
125
127
 
126
- log('guest introduced themselves', { guestProfile: profile });
128
+ log.verbose('guest introduced themselves', { guestProfile: profile });
127
129
  this.guestProfile = profile;
128
130
  this._callbacks.onStateUpdate(Invitation.State.READY_FOR_AUTHENTICATION);
129
-
130
131
  this._challenge =
131
132
  invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? randomBytes(32) : undefined;
132
133
 
@@ -142,7 +143,7 @@ export class InvitationHostExtension extends RpcExtension<
142
143
  log.trace('dxos.sdk.invitation-handler.host.authenticate', trace.begin({ id: traceId }));
143
144
 
144
145
  const invitation = this._requireActiveInvitation();
145
- log('received authentication request', { authCode: code });
146
+ log.verbose('received authentication request', { authCode: code });
146
147
  let status = AuthenticationResponse.Status.OK;
147
148
 
148
149
  this._assertInvitationState([Invitation.State.AUTHENTICATING, Invitation.State.READY_FOR_AUTHENTICATION]);
@@ -235,18 +236,15 @@ export class InvitationHostExtension extends RpcExtension<
235
236
  await super.onOpen(context);
236
237
 
237
238
  try {
238
- log('host acquire lock');
239
+ log.verbose('host acquire lock');
239
240
  this._invitationFlowLock = await tryAcquireBeforeContextDisposed(this._ctx, this._invitationFlowMutex);
240
- log('host lock acquired');
241
- const lastState = this._requireActiveInvitation().state;
241
+ log.verbose('host lock acquired');
242
242
  this._callbacks.onStateUpdate(Invitation.State.CONNECTING);
243
243
  await this.rpc.InvitationHostService.options({ role: InvitationOptions.Role.HOST });
244
- log('options sent');
244
+ log.verbose('options sent');
245
245
  await cancelWithContext(this._ctx, this._remoteOptionsTrigger.wait({ timeout: OPTIONS_TIMEOUT }));
246
- log('options received');
246
+ log.verbose('options received');
247
247
  if (this._remoteOptions?.role !== InvitationOptions.Role.GUEST) {
248
- // we connected with another host, restore previous real invitation flow status
249
- this._callbacks.onStateUpdate(lastState);
250
248
  throw new InvalidInvitationExtensionRoleError(undefined, {
251
249
  expected: InvitationOptions.Role.GUEST,
252
250
  remoteOptions: this._remoteOptions,
@@ -298,7 +296,7 @@ export class InvitationHostExtension extends RpcExtension<
298
296
  if (this._invitationFlowLock != null) {
299
297
  this._invitationFlowLock?.release();
300
298
  this._invitationFlowLock = null;
301
- log('invitation flow lock released');
299
+ log.verbose('invitation flow lock released');
302
300
  }
303
301
  }
304
302
  }