@dxos/client-services 0.6.13-main.ed424a1 → 0.6.13

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 (132) hide show
  1. package/dist/lib/browser/{chunk-IPWEAPT2.mjs → chunk-CRXXOI45.mjs} +5186 -6222
  2. package/dist/lib/browser/chunk-CRXXOI45.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3 -7
  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 +7 -12
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-DJIOUOAF.cjs → chunk-PZ3JJJ3K.cjs} +5137 -6167
  9. package/dist/lib/node/chunk-PZ3JJJ3K.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +46 -50
  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 +13 -18
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/types/src/index.d.ts +0 -1
  16. package/dist/types/src/index.d.ts.map +1 -1
  17. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  18. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  19. package/dist/types/src/packlets/identity/authenticator.test.d.ts +2 -0
  20. package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +1 -0
  21. package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
  22. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  23. package/dist/types/src/packlets/identity/identity-manager.d.ts +9 -25
  24. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  25. package/dist/types/src/packlets/identity/identity.d.ts +3 -12
  26. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  27. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  28. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +1 -2
  29. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  30. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +1 -2
  31. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  32. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
  33. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  34. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  35. package/dist/types/src/packlets/services/automerge-host.test.d.ts +2 -0
  36. package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +1 -0
  37. package/dist/types/src/packlets/services/service-context.d.ts +9 -12
  38. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  39. package/dist/types/src/packlets/services/service-host.d.ts +0 -1
  40. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  41. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +3 -7
  42. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  43. package/dist/types/src/packlets/spaces/data-space.d.ts +3 -5
  44. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  45. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +0 -3
  46. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  47. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
  48. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  49. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +6 -35
  50. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  51. package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
  52. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  53. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  54. package/dist/types/src/packlets/testing/test-builder.d.ts +2 -1
  55. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  56. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  57. package/dist/types/src/version.d.ts +1 -1
  58. package/dist/types/src/version.d.ts.map +1 -1
  59. package/package.json +39 -43
  60. package/src/index.ts +0 -1
  61. package/src/packlets/devices/devices-service.test.ts +5 -4
  62. package/src/packlets/diagnostics/diagnostics-broadcast.ts +0 -1
  63. package/src/packlets/identity/{authenticator.node.test.ts → authenticator.test.ts} +3 -2
  64. package/src/packlets/identity/authenticator.ts +2 -5
  65. package/src/packlets/identity/contacts-service.ts +1 -1
  66. package/src/packlets/identity/identity-manager.test.ts +16 -31
  67. package/src/packlets/identity/identity-manager.ts +31 -47
  68. package/src/packlets/identity/identity-service.test.ts +8 -4
  69. package/src/packlets/identity/identity.test.ts +239 -130
  70. package/src/packlets/identity/identity.ts +17 -60
  71. package/src/packlets/invitations/device-invitation-protocol.test.ts +4 -7
  72. package/src/packlets/invitations/device-invitation-protocol.ts +1 -5
  73. package/src/packlets/invitations/invitation-guest-extenstion.ts +4 -8
  74. package/src/packlets/invitations/invitation-host-extension.ts +7 -8
  75. package/src/packlets/invitations/invitations-handler.test.ts +9 -16
  76. package/src/packlets/invitations/invitations-handler.ts +93 -23
  77. package/src/packlets/invitations/space-invitation-protocol.test.ts +3 -4
  78. package/src/packlets/invitations/space-invitation-protocol.ts +0 -4
  79. package/src/packlets/logging/logging.test.ts +2 -1
  80. package/src/packlets/network/network-service.test.ts +3 -2
  81. package/src/packlets/services/automerge-host.test.ts +60 -0
  82. package/src/packlets/services/service-context.test.ts +1 -3
  83. package/src/packlets/services/service-context.ts +37 -104
  84. package/src/packlets/services/service-host.test.ts +12 -8
  85. package/src/packlets/services/service-host.ts +6 -16
  86. package/src/packlets/services/service-registry.test.ts +2 -1
  87. package/src/packlets/spaces/data-space-manager.test.ts +2 -2
  88. package/src/packlets/spaces/data-space-manager.ts +7 -44
  89. package/src/packlets/spaces/data-space.ts +6 -37
  90. package/src/packlets/spaces/edge-feed-replicator.ts +22 -80
  91. package/src/packlets/spaces/epoch-migrations.ts +2 -2
  92. package/src/packlets/spaces/notarization-plugin.test.ts +7 -10
  93. package/src/packlets/spaces/notarization-plugin.ts +29 -196
  94. package/src/packlets/spaces/spaces-service.test.ts +9 -5
  95. package/src/packlets/spaces/spaces-service.ts +1 -6
  96. package/src/packlets/storage/storage.ts +1 -0
  97. package/src/packlets/system/system-service.test.ts +2 -1
  98. package/src/packlets/testing/test-builder.ts +4 -7
  99. package/src/packlets/worker/worker-runtime.ts +2 -2
  100. package/src/version.ts +5 -1
  101. package/dist/lib/browser/chunk-IPWEAPT2.mjs.map +0 -7
  102. package/dist/lib/node/chunk-DJIOUOAF.cjs.map +0 -7
  103. package/dist/lib/node-esm/chunk-MMU5KC57.mjs +0 -8752
  104. package/dist/lib/node-esm/chunk-MMU5KC57.mjs.map +0 -7
  105. package/dist/lib/node-esm/index.mjs +0 -420
  106. package/dist/lib/node-esm/index.mjs.map +0 -7
  107. package/dist/lib/node-esm/meta.json +0 -1
  108. package/dist/lib/node-esm/testing/index.mjs +0 -424
  109. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  110. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +0 -35
  111. package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +0 -1
  112. package/dist/types/src/packlets/agents/edge-agent-service.d.ts +0 -10
  113. package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +0 -1
  114. package/dist/types/src/packlets/agents/index.d.ts +0 -3
  115. package/dist/types/src/packlets/agents/index.d.ts.map +0 -1
  116. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +0 -2
  117. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +0 -1
  118. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +0 -30
  119. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +0 -1
  120. package/dist/types/src/packlets/invitations/invitation-state.d.ts +0 -19
  121. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +0 -1
  122. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +0 -2
  123. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +0 -1
  124. package/dist/types/src/testing/setup.d.ts +0 -3
  125. package/dist/types/src/testing/setup.d.ts.map +0 -1
  126. package/src/packlets/agents/edge-agent-manager.ts +0 -163
  127. package/src/packlets/agents/edge-agent-service.ts +0 -42
  128. package/src/packlets/agents/index.ts +0 -6
  129. package/src/packlets/invitations/edge-invitation-handler.ts +0 -185
  130. package/src/packlets/invitations/invitation-state.ts +0 -111
  131. package/src/packlets/spaces/edge-feed-replicator.test.ts +0 -252
  132. package/src/testing/setup.ts +0 -11
@@ -1,163 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { DeferredTask, Event, scheduleTask, synchronized } from '@dxos/async';
6
- import { Resource } from '@dxos/context';
7
- import { type EdgeHttpClient } from '@dxos/edge-client';
8
- import { invariant } from '@dxos/invariant';
9
- import { PublicKey } from '@dxos/keys';
10
- import { log } from '@dxos/log';
11
- import { EdgeAgentStatus, EdgeCallFailedError } from '@dxos/protocols';
12
- import { SpaceState } from '@dxos/protocols/proto/dxos/client/services';
13
- import { type Runtime } from '@dxos/protocols/proto/dxos/config';
14
-
15
- import { type Identity } from '../identity';
16
- import { type DataSpaceManager } from '../spaces';
17
-
18
- const AGENT_STATUS_QUERY_RETRY_INTERVAL = 5000;
19
- const AGENT_STATUS_QUERY_RETRY_JITTER = 1000;
20
-
21
- export type EdgeAgentManagerConfig = {};
22
-
23
- export class EdgeAgentManager extends Resource {
24
- public agentStatusChanged = new Event<EdgeAgentStatus>();
25
-
26
- private _agentDeviceKey: PublicKey | undefined;
27
- private _agentStatus: EdgeAgentStatus | undefined;
28
-
29
- private _lastKnownDeviceCount = 0;
30
-
31
- private _fetchAgentStatusTask: DeferredTask | undefined;
32
-
33
- constructor(
34
- private readonly _edgeFeatures: Runtime.Client.EdgeFeatures | undefined,
35
- private readonly _edgeHttpClient: EdgeHttpClient | undefined,
36
- private readonly _dataSpaceManager: DataSpaceManager,
37
- private readonly _identity: Identity,
38
- ) {
39
- super();
40
- }
41
-
42
- public get agentStatus(): EdgeAgentStatus | undefined {
43
- return this._agentStatus;
44
- }
45
-
46
- public get agentExists() {
47
- return this._agentStatus && this._agentStatus !== EdgeAgentStatus.NOT_FOUND;
48
- }
49
-
50
- @synchronized
51
- public async createAgent() {
52
- invariant(this.isOpen);
53
- invariant(this._edgeHttpClient);
54
- invariant(this._edgeFeatures?.agents);
55
-
56
- const response = await this._edgeHttpClient.createAgent({
57
- identityKey: this._identity.identityKey.toHex(),
58
- haloSpaceId: this._identity.haloSpaceId,
59
- haloSpaceKey: this._identity.haloSpaceKey.toHex(),
60
- });
61
-
62
- const deviceKey = PublicKey.fromHex(response.deviceKey);
63
-
64
- await this._identity.admitDevice({
65
- deviceKey,
66
- controlFeedKey: PublicKey.fromHex(response.feedKey),
67
- // TODO: agents don't have data feed, should be removed
68
- dataFeedKey: PublicKey.random(),
69
- });
70
-
71
- log('agent created', response);
72
-
73
- this._updateStatus(EdgeAgentStatus.ACTIVE, deviceKey);
74
- }
75
-
76
- protected override async _open() {
77
- const isEnabled = this._edgeHttpClient && this._edgeFeatures?.agents;
78
-
79
- log('edge agent manager open', { isEnabled });
80
-
81
- if (!isEnabled) {
82
- return;
83
- }
84
-
85
- this._lastKnownDeviceCount = this._identity.authorizedDeviceKeys.size;
86
- this._fetchAgentStatusTask = new DeferredTask(this._ctx, async () => {
87
- await this._fetchAgentStatus();
88
- });
89
- this._fetchAgentStatusTask.schedule();
90
-
91
- this._dataSpaceManager.updated.on(this._ctx, () => {
92
- if (this._agentDeviceKey) {
93
- this._ensureAgentIsInSpaces(this._agentDeviceKey);
94
- }
95
- });
96
-
97
- this._identity.stateUpdate.on(this._ctx, () => {
98
- const maybeAgentWasCreated = this._identity.authorizedDeviceKeys.size > this._lastKnownDeviceCount;
99
- if (this.agentExists || !maybeAgentWasCreated) {
100
- return;
101
- }
102
- this._lastKnownDeviceCount = this._identity.authorizedDeviceKeys.size;
103
- this._fetchAgentStatusTask?.schedule();
104
- });
105
- }
106
-
107
- protected override async _close() {
108
- this._fetchAgentStatusTask = undefined;
109
- this._lastKnownDeviceCount = 0;
110
- }
111
-
112
- protected async _fetchAgentStatus() {
113
- invariant(this._edgeHttpClient);
114
- try {
115
- log('fetching agent status');
116
- const { agent } = await this._edgeHttpClient.getAgentStatus({ ownerIdentityKey: this._identity.identityKey });
117
- const wasAgentCreatedDuringQuery = this._agentStatus === EdgeAgentStatus.ACTIVE;
118
- if (!wasAgentCreatedDuringQuery) {
119
- const deviceKey = agent.deviceKey ? PublicKey.fromHex(agent.deviceKey) : undefined;
120
- this._updateStatus(agent.status, deviceKey);
121
- }
122
- } catch (err) {
123
- if (err instanceof EdgeCallFailedError) {
124
- if (!err.isRetryable) {
125
- log.warn('non retryable error on agent status fetch attempt', { err });
126
- return;
127
- }
128
- }
129
- const retryAfterMs = AGENT_STATUS_QUERY_RETRY_INTERVAL + Math.random() * AGENT_STATUS_QUERY_RETRY_JITTER;
130
- log.info('agent status fetching failed', { err, retryAfterMs });
131
- scheduleTask(this._ctx, () => this._fetchAgentStatusTask?.schedule(), retryAfterMs);
132
- }
133
- }
134
-
135
- /**
136
- * We don't want notarization plugin to always actively poll edge looking for credentials to notarize,
137
- * because most of the time we'll be getting an empty response.
138
- * Instead, we stay in active polling mode while there are spaces where we don't see our agent's feed.
139
- */
140
- protected _ensureAgentIsInSpaces(agentDeviceKey: PublicKey) {
141
- for (const space of this._dataSpaceManager.spaces.values()) {
142
- if ([SpaceState.SPACE_INACTIVE, SpaceState.SPACE_CLOSED].includes(space.state)) {
143
- continue;
144
- }
145
- const agentFeedNeedsNotarization = !space.inner.spaceState.feeds
146
- .values()
147
- .some((feed) => feed.assertion.deviceKey.equals(agentDeviceKey));
148
- space.notarizationPlugin.setActiveEdgePollingEnabled(agentFeedNeedsNotarization);
149
-
150
- log.info('set active edge polling', { enabled: agentFeedNeedsNotarization, spaceId: space.id });
151
- }
152
- }
153
-
154
- private _updateStatus(status: EdgeAgentStatus, deviceKey: PublicKey | undefined) {
155
- this._agentStatus = status;
156
- this._agentDeviceKey = deviceKey;
157
- this.agentStatusChanged.emit(status);
158
- if (deviceKey) {
159
- this._ensureAgentIsInSpaces(deviceKey);
160
- }
161
- log.info('agent status update', { status });
162
- }
163
- }
@@ -1,42 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { Stream } from '@dxos/codec-protobuf';
6
- import { EdgeAgentStatus } from '@dxos/protocols';
7
- import { QueryAgentStatusResponse, type EdgeAgentService } from '@dxos/protocols/proto/dxos/client/services';
8
-
9
- import { type EdgeAgentManager } from './edge-agent-manager';
10
-
11
- export class EdgeAgentServiceImpl implements EdgeAgentService {
12
- constructor(private readonly _agentManagerProvider: () => Promise<EdgeAgentManager>) {}
13
-
14
- public async createAgent(): Promise<void> {
15
- return (await this._agentManagerProvider()).createAgent();
16
- }
17
-
18
- queryAgentStatus(): Stream<QueryAgentStatusResponse> {
19
- return new Stream(({ ctx, next }) => {
20
- next({ status: QueryAgentStatusResponse.AgentStatus.UNKNOWN });
21
- void this._agentManagerProvider().then((agentManager) => {
22
- next({ status: mapStatus(agentManager.agentStatus) });
23
- agentManager.agentStatusChanged.on(ctx, (newStatus) => {
24
- next({ status: mapStatus(newStatus) });
25
- });
26
- });
27
- });
28
- }
29
- }
30
-
31
- const mapStatus = (agentStatus: EdgeAgentStatus | undefined): QueryAgentStatusResponse.AgentStatus => {
32
- switch (agentStatus) {
33
- case EdgeAgentStatus.ACTIVE:
34
- return QueryAgentStatusResponse.AgentStatus.ACTIVE;
35
- case EdgeAgentStatus.INACTIVE:
36
- return QueryAgentStatusResponse.AgentStatus.INACTIVE;
37
- case EdgeAgentStatus.NOT_FOUND:
38
- return QueryAgentStatusResponse.AgentStatus.NOT_FOUND;
39
- case undefined:
40
- return QueryAgentStatusResponse.AgentStatus.UNKNOWN;
41
- }
42
- };
@@ -1,6 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- export * from './edge-agent-manager';
6
- export * from './edge-agent-service';
@@ -1,185 +0,0 @@
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 DEFAULT_REQUEST_RETRY_INTERVAL_MS = 3000;
36
- export const DEFAULT_REQUEST_RETRY_JITTER_MS = 500;
37
-
38
- export type EdgeInvitationConfig = {
39
- retryInterval?: number;
40
- retryJitter?: number;
41
- };
42
-
43
- export class EdgeInvitationHandler implements FlowLockHolder {
44
- private _flowLock: MutexGuard | undefined;
45
-
46
- private readonly _retryInterval: number;
47
- private readonly _retryJitter: number;
48
-
49
- constructor(
50
- config: EdgeInvitationConfig | undefined,
51
- private readonly _client: EdgeHttpClient | undefined,
52
- private readonly _callbacks: EdgeInvitationHandlerCallbacks,
53
- ) {
54
- this._retryInterval = config?.retryInterval ?? DEFAULT_REQUEST_RETRY_INTERVAL_MS;
55
- this._retryJitter = config?.retryJitter ?? DEFAULT_REQUEST_RETRY_JITTER_MS;
56
- }
57
-
58
- public handle(
59
- ctx: Context,
60
- guardedState: GuardedInvitationState,
61
- protocol: InvitationProtocol,
62
- deviceProfile?: DeviceProfileDocument,
63
- ) {
64
- if (!this._client) {
65
- log('edge disabled');
66
- return;
67
- }
68
-
69
- const invitation = guardedState.current;
70
- const spaceId = invitation.spaceId;
71
- const canBeHandledByEdge =
72
- invitation.authMethod !== Invitation.AuthMethod.SHARED_SECRET &&
73
- invitation.type === Invitation.Type.DELEGATED &&
74
- invitation.kind === Invitation.Kind.SPACE &&
75
- spaceId != null &&
76
- SpaceId.isValid(spaceId);
77
-
78
- if (!canBeHandledByEdge) {
79
- log('invitation could not be handled by edge', { invitation });
80
- return;
81
- }
82
-
83
- ctx.onDispose(() => {
84
- this._flowLock?.release();
85
- this._flowLock = undefined;
86
- });
87
-
88
- const tryHandleInvitation = async () => {
89
- const admissionRequest = await protocol.createAdmissionRequest(deviceProfile);
90
- if (admissionRequest.space) {
91
- try {
92
- await this._handleSpaceInvitationFlow(ctx, guardedState, admissionRequest.space, spaceId);
93
- } catch (error) {
94
- if (error instanceof EdgeCallFailedError) {
95
- log.info('join space with edge unsuccessful', {
96
- reason: error.message,
97
- retryable: error.isRetryable,
98
- after: error.retryAfterMs ?? this._calculateNextRetryMs(),
99
- });
100
- if (error.isRetryable) {
101
- scheduleTask(ctx, tryHandleInvitation, error.retryAfterMs ?? this._calculateNextRetryMs());
102
- }
103
- } else {
104
- log.info('failed to handle invitation with edge', { error });
105
- scheduleTask(ctx, tryHandleInvitation, this._calculateNextRetryMs());
106
- }
107
- }
108
- }
109
- };
110
- scheduleMicroTask(ctx, tryHandleInvitation);
111
- }
112
-
113
- private async _handleSpaceInvitationFlow(
114
- ctx: Context,
115
- guardedState: GuardedInvitationState,
116
- admissionRequest: SpaceAdmissionRequest,
117
- spaceId: SpaceId,
118
- ) {
119
- try {
120
- log('edge invitation flow');
121
- this._flowLock = await tryAcquireBeforeContextDisposed(ctx, guardedState.mutex);
122
- log('edge invitation flow acquired the lock');
123
-
124
- guardedState.set(this, Invitation.State.CONNECTING);
125
-
126
- const response = await this._joinSpaceByInvitation(guardedState, spaceId, {
127
- identityKey: admissionRequest.identityKey.toHex(),
128
- invitationId: guardedState.current.invitationId,
129
- });
130
-
131
- const admissionResponse = await this._mapToAdmissionResponse(response);
132
- await this._callbacks.onInvitationSuccess(admissionResponse, { space: admissionRequest });
133
- } catch (error) {
134
- guardedState.set(this, Invitation.State.ERROR);
135
- throw error;
136
- } finally {
137
- this._flowLock?.release();
138
- this._flowLock = undefined;
139
- }
140
- }
141
-
142
- private async _mapToAdmissionResponse(edgeResponse: JoinSpaceResponseBody): Promise<AdmissionResponse> {
143
- const credentialBytes = Buffer.from(edgeResponse.spaceMemberCredential, 'base64');
144
- const codec = schema.getCodecForType('dxos.halo.credentials.Credential');
145
- return {
146
- space: {
147
- credential: codec.decode(credentialBytes),
148
- },
149
- };
150
- }
151
-
152
- private async _joinSpaceByInvitation(
153
- guardedState: GuardedInvitationState,
154
- spaceId: SpaceId,
155
- request: JoinSpaceRequest,
156
- ): Promise<JoinSpaceResponseBody> {
157
- invariant(this._client);
158
- try {
159
- return await this._client.joinSpaceByInvitation(spaceId, request);
160
- } catch (error: any) {
161
- if (error instanceof EdgeAuthChallengeError) {
162
- const publicKey = guardedState.current.guestKeypair?.publicKey;
163
- const privateKey = guardedState.current.guestKeypair?.privateKey;
164
- if (!privateKey || !publicKey) {
165
- throw error;
166
- }
167
- const signature = sign(Buffer.from(error.challenge, 'base64'), privateKey);
168
- return this._client.joinSpaceByInvitation(spaceId, {
169
- ...request,
170
- signature: Buffer.from(signature).toString('base64'),
171
- });
172
- } else {
173
- throw error;
174
- }
175
- }
176
- }
177
-
178
- public hasFlowLock(): boolean {
179
- return this._flowLock != null;
180
- }
181
-
182
- private _calculateNextRetryMs() {
183
- return this._retryInterval + Math.random() * this._retryJitter;
184
- }
185
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * A utility object for serializing invitation state changes by multiple concurrent
3
- * invitation flow connections.
4
- */
5
- //
6
- // Copyright 2024 DXOS.org
7
- //
8
-
9
- import { Mutex, type PushStream } from '@dxos/async';
10
- import { type Context } from '@dxos/context';
11
- import { log } from '@dxos/log';
12
- import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
13
-
14
- import { stateToString } from './utils';
15
-
16
- export interface FlowLockHolder {
17
- hasFlowLock(): boolean;
18
- }
19
-
20
- export interface GuardedInvitationState {
21
- mutex: Mutex;
22
- current: Invitation;
23
-
24
- complete(newState: Partial<Invitation>): void;
25
- set(lockHolder: FlowLockHolder | null, newState: Invitation.State): boolean;
26
- error(lockHolder: FlowLockHolder | null, error: any): boolean;
27
- }
28
-
29
- export const createGuardedInvitationState = (
30
- ctx: Context,
31
- invitation: Invitation,
32
- stream: PushStream<Invitation>,
33
- ): GuardedInvitationState => {
34
- // the mutex guards invitation flow on host and guest side, making sure only one flow is currently active
35
- // deadlocks seem very unlikely because hosts don't initiate multiple connections
36
- // even if this somehow happens that there are 2 guests (A, B) and 2 hosts (1, 2) and:
37
- // A has lock for flow with 1, B has lock for flow with 2
38
- // 1 has lock for flow with B, 2 has lock for flow with A
39
- // there'll be a 10-second introduction timeout after which connection will be closed and deadlock broken
40
- const mutex = new Mutex();
41
- let lastActiveLockHolder: FlowLockHolder | null = null;
42
- let currentInvitation = { ...invitation };
43
- const isStateChangeAllowed = (lockHolder: FlowLockHolder | null) => {
44
- if (ctx.disposed || (lockHolder !== null && mutex.isLocked() && !lockHolder.hasFlowLock())) {
45
- return false;
46
- }
47
- // don't allow transitions from a terminal state unless a new extension acquired mutex
48
- // handles a case when error occurs (e.g. connection is closed) after we completed the flow
49
- // successfully or already reported another error
50
- return lockHolder == null || lastActiveLockHolder !== lockHolder || isNonTerminalState(currentInvitation.state);
51
- };
52
- return {
53
- mutex,
54
- get current() {
55
- return currentInvitation;
56
- },
57
- // disposing context prevents any further state updates
58
- complete: (newState: Partial<Invitation>) => {
59
- currentInvitation = { ...currentInvitation, ...newState };
60
- stream.next(currentInvitation);
61
- return ctx.dispose();
62
- },
63
- set: (lockHolder: FlowLockHolder | null, newState: Invitation.State): boolean => {
64
- if (isStateChangeAllowed(lockHolder)) {
65
- logStateUpdate(currentInvitation, lockHolder, newState);
66
- currentInvitation = { ...currentInvitation, state: newState };
67
- stream.next(currentInvitation);
68
- lastActiveLockHolder = lockHolder;
69
- return true;
70
- }
71
- return false;
72
- },
73
- error: (lockHolder: FlowLockHolder | null, error: any): boolean => {
74
- if (isStateChangeAllowed(lockHolder)) {
75
- logStateUpdate(currentInvitation, lockHolder, Invitation.State.ERROR);
76
- currentInvitation = { ...currentInvitation, state: Invitation.State.ERROR };
77
- stream.next(currentInvitation);
78
- stream.error(error);
79
- lastActiveLockHolder = lockHolder;
80
- return true;
81
- }
82
- return false;
83
- },
84
- };
85
- };
86
-
87
- const logStateUpdate = (invitation: Invitation, actor: any, newState: Invitation.State) => {
88
- if (isNonTerminalState(newState)) {
89
- log('invitation state update', {
90
- actor: actor?.constructor.name,
91
- newState: stateToString(newState),
92
- oldState: stateToString(invitation.state),
93
- });
94
- } else {
95
- log.info('invitation state update', {
96
- actor: actor?.constructor.name,
97
- newState: stateToString(newState),
98
- oldState: stateToString(invitation.state),
99
- });
100
- }
101
- };
102
-
103
- const isNonTerminalState = (currentState: Invitation.State): boolean => {
104
- return ![
105
- Invitation.State.SUCCESS,
106
- Invitation.State.ERROR,
107
- Invitation.State.CANCELLED,
108
- Invitation.State.TIMEOUT,
109
- Invitation.State.EXPIRED,
110
- ].includes(currentState);
111
- };