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