@tdxvolt/volt-client-grpc 0.1.1 → 0.1.2

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