@ondewo/sip-client-nodejs 5.4.1 → 5.5.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/README.md +94 -0
- package/api/ondewo/sip/sip_grpc_pb.d.ts +50 -0
- package/api/ondewo/sip/sip_grpc_pb.js +143 -8
- package/api/ondewo/sip/sip_pb.d.ts +439 -0
- package/api/ondewo/sip/sip_pb.js +3556 -482
- package/auth/grpcChannel.d.ts +146 -0
- package/auth/grpcChannel.js +346 -0
- package/auth/grpcChannel.spec.ts +747 -0
- package/auth/grpcChannel.ts +408 -0
- package/auth/offlineTokenProvider.d.ts +9 -5
- package/auth/offlineTokenProvider.js +86 -100
- package/package.json +1 -1
- package/public-api.d.ts +4 -2
- package/public-api.js +16 -6
|
@@ -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
|
+
}
|