@naylence/runtime 0.3.5-test.910 → 0.3.5-test.913

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.
Files changed (47) hide show
  1. package/dist/browser/index.cjs +1915 -1214
  2. package/dist/browser/index.mjs +1910 -1209
  3. package/dist/cjs/naylence/fame/config/extended-fame-config.js +52 -0
  4. package/dist/cjs/naylence/fame/factory-manifest.js +2 -0
  5. package/dist/cjs/naylence/fame/http/jwks-api-router.js +16 -18
  6. package/dist/cjs/naylence/fame/http/oauth2-server.js +28 -31
  7. package/dist/cjs/naylence/fame/http/oauth2-token-router.js +901 -93
  8. package/dist/cjs/naylence/fame/http/openid-configuration-router.js +30 -32
  9. package/dist/cjs/naylence/fame/node/admission/admission-profile-factory.js +79 -0
  10. package/dist/cjs/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.js +171 -0
  11. package/dist/cjs/naylence/fame/security/auth/oauth2-pkce-token-provider.js +560 -0
  12. package/dist/cjs/naylence/fame/security/crypto/providers/default-crypto-provider.js +0 -162
  13. package/dist/cjs/naylence/fame/telemetry/open-telemetry-trace-emitter-factory.js +19 -2
  14. package/dist/cjs/naylence/fame/telemetry/open-telemetry-trace-emitter.js +19 -9
  15. package/dist/cjs/naylence/fame/util/register-runtime-factories.js +6 -0
  16. package/dist/cjs/version.js +2 -2
  17. package/dist/esm/naylence/fame/config/extended-fame-config.js +52 -0
  18. package/dist/esm/naylence/fame/factory-manifest.js +2 -0
  19. package/dist/esm/naylence/fame/http/jwks-api-router.js +16 -17
  20. package/dist/esm/naylence/fame/http/oauth2-server.js +28 -31
  21. package/dist/esm/naylence/fame/http/oauth2-token-router.js +901 -93
  22. package/dist/esm/naylence/fame/http/openid-configuration-router.js +30 -31
  23. package/dist/esm/naylence/fame/node/admission/admission-profile-factory.js +79 -0
  24. package/dist/esm/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.js +134 -0
  25. package/dist/esm/naylence/fame/security/auth/oauth2-pkce-token-provider.js +555 -0
  26. package/dist/esm/naylence/fame/security/crypto/providers/default-crypto-provider.js +0 -162
  27. package/dist/esm/naylence/fame/telemetry/open-telemetry-trace-emitter-factory.js +19 -2
  28. package/dist/esm/naylence/fame/telemetry/open-telemetry-trace-emitter.js +19 -9
  29. package/dist/esm/naylence/fame/util/register-runtime-factories.js +6 -0
  30. package/dist/esm/version.js +2 -2
  31. package/dist/node/index.cjs +1911 -1210
  32. package/dist/node/index.mjs +1910 -1209
  33. package/dist/node/node.cjs +2945 -1439
  34. package/dist/node/node.mjs +2944 -1438
  35. package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
  36. package/dist/types/naylence/fame/http/jwks-api-router.d.ts +8 -8
  37. package/dist/types/naylence/fame/http/oauth2-server.d.ts +3 -3
  38. package/dist/types/naylence/fame/http/oauth2-token-router.d.ts +75 -19
  39. package/dist/types/naylence/fame/http/openid-configuration-router.d.ts +8 -8
  40. package/dist/types/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.d.ts +27 -0
  41. package/dist/types/naylence/fame/security/auth/oauth2-pkce-token-provider.d.ts +42 -0
  42. package/dist/types/naylence/fame/security/crypto/providers/default-crypto-provider.d.ts +0 -1
  43. package/dist/types/naylence/fame/telemetry/open-telemetry-trace-emitter.d.ts +4 -0
  44. package/dist/types/version.d.ts +1 -1
  45. package/package.json +4 -4
  46. package/dist/esm/naylence/fame/fastapi/oauth2-server.js +0 -205
  47. package/dist/types/naylence/fame/fastapi/oauth2-server.d.ts +0 -22
@@ -0,0 +1,560 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuth2PkceTokenProvider = exports.OAuth2PkceRedirectInitiatedError = void 0;
4
+ const logging_js_1 = require("../../util/logging.js");
5
+ const credential_provider_js_1 = require("../credential/credential-provider.js");
6
+ const logger = (0, logging_js_1.getLogger)('naylence.fame.security.auth.oauth2_pkce_token_provider');
7
+ const DEFAULT_SCOPES = [];
8
+ const DEFAULT_CLOCK_SKEW_SECONDS = 30;
9
+ const DEFAULT_CODE_VERIFIER_LENGTH = 48; // bytes before base64url encoding
10
+ const DEFAULT_CODE_CHALLENGE_METHOD = 'S256';
11
+ function normalizeScopes(candidate) {
12
+ if (Array.isArray(candidate)) {
13
+ const scopes = candidate
14
+ .map((scope) => (typeof scope === 'string' ? scope.trim() : ''))
15
+ .filter((scope) => scope.length > 0);
16
+ return scopes.length > 0 ? scopes : undefined;
17
+ }
18
+ if (typeof candidate === 'string') {
19
+ const scopes = candidate
20
+ .split(/[,\s]+/u)
21
+ .map((scope) => scope.trim())
22
+ .filter((scope) => scope.length > 0);
23
+ return scopes.length > 0 ? scopes : undefined;
24
+ }
25
+ return undefined;
26
+ }
27
+ function coerceString(value) {
28
+ if (typeof value !== 'string') {
29
+ return undefined;
30
+ }
31
+ const trimmed = value.trim();
32
+ return trimmed.length > 0 ? trimmed : undefined;
33
+ }
34
+ function coerceNumber(value) {
35
+ if (typeof value === 'number' && Number.isFinite(value)) {
36
+ return value;
37
+ }
38
+ if (typeof value === 'string' && value.trim().length > 0) {
39
+ const parsed = Number(value);
40
+ return Number.isFinite(parsed) ? parsed : undefined;
41
+ }
42
+ return undefined;
43
+ }
44
+ function normalizeOptions(raw) {
45
+ const camel = raw;
46
+ const snake = raw;
47
+ const authorizeUrl = coerceString(camel.authorizeUrl) ?? coerceString(snake.authorize_url);
48
+ if (!authorizeUrl) {
49
+ throw new Error('OAuth2PkceTokenProvider authorizeUrl must be provided');
50
+ }
51
+ const tokenUrl = coerceString(camel.tokenUrl) ?? coerceString(snake.token_url);
52
+ if (!tokenUrl) {
53
+ throw new Error('OAuth2PkceTokenProvider tokenUrl must be provided');
54
+ }
55
+ const redirectUri = coerceString(camel.redirectUri) ?? coerceString(snake.redirect_uri);
56
+ if (!redirectUri) {
57
+ throw new Error('OAuth2PkceTokenProvider redirectUri must be provided');
58
+ }
59
+ const clientId = coerceString(camel.clientId) ?? coerceString(snake.client_id);
60
+ if (!clientId) {
61
+ throw new Error('OAuth2PkceTokenProvider clientId must be provided');
62
+ }
63
+ const usernameProvider = camel.usernameProvider ??
64
+ snake.username_provider;
65
+ const clientSecretProvider = camel.clientSecretProvider ??
66
+ snake.client_secret_provider;
67
+ const scopes = normalizeScopes(camel.scopes) ??
68
+ normalizeScopes(snake.scopes ?? snake.scope) ??
69
+ DEFAULT_SCOPES.slice();
70
+ const audience = coerceString(camel.audience) ??
71
+ coerceString(snake.audience ?? snake.aud);
72
+ const fetchImpl = (camel.fetchImpl ?? snake.fetch_impl);
73
+ const clockSkewSeconds = coerceNumber(camel.clockSkewSeconds) ??
74
+ coerceNumber(snake.clock_skew_seconds) ??
75
+ DEFAULT_CLOCK_SKEW_SECONDS;
76
+ const codeVerifierLength = coerceNumber(camel.codeVerifierLength) ??
77
+ coerceNumber(snake.code_verifier_length) ??
78
+ DEFAULT_CODE_VERIFIER_LENGTH;
79
+ const methodCandidate = coerceString(camel.codeChallengeMethod) ??
80
+ coerceString(snake.code_challenge_method);
81
+ const codeChallengeMethod = methodCandidate && methodCandidate.toUpperCase() === 'PLAIN'
82
+ ? 'PLAIN'
83
+ : DEFAULT_CODE_CHALLENGE_METHOD;
84
+ const loginHintParam = coerceString(camel.loginHintParam) ??
85
+ coerceString(snake.login_hint_param) ??
86
+ 'login_hint';
87
+ return {
88
+ authorizeUrl,
89
+ tokenUrl,
90
+ redirectUri,
91
+ clientId,
92
+ usernameProvider,
93
+ clientSecretProvider,
94
+ scopes,
95
+ audience,
96
+ fetchImpl,
97
+ clockSkewSeconds,
98
+ codeVerifierLength,
99
+ codeChallengeMethod,
100
+ loginHintParam,
101
+ };
102
+ }
103
+ function generateRandomBytes(length) {
104
+ if (typeof globalThis.crypto?.getRandomValues !== 'function') {
105
+ throw new Error('crypto.getRandomValues is unavailable. Provide a secure random source.');
106
+ }
107
+ const buffer = new Uint8Array(length);
108
+ globalThis.crypto.getRandomValues(buffer);
109
+ return buffer;
110
+ }
111
+ function base64UrlEncode(buffer) {
112
+ const bufferCtor = globalThis.Buffer;
113
+ if (bufferCtor) {
114
+ return bufferCtor
115
+ .from(buffer)
116
+ .toString('base64')
117
+ .replace(/\+/g, '-')
118
+ .replace(/\//g, '_')
119
+ .replace(/=+$/u, '');
120
+ }
121
+ let binary = '';
122
+ for (const byte of buffer) {
123
+ binary += String.fromCharCode(byte);
124
+ }
125
+ if (typeof globalThis.btoa === 'function') {
126
+ return globalThis
127
+ .btoa(binary)
128
+ .replace(/\+/g, '-')
129
+ .replace(/\//g, '_')
130
+ .replace(/=+$/u, '');
131
+ }
132
+ throw new Error('Base64 encoding is unavailable in this environment');
133
+ }
134
+ async function computeS256(verifier) {
135
+ if (typeof globalThis.crypto?.subtle !== 'object') {
136
+ throw new Error('crypto.subtle.digest is unavailable. Provide an environment with Web Crypto support.');
137
+ }
138
+ const encoder = new TextEncoder();
139
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', encoder.encode(verifier));
140
+ return base64UrlEncode(new Uint8Array(digest));
141
+ }
142
+ function ensureFinitePositive(value, label) {
143
+ if (!Number.isFinite(value) || value <= 0) {
144
+ throw new Error(`${label} must be a positive finite number`);
145
+ }
146
+ return Math.max(1, Math.floor(value));
147
+ }
148
+ const STORAGE_NAMESPACE = 'naylence.oauth2_pkce.';
149
+ const TOKEN_STORAGE_SUFFIX = '.token';
150
+ function getStorageKey(clientId) {
151
+ return `${STORAGE_NAMESPACE}${clientId}`;
152
+ }
153
+ function getTokenStorageKey(clientId) {
154
+ return `${STORAGE_NAMESPACE}${clientId}${TOKEN_STORAGE_SUFFIX}`;
155
+ }
156
+ function isBrowserEnvironment() {
157
+ return (typeof window !== 'undefined' &&
158
+ typeof window.location !== 'undefined' &&
159
+ typeof window.sessionStorage !== 'undefined');
160
+ }
161
+ function readPendingAuthorization(clientId) {
162
+ if (!isBrowserEnvironment()) {
163
+ return null;
164
+ }
165
+ try {
166
+ const raw = window.sessionStorage.getItem(getStorageKey(clientId));
167
+ if (!raw) {
168
+ return null;
169
+ }
170
+ const parsed = JSON.parse(raw);
171
+ if (!parsed || typeof parsed !== 'object') {
172
+ return null;
173
+ }
174
+ return parsed;
175
+ }
176
+ catch (error) {
177
+ logger.debug('pkce_storage_read_failed', {
178
+ error: error instanceof Error ? error.message : String(error),
179
+ });
180
+ return null;
181
+ }
182
+ }
183
+ function writePendingAuthorization(clientId, pending) {
184
+ if (!isBrowserEnvironment()) {
185
+ return;
186
+ }
187
+ try {
188
+ const key = getStorageKey(clientId);
189
+ if (!pending) {
190
+ window.sessionStorage.removeItem(key);
191
+ return;
192
+ }
193
+ window.sessionStorage.setItem(key, JSON.stringify(pending));
194
+ }
195
+ catch (error) {
196
+ logger.debug('pkce_storage_write_failed', {
197
+ error: error instanceof Error ? error.message : String(error),
198
+ });
199
+ }
200
+ }
201
+ function stableScopeKey(scopes) {
202
+ if (!scopes || scopes.length === 0) {
203
+ return '';
204
+ }
205
+ return [...scopes].sort().join(' ');
206
+ }
207
+ function readPersistedToken(clientId) {
208
+ if (!isBrowserEnvironment()) {
209
+ return null;
210
+ }
211
+ try {
212
+ const raw = window.sessionStorage.getItem(getTokenStorageKey(clientId));
213
+ if (!raw) {
214
+ return null;
215
+ }
216
+ const parsed = JSON.parse(raw);
217
+ const value = coerceString(parsed.value);
218
+ if (!value) {
219
+ return null;
220
+ }
221
+ const expiresAt = coerceNumber(parsed.expiresAt);
222
+ const scopes = normalizeScopes(parsed.scopes);
223
+ const audience = coerceString(parsed.audience);
224
+ const record = {
225
+ value,
226
+ scopes,
227
+ audience,
228
+ };
229
+ if (typeof expiresAt === 'number') {
230
+ record.expiresAt = expiresAt;
231
+ }
232
+ return record;
233
+ }
234
+ catch (error) {
235
+ logger.debug('pkce_token_storage_read_failed', {
236
+ error: error instanceof Error ? error.message : String(error),
237
+ });
238
+ return null;
239
+ }
240
+ }
241
+ function writePersistedToken(clientId, token) {
242
+ if (!isBrowserEnvironment()) {
243
+ return;
244
+ }
245
+ const key = getTokenStorageKey(clientId);
246
+ try {
247
+ if (!token) {
248
+ window.sessionStorage.removeItem(key);
249
+ return;
250
+ }
251
+ window.sessionStorage.setItem(key, JSON.stringify(token));
252
+ }
253
+ catch (error) {
254
+ logger.debug('pkce_token_storage_write_failed', {
255
+ error: error instanceof Error ? error.message : String(error),
256
+ });
257
+ }
258
+ }
259
+ function clearOAuthParamsFromUrl(url) {
260
+ if (!isBrowserEnvironment()) {
261
+ return;
262
+ }
263
+ try {
264
+ const cleaned = new URL(url.toString());
265
+ cleaned.searchParams.delete('code');
266
+ cleaned.searchParams.delete('state');
267
+ cleaned.searchParams.delete('error');
268
+ cleaned.searchParams.delete('error_description');
269
+ cleaned.searchParams.delete('error_description'.toUpperCase());
270
+ cleaned.searchParams.delete('scope');
271
+ cleaned.searchParams.delete('scope'.toUpperCase());
272
+ const finalUrl = `${cleaned.pathname}${cleaned.search}${cleaned.hash}`;
273
+ window.history.replaceState(window.history.state, '', finalUrl);
274
+ }
275
+ catch (error) {
276
+ logger.debug('pkce_replace_state_failed', {
277
+ error: error instanceof Error ? error.message : String(error),
278
+ });
279
+ }
280
+ }
281
+ function collectRedirectOutcome(currentLocation) {
282
+ const url = new URL(currentLocation.href);
283
+ const code = url.searchParams.get('code');
284
+ const state = url.searchParams.get('state');
285
+ const error = url.searchParams.get('error');
286
+ const errorDescription = url.searchParams.get('error_description') ??
287
+ url.searchParams.get('error_description'.toUpperCase());
288
+ if (!code && !error) {
289
+ return null;
290
+ }
291
+ return {
292
+ code: code ?? '',
293
+ state: state ?? '',
294
+ error: error ?? undefined,
295
+ errorDescription: errorDescription ?? undefined,
296
+ };
297
+ }
298
+ class OAuth2PkceRedirectInitiatedError extends Error {
299
+ constructor(message = 'OAuth2PkceTokenProvider initiated browser redirect') {
300
+ super(message);
301
+ this.name = 'OAuth2PkceRedirectInitiatedError';
302
+ }
303
+ }
304
+ exports.OAuth2PkceRedirectInitiatedError = OAuth2PkceRedirectInitiatedError;
305
+ class OAuth2PkceTokenProvider {
306
+ constructor(rawOptions) {
307
+ this.options = normalizeOptions(rawOptions);
308
+ }
309
+ async getToken() {
310
+ if (!isBrowserEnvironment()) {
311
+ throw new Error('OAuth2PkceTokenProvider requires a browser environment with sessionStorage support');
312
+ }
313
+ if (!this.cachedToken) {
314
+ const persisted = readPersistedToken(this.options.clientId);
315
+ if (persisted) {
316
+ if (this.isTokenCompatible(persisted.scopes, persisted.audience)) {
317
+ if (!persisted.expiresAt || this.isTokenFresh(persisted)) {
318
+ logger.debug('using_persisted_oauth2_pkce_token', {
319
+ authorize_url: this.options.authorizeUrl,
320
+ });
321
+ const cached = {
322
+ value: persisted.value,
323
+ };
324
+ if (typeof persisted.expiresAt === 'number') {
325
+ cached.expiresAt = persisted.expiresAt;
326
+ }
327
+ this.cachedToken = cached;
328
+ }
329
+ else {
330
+ writePersistedToken(this.options.clientId, null);
331
+ }
332
+ }
333
+ else {
334
+ writePersistedToken(this.options.clientId, null);
335
+ }
336
+ }
337
+ }
338
+ if (this.cachedToken && this.isTokenFresh(this.cachedToken)) {
339
+ logger.debug('using_cached_oauth2_pkce_token', {
340
+ authorize_url: this.options.authorizeUrl,
341
+ });
342
+ return { ...this.cachedToken };
343
+ }
344
+ const tokenFromRedirect = await this.tryCompletePendingAuthorization();
345
+ if (tokenFromRedirect) {
346
+ this.cachedToken = tokenFromRedirect;
347
+ return { ...this.cachedToken };
348
+ }
349
+ await this.beginBrowserAuthorization();
350
+ throw new OAuth2PkceRedirectInitiatedError();
351
+ }
352
+ isTokenFresh(token) {
353
+ if (typeof token.expiresAt !== 'number') {
354
+ return true;
355
+ }
356
+ const skew = this.options.clockSkewSeconds ?? DEFAULT_CLOCK_SKEW_SECONDS;
357
+ return token.expiresAt - skew * 1000 > Date.now();
358
+ }
359
+ async beginBrowserAuthorization() {
360
+ const existing = readPendingAuthorization(this.options.clientId);
361
+ if (existing) {
362
+ logger.debug('pkce_redirect_in_progress', {
363
+ authorize_url: this.options.authorizeUrl,
364
+ });
365
+ this.navigate(existing.authorizeUrl);
366
+ return;
367
+ }
368
+ const verifierLength = ensureFinitePositive(this.options.codeVerifierLength ?? DEFAULT_CODE_VERIFIER_LENGTH, 'codeVerifierLength');
369
+ const codeVerifier = base64UrlEncode(generateRandomBytes(verifierLength));
370
+ const state = base64UrlEncode(generateRandomBytes(24));
371
+ const codeChallengeMethod = this.options.codeChallengeMethod ?? 'S256';
372
+ const codeChallenge = codeChallengeMethod === 'S256'
373
+ ? await computeS256(codeVerifier)
374
+ : codeVerifier;
375
+ const authorizeUrl = await this.buildAuthorizeUrl({
376
+ state,
377
+ codeChallenge,
378
+ codeChallengeMethod,
379
+ });
380
+ const pending = {
381
+ state,
382
+ codeVerifier,
383
+ codeChallenge,
384
+ codeChallengeMethod,
385
+ authorizeUrl,
386
+ createdAt: Date.now(),
387
+ scopes: this.options.scopes,
388
+ audience: this.options.audience,
389
+ };
390
+ writePersistedToken(this.options.clientId, null);
391
+ writePendingAuthorization(this.options.clientId, pending);
392
+ logger.debug('pkce_redirect_start', {
393
+ authorize_url: this.options.authorizeUrl,
394
+ redirect_uri: this.options.redirectUri,
395
+ });
396
+ this.navigate(authorizeUrl);
397
+ }
398
+ navigate(url) {
399
+ if (!isBrowserEnvironment()) {
400
+ return;
401
+ }
402
+ try {
403
+ window.location.assign(url);
404
+ }
405
+ catch (error) {
406
+ logger.error('pkce_navigation_failed', {
407
+ authorize_url: url,
408
+ error: error instanceof Error ? error.message : String(error),
409
+ });
410
+ throw error;
411
+ }
412
+ }
413
+ async tryCompletePendingAuthorization() {
414
+ const pending = readPendingAuthorization(this.options.clientId);
415
+ if (!pending) {
416
+ return null;
417
+ }
418
+ const outcome = collectRedirectOutcome(window.location);
419
+ if (!outcome) {
420
+ return null;
421
+ }
422
+ clearOAuthParamsFromUrl(new URL(window.location.href));
423
+ writePendingAuthorization(this.options.clientId, null);
424
+ if (outcome.error) {
425
+ const suffix = outcome.errorDescription
426
+ ? ` - ${outcome.errorDescription}`
427
+ : '';
428
+ throw new Error(`OAuth2 authorization failed: ${outcome.error}${suffix}`);
429
+ }
430
+ if (!outcome.code) {
431
+ throw new Error('Authorization redirect missing code parameter');
432
+ }
433
+ if (!outcome.state || outcome.state !== pending.state) {
434
+ throw new Error('Authorization state mismatch');
435
+ }
436
+ const fetchImpl = this.resolveFetch();
437
+ const clientSecret = await this.resolveOptionalSecret(this.options.clientSecretProvider);
438
+ const token = await this.exchangeToken({
439
+ fetchImpl,
440
+ codeVerifier: pending.codeVerifier,
441
+ authorizationCode: outcome.code,
442
+ clientSecret,
443
+ scopes: pending.scopes,
444
+ audience: pending.audience,
445
+ });
446
+ this.persistToken(token, {
447
+ scopes: pending.scopes,
448
+ audience: pending.audience,
449
+ });
450
+ return token;
451
+ }
452
+ isTokenCompatible(scopes, audience) {
453
+ const expectedScopeKey = stableScopeKey(this.options.scopes);
454
+ const tokenScopeKey = stableScopeKey(scopes);
455
+ if (expectedScopeKey !== tokenScopeKey) {
456
+ return false;
457
+ }
458
+ const expectedAudience = this.options.audience ?? '';
459
+ const tokenAudience = audience ?? '';
460
+ return expectedAudience === tokenAudience;
461
+ }
462
+ persistToken(token, metadata) {
463
+ writePersistedToken(this.options.clientId, {
464
+ value: token.value,
465
+ expiresAt: token.expiresAt,
466
+ scopes: metadata.scopes ? [...metadata.scopes] : undefined,
467
+ audience: metadata.audience,
468
+ });
469
+ }
470
+ async buildAuthorizeUrl(params) {
471
+ const authorizeUrl = new URL(this.options.authorizeUrl);
472
+ authorizeUrl.searchParams.set('response_type', 'code');
473
+ authorizeUrl.searchParams.set('client_id', this.options.clientId);
474
+ authorizeUrl.searchParams.set('redirect_uri', this.options.redirectUri);
475
+ authorizeUrl.searchParams.set('state', params.state);
476
+ authorizeUrl.searchParams.set('code_challenge_method', params.codeChallengeMethod);
477
+ authorizeUrl.searchParams.set('code_challenge', params.codeChallenge);
478
+ if (this.options.scopes && this.options.scopes.length > 0) {
479
+ authorizeUrl.searchParams.set('scope', this.options.scopes.join(' '));
480
+ }
481
+ if (this.options.audience) {
482
+ authorizeUrl.searchParams.set('audience', this.options.audience);
483
+ }
484
+ const loginHint = await this.resolveOptionalSecret(this.options.usernameProvider);
485
+ if (loginHint) {
486
+ authorizeUrl.searchParams.set(this.options.loginHintParam ?? 'login_hint', loginHint);
487
+ }
488
+ return authorizeUrl.toString();
489
+ }
490
+ resolveFetch() {
491
+ if (this.options.fetchImpl) {
492
+ return this.options.fetchImpl;
493
+ }
494
+ if (typeof fetch === 'function') {
495
+ return fetch.bind(globalThis);
496
+ }
497
+ throw new Error('Global fetch implementation is not available. Provide fetchImpl in options.');
498
+ }
499
+ async resolveOptionalSecret(provider) {
500
+ if (!provider) {
501
+ return undefined;
502
+ }
503
+ const value = (0, credential_provider_js_1.credentialToString)(await provider.get());
504
+ return value ?? undefined;
505
+ }
506
+ async exchangeToken(params) {
507
+ const { fetchImpl, codeVerifier, authorizationCode, clientSecret, scopes, audience, } = params;
508
+ const form = new URLSearchParams({
509
+ grant_type: 'authorization_code',
510
+ client_id: this.options.clientId,
511
+ code: authorizationCode,
512
+ redirect_uri: this.options.redirectUri,
513
+ code_verifier: codeVerifier,
514
+ });
515
+ if (scopes && scopes.length > 0) {
516
+ form.set('scope', scopes.join(' '));
517
+ }
518
+ if (audience) {
519
+ form.set('audience', audience);
520
+ }
521
+ if (clientSecret) {
522
+ form.set('client_secret', clientSecret);
523
+ }
524
+ const response = await fetchImpl(this.options.tokenUrl, {
525
+ method: 'POST',
526
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
527
+ body: form.toString(),
528
+ });
529
+ if (!response.ok) {
530
+ const errorBody = await response.text().catch(() => '<unavailable>');
531
+ throw new Error(`OAuth2 PKCE token request failed: ${response.status} ${response.statusText} - ${errorBody}`);
532
+ }
533
+ const payload = (await response.json());
534
+ const accessToken = payload.access_token;
535
+ if (typeof accessToken !== 'string' || accessToken.length === 0) {
536
+ throw new Error('OAuth2 PKCE token response missing access_token');
537
+ }
538
+ const expiresInCandidate = typeof payload.expires_in === 'number'
539
+ ? payload.expires_in
540
+ : typeof payload.expires === 'number'
541
+ ? payload.expires
542
+ : undefined;
543
+ const expiresInSeconds = expiresInCandidate && Number.isFinite(expiresInCandidate)
544
+ ? Math.max(1, Math.floor(expiresInCandidate))
545
+ : 3600;
546
+ const token = {
547
+ value: accessToken,
548
+ expiresAt: Date.now() + expiresInSeconds * 1000,
549
+ };
550
+ logger.debug('oauth2_pkce_token_fetched', {
551
+ authorize_url: this.options.authorizeUrl,
552
+ token_url: this.options.tokenUrl,
553
+ expires_in: expiresInSeconds,
554
+ scopes,
555
+ audience,
556
+ });
557
+ return token;
558
+ }
559
+ }
560
+ exports.OAuth2PkceTokenProvider = OAuth2PkceTokenProvider;