@ondewo/sip-client-angular 5.4.1 → 5.4.3

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.
@@ -1,22 +1,13 @@
1
- import * as i0 from '@angular/core';
2
- import { InjectionToken, Optional, Inject, Injectable } from '@angular/core';
3
1
  import { uint8ArrayToBase64, GrpcMetadata, GrpcCallType } from '@ngx-grpc/common';
4
2
  import { BinaryReader, BinaryWriter } from 'google-protobuf';
5
3
  import * as googleProtobuf000 from '@ngx-grpc/well-known-types';
4
+ import * as i0 from '@angular/core';
5
+ import { InjectionToken, Optional, Inject, Injectable, inject, makeEnvironmentProviders } from '@angular/core';
6
6
  import * as i1 from '@ngx-grpc/core';
7
- import { throwStatusErrors, takeMessages, GRPC_CLIENT_FACTORY } from '@ngx-grpc/core';
8
-
9
- /* tslint:disable */
10
- /* eslint-disable */
11
- // @ts-nocheck
12
- //
13
- // THIS IS A GENERATED FILE
14
- // DO NOT MODIFY IT! YOUR CHANGES WILL BE LOST
15
- /**
16
- * Specific GrpcClientSettings for Sip.
17
- * Use it only if your default settings are not set or the service requires other settings.
18
- */
19
- const GRPC_SIP_CLIENT_SETTINGS = new InjectionToken('GRPC_SIP_CLIENT_SETTINGS');
7
+ import { throwStatusErrors, takeMessages, GRPC_CLIENT_FACTORY, GRPC_INTERCEPTORS } from '@ngx-grpc/core';
8
+ import { isObservable, from, Observable, of, switchMap, firstValueFrom } from 'rxjs';
9
+ import * as i1$1 from '@angular/common/http';
10
+ import { HttpHeaders } from '@angular/common/http';
20
11
 
21
12
  /* tslint:disable */
22
13
  /* eslint-disable */
@@ -1510,6 +1501,18 @@ class SipPlayWavFilesRequest {
1510
1501
  }
1511
1502
  }
1512
1503
 
1504
+ /* tslint:disable */
1505
+ /* eslint-disable */
1506
+ // @ts-nocheck
1507
+ //
1508
+ // THIS IS A GENERATED FILE
1509
+ // DO NOT MODIFY IT! YOUR CHANGES WILL BE LOST
1510
+ /**
1511
+ * Specific GrpcClientSettings for Sip.
1512
+ * Use it only if your default settings are not set or the service requires other settings.
1513
+ */
1514
+ const GRPC_SIP_CLIENT_SETTINGS = new InjectionToken('GRPC_SIP_CLIENT_SETTINGS');
1515
+
1513
1516
  /* tslint:disable */
1514
1517
  /* eslint-disable */
1515
1518
  // @ts-nocheck
@@ -1861,10 +1864,10 @@ class SipClient {
1861
1864
  .sipUnMute(requestData, requestMetadata)
1862
1865
  .pipe(throwStatusErrors(), takeMessages());
1863
1866
  }
1864
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.27", ngImport: i0, type: SipClient, deps: [{ token: GRPC_SIP_CLIENT_SETTINGS, optional: true }, { token: GRPC_CLIENT_FACTORY }, { token: i1.GrpcHandler }], target: i0.ɵɵFactoryTarget.Injectable }); }
1865
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.27", ngImport: i0, type: SipClient, providedIn: 'any' }); }
1867
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: SipClient, deps: [{ token: GRPC_SIP_CLIENT_SETTINGS, optional: true }, { token: GRPC_CLIENT_FACTORY }, { token: i1.GrpcHandler }], target: i0.ɵɵFactoryTarget.Injectable }); }
1868
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: SipClient, providedIn: 'any' }); }
1866
1869
  }
1867
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.27", ngImport: i0, type: SipClient, decorators: [{
1870
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: SipClient, decorators: [{
1868
1871
  type: Injectable,
1869
1872
  args: [{ providedIn: 'any' }]
1870
1873
  }], ctorParameters: () => [{ type: undefined, decorators: [{
@@ -1877,9 +1880,626 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.27", ngImpo
1877
1880
  args: [GRPC_CLIENT_FACTORY]
1878
1881
  }] }, { type: i1.GrpcHandler }] });
1879
1882
 
1883
+ /**
1884
+ * DI token under which the consuming application registers its
1885
+ * {@link TokenProvider} implementation.
1886
+ *
1887
+ * Example:
1888
+ *
1889
+ * ```ts
1890
+ * providers: [
1891
+ * { provide: TOKEN_PROVIDER, useExisting: KeycloakTokenProvider },
1892
+ * ]
1893
+ * ```
1894
+ */
1895
+ const TOKEN_PROVIDER = new InjectionToken("ONDEWO_SIP_TOKEN_PROVIDER");
1896
+
1897
+ /**
1898
+ * The HTTP / gRPC header under which the bearer credential is attached.
1899
+ *
1900
+ * Canonical `Authorization` casing: gRPC-web metadata keys are case-insensitive
1901
+ * and the HTTP/2 transport lower-cases header names on the wire, but the ONDEWO
1902
+ * SDKs standardize on the capitalized `Authorization` key in source.
1903
+ */
1904
+ const AUTHORIZATION_HEADER = "Authorization";
1905
+ /** The credential scheme prefix prepended to the raw access token. */
1906
+ const BEARER_PREFIX = "Bearer ";
1907
+ /**
1908
+ * Normalize the value returned by a `TokenProvider.getToken()` call — which may
1909
+ * be a `string`, `null`, a `Promise` or an `Observable` — into a single
1910
+ * `Observable<string | null>` that emits exactly once.
1911
+ *
1912
+ * A non-empty token is returned trimmed; `null`, `undefined`, an empty string
1913
+ * and a whitespace-only string are all collapsed to `null` so callers have a
1914
+ * single "no usable token" signal and never build an empty `Bearer` header.
1915
+ *
1916
+ * @param result the raw value returned by `TokenProvider.getToken()`.
1917
+ * @returns an observable emitting the usable token, or `null` when absent.
1918
+ */
1919
+ function resolveToken(result) {
1920
+ const source = isObservable(result)
1921
+ ? result
1922
+ : from(Promise.resolve(result));
1923
+ return new Observable((subscriber) => {
1924
+ const subscription = source.subscribe({
1925
+ next: (token) => subscriber.next(normalizeToken(token)),
1926
+ error: (caughtError) => subscriber.error(caughtError),
1927
+ complete: () => subscriber.complete()
1928
+ });
1929
+ return () => subscription.unsubscribe();
1930
+ });
1931
+ }
1932
+ /**
1933
+ * Build the `Authorization` header value for a resolved token, or `null` when
1934
+ * the token is absent.
1935
+ *
1936
+ * @param token a usable token, or `null`.
1937
+ * @returns the `"Bearer <token>"` string, or `null` when there is no token.
1938
+ */
1939
+ function buildBearerValue(token) {
1940
+ return token === null ? null : `${BEARER_PREFIX}${token}`;
1941
+ }
1942
+ /**
1943
+ * Convenience wrapper: emit the ready-to-use `Authorization` header value, or
1944
+ * `null` when no token is available.
1945
+ *
1946
+ * @param result the raw value returned by `TokenProvider.getToken()`.
1947
+ * @returns an observable emitting the bearer header value, or `null`.
1948
+ */
1949
+ function resolveBearerValue(result) {
1950
+ return new Observable((subscriber) => {
1951
+ const subscription = resolveToken(result).subscribe({
1952
+ next: (token) => subscriber.next(buildBearerValue(token)),
1953
+ error: (caughtError) => subscriber.error(caughtError),
1954
+ complete: () => subscriber.complete()
1955
+ });
1956
+ return () => subscription.unsubscribe();
1957
+ });
1958
+ }
1959
+ /**
1960
+ * Collapse every "no usable token" value to `null` and trim a real token.
1961
+ *
1962
+ * @param token the raw token emitted by the source.
1963
+ * @returns the trimmed token, or `null` when empty / whitespace-only / absent.
1964
+ */
1965
+ function normalizeToken(token) {
1966
+ if (token === null || token === undefined) {
1967
+ return null;
1968
+ }
1969
+ const trimmed = token.trim();
1970
+ return trimmed.length === 0 ? null : trimmed;
1971
+ }
1972
+ /**
1973
+ * Wrap a synchronous value as a single-emission observable. Used by callers that
1974
+ * want to stay in the observable world without importing `rxjs` `of` directly.
1975
+ *
1976
+ * @param value the value to emit.
1977
+ * @returns an observable emitting `value` once and completing.
1978
+ */
1979
+ function once(value) {
1980
+ return of(value);
1981
+ }
1982
+
1983
+ /**
1984
+ * Functional Angular `HttpInterceptor` that attaches the current Keycloak access
1985
+ * token as an `Authorization: Bearer <token>` header to outgoing HTTP requests.
1986
+ *
1987
+ * Behaviour:
1988
+ * - token present → a cloned request carrying the bearer header is forwarded.
1989
+ * - token absent / empty → the original request is forwarded untouched (no empty
1990
+ * `Bearer` header is ever sent).
1991
+ * - token source is async (Promise/Observable) → resolved before the request is
1992
+ * sent.
1993
+ * - an existing `Authorization` header on the request is left untouched, so a
1994
+ * caller that already set credentials explicitly wins.
1995
+ *
1996
+ * Register it in the application's HTTP pipeline:
1997
+ *
1998
+ * ```ts
1999
+ * provideHttpClient(withInterceptors([authHttpInterceptor]))
2000
+ * ```
2001
+ *
2002
+ * Errors raised by the `TokenProvider` propagate to the caller (the request is
2003
+ * not sent) so an authentication failure surfaces rather than silently issuing
2004
+ * an unauthenticated request.
2005
+ *
2006
+ * @param req the outgoing HTTP request.
2007
+ * @param next the next handler in the interceptor chain.
2008
+ * @returns the stream of HTTP events for the (possibly authorized) request.
2009
+ */
2010
+ function authHttpInterceptor(req, next) {
2011
+ if (req.headers.has(AUTHORIZATION_HEADER)) {
2012
+ return next(req);
2013
+ }
2014
+ const tokenProvider = inject(TOKEN_PROVIDER);
2015
+ return resolveBearerValue(tokenProvider.getToken()).pipe(switchMap((bearerValue) => {
2016
+ if (bearerValue === null) {
2017
+ return next(req);
2018
+ }
2019
+ const authorizedRequest = req.clone({
2020
+ setHeaders: { [AUTHORIZATION_HEADER]: bearerValue }
2021
+ });
2022
+ return next(authorizedRequest);
2023
+ }));
2024
+ }
2025
+
2026
+ /**
2027
+ * `@ngx-grpc` interceptor that attaches the current Keycloak access token as an
2028
+ * `authorization: Bearer <token>` entry on the gRPC-web request metadata. This
2029
+ * is the gRPC-web counterpart of {@link authHttpInterceptor} and matches the
2030
+ * `@ngx-grpc` client style used by the generated `SipClient` service client
2031
+ * (`api/ondewo/sip/sip.pbsc.ts`) in this library.
2032
+ *
2033
+ * Behaviour mirrors the HTTP interceptor:
2034
+ * - token present → the bearer credential is set on `requestMetadata`.
2035
+ * - token absent / empty → the request metadata is left untouched (no empty
2036
+ * `Bearer` value is ever attached).
2037
+ * - token source is async (Promise/Observable) → resolved before the request is
2038
+ * handed to the next handler.
2039
+ * - an `authorization` entry already present on the request metadata is left
2040
+ * untouched, so an explicitly-set credential wins.
2041
+ *
2042
+ * Register it via the standard `@ngx-grpc` multi-provider:
2043
+ *
2044
+ * ```ts
2045
+ * providers: [
2046
+ * { provide: GRPC_INTERCEPTORS, useClass: AuthGrpcInterceptor, multi: true },
2047
+ * ]
2048
+ * ```
2049
+ */
2050
+ class AuthGrpcInterceptor {
2051
+ /**
2052
+ * @param tokenProvider the application-supplied {@link TokenProvider}, injected
2053
+ * under the {@link TOKEN_PROVIDER} DI token, that yields the current access
2054
+ * token.
2055
+ */
2056
+ constructor(tokenProvider) {
2057
+ this.tokenProvider = tokenProvider;
2058
+ }
2059
+ /**
2060
+ * Attach the bearer credential (when available) to the request metadata, then
2061
+ * delegate to the next handler in the chain.
2062
+ *
2063
+ * @param request the intercepted gRPC request.
2064
+ * @param next the next handler to pass the request through.
2065
+ * @returns the stream of gRPC events for the (possibly authorized) request.
2066
+ */
2067
+ intercept(request, next) {
2068
+ if (request.requestMetadata.has(AUTHORIZATION_HEADER)) {
2069
+ return next.handle(request);
2070
+ }
2071
+ return resolveBearerValue(this.tokenProvider.getToken()).pipe(switchMap((bearerValue) => {
2072
+ if (bearerValue !== null) {
2073
+ request.requestMetadata.set(AUTHORIZATION_HEADER, bearerValue);
2074
+ }
2075
+ return next.handle(request);
2076
+ }));
2077
+ }
2078
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: AuthGrpcInterceptor, deps: [{ token: TOKEN_PROVIDER }], target: i0.ɵɵFactoryTarget.Injectable }); }
2079
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: AuthGrpcInterceptor }); }
2080
+ }
2081
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: AuthGrpcInterceptor, decorators: [{
2082
+ type: Injectable
2083
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
2084
+ type: Inject,
2085
+ args: [TOKEN_PROVIDER]
2086
+ }] }] });
2087
+
2088
+ /**
2089
+ * Seconds of head-room subtracted from a token's `expires_in` so the background
2090
+ * refresh fires *before* the access token actually lapses (covers clock skew and
2091
+ * the round-trip to Keycloak). Mirrors the nodejs `REFRESH_SKEW_IN_S` and the
2092
+ * python `_EXPIRY_LEEWAY_S` references.
2093
+ */
2094
+ const REFRESH_SKEW_IN_S = 30;
2095
+ /**
2096
+ * Lower bound (in seconds) on the scheduled refresh delay, so a tiny or zero
2097
+ * `expires_in` cannot spin a hot refresh loop.
2098
+ */
2099
+ const MIN_REFRESH_DELAY_IN_S = 1;
2100
+ /**
2101
+ * DI token under which {@link KeycloakTokenProvider} reads its
2102
+ * {@link KeycloakTokenProviderConfig}. The consuming application provides a value
2103
+ * for it (see {@link provideKeycloakTokenProvider}).
2104
+ */
2105
+ const KEYCLOAK_TOKEN_PROVIDER_CONFIG = new InjectionToken("ONDEWO_SIP_KEYCLOAK_TOKEN_PROVIDER_CONFIG");
2106
+ /** Error raised on a missing/invalid configuration or any token-endpoint failure. */
2107
+ class KeycloakAuthenticationError extends Error {
2108
+ /**
2109
+ * @param message a human-readable description of the configuration or token failure.
2110
+ */
2111
+ constructor(message) {
2112
+ super(message);
2113
+ this.name = "KeycloakAuthenticationError";
2114
+ }
2115
+ }
2116
+ /**
2117
+ * Concrete, ready-to-use {@link TokenProvider} that performs the Keycloak headless
2118
+ * offline-token flow itself, so consumers get background access-token refresh
2119
+ * without implementing {@link TokenProvider}.
2120
+ *
2121
+ * On the first {@link getToken} call it logs in once against the Keycloak token
2122
+ * endpoint — either with a supplied offline / refresh token
2123
+ * (`grant_type=refresh_token`) or with a `username` + `password` ROPC grant
2124
+ * (`grant_type=password`, `scope=offline_access`) — then keeps the access token
2125
+ * fresh via a background timer that refreshes shortly *before* expiry (clamped to
2126
+ * an optional bounded deadline, mirroring the nodejs `OfflineTokenProvider` and
2127
+ * python `KeycloakTokenProvider` references). {@link getToken} returns the current
2128
+ * valid access token; the library's interceptors attach it as
2129
+ * `Authorization: Bearer <token>`.
2130
+ *
2131
+ * Register it with {@link provideKeycloakTokenProvider}, then point the SDK auth at
2132
+ * it:
2133
+ *
2134
+ * ```ts
2135
+ * bootstrapApplication(AppComponent, {
2136
+ * providers: [
2137
+ * provideHttpClient(),
2138
+ * provideKeycloakTokenProvider({
2139
+ * keycloakUrl: "https://auth.example.com/auth",
2140
+ * realm: "ondewo-ccai-platform",
2141
+ * clientId: "ondewo-nlu-cai-sdk-public",
2142
+ * refreshToken: "<offline-token>",
2143
+ * }),
2144
+ * provideOndewoSipAuth(KeycloakTokenProvider),
2145
+ * provideHttpClient(withInterceptors([authHttpInterceptor])),
2146
+ * ],
2147
+ * });
2148
+ * ```
2149
+ */
2150
+ class KeycloakTokenProvider {
2151
+ /**
2152
+ * @param http the Angular {@link HttpClient} used for the token-endpoint calls.
2153
+ * @param zone the {@link NgZone}; the background timer is armed outside Angular so it
2154
+ * does not keep change detection / zone stability churning between refreshes.
2155
+ * @param config the {@link KeycloakTokenProviderConfig}, injected under
2156
+ * {@link KEYCLOAK_TOKEN_PROVIDER_CONFIG}.
2157
+ */
2158
+ constructor(http, zone, config) {
2159
+ this.http = http;
2160
+ this.zone = zone;
2161
+ /** The current access token, or `null` before the first login / after the bounded loop lapses. */
2162
+ this.accessToken = null;
2163
+ /** The current refresh token, or `null` before any login completes. */
2164
+ this.refreshToken = null;
2165
+ /** Handle of the armed refresh timer, or `null` when no refresh is scheduled. */
2166
+ this.timer = null;
2167
+ /** Whether {@link ngOnDestroy} has run; suppresses any further (re-)scheduling. */
2168
+ this.stopped = false;
2169
+ /** Absolute epoch-ms deadline for the bounded loop, or `null` when unbounded. */
2170
+ this.deadlineInMs = null;
2171
+ /** The in-flight (or settled) one-time login promise; ensures login happens exactly once. */
2172
+ this.loginPromise = null;
2173
+ if (config === null) {
2174
+ throw new KeycloakAuthenticationError("KeycloakTokenProvider requires a KEYCLOAK_TOKEN_PROVIDER_CONFIG value; " +
2175
+ "register it with provideKeycloakTokenProvider({ ... })");
2176
+ }
2177
+ this.loginRequest = this.validateAndBuildLoginRequest(config);
2178
+ this.tokenEndpoint = KeycloakTokenProvider.buildTokenEndpoint(config.keycloakUrl, config.realm);
2179
+ this.clientId = config.clientId;
2180
+ this.tokenExpirationInS = config.tokenExpirationInS;
2181
+ // Stored for cross-SDK config parity; a no-op on the browser transport (see field doc).
2182
+ this.verifySsl = config.keycloakVerifySsl ?? true;
2183
+ if (config.refreshToken !== undefined) {
2184
+ this.refreshToken = config.refreshToken;
2185
+ }
2186
+ }
2187
+ /**
2188
+ * Return the current access token, logging in on the first call.
2189
+ *
2190
+ * The first invocation returns a `Promise` that resolves once the one-time login
2191
+ * has completed and the background refresh is armed. Subsequent invocations
2192
+ * return the synchronously-held current access token (or `null` if the bounded
2193
+ * loop has lapsed), so interceptors pay no async cost on the hot path.
2194
+ *
2195
+ * @returns the current access token as a {@link TokenResult}.
2196
+ */
2197
+ getToken() {
2198
+ if (this.loginPromise === null) {
2199
+ this.loginPromise = this.bootstrap();
2200
+ }
2201
+ if (this.accessToken !== null) {
2202
+ return this.accessToken;
2203
+ }
2204
+ return this.loginPromise.then(() => this.accessToken);
2205
+ }
2206
+ /**
2207
+ * The resolved TLS-verification setting from
2208
+ * {@link KeycloakTokenProviderConfig.keycloakVerifySsl} (defaults to `true`).
2209
+ *
2210
+ * Exposed for cross-SDK config parity and introspection only. It is a NO-OP in
2211
+ * this browser client — the browser owns the TLS handshake, so the value never
2212
+ * reaches {@link postTokenRequest} and does not change the outgoing request.
2213
+ *
2214
+ * @returns `true` when TLS verification is requested (the default), `false`
2215
+ * when the config explicitly opted out (still inert here).
2216
+ */
2217
+ get keycloakVerifySsl() {
2218
+ return this.verifySsl;
2219
+ }
2220
+ /** Stop the background refresh loop when the provider is torn down. Idempotent. */
2221
+ ngOnDestroy() {
2222
+ this.stopped = true;
2223
+ if (this.timer !== null) {
2224
+ clearTimeout(this.timer);
2225
+ this.timer = null;
2226
+ }
2227
+ }
2228
+ /**
2229
+ * Perform the one-time login (offline-token or ROPC) and arm the first refresh.
2230
+ *
2231
+ * @returns a promise that resolves once the first token is stored and the refresh is armed.
2232
+ * @throws {@link KeycloakAuthenticationError} if the token endpoint fails or returns no
2233
+ * `access_token` / `refresh_token`.
2234
+ */
2235
+ async bootstrap() {
2236
+ const response = await this.postTokenRequest(this.loginRequest);
2237
+ this.storeTokens(response);
2238
+ if (this.refreshToken === null) {
2239
+ throw new KeycloakAuthenticationError("Keycloak token response did not contain a refresh_token; the SDK client must have " +
2240
+ "directAccessGrants + the offline_access scope (e.g. ondewo-nlu-cai-sdk-public)");
2241
+ }
2242
+ if (this.tokenExpirationInS !== undefined) {
2243
+ this.deadlineInMs = Date.now() + (this.tokenExpirationInS * 1000);
2244
+ }
2245
+ this.scheduleRefresh(response.expires_in);
2246
+ }
2247
+ /**
2248
+ * Exchange the refresh token for a fresh access token and re-arm the next refresh.
2249
+ *
2250
+ * Stops the loop (instead of refreshing) once the bounded deadline has elapsed,
2251
+ * letting the access token lapse. If the provider was torn down while this refresh's
2252
+ * request was in flight, {@link scheduleRefresh} declines to arm the next timer.
2253
+ *
2254
+ * @returns a promise that resolves once the token is refreshed and the next refresh is armed.
2255
+ * @throws {@link KeycloakAuthenticationError} if the refresh call fails or returns no `access_token`.
2256
+ */
2257
+ async refresh() {
2258
+ if (this.deadlineInMs !== null && Date.now() >= this.deadlineInMs) {
2259
+ this.ngOnDestroy();
2260
+ return;
2261
+ }
2262
+ const response = await this.postTokenRequest({
2263
+ grant_type: "refresh_token",
2264
+ client_id: this.clientId,
2265
+ // refreshToken is non-null here (bootstrap stored it before arming any refresh), but TS keeps
2266
+ // the field's `string | null` type across the async boundary, so the assertion is required.
2267
+ /* eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- required under the build tsconfig */
2268
+ refresh_token: this.refreshToken
2269
+ });
2270
+ this.storeTokens(response);
2271
+ this.scheduleRefresh(response.expires_in);
2272
+ }
2273
+ /**
2274
+ * Arm a single timer for the next refresh, clamped to the bounded deadline.
2275
+ *
2276
+ * The delay is `expiresInRaw` minus {@link REFRESH_SKEW_IN_S}, floored at
2277
+ * {@link MIN_REFRESH_DELAY_IN_S}, then clamped to the time remaining before the
2278
+ * deadline. Stops silently once `tokenExpirationInS` has elapsed.
2279
+ *
2280
+ * @param expiresInRaw the `expires_in` (seconds) from the latest token response; a missing
2281
+ * or non-positive value falls back to {@link MIN_REFRESH_DELAY_IN_S}.
2282
+ */
2283
+ scheduleRefresh(expiresInRaw) {
2284
+ if (this.stopped) {
2285
+ return;
2286
+ }
2287
+ const expiresInS = typeof expiresInRaw === "number" && expiresInRaw > 0 ? expiresInRaw : MIN_REFRESH_DELAY_IN_S;
2288
+ let delayInS = Math.max(expiresInS - REFRESH_SKEW_IN_S, MIN_REFRESH_DELAY_IN_S);
2289
+ if (this.deadlineInMs !== null) {
2290
+ const remainingInMs = this.deadlineInMs - Date.now();
2291
+ if (remainingInMs <= 0) {
2292
+ this.ngOnDestroy();
2293
+ return;
2294
+ }
2295
+ delayInS = Math.min(delayInS, remainingInMs / 1000);
2296
+ }
2297
+ // Arm outside Angular so the recurring timer does not keep the zone unstable.
2298
+ this.zone.runOutsideAngular(() => {
2299
+ this.timer = setTimeout(() => {
2300
+ void this.refresh();
2301
+ }, delayInS * 1000);
2302
+ });
2303
+ }
2304
+ /**
2305
+ * POST a form-encoded body to the token endpoint and return the parsed JSON.
2306
+ *
2307
+ * @param params the form fields to URL-encode (grant type, client id, credentials).
2308
+ * @returns the parsed {@link KeycloakTokenResponse}.
2309
+ * @throws {@link KeycloakAuthenticationError} on a transport error.
2310
+ */
2311
+ async postTokenRequest(params) {
2312
+ const body = new URLSearchParams(params).toString();
2313
+ const headers = new HttpHeaders({
2314
+ "Content-Type": "application/x-www-form-urlencoded",
2315
+ Accept: "application/json"
2316
+ });
2317
+ try {
2318
+ return await firstValueFrom(this.http.post(this.tokenEndpoint, body, { headers }));
2319
+ }
2320
+ catch (caughtError) {
2321
+ throw new KeycloakAuthenticationError(`Keycloak token endpoint request failed: ${KeycloakTokenProvider.describeError(caughtError)}`);
2322
+ }
2323
+ }
2324
+ /**
2325
+ * Store the access token (and any rotated refresh token) from a token response.
2326
+ *
2327
+ * Keycloak may omit the refresh token on a same-token refresh; the previous one is
2328
+ * kept in that case so it is never blanked out.
2329
+ *
2330
+ * @param response the parsed token-endpoint response.
2331
+ * @throws {@link KeycloakAuthenticationError} if the response carries no `access_token`.
2332
+ */
2333
+ storeTokens(response) {
2334
+ if (typeof response.access_token !== "string" || response.access_token.length === 0) {
2335
+ throw new KeycloakAuthenticationError("Keycloak token response did not contain an access_token");
2336
+ }
2337
+ this.accessToken = response.access_token;
2338
+ if (typeof response.refresh_token === "string" && response.refresh_token.length > 0) {
2339
+ this.refreshToken = response.refresh_token;
2340
+ }
2341
+ }
2342
+ /**
2343
+ * Validate the config and build the one-time login grant parameters in a single pass.
2344
+ *
2345
+ * Validating and building together lets the credential checks narrow the optional
2346
+ * `username` / `password` fields to `string` for the request shape, so no type
2347
+ * assertion or unreachable guard is needed.
2348
+ *
2349
+ * @param config the {@link KeycloakTokenProviderConfig} to validate.
2350
+ * @returns the form parameters for the offline-token (`grant_type=refresh_token`) or
2351
+ * ROPC (`grant_type=password`) login.
2352
+ * @throws {@link KeycloakAuthenticationError} on a missing base field or an invalid
2353
+ * credential combination (neither, or both, credential shapes supplied).
2354
+ */
2355
+ validateAndBuildLoginRequest(config) {
2356
+ for (const key of ["keycloakUrl", "realm", "clientId"]) {
2357
+ if (typeof config[key] !== "string" || config[key].length === 0) {
2358
+ throw new KeycloakAuthenticationError(`KeycloakTokenProviderConfig.${key} is required and must be a non-empty string`);
2359
+ }
2360
+ }
2361
+ const refreshToken = config.refreshToken;
2362
+ const username = config.username;
2363
+ const password = config.password;
2364
+ const hasRefreshToken = typeof refreshToken === "string" && refreshToken.length > 0;
2365
+ const hasCredentials = typeof username === "string" && username.length > 0 && typeof password === "string" && password.length > 0;
2366
+ if (hasRefreshToken === hasCredentials) {
2367
+ throw new KeycloakAuthenticationError("KeycloakTokenProviderConfig requires exactly one of: a non-empty refreshToken, " +
2368
+ "or a non-empty username + password pair");
2369
+ }
2370
+ if (refreshToken !== undefined && refreshToken.length > 0) {
2371
+ return {
2372
+ grant_type: "refresh_token",
2373
+ client_id: config.clientId,
2374
+ refresh_token: refreshToken
2375
+ };
2376
+ }
2377
+ // hasCredentials === !hasRefreshToken here guarantees both are non-empty strings, but TS does
2378
+ // not propagate that from the boolean above; the assertions narrow them for the request shape.
2379
+ return {
2380
+ grant_type: "password",
2381
+ client_id: config.clientId,
2382
+ /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion -- required under the build tsconfig */
2383
+ username: username,
2384
+ password: password,
2385
+ /* eslint-enable @typescript-eslint/no-unnecessary-type-assertion */
2386
+ scope: "offline_access"
2387
+ };
2388
+ }
2389
+ /**
2390
+ * Build the OIDC token endpoint URL for a realm, tolerating a trailing slash.
2391
+ *
2392
+ * @param keycloakUrl the base Keycloak URL (trailing slashes are stripped).
2393
+ * @param realm the realm name; URL-encoded into the path.
2394
+ * @returns the fully-qualified `.../protocol/openid-connect/token` endpoint URL.
2395
+ */
2396
+ static buildTokenEndpoint(keycloakUrl, realm) {
2397
+ const base = keycloakUrl.replace(/\/+$/, "");
2398
+ return `${base}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/token`;
2399
+ }
2400
+ /**
2401
+ * Render an arbitrary thrown value into a short message for error wrapping.
2402
+ *
2403
+ * @param caughtError the value thrown by the failing token call.
2404
+ * @returns the error's `message` when it is an `Error`, otherwise its string form.
2405
+ */
2406
+ static describeError(caughtError) {
2407
+ if (caughtError instanceof Error) {
2408
+ return caughtError.message;
2409
+ }
2410
+ return typeof caughtError === "string" ? caughtError : JSON.stringify(caughtError);
2411
+ }
2412
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: KeycloakTokenProvider, deps: [{ token: i1$1.HttpClient }, { token: i0.NgZone }, { token: KEYCLOAK_TOKEN_PROVIDER_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
2413
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: KeycloakTokenProvider, providedIn: "root" }); }
2414
+ }
2415
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.29", ngImport: i0, type: KeycloakTokenProvider, decorators: [{
2416
+ type: Injectable,
2417
+ args: [{ providedIn: "root" }]
2418
+ }], ctorParameters: () => [{ type: i1$1.HttpClient }, { type: i0.NgZone }, { type: undefined, decorators: [{
2419
+ type: Optional
2420
+ }, {
2421
+ type: Inject,
2422
+ args: [KEYCLOAK_TOKEN_PROVIDER_CONFIG]
2423
+ }] }] });
2424
+
2425
+ /**
2426
+ * Wire a consuming application's {@link TokenProvider} implementation into this
2427
+ * library and register the `@ngx-grpc` {@link AuthGrpcInterceptor} that uses it.
2428
+ *
2429
+ * This covers the gRPC-web side. For HTTP requests, additionally register the
2430
+ * functional `authHttpInterceptor`:
2431
+ *
2432
+ * ```ts
2433
+ * provideHttpClient(withInterceptors([authHttpInterceptor]))
2434
+ * ```
2435
+ *
2436
+ * Usage in an application's `providers` (standalone bootstrap or `AppModule`):
2437
+ *
2438
+ * ```ts
2439
+ * import { provideOndewoSipAuth } from "@ondewo/sip-client-angular";
2440
+ *
2441
+ * bootstrapApplication(AppComponent, {
2442
+ * providers: [
2443
+ * provideOndewoSipAuth(KeycloakTokenProvider),
2444
+ * provideHttpClient(withInterceptors([authHttpInterceptor])),
2445
+ * ],
2446
+ * });
2447
+ * ```
2448
+ *
2449
+ * @param tokenProvider the application's `TokenProvider` class (e.g. one that
2450
+ * wraps `keycloak-js` / `keycloak-angular`).
2451
+ * @returns environment providers binding the token provider and the gRPC
2452
+ * interceptor.
2453
+ */
2454
+ function provideOndewoSipAuth(tokenProvider) {
2455
+ const providers = [
2456
+ tokenProvider,
2457
+ { provide: TOKEN_PROVIDER, useExisting: tokenProvider },
2458
+ { provide: GRPC_INTERCEPTORS, useClass: AuthGrpcInterceptor, multi: true }
2459
+ ];
2460
+ return makeEnvironmentProviders(providers);
2461
+ }
2462
+ /**
2463
+ * Register the configuration the built-in `KeycloakTokenProvider` reads.
2464
+ *
2465
+ * Pair it with `provideOndewoSipAuth(KeycloakTokenProvider)` (and `provideHttpClient()`)
2466
+ * so consumers get background access-token refresh without implementing
2467
+ * {@link TokenProvider} themselves:
2468
+ *
2469
+ * ```ts
2470
+ * bootstrapApplication(AppComponent, {
2471
+ * providers: [
2472
+ * provideHttpClient(withInterceptors([authHttpInterceptor])),
2473
+ * provideKeycloakTokenProvider({
2474
+ * keycloakUrl: "https://auth.example.com/auth",
2475
+ * realm: "ondewo-ccai-platform",
2476
+ * clientId: "ondewo-nlu-cai-sdk-public",
2477
+ * refreshToken: "<offline-token>",
2478
+ * }),
2479
+ * provideOndewoSipAuth(KeycloakTokenProvider),
2480
+ * ],
2481
+ * });
2482
+ * ```
2483
+ *
2484
+ * @param config the {@link KeycloakTokenProviderConfig} the provider logs in with.
2485
+ * @returns environment providers binding the config under {@link KEYCLOAK_TOKEN_PROVIDER_CONFIG}.
2486
+ */
2487
+ function provideKeycloakTokenProvider(config) {
2488
+ return makeEnvironmentProviders([{ provide: KEYCLOAK_TOKEN_PROVIDER_CONFIG, useValue: config }]);
2489
+ }
2490
+
2491
+ /**
2492
+ * Public auth surface for `@ondewo/sip-client-angular`.
2493
+ *
2494
+ * The consuming application supplies the current Keycloak access token through a
2495
+ * {@link TokenProvider} (fed from `keycloak-js` / `keycloak-angular`); this
2496
+ * library attaches it as an `Authorization: Bearer <token>` credential to
2497
+ * outgoing gRPC-web and HTTP requests. No OAuth/OIDC flow is performed here.
2498
+ */
2499
+
1880
2500
  /**
1881
2501
  * Generated bundle index. Do not edit.
1882
2502
  */
1883
2503
 
1884
- export { GRPC_SIP_CLIENT_SETTINGS, SipClient, SipEndCallRequest, SipPlayWavFilesRequest, SipRegisterAccountRequest, SipStartCallRequest, SipStartSessionRequest, SipStatus, SipStatusHistoryResponse, SipTransferCallRequest };
2504
+ export { AUTHORIZATION_HEADER, AuthGrpcInterceptor, BEARER_PREFIX, GRPC_SIP_CLIENT_SETTINGS, KEYCLOAK_TOKEN_PROVIDER_CONFIG, KeycloakAuthenticationError, KeycloakTokenProvider, MIN_REFRESH_DELAY_IN_S, REFRESH_SKEW_IN_S, SipClient, SipEndCallRequest, SipPlayWavFilesRequest, SipRegisterAccountRequest, SipStartCallRequest, SipStartSessionRequest, SipStatus, SipStatusHistoryResponse, SipTransferCallRequest, TOKEN_PROVIDER, authHttpInterceptor, buildBearerValue, provideKeycloakTokenProvider, provideOndewoSipAuth, resolveBearerValue, resolveToken };
1885
2505
  //# sourceMappingURL=ondewo-sip-client-angular.mjs.map