@tdxvolt/volt-client-grpc 0.1.2 → 0.11.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.
@@ -5,12 +5,12 @@ import path from "path";
5
5
  import { getServiceDescriptors } from "./proto-utils.js";
6
6
  import { createClient as createGrpcClient } from "./grpc-utils.js";
7
7
  import {
8
- bindInternal,
9
- connectInternal,
10
- fetchVoltConfig,
11
- fetchVoltConfigFromDID,
12
- getVoltAPIClientInternal,
13
- unaryCall,
8
+ bindInternal,
9
+ connectInternal,
10
+ fetchVoltConfig,
11
+ fetchVoltConfigFromDID,
12
+ getVoltAPIClientInternal,
13
+ unaryCall,
14
14
  } from "./volt-client-internal.js";
15
15
  import fs from "fs";
16
16
  import GRPCCall from "./grpc-call.js";
@@ -19,538 +19,539 @@ const { readFile } = fs.promises;
19
19
  const log = debug("volt-client-grpc:volt-client");
20
20
 
21
21
  export class VoltClient extends EventEmitter {
22
- /**
23
- * @param {object} grpc - an instance of the grpc module (see https://www.npmjs.com/package/grpc)
24
- */
25
- constructor(grpc) {
26
- super();
27
-
28
- if (!grpc) {
29
- throw new Error("invalid arguments, grpc is required");
30
- }
31
- this._grpc = grpc;
32
- this._cachedClient = null;
33
- this._cachedRemoteClient = null;
34
- this._session = null;
35
- this._config = null;
36
- this._voltConfig = null;
37
- this._voltConnection = null;
38
- }
39
-
40
- get config() {
41
- return this._config;
42
- }
43
-
44
- get credential() {
45
- return this._credential;
46
- }
47
-
48
- get grpc() {
49
- return this._grpc;
50
- }
51
-
52
- get voltConfig() {
53
- return this._voltConfig;
54
- }
55
-
56
- /**
57
- * Initialises a binding and connection to the volt.
58
- * @param {string} [config] - the location of the Volt configuration file, or a configuration object
59
- */
60
- async initialise(config, extras = {}) {
61
- try {
62
- if (!config) {
63
- throw new Error(
64
- "configPath argument is required to be a non-empty string",
65
- );
66
- }
67
-
68
- let configPath;
69
- let configJSON;
70
- if (typeof config === "string") {
71
- configPath = path.resolve(config);
72
- log("Attempt to load config from file %s", configPath);
73
- try {
74
- const configContents = await readFile(
75
- new URL(configPath, import.meta.url),
76
- );
77
- configJSON = JSON.parse(configContents);
78
- } catch (err) {
79
- log("failure loading config file %s [%s]", configPath, err.message);
80
- throw new Error(
81
- `failed to load configuration - check JSON format in ${configPath}`,
82
- );
83
- }
84
- } else if (typeof config === "object") {
85
- configJSON = config;
86
- } else {
87
- throw new Error("config must be either a file path or a config object");
88
- }
89
-
90
- if (!configJSON || typeof configJSON !== "object") {
91
- throw new Error("config argument is required to be an object");
92
- }
93
-
94
- if (
95
- !configJSON.client_name ||
96
- typeof configJSON.client_name !== "string"
97
- ) {
98
- throw new Error("client_name property required in config");
99
- }
100
-
101
- this._config = { ...configJSON, ...extras };
102
-
103
- let voltDID = "";
104
- let voltDiscovery = "";
105
- if (typeof this._config.volt === "string") {
106
- if (this._config.volt.indexOf("did:") === 0) {
107
- voltDID = this._config.volt;
108
- } else {
109
- voltDiscovery = this._config.volt;
110
- }
111
- } else if (typeof this._config.volt !== "object") {
112
- throw new Error("configuration is missing the 'volt' object");
113
- } else {
114
- voltDID = this._config.volt.did;
115
- voltDiscovery = this._config.volt.discovery_url;
116
- }
117
-
118
- if (voltDID) {
119
- const didConfig = await fetchVoltConfigFromDID.call(this, voltDID);
120
- this._config = { ...this._config, ...didConfig };
121
- log("resolved config from DID: %s", JSON.stringify(didConfig, null, 2));
122
- } else if (voltDiscovery) {
123
- const discoConfig = await fetchVoltConfig.call(this, voltDiscovery);
124
- this._config = { ...this._config, ...discoConfig };
125
- }
126
-
127
- this._voltConfig = this._config.volt;
128
- this._voltConfig.did = voltDID;
129
- this._voltConfig.discovery_url = voltDiscovery;
130
-
131
- if (!this._voltConfig.id || typeof this._voltConfig.id !== "string") {
132
- throw new Error("'id' property is missing in Volt configuration");
133
- }
134
-
135
- this._credential = new VoltCredential(this._config, configPath);
136
-
137
- await this._credential.initialise();
138
-
139
- if (!this.isRemote) {
140
- // If we're not connecting via a Relay, we must have the address and CA.
141
- if (
142
- !this._voltConfig.address ||
143
- typeof this._voltConfig.address !== "string"
144
- ) {
145
- throw new Error("'address' property missing in Volt configuration");
146
- }
147
-
148
- if (
149
- !this._voltConfig.ca_pem ||
150
- typeof this._voltConfig.ca_pem !== "string"
151
- ) {
152
- throw new Error("'ca_pPem' property missing in Volt configuration");
153
- }
154
- }
155
-
156
- let isBound;
157
- if (this._credential._cryptoCache.cert) {
158
- isBound = true;
159
- } else {
160
- // No certificate present in the cache yet => we need to bind.
161
- try {
162
- isBound = await bindInternal.call(this);
163
- } catch (bindErr) {
164
- if (bindErr.message === "invalid arguments") {
165
- // This is usually the result of a missing challenge signature or credential.
166
- log(
167
- "failure binding to Volt - did you supply a challenge code or verified credential?",
168
- );
169
- throw bindErr;
170
- }
171
- }
172
- }
173
-
174
- if (!isBound) {
175
- // This is usually because of request being denied.
176
- throw new Error("Volt binding failed");
177
- }
178
-
179
- return Promise.resolve();
180
- } catch (err) {
181
- log("Failure starting Volt client [%s]", err.message);
182
- throw err;
183
- }
184
- }
185
-
186
- initialiseAndConnect(configPath, extras = {}) {
187
- return this.initialise(configPath, extras)
188
- .then(() => {
189
- return connectInternal.call(this);
190
- })
191
- .catch((err) => {
192
- log("initialiseAndConnect failure: %s", err.message);
193
- return Promise.reject(err);
194
- });
195
- }
196
-
197
- /**
198
- * Creates a GRPC client for the given Volt service description.
199
- *
200
- * Automatically handles remote connections.
201
- *
202
- * n.b. for this javascript client Protobuf definitions should conform to the recommended
203
- * structure, which is that a package proto file resides in a folder path that matches the
204
- * package name, and services contained in the package are named in in Pascal Case and using
205
- * the suffix 'API'. For example the WebcamControlAPI in package tdx.volt_api.webcam.v1 should reside
206
- * in a protobuf file at tdx/api/webcam/v1/webcam_control_api.proto
207
- *
208
- * @param {object} service a service description.
209
- * @param {object} descriptors the service descriptors, must be specified unless the service
210
- * packages are well-known Volt APIs.
211
- */
212
- createServiceClient(service, descriptors) {
213
- const pkg = service.service_description.service_api;
214
-
215
- let serviceDescriptors;
216
- if (descriptors) {
217
- serviceDescriptors = descriptors;
218
- } else {
219
- serviceDescriptors = {};
220
- pkg.forEach((svc) => {
221
- log("adding service %s to service client", svc);
222
- const packageDescriptors = getServiceDescriptors(this._grpc, svc);
223
- serviceDescriptors = { ...serviceDescriptors, ...packageDescriptors };
224
- });
225
- }
226
-
227
- if (this.isRemote) {
228
- if (!this._config?.relay?.address) {
229
- throw new Error(
230
- "Remote connection enabled but no Relay address - have you called start()?",
231
- );
232
- }
233
-
234
- // In remote mode => create a client using the discovered Relay address **and** passing the service reourceId
235
- return createGrpcClient(
236
- this._grpc,
237
- serviceDescriptors,
238
- this._config.relay.address,
239
- this._credential,
240
- false,
241
- service.volt_id,
242
- service.id,
243
- );
244
- } else {
245
- return createGrpcClient(
246
- this._grpc,
247
- serviceDescriptors,
248
- service.service_description.address,
249
- this._credential,
250
- );
251
- }
252
- }
253
-
254
- get isRemote() {
255
- return !!(this._config?.relay && this._voltConfig.id);
256
- }
257
-
258
- get isVoltRelay() {
259
- return this.isRemote && !this._config?.relay.cloud_relay;
260
- }
261
-
262
- connect() {
263
- return connectInternal.call(this);
264
- }
265
-
266
- disconnect() {
267
- if (this._voltConnection) {
268
- this._voltConnection.disconnect();
269
- this._voltConnection = null;
270
- }
271
- }
272
-
273
- getVoltAPIClient() {
274
- return getVoltAPIClientInternal.call(this);
275
- }
276
-
277
- /**
278
- * Request resource access.
279
- * @param {*} targetResourceId
280
- * @param {*} accessType - see vault.proto for enum values.
281
- */
282
- async RequestAccessBlocking(
283
- targetResourceId,
284
- accessType = "VOLT_ACCESS_READ",
285
- ) {
286
- try {
287
- const accessRequest = {
288
- access: accessType,
289
- resource_id: targetResourceId,
290
- };
291
-
292
- // This will block while the request is pending.
293
- // TODO - implement streamed rpc on volt? Not sure there is a need really...
294
- const pollInterval = 10000;
295
- let decision = "";
296
- do {
297
- if (decision) {
298
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
299
- }
300
- const accessResponse = await this.RequestAccess(accessRequest);
301
- decision = accessResponse.decision;
302
- log("access decision: %s", decision);
303
- } while (
304
- decision === "POLICY_DECISION_PROMPT" ||
305
- decision === "POLICY_DECISION_PENDING"
306
- );
307
-
308
- log("requestResourceAccess result is %j", decision);
309
- return decision === "POLICY_DECISION_PERMIT";
310
- } catch (err) {
311
- log("failure requesting resource access [%s]", err.message);
312
- return Promise.reject(err);
313
- }
314
- }
315
-
316
- /**
317
- * VoltAPI - resource management
318
- */
319
-
320
- CanAccessResource(request) {
321
- return unaryCall.call(this, "CanAccessResource", request);
322
- }
323
-
324
- DeleteResource(request) {
325
- return unaryCall.call(this, "DeleteResource", request);
326
- }
327
-
328
- DiscoverServices(request) {
329
- return unaryCall.call(this, "DiscoverServices", request);
330
- }
331
-
332
- GetResource(request) {
333
- return unaryCall.call(this, "GetResource", request);
334
- }
335
-
336
- GetResources(request) {
337
- return unaryCall.call(this, "GetResources", request);
338
- }
339
-
340
- GetResourceAncestors(request) {
341
- return unaryCall.call(this, "GetResourceAncestors", request);
342
- }
343
-
344
- GetResourceDescendants(request) {
345
- return unaryCall.call(this, "GetResourceDescendants", request);
346
- }
347
-
348
- RequestAccess(request) {
349
- return unaryCall.call(this, "RequestAccess", request);
350
- }
351
-
352
- SaveResource(request) {
353
- return unaryCall.call(this, "SaveResource", request);
354
- }
355
-
356
- SaveResourceAttribute(request) {
357
- return unaryCall.call(this, "SaveResourceAttribute", request);
358
- }
359
-
360
- SetServiceStatus(request) {
361
- return unaryCall.call(this, "SetServiceStatus", request);
362
- }
363
-
364
- /**
365
- * VoltAPI - volt management
366
- */
367
-
368
- Bind(request) {
369
- return unaryCall.call(this, "Bind", request);
370
- }
371
-
372
- DeleteAccess(request) {
373
- return unaryCall.call(this, "DeleteAccess", request);
374
- }
375
-
376
- DeleteVolt(request) {
377
- return unaryCall.call(this, "DeleteVolt", request);
378
- }
379
-
380
- GetAccess(request) {
381
- return unaryCall.call(this, "GetAccess", request);
382
- }
383
-
384
- GetBindings(request) {
385
- return unaryCall.call(this, "GetBindings", request);
386
- }
387
-
388
- GetIdentities(request) {
389
- return unaryCall.call(this, "GetIdentities", request);
390
- }
391
-
392
- GetIdentity(request) {
393
- return unaryCall.call(this, "GetIdentity", request);
394
- }
395
-
396
- GetIdentityToken(request) {
397
- return unaryCall.call(this, "GetIdentityToken", request);
398
- }
399
-
400
- GetPolicy(request) {
401
- return unaryCall.call(this, "GetPolicy", request);
402
- }
403
-
404
- GetSettings(request) {
405
- return unaryCall.call(this, "GetSettings", request);
406
- }
407
-
408
- SaveAccess(request) {
409
- return unaryCall.call(this, "SaveAccess", request);
410
- }
411
-
412
- SaveCloudConnection(request) {
413
- return unaryCall.call(this, "SaveCloudConnection", request);
414
- }
415
-
416
- SaveIdentity(request) {
417
- return unaryCall.call(this, "SaveIdentity", request);
418
- }
419
-
420
- SaveSettings(request) {
421
- return unaryCall.call(this, "SaveSettings", request);
422
- }
423
-
424
- SetAccessRequestDecision(request) {
425
- return unaryCall.call(this, "SetAccessRequestDecision", request);
426
- }
427
-
428
- SetBindingDecision(request) {
429
- return unaryCall.call(this, "SetBindingDecision", request);
430
- }
431
-
432
- Shutdown(request) {
433
- return unaryCall.call(this, "Shutdown", request);
434
- }
435
-
436
- SignVerify(request) {
437
- return unaryCall.call(this, "SignVerify", request);
438
- }
439
-
440
- /**
441
- * FileAPI
442
- */
443
- GetFileDescendants(request) {
444
- return unaryCall.call(this, "GetFileDescendants", request);
445
- }
446
-
447
- /**
448
- * Asynchronous file download
449
- * @param {*} request
450
- * @returns stream
451
- */
452
- DownloadFile(request) {
453
- const grpcClient = this.getVoltAPIClient();
454
-
455
- return new Promise((resolve, reject) => {
456
- const meta = this._credential.getIdentityMetadata(
457
- this._grpc,
458
- this._voltConfig.id,
459
- this.isRemote,
460
- );
461
- resolve(grpcClient.DownloadFile(request, meta.metadata));
462
- }).catch((err) => {
463
- log("error during %s [%s]", method, err.message);
464
- return Promise.reject(err);
465
- });
466
- }
467
-
468
- UploadFile(request, callback) {
469
- const grpcClient = this.getVoltAPIClient();
470
-
471
- const uploadCall = new GRPCCall(this, "UploadFile", "METHOD_TYPE_BIDI");
472
- return uploadCall.start(grpcClient, request, callback);
473
- }
474
-
475
- /**
476
- * Synchronous file download
477
- * @param {*} request
478
- * @returns file buffer, base64 encoded
479
- */
480
- DownloadFileSync(request) {
481
- const grpcClient = this.getVoltAPIClient();
482
-
483
- const blocks = [];
484
-
485
- const call = new GRPCCall(
486
- this,
487
- "DownloadFile",
488
- "METHOD_TYPE_SERVER_STREAM",
489
- );
490
- call.start(grpcClient, request, (err, response) => {
491
- if (err) {
492
- return call.reject(err);
493
- }
494
-
495
- switch (response.payload) {
496
- case "block": {
497
- blocks.push(response.block);
498
- break;
499
- }
500
- case "status": {
501
- if (response.status.message) {
502
- return call.reject(new Error(response.status.message));
503
- } else {
504
- // Non-error response status => do nothing and wait for the stream to end.
505
- }
506
- break;
507
- }
508
- default:
509
- log("unrecognised response payload %s", response.payload);
510
- break;
511
- }
512
- });
513
-
514
- return call
515
- .wait()
516
- .then(() => {
517
- log("Download finished");
518
- return { buffer: Buffer.concat(blocks).toString("base64") };
519
- })
520
- .catch((err) => {
521
- log("error during DownloadFile [%s]", err.message);
522
- return Promise.reject(err);
523
- });
524
- }
525
-
526
- /**
527
- * SqliteDatabaseAPI
528
- */
529
- SqlExecute(request, callback) {
530
- const grpcClient = this.getVoltAPIClient();
531
-
532
- const subscribeCall = new GRPCCall(this, "Execute", "METHOD_TYPE_BIDI");
533
- return subscribeCall.start(grpcClient, request, callback);
534
- }
535
-
536
- /**
537
- * WireAPI
538
- */
539
- PublishWire(request, callback) {
540
- const grpcClient = this.getVoltAPIClient();
541
-
542
- const publishCall = new GRPCCall(this, "PublishWire", "METHOD_TYPE_BIDI");
543
- return publishCall.start(grpcClient, request, callback);
544
- }
545
-
546
- SubscribeWire(request, callback) {
547
- const grpcClient = this.getVoltAPIClient();
548
-
549
- const subscribeCall = new GRPCCall(
550
- this,
551
- "SubscribeWire",
552
- "METHOD_TYPE_BIDI",
553
- );
554
- return subscribeCall.start(grpcClient, request, callback);
555
- }
22
+ /**
23
+ * @param {object} grpc - an instance of the grpc module (see https://www.npmjs.com/package/grpc)
24
+ */
25
+ constructor(grpc) {
26
+ super();
27
+
28
+ if (!grpc) {
29
+ throw new Error("invalid arguments, grpc is required");
30
+ }
31
+ this._grpc = grpc;
32
+ this._cachedClient = null;
33
+ this._cachedRemoteClient = null;
34
+ this._session = null;
35
+ this._config = null;
36
+ this._voltConfig = null;
37
+ this._voltConnection = null;
38
+ }
39
+
40
+ get config() {
41
+ return this._config;
42
+ }
43
+
44
+ get credential() {
45
+ return this._credential;
46
+ }
47
+
48
+ get grpc() {
49
+ return this._grpc;
50
+ }
51
+
52
+ get voltConfig() {
53
+ return this._voltConfig;
54
+ }
55
+
56
+ /**
57
+ * Initialises a binding and connection to the volt.
58
+ * @param {string} [config] - the location of the Volt configuration file, or a configuration object
59
+ */
60
+ async initialise(config, extras = {}) {
61
+ try {
62
+ if (!config) {
63
+ throw new Error(
64
+ "configPath argument is required to be a non-empty string",
65
+ );
66
+ }
67
+
68
+ let configPath;
69
+ let configJSON;
70
+ if (typeof config === "string") {
71
+ configPath = path.resolve(config);
72
+ log("Attempt to load config from file %s", configPath);
73
+ try {
74
+ const configContents = await readFile(
75
+ new URL(configPath, import.meta.url),
76
+ );
77
+ configJSON = JSON.parse(configContents);
78
+ } catch (err) {
79
+ log("failure loading config file %s [%s]", configPath, err.message);
80
+ throw new Error(
81
+ `failed to load configuration - check JSON format in ${configPath}`,
82
+ );
83
+ }
84
+ } else if (typeof config === "object") {
85
+ configJSON = config;
86
+ } else {
87
+ throw new Error("config must be either a file path or a config object");
88
+ }
89
+
90
+ if (!configJSON || typeof configJSON !== "object") {
91
+ throw new Error("config argument is required to be an object");
92
+ }
93
+
94
+ if (
95
+ !configJSON.client_name ||
96
+ typeof configJSON.client_name !== "string"
97
+ ) {
98
+ throw new Error("client_name property required in config");
99
+ }
100
+
101
+ this._config = { ...configJSON, ...extras };
102
+
103
+ let voltDID = "";
104
+ let voltHttpAddress = "";
105
+ if (typeof this._config.volt === "string") {
106
+ if (this._config.volt.indexOf("did:") === 0) {
107
+ voltDID = this._config.volt;
108
+ } else {
109
+ voltHttpAddress = this._config.volt;
110
+ }
111
+ } else if (typeof this._config.volt !== "object") {
112
+ throw new Error("configuration is missing the 'volt' object");
113
+ } else {
114
+ voltDID = this._config.volt.did;
115
+ voltHttpAddress = this._config.volt.http_address;
116
+ }
117
+
118
+ if (voltDID) {
119
+ const didConfig = await fetchVoltConfigFromDID.call(this, voltDID);
120
+ this._config = { ...this._config, volt: didConfig };
121
+ log("resolved config from DID: %s", JSON.stringify(didConfig, null, 2));
122
+ } else if (voltHttpAddress) {
123
+ const discoConfig = await fetchVoltConfig.call(
124
+ this,
125
+ `${voltHttpAddress}/discovery`,
126
+ );
127
+ this._config = {
128
+ ...this._config,
129
+ volt: { ...this._config.volt, ...discoConfig },
130
+ };
131
+ }
132
+
133
+ this._voltConfig = this._config.volt;
134
+ this._voltConfig.did = voltDID;
135
+ this._voltConfig.http_address = voltHttpAddress;
136
+
137
+ if (!this._voltConfig.id || typeof this._voltConfig.id !== "string") {
138
+ throw new Error("'id' property is missing in Volt configuration");
139
+ }
140
+
141
+ this._credential = new VoltCredential(this._config, configPath);
142
+
143
+ await this._credential.initialise();
144
+
145
+ if (!this.isRemote) {
146
+ // If we're not connecting via a Relay, we must have the address and CA.
147
+ if (
148
+ !this._voltConfig.address ||
149
+ typeof this._voltConfig.address !== "string"
150
+ ) {
151
+ throw new Error("'address' property missing in Volt configuration");
152
+ }
153
+
154
+ if (
155
+ !this._voltConfig.ca_pem ||
156
+ typeof this._voltConfig.ca_pem !== "string"
157
+ ) {
158
+ throw new Error("'ca_pPem' property missing in Volt configuration");
159
+ }
160
+ }
161
+
162
+ let isBound;
163
+ if (this._credential._cryptoCache.cert) {
164
+ isBound = true;
165
+ } else {
166
+ // No certificate present in the cache yet => we need to bind.
167
+ try {
168
+ isBound = await bindInternal.call(this);
169
+ } catch (bindErr) {
170
+ if (bindErr.message === "invalid arguments") {
171
+ // This is usually the result of a missing challenge signature or credential.
172
+ log(
173
+ "failure binding to Volt - did you supply a challenge code or verified credential?",
174
+ );
175
+ throw bindErr;
176
+ }
177
+ }
178
+ }
179
+
180
+ if (!isBound) {
181
+ // This is usually because of request being denied.
182
+ throw new Error("Volt binding failed");
183
+ }
184
+
185
+ return Promise.resolve();
186
+ } catch (err) {
187
+ log("Failure starting Volt client [%s]", err.message);
188
+ throw err;
189
+ }
190
+ }
191
+
192
+ initialiseAndConnect(configPath, extras = {}) {
193
+ return this.initialise(configPath, extras)
194
+ .then(() => {
195
+ return connectInternal.call(this);
196
+ })
197
+ .catch((err) => {
198
+ log("initialiseAndConnect failure: %s", err.message);
199
+ return Promise.reject(err);
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Creates a GRPC client for the given Volt service description.
205
+ *
206
+ * Automatically handles remote connections.
207
+ *
208
+ * n.b. for this javascript client Protobuf definitions should conform to the recommended
209
+ * structure, which is that a package proto file resides in a folder path that matches the
210
+ * package name, and services contained in the package are named in in Pascal Case and using
211
+ * the suffix 'API'. For example the WebcamControlAPI in package tdx.volt_api.webcam.v1 should reside
212
+ * in a protobuf file at tdx/api/webcam/v1/webcam_control_api.proto
213
+ *
214
+ * @param {object} service a service description.
215
+ * @param {object} descriptors the service descriptors, must be specified unless the service
216
+ * packages are well-known Volt APIs.
217
+ */
218
+ createServiceClient(service, descriptors) {
219
+ const pkg = service.service_description.service_api;
220
+
221
+ let serviceDescriptors;
222
+ if (descriptors) {
223
+ serviceDescriptors = descriptors;
224
+ } else {
225
+ serviceDescriptors = {};
226
+ pkg.forEach((svc) => {
227
+ log("adding service %s to service client", svc);
228
+ const packageDescriptors = getServiceDescriptors(this._grpc, svc);
229
+ serviceDescriptors = { ...serviceDescriptors, ...packageDescriptors };
230
+ });
231
+ }
232
+
233
+ if (this.isRemote) {
234
+ if (!this._config?.relay?.address) {
235
+ throw new Error(
236
+ "Remote connection enabled but no Relay address - have you called start()?",
237
+ );
238
+ }
239
+
240
+ // In remote mode => create a client using the discovered Relay address **and** passing the service reourceId
241
+ return createGrpcClient(
242
+ this._grpc,
243
+ serviceDescriptors,
244
+ this._config.relay.address,
245
+ this._credential,
246
+ false,
247
+ service.volt_id,
248
+ service.id,
249
+ );
250
+ } else {
251
+ return createGrpcClient(
252
+ this._grpc,
253
+ serviceDescriptors,
254
+ service.service_description.address,
255
+ this._credential,
256
+ );
257
+ }
258
+ }
259
+
260
+ get isRemote() {
261
+ return !!(this._config?.relay && this._voltConfig.id);
262
+ }
263
+
264
+ get isVoltRelay() {
265
+ return this.isRemote && !this._config?.relay.cloud;
266
+ }
267
+
268
+ connect() {
269
+ return connectInternal.call(this);
270
+ }
271
+
272
+ disconnect() {
273
+ if (this._voltConnection) {
274
+ this._voltConnection.disconnect();
275
+ this._voltConnection = null;
276
+ }
277
+ }
278
+
279
+ getVoltAPIClient() {
280
+ return getVoltAPIClientInternal.call(this);
281
+ }
282
+
283
+ /**
284
+ * Request resource access.
285
+ * @param {*} targetResourceId
286
+ * @param {*} accessType - see volt.proto for values.
287
+ */
288
+ async RequestAccessBlocking(
289
+ targetResourceId,
290
+ accessType = "VOLT_ACCESS_READ",
291
+ ) {
292
+ try {
293
+ const accessRequest = {
294
+ access: accessType,
295
+ resource_id: targetResourceId,
296
+ };
297
+
298
+ // This will block while the request is pending.
299
+ // TODO - implement streamed rpc on volt? Not sure there is a need really...
300
+ const pollInterval = 10000;
301
+ let decision = "";
302
+ do {
303
+ if (decision) {
304
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
305
+ }
306
+ const accessResponse = await this.RequestAccess(accessRequest);
307
+ decision = accessResponse.decision;
308
+ log("access decision: %s", decision);
309
+ } while (
310
+ decision === "POLICY_DECISION_PROMPT" ||
311
+ decision === "POLICY_DECISION_PENDING"
312
+ );
313
+
314
+ log("requestResourceAccess result is %j", decision);
315
+ return decision === "POLICY_DECISION_PERMIT";
316
+ } catch (err) {
317
+ log("failure requesting resource access [%s]", err.message);
318
+ return Promise.reject(err);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * VoltAPI - resource management
324
+ */
325
+
326
+ CanAccessResource(request) {
327
+ return unaryCall.call(this, "CanAccessResource", request);
328
+ }
329
+
330
+ DeleteResource(request) {
331
+ return unaryCall.call(this, "DeleteResource", request);
332
+ }
333
+
334
+ DiscoverServices(request) {
335
+ return unaryCall.call(this, "DiscoverServices", request);
336
+ }
337
+
338
+ GetResource(request) {
339
+ return unaryCall.call(this, "GetResource", request);
340
+ }
341
+
342
+ GetResources(request) {
343
+ return unaryCall.call(this, "GetResources", request);
344
+ }
345
+
346
+ GetResourceAncestors(request) {
347
+ return unaryCall.call(this, "GetResourceAncestors", request);
348
+ }
349
+
350
+ GetResourceDescendants(request) {
351
+ return unaryCall.call(this, "GetResourceDescendants", request);
352
+ }
353
+
354
+ RequestAccess(request) {
355
+ return unaryCall.call(this, "RequestAccess", request);
356
+ }
357
+
358
+ SaveResource(request) {
359
+ return unaryCall.call(this, "SaveResource", request);
360
+ }
361
+
362
+ SaveResourceAttribute(request) {
363
+ return unaryCall.call(this, "SaveResourceAttribute", request);
364
+ }
365
+
366
+ SetServiceStatus(request) {
367
+ return unaryCall.call(this, "SetServiceStatus", request);
368
+ }
369
+
370
+ /**
371
+ * VoltAPI - volt management
372
+ */
373
+
374
+ Bind(request) {
375
+ return unaryCall.call(this, "Bind", request);
376
+ }
377
+
378
+ DeleteAccess(request) {
379
+ return unaryCall.call(this, "DeleteAccess", request);
380
+ }
381
+
382
+ DeleteVolt(request) {
383
+ return unaryCall.call(this, "DeleteVolt", request);
384
+ }
385
+
386
+ GetAccess(request) {
387
+ return unaryCall.call(this, "GetAccess", request);
388
+ }
389
+
390
+ GetBindings(request) {
391
+ return unaryCall.call(this, "GetBindings", request);
392
+ }
393
+
394
+ GetIdentities(request) {
395
+ return unaryCall.call(this, "GetIdentities", request);
396
+ }
397
+
398
+ GetIdentity(request) {
399
+ return unaryCall.call(this, "GetIdentity", request);
400
+ }
401
+
402
+ GetIdentityToken(request) {
403
+ return unaryCall.call(this, "GetIdentityToken", request);
404
+ }
405
+
406
+ GetPolicy(request) {
407
+ return unaryCall.call(this, "GetPolicy", request);
408
+ }
409
+
410
+ GetSettings(request) {
411
+ return unaryCall.call(this, "GetSettings", request);
412
+ }
413
+
414
+ SaveAccess(request) {
415
+ return unaryCall.call(this, "SaveAccess", request);
416
+ }
417
+
418
+ SaveCloudConnection(request) {
419
+ return unaryCall.call(this, "SaveCloudConnection", request);
420
+ }
421
+
422
+ SaveIdentity(request) {
423
+ return unaryCall.call(this, "SaveIdentity", request);
424
+ }
425
+
426
+ SaveSettings(request) {
427
+ return unaryCall.call(this, "SaveSettings", request);
428
+ }
429
+
430
+ SetAccessRequestDecision(request) {
431
+ return unaryCall.call(this, "SetAccessRequestDecision", request);
432
+ }
433
+
434
+ SetBindingDecision(request) {
435
+ return unaryCall.call(this, "SetBindingDecision", request);
436
+ }
437
+
438
+ Shutdown(request) {
439
+ return unaryCall.call(this, "Shutdown", request);
440
+ }
441
+
442
+ SignVerify(request) {
443
+ return unaryCall.call(this, "SignVerify", request);
444
+ }
445
+
446
+ /**
447
+ * FileAPI
448
+ */
449
+ GetFileDescendants(request) {
450
+ return unaryCall.call(this, "GetFileDescendants", request);
451
+ }
452
+
453
+ /**
454
+ * Asynchronous file download
455
+ * @param {*} request
456
+ * @returns stream
457
+ */
458
+ DownloadFile(request) {
459
+ const grpcClient = this.getVoltAPIClient();
460
+
461
+ const downloadCall = new GRPCCall(
462
+ this,
463
+ "DownloadFile",
464
+ "METHOD_TYPE_SERVER_STREAM",
465
+ );
466
+ return downloadCall.start(grpcClient, request);
467
+ }
468
+
469
+ UploadFile(request) {
470
+ const grpcClient = this.getVoltAPIClient();
471
+
472
+ const uploadCall = new GRPCCall(this, "UploadFile", "METHOD_TYPE_BIDI");
473
+ return uploadCall.start(grpcClient, request);
474
+ }
475
+
476
+ /**
477
+ * Synchronous file download
478
+ * @param {*} request
479
+ * @returns file buffer, base64 encoded
480
+ */
481
+ DownloadFileSync(request) {
482
+ return new Promise((resolve, reject) => {
483
+ const grpcClient = this.getVoltAPIClient();
484
+
485
+ const blocks = [];
486
+
487
+ const call = new GRPCCall(
488
+ this,
489
+ "DownloadFile",
490
+ "METHOD_TYPE_SERVER_STREAM",
491
+ );
492
+
493
+ call.on("data", (response) => {
494
+ switch (response.payload) {
495
+ case "block": {
496
+ blocks.push(response.block);
497
+ break;
498
+ }
499
+ case "status": {
500
+ if (response.status.message) {
501
+ return call.reject(new Error(response.status.message));
502
+ } else {
503
+ // Non-error response status => do nothing and wait for the stream to end.
504
+ }
505
+ break;
506
+ }
507
+ default:
508
+ log("unrecognised response payload %s", response.payload);
509
+ break;
510
+ }
511
+ });
512
+
513
+ call.on("error", (err) => {
514
+ log("error during DownloadFile [%s]", err.message);
515
+ reject(err);
516
+ });
517
+
518
+ call.on("end", () => {
519
+ log("Download finished");
520
+ resolve({ buffer: Buffer.concat(blocks).toString("base64") });
521
+ });
522
+
523
+ call.start(grpcClient, request);
524
+ });
525
+ }
526
+
527
+ /**
528
+ * SqliteDatabaseAPI
529
+ */
530
+ SqlExecute(request) {
531
+ const grpcClient = this.getVoltAPIClient();
532
+
533
+ const subscribeCall = new GRPCCall(this, "Execute", "METHOD_TYPE_BIDI");
534
+ return subscribeCall.start(grpcClient, request);
535
+ }
536
+
537
+ /**
538
+ * WireAPI
539
+ */
540
+ PublishWire(request) {
541
+ const grpcClient = this.getVoltAPIClient();
542
+
543
+ const publishCall = new GRPCCall(this, "PublishWire", "METHOD_TYPE_BIDI");
544
+ return publishCall.start(grpcClient, request);
545
+ }
546
+
547
+ SubscribeWire(request) {
548
+ const grpcClient = this.getVoltAPIClient();
549
+
550
+ const subscribeCall = new GRPCCall(
551
+ this,
552
+ "SubscribeWire",
553
+ "METHOD_TYPE_BIDI",
554
+ );
555
+ return subscribeCall.start(grpcClient, request);
556
+ }
556
557
  }