@ondewo/sip-client-nodejs 5.4.1 → 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 +94 -0
- 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
|
@@ -13,15 +13,6 @@
|
|
|
13
13
|
// See the License for the specific language governing permissions and
|
|
14
14
|
// limitations under the License.
|
|
15
15
|
//
|
|
16
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
17
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
18
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
19
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
20
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
21
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
22
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
23
|
-
});
|
|
24
|
-
};
|
|
25
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
17
|
exports.OfflineTokenProvider = exports.TokenError = void 0;
|
|
27
18
|
exports.login = login;
|
|
@@ -76,54 +67,54 @@ function buildTokenEndpoint(keycloakUrl, realm) {
|
|
|
76
67
|
* @returns A promise resolving to the parsed {@link KeycloakTokenResponse}.
|
|
77
68
|
* @throws {TokenError} On a non-2xx response, an unparseable body, or a missing `access_token`.
|
|
78
69
|
*/
|
|
79
|
-
function postTokenRequest(tokenEndpoint, params, fetchImpl) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
body
|
|
89
|
-
});
|
|
90
|
-
const text = yield response.text();
|
|
91
|
-
if (!response.ok) {
|
|
92
|
-
throw new TokenError(`Keycloak token endpoint returned HTTP ${response.status}: ${text}`);
|
|
93
|
-
}
|
|
94
|
-
let parsed;
|
|
95
|
-
try {
|
|
96
|
-
parsed = JSON.parse(text);
|
|
97
|
-
}
|
|
98
|
-
catch (_a) {
|
|
99
|
-
throw new TokenError(`Keycloak token endpoint returned a non-JSON body: ${text}`);
|
|
100
|
-
}
|
|
101
|
-
if (typeof parsed.access_token !== 'string' || parsed.access_token.length === 0) {
|
|
102
|
-
throw new TokenError('Keycloak token response did not contain an access_token');
|
|
103
|
-
}
|
|
104
|
-
return parsed;
|
|
70
|
+
async function postTokenRequest(tokenEndpoint, params, fetchImpl) {
|
|
71
|
+
const body = new URLSearchParams(params).toString();
|
|
72
|
+
const response = await fetchImpl(tokenEndpoint, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
76
|
+
Accept: 'application/json'
|
|
77
|
+
},
|
|
78
|
+
body
|
|
105
79
|
});
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new TokenError(`Keycloak token endpoint returned HTTP ${response.status}: ${text}`);
|
|
83
|
+
}
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new TokenError(`Keycloak token endpoint returned a non-JSON body: ${text}`);
|
|
90
|
+
}
|
|
91
|
+
if (typeof parsed.access_token !== 'string' || parsed.access_token.length === 0) {
|
|
92
|
+
throw new TokenError('Keycloak token response did not contain an access_token');
|
|
93
|
+
}
|
|
94
|
+
return parsed;
|
|
106
95
|
}
|
|
107
96
|
/**
|
|
108
|
-
* Build the default
|
|
97
|
+
* Build the default {@link TokenFetch}: delegate to the global `fetch` (Node >= 18).
|
|
109
98
|
*
|
|
110
99
|
* When `verifySsl` is `false`, a cached undici `Agent` with `rejectUnauthorized: false` is attached
|
|
111
100
|
* to every request as its `dispatcher`, so the Keycloak token call skips TLS certificate verification
|
|
112
101
|
* (opt-in insecure; Node-only). The dispatcher is built once here and reused for all requests this
|
|
113
102
|
* transport makes (login + refreshes); the secure default never loads undici.
|
|
114
103
|
*
|
|
115
|
-
* @param
|
|
116
|
-
* @returns
|
|
104
|
+
* @param verifySsl - Whether to verify the Keycloak server's TLS certificate.
|
|
105
|
+
* @returns A fetch layer bound to the chosen TLS-verification behaviour.
|
|
117
106
|
*/
|
|
118
107
|
function createDefaultFetch(verifySsl) {
|
|
119
108
|
let dispatcher;
|
|
120
109
|
if (!verifySsl) {
|
|
110
|
+
// Lazy require keeps undici out of the default (secure) code path.
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
121
112
|
const { Agent } = require('undici');
|
|
122
113
|
dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
|
123
114
|
}
|
|
124
115
|
return (url, init) => {
|
|
125
116
|
const globalFetch = globalThis.fetch;
|
|
126
|
-
return globalFetch(url, dispatcher === undefined ? init :
|
|
117
|
+
return globalFetch(url, dispatcher === undefined ? init : { ...init, dispatcher });
|
|
127
118
|
};
|
|
128
119
|
}
|
|
129
120
|
/**
|
|
@@ -141,7 +132,8 @@ class OfflineTokenProvider {
|
|
|
141
132
|
this.tokenEndpoint = buildTokenEndpoint(options.keycloakUrl, options.realm);
|
|
142
133
|
this.clientId = options.clientId;
|
|
143
134
|
this.tokenExpirationInS = options.tokenExpirationInS;
|
|
144
|
-
this.fetchImpl =
|
|
135
|
+
this.fetchImpl =
|
|
136
|
+
options.fetchImpl !== undefined ? options.fetchImpl : createDefaultFetch(options.keycloakVerifySsl ?? true);
|
|
145
137
|
this.nowInMs = options.nowInMs !== undefined ? options.nowInMs : Date.now;
|
|
146
138
|
this.accessToken = null;
|
|
147
139
|
this.refreshToken = null;
|
|
@@ -159,27 +151,25 @@ class OfflineTokenProvider {
|
|
|
159
151
|
* @throws {TokenError} If the token endpoint fails or the response carries no `refresh_token`
|
|
160
152
|
* (the SDK client lacks `directAccessGrants` + the `offline_access` scope).
|
|
161
153
|
*/
|
|
162
|
-
bootstrap(username, password) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
this.scheduleRefresh(tokenResponse.expires_in);
|
|
182
|
-
});
|
|
154
|
+
async bootstrap(username, password) {
|
|
155
|
+
const tokenResponse = await postTokenRequest(this.tokenEndpoint, {
|
|
156
|
+
grant_type: 'password',
|
|
157
|
+
client_id: this.clientId,
|
|
158
|
+
username,
|
|
159
|
+
password,
|
|
160
|
+
scope: 'offline_access'
|
|
161
|
+
}, this.fetchImpl);
|
|
162
|
+
this.accessToken = tokenResponse.access_token;
|
|
163
|
+
this.refreshToken = typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : null;
|
|
164
|
+
if (this.refreshToken === null) {
|
|
165
|
+
throw new TokenError('Keycloak token response did not contain a refresh_token; the SDK client must have ' +
|
|
166
|
+
'directAccessGrants + the offline_access scope (ondewo-sip-cai-sdk-public)');
|
|
167
|
+
}
|
|
168
|
+
if (this.tokenExpirationInS !== undefined) {
|
|
169
|
+
const expirationInMs = this.tokenExpirationInS * 1000;
|
|
170
|
+
this.deadlineInMs = this.nowInMs() + expirationInMs;
|
|
171
|
+
}
|
|
172
|
+
this.scheduleRefresh(tokenResponse.expires_in);
|
|
183
173
|
}
|
|
184
174
|
/**
|
|
185
175
|
* Exchange the offline refresh token for a fresh access token and re-arm the next refresh.
|
|
@@ -188,30 +178,28 @@ class OfflineTokenProvider {
|
|
|
188
178
|
* @returns A promise that resolves once the token is refreshed (or the loop has lapsed/stopped).
|
|
189
179
|
* @throws {TokenError} If the refresh token-endpoint call fails or returns an invalid body.
|
|
190
180
|
*/
|
|
191
|
-
refresh() {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.scheduleRefresh(tokenResponse.expires_in);
|
|
214
|
-
});
|
|
181
|
+
async refresh() {
|
|
182
|
+
/* c8 ignore next 3 -- unreachable: stop() always clears the only timer that calls refresh() */
|
|
183
|
+
if (this.stopped) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Re-check the bounded deadline at fire time (not just at schedule time): once it has elapsed the
|
|
187
|
+
// loop stops with no further renewal -> the access token lapses -> re-login is required.
|
|
188
|
+
if (this.deadlineInMs !== null && this.nowInMs() >= this.deadlineInMs) {
|
|
189
|
+
this.stop();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const tokenResponse = await postTokenRequest(this.tokenEndpoint, {
|
|
193
|
+
grant_type: 'refresh_token',
|
|
194
|
+
client_id: this.clientId,
|
|
195
|
+
refresh_token: this.refreshToken
|
|
196
|
+
}, this.fetchImpl);
|
|
197
|
+
this.accessToken = tokenResponse.access_token;
|
|
198
|
+
// Keycloak may rotate the offline refresh token; keep the newest one when present.
|
|
199
|
+
if (typeof tokenResponse.refresh_token === 'string' && tokenResponse.refresh_token.length > 0) {
|
|
200
|
+
this.refreshToken = tokenResponse.refresh_token;
|
|
201
|
+
}
|
|
202
|
+
this.scheduleRefresh(tokenResponse.expires_in);
|
|
215
203
|
}
|
|
216
204
|
/**
|
|
217
205
|
* Arm a single timer for the next refresh, clamped to the bounded deadline. Stops silently once
|
|
@@ -301,20 +289,18 @@ exports.OfflineTokenProvider = OfflineTokenProvider;
|
|
|
301
289
|
* @throws {TokenError} If `options` is missing, a required string option is empty, or the token
|
|
302
290
|
* endpoint / response is invalid.
|
|
303
291
|
*/
|
|
304
|
-
function login(options) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
throw new TokenError(`login() option "${key}" is required and must be a non-empty string`);
|
|
314
|
-
}
|
|
292
|
+
async function login(options) {
|
|
293
|
+
if (options === undefined || options === null) {
|
|
294
|
+
throw new TokenError('login() requires an options object');
|
|
295
|
+
}
|
|
296
|
+
const requiredKeys = ['keycloakUrl', 'realm', 'clientId', 'username', 'password'];
|
|
297
|
+
for (const key of requiredKeys) {
|
|
298
|
+
const value = options[key];
|
|
299
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
300
|
+
throw new TokenError(`login() option "${key}" is required and must be a non-empty string`);
|
|
315
301
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
302
|
+
}
|
|
303
|
+
const provider = new OfflineTokenProvider(options);
|
|
304
|
+
await provider.bootstrap(options.username, options.password);
|
|
305
|
+
return provider;
|
|
320
306
|
}
|
package/package.json
CHANGED
package/public-api.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export * from './api/ondewo/sip/sip_grpc_pb.d';
|
|
2
|
-
export * from './api/ondewo/sip/sip_pb.d';
|
|
3
1
|
export * from './api/google/protobuf/empty_pb.d';
|
|
4
2
|
export * from './api/google/protobuf/timestamp_pb.d';
|
|
3
|
+
export * from './api/ondewo/sip/sip_grpc_pb.d';
|
|
4
|
+
export * from './api/ondewo/sip/sip_pb.d';
|
|
5
|
+
export * from './auth/grpcChannel';
|
|
6
|
+
export * from './auth/offlineTokenProvider';
|
package/public-api.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
function reexport(m) {
|
|
3
|
+
Object.keys(m).forEach(function (k) {
|
|
4
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) {
|
|
5
|
+
Object.defineProperty(exports, k, { enumerable: true, get: function () { return m[k]; } });
|
|
6
|
+
}
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
reexport(require('./api/google/protobuf/empty_grpc_pb'));
|
|
10
|
+
reexport(require('./api/google/protobuf/empty_pb'));
|
|
11
|
+
reexport(require('./api/google/protobuf/timestamp_grpc_pb'));
|
|
12
|
+
reexport(require('./api/google/protobuf/timestamp_pb'));
|
|
13
|
+
reexport(require('./api/ondewo/sip/sip_grpc_pb'));
|
|
14
|
+
reexport(require('./api/ondewo/sip/sip_pb'));
|
|
15
|
+
reexport(require('./auth/grpcChannel'));
|
|
16
|
+
reexport(require('./auth/offlineTokenProvider'));
|