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