@tdxvolt/volt-client-grpc 0.16.0-beta.8 → 0.17.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/src/utils.js CHANGED
@@ -1,12 +1,9 @@
1
1
  import ip from "ip";
2
2
  import _ from "lodash";
3
- import forge from "node-forge";
4
- import bs58 from "bs58";
3
+ export * as crypto from "@tdxvolt/volt-client-web";
5
4
 
6
5
  import { constants } from "./constants.js";
7
6
 
8
- const { pki } = forge;
9
-
10
7
  export const createSecureContextOptions = (cryptoOptions) => {
11
8
  const tlsOptions = {
12
9
  cert: Buffer.from(cryptoOptions.cert),
@@ -36,53 +33,7 @@ export const getServiceAddress = (mdnsService) => {
36
33
  return address;
37
34
  };
38
35
 
39
- export const privateKeyToPem = (keyPair) => {
40
- return pki.privateKeyToPem(keyPair.privateKey);
41
- };
42
-
43
- export const publicKeyToPem = (keyPair) => {
44
- return pki.publicKeyToPem(keyPair.publicKey);
45
- };
46
-
47
- export const deserialisePEM = (pem) => {
48
- return pem.replace(/\\r\\n/g, "\r\n");
49
- };
50
-
51
- export const removeCarriageReturn = (inp) => {
52
- return inp.replace(/\r\n/g, "\n");
53
- };
54
-
55
- export const createBase58Hash = (inp) => {
56
- const md = forge.md.sha256.create();
57
- md.update(inp);
58
- const hashBuf = Buffer.from(md.digest().getBytes(), "binary");
59
- return bs58.encode(hashBuf);
60
- };
61
-
62
- export const createPublicKeyFingerprint = (key) => {
63
- // Remove carriage returns before creating hash to bring into line with openSSL / volt format.
64
- return createBase58Hash(removeCarriageReturn(key));
65
- };
66
-
67
- export const base64ToBase64Url = (base64) => {
68
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
69
- };
70
-
71
- export const signBase64 = (key, msg) => {
72
- const md = forge.md.sha256.create();
73
- md.update(msg);
74
- const signature = key.sign(md);
75
- const base64Standard = forge.util.encode64(signature);
76
- return base64ToBase64Url(base64Standard);
77
- };
78
-
79
- export const verifyBase64 = (key, msg, signature) => {
80
- const md = forge.md.sha256.create();
81
- md.update(msg);
82
- return key.verify(md.digest().bytes(), forge.util.decode64(signature));
83
- };
84
-
85
- export const getDIDResolutionURL = (didIn) => {
36
+ export const getDIDResolutionURL = (didIn, registryUrl = "") => {
86
37
  const did = didIn.toLowerCase().split(":");
87
38
  if (did.length < 3) {
88
39
  return "";
@@ -92,75 +43,10 @@ export const getDIDResolutionURL = (didIn) => {
92
43
  return "";
93
44
  }
94
45
 
95
- if (did[1] !== "tdx") {
46
+ if (did[1] !== "volt") {
96
47
  return "";
97
48
  }
98
49
 
99
- let hostName;
100
- let didGUID;
101
- if (did.length === 3) {
102
- hostName = constants.defaultDIDHostName;
103
- didGUID = did[2];
104
- } else {
105
- hostName = did[2];
106
- didGUID = did[3];
107
- }
108
-
109
- return `https://${hostName}/api/identity/${didGUID}`;
110
- };
111
-
112
- const forgePrivateKeyToPem = (decryptedKey) => {
113
- // Convert a Forge private key to an ASN.1 RSAPrivateKey
114
- const asnPrivateKey = pki.privateKeyToAsn1(decryptedKey);
115
-
116
- // Wrap an RSAPrivateKey ASN.1 object in a PKCS#8 ASN.1 PrivateKeyInfo
117
- const privateKeyInfo = pki.wrapRsaPrivateKey(asnPrivateKey);
118
-
119
- // Convert a PKCS#8 ASN.1 PrivateKeyInfo to PEM
120
- return pki.privateKeyInfoToPem(privateKeyInfo);
121
- };
122
-
123
- export const createRSAKey = (passphrase = "") => {
124
- return new Promise((resolve, reject) => {
125
- pki.rsa.generateKeyPair({ bits: 2048, workers: -1 }, (err, keypair) => {
126
- if (err) {
127
- reject(err);
128
- } else {
129
- let pem;
130
- if (passphrase) {
131
- pem = pki.encryptRsaPrivateKey(keypair.privateKey, passphrase);
132
- } else {
133
- pem = forgePrivateKeyToPem(keypair.privateKey);
134
- }
135
-
136
- resolve({ keypair, pem });
137
- }
138
- });
139
- });
140
- };
141
-
142
- export const sha256Base64 = (msg) => {
143
- const md = forge.md.sha256.create();
144
- md.update(msg);
145
- const base64Standard = forge.util.encode64(md.digest().bytes());
146
- return base64ToBase64Url(base64Standard);
147
- };
148
-
149
- export const decryptPrivateKey = (pem, passphrase) => {
150
- const decryptedKey = pki.decryptRsaPrivateKey(pem, passphrase);
151
- return forgePrivateKeyToPem(decryptedKey);
152
- };
153
-
154
- export const jwsSignature = (signature) => {
155
- //
156
- // Use an unattached payload JWS signature for the DID document proof.
157
- // This is a JWS with an empty payload and a detached payload digest.
158
- //
159
- // (https://tools.ietf.org/html/rfc7797#appendix-F) as used in the VC spec
160
- // (https://www.w3.org/TR/vc-data-model/).
161
- //
162
- const jwsHeader = base64ToBase64Url(
163
- forge.util.encode64('{"alg":"RS256","b64": false}')
164
- );
165
- return jwsHeader + ".." + signature;
50
+ const lookupUrl = registryUrl || `https://${constants.defaultDIDHostName}`;
51
+ return `${lookupUrl}/did/${didIn}`;
166
52
  };
@@ -2,7 +2,6 @@
2
2
  import debug from "debug";
3
3
  import ip from "ip";
4
4
  import bent from "bent";
5
- import forge from "node-forge";
6
5
  import lodash from "lodash";
7
6
  import VoltConnection from "./volt-connection.js";
8
7
  import { join } from "path";
@@ -17,8 +16,12 @@ import { constants } from "./constants.js";
17
16
  import GRPCCall from "./grpc-call.js";
18
17
  import RpcInvocation from "./rpc-invocation.js";
19
18
  import { v4 as generateId } from "uuid";
19
+ import {
20
+ publicKeyToPem,
21
+ removeCarriageReturn,
22
+ signBase64
23
+ } from "@tdxvolt/volt-client-web";
20
24
 
21
- const { pki } = forge;
22
25
  const { filter, pick } = lodash;
23
26
 
24
27
  const log = debug("volt-client-grpc:volt-client-internal");
@@ -54,9 +57,15 @@ function issueAuthenticate(authenticateRequest, ttl) {
54
57
  // This is the signing CA used by the volt.
55
58
  this._credential.cache.ca = authenticateResponse.chain;
56
59
 
57
- // Identity resource id is assigned by the volt.
60
+ // The session id is assigned by the volt.
58
61
  this._credential.cache.session_id = authenticateResponse.session_id;
59
62
 
63
+ if (!this._credential.cache.identity_did) {
64
+ // Identity DID has been assigned by the volt.
65
+ this._credential.cache.identity_did =
66
+ authenticateResponse.identity_did;
67
+ }
68
+
60
69
  this._credential.saveCache();
61
70
  break;
62
71
  }
@@ -67,6 +76,20 @@ function issueAuthenticate(authenticateRequest, ttl) {
67
76
  case "POLICY_DECISION_PROMPT":
68
77
  case "POLICY_DECISION_PENDING": {
69
78
  log("access pending approval - waiting...");
79
+
80
+ // This is the signing CA used by the volt.
81
+ this._credential.cache.ca = authenticateResponse.chain;
82
+
83
+ // The session id is assigned by the volt.
84
+ this._credential.cache.session_id = authenticateResponse.session_id;
85
+
86
+ if (!this._credential.cache.identity_did) {
87
+ // Identity DID has been assigned by the volt.
88
+ this._credential.cache.identity_did =
89
+ authenticateResponse.identity_did;
90
+ }
91
+
92
+ this._credential.saveCache();
70
93
  break;
71
94
  }
72
95
  default:
@@ -91,7 +114,10 @@ function findDIDDocumentService(document, serviceType) {
91
114
  return matches;
92
115
  }
93
116
 
94
- export async function authenticateInternal(useDID = true) {
117
+ export async function authenticateInternal(
118
+ sessionOnly = false,
119
+ ownDID = false
120
+ ) {
95
121
  try {
96
122
  // Check if we need to resolve a Relay volt.
97
123
  if (
@@ -103,7 +129,7 @@ export async function authenticateInternal(useDID = true) {
103
129
  log("attempting to retrieve Relay information from %s", relayURL);
104
130
  this._voltConfig.relay = await fetchVoltConfig.call(
105
131
  this,
106
- `${relayURL}/discovery`
132
+ `${relayURL}/discover`
107
133
  );
108
134
 
109
135
  if (this._voltConfig.relay.ca_pem) {
@@ -128,8 +154,8 @@ export async function authenticateInternal(useDID = true) {
128
154
  }
129
155
 
130
156
  const keyPair = this._credential.getKey();
131
- const publicKeyPem = voltUtils.removeCarriageReturn(
132
- pki.publicKeyToPem(keyPair.publicKey)
157
+ const publicKeyPem = removeCarriageReturn(
158
+ publicKeyToPem(keyPair.publicKey)
133
159
  );
134
160
 
135
161
  if (!this._credential.cache.bindIp) {
@@ -142,12 +168,12 @@ export async function authenticateInternal(useDID = true) {
142
168
 
143
169
  // Form the request message.
144
170
  const authenticateRequest = {
145
- binding_name: this._config.client_name,
171
+ client_name: this._config.client_name,
146
172
  host: this._credential.cache.bindIp
147
173
  };
148
174
 
149
175
  if (this._voltConfig.challenge_code) {
150
- authenticateRequest.challenge = voltUtils.signBase64(
176
+ authenticateRequest.challenge = signBase64(
151
177
  keyPair.privateKey,
152
178
  this._voltConfig.challenge_code
153
179
  );
@@ -157,68 +183,58 @@ export async function authenticateInternal(useDID = true) {
157
183
  );
158
184
  }
159
185
 
160
- if (!useDID) {
186
+ if (sessionOnly) {
187
+ // We want to create a session-based authentication based simply on our key, rather than a
188
+ // fully-blown DID-based authentication.
189
+ log("creating session-only request");
161
190
  authenticateRequest.public_key = publicKeyPem;
162
- } else if (!this._credential.cache.identity_id) {
163
- // If we don't have an identity id, then we need to create a DID.
164
- this._credential.cache.identity_id = `did:volt:${generateId()}`;
165
-
166
- const did = this._credential.cache.identity_id;
167
-
168
- // Order of properties in the DID document is important,
169
- // they must be in alphabetical order throughout the document.
170
- // This is because the DID document is signed and the signature
171
- // is over the entire document.
172
- const didDocument = {
173
- "@context": "https://www.w3.org/ns/did/v1",
174
- authentication: [`${did}#key-1`],
175
- description: this._config.client_name,
176
- id: did,
177
- verificationMethod: [
178
- {
179
- controller: did,
180
- id: `${did}#key-1`,
181
- publicKeyPem: publicKeyPem,
182
- type: "RsaSignature2018"
183
- }
184
- ]
185
- };
186
-
187
- const signDocument = JSON.stringify(didDocument, null, 2);
188
- log("signing did document");
189
- log(signDocument);
190
-
191
- // Sign the DID document.
192
- const documentDigest = voltUtils.signBase64(
193
- keyPair.privateKey,
194
- signDocument
195
- );
196
-
197
- const proofSignature = voltUtils.jwsSignature(documentDigest);
198
-
199
- // Create the DID document proof.
200
- didDocument.proof = {
201
- created: new Date().toISOString(),
202
- jws: proofSignature,
203
- proofPurpose: "assertionMethod",
204
- type: "RsaSignature2018",
205
- verificationMethod: `${did}#key-1`
206
- };
207
-
208
- // Set the DID document with proof in the authenticate request.
209
- authenticateRequest.did_document = JSON.stringify(didDocument, null, 2);
210
-
211
- // Now sign the entire DID document, including the proof and set it in the
212
- // bind request. This is required by the Volt to verify that we have the private key for the DID.
213
- authenticateRequest.did_document_signature = voltUtils.signBase64(
214
- keyPair.privateKey,
215
- authenticateRequest.did_document
216
- );
191
+ } else if (!this._credential.cache.identity_did) {
192
+ if (!ownDID) {
193
+ // We don't want to have to manage our own DID, so we ask the Volt to do it for us.
194
+ // The Volt will create a new DID and return it to us in the response.
195
+ log("creating DID-based request");
196
+ authenticateRequest.did_public_key = publicKeyPem;
197
+ } else {
198
+ // We want to create and manage our own DID.
199
+ const did =
200
+ (this._credential.cache.identity_did = `did:volt:${generateId()}`);
201
+
202
+ // Order of properties in the DID document is important,
203
+ // they must be in alphabetical order throughout the document.
204
+ // This is because the DID document is signed and the signature
205
+ // is over the entire document.
206
+ const didDocument = {
207
+ "@context": "https://www.w3.org/ns/did/v1",
208
+ authentication: [`${did}#key-1`],
209
+ controller: did,
210
+ description: this._config.client_name,
211
+ id: did,
212
+ verificationMethod: [
213
+ {
214
+ id: `${did}#key-1`,
215
+ publicKeyPem: publicKeyPem,
216
+ type: "Ed25519VerificationKey2018"
217
+ }
218
+ ]
219
+ };
220
+
221
+ // Set the DID document with proof in the authenticate request.
222
+ authenticateRequest.did_document = JSON.stringify(didDocument, null, 2);
223
+
224
+ // Now sign the entire DID document, including the proof and set it in the
225
+ // bind request. This is required by the Volt to verify that we have the private key for the DID.
226
+ authenticateRequest.did_document_signature = signBase64(
227
+ keyPair.privateKey,
228
+ authenticateRequest.did_document
229
+ );
217
230
 
218
- log("did document is %s", authenticateRequest.did_document);
219
- log("did signature is %s", authenticateRequest.did_document_signature);
231
+ log("did document is %s", authenticateRequest.did_document);
232
+ log("did signature is %s", authenticateRequest.did_document_signature);
233
+ }
220
234
  } else {
221
- authenticateRequest.did = this._credential.cache.identity_id;
235
+ // We already have a DID cached, so re-use it.
236
+ log("using cached DID %s", this._credential.cache.identity_did);
237
+ authenticateRequest.did = this._credential.cache.identity_did;
222
238
  }
223
239
 
224
240
  log("authenticate TTL is ", this._config.bindRequestTTL);
@@ -287,18 +303,18 @@ export function connectInternal(helloPayload) {
287
303
  this._voltConnection.on("error", (err) => {
288
304
  // Should auto-reconnect.
289
305
  log("Volt connection error [%s]", err.message);
290
- if (err.message.endsWith("session invalid")) {
291
- log("Volt session invalid - clearing session");
292
- this._credential.cache.session_id = null;
293
- this._credential.cache.cert = null;
294
- this._credential.saveCache();
295
- }
296
-
297
306
  if (this._connected) {
298
307
  onDisconnected();
299
308
  this.emit("connected", false);
300
309
  }
301
310
 
311
+ if (err.message.endsWith("session invalid")) {
312
+ log("Volt session invalid - clearing session");
313
+ delete this._credential.cache.session_id;
314
+ delete this._credential.cache.cert;
315
+ this._credential.saveCache();
316
+ }
317
+
302
318
  this.emit("connectionError", err);
303
319
  });
304
320
 
@@ -312,10 +328,24 @@ export function connectInternal(helloPayload) {
312
328
 
313
329
  this._voltConnection.on("invoke_request", (invoke_request) => {
314
330
  const invokeId = invoke_request.invoke_id;
315
- if (this._activeRPC[invokeId]) {
316
- this._activeRPC[invokeId].parsePayload(invoke_request);
331
+
332
+ let rpcInvocation = this._activeRPC[invokeId];
333
+
334
+ if (rpcInvocation) {
335
+ if (rpcInvocation.started) {
336
+ rpcInvocation.parsePayload(invoke_request);
337
+ } else {
338
+ rpcInvocation
339
+ .initialise(invoke_request)
340
+ .then(() => {
341
+ this.emit("invoke_request", rpcInvocation);
342
+ })
343
+ .catch((err) => {
344
+ log("invoke_request - error [%s]", err.message);
345
+ });
346
+ }
317
347
  } else {
318
- const rpcInvocation = new RpcInvocation(this, this._voltConnection);
348
+ rpcInvocation = new RpcInvocation(this, this._voltConnection);
319
349
 
320
350
  rpcInvocation.on("end", () => {
321
351
  log("removing active RPC [%s]", invokeId);
@@ -324,9 +354,11 @@ export function connectInternal(helloPayload) {
324
354
 
325
355
  rpcInvocation
326
356
  .initialise(invoke_request)
327
- .then(() => {
357
+ .then((response) => {
328
358
  this._activeRPC[invokeId] = rpcInvocation;
329
- this.emit("invoke_request", rpcInvocation);
359
+ if (!response.key_exchange) {
360
+ this.emit("invoke_request", rpcInvocation);
361
+ }
330
362
  })
331
363
  .catch((err) => {
332
364
  log("invoke_request - error [%s]", err.message);
@@ -472,6 +504,7 @@ export async function fetchVoltConfig(discovery_url) {
472
504
  "public_key",
473
505
  "fingerprint",
474
506
  "address",
507
+ "http_address",
475
508
  "ca_pem",
476
509
  "challenge_code",
477
510
  "cloud",
@@ -59,6 +59,7 @@ export class VoltClient extends EventEmitter {
59
59
  /**
60
60
  * Initialises a binding and connection to the volt.
61
61
  * @param {string} [config] - the location of the Volt configuration file, or a configuration object
62
+ * @param {object} [extras] - additional configuration properties
62
63
  */
63
64
  async initialise(config, extras = {}) {
64
65
  try {
@@ -74,10 +75,15 @@ export class VoltClient extends EventEmitter {
74
75
  configPath = path.resolve(config);
75
76
  log("Attempt to load config from file %s", configPath);
76
77
  try {
77
- const configContents = await readFile(
78
- new URL(configPath, import.meta.url)
79
- );
80
- configJSON = JSON.parse(configContents);
78
+ if (fs.existsSync(configPath)) {
79
+ const configContents = await readFile(
80
+ new URL(configPath, import.meta.url)
81
+ );
82
+ configJSON = JSON.parse(configContents);
83
+ } else {
84
+ // There is currently no file at the given configuration path.
85
+ configJSON = {};
86
+ }
81
87
  } catch (err) {
82
88
  log("failure loading config file %s [%s]", configPath, err.message);
83
89
  throw new Error(
@@ -94,15 +100,15 @@ export class VoltClient extends EventEmitter {
94
100
  throw new Error("config argument is required to be an object");
95
101
  }
96
102
 
103
+ this._config = { ...extras, ...configJSON };
104
+
97
105
  if (
98
- !configJSON.client_name ||
99
- typeof configJSON.client_name !== "string"
106
+ !this._config.client_name ||
107
+ typeof this._config.client_name !== "string"
100
108
  ) {
101
109
  throw new Error("client_name property required in config");
102
110
  }
103
111
 
104
- this._config = { ...configJSON, ...extras };
105
-
106
112
  let voltDID = "";
107
113
  let voltHttpAddress = "";
108
114
  if (typeof this._config.volt === "string") {
@@ -111,6 +117,7 @@ export class VoltClient extends EventEmitter {
111
117
  } else {
112
118
  voltHttpAddress = this._config.volt;
113
119
  }
120
+ delete this._config.volt;
114
121
  } else if (typeof this._config.volt !== "object") {
115
122
  throw new Error("configuration is missing the 'volt' object");
116
123
  } else {
@@ -125,7 +132,7 @@ export class VoltClient extends EventEmitter {
125
132
  } else if (voltHttpAddress && !this._config?.volt?.id) {
126
133
  const discoConfig = await fetchVoltConfig.call(
127
134
  this,
128
- `${voltHttpAddress}/discovery`
135
+ `${voltHttpAddress}/discover`
129
136
  );
130
137
  this._config = {
131
138
  ...this._config,
@@ -134,8 +141,12 @@ export class VoltClient extends EventEmitter {
134
141
  }
135
142
 
136
143
  this._voltConfig = this._config.volt;
137
- this._voltConfig.did = voltDID;
138
- this._voltConfig.http_address = voltHttpAddress;
144
+ if (voltDID) {
145
+ this._voltConfig.did = voltDID;
146
+ }
147
+ if (voltHttpAddress) {
148
+ this._voltConfig.http_address = voltHttpAddress;
149
+ }
139
150
 
140
151
  if (!this._voltConfig.id || typeof this._voltConfig.id !== "string") {
141
152
  throw new Error("'id' property is missing in Volt configuration");
@@ -183,9 +194,12 @@ export class VoltClient extends EventEmitter {
183
194
  if (!isBound) {
184
195
  // This is usually because of request being denied.
185
196
  throw new Error("Volt authentication failed");
197
+ } else {
198
+ // Perist the configuration.
199
+ this._config = this._credential.saveCache();
186
200
  }
187
201
 
188
- return Promise.resolve();
202
+ return Promise.resolve(this._config);
189
203
  } catch (err) {
190
204
  log("Failure starting Volt client [%s]", err.message);
191
205
  throw err;
@@ -654,6 +668,13 @@ export class VoltClient extends EventEmitter {
654
668
  });
655
669
  }
656
670
 
671
+ SqlTransaction(request) {
672
+ const grpcClient = this.getVoltAPIClient();
673
+
674
+ const call = new GRPCCall(this, "Transaction", "METHOD_TYPE_BIDI");
675
+ return call.start(grpcClient, request);
676
+ }
677
+
657
678
  /**
658
679
  * WireAPI
659
680
  */
@@ -19,10 +19,9 @@ function _cleanUp(immediateReconnect) {
19
19
  this._pingTimeoutTimer = 0;
20
20
  }
21
21
 
22
- if (
23
- !this._dying &&
24
- (immediateReconnect || (this._autoRetry && !this._reconnectTimer))
25
- ) {
22
+ if (this._dying) {
23
+ clearTimeout(this._reconnectTimer);
24
+ } else if (immediateReconnect || (this._autoRetry && !this._reconnectTimer)) {
26
25
  this._reconnectTimer = setTimeout(
27
26
  () => {
28
27
  this._reconnectTimer = 0;
@@ -48,13 +47,18 @@ function _doConnect(connectHello) {
48
47
 
49
48
  const grpcClient = this._voltClient.getVoltAPIClient();
50
49
 
51
- let helloPayload = connectHello;
52
- if (!helloPayload) {
53
- log("defaulting hello payload");
54
- helloPayload = {
55
- hello: {}
56
- };
57
- }
50
+ // Notify the Volt of our ping interval. If the Volt doesn't receive a ping from us
51
+ // within this time, it will assume the connection is dead.
52
+ const defaultHello = {
53
+ ping_interval: this._pingInterval,
54
+ timestamp: Date.now()
55
+ };
56
+
57
+ const helloPayload = { ...connectHello } || {};
58
+
59
+ helloPayload.hello = { ...defaultHello, ...connectHello?.hello };
60
+
61
+ log("hello is %j", helloPayload);
58
62
 
59
63
  this.call.on("error", (err) => {
60
64
  log("error on connection stream [%s]", err.message);
@@ -72,17 +76,22 @@ function _doConnect(connectHello) {
72
76
  clearTimeout(this._pingTimeoutTimer);
73
77
  }
74
78
 
75
- // Schedule the next ping.
76
- _startPingTimer.call(this);
77
-
78
79
  this.emit("ping", response.ping.timestamp);
79
80
  break;
80
81
  }
81
82
  case "acknowledge": {
82
83
  if (!this._connectionId) {
83
84
  this._connectionId = response.acknowledge.connection_id;
85
+
86
+ // Set the ping timeout interval to the value returned by the Volt. If we don't
87
+ // hear from the Volt in this time, we'll assume the connection is dead.
88
+ this._timeoutInterval = response.acknowledge.ping_interval;
89
+
84
90
  log("connection id is %s", this._connectionId);
85
91
  this.emit("connected", this._connectionId);
92
+
93
+ // Schedule the next ping.
94
+ _startPingTimer.call(this);
86
95
  }
87
96
  break;
88
97
  }
@@ -110,8 +119,6 @@ function _doConnect(connectHello) {
110
119
  });
111
120
 
112
121
  this.call.start(grpcClient, helloPayload);
113
-
114
- _startPingTimer.call(this);
115
122
  }
116
123
 
117
124
  function _startPingTimer() {
@@ -138,6 +145,8 @@ function _startPingTimer() {
138
145
  _connectionTimeoutHandler.bind(this),
139
146
  this._timeoutInterval
140
147
  );
148
+
149
+ _startPingTimer.call(this);
141
150
  }, this._pingInterval);
142
151
  }
143
152