@ondewo/sip-client-nodejs 5.4.0 → 5.4.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.
package/README.md CHANGED
@@ -70,3 +70,97 @@ npm
70
70
  └── README.md
71
71
  ```
72
72
 
73
+ ## TLS, mutual TLS and certificates
74
+
75
+ gRPC encrypts with **TLS** ("SSL" in names such as `credentials.createSsl` or `grpc.ssl_target_name_override` is legacy naming). The package ships a channel helper, `auth/grpcChannel` (exported from the package root), that builds the `@grpc/grpc-js` credentials and channel options for every generated client:
76
+
77
+ | Mode | `useSecureChannel` | Config fields |
78
+ |-----------------------------------------|--------------------|-----------------------------------------------------------------|
79
+ | Plaintext (not for production) | `false` | none |
80
+ | TLS, server verified by the trust store | `true` (default) | none |
81
+ | TLS, server verified by your CA | `true` (default) | `grpcCert` = PEM of the CA that signed the server certificate |
82
+ | Mutual TLS | `true` (default) | `grpcCert` (optional) plus `grpcClientCert` and `grpcClientKey` |
83
+
84
+ Rules the code enforces:
85
+
86
+ - The three fields hold **PEM content** (`string` or `Buffer`), **not file paths**. Read the files yourself; a value without a PEM header line is refused.
87
+ - `grpcClientCert` and `grpcClientKey` go together: setting only one throws when the `GrpcClientConfig` is built, and `createChannelCredentials` refuses half a pair again before anything reaches gRPC. Empty strings on both mean plain server-authenticated TLS.
88
+ - `useSecureChannel: false` with a client certificate throws instead of silently dropping the identity. A plaintext channel otherwise logs a warning naming `host:port` (through `console.warn`, or the `logger` you pass).
89
+ - Without `grpcCert` the server is verified against Node's default CA store (Node's bundled Mozilla roots plus `NODE_EXTRA_CA_CERTS`; start Node with `--use-system-ca` to use the operating system's store instead).
90
+ - The host you connect to must match a subject alternative name (SAN) of the server certificate. When you connect by IP and the certificate has no IP SAN, pass the name to check as the channel option `'grpc.ssl_target_name_override'`.
91
+ - A bare IPv6 host is bracketed for you (`::1` becomes `[::1]:50051`).
92
+
93
+ ```ts
94
+ import { readFileSync } from 'fs';
95
+
96
+ import * as grpc from '@grpc/grpc-js';
97
+ import { createChannelCredentials, createGrpcClient, GrpcClientConfig, SipClient } from '@ondewo/sip-client-nodejs';
98
+
99
+ const config = new GrpcClientConfig({
100
+ host: '10.0.0.5',
101
+ port: 50051,
102
+ grpcCert: readFileSync('certs/ca.pem'),
103
+ grpcClientCert: readFileSync('certs/client.pem'), // leave both out for server-authenticated TLS
104
+ grpcClientKey: readFileSync('certs/client.key')
105
+ });
106
+
107
+ const client: SipClient = createGrpcClient(SipClient, config, {
108
+ channelOptions: { 'grpc.ssl_target_name_override': 'sip.example.internal' } // only when connecting by IP
109
+ });
110
+ ```
111
+
112
+ `createGrpcClient` starts from `DEFAULT_GRPC_CHANNEL_OPTIONS` (maximum message size 2³¹-1 bytes in both directions, reconnect backoff capped at 5 s) and applies your `channelOptions` on top. To build credentials only, call `createChannelCredentials(config, { useSecureChannel })` and pass the result to any generated client constructor.
113
+
114
+ **One connection for several services.** Clients built with the **same credentials object** (and the same target and options) share one connection, so the TLS handshake happens once:
115
+
116
+ ```ts
117
+ const credentials: grpc.ChannelCredentials = createChannelCredentials(config);
118
+ const first = createGrpcClient(SipClient, config, { credentials });
119
+ const second = createGrpcClient(SipClient, config, { credentials }); // any other generated client works the same way
120
+ ```
121
+
122
+ **Keepalive is off by default.** `@grpc/grpc-js` has no `grpc.http2.max_pings_without_data`, so with `grpc.keepalive_time_ms` set it keeps pinging a stream that carries no data, and a grpc-core server (as every ONDEWO server is) answers with `GOAWAY too_many_pings`: the call fails with `RESOURCE_EXHAUSTED` (measured: after 150 s at a 30 s keepalive). The Python SDKs' 30 s keepalive therefore is not copied; set `grpc.keepalive_time_ms` yourself only against a server whose `grpc.http2.min_ping_interval_without_data_ms` allows it. `grpc.keepalive_timeout_ms` is preset to 20 s for that case.
123
+
124
+ ### A test PKI with openssl
125
+
126
+ A CA, a server certificate with SANs and a client certificate with the `clientAuth` extended key usage. For tests only: the keys are unencrypted.
127
+
128
+ ```bash
129
+ openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 365 \
130
+ -subj "/CN=Test CA" -keyout ca.key -out ca.pem
131
+
132
+ printf 'subjectAltName=DNS:localhost,IP:127.0.0.1\nextendedKeyUsage=serverAuth\n' > server.ext
133
+ openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
134
+ -subj "/CN=localhost" -keyout server.key -out server.csr
135
+ openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 365 \
136
+ -extfile server.ext -out server.pem
137
+
138
+ printf 'extendedKeyUsage=clientAuth\n' > client.ext
139
+ openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
140
+ -subj "/CN=my-client" -keyout client.key -out client.csr
141
+ openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 365 \
142
+ -extfile client.ext -out client.pem
143
+
144
+ chmod 600 *.key
145
+ openssl verify -CAfile ca.pem server.pem client.pem
146
+ ```
147
+
148
+ The client then uses `ca.pem` / `client.pem` / `client.key`; a server that requires client certificates uses `server.pem` / `server.key` and trusts `ca.pem` for its clients.
149
+
150
+ ### TLS security notes
151
+
152
+ - Keep the client key out of source control and out of serialized configs: load it from a file (mode `0600`) or a secret store at startup.
153
+ - `GrpcClientConfig` renders the key as `***REDACTED***` in `toString()`, `util.inspect()` / `console.log()` and `JSON.stringify()` (an empty key renders empty). There is no serializer that writes the key, so a config cannot be persisted with it. A plain object you pass instead of a `GrpcClientConfig` has none of this protection: do not log it.
154
+ - No error message of the helper contains a PEM, a key or a whole config; they name the field and `host:port`.
155
+
156
+ ### TLS troubleshooting
157
+
158
+ A failed handshake surfaces as gRPC status `UNAVAILABLE` (14); the cause is in the error's `details`:
159
+
160
+ - **`unable to verify the first certificate`**: the server certificate is not signed by `grpcCert` (or, without `grpcCert`, not by a CA Node trusts). Pass the CA that signed the server certificate.
161
+ - **`ERR_TLS_CERT_ALTNAME_INVALID: Hostname/IP does not match certificate's altnames`**: the host you dial is not a SAN of the server certificate. Dial a name in the certificate, or set `'grpc.ssl_target_name_override'`. Some Node releases (seen on 22.23 and 24.18) also reject an IPv6 literal against a matching IP SAN; the override to a DNS SAN works around it.
162
+ - **`tlsv13 alert certificate required`**: the server requires mutual TLS and the client sent no certificate. Set `grpcClientCert` and `grpcClientKey`.
163
+ - **`Failed to connect`** right after the handshake with a client certificate: the server does not trust the CA that signed `grpcClientCert`.
164
+ - **`... grpcCert is not PEM content (no PEM header line)`** (thrown when the config is built): a field holds a file path or other text instead of the file's content. Pass `readFileSync(path)`.
165
+ - **`createChannelCredentials for host:port: gRPC rejected the TLS material: ... key values mismatch`** (thrown, not a call error): `grpcClientKey` is not the key of `grpcClientCert`. Other OpenSSL reasons there mean a damaged PEM block.
166
+
@@ -0,0 +1,146 @@
1
+ import { inspect } from 'util';
2
+ import * as grpc from '@grpc/grpc-js';
3
+ /** PEM content, as text or bytes. */
4
+ export type PemContent = string | Buffer;
5
+ /** What the redacted renderings show in place of a non-empty secret. */
6
+ export declare const REDACTED: string;
7
+ /** Largest message size gRPC accepts (2**31 - 1 bytes), used for both directions. */
8
+ export declare const MAX_MESSAGE_LENGTH: number;
9
+ /**
10
+ * Channel options every channel built here starts from; caller options override them key by key.
11
+ *
12
+ * The same values as the Python SDKs, as far as `@grpc/grpc-js` exposes them:
13
+ * - `grpc.max_reconnect_backoff_ms` 5000 (gRPC default 120 s): a client reconnects within seconds
14
+ * once the server is back instead of waiting up to two minutes.
15
+ * - `grpc.keepalive_timeout_ms` 20000 and `grpc.keepalive_permit_without_calls` 0: in grpc-js the
16
+ * keepalive timeout IS the ping-ack timeout (Python's `grpc.http2.ping_timeout_ms`). They only take
17
+ * effect once a caller sets `grpc.keepalive_time_ms`.
18
+ *
19
+ * `grpc.keepalive_time_ms` is deliberately NOT set: grpc-js has no `grpc.http2.max_pings_without_data`,
20
+ * so it keeps pinging a silent stream, and a grpc-core server (every ONDEWO server) answers with
21
+ * GOAWAY `too_many_pings`, failing the call with RESOURCE_EXHAUSTED (measured: after 50 s at a 10 s
22
+ * keepalive, 150 s at 30 s). Set it only for servers that allow it (`grpc.http2.min_ping_interval_without_data_ms`).
23
+ */
24
+ export declare const DEFAULT_GRPC_CHANNEL_OPTIONS: Readonly<grpc.ChannelOptions>;
25
+ /** Connection and certificate fields of a gRPC client configuration. */
26
+ export interface GrpcClientConfigFields {
27
+ /** Server host name or IP address; a bare IPv6 literal is bracketed by {@link hostAndPort}. */
28
+ host: string;
29
+ /** Server port. */
30
+ port: string | number;
31
+ /** PEM content of the CA (or server certificate) to trust; empty/unset = the system trust store. */
32
+ grpcCert?: PemContent;
33
+ /** PEM content of the client certificate chain for mutual TLS; set together with `grpcClientKey`. */
34
+ grpcClientCert?: PemContent;
35
+ /** PEM content of the client private key for mutual TLS; set together with `grpcClientCert`. */
36
+ grpcClientKey?: PemContent;
37
+ }
38
+ /** Destination of the insecure-channel warning; `console` satisfies it. */
39
+ export interface GrpcChannelLogger {
40
+ /**
41
+ * Log a warning.
42
+ *
43
+ * @param message - The warning text.
44
+ */
45
+ warn(message: string): void;
46
+ }
47
+ /** Options of {@link createChannelCredentials}. */
48
+ export interface ChannelCredentialsOptions {
49
+ /** `true` (default) for TLS / mutual TLS, `false` for a plaintext channel. */
50
+ useSecureChannel?: boolean;
51
+ /** Receives the insecure-channel warning; defaults to `console`. */
52
+ logger?: GrpcChannelLogger;
53
+ }
54
+ /** Options of {@link createGrpcClient}. */
55
+ export interface CreateGrpcClientOptions extends ChannelCredentialsOptions {
56
+ /** Channel options, applied over {@link DEFAULT_GRPC_CHANNEL_OPTIONS}. */
57
+ channelOptions?: grpc.ClientOptions;
58
+ /**
59
+ * Credentials from {@link createChannelCredentials} to reuse. Clients built with the SAME credentials
60
+ * object, target and options share one connection; when given, `useSecureChannel` and `logger`
61
+ * are not consulted.
62
+ */
63
+ credentials?: grpc.ChannelCredentials;
64
+ }
65
+ /** Constructor of a generated gRPC client (every `*Client` class in `api/`, or `grpc.Client`). */
66
+ export type GrpcClientConstructor<T> = new (address: string, credentials: grpc.ChannelCredentials, options?: grpc.ClientOptions) => T;
67
+ /**
68
+ * Combine host and port into a gRPC target, bracketing a bare IPv6 literal (`::1` becomes `[::1]:50051`).
69
+ * A host that is already bracketed or carries a scheme (`ipv6:[::1]`, `dns:...`, `unix:...`) is left as it is.
70
+ *
71
+ * @param host - Host name or IP address.
72
+ * @param port - Port.
73
+ * @returns The `host:port` target.
74
+ */
75
+ export declare function hostAndPort(host: string, port: string | number): string;
76
+ /**
77
+ * Connection settings of an ONDEWO gRPC client. Validated on construction (shape and the
78
+ * both-or-neither client identity), immutable, and redacting `grpcClientKey` in `toString()`,
79
+ * `util.inspect()` / `console.log()` and `JSON.stringify()`. There is deliberately no serialization
80
+ * that writes the key: keep PEMs in files or a secret store and pass their content in.
81
+ */
82
+ export declare class GrpcClientConfig implements GrpcClientConfigFields {
83
+ readonly host: string;
84
+ readonly port: string | number;
85
+ readonly grpcCert?: PemContent;
86
+ readonly grpcClientCert?: PemContent;
87
+ readonly grpcClientKey?: PemContent;
88
+ /**
89
+ * @param fields - Host, port and the optional PEM contents.
90
+ * @throws {TypeError} If a field has the wrong type (see {@link GrpcClientConfigFields}).
91
+ * @throws {Error} If exactly one of `grpcClientCert` and `grpcClientKey` is set, or a PEM field holds no PEM block.
92
+ */
93
+ constructor(fields: GrpcClientConfigFields);
94
+ /**
95
+ * The gRPC target, see {@link hostAndPort}.
96
+ *
97
+ * @returns `host:port`, with a bare IPv6 literal bracketed.
98
+ */
99
+ get hostAndPort(): string;
100
+ /**
101
+ * Loggable form: certificates as their text, the client key as {@link REDACTED} (empty stays empty).
102
+ *
103
+ * @returns A plain object safe to log or `JSON.stringify`.
104
+ */
105
+ toJSON(): Record<string, string | number | undefined>;
106
+ /**
107
+ * Short, redacted description: target and which certificates are set, no PEM content.
108
+ *
109
+ * @returns For example `GrpcClientConfig(host=localhost, port=50051, grpcCert=<set>, grpcClientCert=<set>, grpcClientKey=***REDACTED***)`.
110
+ */
111
+ toString(): string;
112
+ /**
113
+ * `util.inspect()` / `console.log()` rendering: the same as {@link toString}.
114
+ *
115
+ * @returns The redacted description.
116
+ */
117
+ [inspect.custom](): string;
118
+ }
119
+ /**
120
+ * Build the channel credentials for a configuration.
121
+ *
122
+ * Secure (default): trusts `grpcCert` if set, the system trust store otherwise, and presents the
123
+ * client identity when both `grpcClientCert` and `grpcClientKey` are set. Insecure: plaintext, with a
124
+ * warning naming `host:port`.
125
+ *
126
+ * @param config - A {@link GrpcClientConfig} or any object with its fields.
127
+ * @param options - `useSecureChannel` (default `true`) and the warning `logger` (default `console`).
128
+ * @returns The credentials to pass to a generated client.
129
+ * @throws {TypeError} If a field has the wrong type.
130
+ * @throws {Error} If exactly one of `grpcClientCert` / `grpcClientKey` is set; if a PEM field holds no
131
+ * PEM block (e.g. a file path); if `useSecureChannel`
132
+ * is `false` while a client identity is set; or if gRPC rejects a PEM (the message names the target
133
+ * and carries the TLS library's reason, never the PEM).
134
+ */
135
+ export declare function createChannelCredentials(config: GrpcClientConfigFields, options?: ChannelCredentialsOptions): grpc.ChannelCredentials;
136
+ /**
137
+ * Construct a generated gRPC client for a configuration, with {@link DEFAULT_GRPC_CHANNEL_OPTIONS}
138
+ * under the caller's `channelOptions`. Nothing connects until the first call.
139
+ *
140
+ * @param clientConstructor - The generated client class, e.g. a `*Client` from `api/`.
141
+ * @param config - A {@link GrpcClientConfig} or any object with its fields.
142
+ * @param options - See {@link CreateGrpcClientOptions}.
143
+ * @returns The client.
144
+ * @throws {Error} See {@link createChannelCredentials}.
145
+ */
146
+ export declare function createGrpcClient<T>(clientConstructor: GrpcClientConstructor<T>, config: GrpcClientConfigFields, options?: CreateGrpcClientOptions): T;
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ // Copyright 2021-2026 ONDEWO GmbH
3
+ //
4
+ // Licensed under the Apache License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License.
6
+ // You may obtain a copy of the License at
7
+ //
8
+ // http://www.apache.org/licenses/LICENSE-2.0
9
+ //
10
+ // Unless required by applicable law or agreed to in writing, software
11
+ // distributed under the License is distributed on an "AS IS" BASIS,
12
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ // See the License for the specific language governing permissions and
14
+ // limitations under the License.
15
+ //
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.GrpcClientConfig = exports.DEFAULT_GRPC_CHANNEL_OPTIONS = exports.MAX_MESSAGE_LENGTH = exports.REDACTED = void 0;
51
+ exports.hostAndPort = hostAndPort;
52
+ exports.createChannelCredentials = createChannelCredentials;
53
+ exports.createGrpcClient = createGrpcClient;
54
+ /**
55
+ * gRPC channel helper: TLS, mutual TLS and the ONDEWO default channel options for the generated
56
+ * `@grpc/grpc-js` clients of this package.
57
+ *
58
+ * Same contract as every ONDEWO SDK (reference: the Python `ondewo-client-utils`):
59
+ *
60
+ * - `grpcCert`, `grpcClientCert` and `grpcClientKey` hold PEM CONTENT (string or Buffer), never a
61
+ * path. Reading the files is the caller's job.
62
+ * - A secure channel trusts `grpcCert` when it is set, the system trust store otherwise, and
63
+ * presents the client identity (`grpcClientCert` + `grpcClientKey`) when BOTH are set.
64
+ * - Exactly one of `grpcClientCert` / `grpcClientKey` is refused with an error before anything
65
+ * reaches gRPC. Empty on both means plain server-authenticated TLS.
66
+ * - `useSecureChannel: false` with a client identity is refused instead of silently dropping it; an
67
+ * insecure channel otherwise logs a warning naming `host:port`.
68
+ * - No error message, `toString()`, `util.inspect()` or `JSON.stringify()` output contains a PEM or
69
+ * the client key (rendered as {@link REDACTED}).
70
+ *
71
+ * @packageDocumentation
72
+ */
73
+ const net_1 = require("net");
74
+ const util_1 = require("util");
75
+ const grpc = __importStar(require("@grpc/grpc-js"));
76
+ /** What the redacted renderings show in place of a non-empty secret. */
77
+ exports.REDACTED = '***REDACTED***';
78
+ /** Largest message size gRPC accepts (2**31 - 1 bytes), used for both directions. */
79
+ exports.MAX_MESSAGE_LENGTH = 2147483647;
80
+ /**
81
+ * Channel options every channel built here starts from; caller options override them key by key.
82
+ *
83
+ * The same values as the Python SDKs, as far as `@grpc/grpc-js` exposes them:
84
+ * - `grpc.max_reconnect_backoff_ms` 5000 (gRPC default 120 s): a client reconnects within seconds
85
+ * once the server is back instead of waiting up to two minutes.
86
+ * - `grpc.keepalive_timeout_ms` 20000 and `grpc.keepalive_permit_without_calls` 0: in grpc-js the
87
+ * keepalive timeout IS the ping-ack timeout (Python's `grpc.http2.ping_timeout_ms`). They only take
88
+ * effect once a caller sets `grpc.keepalive_time_ms`.
89
+ *
90
+ * `grpc.keepalive_time_ms` is deliberately NOT set: grpc-js has no `grpc.http2.max_pings_without_data`,
91
+ * so it keeps pinging a silent stream, and a grpc-core server (every ONDEWO server) answers with
92
+ * GOAWAY `too_many_pings`, failing the call with RESOURCE_EXHAUSTED (measured: after 50 s at a 10 s
93
+ * keepalive, 150 s at 30 s). Set it only for servers that allow it (`grpc.http2.min_ping_interval_without_data_ms`).
94
+ */
95
+ exports.DEFAULT_GRPC_CHANNEL_OPTIONS = Object.freeze({
96
+ 'grpc.max_send_message_length': exports.MAX_MESSAGE_LENGTH,
97
+ 'grpc.max_receive_message_length': exports.MAX_MESSAGE_LENGTH,
98
+ 'grpc.max_reconnect_backoff_ms': 5000,
99
+ 'grpc.keepalive_timeout_ms': 20000,
100
+ 'grpc.keepalive_permit_without_calls': 0
101
+ });
102
+ /** The configuration fields that hold PEM content. */
103
+ const PEM_FIELDS = [
104
+ 'grpcCert',
105
+ 'grpcClientCert',
106
+ 'grpcClientKey'
107
+ ];
108
+ /**
109
+ * Combine host and port into a gRPC target, bracketing a bare IPv6 literal (`::1` becomes `[::1]:50051`).
110
+ * A host that is already bracketed or carries a scheme (`ipv6:[::1]`, `dns:...`, `unix:...`) is left as it is.
111
+ *
112
+ * @param host - Host name or IP address.
113
+ * @param port - Port.
114
+ * @returns The `host:port` target.
115
+ */
116
+ function hostAndPort(host, port) {
117
+ if ((0, net_1.isIP)(host) === 6) {
118
+ return `[${host}]:${port}`;
119
+ }
120
+ return `${host}:${port}`;
121
+ }
122
+ /**
123
+ * Turn optional PEM content into the Buffer gRPC takes; empty means unset.
124
+ *
125
+ * @param pem - PEM text or bytes, or nothing.
126
+ * @returns The PEM as a non-empty Buffer, or `null` when it is unset or empty.
127
+ */
128
+ function toPemBuffer(pem) {
129
+ if (pem === undefined || pem.length === 0) {
130
+ return null;
131
+ }
132
+ if (typeof pem === 'string') {
133
+ return Buffer.from(pem, 'utf8');
134
+ }
135
+ return pem;
136
+ }
137
+ /**
138
+ * Refuse half a client identity before it can reach gRPC.
139
+ *
140
+ * @param fields - The configuration to check.
141
+ * @param where - What is checking it (class or function name), for the message.
142
+ * @throws {Error} If exactly one of `grpcClientCert` and `grpcClientKey` is set (empty counts as unset).
143
+ */
144
+ function assertClientIdentityPair(fields, where) {
145
+ const hasCert = toPemBuffer(fields.grpcClientCert) !== null;
146
+ const hasKey = toPemBuffer(fields.grpcClientKey) !== null;
147
+ if (hasCert !== hasKey) {
148
+ throw new Error(`${where} for ${hostAndPort(fields.host, fields.port)} has only one of grpcClientCert and grpcClientKey; ` +
149
+ 'set both to use mutual TLS, or neither.');
150
+ }
151
+ }
152
+ /**
153
+ * Refuse a PEM field that holds no PEM block, typically a file path passed instead of the file's
154
+ * content (OpenSSL would ignore such a `grpcCert` silently and fail the handshake later).
155
+ *
156
+ * @param fields - The configuration to check.
157
+ * @param where - What is checking it, for the message.
158
+ * @throws {Error} If a non-empty PEM field contains no `-----BEGIN` line.
159
+ */
160
+ function assertPemContent(fields, where) {
161
+ for (const name of PEM_FIELDS) {
162
+ const pem = toPemBuffer(fields[name]);
163
+ if (pem !== null && !pem.includes('-----BEGIN')) {
164
+ throw new Error(`${where} for ${hostAndPort(fields.host, fields.port)}: ${name} is not PEM content (no PEM header line); ` +
165
+ "pass the file's content, not its path.");
166
+ }
167
+ }
168
+ }
169
+ /**
170
+ * Validate the shape of a configuration (types only; never renders a value).
171
+ *
172
+ * @param fields - The configuration to check.
173
+ * @param where - What is checking it, for the message.
174
+ * @throws {TypeError} If `host` is not a non-empty string, `port` is empty, or a PEM field is not a string or Buffer.
175
+ */
176
+ function assertConfigShape(fields, where) {
177
+ if (typeof fields.host !== 'string' || fields.host.length === 0) {
178
+ throw new TypeError(`${where}: host must be a non-empty string.`);
179
+ }
180
+ if ((typeof fields.port !== 'string' && typeof fields.port !== 'number') || String(fields.port).length === 0) {
181
+ throw new TypeError(`${where} for host ${fields.host}: port must be a non-empty string or a number.`);
182
+ }
183
+ for (const name of PEM_FIELDS) {
184
+ const value = fields[name];
185
+ if (value !== undefined && typeof value !== 'string' && !Buffer.isBuffer(value)) {
186
+ throw new TypeError(`${where} for ${hostAndPort(fields.host, fields.port)}: ${name} must be PEM content (string or Buffer), not a ${typeof value}.`);
187
+ }
188
+ }
189
+ }
190
+ /**
191
+ * Connection settings of an ONDEWO gRPC client. Validated on construction (shape and the
192
+ * both-or-neither client identity), immutable, and redacting `grpcClientKey` in `toString()`,
193
+ * `util.inspect()` / `console.log()` and `JSON.stringify()`. There is deliberately no serialization
194
+ * that writes the key: keep PEMs in files or a secret store and pass their content in.
195
+ */
196
+ class GrpcClientConfig {
197
+ /**
198
+ * @param fields - Host, port and the optional PEM contents.
199
+ * @throws {TypeError} If a field has the wrong type (see {@link GrpcClientConfigFields}).
200
+ * @throws {Error} If exactly one of `grpcClientCert` and `grpcClientKey` is set, or a PEM field holds no PEM block.
201
+ */
202
+ constructor(fields) {
203
+ assertConfigShape(fields, 'GrpcClientConfig');
204
+ assertClientIdentityPair(fields, 'GrpcClientConfig');
205
+ assertPemContent(fields, 'GrpcClientConfig');
206
+ this.host = fields.host;
207
+ this.port = fields.port;
208
+ this.grpcCert = fields.grpcCert;
209
+ this.grpcClientCert = fields.grpcClientCert;
210
+ this.grpcClientKey = fields.grpcClientKey;
211
+ Object.freeze(this);
212
+ }
213
+ /**
214
+ * The gRPC target, see {@link hostAndPort}.
215
+ *
216
+ * @returns `host:port`, with a bare IPv6 literal bracketed.
217
+ */
218
+ get hostAndPort() {
219
+ return hostAndPort(this.host, this.port);
220
+ }
221
+ /**
222
+ * Loggable form: certificates as their text, the client key as {@link REDACTED} (empty stays empty).
223
+ *
224
+ * @returns A plain object safe to log or `JSON.stringify`.
225
+ */
226
+ toJSON() {
227
+ return {
228
+ host: this.host,
229
+ port: this.port,
230
+ grpcCert: pemToText(this.grpcCert),
231
+ grpcClientCert: pemToText(this.grpcClientCert),
232
+ grpcClientKey: redactSecret(this.grpcClientKey)
233
+ };
234
+ }
235
+ /**
236
+ * Short, redacted description: target and which certificates are set, no PEM content.
237
+ *
238
+ * @returns For example `GrpcClientConfig(host=localhost, port=50051, grpcCert=<set>, grpcClientCert=<set>, grpcClientKey=***REDACTED***)`.
239
+ */
240
+ toString() {
241
+ return (`GrpcClientConfig(host=${this.host}, port=${this.port}, grpcCert=${isSetText(this.grpcCert)}, ` +
242
+ `grpcClientCert=${isSetText(this.grpcClientCert)}, grpcClientKey=${redactSecret(this.grpcClientKey) ?? ''})`);
243
+ }
244
+ /**
245
+ * `util.inspect()` / `console.log()` rendering: the same as {@link toString}.
246
+ *
247
+ * @returns The redacted description.
248
+ */
249
+ [util_1.inspect.custom]() {
250
+ return this.toString();
251
+ }
252
+ }
253
+ exports.GrpcClientConfig = GrpcClientConfig;
254
+ /**
255
+ * Render optional PEM content as text for {@link GrpcClientConfig.toJSON}.
256
+ *
257
+ * @param pem - PEM text or bytes, or nothing.
258
+ * @returns The PEM text, or `undefined` when unset.
259
+ */
260
+ function pemToText(pem) {
261
+ if (pem === undefined) {
262
+ return undefined;
263
+ }
264
+ return typeof pem === 'string' ? pem : pem.toString('utf8');
265
+ }
266
+ /**
267
+ * Redact a secret: non-empty becomes {@link REDACTED}, empty stays empty, unset stays unset.
268
+ *
269
+ * @param secret - The secret, or nothing.
270
+ * @returns The redacted rendering.
271
+ */
272
+ function redactSecret(secret) {
273
+ if (secret === undefined) {
274
+ return undefined;
275
+ }
276
+ return secret.length === 0 ? '' : exports.REDACTED;
277
+ }
278
+ /**
279
+ * Say whether a certificate is set without rendering it.
280
+ *
281
+ * @param pem - PEM text or bytes, or nothing.
282
+ * @returns `<set>` or `<unset>`.
283
+ */
284
+ function isSetText(pem) {
285
+ return toPemBuffer(pem) === null ? '<unset>' : '<set>';
286
+ }
287
+ /**
288
+ * Build the channel credentials for a configuration.
289
+ *
290
+ * Secure (default): trusts `grpcCert` if set, the system trust store otherwise, and presents the
291
+ * client identity when both `grpcClientCert` and `grpcClientKey` are set. Insecure: plaintext, with a
292
+ * warning naming `host:port`.
293
+ *
294
+ * @param config - A {@link GrpcClientConfig} or any object with its fields.
295
+ * @param options - `useSecureChannel` (default `true`) and the warning `logger` (default `console`).
296
+ * @returns The credentials to pass to a generated client.
297
+ * @throws {TypeError} If a field has the wrong type.
298
+ * @throws {Error} If exactly one of `grpcClientCert` / `grpcClientKey` is set; if a PEM field holds no
299
+ * PEM block (e.g. a file path); if `useSecureChannel`
300
+ * is `false` while a client identity is set; or if gRPC rejects a PEM (the message names the target
301
+ * and carries the TLS library's reason, never the PEM).
302
+ */
303
+ function createChannelCredentials(config, options = {}) {
304
+ assertConfigShape(config, 'createChannelCredentials');
305
+ // grpc-core aborts the whole process on half an identity; grpc-js throws. Refuse it here either way.
306
+ assertClientIdentityPair(config, 'createChannelCredentials');
307
+ assertPemContent(config, 'createChannelCredentials');
308
+ const target = hostAndPort(config.host, config.port);
309
+ const clientCert = toPemBuffer(config.grpcClientCert);
310
+ const clientKey = toPemBuffer(config.grpcClientKey);
311
+ if (options.useSecureChannel === false) {
312
+ if (clientCert !== null) {
313
+ throw new Error(`createChannelCredentials for ${target}: useSecureChannel is false but a client identity ` +
314
+ '(grpcClientCert/grpcClientKey) is set; mutual TLS needs a secure channel. ' +
315
+ 'Use a secure channel or remove the client identity.');
316
+ }
317
+ (options.logger ?? console).warn(`Insecure gRPC channel to ${target}: traffic is NOT encrypted (useSecureChannel=false).`);
318
+ return grpc.credentials.createInsecure();
319
+ }
320
+ try {
321
+ // Empty PEMs were mapped to null: no root certs = system trust store, no identity = plain TLS.
322
+ return grpc.credentials.createSsl(toPemBuffer(config.grpcCert), clientKey, clientCert);
323
+ }
324
+ catch (error) {
325
+ const reason = error instanceof Error ? error.message : String(error);
326
+ throw new Error(`createChannelCredentials for ${target}: gRPC rejected the TLS material: ${reason}`);
327
+ }
328
+ }
329
+ /**
330
+ * Construct a generated gRPC client for a configuration, with {@link DEFAULT_GRPC_CHANNEL_OPTIONS}
331
+ * under the caller's `channelOptions`. Nothing connects until the first call.
332
+ *
333
+ * @param clientConstructor - The generated client class, e.g. a `*Client` from `api/`.
334
+ * @param config - A {@link GrpcClientConfig} or any object with its fields.
335
+ * @param options - See {@link CreateGrpcClientOptions}.
336
+ * @returns The client.
337
+ * @throws {Error} See {@link createChannelCredentials}.
338
+ */
339
+ function createGrpcClient(clientConstructor, config, options = {}) {
340
+ assertConfigShape(config, 'createGrpcClient');
341
+ const credentials = options.credentials ?? createChannelCredentials(config, options);
342
+ return new clientConstructor(hostAndPort(config.host, config.port), credentials, {
343
+ ...exports.DEFAULT_GRPC_CHANNEL_OPTIONS,
344
+ ...options.channelOptions
345
+ });
346
+ }