@tdxvolt/volt-client-grpc 0.17.0 → 0.18.1

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.17.0",
6
+ "version": "0.18.1",
7
7
  "description": "tdx Volt library for nodejs clients",
8
8
  "type": "module",
9
9
  "exports": {
@@ -37,7 +37,7 @@
37
37
  "license": "ISC",
38
38
  "dependencies": {
39
39
  "@grpc/proto-loader": "^0.6.4",
40
- "@tdxvolt/volt-client-web": "^0.17.106",
40
+ "@tdxvolt/volt-client-web": "^0.18.1",
41
41
  "bent": "^7.3.12",
42
42
  "builtin-modules": "^3.3.0",
43
43
  "debug": "^4.1.1",
@@ -2,18 +2,19 @@ syntax = "proto3";
2
2
 
3
3
  package tdx.volt_api.volt.v1;
4
4
 
5
- import "tdx/volt_api/volt/v1/ssi.proto";
6
5
  import "tdx/volt_api/volt/v1/status.proto";
6
+ import "tdx/volt_api/volt/v1/ssi.proto";
7
7
 
8
8
  service SsiAPI {
9
9
  // Delete a DID document or all DID documents originating from a given Volt.
10
- // Requires 'volt:delete-did' API privilege.
10
+ // Requires `volt:delete-did` API privilege.
11
11
  rpc DeleteDID(DeleteDIDRequest) returns (DeleteDIDResponse);
12
12
 
13
13
  // Get all updates to the DID registry since the specified timestamp.
14
14
  // Internal use only.
15
15
  rpc GetDIDRegistryUpdates(GetDIDRegistryUpdatesRequest) returns (GetDIDRegistryUpdatesResponse);
16
16
 
17
+ // Import a verifiable credential.
17
18
  rpc ImportCredential(ImportCredentialRequest) returns (ImportCredentialResponse);
18
19
 
19
20
  // Parse a verifiable credential from a URL or a verifiable presentation.
@@ -23,8 +24,8 @@ service SsiAPI {
23
24
  rpc ResolveDID(ResolveDIDRequest) returns (ResolveDIDResponse);
24
25
 
25
26
  // Register a DID document.
26
- // This is currently reserved for use by the DID registry when synchronising with other registries.
27
- // To register a new DID document, use the VoltAPI Authenticate method.
27
+ // This is intended for use by the DID registry when synchronising with other registries.
28
+ // To register a new DID document, it is recommended to use the VoltAPI Authenticate method.
28
29
  rpc RegisterDIDDocument(RegisterDIDDocumentRequest) returns (RegisterDIDDocumentResponse);
29
30
 
30
31
  // Save a verifiable credential.
@@ -153,7 +154,6 @@ message RegisterDIDDocumentRequest {
153
154
  DIDRegistryUpdate did_update = 2;
154
155
 
155
156
  // When updating an existing DID document, it is necessary to include a signature of the document JSON, signed by the DID document's current owner.
156
- // This is different from the proof contained in the DID document itself.
157
157
  string update_signature = 3;
158
158
  }
159
159
 
@@ -309,12 +309,17 @@ message VoltEndpoint {
309
309
  // The online status of the Volt.
310
310
  OnlineStatus online_status = 11;
311
311
 
312
- // Set to indicate that this Volt acts as a Relay.
312
+ // Indicates that this Volt acts as a Relay.
313
313
  bool has_relay = 12;
314
314
 
315
+ // The API version supported by the endpoint.
315
316
  Version api_version = 13;
316
317
 
318
+ // Optional description of the endpoint.
317
319
  string description = 14;
320
+
321
+ // The list of DID registries that this Volt trusts.
322
+ repeated string did_registry = 15;
318
323
  }
319
324
 
320
325
  // Internal use only.
@@ -306,6 +306,9 @@ message ConnectHello {
306
306
 
307
307
  // The current time on the client.
308
308
  uint64 timestamp = 15;
309
+
310
+ // Optionally specify the DID registries supported.
311
+ repeated string did_registry = 16;
309
312
  }
310
313
 
311
314
  message ConnectAcknowledge {
@@ -22,6 +22,10 @@ message PublishWireRequest {
22
22
 
23
23
  // The chunk of data to publish.
24
24
  bytes chunk = 2;
25
+
26
+ // Whether to persist the chunk.
27
+ // This is only valid if the wire is configured to persist messages, i.e. the `volt:wire-persist` attribute is true.
28
+ bool do_not_persist = 3;
25
29
  }
26
30
 
27
31
  message PublishWireResponse {
package/src/constants.js CHANGED
@@ -1,57 +1,10 @@
1
- // eslint-disable-next-line import/prefer-default-export
2
1
  export const constants = {
3
2
  authTokenName: "volt-token",
4
- collectionOperation: {
5
- add: "ADD",
6
- remove: "REMOVE",
7
- update: "UPDATE"
8
- },
9
- crypto: {
10
- subjectDefaults: [
11
- {
12
- name: "countryName",
13
- value: "GB"
14
- },
15
- {
16
- name: "localityName",
17
- value: "Southampton"
18
- },
19
- {
20
- name: "organizationName",
21
- value: "nquiringminds Ltd"
22
- }
23
- ],
24
- userIdOID: "0.9.2342.19200300.100.1.1"
25
- },
26
- defaultDIDHostName: "tdxvolt.com",
3
+ defaultDIDHostName: "coreid.com",
27
4
  didServiceType: {
28
5
  voltConfig: "volt-discovery"
29
6
  },
30
- identityType: {
31
- did: "did",
32
- volt: "volt"
33
- },
34
- onlineStatus: {
35
- offline: "offline",
36
- online: "online",
37
- unknown: "unknown"
38
- },
39
7
  privateEncryptedKeyPrefix: "-----BEGIN ENCRYPTED PRIVATE KEY-----",
40
- resource: {
41
- identity: "ident",
42
- identityView: "ident-view",
43
- proxy: "proxy",
44
- proxyView: "proxy-view",
45
- vc: "vc",
46
- vcView: "vc-view"
47
- },
48
- voltStatus: {
49
- pending: "",
50
- online: "online",
51
- offline: "offline",
52
- deleted: "deleted",
53
- revoked: "revoked"
54
- },
55
8
  /**
56
9
  * These top-level service types should map to a protobuf definition
57
10
  * in @tdxvolt/volt-proto.
@@ -64,10 +17,5 @@ export const constants = {
64
17
  relayAPI: "tdx.volt_api.relay.v1.RelayAPI",
65
18
  voltAPI: "tdx.volt_api.volt.v1.VoltAPI",
66
19
  wireAPI: "tdx.volt_api.volt.v1.WireAPI"
67
- },
68
- tunnelInvokeMethod: "/tdx.volt_api.volt.v1.VoltAPI/Invoke",
69
- tunnelPingInterval: 10000,
70
- tunnelPingTimeout: 2500,
71
- tunnelReconnectAfter: 30000,
72
- tunnelTimeoutInterval: 10000 * 2 // This must always be greater than `tunnelPingInterval`.
20
+ }
73
21
  };
package/src/grpc-call.js CHANGED
@@ -1,7 +1,9 @@
1
1
  /* eslint-disable no-underscore-dangle */
2
2
  import debug from "debug";
3
3
  import EventEmitter from "events";
4
- import {
4
+ import { voltUtils as voltClientUtils } from "@tdxvolt/volt-client-web/js";
5
+
6
+ const {
5
7
  aesCreateKey,
6
8
  aesEncrypt,
7
9
  aesDecrypt,
@@ -11,7 +13,9 @@ import {
11
13
  verify,
12
14
  deriveSharedKey,
13
15
  x25519
14
- } from "@tdxvolt/volt-client-web";
16
+ } = voltClientUtils;
17
+
18
+ const invokeMethod = "Invoke";
15
19
 
16
20
  const log = debug("volt-client-grpc:grpc-call");
17
21
 
@@ -24,10 +28,10 @@ function _prepareInvokeRequest(
24
28
  this._service?.service_description.host_type ===
25
29
  "SERVICE_HOST_TYPE_RELAYED";
26
30
 
27
- const relaying = this._voltClient.isVoltRelay || isServiceRelayed;
31
+ const relaying = this._voltClient.isRelayed || isServiceRelayed;
28
32
 
29
33
  let targetDID = [];
30
- if (this._voltClient.isVoltRelay) {
34
+ if (this._voltClient.isRelayed) {
31
35
  targetDID.push(this._voltClient.voltConfig.id);
32
36
  }
33
37
 
@@ -52,20 +56,23 @@ function _prepareInvokeRequest(
52
56
  let invokeRequest;
53
57
  let callMethod = method;
54
58
 
59
+ const voltGrpcClient = this._voltClient.getVoltAPIClient();
60
+
55
61
  if (relaying) {
56
62
  //
57
63
  // If the volt connection is via a relay (or the target service is relayed),
58
64
  // we wrap the rpc in a RemoteRequest message and send it via a call to Invoke().
59
65
  //
66
+
60
67
  // Replace the target method with a call to Invoke on the relay Volt.
61
- //
62
- callMethod = "Invoke";
68
+ callMethod = invokeMethod;
63
69
 
64
70
  // But first we need to perform a key exchange with the target service.
65
- //
66
71
  if (!this._encryptionKey) {
67
72
  // We haven't exchanged keys yet, so queue the payload.
68
- this._pendingRequests.push(request);
73
+ if (request) {
74
+ this._pendingRequests.push(request);
75
+ }
69
76
 
70
77
  if (this._keyExchangePending) {
71
78
  // We've already sent the key exchange request, so do nothing.
@@ -84,6 +91,13 @@ function _prepareInvokeRequest(
84
91
  : undefined
85
92
  };
86
93
 
94
+ if (this._voltClient.isRelayed) {
95
+ // When tunnelling, we serialise the request using the original serialisation
96
+ // method for Invoke.
97
+ invokeRequest =
98
+ voltGrpcClient.Invoke.requestSerializeOriginal(invokeRequest);
99
+ }
100
+
87
101
  return {
88
102
  request: invokeRequest,
89
103
  method: callMethod,
@@ -94,41 +108,45 @@ function _prepareInvokeRequest(
94
108
 
95
109
  let requestPayload;
96
110
 
97
- // Serialise the rpc request.
111
+ // Serialise the rpc request using the original serialisation method.
98
112
  if (request) {
99
- requestPayload = this._grpcClient[method].requestSerialize(request);
113
+ if (this._voltClient.isRelayed) {
114
+ requestPayload =
115
+ this._grpcClient[method].requestSerializeOriginal(request);
116
+ } else {
117
+ requestPayload = this._grpcClient[method].requestSerialize(request);
118
+ }
100
119
  } else if (request !== null) {
101
120
  log("****************LOOKOUT****************** - empty request");
102
121
  } else {
103
122
  log("explicit empty request payload");
104
123
  }
105
124
 
106
- // Use this placeholder to serialise the relay request wrapper.
107
- const voltGrpcClient = this._voltClient.getVoltAPIClient();
108
- const tunnelMethod = voltGrpcClient.Tunnel;
109
-
110
- const tunnelResp = {
125
+ // Create a tunnel message containing the method call metadata and request payload.
126
+ const tunnelResp = voltGrpcClient.Tunnel.responseSerialize({
111
127
  method_invoke: {
112
128
  method_name: this._grpcClient[method].path,
113
129
  method_type: methodType,
114
130
  request: requestPayload
115
131
  }
116
- };
117
-
118
- let invokePayload;
119
- const iv = aesCreateKey().iv;
132
+ });
120
133
 
121
134
  // When sending via a relay we need to encrypt the payload.
122
- invokePayload = aesEncrypt(
123
- this._encryptionKey,
124
- iv,
125
- tunnelMethod.responseSerialize(tunnelResp)
126
- );
135
+ const iv = aesCreateKey().iv;
136
+ const invokePayload = aesEncrypt(this._encryptionKey, iv, tunnelResp);
127
137
 
138
+ // Construct the Invoke request.
128
139
  invokeRequest = {
129
140
  iv,
130
141
  payload: invokePayload
131
142
  };
143
+
144
+ if (this._voltClient.isRelayed) {
145
+ // When tunnelling, we serialise the request using the original serialisation
146
+ // method for Invoke.
147
+ invokeRequest =
148
+ voltGrpcClient.Invoke.requestSerializeOriginal(invokeRequest);
149
+ }
132
150
  } else {
133
151
  invokeRequest = request;
134
152
  }
@@ -148,32 +166,45 @@ function _preparePayloadRequest(request, invokeInfo) {
148
166
  this._service?.service_description.host_type ===
149
167
  "SERVICE_HOST_TYPE_RELAYED";
150
168
 
151
- let iv;
152
- if (this._voltClient.isVoltRelay || isServiceRelayed) {
153
- iv = aesCreateKey().iv;
154
-
155
- const requestPayload =
156
- this._grpcClient[invokeInfo.methodName].requestSerialize(request);
169
+ if (this._voltClient.isRelayed || isServiceRelayed) {
170
+ let requestPayload;
171
+ if (this._voltClient.isRelayed) {
172
+ // Serialise the rpc request using the original serialisation method.
173
+ requestPayload =
174
+ this._grpcClient[invokeInfo.methodName].requestSerializeOriginal(
175
+ request
176
+ );
177
+ } else {
178
+ requestPayload =
179
+ this._grpcClient[invokeInfo.methodName].requestSerialize(request);
180
+ }
157
181
 
182
+ // Use the Tunnel method to serialise the relay request wrapper, this
158
183
  const voltGrpcClient = this._voltClient.getVoltAPIClient();
159
- const tunnelMethod = voltGrpcClient.Tunnel;
160
184
 
161
- const tunnelResp = {
185
+ // Create a tunnel message containing the method call metadata and request payload.
186
+ const tunnelResp = voltGrpcClient.Tunnel.responseSerialize({
162
187
  method_payload: {
163
188
  payload: requestPayload
164
189
  }
165
- };
190
+ });
166
191
 
167
- const invokePayload = aesEncrypt(
168
- this._encryptionKey,
169
- iv,
170
- tunnelMethod.responseSerialize(tunnelResp)
171
- );
192
+ // Encrypt the payload using the shared key.
193
+ const iv = aesCreateKey().iv;
194
+ const invokePayload = aesEncrypt(this._encryptionKey, iv, tunnelResp);
172
195
 
196
+ // We then serialise the entire request using the original serialisation method for Invoke.
173
197
  payloadRequest = {
174
198
  iv,
175
199
  payload: invokePayload
176
200
  };
201
+
202
+ if (this._voltClient.isRelayed) {
203
+ // When tunnelling, we serialise the request using the original serialisation
204
+ // method for Invoke.
205
+ payloadRequest =
206
+ voltGrpcClient.Invoke.requestSerializeOriginal(payloadRequest);
207
+ }
177
208
  } else {
178
209
  payloadRequest = request;
179
210
  }
@@ -189,7 +220,14 @@ function _parseResponse(method, meta, response) {
189
220
  this._service?.service_description.host_type ===
190
221
  "SERVICE_HOST_TYPE_RELAYED";
191
222
 
192
- if (this._voltClient.isVoltRelay || isServiceRelayed) {
223
+ const voltGrpcClient = this._voltClient.getVoltAPIClient();
224
+
225
+ if (this._voltClient.isRelayed || isServiceRelayed) {
226
+ if (this._voltClient.isRelayed) {
227
+ // Deserialise the incoming response using the original deserialisation method for Invoke.
228
+ response = voltGrpcClient.Invoke.responseDeserializeOriginal(response);
229
+ }
230
+
193
231
  if (response.key_exchange) {
194
232
  const encryptionKey = response.key_exchange.encryption_key;
195
233
  const nonce = response.key_exchange.nonce;
@@ -227,13 +265,24 @@ function _parseResponse(method, meta, response) {
227
265
  decryptedPayload = response.payload;
228
266
  }
229
267
 
230
- const tunnelMethod = this._voltClient.getVoltAPIClient().Tunnel;
231
- const responsePayload = tunnelMethod.requestDeserialize(decryptedPayload);
268
+ // Deserialise the decrypted payload using the Tunnel deserialisation method.
269
+ const responsePayload =
270
+ voltGrpcClient.Tunnel.requestDeserialize(decryptedPayload);
232
271
  if (responsePayload.payload === "method_payload") {
233
272
  try {
234
- invokeResponse.payload = this._grpcClient[method].responseDeserialize(
235
- responsePayload.method_payload.payload
236
- );
273
+ // Deserialise the method payload.
274
+ if (this._voltClient.isRelayed) {
275
+ // Use the original deserialisation method for the target method.
276
+ invokeResponse.payload = this._grpcClient[
277
+ method
278
+ ].responseDeserializeOriginal(
279
+ responsePayload.method_payload.payload
280
+ );
281
+ } else {
282
+ invokeResponse.payload = this._grpcClient[
283
+ method
284
+ ].responseDeserialize(responsePayload.method_payload.payload);
285
+ }
237
286
  } catch (err) {
238
287
  log("failure deserialising payload for %s", this._methodName);
239
288
  throw new Error(
@@ -261,16 +310,6 @@ function _parseResponse(method, meta, response) {
261
310
  invokeResponse.methodId = response.invoke_id;
262
311
  log("started method %d for %s", invokeResponse.methodId, method);
263
312
  }
264
- } else if (this._voltClient.isRemote && this._encryptionKey) {
265
- const decryptedPayload = aesDecrypt(
266
- this._encryptionKey,
267
- meta.sharedKey.iv,
268
- response
269
- );
270
- invokeResponse = {
271
- payload:
272
- this._grpcClient[method].responseDeserializeOriginal(decryptedPayload)
273
- };
274
313
  } else {
275
314
  invokeResponse = { payload: response };
276
315
  }
@@ -310,7 +349,7 @@ export default class GRPCCall extends EventEmitter {
310
349
  "SERVICE_HOST_TYPE_RELAYED";
311
350
 
312
351
  let callClient;
313
- if (this._voltClient.isVoltRelay || isServiceRelayed) {
352
+ if (this._voltClient.isRelayed || isServiceRelayed) {
314
353
  callClient = this._voltClient.getVoltAPIClient();
315
354
  } else {
316
355
  callClient = this._grpcClient;
@@ -377,7 +416,11 @@ export default class GRPCCall extends EventEmitter {
377
416
  });
378
417
 
379
418
  this._call.on("error", (err) => {
380
- log("ERROR - intercepted stream error %s", err.message);
419
+ log(
420
+ "ERROR - intercepted stream error on %s: %s",
421
+ this._methodName,
422
+ err.message
423
+ );
381
424
  this.emit("error", err);
382
425
  });
383
426
 
@@ -413,6 +456,16 @@ export default class GRPCCall extends EventEmitter {
413
456
  }
414
457
  }
415
458
 
459
+ pause() {
460
+ log("pausing call %s", this._methodName);
461
+ return this._call?.pause();
462
+ }
463
+
464
+ resume() {
465
+ log("resuming call %s", this._methodName);
466
+ return this._call?.resume();
467
+ }
468
+
416
469
  get writable() {
417
470
  return this._call?.writable;
418
471
  }
package/src/grpc-utils.js CHANGED
@@ -20,7 +20,7 @@ export function createClient(
20
20
  ) {
21
21
  let grpcCredentials;
22
22
  if (credentials) {
23
- const relaying = !!credentials.config?.volt?.relay;
23
+ const relaying = !!credentials.config?.volt?.relay && !Array.isArray(credentials.config?.volt?.relay);
24
24
  const ca = relaying
25
25
  ? credentials.config?.volt?.relay?.ca_pem
26
26
  : credentials.cache.ca;
@@ -28,8 +28,8 @@ export function createClient(
28
28
  const { key } = credentials.cache;
29
29
 
30
30
  if (key && cert) {
31
- const cryyptoOptions = { ca, cert, key };
32
- const tlsOptions = createSecureContextOptions(cryyptoOptions);
31
+ const cryptoOptions = { ca, cert, key };
32
+ const tlsOptions = createSecureContextOptions(cryptoOptions);
33
33
  grpcCredentials = grpc.credentials.createSsl(
34
34
  tlsOptions.ca,
35
35
  tlsOptions.key,
@@ -1,7 +1,9 @@
1
1
  import debug from "debug";
2
2
  import jwt from "jsonwebtoken";
3
3
  import EventEmitter from "events";
4
- import {
4
+ import { voltUtils as voltClientUtils } from "@tdxvolt/volt-client-web/js";
5
+
6
+ const {
5
7
  aesDecrypt,
6
8
  aesEncrypt,
7
9
  createEncryptionKey,
@@ -13,23 +15,26 @@ import {
13
15
  publicKeyToPem,
14
16
  randomBytes,
15
17
  signBase64,
16
- stripPEMHeaders,
18
+ stripPemHeaders,
17
19
  x25519
18
- } from "@tdxvolt/volt-client-web";
20
+ } = voltClientUtils;
19
21
 
20
- const log = debug("rpc-invocation");
22
+ const log = debug("volt-client-grpc:rpc-invocation");
21
23
 
22
24
  class RpcInvocation extends EventEmitter {
23
25
  #voltClient = null;
24
26
  #voltConnection = null;
25
27
  #token = null;
28
+ #callerPublicKeyPem = null;
26
29
  #invokeId = null;
30
+ #serviceId = null;
27
31
  #payload = null;
28
32
  #isJSON = false;
29
33
  #methodDescriptor = null;
30
34
  #methodName = null;
31
35
  #derivedKey = null;
32
36
  #started = false;
37
+ #endEmitted = false;
33
38
 
34
39
  constructor(voltClient, voltConnection) {
35
40
  super();
@@ -41,6 +46,18 @@ class RpcInvocation extends EventEmitter {
41
46
  return this.#invokeId;
42
47
  }
43
48
 
49
+ get serviceId() {
50
+ return this.#serviceId;
51
+ }
52
+
53
+ get token() {
54
+ return this.#token;
55
+ }
56
+
57
+ get callerPublicKeyPem() {
58
+ return this.#callerPublicKeyPem;
59
+ }
60
+
44
61
  get payload() {
45
62
  if (!this.#payload) {
46
63
  return null;
@@ -82,14 +99,23 @@ class RpcInvocation extends EventEmitter {
82
99
  // The rpc implementation should do the verification using the Volt API (e.g. CanAccessResource).
83
100
  this.#token = jwt.decode(token);
84
101
 
102
+ if (!this.#token) {
103
+ log("failed to decode token %s", token || "null");
104
+ return Promise.reject(new Error("failed to decode token"));
105
+ }
106
+
85
107
  // Store the JWT key id (kid) for use in the response.
86
- const callerPublicKeyPem = formatPEM(this.#token.k, "PUBLIC KEY");
87
- const callerKey = keyFromPem(callerPublicKeyPem, "", x25519).publicKey;
108
+ this.#callerPublicKeyPem = formatPEM(this.#token.k, "PUBLIC KEY");
109
+ const callerKey = keyFromPem(
110
+ this.#callerPublicKeyPem,
111
+ "",
112
+ x25519
113
+ ).publicKey;
88
114
 
89
115
  const ephemeralKeyPem = createEncryptionKey();
90
116
  const ephemeralKey = keyFromPem(ephemeralKeyPem, "", x25519);
91
117
  const ephemeralPublicKey = fromBase64(
92
- stripPEMHeaders(publicKeyToPem(ephemeralKey.publicKey, x25519))
118
+ stripPemHeaders(publicKeyToPem(ephemeralKey.publicKey, x25519))
93
119
  );
94
120
 
95
121
  this.#derivedKey = deriveSharedKey(
@@ -106,7 +132,7 @@ class RpcInvocation extends EventEmitter {
106
132
  message.set(nonce, ephemeralPublicKey.length);
107
133
 
108
134
  const signature = signBase64(
109
- this.#voltClient.credential.getKey().privateKey,
135
+ this.#voltClient.credential.cache.key,
110
136
  message
111
137
  );
112
138
 
@@ -127,7 +153,10 @@ class RpcInvocation extends EventEmitter {
127
153
  }
128
154
 
129
155
  initialise(invoke_request) {
130
- this.#invokeId = invoke_request.invoke_id;
156
+ if (!this.#invokeId) {
157
+ this.#invokeId = invoke_request.invoke_id;
158
+ this.#serviceId = invoke_request.target_service_id;
159
+ }
131
160
 
132
161
  if (!this.#derivedKey) {
133
162
  // We don't have the derived key yet, so we need to perform the key exchange.
@@ -149,8 +178,7 @@ class RpcInvocation extends EventEmitter {
149
178
 
150
179
  if (invoke_request.payload) {
151
180
  let decryptedPayload;
152
-
153
- if (encryptIV) {
181
+ if (encryptIV?.length) {
154
182
  // Decrypt the actual payload using the shared key and iv.
155
183
  decryptedPayload = aesDecrypt(
156
184
  this.#derivedKey,
@@ -218,8 +246,13 @@ class RpcInvocation extends EventEmitter {
218
246
  );
219
247
  this.emit("payload", this);
220
248
  } else if (wrappedPayload.method_end) {
221
- // We don't need to do anything here, since we notify the client when we receive the client_end.
222
249
  log("method_end received");
250
+ if (!this.#endEmitted) {
251
+ this.#endEmitted = true;
252
+ this.emit("end", this);
253
+ } else {
254
+ log("method_end: end already emitted");
255
+ }
223
256
  }
224
257
  } catch (err) {
225
258
  log("JSON.parse failure parsing json_payload: %s", err.message);
@@ -228,7 +261,12 @@ class RpcInvocation extends EventEmitter {
228
261
 
229
262
  resolve();
230
263
  } else if (invoke_request.client_end) {
231
- this.emit("end", this);
264
+ if (!this.#endEmitted) {
265
+ this.#endEmitted = true;
266
+ this.emit("end", this);
267
+ } else {
268
+ log("client_end: end already emitted");
269
+ }
232
270
  } else {
233
271
  // Do we need to support json_payload here?
234
272
  reject(new Error("No payload in invoke request"));
package/src/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import ip from "ip";
2
2
  import _ from "lodash";
3
- export * as crypto from "@tdxvolt/volt-client-web";
3
+ export { voltUtils as crypto } from "@tdxvolt/volt-client-web/js";
4
4
 
5
5
  import { constants } from "./constants.js";
6
6