@ondewo/s2t-client-typescript 7.5.0 → 7.5.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.
package/README.md CHANGED
@@ -106,3 +106,132 @@ provider.stop(); // stops the background refresh loop so the process can exit
106
106
  A runnable version of exactly this flow, configured from `examples/environment.env`, lives in
107
107
  `examples/ts-client.ts`.
108
108
 
109
+ ## TLS, mutual TLS and certificates
110
+
111
+ This package is a **gRPC-web** client. Its generated clients send every call through the browser's `XMLHttpRequest`
112
+ to a gRPC-web proxy (Envoy) in front of the ONDEWO service, so TLS is the browser's TLS: the browser verifies the
113
+ server certificate against its own (operating system / browser) trust store, and a client certificate for mutual TLS
114
+ can only come from the browser's own certificate store. Page code cannot hand a CA certificate, a client certificate
115
+ or a private key to the browser, so this SDK takes none of them; never ship a private key to a browser.
116
+
117
+ `createGrpcWebEndpoint` turns `host` / `port` / `useSecureChannel` into the `hostname` URL and the client options
118
+ every generated `*Client` / `*PromiseClient` takes:
119
+
120
+ | Mode | `createGrpcWebEndpoint` config | Where the certificates live |
121
+ |--------------------------------|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
122
+ | Plaintext (not for production) | `useSecureChannel: false` | none; builds `http://host:port` and logs a warning naming `host:port` |
123
+ | TLS, publicly trusted server | `useSecureChannel: true` (the default) | the server certificate chains to a CA the browser already trusts |
124
+ | TLS, private CA | `useSecureChannel: true` | install the CA (`ca.pem`) in the operating system or browser trust store |
125
+ | Mutual TLS | `useSecureChannel: true`, plus `withCredentials: true` cross-origin | install the client certificate and key (`client.p12`) in the operating system or browser certificate store; the proxy requests it and verifies it against its CA |
126
+
127
+ Rules the code enforces:
128
+
129
+ - A config carrying `grpcCert`, `grpcClientCert` or `grpcClientKey` (or the Python spellings `grpc_cert`,
130
+ `grpc_client_cert`, `grpc_client_key`) with a non-empty value throws an `Error` naming the field, instead of
131
+ silently ignoring a certificate you meant to use. Empty values are ignored, so a config ported from another ONDEWO
132
+ SDK with blank TLS fields still works.
133
+ - `host` is a bare host name or IP address (no scheme, credentials, path or port); a bare IPv6 literal is bracketed
134
+ (`::1` becomes `https://[::1]:50051`). `port` is an integer 1-65535 (number or numeric string).
135
+ - `useSecureChannel` and `withCredentials` must be booleans: parse environment strings yourself (`'false'` is refused,
136
+ not read as `true`).
137
+ - `useSecureChannel: false` logs a warning naming `host:port` through `console.warn`, or through the logger passed as
138
+ the second argument. No error message renders a value of a refused field or the host of a refused URL.
139
+ - `withCredentials: true` is gRPC-web's option for cross-origin calls: only then does the browser send cookies, HTTP
140
+ authentication **and its TLS client certificate** to a proxy on another origin. A same-origin proxy does not need it.
141
+
142
+ ```ts
143
+ import { createGrpcWebEndpoint } from '@ondewo/s2t-client-typescript/auth/offlineTokenProvider';
144
+ import { Speech2TextPromiseClient } from '@ondewo/s2t-client-typescript/api/ondewo/s2t/speech-to-text_grpc_web_pb';
145
+
146
+ const endpoint = createGrpcWebEndpoint({
147
+ host: 's2t.example.com',
148
+ port: 443,
149
+ withCredentials: true // only for mutual TLS against a proxy on another origin
150
+ });
151
+ const client = new Speech2TextPromiseClient(endpoint.hostname, null, endpoint.options);
152
+ ```
153
+
154
+ **Node.js.** The generated clients need `XMLHttpRequest`, which Node.js does not provide (a call fails with
155
+ `XMLHttpRequest is not defined`), so this package's gRPC calls run in browsers only; in Node.js only the Keycloak
156
+ `login` helper is usable. There is therefore no Node.js path for a custom CA or a client certificate in this SDK: for
157
+ a server-side client use the ONDEWO Python SDK, or generate a native `@grpc/grpc-js` client from the
158
+ [API protos](https://github.com/ondewo/ondewo-s2t-api) and pass your PEM files to `credentials.createSsl(ca, clientKey, clientCert)`.
159
+
160
+ ### The proxy side of mutual TLS
161
+
162
+ The browser only offers a client certificate when the TLS server asks for one. With Envoy as the gRPC-web proxy:
163
+
164
+ ```yaml
165
+ transport_socket:
166
+ name: envoy.transport_sockets.tls
167
+ typed_config:
168
+ '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
169
+ require_client_certificate: true
170
+ common_tls_context:
171
+ tls_certificates:
172
+ - certificate_chain: { filename: /etc/envoy/certs/server.pem }
173
+ private_key: { filename: /etc/envoy/certs/server.key }
174
+ validation_context:
175
+ trusted_ca: { filename: /etc/envoy/certs/ca.pem }
176
+ ```
177
+
178
+ For a cross-origin page the CORS policy must allow credentials with an explicit origin (`allow_credentials: true`;
179
+ `Access-Control-Allow-Origin: *` is rejected by the browser for a credentialed request). Envoy may in turn connect to
180
+ the ONDEWO service over TLS or mutual TLS with its own (upstream) certificate.
181
+
182
+ ### A test PKI with openssl
183
+
184
+ A CA, a server certificate with SANs, and a client certificate with the `clientAuth` extended key usage, bundled as
185
+ PKCS#12 for import into a browser or operating system certificate store. For tests only: the keys are unencrypted.
186
+
187
+ ```bash
188
+ openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 365 \
189
+ -subj "/CN=Test CA" -keyout ca.key -out ca.pem
190
+
191
+ printf 'subjectAltName=DNS:localhost,IP:127.0.0.1\nextendedKeyUsage=serverAuth\n' > server.ext
192
+ openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
193
+ -subj "/CN=localhost" -keyout server.key -out server.csr
194
+ openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 365 \
195
+ -extfile server.ext -out server.pem
196
+
197
+ printf 'extendedKeyUsage=clientAuth\n' > client.ext
198
+ openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
199
+ -subj "/CN=my-client" -keyout client.key -out client.csr
200
+ openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 365 \
201
+ -extfile client.ext -out client.pem
202
+ openssl pkcs12 -export -in client.pem -inkey client.key -certfile ca.pem -name my-client -out client.p12
203
+
204
+ chmod 600 *.key client.p12
205
+ openssl verify -CAfile ca.pem server.pem client.pem
206
+ ```
207
+
208
+ Envoy uses `server.pem` / `server.key` and trusts `ca.pem` for its clients; the browser trusts `ca.pem` and imports
209
+ `client.p12`.
210
+
211
+ ### TLS security notes
212
+
213
+ - The private key of a client certificate belongs in the operating system / browser certificate store, never in page
214
+ code, a bundle, `localStorage` or a config file served to the browser. This SDK refuses one rather than carry it.
215
+ - `createGrpcWebEndpoint` returns only the URL and `{ withCredentials }`; logging it reveals no secret.
216
+ - The Keycloak tokens are secrets too: `JSON.stringify(provider)`, `console.log(provider)` and `util.inspect(provider)`
217
+ of the `OfflineTokenProvider` (also nested in another object) render the access and refresh tokens as
218
+ `***REDACTED***` (`getAuthorizationHeader()` still returns the real one). Do not log the `Authorization` header
219
+ yourself.
220
+ - `withCredentials: true` also sends the page's cookies for the proxy's origin; restrict the proxy's allowed origins.
221
+
222
+ ### TLS troubleshooting
223
+
224
+ grpc-web reports a failed TLS connection only as a generic error (the browser hides the TLS cause from JavaScript);
225
+ the cause is in the browser's developer tools (Console / Network tab):
226
+
227
+ - **`net::ERR_CERT_AUTHORITY_INVALID`**: the server certificate does not chain to a CA the browser trusts. Install
228
+ the CA in the trust store, or use a publicly trusted certificate.
229
+ - **`net::ERR_CERT_COMMON_NAME_INVALID`**: the host you connect to is not among the certificate's subject alternative
230
+ names. Connect by a name in the SAN, or reissue the certificate (an IP needs an `IP:` SAN).
231
+ - **`net::ERR_BAD_SSL_CLIENT_AUTH_CERT`** / **`net::ERR_SSL_CLIENT_AUTH_CERT_NEEDED`**: the proxy requires a client
232
+ certificate and the browser offered none, or one not signed by the proxy's `trusted_ca`. Import `client.p12`, pick
233
+ it when the browser asks, and check `openssl verify -CAfile ca.pem client.pem`.
234
+ - **Mixed content blocked**: an `https://` page cannot call an `http://` endpoint; use `useSecureChannel: true`.
235
+ - **CORS error only with `withCredentials: true`**: the proxy answers with `Access-Control-Allow-Origin: *` or
236
+ without `Access-Control-Allow-Credentials: true`.
237
+
@@ -13,13 +13,7 @@
13
13
 
14
14
  var jspb = require('google-protobuf');
15
15
  var goog = jspb;
16
- var global =
17
- (typeof globalThis !== 'undefined' && globalThis) ||
18
- (typeof window !== 'undefined' && window) ||
19
- (typeof global !== 'undefined' && global) ||
20
- (typeof self !== 'undefined' && self) ||
21
- (function () { return this; }).call(null) ||
22
- Function('return this')();
16
+ var global = globalThis;
23
17
 
24
18
  goog.exportSymbol('proto.google.protobuf.Empty', null, global);
25
19
  /**
@@ -88,7 +82,7 @@ proto.google.protobuf.Empty.toObject = function(includeInstance, msg) {
88
82
 
89
83
  /**
90
84
  * Deserializes binary data (in protobuf wire format).
91
- * @param {jspb.ByteSource} bytes The bytes to deserialize.
85
+ * @param {jspb.binary.bytesource.ByteSource} bytes The bytes to deserialize.
92
86
  * @return {!proto.google.protobuf.Empty}
93
87
  */
94
88
  proto.google.protobuf.Empty.deserializeBinary = function(bytes) {
@@ -13,13 +13,7 @@
13
13
 
14
14
  var jspb = require('google-protobuf');
15
15
  var goog = jspb;
16
- var global =
17
- (typeof globalThis !== 'undefined' && globalThis) ||
18
- (typeof window !== 'undefined' && window) ||
19
- (typeof global !== 'undefined' && global) ||
20
- (typeof self !== 'undefined' && self) ||
21
- (function () { return this; }).call(null) ||
22
- Function('return this')();
16
+ var global = globalThis;
23
17
 
24
18
  goog.exportSymbol('proto.google.protobuf.ListValue', null, global);
25
19
  goog.exportSymbol('proto.google.protobuf.NullValue', null, global);
@@ -134,7 +128,7 @@ fieldsMap: (f = msg.getFieldsMap()) ? f.toObject(includeInstance, proto.google.p
134
128
 
135
129
  /**
136
130
  * Deserializes binary data (in protobuf wire format).
137
- * @param {jspb.ByteSource} bytes The bytes to deserialize.
131
+ * @param {jspb.binary.bytesource.ByteSource} bytes The bytes to deserialize.
138
132
  * @return {!proto.google.protobuf.Struct}
139
133
  */
140
134
  proto.google.protobuf.Struct.deserializeBinary = function(bytes) {
@@ -161,7 +155,7 @@ proto.google.protobuf.Struct.deserializeBinaryFromReader = function(msg, reader)
161
155
  case 1:
162
156
  var value = msg.getFieldsMap();
163
157
  reader.readMessage(value, function(message, reader) {
164
- jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readMessage, proto.google.protobuf.Value.deserializeBinaryFromReader, "", new proto.google.protobuf.Value());
158
+ jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readStringRequireUtf8, jspb.BinaryReader.prototype.readMessage, proto.google.protobuf.Value.deserializeBinaryFromReader, "", new proto.google.protobuf.Value());
165
159
  });
166
160
  break;
167
161
  default:
@@ -195,7 +189,13 @@ proto.google.protobuf.Struct.serializeBinaryToWriter = function(message, writer)
195
189
  var f = undefined;
196
190
  f = message.getFieldsMap(true);
197
191
  if (f && f.getLength() > 0) {
198
- f.serializeBinary(1, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeMessage, proto.google.protobuf.Value.serializeBinaryToWriter);
192
+ jspb.internal.public_for_gencode.serializeMapToBinary(
193
+ message.getFieldsMap(true),
194
+ 1,
195
+ writer,
196
+ jspb.BinaryWriter.prototype.writeString,
197
+ jspb.BinaryWriter.prototype.writeMessage,
198
+ proto.google.protobuf.Value.serializeBinaryToWriter);
199
199
  }
200
200
  };
201
201
 
@@ -303,7 +303,7 @@ listValue: (f = msg.getListValue()) && proto.google.protobuf.ListValue.toObject(
303
303
 
304
304
  /**
305
305
  * Deserializes binary data (in protobuf wire format).
306
- * @param {jspb.ByteSource} bytes The bytes to deserialize.
306
+ * @param {jspb.binary.bytesource.ByteSource} bytes The bytes to deserialize.
307
307
  * @return {!proto.google.protobuf.Value}
308
308
  */
309
309
  proto.google.protobuf.Value.deserializeBinary = function(bytes) {
@@ -336,7 +336,7 @@ proto.google.protobuf.Value.deserializeBinaryFromReader = function(msg, reader)
336
336
  msg.setNumberValue(value);
337
337
  break;
338
338
  case 3:
339
- var value = /** @type {string} */ (reader.readString());
339
+ var value = /** @type {string} */ (reader.readStringRequireUtf8());
340
340
  msg.setStringValue(value);
341
341
  break;
342
342
  case 4:
@@ -700,7 +700,7 @@ valuesList: jspb.Message.toObjectList(msg.getValuesList(),
700
700
 
701
701
  /**
702
702
  * Deserializes binary data (in protobuf wire format).
703
- * @param {jspb.ByteSource} bytes The bytes to deserialize.
703
+ * @param {jspb.binary.bytesource.ByteSource} bytes The bytes to deserialize.
704
704
  * @return {!proto.google.protobuf.ListValue}
705
705
  */
706
706
  proto.google.protobuf.ListValue.deserializeBinary = function(bytes) {