@enterprisestandard/react 0.0.5-beta.20260115.2 → 0.0.5-beta.20260115.4

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 (77) hide show
  1. package/dist/index.d.ts +2573 -41
  2. package/dist/index.js +3732 -144
  3. package/dist/index.js.map +1 -0
  4. package/package.json +3 -1
  5. package/dist/group-store.d.ts +0 -164
  6. package/dist/group-store.d.ts.map +0 -1
  7. package/dist/group-store.js +0 -127
  8. package/dist/iam.d.ts +0 -206
  9. package/dist/iam.d.ts.map +0 -1
  10. package/dist/iam.js +0 -680
  11. package/dist/index.d.ts.map +0 -1
  12. package/dist/session-store.d.ts +0 -179
  13. package/dist/session-store.d.ts.map +0 -1
  14. package/dist/session-store.js +0 -105
  15. package/dist/sso-server.d.ts +0 -13
  16. package/dist/sso-server.d.ts.map +0 -1
  17. package/dist/sso-server.js +0 -46
  18. package/dist/sso.d.ts +0 -104
  19. package/dist/sso.d.ts.map +0 -1
  20. package/dist/sso.js +0 -820
  21. package/dist/tenant-server.d.ts +0 -8
  22. package/dist/tenant-server.d.ts.map +0 -1
  23. package/dist/tenant-server.js +0 -6
  24. package/dist/tenant.d.ts +0 -280
  25. package/dist/tenant.d.ts.map +0 -1
  26. package/dist/tenant.js +0 -324
  27. package/dist/types/base-user.d.ts +0 -27
  28. package/dist/types/base-user.d.ts.map +0 -1
  29. package/dist/types/base-user.js +0 -1
  30. package/dist/types/enterprise-user.d.ts +0 -158
  31. package/dist/types/enterprise-user.d.ts.map +0 -1
  32. package/dist/types/enterprise-user.js +0 -1
  33. package/dist/types/oidc-schema.d.ts +0 -86
  34. package/dist/types/oidc-schema.d.ts.map +0 -1
  35. package/dist/types/oidc-schema.js +0 -328
  36. package/dist/types/scim-schema.d.ts +0 -419
  37. package/dist/types/scim-schema.d.ts.map +0 -1
  38. package/dist/types/scim-schema.js +0 -519
  39. package/dist/types/standard-schema.d.ts +0 -56
  40. package/dist/types/standard-schema.d.ts.map +0 -1
  41. package/dist/types/standard-schema.js +0 -1
  42. package/dist/types/user.d.ts +0 -41
  43. package/dist/types/user.d.ts.map +0 -1
  44. package/dist/types/user.js +0 -1
  45. package/dist/types/workload-schema.d.ts +0 -106
  46. package/dist/types/workload-schema.d.ts.map +0 -1
  47. package/dist/types/workload-schema.js +0 -208
  48. package/dist/ui/sign-in-loading.d.ts +0 -5
  49. package/dist/ui/sign-in-loading.d.ts.map +0 -1
  50. package/dist/ui/sign-in-loading.js +0 -8
  51. package/dist/ui/signed-in.d.ts +0 -3
  52. package/dist/ui/signed-in.d.ts.map +0 -1
  53. package/dist/ui/signed-in.js +0 -8
  54. package/dist/ui/signed-out.d.ts +0 -3
  55. package/dist/ui/signed-out.d.ts.map +0 -1
  56. package/dist/ui/signed-out.js +0 -8
  57. package/dist/ui/sso-provider.d.ts +0 -35
  58. package/dist/ui/sso-provider.d.ts.map +0 -1
  59. package/dist/ui/sso-provider.js +0 -275
  60. package/dist/user-store.d.ts +0 -161
  61. package/dist/user-store.d.ts.map +0 -1
  62. package/dist/user-store.js +0 -114
  63. package/dist/utils.d.ts +0 -9
  64. package/dist/utils.d.ts.map +0 -1
  65. package/dist/utils.js +0 -23
  66. package/dist/vault.d.ts +0 -18
  67. package/dist/vault.d.ts.map +0 -1
  68. package/dist/vault.js +0 -22
  69. package/dist/workload-server.d.ts +0 -127
  70. package/dist/workload-server.d.ts.map +0 -1
  71. package/dist/workload-server.js +0 -167
  72. package/dist/workload-token-store.d.ts +0 -187
  73. package/dist/workload-token-store.d.ts.map +0 -1
  74. package/dist/workload-token-store.js +0 -95
  75. package/dist/workload.d.ts +0 -227
  76. package/dist/workload.d.ts.map +0 -1
  77. package/dist/workload.js +0 -691
package/dist/workload.js DELETED
@@ -1,691 +0,0 @@
1
- import { jwtAssertionClaimsSchema, workloadTokenResponseSchema } from './types/workload-schema';
2
- import { must } from './utils';
3
- import { InMemoryWorkloadTokenStore } from './workload-token-store';
4
- const jwksCache = new Map();
5
- /**
6
- * Type guard to check if config is for JWT Bearer Grant mode
7
- */
8
- function isJwtBearerConfig(config) {
9
- return 'workload_id' in config || 'private_key' in config;
10
- }
11
- /**
12
- * Type guard to check if config is for OAuth2 Client Credentials mode
13
- */
14
- function isClientCredentialsConfig(config) {
15
- return 'client_id' in config || 'client_secret' in config;
16
- }
17
- /**
18
- * Type guard to check if config is for server-only mode (token validation only)
19
- */
20
- function isServerOnlyConfig(config) {
21
- return 'jwks_uri' in config && !('workload_id' in config) && !('client_id' in config);
22
- }
23
- /**
24
- * Validates that the config has the required fields for at least one mode:
25
- * - JWT Bearer Grant (client role): workload_id + private_key
26
- * - Client Credentials (client role): client_id + client_secret
27
- * - Server-only (validation role): jwks_uri only
28
- */
29
- function validateWorkloadConfig(config) {
30
- const hasJwtBearer = 'workload_id' in config && 'private_key' in config;
31
- const hasClientCreds = 'client_id' in config && 'client_secret' in config;
32
- const hasServerOnly = 'jwks_uri' in config;
33
- if (!hasJwtBearer && !hasClientCreds && !hasServerOnly) {
34
- throw new Error('Invalid WorkloadConfig: must provide either (workload_id + private_key) for JWT Bearer Grant, ' +
35
- '(client_id + client_secret) for OAuth2 Client Credentials, or (jwks_uri) for server-only token validation');
36
- }
37
- }
38
- /**
39
- * Create a workload identity authentication instance
40
- *
41
- * @param config - Workload authentication configuration
42
- * @returns Workload authentication interface
43
- *
44
- * @example
45
- * ```typescript
46
- * import { workload } from '@enterprisestandard/react';
47
- *
48
- * const workloadAuth = workload({
49
- * token_url: 'https://auth.example.com/oauth/token',
50
- * jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
51
- * workload_id: 'spiffe://trust-domain/ns/service',
52
- * audience: 'https://auth.example.com/oauth/token',
53
- * private_key: '-----BEGIN PRIVATE KEY-----...',
54
- * algorithm: 'RS256',
55
- * });
56
- *
57
- * // Get access token
58
- * const token = await workloadAuth.getToken('api:read api:write');
59
- * ```
60
- */
61
- export function workload(config) {
62
- // Apply defaults based on config type
63
- // For server-only mode, we use a minimal config that only supports token validation
64
- let configWithDefaults;
65
- // Validate config has required fields for at least one mode, or set to undefined if missing
66
- if (!config) {
67
- configWithDefaults = undefined;
68
- }
69
- else {
70
- try {
71
- validateWorkloadConfig(config);
72
- if (isJwtBearerConfig(config)) {
73
- configWithDefaults = {
74
- ...config,
75
- token_url: must(config.token_url, "Missing 'token_url' from Workload Config"),
76
- workload_id: must(config.workload_id, "Missing 'workload_id' from Workload Config"),
77
- audience: must(config.audience, "Missing 'audience' from Workload Config"),
78
- scope: config.scope ?? '',
79
- algorithm: config.algorithm ?? 'RS256',
80
- token_lifetime: config.token_lifetime ?? 300,
81
- refresh_threshold: config.refresh_threshold ?? 60,
82
- auto_refresh: config.auto_refresh !== undefined ? config.auto_refresh : true,
83
- token_store: config.token_store ?? new InMemoryWorkloadTokenStore(),
84
- };
85
- }
86
- else if (isClientCredentialsConfig(config)) {
87
- // Client Credentials mode
88
- configWithDefaults = {
89
- ...config,
90
- token_url: must(config.token_url, "Missing 'token_url' from Workload Config"),
91
- client_id: must(config.client_id, "Missing 'client_id' from Workload Config"),
92
- client_secret: must(config.client_secret, "Missing 'client_secret' from Workload Config"),
93
- scope: config.scope ?? '',
94
- token_lifetime: config.token_lifetime ?? 300,
95
- refresh_threshold: config.refresh_threshold ?? 60,
96
- auto_refresh: config.auto_refresh !== undefined ? config.auto_refresh : true,
97
- token_store: config.token_store ?? new InMemoryWorkloadTokenStore(),
98
- };
99
- }
100
- else {
101
- // Server-only mode - only token validation, no token acquisition
102
- configWithDefaults = config;
103
- }
104
- }
105
- catch {
106
- // If validation fails or must() throws, set to undefined
107
- configWithDefaults = undefined;
108
- }
109
- }
110
- const initialized = true;
111
- function _ensureInitialized() {
112
- if (!initialized || !configWithDefaults) {
113
- throw new Error('Enterprise Standard Workload Manager not initialized');
114
- }
115
- }
116
- // ========== Utility Functions ==========
117
- function generateJTI() {
118
- const array = new Uint8Array(16);
119
- crypto.getRandomValues(array);
120
- return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
121
- }
122
- function base64UrlEncode(data) {
123
- let base64;
124
- if (typeof data === 'string') {
125
- base64 = btoa(data);
126
- }
127
- else {
128
- base64 = btoa(String.fromCharCode(...data));
129
- }
130
- return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
131
- }
132
- function base64UrlDecode(base64url) {
133
- let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
134
- // Add padding if needed (atob requires padding)
135
- while (base64.length % 4) {
136
- base64 += '=';
137
- }
138
- return atob(base64);
139
- }
140
- function getAlgorithmParams(alg) {
141
- if (alg.startsWith('RS')) {
142
- const hash = alg === 'RS256' ? 'SHA-256' : alg === 'RS384' ? 'SHA-384' : 'SHA-512';
143
- return { name: 'RSASSA-PKCS1-v1_5', hash };
144
- }
145
- else if (alg.startsWith('ES')) {
146
- const namedCurve = alg === 'ES256' ? 'P-256' : alg === 'ES384' ? 'P-384' : 'P-521';
147
- const hash = alg === 'ES256' ? 'SHA-256' : alg === 'ES384' ? 'SHA-384' : 'SHA-512';
148
- return { name: 'ECDSA', namedCurve, hash };
149
- }
150
- throw new Error(`Unsupported algorithm: ${alg}`);
151
- }
152
- async function importPrivateKey(pemKey, algorithm) {
153
- // Strip PEM header/footer and whitespace
154
- const pemContents = pemKey
155
- .replace(/-----BEGIN PRIVATE KEY-----/, '')
156
- .replace(/-----END PRIVATE KEY-----/, '')
157
- .replace(/\s/g, '');
158
- // Decode base64 to binary
159
- const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
160
- const algorithmParams = getAlgorithmParams(algorithm);
161
- return crypto.subtle.importKey('pkcs8', binaryDer, algorithmParams, false, ['sign']);
162
- }
163
- async function signJWT(data, privateKeyPEM, algorithm) {
164
- const privateKey = await importPrivateKey(privateKeyPEM, algorithm);
165
- const encoder = new TextEncoder();
166
- const dataBuffer = encoder.encode(data);
167
- const algorithmParams = getAlgorithmParams(algorithm);
168
- const signatureBuffer = await crypto.subtle.sign(algorithmParams, privateKey, dataBuffer);
169
- return base64UrlEncode(new Uint8Array(signatureBuffer));
170
- }
171
- async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000, maxDelay = 30000) {
172
- let lastError = new Error('Placeholder Error');
173
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
174
- try {
175
- return await operation();
176
- }
177
- catch (error) {
178
- lastError = error instanceof Error ? error : new Error(String(error));
179
- // Don't retry on authentication errors (4xx)
180
- if (lastError.message.includes('400') ||
181
- lastError.message.includes('401') ||
182
- lastError.message.includes('403') ||
183
- lastError.message.includes('404')) {
184
- throw lastError;
185
- }
186
- if (attempt < maxRetries) {
187
- // Exponential backoff with jitter
188
- const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
189
- const jitter = Math.random() * delay * 0.1;
190
- await new Promise((resolve) => setTimeout(resolve, delay + jitter));
191
- }
192
- }
193
- }
194
- throw lastError;
195
- }
196
- // ========== Client Methods ==========
197
- async function generateJWTAssertion(scope) {
198
- if (!configWithDefaults) {
199
- throw new Error('Enterprise Standard Workload Manager not initialized');
200
- }
201
- if (!isJwtBearerConfig(configWithDefaults)) {
202
- throw new Error('generateJWTAssertion is only available in JWT Bearer Grant mode');
203
- }
204
- const cfg = configWithDefaults;
205
- const now = Math.floor(Date.now() / 1000);
206
- // Build JWT claims
207
- const claims = {
208
- iss: cfg.workload_id,
209
- sub: cfg.workload_id,
210
- aud: cfg.audience ?? '',
211
- exp: now + cfg.token_lifetime,
212
- iat: now,
213
- jti: generateJTI(),
214
- scope: scope ?? cfg.scope,
215
- };
216
- // Build JWT header
217
- const header = {
218
- alg: cfg.algorithm,
219
- typ: 'JWT',
220
- kid: cfg.key_id,
221
- };
222
- // Encode header and payload
223
- const encodedHeader = base64UrlEncode(JSON.stringify(header));
224
- const encodedPayload = base64UrlEncode(JSON.stringify(claims));
225
- const signatureInput = `${encodedHeader}.${encodedPayload}`;
226
- // Sign the JWT (private_key is guaranteed by isJwtBearerConfig check above)
227
- // biome-ignore lint/style/noNonNullAssertion: private_key guaranteed by type guard
228
- const signature = await signJWT(signatureInput, cfg.private_key, cfg.algorithm);
229
- return `${signatureInput}.${signature}`;
230
- }
231
- async function acquireTokenJwtBearer(scope, validation) {
232
- if (!configWithDefaults) {
233
- throw new Error('Enterprise Standard Workload Manager not initialized');
234
- }
235
- const cfg = configWithDefaults;
236
- return retryWithBackoff(async () => {
237
- const tokenUrl = cfg.token_url;
238
- // Generate JWT assertion
239
- const assertion = await generateJWTAssertion(scope);
240
- // Build request body
241
- const body = new URLSearchParams();
242
- body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
243
- body.append('assertion', assertion);
244
- if (scope)
245
- body.append('scope', scope);
246
- // POST to token endpoint
247
- const response = await fetch(tokenUrl, {
248
- method: 'POST',
249
- headers: {
250
- 'Content-Type': 'application/x-www-form-urlencoded',
251
- Accept: 'application/json',
252
- },
253
- body: body.toString(),
254
- });
255
- const data = await response.json();
256
- if (!response.ok) {
257
- console.error('Token acquisition error:', data);
258
- throw new Error(`Token acquisition failed: ${data.error || response.statusText} - ${data.error_description || ''}`.trim());
259
- }
260
- // Validate response using StandardSchema
261
- const validator = validation?.tokenResponse ?? workloadTokenResponseSchema('builtin');
262
- const result = await validator['~standard'].validate(data);
263
- if ('issues' in result) {
264
- console.error('Token response validation failed:', result.issues);
265
- throw new Error(`Token response validation failed: ${result.issues?.map((i) => i.message).join('; ')}`);
266
- }
267
- // Cache token if store configured
268
- if (cfg.token_store) {
269
- const expiresAt = new Date(Date.now() + (result.value.expires_in ?? 300) * 1000);
270
- const cachedToken = {
271
- workload_id: cfg.workload_id,
272
- access_token: result.value.access_token,
273
- token_type: result.value.token_type,
274
- scope: result.value.scope,
275
- expires_at: expiresAt,
276
- created_at: new Date(),
277
- refresh_token: result.value.refresh_token,
278
- };
279
- await cfg.token_store.set(cachedToken);
280
- }
281
- return result.value;
282
- });
283
- }
284
- async function acquireTokenClientCredentials(scope, validation) {
285
- if (!configWithDefaults) {
286
- throw new Error('Enterprise Standard Workload Manager not initialized');
287
- }
288
- const cfg = configWithDefaults;
289
- return retryWithBackoff(async () => {
290
- const tokenUrl = cfg.token_url;
291
- // Build request body for OAuth2 Client Credentials
292
- const body = new URLSearchParams();
293
- body.append('grant_type', 'client_credentials');
294
- body.append('client_id', cfg.client_id);
295
- body.append('client_secret', cfg.client_secret);
296
- if (scope)
297
- body.append('scope', scope);
298
- // POST to token endpoint
299
- const response = await fetch(tokenUrl, {
300
- method: 'POST',
301
- headers: {
302
- 'Content-Type': 'application/x-www-form-urlencoded',
303
- Accept: 'application/json',
304
- },
305
- body: body.toString(),
306
- });
307
- const data = await response.json();
308
- if (!response.ok) {
309
- console.error('Token acquisition error:', data);
310
- throw new Error(`Token acquisition failed: ${data.error || response.statusText} - ${data.error_description || ''}`.trim());
311
- }
312
- // Validate response using StandardSchema
313
- const validator = validation?.tokenResponse ?? workloadTokenResponseSchema('builtin');
314
- const result = await validator['~standard'].validate(data);
315
- if ('issues' in result) {
316
- console.error('Token response validation failed:', result.issues);
317
- throw new Error(`Token response validation failed: ${result.issues?.map((i) => i.message).join('; ')}`);
318
- }
319
- // Cache token if store configured
320
- if (cfg.token_store) {
321
- const expiresAt = new Date(Date.now() + (result.value.expires_in ?? 300) * 1000);
322
- const cachedToken = {
323
- workload_id: cfg.client_id,
324
- access_token: result.value.access_token,
325
- token_type: result.value.token_type,
326
- scope: result.value.scope,
327
- expires_at: expiresAt,
328
- created_at: new Date(),
329
- refresh_token: result.value.refresh_token,
330
- };
331
- await cfg.token_store.set(cachedToken);
332
- }
333
- return result.value;
334
- });
335
- }
336
- async function getToken(scope) {
337
- if (!configWithDefaults) {
338
- throw new Error('Enterprise Standard Workload Manager not initialized');
339
- }
340
- // Check if in server-only mode (no client credentials configured)
341
- if (isServerOnlyConfig(configWithDefaults)) {
342
- throw new Error('Cannot acquire tokens: Workload is configured in server-only mode (validation only). ' +
343
- 'To acquire tokens, configure client_id + client_secret for OAuth2 Client Credentials, ' +
344
- 'or workload_id + private_key for JWT Bearer Grant.');
345
- }
346
- // Validate client role requirements
347
- if (!configWithDefaults.token_url) {
348
- throw new Error('Cannot acquire tokens: Missing token_url in WorkloadConfig. ' +
349
- 'Client role requires token_url to be configured in vault.');
350
- }
351
- if (isJwtBearerConfig(configWithDefaults)) {
352
- if (!configWithDefaults.private_key) {
353
- throw new Error('Cannot acquire tokens: Missing private_key in WorkloadConfig. ' +
354
- 'JWT Bearer Grant client role requires private_key to be configured in vault.');
355
- }
356
- }
357
- else if (isClientCredentialsConfig(configWithDefaults)) {
358
- if (!configWithDefaults.client_secret) {
359
- throw new Error('Cannot acquire tokens: Missing client_secret in WorkloadConfig. ' +
360
- 'OAuth2 Client Credentials client role requires client_secret to be configured in vault.');
361
- }
362
- }
363
- const requestedScope = scope ?? configWithDefaults.scope ?? '';
364
- // Determine cache key based on config type
365
- let cacheKey;
366
- if (isJwtBearerConfig(configWithDefaults)) {
367
- cacheKey = configWithDefaults.workload_id;
368
- }
369
- else if (isClientCredentialsConfig(configWithDefaults)) {
370
- cacheKey = configWithDefaults.client_id;
371
- }
372
- else {
373
- throw new Error('Invalid WorkloadConfig');
374
- }
375
- // Check cache if token store is configured
376
- const cfg = configWithDefaults;
377
- if (cfg.token_store) {
378
- const cachedToken = await cfg.token_store.get(cacheKey);
379
- if (cachedToken) {
380
- const now = Date.now();
381
- const expiresAt = cachedToken.expires_at.getTime();
382
- const refreshThreshold = cfg.refresh_threshold * 1000;
383
- // If token is not approaching expiry, return it
384
- if (now + refreshThreshold < expiresAt) {
385
- return cachedToken.access_token;
386
- }
387
- // If auto-refresh is enabled and approaching expiry, refresh proactively
388
- if (cfg.auto_refresh) {
389
- try {
390
- const newToken = isJwtBearerConfig(configWithDefaults)
391
- ? await acquireTokenJwtBearer(requestedScope)
392
- : await acquireTokenClientCredentials(requestedScope);
393
- return newToken.access_token;
394
- }
395
- catch (error) {
396
- // If refresh fails but token still valid, use cached token
397
- if (now < expiresAt) {
398
- console.warn('Token refresh failed, using cached token:', error);
399
- return cachedToken.access_token;
400
- }
401
- throw error;
402
- }
403
- }
404
- }
405
- }
406
- // No cache or expired - acquire new token based on mode
407
- const tokenResponse = isJwtBearerConfig(configWithDefaults)
408
- ? await acquireTokenJwtBearer(requestedScope)
409
- : await acquireTokenClientCredentials(requestedScope);
410
- return tokenResponse.access_token;
411
- }
412
- async function refreshToken() {
413
- if (!configWithDefaults) {
414
- throw new Error('Enterprise Standard Workload Manager not initialized');
415
- }
416
- if (isServerOnlyConfig(configWithDefaults)) {
417
- throw new Error('Cannot refresh tokens: Workload is configured in server-only mode (validation only).');
418
- }
419
- const cfg = configWithDefaults;
420
- return isJwtBearerConfig(cfg)
421
- ? await acquireTokenJwtBearer(cfg.scope)
422
- : await acquireTokenClientCredentials(cfg.scope);
423
- }
424
- async function revokeToken(token) {
425
- if (!configWithDefaults) {
426
- throw new Error('Enterprise Standard Workload Manager not initialized');
427
- }
428
- try {
429
- // Only attempt revocation if endpoint is configured
430
- if (!configWithDefaults.revocation_endpoint) {
431
- return;
432
- }
433
- const body = new URLSearchParams();
434
- body.append('token', token);
435
- body.append('token_type_hint', 'access_token');
436
- // Use appropriate client identifier based on mode
437
- if (isJwtBearerConfig(configWithDefaults)) {
438
- const cfg = configWithDefaults;
439
- body.append('client_id', cfg.workload_id);
440
- }
441
- else if (isClientCredentialsConfig(configWithDefaults)) {
442
- const cfg = configWithDefaults;
443
- body.append('client_id', cfg.client_id);
444
- body.append('client_secret', cfg.client_secret);
445
- }
446
- const response = await fetch(configWithDefaults.revocation_endpoint, {
447
- method: 'POST',
448
- headers: {
449
- 'Content-Type': 'application/x-www-form-urlencoded',
450
- },
451
- body: body.toString(),
452
- });
453
- if (!response.ok) {
454
- console.warn('Token revocation failed:', response.status, response.statusText);
455
- }
456
- else {
457
- console.log('Token revoked successfully');
458
- }
459
- // Also delete from cache if using token store
460
- if (configWithDefaults.token_store) {
461
- let cacheKey;
462
- if (isJwtBearerConfig(configWithDefaults)) {
463
- cacheKey = configWithDefaults.workload_id;
464
- }
465
- else if (isClientCredentialsConfig(configWithDefaults)) {
466
- cacheKey = configWithDefaults.client_id;
467
- }
468
- else {
469
- return; // Server-only mode, no cache
470
- }
471
- await configWithDefaults.token_store.delete(cacheKey);
472
- }
473
- }
474
- catch (error) {
475
- console.warn('Error revoking token:', error);
476
- // Don't throw - revocation is best-effort
477
- }
478
- }
479
- // ========== Server Methods ==========
480
- async function fetchJwks() {
481
- if (!configWithDefaults) {
482
- throw new Error('Enterprise Standard Workload Manager not initialized');
483
- }
484
- // configWithDefaults is the merged config from enterpriseStandard, which should include jwks_uri from vault
485
- const url = configWithDefaults.jwks_uri;
486
- if (!url) {
487
- throw new Error('Cannot validate tokens: Missing jwks_uri in WorkloadConfig. ' +
488
- 'Server role requires jwks_uri to be configured in vault to fetch public keys for token validation.');
489
- }
490
- const cached = jwksCache.get(url);
491
- if (cached)
492
- return cached;
493
- return retryWithBackoff(async () => {
494
- const response = await fetch(url);
495
- if (!response.ok)
496
- throw new Error('Failed to fetch JWKS');
497
- const jwks = await response.json();
498
- jwksCache.set(url, jwks);
499
- return jwks;
500
- });
501
- }
502
- async function getPublicKey(kid) {
503
- if (!configWithDefaults) {
504
- throw new Error('Enterprise Standard Workload Manager not initialized');
505
- }
506
- const jwks = await fetchJwks();
507
- const key = jwks.keys.find((k) => k.kid === kid);
508
- if (!key)
509
- throw new Error('Public key not found');
510
- // Use algorithm from JWK or default to RS256
511
- const defaultAlg = isJwtBearerConfig(configWithDefaults)
512
- ? configWithDefaults.algorithm
513
- : 'RS256';
514
- const algorithmParams = getAlgorithmParams(key.alg || defaultAlg);
515
- return crypto.subtle.importKey('jwk', key, algorithmParams, false, ['verify']);
516
- }
517
- async function parseJWT(token, validation) {
518
- if (!configWithDefaults) {
519
- throw new Error('Enterprise Standard Workload Manager not initialized');
520
- }
521
- try {
522
- const parts = token.split('.');
523
- if (parts.length !== 3)
524
- throw new Error('Invalid JWT');
525
- // Decode header and payload
526
- const header = JSON.parse(base64UrlDecode(parts[0]));
527
- const payload = JSON.parse(base64UrlDecode(parts[1]));
528
- // Get public key
529
- const publicKey = await getPublicKey(header.kid);
530
- // Verify signature
531
- const signature = parts[2];
532
- const encoder = new TextEncoder();
533
- const data = encoder.encode(`${parts[0]}.${parts[1]}`);
534
- const signatureBytes = Uint8Array.from(base64UrlDecode(signature), (c) => c.charCodeAt(0));
535
- const algorithmParams = getAlgorithmParams(header.alg);
536
- const isValid = await crypto.subtle.verify(algorithmParams, publicKey, signatureBytes, data);
537
- if (!isValid)
538
- throw new Error('Invalid JWT signature');
539
- // Validate claims using StandardSchema
540
- const validator = validation?.jwtAssertionClaims ?? jwtAssertionClaimsSchema('builtin');
541
- const result = await validator['~standard'].validate(payload);
542
- if ('issues' in result) {
543
- console.error('JWT claims validation failed:', result.issues);
544
- throw new Error(`JWT claims validation failed: ${result.issues?.map((i) => i.message).join('; ')}`);
545
- }
546
- return result.value;
547
- }
548
- catch (error) {
549
- console.error('Error verifying JWT:', error);
550
- throw error;
551
- }
552
- }
553
- async function validateToken(token, validation) {
554
- if (!configWithDefaults) {
555
- throw new Error('Enterprise Standard Workload Manager not initialized');
556
- }
557
- try {
558
- const claims = await parseJWT(token, validation);
559
- // Check expiration
560
- const now = Math.floor(Date.now() / 1000);
561
- if (claims.exp && claims.exp < now) {
562
- return { valid: false, error: 'Token expired' };
563
- }
564
- // Validate based on mode using type guards
565
- if (isJwtBearerConfig(configWithDefaults)) {
566
- // JWT Bearer Grant validation
567
- if (configWithDefaults.audience && claims.aud !== configWithDefaults.audience) {
568
- return { valid: false, error: 'Invalid audience' };
569
- }
570
- }
571
- else if (isClientCredentialsConfig(configWithDefaults)) {
572
- // OAuth2 Client Credentials validation
573
- if (configWithDefaults.issuer && claims.iss !== configWithDefaults.issuer) {
574
- return { valid: false, error: 'Invalid issuer' };
575
- }
576
- if (configWithDefaults.audience && claims.aud !== configWithDefaults.audience) {
577
- return { valid: false, error: 'Invalid audience' };
578
- }
579
- }
580
- else {
581
- // Server-only mode - validate issuer if configured
582
- const serverConfig = configWithDefaults;
583
- if (serverConfig.issuer && claims.iss !== serverConfig.issuer) {
584
- return { valid: false, error: 'Invalid issuer' };
585
- }
586
- }
587
- return {
588
- valid: true,
589
- claims,
590
- expiresAt: claims.exp ? new Date(claims.exp * 1000) : undefined,
591
- };
592
- }
593
- catch (error) {
594
- return {
595
- valid: false,
596
- error: error instanceof Error ? error.message : String(error),
597
- };
598
- }
599
- }
600
- async function getWorkload(request) {
601
- if (!configWithDefaults) {
602
- throw new Error('Enterprise Standard Workload Manager not initialized');
603
- }
604
- // Validate server role requirements
605
- if (!configWithDefaults.jwks_uri) {
606
- throw new Error('Cannot validate tokens: Missing jwks_uri in WorkloadConfig. ' +
607
- 'Server role requires jwks_uri to be configured in vault to fetch public keys for token validation.');
608
- }
609
- // Extract Bearer token from Authorization header
610
- const authHeader = request.headers.get('Authorization');
611
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
612
- return undefined;
613
- }
614
- const token = authHeader.substring(7);
615
- const result = await validateToken(token, configWithDefaults.validation);
616
- if (!result.valid || !result.claims) {
617
- return undefined;
618
- }
619
- return {
620
- workload_id: result.claims.sub, // Subject is the workload identity
621
- client_id: typeof result.claims.client_id === 'string' ? result.claims.client_id : undefined, // May be present in OAuth2 tokens
622
- scope: result.claims.scope,
623
- claims: result.claims,
624
- };
625
- }
626
- // ========== Framework-Agnostic Handler ==========
627
- async function handler(request) {
628
- if (!configWithDefaults) {
629
- throw new Error('Enterprise Standard Workload Manager not initialized');
630
- }
631
- // Read handler URLs and validation directly from config (which is now part of the instance)
632
- const tokenUrl = configWithDefaults.tokenUrl;
633
- const validateUrl = configWithDefaults.validateUrl;
634
- const jwksUrl = configWithDefaults.jwksUrl;
635
- const refreshUrl = configWithDefaults.refreshUrl;
636
- const validation = configWithDefaults.validation;
637
- const path = new URL(request.url).pathname;
638
- // GET /api/workload/token?scope=api:read
639
- if (tokenUrl === path && request.method === 'GET') {
640
- const url = new URL(request.url);
641
- const scope = url.searchParams.get('scope') || undefined;
642
- const token = await getToken(scope);
643
- return new Response(JSON.stringify({ access_token: token, token_type: 'Bearer' }), {
644
- headers: [['Content-Type', 'application/json']],
645
- });
646
- }
647
- // POST /api/workload/validate with Authorization: Bearer <token>
648
- if (validateUrl === path && request.method === 'POST') {
649
- const authHeader = request.headers.get('Authorization');
650
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
651
- return new Response(JSON.stringify({ valid: false, error: 'Missing Authorization header' }), {
652
- status: 401,
653
- headers: [['Content-Type', 'application/json']],
654
- });
655
- }
656
- const token = authHeader.substring(7);
657
- const result = await validateToken(token, validation);
658
- return new Response(JSON.stringify(result), {
659
- status: result.valid ? 200 : 401,
660
- headers: [['Content-Type', 'application/json']],
661
- });
662
- }
663
- // GET /api/workload/jwks
664
- if (jwksUrl === path && request.method === 'GET') {
665
- const jwks = await fetchJwks();
666
- return new Response(JSON.stringify(jwks), {
667
- headers: [['Content-Type', 'application/json']],
668
- });
669
- }
670
- // POST /api/workload/refresh
671
- if (refreshUrl === path && request.method === 'POST') {
672
- const tokenResponse = await refreshToken();
673
- return new Response(JSON.stringify(tokenResponse), {
674
- headers: [['Content-Type', 'application/json']],
675
- });
676
- }
677
- return new Response('Not Found', { status: 404 });
678
- }
679
- // ========== Return Interface ==========
680
- return {
681
- ...(configWithDefaults ?? {}),
682
- getToken,
683
- refreshToken,
684
- generateJWTAssertion,
685
- revokeToken,
686
- validateToken,
687
- getWorkload,
688
- parseJWT,
689
- handler,
690
- };
691
- }