@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.
- package/dist/lib/browser/{chunk-IPWEAPT2.mjs → chunk-CRXXOI45.mjs} +5186 -6222
- package/dist/lib/browser/chunk-CRXXOI45.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +3 -7
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +7 -12
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-DJIOUOAF.cjs → chunk-PZ3JJJ3K.cjs} +5137 -6167
- package/dist/lib/node/chunk-PZ3JJJ3K.cjs.map +7 -0
- package/dist/lib/node/index.cjs +46 -50
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +13 -18
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/authenticator.test.d.ts +2 -0
- package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity-manager.d.ts +9 -25
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +3 -12
- package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +1 -2
- package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +1 -2
- package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/services/automerge-host.test.d.ts +2 -0
- package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +1 -0
- package/dist/types/src/packlets/services/service-context.d.ts +9 -12
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +0 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +3 -7
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +3 -5
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +0 -3
- package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +6 -35
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +2 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/package.json +39 -43
- package/src/index.ts +0 -1
- package/src/packlets/devices/devices-service.test.ts +5 -4
- package/src/packlets/diagnostics/diagnostics-broadcast.ts +0 -1
- package/src/packlets/identity/{authenticator.node.test.ts → authenticator.test.ts} +3 -2
- package/src/packlets/identity/authenticator.ts +2 -5
- package/src/packlets/identity/contacts-service.ts +1 -1
- package/src/packlets/identity/identity-manager.test.ts +16 -31
- package/src/packlets/identity/identity-manager.ts +31 -47
- package/src/packlets/identity/identity-service.test.ts +8 -4
- package/src/packlets/identity/identity.test.ts +239 -130
- package/src/packlets/identity/identity.ts +17 -60
- package/src/packlets/invitations/device-invitation-protocol.test.ts +4 -7
- package/src/packlets/invitations/device-invitation-protocol.ts +1 -5
- package/src/packlets/invitations/invitation-guest-extenstion.ts +4 -8
- package/src/packlets/invitations/invitation-host-extension.ts +7 -8
- package/src/packlets/invitations/invitations-handler.test.ts +9 -16
- package/src/packlets/invitations/invitations-handler.ts +93 -23
- package/src/packlets/invitations/space-invitation-protocol.test.ts +3 -4
- package/src/packlets/invitations/space-invitation-protocol.ts +0 -4
- package/src/packlets/logging/logging.test.ts +2 -1
- package/src/packlets/network/network-service.test.ts +3 -2
- package/src/packlets/services/automerge-host.test.ts +60 -0
- package/src/packlets/services/service-context.test.ts +1 -3
- package/src/packlets/services/service-context.ts +37 -104
- package/src/packlets/services/service-host.test.ts +12 -8
- package/src/packlets/services/service-host.ts +6 -16
- package/src/packlets/services/service-registry.test.ts +2 -1
- package/src/packlets/spaces/data-space-manager.test.ts +2 -2
- package/src/packlets/spaces/data-space-manager.ts +7 -44
- package/src/packlets/spaces/data-space.ts +6 -37
- package/src/packlets/spaces/edge-feed-replicator.ts +22 -80
- package/src/packlets/spaces/epoch-migrations.ts +2 -2
- package/src/packlets/spaces/notarization-plugin.test.ts +7 -10
- package/src/packlets/spaces/notarization-plugin.ts +29 -196
- package/src/packlets/spaces/spaces-service.test.ts +9 -5
- package/src/packlets/spaces/spaces-service.ts +1 -6
- package/src/packlets/storage/storage.ts +1 -0
- package/src/packlets/system/system-service.test.ts +2 -1
- package/src/packlets/testing/test-builder.ts +4 -7
- package/src/packlets/worker/worker-runtime.ts +2 -2
- package/src/version.ts +5 -1
- package/dist/lib/browser/chunk-IPWEAPT2.mjs.map +0 -7
- package/dist/lib/node/chunk-DJIOUOAF.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-MMU5KC57.mjs +0 -8752
- package/dist/lib/node-esm/chunk-MMU5KC57.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -420
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -424
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +0 -35
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +0 -1
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts +0 -10
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +0 -1
- package/dist/types/src/packlets/agents/index.d.ts +0 -3
- package/dist/types/src/packlets/agents/index.d.ts.map +0 -1
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +0 -2
- package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +0 -30
- package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +0 -1
- package/dist/types/src/packlets/invitations/invitation-state.d.ts +0 -19
- package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +0 -1
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +0 -2
- package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +0 -1
- package/dist/types/src/testing/setup.d.ts +0 -3
- package/dist/types/src/testing/setup.d.ts.map +0 -1
- package/src/packlets/agents/edge-agent-manager.ts +0 -163
- package/src/packlets/agents/edge-agent-service.ts +0 -42
- package/src/packlets/agents/index.ts +0 -6
- package/src/packlets/invitations/edge-invitation-handler.ts +0 -185
- package/src/packlets/invitations/invitation-state.ts +0 -111
- package/src/packlets/spaces/edge-feed-replicator.test.ts +0 -252
- 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,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
|
-
};
|