@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.
- package/dist/browser/index.cjs +1847 -1054
- package/dist/browser/index.mjs +1842 -1049
- package/dist/cjs/naylence/fame/factory-manifest.js +2 -0
- package/dist/cjs/naylence/fame/http/oauth2-token-router.js +751 -88
- package/dist/cjs/naylence/fame/node/admission/admission-profile-factory.js +61 -0
- package/dist/cjs/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.js +171 -0
- package/dist/cjs/naylence/fame/security/auth/oauth2-pkce-token-provider.js +560 -0
- package/dist/cjs/naylence/fame/telemetry/open-telemetry-trace-emitter-factory.js +19 -2
- package/dist/cjs/naylence/fame/telemetry/open-telemetry-trace-emitter.js +19 -9
- package/dist/cjs/naylence/fame/util/register-runtime-factories.js +6 -0
- package/dist/cjs/version.js +2 -2
- package/dist/esm/naylence/fame/factory-manifest.js +2 -0
- package/dist/esm/naylence/fame/http/oauth2-token-router.js +751 -88
- package/dist/esm/naylence/fame/node/admission/admission-profile-factory.js +61 -0
- package/dist/esm/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.js +134 -0
- package/dist/esm/naylence/fame/security/auth/oauth2-pkce-token-provider.js +555 -0
- package/dist/esm/naylence/fame/telemetry/open-telemetry-trace-emitter-factory.js +19 -2
- package/dist/esm/naylence/fame/telemetry/open-telemetry-trace-emitter.js +19 -9
- package/dist/esm/naylence/fame/util/register-runtime-factories.js +6 -0
- package/dist/esm/version.js +2 -2
- package/dist/node/index.cjs +1843 -1050
- package/dist/node/index.mjs +1842 -1049
- package/dist/node/node.cjs +2658 -1202
- package/dist/node/node.mjs +2657 -1201
- package/dist/types/naylence/fame/factory-manifest.d.ts +1 -1
- package/dist/types/naylence/fame/http/oauth2-token-router.d.ts +73 -17
- package/dist/types/naylence/fame/security/auth/oauth2-pkce-token-provider-factory.d.ts +27 -0
- package/dist/types/naylence/fame/security/auth/oauth2-pkce-token-provider.d.ts +42 -0
- package/dist/types/naylence/fame/telemetry/open-telemetry-trace-emitter.d.ts +4 -0
- package/dist/types/version.d.ts +1 -1
- 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:
|
|
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
|
|
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:
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
};
|