@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.
Files changed (70) hide show
  1. package/dist/lib/browser/{chunk-UEQIHAL2.mjs → chunk-IPWEAPT2.mjs} +728 -379
  2. package/dist/lib/browser/chunk-IPWEAPT2.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +5 -1
  4. package/dist/lib/browser/index.mjs.map +1 -1
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +5 -1
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-MA5EWTRH.cjs → chunk-DJIOUOAF.cjs} +769 -422
  9. package/dist/lib/node/chunk-DJIOUOAF.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +49 -45
  11. package/dist/lib/node/index.cjs.map +1 -1
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +12 -8
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/lib/node-esm/{chunk-AIBLDI4U.mjs → chunk-MMU5KC57.mjs} +728 -379
  16. package/dist/lib/node-esm/chunk-MMU5KC57.mjs.map +7 -0
  17. package/dist/lib/node-esm/index.mjs +5 -1
  18. package/dist/lib/node-esm/index.mjs.map +1 -1
  19. package/dist/lib/node-esm/meta.json +1 -1
  20. package/dist/lib/node-esm/testing/index.mjs +5 -1
  21. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  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/identity/identity-manager.d.ts +11 -7
  31. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  32. package/dist/types/src/packlets/identity/identity.d.ts +4 -2
  33. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  34. package/dist/types/src/packlets/invitations/device-invitation-protocol.d.ts.map +1 -1
  35. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +1 -1
  36. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  37. package/dist/types/src/packlets/services/service-context.d.ts +4 -1
  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.map +1 -1
  40. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +1 -0
  41. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  42. package/dist/types/src/packlets/spaces/data-space.d.ts +1 -0
  43. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  44. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +4 -0
  45. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  46. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  47. package/dist/types/src/version.d.ts +1 -1
  48. package/package.json +38 -38
  49. package/src/index.ts +1 -0
  50. package/src/packlets/agents/edge-agent-manager.ts +163 -0
  51. package/src/packlets/agents/edge-agent-service.ts +42 -0
  52. package/src/packlets/agents/index.ts +6 -0
  53. package/src/packlets/identity/identity-manager.test.ts +26 -10
  54. package/src/packlets/identity/identity-manager.ts +19 -19
  55. package/src/packlets/identity/identity.test.ts +2 -0
  56. package/src/packlets/identity/identity.ts +21 -12
  57. package/src/packlets/invitations/device-invitation-protocol.ts +5 -1
  58. package/src/packlets/invitations/edge-invitation-handler.ts +6 -5
  59. package/src/packlets/invitations/invitations-handler.ts +1 -2
  60. package/src/packlets/services/service-context.ts +75 -45
  61. package/src/packlets/services/service-host.ts +8 -0
  62. package/src/packlets/spaces/data-space-manager.ts +4 -2
  63. package/src/packlets/spaces/data-space.ts +3 -0
  64. package/src/packlets/spaces/edge-feed-replicator.test.ts +5 -6
  65. package/src/packlets/spaces/notarization-plugin.ts +27 -0
  66. package/src/packlets/testing/test-builder.ts +4 -0
  67. package/src/version.ts +1 -1
  68. package/dist/lib/browser/chunk-UEQIHAL2.mjs.map +0 -7
  69. package/dist/lib/node/chunk-MA5EWTRH.cjs.map +0 -7
  70. 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.548ca8d",
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.548ca8d",
51
- "@dxos/automerge": "0.6.13-main.548ca8d",
52
- "@dxos/client-protocol": "0.6.13-main.548ca8d",
53
- "@dxos/codec-protobuf": "0.6.13-main.548ca8d",
54
- "@dxos/config": "0.6.13-main.548ca8d",
55
- "@dxos/credentials": "0.6.13-main.548ca8d",
56
- "@dxos/crypto": "0.6.13-main.548ca8d",
57
- "@dxos/context": "0.6.13-main.548ca8d",
58
- "@dxos/debug": "0.6.13-main.548ca8d",
59
- "@dxos/echo-db": "0.6.13-main.548ca8d",
60
- "@dxos/echo-pipeline": "0.6.13-main.548ca8d",
61
- "@dxos/echo-schema": "0.6.13-main.548ca8d",
62
- "@dxos/edge-client": "0.6.13-main.548ca8d",
63
- "@dxos/feed-store": "0.6.13-main.548ca8d",
64
- "@dxos/echo-protocol": "0.6.13-main.548ca8d",
65
- "@dxos/indexing": "0.6.13-main.548ca8d",
66
- "@dxos/invariant": "0.6.13-main.548ca8d",
67
- "@dxos/keys": "0.6.13-main.548ca8d",
68
- "@dxos/keyring": "0.6.13-main.548ca8d",
69
- "@dxos/kv-store": "0.6.13-main.548ca8d",
70
- "@dxos/lock-file": "0.6.13-main.548ca8d",
71
- "@dxos/log": "0.6.13-main.548ca8d",
72
- "@dxos/messaging": "0.6.13-main.548ca8d",
73
- "@dxos/node-std": "0.6.13-main.548ca8d",
74
- "@dxos/network-manager": "0.6.13-main.548ca8d",
75
- "@dxos/protocols": "0.6.13-main.548ca8d",
76
- "@dxos/rpc": "0.6.13-main.548ca8d",
77
- "@dxos/random-access-storage": "0.6.13-main.548ca8d",
78
- "@dxos/teleport": "0.6.13-main.548ca8d",
79
- "@dxos/teleport-extension-gossip": "0.6.13-main.548ca8d",
80
- "@dxos/teleport-extension-object-sync": "0.6.13-main.548ca8d",
81
- "@dxos/timeframe": "0.6.13-main.548ca8d",
82
- "@dxos/tracing": "0.6.13-main.548ca8d",
83
- "@dxos/util": "0.6.13-main.548ca8d",
84
- "@dxos/websocket-rpc": "0.6.13-main.548ca8d"
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/test-utils": "0.6.13-main.548ca8d",
91
- "@dxos/signal": "0.6.13-main.548ca8d"
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
@@ -11,3 +11,4 @@ export * from './packlets/services';
11
11
  export * from './packlets/spaces';
12
12
  export * from './packlets/storage';
13
13
  export * from './packlets/worker';
14
+ export * from './packlets/agents';
@@ -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
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './edge-agent-manager';
6
+ export * from './edge-agent-service';
@@ -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: await identity1.getIdentityCredentialSigner().createCredential({
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
- // Identity2 is not yet ready at this point. Peer1 needs to admit peer2 device key and feed keys.
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
- * Accept an existing identity. Expects its device key to be authorized (now or later).
241
+ * Prepare an identity object as the first step of acceptIdentity flow.
250
242
  */
251
- async acceptIdentity(params: JoinIdentityParams) {
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
- await this._metadataStore.setIdentityRecord(identityRecord);
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: identityRecord.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
- ...params.deviceProfile,
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
- await signer.createCredential({
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 { ed25519Signature } from '@dxos/crypto';
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 = await ed25519Signature(privateKey, Buffer.from(error.challenge, 'base64'));
169
- request.signature = Buffer.from(signature).toString('base64');
170
- return this._client.joinSpaceByInvitation(spaceId, request);
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
  }