@dxos/client-services 0.6.13-main.548ca8d → 0.6.13-main.ed424a1
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-UEQIHAL2.mjs → chunk-IPWEAPT2.mjs} +728 -379
- package/dist/lib/browser/chunk-IPWEAPT2.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +5 -1
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +5 -1
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-MA5EWTRH.cjs → chunk-DJIOUOAF.cjs} +769 -422
- package/dist/lib/node/chunk-DJIOUOAF.cjs.map +7 -0
- package/dist/lib/node/index.cjs +49 -45
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +12 -8
- package/dist/lib/node/testing/index.cjs.map +3 -3
- package/dist/lib/node-esm/{chunk-AIBLDI4U.mjs → chunk-MMU5KC57.mjs} +728 -379
- package/dist/lib/node-esm/chunk-MMU5KC57.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +5 -1
- package/dist/lib/node-esm/index.mjs.map +1 -1
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +5 -1
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts +35 -0
- package/dist/types/src/packlets/agents/edge-agent-manager.d.ts.map +1 -0
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts +10 -0
- package/dist/types/src/packlets/agents/edge-agent-service.d.ts.map +1 -0
- package/dist/types/src/packlets/agents/index.d.ts +3 -0
- package/dist/types/src/packlets/agents/index.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/identity-manager.d.ts +11 -7
- package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/identity/identity.d.ts +4 -2
- 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/edge-invitation-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts +4 -1
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -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 +1 -0
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts +1 -0
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +4 -0
- package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/package.json +38 -38
- package/src/index.ts +1 -0
- package/src/packlets/agents/edge-agent-manager.ts +163 -0
- package/src/packlets/agents/edge-agent-service.ts +42 -0
- package/src/packlets/agents/index.ts +6 -0
- package/src/packlets/identity/identity-manager.test.ts +26 -10
- package/src/packlets/identity/identity-manager.ts +19 -19
- package/src/packlets/identity/identity.test.ts +2 -0
- package/src/packlets/identity/identity.ts +21 -12
- package/src/packlets/invitations/device-invitation-protocol.ts +5 -1
- package/src/packlets/invitations/edge-invitation-handler.ts +6 -5
- package/src/packlets/invitations/invitations-handler.ts +1 -2
- package/src/packlets/services/service-context.ts +75 -45
- package/src/packlets/services/service-host.ts +8 -0
- package/src/packlets/spaces/data-space-manager.ts +4 -2
- package/src/packlets/spaces/data-space.ts +3 -0
- package/src/packlets/spaces/edge-feed-replicator.test.ts +5 -6
- package/src/packlets/spaces/notarization-plugin.ts +27 -0
- package/src/packlets/testing/test-builder.ts +4 -0
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-UEQIHAL2.mjs.map +0 -7
- package/dist/lib/node/chunk-MA5EWTRH.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-AIBLDI4U.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/client-services",
|
|
3
|
-
"version": "0.6.13-main.
|
|
3
|
+
"version": "0.6.13-main.ed424a1",
|
|
4
4
|
"description": "DXOS client services implementation",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -47,48 +47,48 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"cbor-x": "^1.5.4",
|
|
49
49
|
"platform": "^1.3.6",
|
|
50
|
-
"@dxos/async": "0.6.13-main.
|
|
51
|
-
"@dxos/automerge": "0.6.13-main.
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/debug": "0.6.13-main.
|
|
59
|
-
"@dxos/echo-
|
|
60
|
-
"@dxos/echo-
|
|
61
|
-
"@dxos/echo-
|
|
62
|
-
"@dxos/
|
|
63
|
-
"@dxos/feed-store": "0.6.13-main.
|
|
64
|
-
"@dxos/
|
|
65
|
-
"@dxos/indexing": "0.6.13-main.
|
|
66
|
-
"@dxos/invariant": "0.6.13-main.
|
|
67
|
-
"@dxos/
|
|
68
|
-
"@dxos/
|
|
69
|
-
"@dxos/
|
|
70
|
-
"@dxos/
|
|
71
|
-
"@dxos/
|
|
72
|
-
"@dxos/messaging": "0.6.13-main.
|
|
73
|
-
"@dxos/
|
|
74
|
-
"@dxos/
|
|
75
|
-
"@dxos/
|
|
76
|
-
"@dxos/
|
|
77
|
-
"@dxos/random-access-storage": "0.6.13-main.
|
|
78
|
-
"@dxos/teleport": "0.6.13-main.
|
|
79
|
-
"@dxos/teleport-extension-gossip": "0.6.13-main.
|
|
80
|
-
"@dxos/teleport-extension-object-sync": "0.6.13-main.
|
|
81
|
-
"@dxos/timeframe": "0.6.13-main.
|
|
82
|
-
"@dxos/
|
|
83
|
-
"@dxos/util": "0.6.13-main.
|
|
84
|
-
"@dxos/
|
|
50
|
+
"@dxos/async": "0.6.13-main.ed424a1",
|
|
51
|
+
"@dxos/automerge": "0.6.13-main.ed424a1",
|
|
52
|
+
"@dxos/codec-protobuf": "0.6.13-main.ed424a1",
|
|
53
|
+
"@dxos/config": "0.6.13-main.ed424a1",
|
|
54
|
+
"@dxos/context": "0.6.13-main.ed424a1",
|
|
55
|
+
"@dxos/client-protocol": "0.6.13-main.ed424a1",
|
|
56
|
+
"@dxos/credentials": "0.6.13-main.ed424a1",
|
|
57
|
+
"@dxos/crypto": "0.6.13-main.ed424a1",
|
|
58
|
+
"@dxos/debug": "0.6.13-main.ed424a1",
|
|
59
|
+
"@dxos/echo-pipeline": "0.6.13-main.ed424a1",
|
|
60
|
+
"@dxos/echo-protocol": "0.6.13-main.ed424a1",
|
|
61
|
+
"@dxos/echo-db": "0.6.13-main.ed424a1",
|
|
62
|
+
"@dxos/echo-schema": "0.6.13-main.ed424a1",
|
|
63
|
+
"@dxos/feed-store": "0.6.13-main.ed424a1",
|
|
64
|
+
"@dxos/edge-client": "0.6.13-main.ed424a1",
|
|
65
|
+
"@dxos/indexing": "0.6.13-main.ed424a1",
|
|
66
|
+
"@dxos/invariant": "0.6.13-main.ed424a1",
|
|
67
|
+
"@dxos/keyring": "0.6.13-main.ed424a1",
|
|
68
|
+
"@dxos/kv-store": "0.6.13-main.ed424a1",
|
|
69
|
+
"@dxos/keys": "0.6.13-main.ed424a1",
|
|
70
|
+
"@dxos/log": "0.6.13-main.ed424a1",
|
|
71
|
+
"@dxos/lock-file": "0.6.13-main.ed424a1",
|
|
72
|
+
"@dxos/messaging": "0.6.13-main.ed424a1",
|
|
73
|
+
"@dxos/network-manager": "0.6.13-main.ed424a1",
|
|
74
|
+
"@dxos/protocols": "0.6.13-main.ed424a1",
|
|
75
|
+
"@dxos/rpc": "0.6.13-main.ed424a1",
|
|
76
|
+
"@dxos/node-std": "0.6.13-main.ed424a1",
|
|
77
|
+
"@dxos/random-access-storage": "0.6.13-main.ed424a1",
|
|
78
|
+
"@dxos/teleport": "0.6.13-main.ed424a1",
|
|
79
|
+
"@dxos/teleport-extension-gossip": "0.6.13-main.ed424a1",
|
|
80
|
+
"@dxos/teleport-extension-object-sync": "0.6.13-main.ed424a1",
|
|
81
|
+
"@dxos/timeframe": "0.6.13-main.ed424a1",
|
|
82
|
+
"@dxos/websocket-rpc": "0.6.13-main.ed424a1",
|
|
83
|
+
"@dxos/util": "0.6.13-main.ed424a1",
|
|
84
|
+
"@dxos/tracing": "0.6.13-main.ed424a1"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@types/platform": "^1.3.4",
|
|
88
88
|
"@types/readable-stream": "^2.3.9",
|
|
89
89
|
"get-port-please": "^3.1.1",
|
|
90
|
-
"@dxos/
|
|
91
|
-
"@dxos/
|
|
90
|
+
"@dxos/signal": "0.6.13-main.ed424a1",
|
|
91
|
+
"@dxos/test-utils": "0.6.13-main.ed424a1"
|
|
92
92
|
},
|
|
93
93
|
"publishConfig": {
|
|
94
94
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
};
|
|
@@ -53,6 +53,7 @@ describe('identity/identity-manager', () => {
|
|
|
53
53
|
const identityManager = new IdentityManager({ metadataStore, keyring, feedStore, spaceManager });
|
|
54
54
|
|
|
55
55
|
return {
|
|
56
|
+
networkManager,
|
|
56
57
|
metadataStore,
|
|
57
58
|
identityManager,
|
|
58
59
|
feedStore,
|
|
@@ -107,6 +108,11 @@ describe('identity/identity-manager', () => {
|
|
|
107
108
|
|
|
108
109
|
const peer1 = await setupPeer({ signalContext });
|
|
109
110
|
const identity1 = await peer1.identityManager.createIdentity();
|
|
111
|
+
peer1.networkManager.setPeerInfo({
|
|
112
|
+
peerKey: identity1.deviceKey.toHex(),
|
|
113
|
+
identityKey: identity1.identityKey.toHex(),
|
|
114
|
+
});
|
|
115
|
+
await identity1.joinNetwork();
|
|
110
116
|
|
|
111
117
|
const peer2 = await setupPeer({ signalContext });
|
|
112
118
|
|
|
@@ -114,28 +120,38 @@ describe('identity/identity-manager', () => {
|
|
|
114
120
|
const controlFeedKey = await peer2.keyring.createKey();
|
|
115
121
|
const dataFeedKey = await peer2.keyring.createKey();
|
|
116
122
|
|
|
123
|
+
const credential = await identity1.getIdentityCredentialSigner().createCredential({
|
|
124
|
+
subject: deviceKey,
|
|
125
|
+
assertion: {
|
|
126
|
+
'@type': 'dxos.halo.credentials.AuthorizedDevice',
|
|
127
|
+
identityKey: identity1.identityKey,
|
|
128
|
+
deviceKey,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
117
132
|
await identity1.controlPipeline.writer.write({
|
|
118
133
|
credential: {
|
|
119
|
-
credential
|
|
120
|
-
subject: deviceKey,
|
|
121
|
-
assertion: {
|
|
122
|
-
'@type': 'dxos.halo.credentials.AuthorizedDevice',
|
|
123
|
-
identityKey: identity1.identityKey,
|
|
124
|
-
deviceKey,
|
|
125
|
-
},
|
|
126
|
-
}),
|
|
134
|
+
credential,
|
|
127
135
|
},
|
|
128
136
|
});
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
const identity2 = await peer2.identityManager.acceptIdentity({
|
|
138
|
+
const { identity: identity2, identityRecord } = await peer2.identityManager.prepareIdentity({
|
|
132
139
|
identityKey: identity1.identityKey,
|
|
133
140
|
deviceKey,
|
|
134
141
|
haloSpaceKey: identity1.haloSpaceKey,
|
|
135
142
|
haloGenesisFeedKey: identity1.haloGenesisFeedKey,
|
|
136
143
|
controlFeedKey,
|
|
137
144
|
dataFeedKey,
|
|
145
|
+
authorizedDeviceCredential: credential,
|
|
146
|
+
});
|
|
147
|
+
peer2.networkManager.setPeerInfo({
|
|
148
|
+
peerKey: identity2.deviceKey.toHex(),
|
|
149
|
+
identityKey: identity2.identityKey.toHex(),
|
|
138
150
|
});
|
|
151
|
+
await identity2.joinNetwork();
|
|
152
|
+
|
|
153
|
+
// Identity2 is not yet ready at this point. Peer1 needs to admit peer2 device key and feed keys.
|
|
154
|
+
await peer2.identityManager.acceptIdentity(identity2, identityRecord);
|
|
139
155
|
|
|
140
156
|
// TODO(dmaretskyi): We'd also need to admit device2's feeds otherwise messages from them won't be processed by the pipeline.
|
|
141
157
|
// This would mean that peer2 has replicated it's device credential chain from peer1 and is ready to issue credentials.
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type DeviceProfileDocument,
|
|
24
24
|
DeviceType,
|
|
25
25
|
type ProfileDocument,
|
|
26
|
+
type Credential,
|
|
26
27
|
} from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
27
28
|
import { Gossip, Presence } from '@dxos/teleport-extension-gossip';
|
|
28
29
|
import { Timeframe } from '@dxos/timeframe';
|
|
@@ -49,6 +50,7 @@ export type JoinIdentityParams = {
|
|
|
49
50
|
haloGenesisFeedKey: PublicKey;
|
|
50
51
|
controlFeedKey: PublicKey;
|
|
51
52
|
dataFeedKey: PublicKey;
|
|
53
|
+
authorizedDeviceCredential: Credential;
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Latest known timeframe for the control pipeline.
|
|
@@ -65,10 +67,6 @@ export type CreateIdentityOptions = {
|
|
|
65
67
|
deviceProfile?: DeviceProfileDocument;
|
|
66
68
|
};
|
|
67
69
|
|
|
68
|
-
export type IdentityManagerCallbacks = {
|
|
69
|
-
onIdentityConstruction?: (identity: Identity) => void;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
70
|
export type IdentityManagerParams = {
|
|
73
71
|
metadataStore: MetadataStore;
|
|
74
72
|
keyring: Keyring;
|
|
@@ -78,7 +76,6 @@ export type IdentityManagerParams = {
|
|
|
78
76
|
edgeFeatures?: Runtime.Client.EdgeFeatures;
|
|
79
77
|
devicePresenceAnnounceInterval?: number;
|
|
80
78
|
devicePresenceOfflineTimeout?: number;
|
|
81
|
-
callbacks?: IdentityManagerCallbacks;
|
|
82
79
|
};
|
|
83
80
|
|
|
84
81
|
// TODO(dmaretskyi): Rename: represents the peer's state machine.
|
|
@@ -94,7 +91,6 @@ export class IdentityManager {
|
|
|
94
91
|
private readonly _devicePresenceOfflineTimeout: number;
|
|
95
92
|
private readonly _edgeConnection: EdgeConnection | undefined;
|
|
96
93
|
private readonly _edgeFeatures: Runtime.Client.EdgeFeatures | undefined;
|
|
97
|
-
private readonly _callbacks: IdentityManagerCallbacks | undefined;
|
|
98
94
|
|
|
99
95
|
private _identity?: Identity;
|
|
100
96
|
|
|
@@ -108,7 +104,6 @@ export class IdentityManager {
|
|
|
108
104
|
this._edgeFeatures = params.edgeFeatures;
|
|
109
105
|
this._devicePresenceAnnounceInterval = params.devicePresenceAnnounceInterval ?? DEVICE_PRESENCE_ANNOUNCE_INTERVAL;
|
|
110
106
|
this._devicePresenceOfflineTimeout = params.devicePresenceOfflineTimeout ?? DEVICE_PRESENCE_OFFLINE_TIMEOUT;
|
|
111
|
-
this._callbacks = params.callbacks;
|
|
112
107
|
}
|
|
113
108
|
|
|
114
109
|
get identity() {
|
|
@@ -198,10 +193,6 @@ export class IdentityManager {
|
|
|
198
193
|
}
|
|
199
194
|
}
|
|
200
195
|
|
|
201
|
-
// TODO(burdon): ???
|
|
202
|
-
// await this._keyring.deleteKey(identityRecord.identity_key);
|
|
203
|
-
// await this._keyring.deleteKey(identityRecord.halo_space.space_key);
|
|
204
|
-
|
|
205
196
|
await this._metadataStore.setIdentityRecord(identityRecord);
|
|
206
197
|
this._identity = identity;
|
|
207
198
|
await this._identity.ready();
|
|
@@ -216,6 +207,7 @@ export class IdentityManager {
|
|
|
216
207
|
deviceKey: identity.deviceKey,
|
|
217
208
|
profile: identity.profileDocument,
|
|
218
209
|
});
|
|
210
|
+
|
|
219
211
|
return identity;
|
|
220
212
|
}
|
|
221
213
|
|
|
@@ -246,9 +238,9 @@ export class IdentityManager {
|
|
|
246
238
|
}
|
|
247
239
|
|
|
248
240
|
/**
|
|
249
|
-
*
|
|
241
|
+
* Prepare an identity object as the first step of acceptIdentity flow.
|
|
250
242
|
*/
|
|
251
|
-
async
|
|
243
|
+
async prepareIdentity(params: JoinIdentityParams) {
|
|
252
244
|
log('accepting identity', { params });
|
|
253
245
|
invariant(!this._identity, 'Identity already exists.');
|
|
254
246
|
|
|
@@ -263,24 +255,33 @@ export class IdentityManager {
|
|
|
263
255
|
controlTimeframe: params.controlTimeframe,
|
|
264
256
|
},
|
|
265
257
|
};
|
|
266
|
-
|
|
267
258
|
const identity = await this._constructIdentity(identityRecord);
|
|
268
259
|
await identity.open(new Context());
|
|
260
|
+
return { identity, identityRecord };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Accept an existing identity. Expects its device key to be authorized (now or later).
|
|
265
|
+
*/
|
|
266
|
+
public async acceptIdentity(identity: Identity, identityRecord: IdentityRecord, profile?: DeviceProfileDocument) {
|
|
269
267
|
this._identity = identity;
|
|
270
|
-
|
|
268
|
+
|
|
269
|
+
// Identity becomes ready after device chain is replicated. Wait for it before storing the record.
|
|
271
270
|
await this._identity.ready();
|
|
271
|
+
await this._metadataStore.setIdentityRecord(identityRecord);
|
|
272
|
+
|
|
272
273
|
log.trace('dxos.halo.identity', {
|
|
273
|
-
identityKey:
|
|
274
|
+
identityKey: this._identity!.identityKey,
|
|
274
275
|
displayName: this._identity.profileDocument?.displayName,
|
|
275
276
|
});
|
|
276
277
|
|
|
277
278
|
await this.updateDeviceProfile({
|
|
278
279
|
...this.createDefaultDeviceProfile(),
|
|
279
|
-
...
|
|
280
|
+
...profile,
|
|
280
281
|
});
|
|
281
282
|
this.stateUpdate.emit();
|
|
283
|
+
|
|
282
284
|
log('accepted identity', { identityKey: identity.identityKey, deviceKey: identity.deviceKey });
|
|
283
|
-
return identity;
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
/**
|
|
@@ -378,7 +379,6 @@ export class IdentityManager {
|
|
|
378
379
|
edgeFeatures: this._edgeFeatures,
|
|
379
380
|
});
|
|
380
381
|
log('done', { identityKey: identityRecord.identityKey });
|
|
381
|
-
this._callbacks?.onIdentityConstruction?.(identity);
|
|
382
382
|
|
|
383
383
|
// TODO(mykola): Set new timeframe on a write to a feed.
|
|
384
384
|
if (identityRecord.haloSpace.controlTimeframe) {
|
|
@@ -173,6 +173,7 @@ describe('identity/identity', () => {
|
|
|
173
173
|
networkManager: new SwarmNetworkManager({
|
|
174
174
|
signalManager: new MemorySignalManager(args?.signalContext ?? new MemorySignalManagerContext()),
|
|
175
175
|
transportFactory: MemoryTransportFactory,
|
|
176
|
+
peerInfo: { identityKey: identityKey.toHex(), peerKey: deviceKey.toHex() },
|
|
176
177
|
}),
|
|
177
178
|
});
|
|
178
179
|
|
|
@@ -202,6 +203,7 @@ describe('identity/identity', () => {
|
|
|
202
203
|
});
|
|
203
204
|
|
|
204
205
|
await identity.open(new Context());
|
|
206
|
+
await identity.joinNetwork();
|
|
205
207
|
onTestFinished(() => identity.close(new Context()));
|
|
206
208
|
return { identity, identityKey, keyring, deviceKey, controlFeed, spaceKey, dataFeed };
|
|
207
209
|
};
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
AdmittedFeed,
|
|
26
26
|
type DeviceProfileDocument,
|
|
27
27
|
type ProfileDocument,
|
|
28
|
+
type Credential,
|
|
28
29
|
} from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
29
30
|
import { type DeviceAdmissionRequest } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
30
31
|
import { type Presence } from '@dxos/teleport-extension-gossip';
|
|
@@ -117,13 +118,14 @@ export class Identity {
|
|
|
117
118
|
await this.space.spaceState.addCredentialProcessor(this._deviceStateMachine);
|
|
118
119
|
await this.space.spaceState.addCredentialProcessor(this._profileStateMachine);
|
|
119
120
|
await this.space.spaceState.addCredentialProcessor(this._defaultSpaceStateMachine);
|
|
121
|
+
await this.space.open(ctx);
|
|
122
|
+
}
|
|
120
123
|
|
|
124
|
+
public async joinNetwork() {
|
|
121
125
|
if (this._edgeFeedReplicator) {
|
|
122
126
|
this.space.protocol.feedAdded.append(this._onFeedAdded);
|
|
123
127
|
}
|
|
124
|
-
|
|
125
|
-
await this.space.open(ctx);
|
|
126
|
-
|
|
128
|
+
await this.space.startProtocol();
|
|
127
129
|
await this._edgeFeedReplicator?.open();
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -161,6 +163,10 @@ export class Identity {
|
|
|
161
163
|
return this.space.controlPipeline;
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
get haloSpaceId() {
|
|
167
|
+
return this.space.id;
|
|
168
|
+
}
|
|
169
|
+
|
|
164
170
|
get haloSpaceKey() {
|
|
165
171
|
return this.space.key;
|
|
166
172
|
}
|
|
@@ -210,7 +216,7 @@ export class Identity {
|
|
|
210
216
|
await this.controlPipeline.state.waitUntilTimeframe(new Timeframe([[receipt.feedKey, receipt.seq]]));
|
|
211
217
|
}
|
|
212
218
|
|
|
213
|
-
async admitDevice({ deviceKey, controlFeedKey, dataFeedKey }: DeviceAdmissionRequest) {
|
|
219
|
+
async admitDevice({ deviceKey, controlFeedKey, dataFeedKey }: DeviceAdmissionRequest): Promise<Credential> {
|
|
214
220
|
log('Admitting device:', {
|
|
215
221
|
identityKey: this.identityKey,
|
|
216
222
|
hostDevice: this.deviceKey,
|
|
@@ -219,17 +225,18 @@ export class Identity {
|
|
|
219
225
|
dataFeedKey,
|
|
220
226
|
});
|
|
221
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
|
+
});
|
|
222
236
|
await writeMessages(
|
|
223
237
|
this.controlPipeline.writer,
|
|
224
238
|
[
|
|
225
|
-
|
|
226
|
-
subject: deviceKey,
|
|
227
|
-
assertion: {
|
|
228
|
-
'@type': 'dxos.halo.credentials.AuthorizedDevice',
|
|
229
|
-
identityKey: this.identityKey,
|
|
230
|
-
deviceKey,
|
|
231
|
-
},
|
|
232
|
-
}),
|
|
239
|
+
deviceCredential,
|
|
233
240
|
await signer.createCredential({
|
|
234
241
|
subject: controlFeedKey,
|
|
235
242
|
assertion: {
|
|
@@ -252,6 +259,8 @@ export class Identity {
|
|
|
252
259
|
}),
|
|
253
260
|
].map((credential): FeedMessage.Payload => ({ credential: { credential } })),
|
|
254
261
|
);
|
|
262
|
+
|
|
263
|
+
return deviceCredential;
|
|
255
264
|
}
|
|
256
265
|
|
|
257
266
|
private _onFeedAdded = async (feed: FeedWrapper<any>) => {
|
|
@@ -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';
|
|
@@ -49,7 +50,8 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
|
|
|
49
50
|
async admit(_: Invitation, request: AdmissionRequest): Promise<AdmissionResponse> {
|
|
50
51
|
invariant(request.device);
|
|
51
52
|
const identity = this._getIdentity();
|
|
52
|
-
await identity.admitDevice(request.device);
|
|
53
|
+
const credential = await identity.admitDevice(request.device);
|
|
54
|
+
invariant(getCredentialAssertion(credential)['@type'] === 'dxos.halo.credentials.AuthorizedDevice');
|
|
53
55
|
|
|
54
56
|
return {
|
|
55
57
|
device: {
|
|
@@ -57,6 +59,7 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
|
|
|
57
59
|
haloSpaceKey: identity.haloSpaceKey,
|
|
58
60
|
genesisFeedKey: identity.haloGenesisFeedKey,
|
|
59
61
|
controlTimeframe: identity.controlPipeline.state.timeframe,
|
|
62
|
+
credential,
|
|
60
63
|
},
|
|
61
64
|
};
|
|
62
65
|
}
|
|
@@ -109,6 +112,7 @@ export class DeviceInvitationProtocol implements InvitationProtocol {
|
|
|
109
112
|
dataFeedKey,
|
|
110
113
|
controlTimeframe,
|
|
111
114
|
deviceProfile: profile,
|
|
115
|
+
authorizedDeviceCredential: response.device.credential,
|
|
112
116
|
});
|
|
113
117
|
|
|
114
118
|
return { identityKey };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { type MutexGuard, scheduleMicroTask, scheduleTask } from '@dxos/async';
|
|
6
6
|
import { type Context } from '@dxos/context';
|
|
7
|
-
import {
|
|
7
|
+
import { sign } from '@dxos/crypto';
|
|
8
8
|
import { type EdgeHttpClient } from '@dxos/edge-client';
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
import { SpaceId } from '@dxos/keys';
|
|
@@ -159,15 +159,16 @@ export class EdgeInvitationHandler implements FlowLockHolder {
|
|
|
159
159
|
return await this._client.joinSpaceByInvitation(spaceId, request);
|
|
160
160
|
} catch (error: any) {
|
|
161
161
|
if (error instanceof EdgeAuthChallengeError) {
|
|
162
|
-
guardedState.set(this, Invitation.State.AUTHENTICATING);
|
|
163
162
|
const publicKey = guardedState.current.guestKeypair?.publicKey;
|
|
164
163
|
const privateKey = guardedState.current.guestKeypair?.privateKey;
|
|
165
164
|
if (!privateKey || !publicKey) {
|
|
166
165
|
throw error;
|
|
167
166
|
}
|
|
168
|
-
const signature =
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
});
|
|
171
172
|
} else {
|
|
172
173
|
throw error;
|
|
173
174
|
}
|