tlc-claude-code 1.3.0 → 1.4.1

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 (105) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
  14. package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
  15. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  16. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  17. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  18. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  19. package/package.json +1 -1
  20. package/server/lib/access-control-doc.js +541 -0
  21. package/server/lib/access-control-doc.test.js +672 -0
  22. package/server/lib/adr-generator.js +423 -0
  23. package/server/lib/adr-generator.test.js +586 -0
  24. package/server/lib/agent-progress-monitor.js +223 -0
  25. package/server/lib/agent-progress-monitor.test.js +202 -0
  26. package/server/lib/audit-attribution.js +191 -0
  27. package/server/lib/audit-attribution.test.js +359 -0
  28. package/server/lib/audit-classifier.js +202 -0
  29. package/server/lib/audit-classifier.test.js +209 -0
  30. package/server/lib/audit-command.js +275 -0
  31. package/server/lib/audit-command.test.js +325 -0
  32. package/server/lib/audit-exporter.js +380 -0
  33. package/server/lib/audit-exporter.test.js +464 -0
  34. package/server/lib/audit-logger.js +236 -0
  35. package/server/lib/audit-logger.test.js +364 -0
  36. package/server/lib/audit-query.js +257 -0
  37. package/server/lib/audit-query.test.js +352 -0
  38. package/server/lib/audit-storage.js +269 -0
  39. package/server/lib/audit-storage.test.js +272 -0
  40. package/server/lib/bulk-repo-init.js +342 -0
  41. package/server/lib/bulk-repo-init.test.js +388 -0
  42. package/server/lib/compliance-checklist.js +866 -0
  43. package/server/lib/compliance-checklist.test.js +476 -0
  44. package/server/lib/compliance-command.js +616 -0
  45. package/server/lib/compliance-command.test.js +551 -0
  46. package/server/lib/compliance-reporter.js +692 -0
  47. package/server/lib/compliance-reporter.test.js +707 -0
  48. package/server/lib/data-flow-doc.js +665 -0
  49. package/server/lib/data-flow-doc.test.js +659 -0
  50. package/server/lib/ephemeral-storage.js +249 -0
  51. package/server/lib/ephemeral-storage.test.js +254 -0
  52. package/server/lib/evidence-collector.js +627 -0
  53. package/server/lib/evidence-collector.test.js +901 -0
  54. package/server/lib/flow-diagram-generator.js +474 -0
  55. package/server/lib/flow-diagram-generator.test.js +446 -0
  56. package/server/lib/idp-manager.js +626 -0
  57. package/server/lib/idp-manager.test.js +587 -0
  58. package/server/lib/memory-exclusion.js +326 -0
  59. package/server/lib/memory-exclusion.test.js +241 -0
  60. package/server/lib/mfa-handler.js +452 -0
  61. package/server/lib/mfa-handler.test.js +490 -0
  62. package/server/lib/oauth-flow.js +375 -0
  63. package/server/lib/oauth-flow.test.js +487 -0
  64. package/server/lib/oauth-registry.js +190 -0
  65. package/server/lib/oauth-registry.test.js +306 -0
  66. package/server/lib/readme-generator.js +490 -0
  67. package/server/lib/readme-generator.test.js +493 -0
  68. package/server/lib/repo-dependency-tracker.js +261 -0
  69. package/server/lib/repo-dependency-tracker.test.js +350 -0
  70. package/server/lib/retention-policy.js +281 -0
  71. package/server/lib/retention-policy.test.js +486 -0
  72. package/server/lib/role-mapper.js +236 -0
  73. package/server/lib/role-mapper.test.js +395 -0
  74. package/server/lib/saml-provider.js +765 -0
  75. package/server/lib/saml-provider.test.js +643 -0
  76. package/server/lib/security-policy-generator.js +682 -0
  77. package/server/lib/security-policy-generator.test.js +544 -0
  78. package/server/lib/sensitive-detector.js +112 -0
  79. package/server/lib/sensitive-detector.test.js +209 -0
  80. package/server/lib/service-interaction-diagram.js +700 -0
  81. package/server/lib/service-interaction-diagram.test.js +638 -0
  82. package/server/lib/service-summary.js +553 -0
  83. package/server/lib/service-summary.test.js +619 -0
  84. package/server/lib/session-purge.js +460 -0
  85. package/server/lib/session-purge.test.js +312 -0
  86. package/server/lib/sso-command.js +544 -0
  87. package/server/lib/sso-command.test.js +552 -0
  88. package/server/lib/sso-session.js +492 -0
  89. package/server/lib/sso-session.test.js +670 -0
  90. package/server/lib/workspace-command.js +249 -0
  91. package/server/lib/workspace-command.test.js +264 -0
  92. package/server/lib/workspace-config.js +270 -0
  93. package/server/lib/workspace-config.test.js +312 -0
  94. package/server/lib/workspace-docs-command.js +547 -0
  95. package/server/lib/workspace-docs-command.test.js +692 -0
  96. package/server/lib/workspace-memory.js +451 -0
  97. package/server/lib/workspace-memory.test.js +403 -0
  98. package/server/lib/workspace-scanner.js +452 -0
  99. package/server/lib/workspace-scanner.test.js +677 -0
  100. package/server/lib/workspace-test-runner.js +315 -0
  101. package/server/lib/workspace-test-runner.test.js +294 -0
  102. package/server/lib/zero-retention-command.js +439 -0
  103. package/server/lib/zero-retention-command.test.js +448 -0
  104. package/server/lib/zero-retention.js +322 -0
  105. package/server/lib/zero-retention.test.js +258 -0
@@ -0,0 +1,626 @@
1
+ /**
2
+ * Identity Provider Manager
3
+ *
4
+ * Unified interface for OAuth and SAML identity providers.
5
+ * Routes authentication requests to the appropriate provider type,
6
+ * normalizes user profiles, and manages provider metadata caching.
7
+ */
8
+
9
+ const { createOAuthRegistry } = require('./oauth-registry.js');
10
+ const { createSAMLProvider } = require('./saml-provider.js');
11
+
12
+ /**
13
+ * Provider type constants
14
+ */
15
+ const PROVIDER_TYPES = {
16
+ OAUTH: 'oauth',
17
+ SAML: 'saml',
18
+ };
19
+
20
+ /**
21
+ * Profile field mappings for different OAuth providers
22
+ */
23
+ const OAUTH_PROFILE_MAPPINGS = {
24
+ github: {
25
+ id: (data) => String(data.id),
26
+ email: (data) => data.email,
27
+ name: (data) => data.name || data.login,
28
+ firstName: (data) => (data.name ? data.name.split(' ')[0] : data.login),
29
+ lastName: (data) => (data.name ? data.name.split(' ').slice(1).join(' ') : ''),
30
+ avatarUrl: (data) => data.avatar_url,
31
+ },
32
+ google: {
33
+ id: (data) => String(data.id),
34
+ email: (data) => data.email,
35
+ name: (data) => data.name,
36
+ firstName: (data) => data.given_name || (data.name ? data.name.split(' ')[0] : ''),
37
+ lastName: (data) => data.family_name || (data.name ? data.name.split(' ').slice(1).join(' ') : ''),
38
+ avatarUrl: (data) => data.picture,
39
+ },
40
+ azuread: {
41
+ id: (data) => String(data.id),
42
+ email: (data) => data.mail || data.userPrincipalName,
43
+ name: (data) => data.displayName,
44
+ firstName: (data) => data.givenName || (data.displayName ? data.displayName.split(' ')[0] : ''),
45
+ lastName: (data) => data.surname || (data.displayName ? data.displayName.split(' ').slice(1).join(' ') : ''),
46
+ avatarUrl: () => null, // Azure AD requires separate Graph API call for photo
47
+ },
48
+ };
49
+
50
+ /**
51
+ * SAML attribute claim URIs
52
+ */
53
+ const SAML_CLAIMS = {
54
+ email: [
55
+ 'email',
56
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
57
+ 'http://schemas.xmlsoap.org/claims/EmailAddress',
58
+ 'urn:oid:0.9.2342.19200300.100.1.3',
59
+ ],
60
+ firstName: [
61
+ 'firstName',
62
+ 'givenName',
63
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
64
+ 'urn:oid:2.5.4.42',
65
+ ],
66
+ lastName: [
67
+ 'lastName',
68
+ 'surname',
69
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
70
+ 'urn:oid:2.5.4.4',
71
+ ],
72
+ name: [
73
+ 'name',
74
+ 'displayName',
75
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name',
76
+ 'urn:oid:2.16.840.1.113730.3.1.241',
77
+ ],
78
+ };
79
+
80
+ /**
81
+ * Find a value from SAML attributes using multiple possible claim names
82
+ *
83
+ * @param {Object} attrs - SAML attributes object
84
+ * @param {string[]} claimNames - Possible claim names to check
85
+ * @returns {string|null} The value or null if not found
86
+ */
87
+ function findSAMLAttribute(attrs, claimNames) {
88
+ for (const name of claimNames) {
89
+ if (attrs[name]) {
90
+ return attrs[name];
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Detect provider type from configuration
98
+ *
99
+ * @param {Object} config - Provider configuration
100
+ * @returns {string} 'oauth' or 'saml'
101
+ */
102
+ function detectProviderType(config) {
103
+ if (config.type) {
104
+ return config.type.toLowerCase();
105
+ }
106
+
107
+ // Infer from config properties
108
+ if (config.clientId || config.clientSecret) {
109
+ return PROVIDER_TYPES.OAUTH;
110
+ }
111
+
112
+ if (config.entityId || config.ssoUrl) {
113
+ return PROVIDER_TYPES.SAML;
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Creates an Identity Provider Manager instance.
121
+ *
122
+ * @param {Object} options - Manager configuration
123
+ * @param {string} options.baseUrl - Base URL for the application
124
+ * @param {string} [options.callbackPath] - Callback path for authentication
125
+ * @returns {Object} IdP Manager instance
126
+ */
127
+ function createIdPManager(options = {}) {
128
+ const { baseUrl, callbackPath = '/auth/callback' } = options;
129
+
130
+ // Create underlying provider instances
131
+ const oauthRegistry = createOAuthRegistry();
132
+ const samlProvider = createSAMLProvider({
133
+ entityId: baseUrl,
134
+ callbackUrl: `${baseUrl}${callbackPath}/saml`,
135
+ });
136
+
137
+ // Metadata cache
138
+ const metadataCache = new Map();
139
+
140
+ // Track registered provider types
141
+ const providerTypes = new Map();
142
+
143
+ /**
144
+ * Register an identity provider
145
+ *
146
+ * @param {string} name - Provider name
147
+ * @param {Object} config - Provider configuration
148
+ */
149
+ function registerProvider(name, config) {
150
+ const type = detectProviderType(config);
151
+
152
+ if (!type) {
153
+ throw new Error('Unable to detect provider type. Specify type: "oauth" or "saml"');
154
+ }
155
+
156
+ if (type !== PROVIDER_TYPES.OAUTH && type !== PROVIDER_TYPES.SAML) {
157
+ throw new Error(`Unsupported provider type: ${type}`);
158
+ }
159
+
160
+ if (type === PROVIDER_TYPES.OAUTH) {
161
+ oauthRegistry.registerProvider(name, config);
162
+ providerTypes.set(name.toLowerCase(), PROVIDER_TYPES.OAUTH);
163
+ } else {
164
+ samlProvider.registerIdP(name, config);
165
+ providerTypes.set(name.toLowerCase(), PROVIDER_TYPES.SAML);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get login URL for a provider
171
+ *
172
+ * @param {string} name - Provider name
173
+ * @param {Object} options - Login options
174
+ * @returns {string} Login URL
175
+ */
176
+ function getLoginUrl(name, options = {}) {
177
+ const normalizedName = name.toLowerCase();
178
+
179
+ // Try OAuth first
180
+ const oauthProvider = oauthRegistry.getProvider(normalizedName);
181
+ if (oauthProvider) {
182
+ const params = new URLSearchParams();
183
+ params.set('client_id', oauthProvider.clientId);
184
+ params.set('response_type', 'code');
185
+
186
+ if (options.redirectUri) {
187
+ params.set('redirect_uri', options.redirectUri);
188
+ } else {
189
+ params.set('redirect_uri', `${baseUrl}${callbackPath}/${normalizedName}`);
190
+ }
191
+
192
+ if (options.state) {
193
+ params.set('state', options.state);
194
+ }
195
+
196
+ if (oauthProvider.scopes) {
197
+ params.set('scope', oauthProvider.scopes.join(' '));
198
+ }
199
+
200
+ return `${oauthProvider.authUrl}?${params.toString()}`;
201
+ }
202
+
203
+ // Try SAML
204
+ const samlIdP = samlProvider.getIdP(normalizedName);
205
+ if (samlIdP) {
206
+ const request = samlProvider.createLoginRequest(normalizedName, {
207
+ relayState: options.relayState || options.state,
208
+ binding: options.binding || 'redirect',
209
+ });
210
+ return request.url;
211
+ }
212
+
213
+ throw new Error(`Provider not found: ${name}`);
214
+ }
215
+
216
+ /**
217
+ * Handle authentication callback
218
+ *
219
+ * @param {string} name - Provider name
220
+ * @param {Object} params - Callback parameters
221
+ * @returns {Promise<Object>} Authentication result with profile
222
+ */
223
+ async function handleCallback(name, params) {
224
+ const normalizedName = name.toLowerCase();
225
+
226
+ // Try OAuth first
227
+ const oauthProvider = oauthRegistry.getProvider(normalizedName);
228
+ if (oauthProvider) {
229
+ return handleOAuthCallback(normalizedName, oauthProvider, params);
230
+ }
231
+
232
+ // Try SAML
233
+ const samlIdP = samlProvider.getIdP(normalizedName);
234
+ if (samlIdP) {
235
+ return handleSAMLCallback(normalizedName, params);
236
+ }
237
+
238
+ return {
239
+ success: false,
240
+ error: `Provider not found: ${name}`,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Handle OAuth callback
246
+ *
247
+ * @param {string} name - Provider name
248
+ * @param {Object} provider - Provider configuration
249
+ * @param {Object} params - Callback parameters
250
+ * @returns {Promise<Object>} Authentication result
251
+ */
252
+ async function handleOAuthCallback(name, provider, params) {
253
+ const { code, state, redirectUri } = params;
254
+
255
+ try {
256
+ // Exchange code for token
257
+ const tokenResponse = await fetch(provider.tokenUrl, {
258
+ method: 'POST',
259
+ headers: {
260
+ 'Content-Type': 'application/x-www-form-urlencoded',
261
+ Accept: 'application/json',
262
+ },
263
+ body: new URLSearchParams({
264
+ client_id: provider.clientId,
265
+ client_secret: provider.clientSecret,
266
+ code,
267
+ grant_type: 'authorization_code',
268
+ redirect_uri: redirectUri || `${baseUrl}${callbackPath}/${name}`,
269
+ }),
270
+ });
271
+
272
+ if (!tokenResponse.ok) {
273
+ const error = await tokenResponse.json().catch(() => ({}));
274
+ return {
275
+ success: false,
276
+ error: error.error || 'Failed to exchange authorization code',
277
+ };
278
+ }
279
+
280
+ const tokenData = await tokenResponse.json();
281
+ const accessToken = tokenData.access_token;
282
+
283
+ // Fetch user info
284
+ const userInfo = await fetchUserInfo(name, accessToken);
285
+
286
+ // Normalize profile
287
+ const profile = normalizeProfile(name, PROVIDER_TYPES.OAUTH, userInfo);
288
+
289
+ return {
290
+ success: true,
291
+ profile,
292
+ tokens: {
293
+ accessToken,
294
+ refreshToken: tokenData.refresh_token,
295
+ expiresIn: tokenData.expires_in,
296
+ },
297
+ state,
298
+ };
299
+ } catch (error) {
300
+ return {
301
+ success: false,
302
+ error: error.message,
303
+ };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Handle SAML callback
309
+ *
310
+ * @param {string} name - Provider name
311
+ * @param {Object} params - Callback parameters
312
+ * @returns {Promise<Object>} Authentication result
313
+ */
314
+ async function handleSAMLCallback(name, params) {
315
+ const { SAMLResponse, RelayState } = params;
316
+
317
+ try {
318
+ const result = await samlProvider.handleLoginResponse(SAMLResponse, {
319
+ idpId: name,
320
+ relayState: RelayState,
321
+ });
322
+
323
+ if (!result.success) {
324
+ return {
325
+ success: false,
326
+ error: result.errors ? result.errors.join(', ') : 'SAML authentication failed',
327
+ };
328
+ }
329
+
330
+ // Normalize profile
331
+ const profile = normalizeProfile(name, PROVIDER_TYPES.SAML, result.user);
332
+
333
+ return {
334
+ success: true,
335
+ profile,
336
+ relayState: RelayState,
337
+ };
338
+ } catch (error) {
339
+ return {
340
+ success: false,
341
+ error: error.message,
342
+ };
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Normalize user profile from different providers
348
+ *
349
+ * @param {string} provider - Provider name
350
+ * @param {string} providerType - 'oauth' or 'saml'
351
+ * @param {Object} rawProfile - Raw profile data
352
+ * @returns {Object} Normalized profile
353
+ */
354
+ function normalizeProfile(provider, providerType, rawProfile) {
355
+ if (providerType === PROVIDER_TYPES.OAUTH) {
356
+ return normalizeOAuthProfile(provider, rawProfile);
357
+ } else {
358
+ return normalizeSAMLProfile(provider, rawProfile);
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Normalize OAuth profile
364
+ *
365
+ * @param {string} provider - Provider name
366
+ * @param {Object} data - Raw profile data
367
+ * @returns {Object} Normalized profile
368
+ */
369
+ function normalizeOAuthProfile(provider, data) {
370
+ const mapping = OAUTH_PROFILE_MAPPINGS[provider.toLowerCase()] || {
371
+ id: (d) => String(d.id || d.sub),
372
+ email: (d) => d.email,
373
+ name: (d) => d.name || d.login || d.username,
374
+ firstName: (d) => d.given_name || (d.name ? d.name.split(' ')[0] : ''),
375
+ lastName: (d) => d.family_name || (d.name ? d.name.split(' ').slice(1).join(' ') : ''),
376
+ avatarUrl: (d) => d.picture || d.avatar_url || null,
377
+ };
378
+
379
+ return {
380
+ id: mapping.id(data),
381
+ email: mapping.email(data),
382
+ name: mapping.name(data),
383
+ firstName: mapping.firstName(data),
384
+ lastName: mapping.lastName(data),
385
+ avatarUrl: mapping.avatarUrl(data),
386
+ provider,
387
+ providerType: PROVIDER_TYPES.OAUTH,
388
+ raw: data,
389
+ };
390
+ }
391
+
392
+ /**
393
+ * Normalize SAML profile
394
+ *
395
+ * @param {string} provider - Provider name
396
+ * @param {Object} attrs - SAML attributes
397
+ * @returns {Object} Normalized profile
398
+ */
399
+ function normalizeSAMLProfile(provider, attrs) {
400
+ const email = findSAMLAttribute(attrs, SAML_CLAIMS.email) || attrs.nameId;
401
+ const firstName = findSAMLAttribute(attrs, SAML_CLAIMS.firstName);
402
+ const lastName = findSAMLAttribute(attrs, SAML_CLAIMS.lastName);
403
+ const name = findSAMLAttribute(attrs, SAML_CLAIMS.name) ||
404
+ (firstName && lastName ? `${firstName} ${lastName}` : firstName || lastName || null);
405
+
406
+ return {
407
+ id: attrs.nameId || email,
408
+ email,
409
+ name,
410
+ firstName: firstName || null,
411
+ lastName: lastName || null,
412
+ avatarUrl: null,
413
+ provider,
414
+ providerType: PROVIDER_TYPES.SAML,
415
+ raw: attrs,
416
+ };
417
+ }
418
+
419
+ /**
420
+ * Fetch user info from OAuth provider
421
+ *
422
+ * @param {string} name - Provider name
423
+ * @param {string} accessToken - Access token
424
+ * @returns {Promise<Object>} User info
425
+ */
426
+ async function fetchUserInfo(name, accessToken) {
427
+ const provider = oauthRegistry.getProvider(name);
428
+ if (!provider || !provider.userInfoUrl) {
429
+ throw new Error(`No user info URL configured for provider: ${name}`);
430
+ }
431
+
432
+ const response = await fetch(provider.userInfoUrl, {
433
+ headers: {
434
+ Authorization: `Bearer ${accessToken}`,
435
+ Accept: 'application/json',
436
+ },
437
+ });
438
+
439
+ if (!response.ok) {
440
+ throw new Error(`Failed to fetch user info: ${response.status}`);
441
+ }
442
+
443
+ const userInfo = await response.json();
444
+
445
+ // GitHub special case: email might be private
446
+ if (name.toLowerCase() === 'github' && !userInfo.email) {
447
+ const emailResponse = await fetch('https://api.github.com/user/emails', {
448
+ headers: {
449
+ Authorization: `Bearer ${accessToken}`,
450
+ Accept: 'application/json',
451
+ },
452
+ });
453
+
454
+ if (emailResponse.ok) {
455
+ const emails = await emailResponse.json();
456
+ const primaryEmail = emails.find((e) => e.primary && e.verified);
457
+ if (primaryEmail) {
458
+ userInfo.email = primaryEmail.email;
459
+ }
460
+ }
461
+ }
462
+
463
+ return userInfo;
464
+ }
465
+
466
+ /**
467
+ * Cache provider metadata
468
+ *
469
+ * @param {string} name - Provider name
470
+ * @param {Object} metadata - Metadata to cache
471
+ */
472
+ function cacheMetadata(name, metadata) {
473
+ metadataCache.set(name.toLowerCase(), {
474
+ ...metadata,
475
+ cachedAt: metadata.fetchedAt || new Date(),
476
+ });
477
+ }
478
+
479
+ /**
480
+ * Get cached metadata
481
+ *
482
+ * @param {string} name - Provider name
483
+ * @returns {Object|null} Cached metadata or null
484
+ */
485
+ function getCachedMetadata(name) {
486
+ return metadataCache.get(name.toLowerCase()) || null;
487
+ }
488
+
489
+ /**
490
+ * Check if cached metadata is expired
491
+ *
492
+ * @param {string} name - Provider name
493
+ * @param {number} maxAgeMs - Max age in milliseconds
494
+ * @returns {boolean} True if expired or not cached
495
+ */
496
+ function isMetadataExpired(name, maxAgeMs) {
497
+ const cached = metadataCache.get(name.toLowerCase());
498
+ if (!cached) {
499
+ return true;
500
+ }
501
+
502
+ const age = Date.now() - new Date(cached.fetchedAt || cached.cachedAt).getTime();
503
+ return age > maxAgeMs;
504
+ }
505
+
506
+ /**
507
+ * Clear cached metadata
508
+ *
509
+ * @param {string} name - Provider name
510
+ */
511
+ function clearMetadataCache(name) {
512
+ metadataCache.delete(name.toLowerCase());
513
+ }
514
+
515
+ /**
516
+ * Get provider by name
517
+ *
518
+ * @param {string} name - Provider name
519
+ * @returns {Object|null} Provider info or null
520
+ */
521
+ function getProvider(name) {
522
+ const normalizedName = name.toLowerCase();
523
+
524
+ // Try OAuth first
525
+ const oauthProvider = oauthRegistry.getProvider(normalizedName);
526
+ if (oauthProvider) {
527
+ return {
528
+ name: normalizedName,
529
+ type: PROVIDER_TYPES.OAUTH,
530
+ config: oauthProvider,
531
+ };
532
+ }
533
+
534
+ // Try SAML
535
+ const samlIdP = samlProvider.getIdP(normalizedName);
536
+ if (samlIdP) {
537
+ return {
538
+ name: normalizedName,
539
+ type: PROVIDER_TYPES.SAML,
540
+ config: samlIdP,
541
+ };
542
+ }
543
+
544
+ return null;
545
+ }
546
+
547
+ /**
548
+ * List all registered providers
549
+ *
550
+ * @returns {Object[]} List of providers with names and types
551
+ */
552
+ function listProviders() {
553
+ const providers = [];
554
+
555
+ // Get OAuth providers
556
+ const oauthProviders = oauthRegistry.listProviders();
557
+ for (const provider of oauthProviders) {
558
+ providers.push({
559
+ name: provider.name,
560
+ type: PROVIDER_TYPES.OAUTH,
561
+ });
562
+ }
563
+
564
+ // Get SAML providers
565
+ const samlIdPs = samlProvider.listIdPs();
566
+ for (const idpId of samlIdPs) {
567
+ providers.push({
568
+ name: idpId,
569
+ type: PROVIDER_TYPES.SAML,
570
+ });
571
+ }
572
+
573
+ return providers;
574
+ }
575
+
576
+ /**
577
+ * Remove a provider
578
+ *
579
+ * @param {string} name - Provider name
580
+ */
581
+ function removeProvider(name) {
582
+ const normalizedName = name.toLowerCase();
583
+
584
+ if (oauthRegistry.hasProvider(normalizedName)) {
585
+ oauthRegistry.removeProvider(normalizedName);
586
+ providerTypes.delete(normalizedName);
587
+ } else if (samlProvider.getIdP(normalizedName)) {
588
+ samlProvider.removeIdP(normalizedName);
589
+ providerTypes.delete(normalizedName);
590
+ }
591
+
592
+ // Also clear metadata cache
593
+ clearMetadataCache(normalizedName);
594
+ }
595
+
596
+ return {
597
+ // Provider management
598
+ registerProvider,
599
+ getProvider,
600
+ listProviders,
601
+ removeProvider,
602
+
603
+ // Authentication
604
+ getLoginUrl,
605
+ handleCallback,
606
+ fetchUserInfo,
607
+
608
+ // Profile normalization
609
+ normalizeProfile,
610
+
611
+ // Metadata caching
612
+ cacheMetadata,
613
+ getCachedMetadata,
614
+ isMetadataExpired,
615
+ clearMetadataCache,
616
+
617
+ // Expose underlying providers for advanced use
618
+ oauthRegistry,
619
+ samlProvider,
620
+ };
621
+ }
622
+
623
+ module.exports = {
624
+ createIdPManager,
625
+ PROVIDER_TYPES,
626
+ };