@dxos/client-services 0.4.10-main.d9dc7ae → 0.4.10-main.e2d2318
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-WYFJLESV.mjs → chunk-HAFART26.mjs} +830 -622
- package/dist/lib/browser/chunk-HAFART26.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +11 -3
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +8 -4
- package/dist/lib/browser/packlets/testing/index.mjs.map +3 -3
- package/dist/lib/node/{chunk-SF4YXR7W.cjs → chunk-5YDVC6TU.cjs} +849 -723
- package/dist/lib/node/chunk-5YDVC6TU.cjs.map +7 -0
- package/dist/lib/node/index.cjs +47 -39
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +13 -9
- package/dist/lib/node/packlets/testing/index.cjs.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/diagnostics/browser-diagnostics-broadcast.d.ts +5 -0
- package/dist/types/src/packlets/diagnostics/browser-diagnostics-broadcast.d.ts.map +1 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts +5 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts +15 -0
- package/dist/types/src/packlets/diagnostics/diagnostics-collector.d.ts.map +1 -0
- package/dist/types/src/packlets/{services → diagnostics}/diagnostics.d.ts +1 -1
- package/dist/types/src/packlets/diagnostics/diagnostics.d.ts.map +1 -0
- package/dist/types/src/packlets/diagnostics/index.d.ts +4 -0
- package/dist/types/src/packlets/diagnostics/index.d.ts.map +1 -0
- package/dist/types/src/packlets/invitations/invitation-extension.d.ts +1 -0
- package/dist/types/src/packlets/invitations/invitation-extension.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts +4 -2
- 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/services/index.d.ts +1 -1
- package/dist/types/src/packlets/services/index.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts +4 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/services/util.d.ts +1 -0
- package/dist/types/src/packlets/services/util.d.ts.map +1 -1
- package/dist/types/src/packlets/system/system-service.d.ts +1 -1
- package/dist/types/src/packlets/system/system-service.d.ts.map +1 -1
- package/dist/types/src/packlets/testing/test-builder.d.ts +4 -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 +35 -34
- package/src/index.ts +1 -0
- package/src/packlets/diagnostics/browser-diagnostics-broadcast.ts +94 -0
- package/src/packlets/diagnostics/diagnostics-broadcast.ts +20 -0
- package/src/packlets/diagnostics/diagnostics-collector.ts +65 -0
- package/src/packlets/{services → diagnostics}/diagnostics.ts +2 -2
- package/src/packlets/diagnostics/index.ts +7 -0
- package/src/packlets/invitations/invitation-extension.ts +28 -1
- package/src/packlets/invitations/invitations-handler.ts +73 -32
- package/src/packlets/invitations/invitations-service.ts +5 -5
- package/src/packlets/services/automerge-host.test.ts +9 -3
- package/src/packlets/services/index.ts +1 -1
- package/src/packlets/services/service-context.test.ts +4 -1
- package/src/packlets/services/service-context.ts +2 -0
- package/src/packlets/services/service-host.ts +23 -4
- package/src/packlets/services/util.ts +2 -0
- package/src/packlets/spaces/data-space-manager.test.ts +4 -4
- package/src/packlets/storage/level.ts +1 -1
- package/src/packlets/system/system-service.ts +1 -1
- package/src/packlets/testing/test-builder.ts +20 -4
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-WYFJLESV.mjs.map +0 -7
- package/dist/lib/node/chunk-SF4YXR7W.cjs.map +0 -7
- package/dist/types/src/packlets/services/diagnostics.d.ts.map +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Trigger } from '@dxos/async';
|
|
6
|
+
import { log } from '@dxos/log';
|
|
7
|
+
import { type SystemService } from '@dxos/protocols/proto/dxos/client/services';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type CollectDiagnosticsBroadcastSender,
|
|
11
|
+
type CollectDiagnosticsBroadcastHandler,
|
|
12
|
+
} from './diagnostics-collector';
|
|
13
|
+
|
|
14
|
+
const CHANNEL_NAME = 'dxos.diagnostics.broadcast';
|
|
15
|
+
|
|
16
|
+
enum MessageType {
|
|
17
|
+
PROBE = 'probe',
|
|
18
|
+
PROBE_ACK = 'probe-ack',
|
|
19
|
+
REQUEST_DIAGNOSTICS = 'request-diagnostics',
|
|
20
|
+
RECEIVE_DIAGNOSTICS = 'receive-diagnostics',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Message {
|
|
24
|
+
type: MessageType;
|
|
25
|
+
payload?: any;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const createCollectDiagnosticsBroadcastSender = (): CollectDiagnosticsBroadcastSender => {
|
|
29
|
+
return {
|
|
30
|
+
broadcastDiagnosticsRequest: async () => {
|
|
31
|
+
let expectedResponse = MessageType.PROBE_ACK;
|
|
32
|
+
let channel: BroadcastChannel | undefined;
|
|
33
|
+
try {
|
|
34
|
+
const trigger = new Trigger<Message>();
|
|
35
|
+
channel = new BroadcastChannel(CHANNEL_NAME);
|
|
36
|
+
channel.onmessage = (msg) => {
|
|
37
|
+
if (expectedResponse === msg.data.type) {
|
|
38
|
+
trigger.wake(msg.data);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
channel.postMessage({ type: MessageType.PROBE });
|
|
42
|
+
await trigger.wait({ timeout: 200 });
|
|
43
|
+
expectedResponse = MessageType.RECEIVE_DIAGNOSTICS;
|
|
44
|
+
trigger.reset();
|
|
45
|
+
channel.postMessage({ type: MessageType.REQUEST_DIAGNOSTICS });
|
|
46
|
+
const diagnostics = await trigger.wait({ timeout: 5000 });
|
|
47
|
+
return diagnostics.payload;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
const errorDescription = e instanceof Error ? e.message : JSON.stringify(e);
|
|
50
|
+
return { expectedResponse, errorDescription };
|
|
51
|
+
} finally {
|
|
52
|
+
safeClose(channel);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const createCollectDiagnosticsBroadcastHandler = (
|
|
59
|
+
systemService: SystemService,
|
|
60
|
+
): CollectDiagnosticsBroadcastHandler => {
|
|
61
|
+
let channel: BroadcastChannel | undefined;
|
|
62
|
+
return {
|
|
63
|
+
start: () => {
|
|
64
|
+
channel = new BroadcastChannel(CHANNEL_NAME);
|
|
65
|
+
channel.onmessage = async (message) => {
|
|
66
|
+
try {
|
|
67
|
+
if (message.data.type === MessageType.PROBE) {
|
|
68
|
+
channel?.postMessage({ type: MessageType.PROBE_ACK });
|
|
69
|
+
} else if (message.data.type === MessageType.REQUEST_DIAGNOSTICS) {
|
|
70
|
+
const diagnostics = await systemService.getDiagnostics({});
|
|
71
|
+
channel?.postMessage({
|
|
72
|
+
type: MessageType.RECEIVE_DIAGNOSTICS,
|
|
73
|
+
payload: diagnostics,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
log.catch(error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
stop: () => {
|
|
82
|
+
safeClose(channel);
|
|
83
|
+
channel = undefined;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const safeClose = (channel?: BroadcastChannel) => {
|
|
89
|
+
try {
|
|
90
|
+
channel?.close();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// ignored
|
|
93
|
+
}
|
|
94
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { type SystemService } from '@dxos/protocols/proto/dxos/client/services';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type CollectDiagnosticsBroadcastSender,
|
|
8
|
+
type CollectDiagnosticsBroadcastHandler,
|
|
9
|
+
} from './diagnostics-collector';
|
|
10
|
+
|
|
11
|
+
export const createCollectDiagnosticsBroadcastSender = (): CollectDiagnosticsBroadcastSender => {
|
|
12
|
+
return { broadcastDiagnosticsRequest: async () => undefined };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const createCollectDiagnosticsBroadcastHandler = (_: SystemService): CollectDiagnosticsBroadcastHandler => {
|
|
16
|
+
return {
|
|
17
|
+
start: () => {},
|
|
18
|
+
stop: () => {},
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type ClientServicesProvider } from '@dxos/client-protocol';
|
|
6
|
+
import { type Config, ConfigResource } from '@dxos/config';
|
|
7
|
+
import { GetDiagnosticsRequest } from '@dxos/protocols/proto/dxos/client/services';
|
|
8
|
+
import { TRACE_PROCESSOR } from '@dxos/tracing';
|
|
9
|
+
import { type JsonKeyOptions, jsonKeyReplacer, nonNullable } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
import { createCollectDiagnosticsBroadcastSender } from './diagnostics-broadcast';
|
|
12
|
+
import { ClientServicesProviderResource } from '../services';
|
|
13
|
+
|
|
14
|
+
export interface CollectDiagnosticsBroadcastSender {
|
|
15
|
+
broadcastDiagnosticsRequest(): any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CollectDiagnosticsBroadcastHandler {
|
|
19
|
+
start(): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DiagnosticsCollector {
|
|
24
|
+
private static broadcastSender = createCollectDiagnosticsBroadcastSender();
|
|
25
|
+
|
|
26
|
+
public static async collect(
|
|
27
|
+
config: Config | Config[] = findConfigs(),
|
|
28
|
+
services: ClientServicesProvider | null = findSystemServiceProvider(),
|
|
29
|
+
options: JsonKeyOptions = {},
|
|
30
|
+
): Promise<any> {
|
|
31
|
+
const serviceDiagnostics = await services?.services?.SystemService?.getDiagnostics({
|
|
32
|
+
keys: options.humanize
|
|
33
|
+
? GetDiagnosticsRequest.KEY_OPTION.HUMANIZE
|
|
34
|
+
: options.truncate
|
|
35
|
+
? GetDiagnosticsRequest.KEY_OPTION.TRUNCATE
|
|
36
|
+
: undefined,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const clientDiagnostics = {
|
|
40
|
+
config,
|
|
41
|
+
trace: TRACE_PROCESSOR.getDiagnostics(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const diagnostics =
|
|
45
|
+
serviceDiagnostics != null
|
|
46
|
+
? { client: clientDiagnostics, services: serviceDiagnostics }
|
|
47
|
+
: {
|
|
48
|
+
client: clientDiagnostics,
|
|
49
|
+
broadcast: await this.broadcastSender.broadcastDiagnosticsRequest(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return JSON.parse(JSON.stringify(diagnostics, jsonKeyReplacer(options)));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const findSystemServiceProvider = (): ClientServicesProvider | null => {
|
|
57
|
+
const serviceProviders = TRACE_PROCESSOR.findByAnnotation(ClientServicesProviderResource);
|
|
58
|
+
const providerResource = serviceProviders.find((r) => r.instance.deref()?.services?.SystemService != null);
|
|
59
|
+
return providerResource?.instance?.deref() ?? null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const findConfigs = (): Config[] => {
|
|
63
|
+
const configs = TRACE_PROCESSOR.findByAnnotation(ConfigResource);
|
|
64
|
+
return configs.map((r) => r.instance.deref()).filter(nonNullable);
|
|
65
|
+
};
|
|
@@ -25,9 +25,9 @@ import { type Epoch } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
|
25
25
|
import { type Resource, type Span } from '@dxos/protocols/proto/dxos/tracing';
|
|
26
26
|
import { TRACE_PROCESSOR } from '@dxos/tracing';
|
|
27
27
|
|
|
28
|
-
import { getPlatform } from './platform';
|
|
29
|
-
import { type ServiceContext } from './service-context';
|
|
30
28
|
import { DXOS_VERSION } from '../../version';
|
|
29
|
+
import { type ServiceContext } from '../services';
|
|
30
|
+
import { getPlatform } from '../services/platform';
|
|
31
31
|
import { type DataSpace } from '../spaces';
|
|
32
32
|
|
|
33
33
|
const DEFAULT_TIMEOUT = 1_000;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { Trigger } from '@dxos/async';
|
|
6
6
|
import { cancelWithContext, Context } from '@dxos/context';
|
|
7
|
+
import { randomBytes, verify } from '@dxos/crypto';
|
|
7
8
|
import { invariant } from '@dxos/invariant';
|
|
8
9
|
import { PublicKey } from '@dxos/keys';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
@@ -51,6 +52,8 @@ export class InvitationHostExtension extends RpcExtension<
|
|
|
51
52
|
private _remoteOptions?: Options;
|
|
52
53
|
private _remoteOptionsTrigger = new Trigger();
|
|
53
54
|
|
|
55
|
+
private _challenge?: Buffer = undefined;
|
|
56
|
+
|
|
54
57
|
public invitation?: Invitation = undefined;
|
|
55
58
|
|
|
56
59
|
public guestProfile?: ProfileDocument = undefined;
|
|
@@ -113,13 +116,17 @@ export class InvitationHostExtension extends RpcExtension<
|
|
|
113
116
|
|
|
114
117
|
this._callbacks.onStateUpdate({ ...this.invitation, state: Invitation.State.READY_FOR_AUTHENTICATION });
|
|
115
118
|
|
|
119
|
+
this._challenge =
|
|
120
|
+
this.invitation.authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? randomBytes(32) : undefined;
|
|
121
|
+
|
|
116
122
|
log.trace('dxos.sdk.invitation-handler.host.introduce', trace.end({ id: traceId }));
|
|
117
123
|
return {
|
|
118
124
|
authMethod: this.invitation.authMethod,
|
|
125
|
+
challenge: this._challenge,
|
|
119
126
|
};
|
|
120
127
|
},
|
|
121
128
|
|
|
122
|
-
authenticate: async ({ authCode: code }) => {
|
|
129
|
+
authenticate: async ({ authCode: code, signedChallenge }) => {
|
|
123
130
|
const traceId = PublicKey.random().toHex();
|
|
124
131
|
log.trace('dxos.sdk.invitation-handler.host.authenticate', trace.begin({ id: traceId }));
|
|
125
132
|
log('received authentication request', { authCode: code });
|
|
@@ -145,6 +152,26 @@ export class InvitationHostExtension extends RpcExtension<
|
|
|
145
152
|
break;
|
|
146
153
|
}
|
|
147
154
|
|
|
155
|
+
case Invitation.AuthMethod.KNOWN_PUBLIC_KEY: {
|
|
156
|
+
if (!this.invitation.guestKeypair) {
|
|
157
|
+
status = AuthenticationResponse.Status.INTERNAL_ERROR;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
const isSignatureValid =
|
|
161
|
+
this._challenge &&
|
|
162
|
+
verify(
|
|
163
|
+
this._challenge,
|
|
164
|
+
Buffer.from(signedChallenge ?? []),
|
|
165
|
+
this.invitation.guestKeypair.publicKey.asBuffer(),
|
|
166
|
+
);
|
|
167
|
+
if (isSignatureValid) {
|
|
168
|
+
this.authenticationPassed = true;
|
|
169
|
+
} else {
|
|
170
|
+
status = AuthenticationResponse.Status.INVALID_SIGNATURE;
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
148
175
|
default: {
|
|
149
176
|
log.error('invalid authentication method', { authMethod: this.invitation.authMethod });
|
|
150
177
|
status = AuthenticationResponse.Status.INTERNAL_ERROR;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@dxos/client-protocol';
|
|
12
12
|
import { Context } from '@dxos/context';
|
|
13
13
|
import { generatePasscode } from '@dxos/credentials';
|
|
14
|
+
import { createKeyPair, sign } from '@dxos/crypto';
|
|
14
15
|
import { invariant } from '@dxos/invariant';
|
|
15
16
|
import { PublicKey } from '@dxos/keys';
|
|
16
17
|
import { log } from '@dxos/log';
|
|
@@ -21,9 +22,9 @@ import {
|
|
|
21
22
|
type SwarmConnection,
|
|
22
23
|
} from '@dxos/network-manager';
|
|
23
24
|
import { InvalidInvitationExtensionRoleError, trace } from '@dxos/protocols';
|
|
24
|
-
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
25
|
+
import { type AdmissionKeypair, Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
25
26
|
import { type DeviceProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
26
|
-
import { AuthenticationResponse } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
27
|
+
import { AuthenticationResponse, type IntroductionResponse } from '@dxos/protocols/proto/dxos/halo/invitations';
|
|
27
28
|
|
|
28
29
|
import {
|
|
29
30
|
InvitationGuestExtension,
|
|
@@ -74,9 +75,11 @@ export class InvitationsHandler {
|
|
|
74
75
|
state = Invitation.State.INIT,
|
|
75
76
|
timeout = INVITATION_TIMEOUT,
|
|
76
77
|
swarmKey = PublicKey.random(),
|
|
77
|
-
persistent =
|
|
78
|
+
persistent = options?.authMethod !== Invitation.AuthMethod.KNOWN_PUBLIC_KEY, // default no not storing keypairs
|
|
78
79
|
created = new Date(),
|
|
79
|
-
|
|
80
|
+
guestKeypair = undefined,
|
|
81
|
+
lifetime = 86400, // 1 day,
|
|
82
|
+
multiUse = false,
|
|
80
83
|
} = options ?? {};
|
|
81
84
|
const authCode =
|
|
82
85
|
options?.authCode ??
|
|
@@ -91,9 +94,12 @@ export class InvitationsHandler {
|
|
|
91
94
|
swarmKey,
|
|
92
95
|
authCode,
|
|
93
96
|
timeout,
|
|
94
|
-
persistent,
|
|
97
|
+
persistent: persistent && type !== Invitation.Type.DELEGATED, // delegated invitations are persisted in control feed
|
|
98
|
+
guestKeypair:
|
|
99
|
+
guestKeypair ?? (authMethod === Invitation.AuthMethod.KNOWN_PUBLIC_KEY ? createAdmissionKeypair() : undefined),
|
|
95
100
|
created,
|
|
96
101
|
lifetime,
|
|
102
|
+
multiUse,
|
|
97
103
|
...protocol.getInvitationContext(),
|
|
98
104
|
};
|
|
99
105
|
|
|
@@ -162,7 +168,7 @@ export class InvitationsHandler {
|
|
|
162
168
|
}
|
|
163
169
|
log.trace('dxos.sdk.invitations-handler.host.onOpen', trace.error({ id: traceId, error: err }));
|
|
164
170
|
} finally {
|
|
165
|
-
if (
|
|
171
|
+
if (!multiUse) {
|
|
166
172
|
// Wait for graceful close before disposing.
|
|
167
173
|
await swarmConnection.close();
|
|
168
174
|
await ctx.dispose();
|
|
@@ -317,26 +323,13 @@ export class InvitationsHandler {
|
|
|
317
323
|
|
|
318
324
|
// 2. Get authentication code.
|
|
319
325
|
if (isAuthenticationRequired(invitation)) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
setState({ state: Invitation.State.AUTHENTICATING });
|
|
327
|
-
const response = await extension.rpc.InvitationHostService.authenticate({ authCode });
|
|
328
|
-
if (response.status === undefined || response.status === AuthenticationResponse.Status.OK) {
|
|
326
|
+
switch (invitation.authMethod) {
|
|
327
|
+
case Invitation.AuthMethod.SHARED_SECRET:
|
|
328
|
+
await this._handleGuestOtpAuth(extension, setState, authenticated, { timeout });
|
|
329
|
+
break;
|
|
330
|
+
case Invitation.AuthMethod.KNOWN_PUBLIC_KEY:
|
|
331
|
+
await this._handleGuestKpkAuth(extension, setState, invitation, introductionResponse);
|
|
329
332
|
break;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (response.status === AuthenticationResponse.Status.INVALID_OTP) {
|
|
333
|
-
if (attempt === MAX_OTP_ATTEMPTS) {
|
|
334
|
-
throw new Error(`Maximum retry attempts: ${MAX_OTP_ATTEMPTS}`);
|
|
335
|
-
} else {
|
|
336
|
-
log('retrying invalid code', { attempt });
|
|
337
|
-
authenticated.reset();
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
333
|
}
|
|
341
334
|
}
|
|
342
335
|
|
|
@@ -423,13 +416,61 @@ export class InvitationsHandler {
|
|
|
423
416
|
|
|
424
417
|
return observable;
|
|
425
418
|
}
|
|
419
|
+
|
|
420
|
+
private async _handleGuestOtpAuth(
|
|
421
|
+
extension: InvitationGuestExtension,
|
|
422
|
+
setState: (newState: Partial<Invitation>) => void,
|
|
423
|
+
authenticated: Trigger<string>,
|
|
424
|
+
options: { timeout: number },
|
|
425
|
+
) {
|
|
426
|
+
for (let attempt = 1; attempt <= MAX_OTP_ATTEMPTS; attempt++) {
|
|
427
|
+
log('guest waiting for authentication code...');
|
|
428
|
+
setState({ state: Invitation.State.READY_FOR_AUTHENTICATION });
|
|
429
|
+
const authCode = await authenticated.wait(options);
|
|
430
|
+
|
|
431
|
+
log('sending authentication request');
|
|
432
|
+
setState({ state: Invitation.State.AUTHENTICATING });
|
|
433
|
+
const response = await extension.rpc.InvitationHostService.authenticate({ authCode });
|
|
434
|
+
if (response.status === undefined || response.status === AuthenticationResponse.Status.OK) {
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (response.status === AuthenticationResponse.Status.INVALID_OTP) {
|
|
439
|
+
if (attempt === MAX_OTP_ATTEMPTS) {
|
|
440
|
+
throw new Error(`Maximum retry attempts: ${MAX_OTP_ATTEMPTS}`);
|
|
441
|
+
} else {
|
|
442
|
+
log('retrying invalid code', { attempt });
|
|
443
|
+
authenticated.reset();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private async _handleGuestKpkAuth(
|
|
450
|
+
extension: InvitationGuestExtension,
|
|
451
|
+
setState: (newState: Partial<Invitation>) => void,
|
|
452
|
+
invitation: Invitation,
|
|
453
|
+
introductionResponse: IntroductionResponse,
|
|
454
|
+
) {
|
|
455
|
+
if (invitation.guestKeypair?.privateKey == null) {
|
|
456
|
+
throw new Error('keypair missing in the invitation');
|
|
457
|
+
}
|
|
458
|
+
if (introductionResponse.challenge == null) {
|
|
459
|
+
throw new Error('challenge missing in the introduction');
|
|
460
|
+
}
|
|
461
|
+
log('sending authentication request');
|
|
462
|
+
setState({ state: Invitation.State.AUTHENTICATING });
|
|
463
|
+
const signature = sign(Buffer.from(introductionResponse.challenge), invitation.guestKeypair.privateKey);
|
|
464
|
+
const response = await extension.rpc.InvitationHostService.authenticate({
|
|
465
|
+
signedChallenge: signature,
|
|
466
|
+
});
|
|
467
|
+
if (response.status !== AuthenticationResponse.Status.OK) {
|
|
468
|
+
throw new Error(`Authentication failed with code: ${response.status}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
426
471
|
}
|
|
427
472
|
|
|
428
|
-
export const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
invitation.lifetime &&
|
|
432
|
-
invitation.lifetime !== 0 &&
|
|
433
|
-
invitation.created.getTime() + invitation.lifetime * 1000 < Date.now()
|
|
434
|
-
);
|
|
473
|
+
export const createAdmissionKeypair = (): AdmissionKeypair => {
|
|
474
|
+
const keypair = createKeyPair();
|
|
475
|
+
return { publicKey: PublicKey.from(keypair.publicKey), privateKey: keypair.secretKey };
|
|
435
476
|
};
|
|
@@ -6,7 +6,7 @@ import { Event, scheduleTask } from '@dxos/async';
|
|
|
6
6
|
import { type AuthenticatingInvitation, type CancellableInvitation } from '@dxos/client-protocol';
|
|
7
7
|
import { Stream } from '@dxos/codec-protobuf';
|
|
8
8
|
import { Context } from '@dxos/context';
|
|
9
|
-
import { type MetadataStore } from '@dxos/echo-pipeline';
|
|
9
|
+
import { hasInvitationExpired, type MetadataStore } from '@dxos/echo-pipeline';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
import {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
19
19
|
|
|
20
20
|
import { type InvitationProtocol } from './invitation-protocol';
|
|
21
|
-
import {
|
|
21
|
+
import { type InvitationsHandler } from './invitations-handler';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Adapts invitation service observable to client/service stream.
|
|
@@ -91,7 +91,7 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
this._createInvitations.delete(invitation.get().invitationId);
|
|
94
|
-
if (invitation.get().
|
|
94
|
+
if (!invitation.get().multiUse) {
|
|
95
95
|
this._removedCreated.emit(invitation.get());
|
|
96
96
|
}
|
|
97
97
|
},
|
|
@@ -103,7 +103,7 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
103
103
|
const persistentInvitations = this._metadataStore.getInvitations();
|
|
104
104
|
|
|
105
105
|
// get saved persistent invitations, filter and remove from storage those that have expired.
|
|
106
|
-
const freshInvitations = persistentInvitations.filter(async (invitation) => !
|
|
106
|
+
const freshInvitations = persistentInvitations.filter(async (invitation) => !hasInvitationExpired(invitation));
|
|
107
107
|
|
|
108
108
|
const cInvitations = freshInvitations.map((persistentInvitation) => {
|
|
109
109
|
invariant(!this._createInvitations.get(persistentInvitation.invitationId), 'invitation already exists');
|
|
@@ -148,7 +148,7 @@ export class InvitationsServiceImpl implements InvitationsService {
|
|
|
148
148
|
() => {
|
|
149
149
|
close();
|
|
150
150
|
this._acceptInvitations.delete(invitation.get().invitationId);
|
|
151
|
-
if (invitation.get().
|
|
151
|
+
if (!invitation.get().multiUse) {
|
|
152
152
|
this._removedAccepted.emit(invitation.get());
|
|
153
153
|
}
|
|
154
154
|
},
|
|
@@ -6,8 +6,8 @@ import { expect } from 'chai';
|
|
|
6
6
|
|
|
7
7
|
import { asyncTimeout, sleep } from '@dxos/async';
|
|
8
8
|
import { AutomergeHost, DataServiceImpl } from '@dxos/echo-pipeline';
|
|
9
|
+
import { createTestLevel } from '@dxos/echo-pipeline/testing';
|
|
9
10
|
import { AutomergeContext } from '@dxos/echo-schema';
|
|
10
|
-
import { StorageType, createStorage } from '@dxos/random-access-storage';
|
|
11
11
|
import { afterTest, describe, test } from '@dxos/test';
|
|
12
12
|
|
|
13
13
|
describe('AutomergeHost', () => {
|
|
@@ -19,10 +19,16 @@ describe('AutomergeHost', () => {
|
|
|
19
19
|
// creates repo and document | replicates repo | finds document in repo
|
|
20
20
|
//
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const level = createTestLevel();
|
|
23
|
+
await level.open();
|
|
24
|
+
afterTest(() => level.close());
|
|
23
25
|
|
|
24
|
-
const host = new AutomergeHost({
|
|
26
|
+
const host = new AutomergeHost({
|
|
27
|
+
db: level.sublevel('automerge'),
|
|
28
|
+
});
|
|
29
|
+
await host.open();
|
|
25
30
|
afterTest(() => host.close());
|
|
31
|
+
|
|
26
32
|
const dataService = new DataServiceImpl(host);
|
|
27
33
|
const client = new AutomergeContext(dataService);
|
|
28
34
|
afterTest(() => client.close());
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { MemorySignalManagerContext } from '@dxos/messaging';
|
|
6
6
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
7
|
-
import { describe, test } from '@dxos/test';
|
|
7
|
+
import { describe, openAndClose, test } from '@dxos/test';
|
|
8
8
|
|
|
9
9
|
import { createServiceContext } from '../testing';
|
|
10
10
|
import { performInvitation } from '../testing/invitation-utils';
|
|
@@ -27,12 +27,15 @@ describe('services/ServiceContext', () => {
|
|
|
27
27
|
test('joined space is synchronized on device invitations', async () => {
|
|
28
28
|
const networkContext = new MemorySignalManagerContext();
|
|
29
29
|
const device1 = await createServiceContext({ signalContext: networkContext });
|
|
30
|
+
await openAndClose(device1.automergeHost);
|
|
30
31
|
await device1.createIdentity();
|
|
31
32
|
|
|
32
33
|
const device2 = await createServiceContext({ signalContext: networkContext });
|
|
34
|
+
await openAndClose(device2.automergeHost);
|
|
33
35
|
await Promise.all(performInvitation({ host: device1, guest: device2, options: { kind: Invitation.Kind.DEVICE } }));
|
|
34
36
|
|
|
35
37
|
const identity2 = await createServiceContext({ signalContext: networkContext });
|
|
38
|
+
await openAndClose(identity2.automergeHost);
|
|
36
39
|
await identity2.createIdentity();
|
|
37
40
|
const space1 = await identity2.dataSpaceManager!.createSpace();
|
|
38
41
|
await Promise.all(
|
|
@@ -124,6 +124,7 @@ export class ServiceContext extends Resource {
|
|
|
124
124
|
|
|
125
125
|
this.automergeHost = new AutomergeHost({
|
|
126
126
|
directory: storage.createDirectory('automerge'),
|
|
127
|
+
db: level.sublevel('automerge'),
|
|
127
128
|
metadata: this.indexMetadata,
|
|
128
129
|
});
|
|
129
130
|
|
|
@@ -158,6 +159,7 @@ export class ServiceContext extends Resource {
|
|
|
158
159
|
await this.signalManager.open();
|
|
159
160
|
await this.networkManager.open();
|
|
160
161
|
|
|
162
|
+
await this.automergeHost.open();
|
|
161
163
|
await this.metadataStore.load();
|
|
162
164
|
await this.spaceManager.open();
|
|
163
165
|
await this.identityManager.open(ctx);
|
|
@@ -8,8 +8,14 @@ import { Event, synchronized } from '@dxos/async';
|
|
|
8
8
|
import { clientServiceBundle, defaultKey, type ClientServices, Properties } from '@dxos/client-protocol';
|
|
9
9
|
import { type Config } from '@dxos/config';
|
|
10
10
|
import { Context } from '@dxos/context';
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
DataServiceImpl,
|
|
13
|
+
type ObjectStructure,
|
|
14
|
+
encodeReference,
|
|
15
|
+
type SpaceDoc,
|
|
16
|
+
type MyLevel,
|
|
17
|
+
} from '@dxos/echo-pipeline';
|
|
18
|
+
import { getTypeReference } from '@dxos/echo-schema';
|
|
13
19
|
import { IndexServiceImpl } from '@dxos/indexing';
|
|
14
20
|
import { invariant } from '@dxos/invariant';
|
|
15
21
|
import { PublicKey } from '@dxos/keys';
|
|
@@ -23,11 +29,15 @@ import { TRACE_PROCESSOR, trace as Trace } from '@dxos/tracing';
|
|
|
23
29
|
import { assignDeep } from '@dxos/util';
|
|
24
30
|
import { WebsocketRpcClient } from '@dxos/websocket-rpc';
|
|
25
31
|
|
|
26
|
-
import { createDiagnostics } from './diagnostics';
|
|
27
32
|
import { ServiceContext, type ServiceContextRuntimeParams } from './service-context';
|
|
28
33
|
import { ServiceRegistry } from './service-registry';
|
|
29
34
|
import { DevicesServiceImpl } from '../devices';
|
|
30
35
|
import { DevtoolsHostEvents, DevtoolsServiceImpl } from '../devtools';
|
|
36
|
+
import {
|
|
37
|
+
type CollectDiagnosticsBroadcastHandler,
|
|
38
|
+
createCollectDiagnosticsBroadcastHandler,
|
|
39
|
+
createDiagnostics,
|
|
40
|
+
} from '../diagnostics';
|
|
31
41
|
import { IdentityServiceImpl, type CreateIdentityOptions } from '../identity';
|
|
32
42
|
import { InvitationsServiceImpl } from '../invitations';
|
|
33
43
|
import { Lock, type ResourceLock } from '../locks';
|
|
@@ -46,6 +56,7 @@ export type ClientServicesHostParams = {
|
|
|
46
56
|
signalManager?: SignalManager;
|
|
47
57
|
connectionLog?: boolean;
|
|
48
58
|
storage?: Storage;
|
|
59
|
+
level?: MyLevel;
|
|
49
60
|
lockKey?: string;
|
|
50
61
|
callbacks?: ClientServicesHostCallbacks;
|
|
51
62
|
runtimeParams?: ServiceContextRuntimeParams;
|
|
@@ -84,6 +95,7 @@ export class ClientServicesHost {
|
|
|
84
95
|
|
|
85
96
|
private _serviceContext!: ServiceContext;
|
|
86
97
|
private readonly _runtimeParams?: ServiceContextRuntimeParams;
|
|
98
|
+
private diagnosticsBroadcastHandler: CollectDiagnosticsBroadcastHandler;
|
|
87
99
|
|
|
88
100
|
@Trace.info()
|
|
89
101
|
private _opening = false;
|
|
@@ -96,12 +108,14 @@ export class ClientServicesHost {
|
|
|
96
108
|
transportFactory,
|
|
97
109
|
signalManager,
|
|
98
110
|
storage,
|
|
111
|
+
level,
|
|
99
112
|
// TODO(wittjosiah): Turn this on by default.
|
|
100
113
|
lockKey,
|
|
101
114
|
callbacks,
|
|
102
115
|
runtimeParams,
|
|
103
116
|
}: ClientServicesHostParams = {}) {
|
|
104
117
|
this._storage = storage;
|
|
118
|
+
this._level = level;
|
|
105
119
|
this._callbacks = callbacks;
|
|
106
120
|
this._runtimeParams = runtimeParams;
|
|
107
121
|
|
|
@@ -141,6 +155,7 @@ export class ClientServicesHost {
|
|
|
141
155
|
},
|
|
142
156
|
});
|
|
143
157
|
|
|
158
|
+
this.diagnosticsBroadcastHandler = createCollectDiagnosticsBroadcastHandler(this._systemService);
|
|
144
159
|
this._loggingService = new LoggingServiceImpl();
|
|
145
160
|
|
|
146
161
|
this._serviceRegistry = new ServiceRegistry<ClientServices>(clientServiceBundle, {
|
|
@@ -233,6 +248,8 @@ export class ClientServicesHost {
|
|
|
233
248
|
if (!this._level) {
|
|
234
249
|
this._level = await createLevel(this._config.get('runtime.client.storage', {})!);
|
|
235
250
|
}
|
|
251
|
+
await this._level.open();
|
|
252
|
+
|
|
236
253
|
await this._resourceLock?.acquire();
|
|
237
254
|
|
|
238
255
|
await this._loggingService.open();
|
|
@@ -309,6 +326,7 @@ export class ClientServicesHost {
|
|
|
309
326
|
});
|
|
310
327
|
void this._devtoolsProxy.open();
|
|
311
328
|
}
|
|
329
|
+
this.diagnosticsBroadcastHandler.start();
|
|
312
330
|
|
|
313
331
|
this._opening = false;
|
|
314
332
|
this._open = true;
|
|
@@ -327,6 +345,7 @@ export class ClientServicesHost {
|
|
|
327
345
|
|
|
328
346
|
const deviceKey = this._serviceContext.identityManager.identity?.deviceKey;
|
|
329
347
|
log('closing...', { deviceKey });
|
|
348
|
+
this.diagnosticsBroadcastHandler.stop();
|
|
330
349
|
await this._devtoolsProxy?.close();
|
|
331
350
|
this._serviceRegistry.setServices({ SystemService: this._systemService });
|
|
332
351
|
await this._loggingService.close();
|
|
@@ -364,7 +383,7 @@ export class ClientServicesHost {
|
|
|
364
383
|
// TODO(dmaretskyi): Better API for low-level data access.
|
|
365
384
|
const properties: ObjectStructure = {
|
|
366
385
|
system: {
|
|
367
|
-
type: encodeReference(
|
|
386
|
+
type: encodeReference(getTypeReference(Properties)!),
|
|
368
387
|
},
|
|
369
388
|
data: {
|
|
370
389
|
[defaultKey]: identity.identityKey.toHex(),
|