@dxos/edge-client 0.6.13 → 0.6.14-main.69511f5
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-ZWJXA37R.mjs +113 -0
- package/dist/lib/browser/chunk-ZWJXA37R.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +469 -160
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +125 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node/chunk-ANV2HBEH.cjs +136 -0
- package/dist/lib/node/chunk-ANV2HBEH.cjs.map +7 -0
- package/dist/lib/node/index.cjs +468 -158
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing/index.cjs +155 -0
- package/dist/lib/node/testing/index.cjs.map +7 -0
- package/dist/lib/node-esm/chunk-HNVT57AU.mjs +115 -0
- package/dist/lib/node-esm/chunk-HNVT57AU.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +787 -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 +126 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/auth.d.ts +22 -0
- package/dist/types/src/auth.d.ts.map +1 -0
- package/dist/types/src/defs.d.ts.map +1 -1
- package/dist/types/src/edge-client.d.ts +14 -13
- package/dist/types/src/edge-client.d.ts.map +1 -1
- package/dist/types/src/edge-http-client.d.ts +48 -0
- package/dist/types/src/edge-http-client.d.ts.map +1 -0
- package/dist/types/src/edge-identity.d.ts +15 -0
- package/dist/types/src/edge-identity.d.ts.map +1 -0
- package/dist/types/src/errors.d.ts +4 -1
- package/dist/types/src/errors.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/protocol.d.ts +2 -2
- package/dist/types/src/protocol.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/test-utils.d.ts +21 -0
- package/dist/types/src/testing/test-utils.d.ts.map +1 -0
- package/dist/types/src/utils.d.ts +2 -0
- package/dist/types/src/utils.d.ts.map +1 -0
- package/package.json +27 -17
- package/src/auth.ts +135 -0
- package/src/defs.ts +2 -3
- package/src/edge-client.test.ts +50 -18
- package/src/edge-client.ts +76 -24
- package/src/edge-http-client.ts +210 -0
- package/src/edge-identity.ts +31 -0
- package/src/errors.ts +8 -2
- package/src/index.ts +4 -0
- package/src/persistent-lifecycle.test.ts +2 -2
- package/src/protocol.test.ts +1 -2
- package/src/protocol.ts +2 -2
- package/src/testing/index.ts +5 -0
- package/src/testing/test-utils.ts +114 -0
- package/src/utils.ts +10 -0
- package/src/websocket.test.ts +5 -4
- package/dist/types/src/test-utils.d.ts +0 -11
- package/dist/types/src/test-utils.d.ts.map +0 -1
- package/src/test-utils.ts +0 -49
package/package.json
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/edge-client",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.14-main.69511f5",
|
|
4
4
|
"description": "EDGE Client",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "DXOS.org",
|
|
9
|
+
"sideEffects": true,
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
12
|
+
"types": "./dist/types/src/index.d.ts",
|
|
11
13
|
"browser": "./dist/lib/browser/index.mjs",
|
|
12
|
-
"node":
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"types": "./dist/types/src/index.d.ts"
|
|
14
|
+
"node": "./dist/lib/node-esm/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./testing": {
|
|
17
|
+
"types": "./dist/types/src/testing/index.d.ts",
|
|
18
|
+
"browser": "./dist/lib/browser/testing/index.mjs",
|
|
19
|
+
"node": "./dist/lib/node-esm/testing/index.mjs"
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
"types": "dist/types/src/index.d.ts",
|
|
19
23
|
"typesVersions": {
|
|
20
|
-
"*": {
|
|
24
|
+
"*": {
|
|
25
|
+
"testing": [
|
|
26
|
+
"dist/types/src/testing/index.d.ts"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
21
29
|
},
|
|
22
30
|
"files": [
|
|
23
31
|
"dist",
|
|
@@ -25,21 +33,23 @@
|
|
|
25
33
|
"README.md"
|
|
26
34
|
],
|
|
27
35
|
"dependencies": {
|
|
28
|
-
"@bufbuild/protobuf": "^2.0.0",
|
|
29
36
|
"isomorphic-ws": "^5.0.0",
|
|
30
37
|
"ws": "^8.14.2",
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
38
|
+
"@dxos/context": "0.6.14-main.69511f5",
|
|
39
|
+
"@dxos/credentials": "0.6.14-main.69511f5",
|
|
40
|
+
"@dxos/async": "0.6.14-main.69511f5",
|
|
41
|
+
"@dxos/crypto": "0.6.14-main.69511f5",
|
|
42
|
+
"@dxos/debug": "0.6.14-main.69511f5",
|
|
43
|
+
"@dxos/invariant": "0.6.14-main.69511f5",
|
|
44
|
+
"@dxos/keys": "0.6.14-main.69511f5",
|
|
45
|
+
"@dxos/log": "0.6.14-main.69511f5",
|
|
46
|
+
"@dxos/keyring": "0.6.14-main.69511f5",
|
|
47
|
+
"@dxos/node-std": "0.6.14-main.69511f5",
|
|
48
|
+
"@dxos/protocols": "0.6.14-main.69511f5",
|
|
49
|
+
"@dxos/util": "0.6.14-main.69511f5"
|
|
39
50
|
},
|
|
40
51
|
"devDependencies": {
|
|
41
|
-
"
|
|
42
|
-
"@dxos/keys": "0.6.13"
|
|
52
|
+
"@dxos/test-utils": "0.6.14-main.69511f5"
|
|
43
53
|
},
|
|
44
54
|
"publishConfig": {
|
|
45
55
|
"access": "public"
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createCredential, signPresentation } from '@dxos/credentials';
|
|
6
|
+
import { type Signer } from '@dxos/crypto';
|
|
7
|
+
import { Keyring } from '@dxos/keyring';
|
|
8
|
+
import { PublicKey } from '@dxos/keys';
|
|
9
|
+
import { type Chain, type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
10
|
+
|
|
11
|
+
import type { EdgeIdentity } from './edge-identity';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Edge identity backed by a device key without a credential chain.
|
|
15
|
+
*/
|
|
16
|
+
export const createDeviceEdgeIdentity = async (signer: Signer, key: PublicKey): Promise<EdgeIdentity> => {
|
|
17
|
+
return {
|
|
18
|
+
identityKey: key.toHex(),
|
|
19
|
+
peerKey: key.toHex(),
|
|
20
|
+
presentCredentials: async ({ challenge }) => {
|
|
21
|
+
return signPresentation({
|
|
22
|
+
presentation: {
|
|
23
|
+
credentials: [
|
|
24
|
+
// Verifier requires at least one credential in the presentation to establish the subject.
|
|
25
|
+
await createCredential({
|
|
26
|
+
assertion: {
|
|
27
|
+
'@type': 'dxos.halo.credentials.Auth',
|
|
28
|
+
},
|
|
29
|
+
issuer: key,
|
|
30
|
+
subject: key,
|
|
31
|
+
signer,
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
signer,
|
|
36
|
+
signerKey: key,
|
|
37
|
+
nonce: challenge,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Edge identity backed by a chain of credentials.
|
|
45
|
+
*/
|
|
46
|
+
export const createChainEdgeIdentity = async (
|
|
47
|
+
signer: Signer,
|
|
48
|
+
identityKey: PublicKey,
|
|
49
|
+
peerKey: PublicKey,
|
|
50
|
+
chain: Chain,
|
|
51
|
+
credentials: Credential[],
|
|
52
|
+
): Promise<EdgeIdentity> => {
|
|
53
|
+
const credentialsToSign =
|
|
54
|
+
credentials.length > 0
|
|
55
|
+
? credentials
|
|
56
|
+
: [
|
|
57
|
+
await createCredential({
|
|
58
|
+
assertion: {
|
|
59
|
+
'@type': 'dxos.halo.credentials.Auth',
|
|
60
|
+
},
|
|
61
|
+
issuer: identityKey,
|
|
62
|
+
subject: identityKey,
|
|
63
|
+
signer,
|
|
64
|
+
chain,
|
|
65
|
+
signingKey: peerKey,
|
|
66
|
+
}),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
identityKey: identityKey.toHex(),
|
|
71
|
+
peerKey: peerKey.toHex(),
|
|
72
|
+
presentCredentials: async ({ challenge }) => {
|
|
73
|
+
return signPresentation({
|
|
74
|
+
presentation: {
|
|
75
|
+
credentials: credentialsToSign,
|
|
76
|
+
},
|
|
77
|
+
signer,
|
|
78
|
+
nonce: challenge,
|
|
79
|
+
signerKey: peerKey,
|
|
80
|
+
chain,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Edge identity backed by a random ephemeral key without HALO.
|
|
88
|
+
*/
|
|
89
|
+
export const createEphemeralEdgeIdentity = async (): Promise<EdgeIdentity> => {
|
|
90
|
+
const keyring = new Keyring();
|
|
91
|
+
const key = await keyring.createKey();
|
|
92
|
+
return createDeviceEdgeIdentity(keyring, key);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a HALO chain of credentials to act as an edge identity.
|
|
97
|
+
*/
|
|
98
|
+
export const createTestHaloEdgeIdentity = async (
|
|
99
|
+
signer: Signer,
|
|
100
|
+
identityKey: PublicKey,
|
|
101
|
+
deviceKey: PublicKey,
|
|
102
|
+
): Promise<EdgeIdentity> => {
|
|
103
|
+
const deviceAdmission = await createCredential({
|
|
104
|
+
assertion: {
|
|
105
|
+
'@type': 'dxos.halo.credentials.AuthorizedDevice',
|
|
106
|
+
deviceKey,
|
|
107
|
+
identityKey,
|
|
108
|
+
},
|
|
109
|
+
issuer: identityKey,
|
|
110
|
+
subject: deviceKey,
|
|
111
|
+
signer,
|
|
112
|
+
});
|
|
113
|
+
return createChainEdgeIdentity(signer, identityKey, deviceKey, { credential: deviceAdmission }, [
|
|
114
|
+
await createCredential({
|
|
115
|
+
assertion: {
|
|
116
|
+
'@type': 'dxos.halo.credentials.Auth',
|
|
117
|
+
},
|
|
118
|
+
issuer: identityKey,
|
|
119
|
+
subject: identityKey,
|
|
120
|
+
signer,
|
|
121
|
+
}),
|
|
122
|
+
]);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const createStubEdgeIdentity = (): EdgeIdentity => {
|
|
126
|
+
const identityKey = PublicKey.random();
|
|
127
|
+
const deviceKey = PublicKey.random();
|
|
128
|
+
return {
|
|
129
|
+
identityKey: identityKey.toHex(),
|
|
130
|
+
peerKey: deviceKey.toHex(),
|
|
131
|
+
presentCredentials: async () => {
|
|
132
|
+
throw new Error('Stub identity does not support authentication.');
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
};
|
package/src/defs.ts
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import { bufWkt } from '@dxos/protocols/buf';
|
|
7
6
|
import { SwarmRequestSchema, SwarmResponseSchema, TextMessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
8
7
|
|
|
9
8
|
import { Protocol } from './protocol';
|
|
10
9
|
|
|
11
|
-
export const protocol = new Protocol([SwarmRequestSchema, SwarmResponseSchema, TextMessageSchema, AnySchema]);
|
|
10
|
+
export const protocol = new Protocol([SwarmRequestSchema, SwarmResponseSchema, TextMessageSchema, bufWkt.AnySchema]);
|
package/src/edge-client.test.ts
CHANGED
|
@@ -2,49 +2,81 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import chaiAsPromised from 'chai-as-promised';
|
|
5
|
+
import { describe, expect, onTestFinished, test } from 'vitest';
|
|
7
6
|
|
|
8
|
-
import {
|
|
7
|
+
import { Trigger } from '@dxos/async';
|
|
8
|
+
import { Keyring } from '@dxos/keyring';
|
|
9
9
|
import { TextMessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
10
|
-
import {
|
|
10
|
+
import { openAndClose } from '@dxos/test-utils';
|
|
11
11
|
|
|
12
|
+
import { createEphemeralEdgeIdentity, createTestHaloEdgeIdentity } from './auth';
|
|
12
13
|
import { protocol } from './defs';
|
|
13
14
|
import { EdgeClient } from './edge-client';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
chai.use(chaiAsPromised);
|
|
15
|
+
import { createTestEdgeWsServer } from './testing';
|
|
17
16
|
|
|
18
17
|
describe('EdgeClient', () => {
|
|
19
18
|
const textMessage = (message: string) => protocol.createMessage(TextMessageSchema, { payload: { message } });
|
|
20
19
|
|
|
21
20
|
test('reconnects on error', async () => {
|
|
22
|
-
const {
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const { closeConnection, endpoint, cleanup } = await createTestEdgeWsServer(8001);
|
|
22
|
+
onTestFinished(cleanup);
|
|
23
|
+
|
|
24
|
+
const client = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: endpoint });
|
|
25
25
|
await openAndClose(client);
|
|
26
26
|
await client.send(textMessage('Hello world 1'));
|
|
27
27
|
expect(client.isOpen).is.true;
|
|
28
28
|
|
|
29
29
|
const reconnected = client.reconnect.waitForCount(1);
|
|
30
|
-
await
|
|
30
|
+
await closeConnection();
|
|
31
31
|
await reconnected;
|
|
32
|
-
await expect(client.send(textMessage('Hello world 2'))).
|
|
32
|
+
await expect(client.send(textMessage('Hello world 2'))).resolves.not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('isConnected', async () => {
|
|
36
|
+
const admitConnection = new Trigger();
|
|
37
|
+
const { closeConnection, endpoint, cleanup } = await createTestEdgeWsServer(8001, { admitConnection });
|
|
38
|
+
onTestFinished(cleanup);
|
|
39
|
+
|
|
40
|
+
const client = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: endpoint });
|
|
41
|
+
await openAndClose(client);
|
|
42
|
+
|
|
43
|
+
expect(client.isConnected).toBeFalsy();
|
|
44
|
+
admitConnection.wake();
|
|
45
|
+
await expect.poll(() => client.isConnected).toBeTruthy();
|
|
46
|
+
|
|
47
|
+
admitConnection.reset();
|
|
48
|
+
await closeConnection();
|
|
49
|
+
expect(client.isOpen).is.true;
|
|
50
|
+
await expect.poll(() => client.isConnected).toBeFalsy();
|
|
51
|
+
|
|
52
|
+
admitConnection.wake();
|
|
53
|
+
await expect.poll(() => client.isConnected).toBeTruthy();
|
|
33
54
|
});
|
|
34
55
|
|
|
35
56
|
test('set identity reconnects', async () => {
|
|
36
|
-
const { endpoint } = await
|
|
57
|
+
const { endpoint, cleanup } = await createTestEdgeWsServer(8002);
|
|
58
|
+
onTestFinished(cleanup);
|
|
37
59
|
|
|
38
|
-
const
|
|
39
|
-
const client = new EdgeClient(id, id, { socketEndpoint: endpoint });
|
|
60
|
+
const client = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: endpoint });
|
|
40
61
|
await openAndClose(client);
|
|
41
62
|
await client.send(textMessage('Hello world 1'));
|
|
42
63
|
expect(client.isOpen).is.true;
|
|
43
64
|
|
|
44
|
-
const newId = PublicKey.random().toHex();
|
|
45
65
|
const reconnected = client.reconnect.waitForCount(1);
|
|
46
|
-
client.setIdentity(
|
|
66
|
+
client.setIdentity(await createEphemeralEdgeIdentity());
|
|
47
67
|
await reconnected;
|
|
48
|
-
await expect(client.send(textMessage('Hello world 2'))).
|
|
68
|
+
await expect(client.send(textMessage('Hello world 2'))).resolves.not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test.skipIf(!process.env.EDGE_ENDPOINT)('connect to local edge server', async () => {
|
|
72
|
+
// const identity = await createEphemeralEdgeIdentity();
|
|
73
|
+
|
|
74
|
+
const keyring = new Keyring();
|
|
75
|
+
const identity = await createTestHaloEdgeIdentity(keyring, await keyring.createKey(), await keyring.createKey());
|
|
76
|
+
|
|
77
|
+
const client = new EdgeClient(identity, { socketEndpoint: process.env.EDGE_ENDPOINT! });
|
|
78
|
+
await openAndClose(client);
|
|
79
|
+
await client.send(textMessage('Hello world 1'));
|
|
80
|
+
expect(client.isOpen).is.true;
|
|
49
81
|
});
|
|
50
82
|
});
|
package/src/edge-client.ts
CHANGED
|
@@ -6,15 +6,16 @@ import WebSocket from 'isomorphic-ws';
|
|
|
6
6
|
|
|
7
7
|
import { Trigger, Event, scheduleTaskInterval, scheduleTask, TriggerState } from '@dxos/async';
|
|
8
8
|
import { Context, LifecycleState, Resource, type Lifecycle } from '@dxos/context';
|
|
9
|
-
import { invariant } from '@dxos/invariant';
|
|
10
9
|
import { log } from '@dxos/log';
|
|
11
10
|
import { buf } from '@dxos/protocols/buf';
|
|
12
11
|
import { type Message, MessageSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
|
|
13
12
|
|
|
14
13
|
import { protocol } from './defs';
|
|
15
|
-
import {
|
|
14
|
+
import { type EdgeIdentity, handleAuthChallenge } from './edge-identity';
|
|
15
|
+
import { EdgeConnectionClosedError, EdgeIdentityChangedError } from './errors';
|
|
16
16
|
import { PersistentLifecycle } from './persistent-lifecycle';
|
|
17
17
|
import { type Protocol, toUint8Array } from './protocol';
|
|
18
|
+
import { getEdgeUrlWithProtocol } from './utils';
|
|
18
19
|
|
|
19
20
|
const DEFAULT_TIMEOUT = 10_000;
|
|
20
21
|
const SIGNAL_KEEPALIVE_INTERVAL = 5_000;
|
|
@@ -22,13 +23,15 @@ const SIGNAL_KEEPALIVE_INTERVAL = 5_000;
|
|
|
22
23
|
export type MessageListener = (message: Message) => void | Promise<void>;
|
|
23
24
|
|
|
24
25
|
export interface EdgeConnection extends Required<Lifecycle> {
|
|
26
|
+
connected: Event;
|
|
25
27
|
reconnect: Event;
|
|
26
28
|
|
|
27
29
|
get info(): any;
|
|
28
30
|
get identityKey(): string;
|
|
29
31
|
get peerKey(): string;
|
|
30
32
|
get isOpen(): boolean;
|
|
31
|
-
|
|
33
|
+
get isConnected(): boolean;
|
|
34
|
+
setIdentity(identity: EdgeIdentity): void;
|
|
32
35
|
addListener(listener: MessageListener): () => void;
|
|
33
36
|
send(message: Message): Promise<void>;
|
|
34
37
|
}
|
|
@@ -37,13 +40,15 @@ export type MessengerConfig = {
|
|
|
37
40
|
socketEndpoint: string;
|
|
38
41
|
timeout?: number;
|
|
39
42
|
protocol?: Protocol;
|
|
43
|
+
disableAuth?: boolean;
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* Messenger client.
|
|
44
48
|
*/
|
|
45
49
|
export class EdgeClient extends Resource implements EdgeConnection {
|
|
46
|
-
public reconnect = new Event();
|
|
50
|
+
public readonly reconnect = new Event();
|
|
51
|
+
public readonly connected = new Event();
|
|
47
52
|
private readonly _persistentLifecycle = new PersistentLifecycle({
|
|
48
53
|
start: async () => this._openWebSocket(),
|
|
49
54
|
stop: async () => this._closeWebSocket(),
|
|
@@ -51,42 +56,50 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
private readonly _listeners = new Set<MessageListener>();
|
|
54
|
-
private readonly _protocol: Protocol;
|
|
55
59
|
private _ready = new Trigger();
|
|
56
60
|
private _ws?: WebSocket = undefined;
|
|
57
61
|
private _keepaliveCtx?: Context = undefined;
|
|
58
62
|
private _heartBeatContext?: Context = undefined;
|
|
59
63
|
|
|
64
|
+
private _baseWsUrl: string;
|
|
65
|
+
private _baseHttpUrl: string;
|
|
66
|
+
|
|
60
67
|
constructor(
|
|
61
|
-
private
|
|
62
|
-
private _peerKey: string,
|
|
68
|
+
private _identity: EdgeIdentity,
|
|
63
69
|
private readonly _config: MessengerConfig,
|
|
64
70
|
) {
|
|
65
71
|
super();
|
|
66
|
-
this.
|
|
72
|
+
this._baseWsUrl = getEdgeUrlWithProtocol(_config.socketEndpoint, 'ws');
|
|
73
|
+
this._baseHttpUrl = getEdgeUrlWithProtocol(_config.socketEndpoint, 'http');
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
// TODO(burdon): Attach logging.
|
|
70
77
|
public get info() {
|
|
71
78
|
return {
|
|
72
79
|
open: this.isOpen,
|
|
73
|
-
identity: this.
|
|
74
|
-
device: this.
|
|
80
|
+
identity: this._identity.identityKey,
|
|
81
|
+
device: this._identity.peerKey,
|
|
75
82
|
};
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
get isConnected() {
|
|
86
|
+
return Boolean(this._ws) && this._ready.state === TriggerState.RESOLVED;
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
get identityKey() {
|
|
79
|
-
return this.
|
|
90
|
+
return this._identity.identityKey;
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
get peerKey() {
|
|
83
|
-
return this.
|
|
94
|
+
return this._identity.peerKey;
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
setIdentity(
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
setIdentity(identity: EdgeIdentity) {
|
|
98
|
+
if (identity.identityKey !== this._identity.identityKey || identity.peerKey !== this._identity.peerKey) {
|
|
99
|
+
log('Edge identity changed', { identity, oldIdentity: this._identity });
|
|
100
|
+
this._identity = identity;
|
|
101
|
+
this._persistentLifecycle.scheduleRestart();
|
|
102
|
+
}
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
public addListener(listener: MessageListener): () => void {
|
|
@@ -108,17 +121,25 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
108
121
|
* Close connection and free resources.
|
|
109
122
|
*/
|
|
110
123
|
protected override async _close() {
|
|
111
|
-
log('closing...', { peerKey: this.
|
|
124
|
+
log('closing...', { peerKey: this._identity.peerKey });
|
|
112
125
|
await this._persistentLifecycle.close();
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
private async _openWebSocket() {
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
if (this._ctx.disposed) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const path = `/ws/${this._identity.identityKey}/${this._identity.peerKey}`;
|
|
133
|
+
const protocolHeader = this._config.disableAuth ? undefined : await this._createAuthHeader(path);
|
|
134
|
+
|
|
135
|
+
const url = new URL(path, this._baseWsUrl);
|
|
136
|
+
log('Opening websocket', { url: url.toString(), protocolHeader });
|
|
137
|
+
this._ws = new WebSocket(url, protocolHeader ? [protocolHeader] : []);
|
|
118
138
|
|
|
119
139
|
this._ws.onopen = () => {
|
|
120
140
|
log('opened', this.info);
|
|
121
141
|
this._ready.wake();
|
|
142
|
+
this.connected.emit();
|
|
122
143
|
};
|
|
123
144
|
this._ws.onclose = () => {
|
|
124
145
|
log('closed', this.info);
|
|
@@ -138,7 +159,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
138
159
|
}
|
|
139
160
|
const data = await toUint8Array(event.data);
|
|
140
161
|
const message = buf.fromBinary(MessageSchema, data);
|
|
141
|
-
log('received', { peerKey: this.
|
|
162
|
+
log('received', { peerKey: this._identity.peerKey, payload: protocol.getPayloadType(message) });
|
|
142
163
|
if (message) {
|
|
143
164
|
for (const listener of this._listeners) {
|
|
144
165
|
try {
|
|
@@ -150,7 +171,11 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
150
171
|
}
|
|
151
172
|
};
|
|
152
173
|
|
|
174
|
+
// TODO(dmaretskyi): Potential race condition here since web socket errors don't resolve this trigger.
|
|
153
175
|
await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
|
|
176
|
+
log('Websocket is ready', { identity: this._identity.identityKey, peer: this._identity.peerKey });
|
|
177
|
+
|
|
178
|
+
// TODO(dmaretskyi): Potential leak: context re-assigned without disposing the previous one.
|
|
154
179
|
this._keepaliveCtx = new Context();
|
|
155
180
|
scheduleTaskInterval(
|
|
156
181
|
this._keepaliveCtx,
|
|
@@ -170,7 +195,7 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
170
195
|
return;
|
|
171
196
|
}
|
|
172
197
|
try {
|
|
173
|
-
this._ready.throw(new
|
|
198
|
+
this._ready.throw(this.isOpen ? new EdgeIdentityChangedError() : new EdgeConnectionClosedError());
|
|
174
199
|
this._ready.reset();
|
|
175
200
|
void this._keepaliveCtx?.dispose();
|
|
176
201
|
this._keepaliveCtx = undefined;
|
|
@@ -197,11 +222,20 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
197
222
|
*/
|
|
198
223
|
public async send(message: Message): Promise<void> {
|
|
199
224
|
if (this._ready.state !== TriggerState.RESOLVED) {
|
|
225
|
+
log('waiting for websocket to become ready');
|
|
200
226
|
await this._ready.wait({ timeout: this._config.timeout ?? DEFAULT_TIMEOUT });
|
|
201
227
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
228
|
+
if (!this._ws) {
|
|
229
|
+
throw new EdgeConnectionClosedError();
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
message.source &&
|
|
233
|
+
(message.source.peerKey !== this._identity.peerKey || message.source.identityKey !== this.identityKey)
|
|
234
|
+
) {
|
|
235
|
+
throw new EdgeIdentityChangedError();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
log('sending...', { peerKey: this._identity.peerKey, payload: protocol.getPayloadType(message) });
|
|
205
239
|
this._ws.send(buf.toBinary(MessageSchema, message));
|
|
206
240
|
}
|
|
207
241
|
|
|
@@ -219,4 +253,22 @@ export class EdgeClient extends Resource implements EdgeConnection {
|
|
|
219
253
|
2 * SIGNAL_KEEPALIVE_INTERVAL,
|
|
220
254
|
);
|
|
221
255
|
}
|
|
256
|
+
|
|
257
|
+
private async _createAuthHeader(path: string): Promise<string | undefined> {
|
|
258
|
+
const httpUrl = new URL(path, this._baseHttpUrl);
|
|
259
|
+
httpUrl.protocol = getEdgeUrlWithProtocol(this._baseWsUrl.toString(), 'http');
|
|
260
|
+
const response = await fetch(httpUrl, { method: 'GET' });
|
|
261
|
+
if (response.status === 401) {
|
|
262
|
+
return encodePresentationWsAuthHeader(await handleAuthChallenge(response, this._identity));
|
|
263
|
+
} else {
|
|
264
|
+
log.warn('no auth challenge from edge', { status: response.status, statusText: response.statusText });
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
222
268
|
}
|
|
269
|
+
|
|
270
|
+
const encodePresentationWsAuthHeader = (encodedPresentation: Uint8Array): string => {
|
|
271
|
+
// = and / characters are not allowed in the WebSocket subprotocol header.
|
|
272
|
+
const encodedToken = Buffer.from(encodedPresentation).toString('base64').replace(/=*$/, '').replaceAll('/', '|');
|
|
273
|
+
return `base64url.bearer.authorization.dxos.org.${encodedToken}`;
|
|
274
|
+
};
|