@ondewo/s2t-client-typescript 7.4.1 → 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.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * gRPC-web endpoint helper: turns `host` / `port` / `useSecureChannel` into the `hostname` URL and the
3
+ * client options every generated `*Client` / `*PromiseClient` constructor takes.
4
+ *
5
+ * TLS in a browser belongs to the browser: the server certificate is verified against the browser's /
6
+ * operating system's trust store, and a client certificate (mutual TLS) can only come from the
7
+ * browser's own certificate store. So this helper accepts no CA, no client certificate and no private
8
+ * key -- a config carrying one (e.g. ported from the Python SDK's `grpc_cert` / `grpc_client_cert` /
9
+ * `grpc_client_key`) is refused instead of silently dropped, and a private key never has to be shipped to
10
+ * a browser. See the README section "TLS, mutual TLS and certificates".
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ /** Connection settings of one gRPC-web (Envoy) endpoint. */
15
+ export interface GrpcWebEndpointConfig {
16
+ /** Bare host name or IP address (IPv6 with or without brackets); no scheme, port or path. */
17
+ host: string;
18
+ /** TCP port, 1-65535. */
19
+ port: number | string;
20
+ /** `true` (the default) builds an `https://` URL; `false` builds a plaintext `http://` one and warns. */
21
+ useSecureChannel?: boolean;
22
+ /**
23
+ * Send credentials on cross-origin calls (gRPC-web's `withCredentials` client option, default
24
+ * `false`). Cookies, HTTP auth and the browser's TLS client certificate (mutual TLS) are only sent on
25
+ * a cross-origin request when this is `true`.
26
+ */
27
+ withCredentials?: boolean;
28
+ }
29
+ /** The client options this helper sets; pass them as the third argument of a generated client. */
30
+ export interface GrpcWebClientOptions {
31
+ /** gRPC-web's `withCredentials` option. */
32
+ withCredentials: boolean;
33
+ }
34
+ /** What a generated gRPC-web client constructor takes: `new XClient(hostname, null, options)`. */
35
+ export interface GrpcWebEndpoint {
36
+ /** `https://host:port` (or `http://` for an insecure channel); IPv6 hosts are bracketed. */
37
+ readonly hostname: string;
38
+ /** The gRPC-web client options. */
39
+ readonly options: GrpcWebClientOptions;
40
+ }
41
+ /** Where the insecure-channel warning goes (the global `console` by default). */
42
+ export interface GrpcWebEndpointLogger {
43
+ /**
44
+ * Write one warning.
45
+ *
46
+ * @param message - The warning text.
47
+ */
48
+ warn(message: string): void;
49
+ }
50
+ /**
51
+ * Config fields of the other ONDEWO SDKs (camelCase and Python's snake_case) that a browser cannot use,
52
+ * with the reason each is refused. A non-empty value for any of them raises; an empty one is ignored.
53
+ */
54
+ export declare const REFUSED_TLS_FIELDS: Readonly<Record<string, string>>;
55
+ /**
56
+ * Render `host:port` for a URL, bracketing a bare IPv6 literal (`::1` -> `[::1]:50051`).
57
+ *
58
+ * @param host - A bare host name, IPv4 address, or IPv6 address with or without brackets.
59
+ * @param port - The port.
60
+ * @returns `host:port`.
61
+ * @throws {Error} When the host contains a colon but is neither a bracketed nor a bare IPv6 literal.
62
+ */
63
+ export declare function hostAndPort(host: string, port: number | string): string;
64
+ /**
65
+ * Validate a gRPC-web endpoint config and build the `hostname` URL and client options for a generated client.
66
+ *
67
+ * ```ts
68
+ * const endpoint: GrpcWebEndpoint = createGrpcWebEndpoint({ host: 'nlu.example.com', port: 443 });
69
+ * const agents: AgentsClient = new AgentsClient(endpoint.hostname, null, endpoint.options);
70
+ * ```
71
+ *
72
+ * Error messages name the offending field only, never its value.
73
+ *
74
+ * @param config - Host, port and channel settings.
75
+ * @param logger - Receives the warning for an insecure channel (default: the global `console`).
76
+ * @returns The endpoint URL and the gRPC-web client options.
77
+ * @throws {Error} When `host` / `port` / `useSecureChannel` / `withCredentials` is invalid, or the config
78
+ * carries a CA, client certificate or private key (see {@link REFUSED_TLS_FIELDS}).
79
+ */
80
+ export declare function createGrpcWebEndpoint(config: GrpcWebEndpointConfig, logger?: GrpcWebEndpointLogger): GrpcWebEndpoint;
@@ -0,0 +1,121 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.REFUSED_TLS_FIELDS = void 0;
18
+ exports.hostAndPort = hostAndPort;
19
+ exports.createGrpcWebEndpoint = createGrpcWebEndpoint;
20
+ /** Why a browser cannot use a CA certificate given in code. */
21
+ const CA_REASON = 'a browser verifies the server certificate against its own trust store; install the CA there or use a publicly trusted certificate';
22
+ /** Why a browser cannot use a client certificate given in code. */
23
+ const CLIENT_CERT_REASON = 'a browser presents a TLS client certificate only from its own certificate store; install the client certificate there';
24
+ /** Why a private key is refused. */
25
+ const CLIENT_KEY_REASON = 'a private key must never be shipped to a browser; install the client certificate in its certificate store';
26
+ /**
27
+ * Config fields of the other ONDEWO SDKs (camelCase and Python's snake_case) that a browser cannot use,
28
+ * with the reason each is refused. A non-empty value for any of them raises; an empty one is ignored.
29
+ */
30
+ exports.REFUSED_TLS_FIELDS = {
31
+ grpcCert: CA_REASON,
32
+ grpc_cert: CA_REASON,
33
+ grpcClientCert: CLIENT_CERT_REASON,
34
+ grpc_client_cert: CLIENT_CERT_REASON,
35
+ grpcClientKey: CLIENT_KEY_REASON,
36
+ grpc_client_key: CLIENT_KEY_REASON
37
+ };
38
+ /** A bare IPv6 literal: hex groups, colons and an optional embedded IPv4 tail, at least two colons. */
39
+ const BARE_IPV6_PATTERN = /^(?=(?:[^:]*:){2})[0-9A-Fa-f:.]+$/;
40
+ /**
41
+ * Render `host:port` for a URL, bracketing a bare IPv6 literal (`::1` -> `[::1]:50051`).
42
+ *
43
+ * @param host - A bare host name, IPv4 address, or IPv6 address with or without brackets.
44
+ * @param port - The port.
45
+ * @returns `host:port`.
46
+ * @throws {Error} When the host contains a colon but is neither a bracketed nor a bare IPv6 literal.
47
+ */
48
+ function hostAndPort(host, port) {
49
+ if (host.startsWith('[') && host.endsWith(']')) {
50
+ return `${host}:${port}`;
51
+ }
52
+ if (BARE_IPV6_PATTERN.test(host)) {
53
+ return `[${host}]:${port}`;
54
+ }
55
+ if (host.includes(':')) {
56
+ throw new Error('GrpcWebEndpointConfig.host must not contain a port; pass the port in GrpcWebEndpointConfig.port');
57
+ }
58
+ return `${host}:${port}`;
59
+ }
60
+ /**
61
+ * Validate a gRPC-web endpoint config and build the `hostname` URL and client options for a generated client.
62
+ *
63
+ * ```ts
64
+ * const endpoint: GrpcWebEndpoint = createGrpcWebEndpoint({ host: 'nlu.example.com', port: 443 });
65
+ * const agents: AgentsClient = new AgentsClient(endpoint.hostname, null, endpoint.options);
66
+ * ```
67
+ *
68
+ * Error messages name the offending field only, never its value.
69
+ *
70
+ * @param config - Host, port and channel settings.
71
+ * @param logger - Receives the warning for an insecure channel (default: the global `console`).
72
+ * @returns The endpoint URL and the gRPC-web client options.
73
+ * @throws {Error} When `host` / `port` / `useSecureChannel` / `withCredentials` is invalid, or the config
74
+ * carries a CA, client certificate or private key (see {@link REFUSED_TLS_FIELDS}).
75
+ */
76
+ function createGrpcWebEndpoint(config, logger = console) {
77
+ const fields = config;
78
+ for (const [name, reason] of Object.entries(exports.REFUSED_TLS_FIELDS)) {
79
+ const value = fields[name];
80
+ if (value !== undefined && value !== null && value !== '') {
81
+ throw new Error(`GrpcWebEndpointConfig.${name} is not supported by a gRPC-web client: ${reason}`);
82
+ }
83
+ }
84
+ const host = config.host;
85
+ if (typeof host !== 'string' || host.trim() === '' || host.trim() !== host) {
86
+ throw new Error('GrpcWebEndpointConfig.host must be a non-empty host name or IP address without whitespace');
87
+ }
88
+ if (host.includes('/') || host.includes('@')) {
89
+ throw new Error('GrpcWebEndpointConfig.host must be a bare host name or IP address without scheme, credentials or path; ' +
90
+ 'the scheme comes from GrpcWebEndpointConfig.useSecureChannel');
91
+ }
92
+ const port = config.port;
93
+ let portNumber = Number.NaN;
94
+ if (typeof port === 'number') {
95
+ portNumber = port;
96
+ }
97
+ else if (typeof port === 'string' && /^[0-9]{1,5}$/.test(port)) {
98
+ portNumber = Number(port);
99
+ }
100
+ if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > 65535) {
101
+ throw new Error('GrpcWebEndpointConfig.port must be an integer between 1 and 65535');
102
+ }
103
+ const useSecureChannel = config.useSecureChannel ?? true;
104
+ if (typeof useSecureChannel !== 'boolean') {
105
+ throw new Error('GrpcWebEndpointConfig.useSecureChannel must be a boolean');
106
+ }
107
+ const withCredentials = config.withCredentials ?? false;
108
+ if (typeof withCredentials !== 'boolean') {
109
+ throw new Error('GrpcWebEndpointConfig.withCredentials must be a boolean');
110
+ }
111
+ const target = hostAndPort(host, portNumber);
112
+ let scheme = 'https';
113
+ if (!useSecureChannel) {
114
+ scheme = 'http';
115
+ logger.warn(`ONDEWO gRPC-web: the channel to ${target} is NOT encrypted (useSecureChannel=false); use it only for local development`);
116
+ }
117
+ return {
118
+ hostname: `${scheme}://${target}`,
119
+ options: { withCredentials }
120
+ };
121
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Minimal structural type of the fetch Response fields this helper reads. Keeps the module
3
+ * self-contained (no DOM lib dependency) while still typing the injectable `fetchImpl`.
4
+ */
5
+ export interface TokenFetchResponse {
6
+ /** Whether the HTTP status is in the 2xx success range. */
7
+ ok: boolean;
8
+ /** The numeric HTTP status code of the response. */
9
+ status: number;
10
+ /**
11
+ * Read the response body as text.
12
+ *
13
+ * @returns A promise resolving to the raw response body string.
14
+ */
15
+ text(): Promise<string>;
16
+ }
17
+ /** Init object passed to the injectable fetch when POSTing to the token endpoint. */
18
+ export interface TokenFetchInit {
19
+ /** HTTP method (always `"POST"` for the token endpoint). */
20
+ method: string;
21
+ /** Request headers (Content-Type + Accept) as a plain string map. */
22
+ headers: Record<string, string>;
23
+ /** The `application/x-www-form-urlencoded` request body. */
24
+ body: string;
25
+ /**
26
+ * Optional undici dispatcher (Node only). The default transport attaches an insecure
27
+ * `Agent({ connect: { rejectUnauthorized: false } })` here when `verifySsl` is `false`; the global
28
+ * WHATWG fetch honours it. Never set on an injected `fetchImpl` and ignored in a browser bundle.
29
+ */
30
+ dispatcher?: unknown;
31
+ }
32
+ /**
33
+ * Injectable fetch signature (a subset of the global `fetch`) used by the token endpoint call.
34
+ *
35
+ * @param url - The token endpoint URL to POST to.
36
+ * @param init - The request method, headers, and form-encoded body.
37
+ * @returns A promise resolving to the structural {@link TokenFetchResponse}.
38
+ */
39
+ export type TokenFetch = (url: string, init: TokenFetchInit) => Promise<TokenFetchResponse>;
40
+ /** Options for the D18 headless-SDK offline-token login. */
41
+ export interface OfflineTokenLoginOptions {
42
+ /** Base Keycloak URL, e.g. "https://auth.example.com/auth" (trailing slash tolerated). */
43
+ keycloakUrl: string;
44
+ /** Realm name, e.g. "ondewo-ccai-platform". */
45
+ realm: string;
46
+ /** Public SDK client id, e.g. "ondewo-nlu-cai-sdk-public". NO client_secret (Q1). */
47
+ clientId: string;
48
+ /** 2FA-exempt technical-user email. */
49
+ username: string;
50
+ /** Technical-user password. */
51
+ password: string;
52
+ /** Optional cap (seconds) on how long the auto-refresh loop runs after login. */
53
+ tokenExpirationInS?: number;
54
+ /** Optional fetch override (tests inject a mock); defaults to the global fetch. */
55
+ fetchImpl?: TokenFetch;
56
+ /**
57
+ * Verify the Keycloak TLS certificate on the token-endpoint call. Default `true` (secure). Set
58
+ * `false` ONLY for a self-signed local Envoy (e.g. `https://localhost:12001/auth`). Node-only: it is
59
+ * ignored in a browser bundle (the browser owns TLS) and ignored when a custom `fetchImpl` is
60
+ * injected. When `false` under Node the default transport attaches an insecure undici dispatcher.
61
+ */
62
+ keycloakVerifySsl?: boolean;
63
+ /** Optional clock override returning epoch ms (tests); defaults to Date.now. */
64
+ nowInMs?: () => number;
65
+ }
66
+ /** Error raised on any token-endpoint or token-shape failure. */
67
+ export declare class TokenError extends Error {
68
+ /**
69
+ * Construct a `TokenError` with a fixed `name` of `"TokenError"`.
70
+ *
71
+ * @param message - Human-readable description of the token failure.
72
+ */
73
+ constructor(message: string);
74
+ }
75
+ /**
76
+ * The undici `Agent` options that switch TLS certificate verification OFF for the token request.
77
+ * Exported so the security-relevant `rejectUnauthorized: false` literal is pinned by a test rather than
78
+ * living as a bare literal that could be flipped without any test noticing.
79
+ */
80
+ export declare const INSECURE_AGENT_OPTIONS: {
81
+ connect: {
82
+ rejectUnauthorized: boolean;
83
+ };
84
+ };
85
+ /**
86
+ * Build the DEFAULT token transport (used only when no `fetchImpl` is injected). It delegates to the
87
+ * global WHATWG fetch, and -- when `verifySsl` is `false` under Node -- attaches an insecure undici
88
+ * dispatcher to the request init so the token POST skips certificate verification. With `verifySsl`
89
+ * `true` (the default) it is a plain pass-through to `globalThis.fetch`, i.e. unchanged behavior.
90
+ *
91
+ * @param verifySsl - Whether to verify the Keycloak TLS certificate; `false` opts into the insecure path.
92
+ * @returns A {@link TokenFetch} that resolves the global fetch at call time (honoring test overrides).
93
+ */
94
+ export declare function createDefaultTokenFetch(verifySsl: boolean): TokenFetch;
95
+ /** What {@link OfflineTokenProvider.toJSON} renders in place of a non-empty token. */
96
+ export declare const REDACTED: string;
97
+ /** The logging-safe view of an {@link OfflineTokenProvider}: both tokens redacted. */
98
+ export interface OfflineTokenProviderLogView {
99
+ /** The OIDC token endpoint. */
100
+ tokenEndpoint: string;
101
+ /** The Keycloak client id. */
102
+ clientId: string;
103
+ /** `***REDACTED***`, or the token as is when it is `null` (before login) or empty. */
104
+ accessToken: string | null;
105
+ /** `***REDACTED***`, or the token as is when it is `null` (before login) or empty. */
106
+ refreshToken: string | null;
107
+ /** Whether {@link OfflineTokenProvider.stop} was called. */
108
+ stopped: boolean;
109
+ }
110
+ /**
111
+ * A live access-token holder backed by a bounded auto-refresh loop. Obtain one from {@link login};
112
+ * read {@link getAuthorizationHeader} for the gRPC `Authorization` metadata and call {@link stop} when done.
113
+ */
114
+ export declare class OfflineTokenProvider {
115
+ /** Pre-built OIDC token endpoint URL the provider POSTs to for both login and refresh. */
116
+ private readonly tokenEndpoint;
117
+ /** Public SDK client id sent as `client_id` (no `client_secret` -- Q1). */
118
+ private readonly clientId;
119
+ /** Optional cap (seconds) after which the auto-refresh loop stops; `undefined` means unbounded. */
120
+ private readonly tokenExpirationInS;
121
+ /** The injectable fetch used for every token request (defaults to the global `fetch`). */
122
+ private readonly fetchImpl;
123
+ /** Clock returning epoch milliseconds, used for deadline arithmetic (defaults to `Date.now`). */
124
+ private readonly nowInMs;
125
+ /** The current access token, or `null` before bootstrap / after the bounded loop lapses. */
126
+ private accessToken;
127
+ /** The newest offline refresh token, or `null` before bootstrap. */
128
+ private refreshToken;
129
+ /** Handle of the single armed refresh timer, or `null` when none is pending. */
130
+ private timer;
131
+ /** Whether {@link stop} has been called; suppresses any further refresh scheduling. */
132
+ private stopped;
133
+ /** Absolute epoch-ms deadline at which the loop stops, or `null` when unbounded. */
134
+ private deadlineInMs;
135
+ /** Optional callback invoked with the error of a failed background refresh. */
136
+ private onRefreshErrorHandler;
137
+ /**
138
+ * Initialize the provider from login options without performing any network call. Use {@link login}
139
+ * (which constructs and then {@link bootstrap}s) for the normal flow.
140
+ *
141
+ * @param options - The D18 headless-SDK offline-token login options.
142
+ */
143
+ constructor(options: OfflineTokenLoginOptions);
144
+ /**
145
+ * Perform the one-time ROPC login and arm the first refresh. Awaited by {@link login}.
146
+ *
147
+ * @param username - The 2FA-exempt technical-user email.
148
+ * @param password - The technical-user password.
149
+ * @returns A promise that resolves once the access token is stored and the first refresh is scheduled.
150
+ * @throws {@link TokenError} If the token endpoint fails, or when the response carries no usable
151
+ * `refresh_token` -- absent, not a string, or empty (i.e. the SDK client lacks directAccessGrants
152
+ * + the `offline_access` scope).
153
+ */
154
+ bootstrap(username: string, password: string): Promise<void>;
155
+ /**
156
+ * Exchange the offline refresh token for a fresh access token and re-arm the next refresh. No-ops
157
+ * once {@link stop} has been called or the bounded deadline has elapsed.
158
+ *
159
+ * @returns A promise that resolves once the refreshed token is stored and the next refresh is armed.
160
+ * @throws {@link TokenError} If the refresh token request fails or returns an invalid body; the
161
+ * rejection is routed to the {@link onRefreshError} handler by the firing timer.
162
+ */
163
+ private refresh;
164
+ /**
165
+ * Arm a single timer for the next refresh, clamped to the bounded deadline. Stops silently once
166
+ * `tokenExpirationInS` has elapsed (no further renewal -> access lapses -> re-login required).
167
+ *
168
+ * @param expiresInRaw - The `expires_in` (seconds) from the latest token response, or `undefined`;
169
+ * non-positive / missing values fall back to {@link MIN_REFRESH_DELAY_IN_S}.
170
+ */
171
+ private scheduleRefresh;
172
+ /**
173
+ * Register a callback invoked with the error of a failed background refresh (optional diagnostics).
174
+ * A later call replaces any previously registered handler.
175
+ *
176
+ * @param handler - Receives the rejection value of a failed background refresh.
177
+ */
178
+ onRefreshError(handler: (error: unknown) => void): void;
179
+ /**
180
+ * Read the current access token.
181
+ *
182
+ * @returns The current access token, or `null` before bootstrap / after the bounded loop has lapsed.
183
+ */
184
+ getAccessToken(): string | null;
185
+ /**
186
+ * Build the value for an `Authorization` gRPC metadata header.
187
+ *
188
+ * @returns The header value `Bearer <access_token>`.
189
+ * @throws {@link TokenError} If no access token is available (login not completed or already lapsed).
190
+ */
191
+ getAuthorizationHeader(): string;
192
+ /**
193
+ * A logging-safe view of this provider: the access and refresh tokens render as `***REDACTED***`
194
+ * (`null` before login and an empty token stay as they are). `JSON.stringify(provider)` uses it, and so
195
+ * do Node's `console.log(provider)` / `util.inspect(provider)` through the hook below, so none of them
196
+ * prints a token. {@link OfflineTokenProvider.getAuthorizationHeader} still returns the real one.
197
+ *
198
+ * @returns The endpoint, client id and stop flag, with both tokens redacted.
199
+ */
200
+ toJSON(): OfflineTokenProviderLogView;
201
+ /**
202
+ * Stop the auto-refresh loop and clear any pending timer. Idempotent; safe to call from any state.
203
+ * After this the access token is no longer renewed and will eventually lapse.
204
+ */
205
+ stop(): void;
206
+ }
207
+ /**
208
+ * One-time ROPC + offline_access login against the PUBLIC SDK client, returning a live token provider
209
+ * whose access token is auto-refreshed in the background until `tokenExpirationInS` elapses.
210
+ *
211
+ * @param options - The D18 headless-SDK offline-token login options; the five string fields
212
+ * (`keycloakUrl`, `realm`, `clientId`, `username`, `password`) are required and must be non-empty.
213
+ * @returns A promise resolving to a bootstrapped {@link OfflineTokenProvider} with its first refresh armed.
214
+ * @throws {@link TokenError} If `options` is missing, a required field is absent/empty, or the
215
+ * bootstrap login fails (see {@link OfflineTokenProvider.bootstrap}).
216
+ */
217
+ export declare function login(options: OfflineTokenLoginOptions): Promise<OfflineTokenProvider>;
218
+ export * from './grpcWebEndpoint';