@dxos/edge-client 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.
Files changed (67) hide show
  1. package/dist/lib/browser/chunk-ZWJXA37R.mjs +113 -0
  2. package/dist/lib/browser/chunk-ZWJXA37R.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +802 -286
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +128 -0
  7. package/dist/lib/browser/testing/index.mjs.map +7 -0
  8. package/dist/lib/node/chunk-ANV2HBEH.cjs +136 -0
  9. package/dist/lib/node/chunk-ANV2HBEH.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +798 -283
  11. package/dist/lib/node/index.cjs.map +4 -4
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +158 -0
  14. package/dist/lib/node/testing/index.cjs.map +7 -0
  15. package/dist/lib/node-esm/chunk-HNVT57AU.mjs +115 -0
  16. package/dist/lib/node-esm/chunk-HNVT57AU.mjs.map +7 -0
  17. package/dist/lib/node-esm/index.mjs +994 -0
  18. package/dist/lib/node-esm/index.mjs.map +7 -0
  19. package/dist/lib/node-esm/meta.json +1 -0
  20. package/dist/lib/node-esm/testing/index.mjs +129 -0
  21. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  22. package/dist/types/src/auth.d.ts +22 -0
  23. package/dist/types/src/auth.d.ts.map +1 -0
  24. package/dist/types/src/defs.d.ts.map +1 -1
  25. package/dist/types/src/edge-client.d.ts +30 -26
  26. package/dist/types/src/edge-client.d.ts.map +1 -1
  27. package/dist/types/src/edge-http-client.d.ts +48 -0
  28. package/dist/types/src/edge-http-client.d.ts.map +1 -0
  29. package/dist/types/src/edge-identity.d.ts +15 -0
  30. package/dist/types/src/edge-identity.d.ts.map +1 -0
  31. package/dist/types/src/edge-ws-connection.d.ts +30 -0
  32. package/dist/types/src/edge-ws-connection.d.ts.map +1 -0
  33. package/dist/types/src/errors.d.ts +4 -1
  34. package/dist/types/src/errors.d.ts.map +1 -1
  35. package/dist/types/src/index.d.ts +4 -0
  36. package/dist/types/src/index.d.ts.map +1 -1
  37. package/dist/types/src/persistent-lifecycle.d.ts +7 -5
  38. package/dist/types/src/persistent-lifecycle.d.ts.map +1 -1
  39. package/dist/types/src/protocol.d.ts +2 -2
  40. package/dist/types/src/protocol.d.ts.map +1 -1
  41. package/dist/types/src/testing/index.d.ts +2 -0
  42. package/dist/types/src/testing/index.d.ts.map +1 -0
  43. package/dist/types/src/testing/test-utils.d.ts +22 -0
  44. package/dist/types/src/testing/test-utils.d.ts.map +1 -0
  45. package/dist/types/src/utils.d.ts +2 -0
  46. package/dist/types/src/utils.d.ts.map +1 -0
  47. package/package.json +27 -17
  48. package/src/auth.ts +135 -0
  49. package/src/defs.ts +2 -3
  50. package/src/edge-client.test.ts +144 -25
  51. package/src/edge-client.ts +181 -127
  52. package/src/edge-http-client.ts +213 -0
  53. package/src/edge-identity.ts +31 -0
  54. package/src/edge-ws-connection.ts +148 -0
  55. package/src/errors.ts +8 -2
  56. package/src/index.ts +4 -0
  57. package/src/persistent-lifecycle.test.ts +2 -2
  58. package/src/persistent-lifecycle.ts +26 -11
  59. package/src/protocol.test.ts +1 -2
  60. package/src/protocol.ts +2 -2
  61. package/src/testing/index.ts +5 -0
  62. package/src/testing/test-utils.ts +117 -0
  63. package/src/utils.ts +10 -0
  64. package/src/websocket.test.ts +5 -4
  65. package/dist/types/src/test-utils.d.ts +0 -11
  66. package/dist/types/src/test-utils.d.ts.map +0 -1
  67. package/src/test-utils.ts +0 -49
@@ -1,14 +1,14 @@
1
1
  import { Resource } from '@dxos/context';
2
- export type PersistentLifecycleParams = {
2
+ export type PersistentLifecycleParams<T> = {
3
3
  /**
4
4
  * Create connection.
5
5
  * If promise resolves successfully, connection is considered established.
6
6
  */
7
- start: () => Promise<void>;
7
+ start: () => Promise<T | undefined>;
8
8
  /**
9
9
  * Reset connection to initial state.
10
10
  */
11
- stop: () => Promise<void>;
11
+ stop: (state: T) => Promise<void>;
12
12
  /**
13
13
  * Called after successful start.
14
14
  */
@@ -23,17 +23,19 @@ export type PersistentLifecycleParams = {
23
23
  * Handles restarts (e.g. persists connection).
24
24
  * Restarts are scheduled with exponential backoff.
25
25
  */
26
- export declare class PersistentLifecycle extends Resource {
26
+ export declare class PersistentLifecycle<T> extends Resource {
27
27
  private readonly _start;
28
28
  private readonly _stop;
29
29
  private readonly _onRestart?;
30
30
  private readonly _maxRestartDelay;
31
+ private _currentContext;
31
32
  private _restartTask?;
32
33
  private _restartAfter;
33
- constructor({ start, stop, onRestart, maxRestartDelay }: PersistentLifecycleParams);
34
+ constructor({ start, stop, onRestart, maxRestartDelay }: PersistentLifecycleParams<T>);
34
35
  protected _open(): Promise<void>;
35
36
  protected _close(): Promise<void>;
36
37
  private _restart;
38
+ private _stopCurrentContext;
37
39
  /**
38
40
  * Scheduling restart should be done from outside.
39
41
  */
@@ -1 +1 @@
1
- {"version":3,"file":"persistent-lifecycle.d.ts","sourceRoot":"","sources":["../../../src/persistent-lifecycle.ts"],"names":[],"mappings":"AAKA,OAAO,EAAqC,QAAQ,EAAE,MAAM,eAAe,CAAC;AAO5E,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;OAGG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3B;;OAEG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,QAAQ;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,OAAO,CAAC,YAAY,CAAC,CAA2B;IAChD,OAAO,CAAC,aAAa,CAAK;gBAEd,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,eAA2C,EAAE,EAAE,yBAAyB;cASrF,KAAK;cAeL,MAAM;YAMjB,QAAQ;IAgBtB;;OAEG;IAEH,eAAe;CAMhB"}
1
+ {"version":3,"file":"persistent-lifecycle.d.ts","sourceRoot":"","sources":["../../../src/persistent-lifecycle.ts"],"names":[],"mappings":"AAKA,OAAO,EAAqC,QAAQ,EAAE,MAAM,eAAe,CAAC;AAO5E,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IACzC;;;OAGG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAEpC;;OAEG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;GAGG;AACH,qBAAa,mBAAmB,CAAC,CAAC,CAAE,SAAQ,QAAQ;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C,OAAO,CAAC,eAAe,CAA4B;IACnD,OAAO,CAAC,YAAY,CAAC,CAA2B;IAChD,OAAO,CAAC,aAAa,CAAK;gBAEd,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,eAA2C,EAAE,EAAE,yBAAyB,CAAC,CAAC,CAAC;cASxF,KAAK;cAgBL,MAAM;YAMjB,QAAQ;YAkBR,mBAAmB;IAWjC;;OAEG;IAEH,eAAe;CAMhB"}
@@ -1,6 +1,6 @@
1
1
  import { buf } from '@dxos/protocols/buf';
2
- import { type Message, type Peer as PeerProto } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
3
- export type PeerData = Partial<PeerProto>;
2
+ import { type Message, type PeerSchema } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
3
+ export type PeerData = buf.MessageInitShape<typeof PeerSchema>;
4
4
  export declare const getTypename: (typeName: string) => string;
5
5
  /**
6
6
  * NOTE: The type registry should be extended with all message types.
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/protocol.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAU,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,KAAK,OAAO,EAAiB,KAAK,IAAI,IAAI,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAGjH,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;AAE1C,eAAO,MAAM,WAAW,aAAc,MAAM,WAAsC,CAAC;AAEnF;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;gBAEjC,KAAK,EAAE,GAAG,CAAC,WAAW,EAAE;IAIpC,IAAI,YAAY,IAAI,GAAG,CAAC,QAAQ,CAE/B;IAED,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,GAAG;IAQ7B;;OAEG;IACH,UAAU,CAAC,IAAI,SAAS,GAAG,CAAC,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC;IAa9F;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IASpD;;OAEG;IACH,aAAa,CAAC,IAAI,SAAS,GAAG,CAAC,WAAW,EACxC,IAAI,EAAE,IAAI,EACV,EACE,MAAM,EACN,MAAM,EACN,OAAO,EACP,SAAS,GACV,EAAE;QACD,MAAM,CAAC,EAAE,QAAQ,CAAC;QAClB,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;CAUJ;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,SAAgB,GAAG,KAAG,OAAO,CAAC,UAAU,CAYhE,CAAC"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/protocol.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAU,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,KAAK,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,4CAA4C,CAAC;AAG1G,MAAM,MAAM,QAAQ,GAAG,GAAG,CAAC,gBAAgB,CAAC,OAAO,UAAU,CAAC,CAAC;AAE/D,eAAO,MAAM,WAAW,aAAc,MAAM,WAAsC,CAAC;AAEnF;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;gBAEjC,KAAK,EAAE,GAAG,CAAC,WAAW,EAAE;IAIpC,IAAI,YAAY,IAAI,GAAG,CAAC,QAAQ,CAE/B;IAED,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,GAAG;IAQ7B;;OAEG;IACH,UAAU,CAAC,IAAI,SAAS,GAAG,CAAC,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC;IAa9F;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IASpD;;OAEG;IACH,aAAa,CAAC,IAAI,SAAS,GAAG,CAAC,WAAW,EACxC,IAAI,EAAE,IAAI,EACV,EACE,MAAM,EACN,MAAM,EACN,OAAO,EACP,SAAS,GACV,EAAE;QACD,MAAM,CAAC,EAAE,QAAQ,CAAC;QAClB,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;CAUJ;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,SAAgB,GAAG,KAAG,OAAO,CAAC,UAAU,CAYhE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './test-utils';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/testing/index.ts"],"names":[],"mappings":"AAIA,cAAc,cAAc,CAAC"}
@@ -0,0 +1,22 @@
1
+ import WebSocket from 'isomorphic-ws';
2
+ import { Trigger } from '@dxos/async';
3
+ import { type Message } from '@dxos/protocols/buf/dxos/edge/messenger_pb';
4
+ export declare const DEFAULT_PORT = 8080;
5
+ type TestEdgeWsServerParams = {
6
+ admitConnection?: Trigger;
7
+ payloadDecoder?: (payload: Uint8Array) => any;
8
+ messageHandler?: (payload: any) => Promise<Uint8Array | undefined>;
9
+ };
10
+ export declare const createTestEdgeWsServer: (port?: number, params?: TestEdgeWsServerParams) => Promise<{
11
+ server: WebSocket.Server;
12
+ messageSink: any[];
13
+ messageSourceLog: any[];
14
+ endpoint: string;
15
+ cleanup: () => void;
16
+ currentConnection: () => WebSocket | undefined;
17
+ sendResponseMessage: (request: Message, responsePayload: Uint8Array) => void;
18
+ sendMessage: (msg: Message) => void;
19
+ closeConnection: () => Promise<void>;
20
+ }>;
21
+ export {};
22
+ //# sourceMappingURL=test-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../../../../src/testing/test-utils.ts"],"names":[],"mappings":"AAIA,OAAO,SAAS,MAAM,eAAe,CAAC;AAEtC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAGtC,OAAO,EAAoC,KAAK,OAAO,EAAE,MAAM,4CAA4C,CAAC;AAK5G,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,KAAK,sBAAsB,GAAG;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,GAAG,CAAC;IAC9C,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;CACpE,CAAC;AAEF,eAAO,MAAM,sBAAsB,2BAAwC,sBAAsB;;;;;;;mCAsE9E,OAAO,mBAAmB,UAAU;uBA1BhC,OAAO;;EAS7B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const getEdgeUrlWithProtocol: (baseUrl: string, protocol: "http" | "ws") => string;
2
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/utils.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,YAAa,MAAM,YAAY,MAAM,GAAG,IAAI,WAK9E,CAAC"}
package/package.json CHANGED
@@ -1,23 +1,31 @@
1
1
  {
2
2
  "name": "@dxos/edge-client",
3
- "version": "0.6.13",
3
+ "version": "0.6.14-main.1366248",
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
- "default": "./dist/lib/node/index.cjs"
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/async": "0.6.13",
32
- "@dxos/context": "0.6.13",
33
- "@dxos/debug": "0.6.13",
34
- "@dxos/log": "0.6.13",
35
- "@dxos/invariant": "0.6.13",
36
- "@dxos/node-std": "0.6.13",
37
- "@dxos/protocols": "0.6.13",
38
- "@dxos/util": "0.6.13"
38
+ "@dxos/async": "0.6.14-main.1366248",
39
+ "@dxos/context": "0.6.14-main.1366248",
40
+ "@dxos/crypto": "0.6.14-main.1366248",
41
+ "@dxos/debug": "0.6.14-main.1366248",
42
+ "@dxos/invariant": "0.6.14-main.1366248",
43
+ "@dxos/credentials": "0.6.14-main.1366248",
44
+ "@dxos/keyring": "0.6.14-main.1366248",
45
+ "@dxos/log": "0.6.14-main.1366248",
46
+ "@dxos/node-std": "0.6.14-main.1366248",
47
+ "@dxos/keys": "0.6.14-main.1366248",
48
+ "@dxos/protocols": "0.6.14-main.1366248",
49
+ "@dxos/util": "0.6.14-main.1366248"
39
50
  },
40
51
  "devDependencies": {
41
- "jest-websocket-mock": "^2.5.0",
42
- "@dxos/keys": "0.6.13"
52
+ "@dxos/test-utils": "0.6.14-main.1366248"
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 { AnySchema } from '@bufbuild/protobuf/wkt';
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]);
@@ -2,49 +2,168 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import chai, { expect } from 'chai';
6
- import chaiAsPromised from 'chai-as-promised';
5
+ import { describe, expect, onTestFinished, test } from 'vitest';
7
6
 
8
- import { PublicKey } from '@dxos/keys';
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 { test, describe, openAndClose } from '@dxos/test';
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 { createTestWsServer } from './test-utils';
15
-
16
- chai.use(chaiAsPromised);
15
+ import { type EdgeIdentity } from './edge-identity';
16
+ import { EdgeConnectionClosedError, EdgeIdentityChangedError } from './errors';
17
+ import { createTestEdgeWsServer } from './testing';
17
18
 
18
19
  describe('EdgeClient', () => {
19
- const textMessage = (message: string) => protocol.createMessage(TextMessageSchema, { payload: { message } });
20
+ let wsServerPort = 8001;
20
21
 
21
22
  test('reconnects on error', async () => {
22
- const { error: serverError, endpoint } = await createTestWsServer();
23
- const id = PublicKey.random().toHex();
24
- const client = new EdgeClient(id, id, { socketEndpoint: endpoint });
25
- await openAndClose(client);
23
+ const { closeConnection, endpoint, cleanup } = await createTestEdgeWsServer(wsServerPort++);
24
+ onTestFinished(cleanup);
25
+
26
+ const { client, reconnectTrigger } = await openNewClient(endpoint);
26
27
  await client.send(textMessage('Hello world 1'));
27
28
  expect(client.isOpen).is.true;
28
29
 
29
- const reconnected = client.reconnect.waitForCount(1);
30
- await serverError();
31
- await reconnected;
32
- await expect(client.send(textMessage('Hello world 2'))).to.be.fulfilled;
30
+ reconnectTrigger.reset();
31
+ await closeConnection();
32
+ await reconnectTrigger.wait();
33
+ await expect(client.send(textMessage('Hello world 2'))).resolves.not.toThrow();
34
+ });
35
+
36
+ test('isConnected', async () => {
37
+ const admitConnection = new Trigger();
38
+ const { closeConnection, endpoint, cleanup } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
39
+ onTestFinished(cleanup);
40
+
41
+ const { client } = await openNewClient(endpoint);
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 createTestWsServer();
57
+ const { endpoint, cleanup } = await createTestEdgeWsServer(wsServerPort++);
58
+ onTestFinished(cleanup);
37
59
 
38
- const id = PublicKey.random().toHex();
39
- const client = new EdgeClient(id, id, { socketEndpoint: endpoint });
40
- await openAndClose(client);
60
+ const { client, reconnectTrigger } = await openNewClient(endpoint);
41
61
  await client.send(textMessage('Hello world 1'));
42
62
  expect(client.isOpen).is.true;
43
63
 
44
- const newId = PublicKey.random().toHex();
45
- const reconnected = client.reconnect.waitForCount(1);
46
- client.setIdentity({ peerKey: newId, identityKey: newId });
47
- await reconnected;
48
- await expect(client.send(textMessage('Hello world 2'))).to.be.fulfilled;
64
+ reconnectTrigger.reset();
65
+ client.setIdentity(await createEphemeralEdgeIdentity());
66
+ await reconnectTrigger.wait();
67
+ await expect(client.send(textMessage('Hello world 2'))).resolves.not.toThrow();
49
68
  });
69
+
70
+ test('send blocks until connection becomes ready', async () => {
71
+ const admitConnection = new Trigger();
72
+ const { endpoint, messageSink, cleanup } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
73
+ onTestFinished(cleanup);
74
+
75
+ const { client } = await openNewClient(endpoint);
76
+ setTimeout(() => admitConnection.wake(), 20);
77
+ await client.send(textMessage('Hello world 1'));
78
+ await expect.poll(() => messageSink.length).toBe(1);
79
+ });
80
+
81
+ test('send fails if identity changes before connection becomes ready', async () => {
82
+ const admitConnection = new Trigger();
83
+ const { endpoint, cleanup, messageSink } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
84
+ onTestFinished(cleanup);
85
+
86
+ const { client } = await openNewClient(endpoint);
87
+ setTimeout(async () => client.setIdentity(await createEphemeralEdgeIdentity()));
88
+ await expect(client.send(textMessage('Hello world 1'))).rejects.toThrow(EdgeIdentityChangedError);
89
+
90
+ // Test recovers.
91
+ setTimeout(() => admitConnection.wake(), 20);
92
+ await client.send(textMessage('Hello world 1'));
93
+ await expect.poll(() => messageSink.length).toBe(1);
94
+ });
95
+
96
+ test('send fails if client is closed before connection becomes ready', async () => {
97
+ const admitConnection = new Trigger();
98
+ const { endpoint, cleanup } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
99
+ onTestFinished(cleanup);
100
+
101
+ const { client } = await openNewClient(endpoint);
102
+ setTimeout(() => client.close());
103
+ await expect(client.send(textMessage('Hello world 1'))).rejects.toThrow(EdgeConnectionClosedError);
104
+ });
105
+
106
+ test('onReconnect trigger', async () => {
107
+ const admitConnection = new Trigger();
108
+ const { endpoint, cleanup, closeConnection } = await createTestEdgeWsServer(wsServerPort++, { admitConnection });
109
+ onTestFinished(cleanup);
110
+
111
+ const { client } = await openNewClient(endpoint);
112
+ let callCount = 0;
113
+ client.onReconnected(() => callCount++);
114
+ admitConnection.wake();
115
+
116
+ await expect.poll(() => callCount).toEqual(1);
117
+ await closeConnection();
118
+ await expect.poll(() => callCount).toEqual(2);
119
+
120
+ const trigger = new Trigger();
121
+ client.onReconnected(() => trigger.wake());
122
+ await trigger.wait();
123
+ expect(callCount).toEqual(2);
124
+ });
125
+
126
+ test('send message right after identity change is delivered successfully with the new identity', async () => {
127
+ const { endpoint, cleanup, messageSourceLog } = await createTestEdgeWsServer(wsServerPort++);
128
+ onTestFinished(cleanup);
129
+
130
+ const { client, identity: oldIdentity } = await openNewClient(endpoint);
131
+ await client.send(textMessage('Hello world 1', oldIdentity));
132
+ expect(client.isOpen).is.true;
133
+
134
+ const newIdentity = await createEphemeralEdgeIdentity();
135
+ client.setIdentity(newIdentity);
136
+ await client.send(textMessage('Hello world 2', newIdentity));
137
+ await expect.poll(() => messageSourceLog.length).toBe(2);
138
+ expect(messageSourceLog.map((m) => m.peerKey)).toStrictEqual([oldIdentity.peerKey, newIdentity.peerKey]);
139
+ });
140
+
141
+ test.skipIf(!process.env.EDGE_ENDPOINT)('connect to local edge server', async () => {
142
+ // const identity = await createEphemeralEdgeIdentity();
143
+
144
+ const keyring = new Keyring();
145
+ const identity = await createTestHaloEdgeIdentity(keyring, await keyring.createKey(), await keyring.createKey());
146
+
147
+ const client = new EdgeClient(identity, { socketEndpoint: process.env.EDGE_ENDPOINT! });
148
+ await openAndClose(client);
149
+ await client.send(textMessage('Hello world 1'));
150
+ expect(client.isOpen).is.true;
151
+ });
152
+
153
+ const textMessage = (message: string, source?: EdgeIdentity) =>
154
+ protocol.createMessage(TextMessageSchema, {
155
+ source: source && { peerKey: source.peerKey, identityKey: source.identityKey },
156
+ payload: { message },
157
+ });
158
+
159
+ const openNewClient = async (endpoint: string) => {
160
+ const identity = await createEphemeralEdgeIdentity();
161
+ const client = new EdgeClient(identity, { socketEndpoint: endpoint });
162
+ await openAndClose(client);
163
+ const reconnectTrigger = new Trigger();
164
+ client.onReconnected(() => {
165
+ reconnectTrigger.wake();
166
+ });
167
+ return { client, reconnectTrigger, identity };
168
+ };
50
169
  });