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.
- package/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- 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
|
+
};
|