@tdxvolt/volt-client-grpc 0.14.60 → 0.14.134

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.14.60",
6
+ "version": "0.14.134",
7
7
  "description": "tdx Volt library for nodejs clients",
8
8
  "type": "module",
9
9
  "exports": {
@@ -8,7 +8,7 @@ import "tdx/volt_api/volt/v1/file.proto";
8
8
  // The File API exposes basic file management functions.
9
9
  service FileAPI {
10
10
 
11
- // A server-stream to download contents from a file resource.
11
+ // Download from file resource.
12
12
  rpc DownloadFile(DownloadFileRequest) returns (stream DownloadFileResponse);
13
13
 
14
14
  // Get file resource metadata.
@@ -787,8 +787,10 @@ message InvokeRequest {
787
787
  // Indicates the client has ended the invocation.
788
788
  bool client_end = 6;
789
789
 
790
+ // Reserved for internal use.
790
791
  uint32 hop_index = 7;
791
792
 
793
+ // Reserved for internal use.
792
794
  string target_service_id = 8;
793
795
  }
794
796
 
@@ -6,7 +6,7 @@ import "tdx/volt_api/volt/v1/status.proto";
6
6
 
7
7
  // The Wire API allows clients to subscribe and publish to Volt wire resources.
8
8
  service WireAPI {
9
- // Establishes a bi-directional streaming call to the wire resource.
9
+ // Establishes a client-streaming call to the wire resource.
10
10
  rpc PublishWire(stream PublishWireRequest) returns (stream PublishWireResponse);
11
11
 
12
12
  // Establishes a bi-directional streaming call to the wire resource.
package/src/grpc-call.js CHANGED
@@ -10,16 +10,31 @@ function _prepareInvokeRequest(
10
10
  method,
11
11
  methodType = "METHOD_TYPE_UNARY",
12
12
  ) {
13
- const meta = this._voltClient.credential.getIdentityMetadata(
13
+ const isServiceRelayed =
14
+ this._service?.service_description.host_type ===
15
+ "SERVICE_HOST_TYPE_RELAYED";
16
+
17
+ // Use the service's host client id and public key if the service is relayed.
18
+ const methodToken = this._voltClient.credential.getIdentityMetadata(
14
19
  this._voltClient.grpc,
15
- this._voltClient.voltConfig.id,
16
- this._voltClient.isRemote,
20
+ this._service?.service_description.host_client_id ||
21
+ this._voltClient.voltConfig.id,
22
+ this._service?.service_description.host_public_key ||
23
+ this._voltClient.credential.voltPublicKey,
24
+ true,
17
25
  );
26
+
18
27
  let invokeRequest;
19
28
  let callMethod = method;
20
29
 
21
- if (this._voltClient.isVoltRelay) {
30
+ if (this._voltClient.isVoltRelay || isServiceRelayed) {
31
+ //
32
+ // If the volt connection is via a relay (or the target service is relayed),
33
+ // we wrap the rpc in a RemoteRequest message and send it via a call to Invoke().
34
+ //
22
35
  let requestPayload;
36
+
37
+ // Serialise the rpc request.
23
38
  if (request) {
24
39
  requestPayload = this._grpcClient[method].requestSerialize(request);
25
40
  } else if (request !== null) {
@@ -28,45 +43,61 @@ function _prepareInvokeRequest(
28
43
  log("explicit empty request payload");
29
44
  }
30
45
 
31
- const tunnelMethod = this._grpcClient.Tunnel;
46
+ // Use this placeholder to serialise the relay request wrapper.
47
+ const voltGrpcClient = this._voltClient.getVoltAPIClient();
48
+ const tunnelMethod = voltGrpcClient.Tunnel;
32
49
 
33
50
  const tunnelResp = {
34
51
  method_invoke: {
35
52
  method_name: this._grpcClient[method].path,
36
53
  method_type: methodType,
37
- token: meta.token,
54
+ token: methodToken.token,
38
55
  request: requestPayload,
39
56
  },
40
57
  };
41
58
 
42
- const invokePayload = VoltCredential.aesEncrypt(
43
- tunnelMethod.responseSerialize(tunnelResp),
44
- meta.sharedKey.key,
45
- meta.sharedKey.iv,
46
- );
59
+ let targetFingerprint = [];
60
+ let invokePayload;
61
+ if (this._voltClient.isVoltRelay || isServiceRelayed) {
62
+ // When sending via a relay we need to encrypt the payload.
63
+ invokePayload = VoltCredential.aesEncrypt(
64
+ tunnelMethod.responseSerialize(tunnelResp),
65
+ methodToken.sharedKey.key,
66
+ methodToken.sharedKey.iv,
67
+ );
68
+ } else {
69
+ invokePayload = tunnelMethod.responseSerialize(tunnelResp);
70
+ }
47
71
 
48
- log("target volt is %s", this._voltClient.credential.voltFingerprint);
72
+ if (this._voltClient.isVoltRelay) {
73
+ targetFingerprint.push(this._voltClient.credential.voltFingerprint);
74
+ }
75
+
76
+ if (isServiceRelayed) {
77
+ // Add another relay hop if the target service is relayed.
78
+ log("target service is relayed");
79
+ targetFingerprint.push(
80
+ this._service.service_description.host_fingerprint,
81
+ );
82
+ }
83
+
84
+ log("target is %j", targetFingerprint);
49
85
 
50
86
  invokeRequest = {
51
- identity_fingerprint: this._voltClient.credential.voltFingerprint,
87
+ target_fingerprint: targetFingerprint,
88
+ token: methodToken.token,
52
89
  payload: invokePayload,
90
+ target_service_id: isServiceRelayed ? this._service.id : undefined,
53
91
  };
54
92
 
93
+ // Replace the target method with a call to Invoke on the relay Volt.
55
94
  callMethod = "Invoke";
56
- } else if (this._voltClient.isRemote && meta.sharedKey.key) {
57
- const requestPayload =
58
- this._grpcClient[method].requestSerializeOriginal(request);
59
- invokeRequest = VoltCredential.aesEncrypt(
60
- requestPayload,
61
- meta.sharedKey.key,
62
- meta.sharedKey.iv,
63
- );
64
95
  } else {
65
96
  invokeRequest = request;
66
97
  }
67
98
 
68
99
  return {
69
- meta,
100
+ meta: methodToken,
70
101
  method: callMethod,
71
102
  methodName: method,
72
103
  request: invokeRequest,
@@ -76,10 +107,16 @@ function _prepareInvokeRequest(
76
107
  function _preparePayloadRequest(request, invokeInfo) {
77
108
  let payloadRequest;
78
109
 
79
- if (this._voltClient.isVoltRelay) {
110
+ const isServiceRelayed =
111
+ this._service?.service_description.host_type ===
112
+ "SERVICE_HOST_TYPE_RELAYED";
113
+
114
+ if (this._voltClient.isVoltRelay || isServiceRelayed) {
80
115
  const requestPayload =
81
116
  this._grpcClient[invokeInfo.methodName].requestSerialize(request);
82
- const tunnelMethod = this._grpcClient.Tunnel;
117
+
118
+ const voltGrpcClient = this._voltClient.getVoltAPIClient();
119
+ const tunnelMethod = voltGrpcClient.Tunnel;
83
120
 
84
121
  const tunnelResp = {
85
122
  method_payload: {
@@ -96,14 +133,6 @@ function _preparePayloadRequest(request, invokeInfo) {
96
133
  payloadRequest = {
97
134
  payload: invokePayload,
98
135
  };
99
- } else if (this._voltClient.isRemote && invokeInfo.meta.sharedKey.key) {
100
- const requestPayload =
101
- this._grpcClient[invokeInfo.methodName].requestSerializeOriginal(request);
102
- payloadRequest = VoltCredential.aesEncrypt(
103
- requestPayload,
104
- invokeInfo.meta.sharedKey.key,
105
- invokeInfo.meta.sharedKey.iv,
106
- );
107
136
  } else {
108
137
  payloadRequest = request;
109
138
  }
@@ -112,21 +141,39 @@ function _preparePayloadRequest(request, invokeInfo) {
112
141
  }
113
142
 
114
143
  function _parseResponse(method, meta, response) {
144
+ // log("parseResponse for %s", this._methodName);
115
145
  let invokeResponse = {};
116
146
 
117
- if (this._voltClient.isVoltRelay) {
147
+ const isServiceRelayed =
148
+ this._service?.service_description.host_type ===
149
+ "SERVICE_HOST_TYPE_RELAYED";
150
+
151
+ if (this._voltClient.isVoltRelay || isServiceRelayed) {
118
152
  if (response.payload) {
119
- const decryptedPayload = VoltCredential.aesDecrypt(
120
- response.payload,
121
- meta.sharedKey.key,
122
- meta.sharedKey.iv,
123
- );
124
- const tunnelMethod = this._grpcClient.Tunnel;
153
+ let decryptedPayload;
154
+ if (meta.sharedKey.key) {
155
+ decryptedPayload = VoltCredential.aesDecrypt(
156
+ response.payload,
157
+ meta.sharedKey.key,
158
+ meta.sharedKey.iv,
159
+ );
160
+ } else {
161
+ decryptedPayload = response.payload;
162
+ }
163
+
164
+ const tunnelMethod = this._voltClient.getVoltAPIClient().Tunnel;
125
165
  const responsePayload = tunnelMethod.requestDeserialize(decryptedPayload);
126
166
  if (responsePayload.payload === "method_payload") {
127
- invokeResponse.payload = this._grpcClient[method].responseDeserialize(
128
- responsePayload.method_payload.payload,
129
- );
167
+ try {
168
+ invokeResponse.payload = this._grpcClient[method].responseDeserialize(
169
+ responsePayload.method_payload.payload,
170
+ );
171
+ } catch (err) {
172
+ log("failure deserialising payload for %s", this._methodName);
173
+ throw new Error(
174
+ "failure deserialising payload - check protobuf definition matches with data being sent",
175
+ );
176
+ }
130
177
  } else if (responsePayload.payload === "method_end") {
131
178
  invokeResponse = responsePayload.method_end;
132
179
  } else {
@@ -166,17 +213,22 @@ function _parseResponse(method, meta, response) {
166
213
  }
167
214
 
168
215
  export default class GRPCCall extends EventEmitter {
169
- constructor(voltClient, methodName, methodType) {
216
+ constructor(voltClient, methodName, methodType, service = undefined) {
170
217
  super();
171
218
  this._methodName = methodName;
172
219
  this._methodType = methodType;
173
220
  this._call = null;
174
221
  this._voltClient = voltClient;
222
+ this._service = service;
175
223
  }
176
224
 
177
225
  start(grpcClient, request) {
178
226
  this._grpcClient = grpcClient;
179
227
 
228
+ if (typeof this._grpcClient[this._methodName] !== "function") {
229
+ throw new Error(`method not found: '${this._methodName}'`);
230
+ }
231
+
180
232
  this._initialRequest = _prepareInvokeRequest.call(
181
233
  this,
182
234
  request,
@@ -184,13 +236,18 @@ export default class GRPCCall extends EventEmitter {
184
236
  this._methodType,
185
237
  );
186
238
 
187
- if (!this._grpcClient[this._initialRequest.method]) {
188
- return reject(
189
- new Error(`method not found: '${this._initialRequest.method}'`),
190
- );
239
+ const isServiceRelayed =
240
+ this._service?.service_description.host_type ===
241
+ "SERVICE_HOST_TYPE_RELAYED";
242
+
243
+ let callClient;
244
+ if (this._voltClient.isVoltRelay || isServiceRelayed) {
245
+ callClient = this._voltClient.getVoltAPIClient();
246
+ } else {
247
+ callClient = this._grpcClient;
191
248
  }
192
249
 
193
- this._call = this._grpcClient[this._initialRequest.method](
250
+ this._call = callClient[this._initialRequest.method](
194
251
  this._initialRequest.meta.metadata,
195
252
  );
196
253
 
@@ -223,7 +280,11 @@ export default class GRPCCall extends EventEmitter {
223
280
  log("method id is %s", parsedResponse.methodId);
224
281
  }
225
282
  } catch (err) {
226
- log("failure processing connect response: %s", err.message);
283
+ log(
284
+ "failure processing %s call response: %s",
285
+ this._methodName,
286
+ err.message,
287
+ );
227
288
  this.emit("error", err);
228
289
  }
229
290
  });
@@ -2,6 +2,7 @@ import protoLoader from "@grpc/proto-loader";
2
2
  import lodash from "lodash";
3
3
  import { dirname, join } from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import { writeFileSync, mkdirSync } from "fs";
5
6
 
6
7
  const { get: getProp, snakeCase } = lodash;
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -17,23 +18,23 @@ const defaultLoaderOptions = {
17
18
  includeDirs: [defaultProtoPath],
18
19
  };
19
20
 
20
- const getProtoDescriptors = (grpc, protoPath, opts) => {
21
+ function getProtoDescriptors(grpc, protoPath, opts) {
21
22
  const definition = protoLoader.loadSync(
22
23
  protoPath,
23
24
  opts || defaultLoaderOptions,
24
25
  );
25
26
  const descriptors = grpc.loadPackageDefinition(definition);
26
27
  return descriptors;
27
- };
28
+ }
28
29
 
29
- const getProtoService = (descriptors, servicePath) => {
30
+ function getProtoService(descriptors, servicePath) {
30
31
  const serviceDef = getProp(descriptors, servicePath);
31
32
  if (serviceDef?.service) {
32
33
  return serviceDef.service;
33
34
  } else {
34
35
  return null;
35
36
  }
36
- };
37
+ }
37
38
 
38
39
  function getServiceDescriptorsFromPath(
39
40
  grpc,
@@ -41,8 +42,30 @@ function getServiceDescriptorsFromPath(
41
42
  servicePackageName,
42
43
  opts,
43
44
  ) {
45
+ if (!opts) {
46
+ opts = { ...defaultLoaderOptions };
47
+ opts.includeDirs = [protoPath];
48
+ }
49
+
50
+ // Extract the package components of the service proto package. The package name should be of the
51
+ // fully qualified proto path, e.g. tdx.volt_api.webcam.v1.WebcamControlAPI.
52
+ const protoServiceComponents = servicePackageName.split(".");
53
+
54
+ // The service name is the last component of the path.
55
+ const protoServiceName = protoServiceComponents.pop();
56
+
57
+ // The actual file name should match the snake case of the service name.
58
+ const protoFileName = `${snakeCase(protoServiceName).toLowerCase()}.proto`;
59
+
60
+ // The path to the proto file should match each component of the package name.
61
+ const serviceProtoPath = join(
62
+ protoPath,
63
+ ...protoServiceComponents,
64
+ protoFileName,
65
+ );
66
+
44
67
  let serviceDescriptors;
45
- const descriptors = getProtoDescriptors(grpc, protoPath, opts);
68
+ const descriptors = getProtoDescriptors(grpc, serviceProtoPath, opts);
46
69
  if (descriptors) {
47
70
  serviceDescriptors = getProtoService(descriptors, servicePackageName);
48
71
  }
@@ -90,7 +113,16 @@ function getProtoDescriptorMethods(protoDescriptor) {
90
113
  return methods;
91
114
  }
92
115
 
116
+ function createServiceProtobufFiles(service, protoPath) {
117
+ for (let protoFile of service.service_description.proto_file) {
118
+ const filePath = join(protoPath, protoFile.file_path);
119
+ mkdirSync(dirname(filePath), { recursive: true });
120
+ writeFileSync(filePath, protoFile.protobuf);
121
+ }
122
+ }
123
+
93
124
  export {
125
+ createServiceProtobufFiles,
94
126
  getServiceDescriptors,
95
127
  getProtoDescriptors,
96
128
  getProtoDescriptorMethods,
@@ -0,0 +1,273 @@
1
+ import debug from "debug";
2
+ import { VoltCredential } from "./volt-credential.js";
3
+ import jwt from "jsonwebtoken";
4
+ import EventEmitter from "events";
5
+
6
+ const log = debug("rpc-invocation");
7
+
8
+ class RpcInvocation extends EventEmitter {
9
+ #voltClient = null;
10
+ #voltConnection = null;
11
+ #token = null;
12
+ #encryptKey = null;
13
+ #encryptIV = null;
14
+ #invokeId = null;
15
+ #payload = null;
16
+ #isJSON = false;
17
+ #methodDescriptor = null;
18
+ #methodName = null;
19
+
20
+ constructor(voltClient, voltConnection) {
21
+ super();
22
+ this.#voltClient = voltClient;
23
+ this.#voltConnection = voltConnection;
24
+ }
25
+
26
+ get id() {
27
+ return this.#invokeId;
28
+ }
29
+
30
+ get payload() {
31
+ if (!this.#payload) {
32
+ return null;
33
+ }
34
+
35
+ let deserialisedPayload = null;
36
+
37
+ if (this.#isJSON) {
38
+ // Don't need to deserialise JSON payload.
39
+ deserialisedPayload = this.#payload;
40
+ } else if (this.#methodDescriptor) {
41
+ deserialisedPayload = this.#methodDescriptor.requestDeserialize(
42
+ this.#payload,
43
+ );
44
+ } else {
45
+ log("unexpected: no method descriptor");
46
+ throw new Error(
47
+ "no method descriptor - set this before accessing payload",
48
+ );
49
+ }
50
+
51
+ return deserialisedPayload;
52
+ }
53
+
54
+ get methodName() {
55
+ return this.#methodName;
56
+ }
57
+
58
+ set methodDescriptor(methodDescriptor) {
59
+ this.#methodDescriptor = methodDescriptor;
60
+ }
61
+
62
+ #decodeToken(token) {
63
+ // We don't verify the token at this point, as we don't know the public key.
64
+ // The rpc implementation should do the verification using the Volt API (e.g. CanAccessResource).
65
+ this.#token = jwt.decode(token);
66
+
67
+ if (this.#token.sk) {
68
+ // Decrypt the shared key and iv using the private key.
69
+ this.#encryptKey = VoltCredential.rsaDecrypt(
70
+ this.#voltClient.credential.cache.key,
71
+ this.#token.sk,
72
+ );
73
+ this.#encryptIV = VoltCredential.rsaDecrypt(
74
+ this.#voltClient.credential.cache.key,
75
+ this.#token.iv,
76
+ );
77
+ }
78
+
79
+ return Promise.resolve(this.#token);
80
+ }
81
+
82
+ initialise(invoke_request) {
83
+ this.#invokeId = invoke_request.invoke_id;
84
+
85
+ return this.#decodeToken(invoke_request.token)
86
+ .then(() => {
87
+ // Parse the initial payload.
88
+ return this.parsePayload(invoke_request);
89
+ })
90
+ .catch((err) => {
91
+ log("failure initialising invocation: %s", err.message);
92
+ return Promise.reject(err);
93
+ });
94
+ }
95
+
96
+ parsePayload(invoke_request) {
97
+ return new Promise((resolve, reject) => {
98
+ if (invoke_request.payload) {
99
+ let decryptedPayload;
100
+
101
+ if (this.#encryptKey) {
102
+ // Decrypt the actual payload using the shared key and iv.
103
+ decryptedPayload = VoltCredential.aesDecrypt(
104
+ invoke_request.payload,
105
+ this.#encryptKey,
106
+ this.#encryptIV,
107
+ );
108
+ } else {
109
+ // No encryption => just use the payload.
110
+ decryptedPayload = invoke_request.payload;
111
+ }
112
+
113
+ const tunnelMethod = this.#voltClient.getVoltAPIClient().Tunnel;
114
+ const wrappedPayload =
115
+ tunnelMethod.responseDeserialize(decryptedPayload);
116
+
117
+ if (wrappedPayload.method_invoke) {
118
+ // This is the initial request payload.
119
+ this.#methodName = wrappedPayload.method_invoke.method_name;
120
+ this.#payload = wrappedPayload.method_invoke.request;
121
+ this.emit("payload", this);
122
+ } else if (wrappedPayload.method_payload) {
123
+ // This is a subsequent payload, e.g. for streaming rpcs.
124
+ this.#payload = wrappedPayload.method_payload.payload;
125
+ this.emit("payload", this);
126
+ } else if (wrappedPayload.method_end) {
127
+ // This indicates the client has finished sending payloads.
128
+ // We don't need to do anything here, since we notify the client when we receive the client_end.
129
+ log("method_end received");
130
+ }
131
+
132
+ resolve();
133
+ } else if (invoke_request.json_payload) {
134
+ let decryptedPayload;
135
+
136
+ this.#isJSON = true;
137
+
138
+ if (this.#encryptKey) {
139
+ // Decrypt the actual payload using the shared key and iv.
140
+ decryptedPayload = VoltCredential.aesDecrypt(
141
+ Buffer.from(invoke_request.json_payload, "base64"),
142
+ this.#encryptKey,
143
+ this.#encryptIV,
144
+ );
145
+ } else {
146
+ // No encryption => just use the payload.
147
+ decryptedPayload = invoke_request.json_payload;
148
+ }
149
+
150
+ try {
151
+ const wrappedPayload = JSON.parse(decryptedPayload.toString());
152
+
153
+ if (wrappedPayload.method_invoke) {
154
+ this.#methodName = wrappedPayload.method_invoke.method_name;
155
+ this.#payload = JSON.parse(
156
+ wrappedPayload.method_invoke.json_request,
157
+ );
158
+ this.emit("payload", this);
159
+ } else if (wrappedPayload.method_payload) {
160
+ this.#payload = JSON.parse(
161
+ wrappedPayload.method_payload.json_payload,
162
+ );
163
+ this.emit("payload", this);
164
+ } else if (wrappedPayload.method_end) {
165
+ // We don't need to do anything here, since we notify the client when we receive the client_end.
166
+ log("method_end received");
167
+ }
168
+ } catch (err) {
169
+ log("JSON.parse failure parsing json_payload: %s", err.message);
170
+ reject(err);
171
+ }
172
+
173
+ resolve();
174
+ } else if (invoke_request.client_end) {
175
+ this.emit("end", this);
176
+ } else {
177
+ // Do we need to support json_payload here?
178
+ reject(new Error("No payload in invoke request"));
179
+ }
180
+ }).catch((err) => {
181
+ log("failure parsing payload: %s", err.message);
182
+ return Promise.reject(err);
183
+ });
184
+ }
185
+
186
+ sendResponse(response) {
187
+ let responsePayload;
188
+ if (this.#isJSON) {
189
+ responsePayload = JSON.stringify({
190
+ method_payload: { json_payload: JSON.stringify(response) },
191
+ });
192
+ } else {
193
+ // We need to serialise the response, and then wrap it in a RemoteRequest.
194
+ const serialisedResponse =
195
+ this.#methodDescriptor.responseSerialize(response);
196
+ const tunnelMethod = this.#voltClient.getVoltAPIClient().Tunnel;
197
+ responsePayload = tunnelMethod.requestSerialize({
198
+ method_payload: { payload: serialisedResponse },
199
+ });
200
+ }
201
+
202
+ if (this.#token.sk) {
203
+ // Encrypt the response using the shared key and iv.
204
+ responsePayload = VoltCredential.aesEncrypt(
205
+ responsePayload,
206
+ this.#encryptKey,
207
+ this.#encryptIV,
208
+ );
209
+ }
210
+
211
+ const invokeResponse = {
212
+ invoke_id: this.#invokeId,
213
+ };
214
+
215
+ if (this.#isJSON) {
216
+ invokeResponse.json_payload =
217
+ Buffer.from(responsePayload).toString("base64");
218
+ } else {
219
+ invokeResponse.payload = responsePayload;
220
+ }
221
+
222
+ this.#voltConnection.send({
223
+ invoke_response: invokeResponse,
224
+ });
225
+ }
226
+
227
+ sendEnd(errorMessage) {
228
+ const tunnelMethod = this.#voltClient.getVoltAPIClient().Tunnel;
229
+
230
+ let endPayload;
231
+ if (errorMessage) {
232
+ endPayload = {
233
+ method_end: { error: errorMessage, ended: true },
234
+ };
235
+ } else {
236
+ endPayload = {
237
+ method_end: { ended: true },
238
+ };
239
+ }
240
+
241
+ if (this.#isJSON) {
242
+ endPayload = JSON.stringify(endPayload);
243
+ } else {
244
+ endPayload = tunnelMethod.requestSerialize(endPayload);
245
+ }
246
+
247
+ if (this.#token.sk) {
248
+ // Encrypt the response using the shared key and iv.
249
+ endPayload = VoltCredential.aesEncrypt(
250
+ endPayload,
251
+ this.#encryptKey,
252
+ this.#encryptIV,
253
+ );
254
+ }
255
+
256
+ const invokeResponse = {
257
+ invoke_id: this.#invokeId,
258
+ server_end: true,
259
+ };
260
+
261
+ if (this.#isJSON) {
262
+ invokeResponse.json_payload = Buffer.from(endPayload).toString("base64");
263
+ } else {
264
+ invokeResponse.payload = endPayload;
265
+ }
266
+
267
+ this.#voltConnection.send({
268
+ invoke_response: invokeResponse,
269
+ });
270
+ }
271
+ }
272
+
273
+ export default RpcInvocation;