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

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 (31) hide show
  1. package/dist/browser/index.cjs +1847 -1054
  2. package/dist/browser/index.mjs +1842 -1049
  3. package/dist/cjs/naylence/fame/factory-manifest.js +2 -0
  4. package/dist/cjs/naylence/fame/http/oauth2-token-router.js +751 -88
  5. package/dist/cjs/naylence/fame/node/admission/admission-profile-factory.js +61 -0
  6. package/dist/cjs/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.js +171 -0
  7. package/dist/cjs/naylence/fame/security/auth/oauth2-pkce-token-provider.js +560 -0
  8. package/dist/cjs/naylence/fame/telemetry/open-telemetry-trace-emitter-factory.js +19 -2
  9. package/dist/cjs/naylence/fame/telemetry/open-telemetry-trace-emitter.js +19 -9
  10. package/dist/cjs/naylence/fame/util/register-runtime-factories.js +6 -0
  11. package/dist/cjs/version.js +2 -2
  12. package/dist/esm/naylence/fame/factory-manifest.js +2 -0
  13. package/dist/esm/naylence/fame/http/oauth2-token-router.js +751 -88
  14. package/dist/esm/naylence/fame/node/admission/admission-profile-factory.js +61 -0
  15. package/dist/esm/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.js +134 -0
  16. package/dist/esm/naylence/fame/security/auth/oauth2-pkce-token-provider.js +555 -0
  17. package/dist/esm/naylence/fame/telemetry/open-telemetry-trace-emitter-factory.js +19 -2
  18. package/dist/esm/naylence/fame/telemetry/open-telemetry-trace-emitter.js +19 -9
  19. package/dist/esm/naylence/fame/util/register-runtime-factories.js +6 -0
  20. package/dist/esm/version.js +2 -2
  21. package/dist/node/index.cjs +1843 -1050
  22. package/dist/node/index.mjs +1842 -1049
  23. package/dist/node/node.cjs +2658 -1202
  24. package/dist/node/node.mjs +2657 -1201
  25. package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
  26. package/dist/types/naylence/fame/http/oauth2-token-router.d.ts +73 -17
  27. package/dist/types/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.d.ts +27 -0
  28. package/dist/types/naylence/fame/security/auth/oauth2-pkce-token-provider.d.ts +42 -0
  29. package/dist/types/naylence/fame/telemetry/open-telemetry-trace-emitter.d.ts +4 -0
  30. package/dist/types/version.d.ts +1 -1
  31. package/package.json +3 -1
@@ -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;
@@ -40,15 +40,25 @@ const lazy_import_js_1 = require("../util/lazy-import.js");
40
40
  const trace_emitter_factory_js_1 = require("./trace-emitter-factory.js");
41
41
  const logging_js_1 = require("../util/logging.js");
42
42
  let openTelemetryTraceEmitterModulePromise = null;
43
+ let otelApiModulePromise = null;
43
44
  const logger = (0, logging_js_1.getLogger)('naylence.fame.telemetry.open_telemetry_trace_emitter_factory');
45
+ const MISSING_OTEL_HELP_MESSAGE = 'Missing optional OpenTelemetry dependency. Install @opentelemetry/api (and related packages) to enable trace emission.';
44
46
  function getOpenTelemetryTraceEmitterModule() {
45
47
  if (!openTelemetryTraceEmitterModulePromise) {
46
48
  openTelemetryTraceEmitterModulePromise = (0, lazy_import_js_1.safeImport)(() => Promise.resolve().then(() => __importStar(require('./open-telemetry-trace-emitter.js'))), '@opentelemetry/api', {
47
- helpMessage: 'Missing optional OpenTelemetry dependency. Install @opentelemetry/api (and related packages) to enable trace emission.',
49
+ helpMessage: MISSING_OTEL_HELP_MESSAGE,
48
50
  });
49
51
  }
50
52
  return openTelemetryTraceEmitterModulePromise;
51
53
  }
54
+ function getOtelApiModule() {
55
+ if (!otelApiModulePromise) {
56
+ otelApiModulePromise = (0, lazy_import_js_1.safeImport)(() => Promise.resolve().then(() => __importStar(require('@opentelemetry/api'))), '@opentelemetry/api', {
57
+ helpMessage: MISSING_OTEL_HELP_MESSAGE,
58
+ });
59
+ }
60
+ return otelApiModulePromise;
61
+ }
52
62
  exports.FACTORY_META = {
53
63
  base: trace_emitter_factory_js_1.TRACE_EMITTER_FACTORY_BASE_TYPE,
54
64
  key: 'OpenTelemetryTraceEmitter',
@@ -111,9 +121,16 @@ class OpenTelemetryTraceEmitterFactory extends trace_emitter_factory_js_1.TraceE
111
121
  }
112
122
  throw error;
113
123
  }
114
- const { OpenTelemetryTraceEmitter } = await getOpenTelemetryTraceEmitterModule();
124
+ const [{ OpenTelemetryTraceEmitter }, otelModule] = await Promise.all([
125
+ getOpenTelemetryTraceEmitterModule(),
126
+ getOtelApiModule(),
127
+ ]);
115
128
  const emitterOptions = {
116
129
  serviceName: normalized.serviceName,
130
+ otelApi: {
131
+ trace: otelModule.trace,
132
+ SpanStatusCode: otelModule.SpanStatusCode,
133
+ },
117
134
  };
118
135
  if (options.tracer) {
119
136
  emitterOptions.tracer = options.tracer;
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OpenTelemetryTraceEmitter = void 0;
4
- const api_1 = require("@opentelemetry/api");
5
4
  const base_trace_emitter_js_1 = require("./base-trace-emitter.js");
6
5
  const otel_context_js_1 = require("./otel-context.js");
7
6
  class OpenTelemetryTraceSpan {
8
- constructor(span) {
7
+ constructor(span, api) {
9
8
  this.span = span;
9
+ this.api = api;
10
10
  }
11
11
  setAttribute(key, value) {
12
12
  try {
@@ -32,7 +32,7 @@ class OpenTelemetryTraceSpan {
32
32
  setStatusError(description) {
33
33
  try {
34
34
  const status = {
35
- code: api_1.SpanStatusCode.ERROR,
35
+ code: this.api.SpanStatusCode.ERROR,
36
36
  };
37
37
  if (description !== undefined) {
38
38
  status.message = description;
@@ -45,10 +45,10 @@ class OpenTelemetryTraceSpan {
45
45
  }
46
46
  }
47
47
  class OpenTelemetrySpanScope {
48
- constructor(span) {
48
+ constructor(span, api) {
49
49
  this.span = span;
50
50
  this.entered = false;
51
- this.wrapper = new OpenTelemetryTraceSpan(span);
51
+ this.wrapper = new OpenTelemetryTraceSpan(span, api);
52
52
  }
53
53
  enter() {
54
54
  if (!this.entered) {
@@ -83,7 +83,9 @@ class OpenTelemetryTraceEmitter extends base_trace_emitter_js_1.BaseTraceEmitter
83
83
  super();
84
84
  this.shutdownInvoked = false;
85
85
  const normalized = normalizeOpenTelemetryTraceEmitterOptions(options);
86
- this.tracer = normalized.tracer ?? api_1.trace.getTracer(normalized.serviceName);
86
+ this.otelApi = normalized.otelApi;
87
+ this.tracer =
88
+ normalized.tracer ?? this.otelApi.trace.getTracer(normalized.serviceName);
87
89
  this.lifecycle = normalized.lifecycle ?? null;
88
90
  this.authStrategy = normalized.authStrategy ?? null;
89
91
  }
@@ -101,7 +103,7 @@ class OpenTelemetryTraceEmitter extends base_trace_emitter_js_1.BaseTraceEmitter
101
103
  if (typeof envelopeTraceId === 'string') {
102
104
  this.applyEnvelopeTraceId(span, envelopeTraceId);
103
105
  }
104
- return new OpenTelemetrySpanScope(span);
106
+ return new OpenTelemetrySpanScope(span, this.otelApi);
105
107
  }
106
108
  async flush() {
107
109
  if (this.lifecycle?.forceFlush) {
@@ -114,7 +116,7 @@ class OpenTelemetryTraceEmitter extends base_trace_emitter_js_1.BaseTraceEmitter
114
116
  }
115
117
  }
116
118
  try {
117
- const provider = api_1.trace.getTracerProvider();
119
+ const provider = this.otelApi.trace.getTracerProvider();
118
120
  if (provider && typeof provider.forceFlush === 'function') {
119
121
  await provider.forceFlush();
120
122
  }
@@ -149,7 +151,7 @@ class OpenTelemetryTraceEmitter extends base_trace_emitter_js_1.BaseTraceEmitter
149
151
  }
150
152
  }
151
153
  try {
152
- const provider = api_1.trace.getTracerProvider();
154
+ const provider = this.otelApi.trace.getTracerProvider();
153
155
  if (provider && typeof provider.shutdown === 'function') {
154
156
  await provider.shutdown();
155
157
  }
@@ -191,6 +193,13 @@ function normalizeOpenTelemetryTraceEmitterOptions(input) {
191
193
  const source = (input ?? {});
192
194
  const serviceName = extractNonEmptyString(pickFirst(source, ['serviceName', 'service_name'])) ?? 'naylence-service';
193
195
  const tracer = pickFirst(source, ['tracer']);
196
+ const otelApi = pickFirst(source, [
197
+ 'otelApi',
198
+ 'otel_api',
199
+ ]);
200
+ if (!otelApi) {
201
+ throw new Error('OpenTelemetryTraceEmitter requires OpenTelemetry API bindings. Provide otelApi via options.');
202
+ }
194
203
  const lifecycle = pickFirst(source, [
195
204
  'lifecycle',
196
205
  'lifeCycle',
@@ -203,6 +212,7 @@ function normalizeOpenTelemetryTraceEmitterOptions(input) {
203
212
  return {
204
213
  serviceName,
205
214
  tracer,
215
+ otelApi,
206
216
  lifecycle,
207
217
  authStrategy,
208
218
  };