@tdxvolt/volt-client-grpc 0.14.61 → 0.15.0

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.61",
6
+ "version": "0.15.0",
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
  });
package/src/grpc-utils.js CHANGED
@@ -16,7 +16,7 @@ export function createClient(
16
16
  credentials,
17
17
  noSerialise,
18
18
  voltId,
19
- serviceId,
19
+ serviceId
20
20
  ) {
21
21
  let grpcCredentials;
22
22
  if (credentials) {
@@ -30,7 +30,7 @@ export function createClient(
30
30
  grpcCredentials = grpc.credentials.createSsl(
31
31
  tlsOptions.ca,
32
32
  tlsOptions.key,
33
- tlsOptions.cert,
33
+ tlsOptions.cert
34
34
  );
35
35
  } else {
36
36
  // Cache has no private key, this implies we are connecting without supplying a client certificate.
@@ -92,7 +92,7 @@ const grpcWrap = (api) => {
92
92
  // Support class instances and PoJos
93
93
  const isClass = api.constructor !== Object;
94
94
  const iterate = Object.getOwnPropertyNames(
95
- isClass ? api.constructor.prototype : api,
95
+ isClass ? api.constructor.prototype : api
96
96
  );
97
97
 
98
98
  // Wrap each method on the api.
@@ -106,7 +106,7 @@ const grpcWrap = (api) => {
106
106
  // Enforce promise results.
107
107
  if (!(resultPromise?.then && resultPromise?.catch)) {
108
108
  throw new Error(
109
- `implementation methods must return a Promise [${name}]`,
109
+ `implementation methods must return a Promise [${name}]`
110
110
  );
111
111
  }
112
112
 
@@ -143,7 +143,7 @@ class GrpcServer {
143
143
  "grpc.http2.max_pings_without_data": 0,
144
144
  "grpc.keepalive_permit_without_calls": 1,
145
145
  "grpc.http2.min_ping_interval_without_data_ms": 250,
146
- "grpc.http2.max_ping_strikes": 0,
146
+ "grpc.http2.max_ping_strikes": 0
147
147
  });
148
148
 
149
149
  if (tlsOptions) {
@@ -153,7 +153,7 @@ class GrpcServer {
153
153
  caList.push(tlsOption.ca);
154
154
  certChains.push({
155
155
  private_key: tlsOption.key,
156
- cert_chain: tlsOption.cert,
156
+ cert_chain: tlsOption.cert
157
157
  });
158
158
  });
159
159
 
@@ -164,7 +164,7 @@ class GrpcServer {
164
164
  // The certificate chain(s) we present to the client.
165
165
  certChains,
166
166
  // Flag indicating if we insist on a client certificate.
167
- requireClientCert,
167
+ requireClientCert
168
168
  );
169
169
  } else {
170
170
  this._credentials = grpc.ServerCredentials.createInsecure();
@@ -216,7 +216,7 @@ export function createServer(
216
216
  serviceDescriptors,
217
217
  routes,
218
218
  cryptoCache,
219
- requireClientCert,
219
+ requireClientCert
220
220
  ) {
221
221
  let tlsOptions;
222
222
  if (cryptoCache) {
@@ -227,7 +227,7 @@ export function createServer(
227
227
  serviceDescriptors,
228
228
  routes,
229
229
  tlsOptions,
230
- requireClientCert,
230
+ requireClientCert
231
231
  };
232
232
  return new GrpcServer(serverArgs);
233
233
  }
@@ -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,