@enactprotocol/api 2.0.0
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/package.json +34 -0
- package/src/attestations.ts +461 -0
- package/src/auth.ts +293 -0
- package/src/client.ts +349 -0
- package/src/download.ts +298 -0
- package/src/index.ts +109 -0
- package/src/publish.ts +316 -0
- package/src/search.ts +147 -0
- package/src/trust.ts +203 -0
- package/src/types.ts +468 -0
- package/src/utils.ts +86 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript types for Enact Registry API v2
|
|
3
|
+
* Based on docs/API.md and docs/REGISTRY-SPEC.md specification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* API error response
|
|
8
|
+
*/
|
|
9
|
+
export interface ApiError {
|
|
10
|
+
error: {
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
details?: Record<string, unknown> | undefined;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* OAuth provider types
|
|
19
|
+
*/
|
|
20
|
+
export type OAuthProvider = "github" | "google" | "microsoft";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Author/user information from API
|
|
24
|
+
*/
|
|
25
|
+
export interface ApiAuthor {
|
|
26
|
+
username: string;
|
|
27
|
+
avatar_url?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Version metadata object (v2)
|
|
32
|
+
*/
|
|
33
|
+
export interface VersionMetadata {
|
|
34
|
+
/** Version string (e.g., "1.2.0") */
|
|
35
|
+
version: string;
|
|
36
|
+
/** Publication timestamp */
|
|
37
|
+
published_at: string;
|
|
38
|
+
/** Download count for this version */
|
|
39
|
+
downloads: number;
|
|
40
|
+
/** SHA-256 hash of bundle */
|
|
41
|
+
bundle_hash: string;
|
|
42
|
+
/** Bundle size in bytes */
|
|
43
|
+
bundle_size?: number | undefined;
|
|
44
|
+
/** Whether this version is yanked */
|
|
45
|
+
yanked: boolean;
|
|
46
|
+
/** Attestation summary (optional) */
|
|
47
|
+
attestation_summary?:
|
|
48
|
+
| {
|
|
49
|
+
auditor_count: number;
|
|
50
|
+
}
|
|
51
|
+
| undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Tool search result item
|
|
56
|
+
*/
|
|
57
|
+
export interface ToolSearchResult {
|
|
58
|
+
/** Tool name (e.g., "alice/utils/greeter") */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Tool description */
|
|
61
|
+
description: string;
|
|
62
|
+
/** Tool tags */
|
|
63
|
+
tags: string[];
|
|
64
|
+
/** Latest published version */
|
|
65
|
+
version: string;
|
|
66
|
+
/** Tool author */
|
|
67
|
+
author: ApiAuthor;
|
|
68
|
+
/** Total downloads */
|
|
69
|
+
downloads: number;
|
|
70
|
+
/** Trust status */
|
|
71
|
+
trust_status?:
|
|
72
|
+
| {
|
|
73
|
+
auditor_count: number;
|
|
74
|
+
}
|
|
75
|
+
| undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tool metadata from GET /tools/{name}
|
|
80
|
+
*/
|
|
81
|
+
export interface ToolMetadata {
|
|
82
|
+
/** Tool name */
|
|
83
|
+
name: string;
|
|
84
|
+
/** Tool description */
|
|
85
|
+
description: string;
|
|
86
|
+
/** Tool tags */
|
|
87
|
+
tags: string[];
|
|
88
|
+
/** SPDX license identifier */
|
|
89
|
+
license: string;
|
|
90
|
+
/** Tool author */
|
|
91
|
+
author: ApiAuthor;
|
|
92
|
+
/** Repository URL */
|
|
93
|
+
repository?: string | undefined;
|
|
94
|
+
/** Homepage URL */
|
|
95
|
+
homepage?: string | undefined;
|
|
96
|
+
/** Creation timestamp */
|
|
97
|
+
created_at: string;
|
|
98
|
+
/** Last update timestamp */
|
|
99
|
+
updated_at: string;
|
|
100
|
+
/** Latest version */
|
|
101
|
+
latest_version: string;
|
|
102
|
+
/** Version list (paginated) */
|
|
103
|
+
versions: VersionMetadata[];
|
|
104
|
+
/** Total number of versions */
|
|
105
|
+
versions_total: number;
|
|
106
|
+
/** Total downloads across all versions */
|
|
107
|
+
total_downloads: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Bundle information
|
|
112
|
+
*/
|
|
113
|
+
export interface BundleInfo {
|
|
114
|
+
/** SHA-256 hash */
|
|
115
|
+
hash: string;
|
|
116
|
+
/** Size in bytes */
|
|
117
|
+
size: number;
|
|
118
|
+
/** Download URL */
|
|
119
|
+
download_url: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Tool version details from GET /tools/{name}/versions/{version}
|
|
124
|
+
*/
|
|
125
|
+
export interface ToolVersionDetails {
|
|
126
|
+
/** Tool name */
|
|
127
|
+
name: string;
|
|
128
|
+
/** Version */
|
|
129
|
+
version: string;
|
|
130
|
+
/** Tool description */
|
|
131
|
+
description: string;
|
|
132
|
+
/** SPDX license identifier */
|
|
133
|
+
license: string;
|
|
134
|
+
/** Whether this version is yanked */
|
|
135
|
+
yanked: boolean;
|
|
136
|
+
/** Yank reason (if yanked) */
|
|
137
|
+
yank_reason?: string | undefined;
|
|
138
|
+
/** Replacement version (if yanked) */
|
|
139
|
+
yank_replacement?: string | undefined;
|
|
140
|
+
/** When it was yanked */
|
|
141
|
+
yanked_at?: string | undefined;
|
|
142
|
+
/** Full manifest object */
|
|
143
|
+
manifest: Record<string, unknown>;
|
|
144
|
+
/** Bundle information */
|
|
145
|
+
bundle: BundleInfo;
|
|
146
|
+
/** List of attestations */
|
|
147
|
+
attestations: Attestation[];
|
|
148
|
+
/** Who published this version */
|
|
149
|
+
published_by: ApiAuthor;
|
|
150
|
+
/** Publication timestamp */
|
|
151
|
+
published_at: string;
|
|
152
|
+
/** Download count for this version */
|
|
153
|
+
downloads: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Single attestation record (v2 - auditor-only)
|
|
158
|
+
*/
|
|
159
|
+
export interface Attestation {
|
|
160
|
+
/** Auditor email (from Sigstore certificate) */
|
|
161
|
+
auditor: string;
|
|
162
|
+
/** OAuth provider used for attestation */
|
|
163
|
+
auditor_provider: string;
|
|
164
|
+
/** Signing timestamp */
|
|
165
|
+
signed_at: string;
|
|
166
|
+
/** Rekor transparency log ID */
|
|
167
|
+
rekor_log_id: string;
|
|
168
|
+
/** Rekor transparency log index */
|
|
169
|
+
rekor_log_index?: number | undefined;
|
|
170
|
+
/** Verification status */
|
|
171
|
+
verification?:
|
|
172
|
+
| {
|
|
173
|
+
verified: boolean;
|
|
174
|
+
verified_at: string;
|
|
175
|
+
rekor_verified: boolean;
|
|
176
|
+
certificate_verified: boolean;
|
|
177
|
+
signature_verified: boolean;
|
|
178
|
+
}
|
|
179
|
+
| undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Feedback aggregates from GET /tools/{name}/feedback
|
|
184
|
+
*/
|
|
185
|
+
export interface FeedbackAggregates {
|
|
186
|
+
/** Average rating (1-5) */
|
|
187
|
+
rating: number;
|
|
188
|
+
/** Number of ratings */
|
|
189
|
+
rating_count: number;
|
|
190
|
+
/** Total downloads */
|
|
191
|
+
downloads: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* User profile from GET /users/{username}
|
|
196
|
+
*/
|
|
197
|
+
export interface UserProfile {
|
|
198
|
+
/** Username */
|
|
199
|
+
username: string;
|
|
200
|
+
/** Display name */
|
|
201
|
+
display_name?: string | undefined;
|
|
202
|
+
/** Avatar URL */
|
|
203
|
+
avatar_url?: string | undefined;
|
|
204
|
+
/** Account creation date */
|
|
205
|
+
created_at: string;
|
|
206
|
+
/** Number of tools published */
|
|
207
|
+
tools_count?: number | undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Current user info from GET /auth/me (v2)
|
|
212
|
+
*/
|
|
213
|
+
export interface CurrentUser {
|
|
214
|
+
/** User ID */
|
|
215
|
+
id: string;
|
|
216
|
+
/** Username */
|
|
217
|
+
username: string;
|
|
218
|
+
/** Email address */
|
|
219
|
+
email: string;
|
|
220
|
+
/** Namespaces the user owns */
|
|
221
|
+
namespaces: string[];
|
|
222
|
+
/** Account creation date */
|
|
223
|
+
created_at: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* OAuth login request
|
|
228
|
+
*/
|
|
229
|
+
export interface OAuthLoginRequest {
|
|
230
|
+
/** OAuth provider */
|
|
231
|
+
provider: OAuthProvider;
|
|
232
|
+
/** Redirect URI for callback */
|
|
233
|
+
redirect_uri: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* OAuth login response
|
|
238
|
+
*/
|
|
239
|
+
export interface OAuthLoginResponse {
|
|
240
|
+
/** Authorization URL to redirect user to */
|
|
241
|
+
auth_url: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* OAuth callback request
|
|
246
|
+
*/
|
|
247
|
+
export interface OAuthCallbackRequest {
|
|
248
|
+
/** OAuth provider */
|
|
249
|
+
provider: OAuthProvider;
|
|
250
|
+
/** Authorization code from OAuth provider */
|
|
251
|
+
code: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* OAuth token response
|
|
256
|
+
*/
|
|
257
|
+
export interface OAuthTokenResponse {
|
|
258
|
+
/** Access token (JWT) */
|
|
259
|
+
access_token: string;
|
|
260
|
+
/** Refresh token */
|
|
261
|
+
refresh_token: string;
|
|
262
|
+
/** Token expiration in seconds */
|
|
263
|
+
expires_in: number;
|
|
264
|
+
/** User information */
|
|
265
|
+
user: {
|
|
266
|
+
id: string;
|
|
267
|
+
username: string;
|
|
268
|
+
email: string;
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Refresh token request
|
|
274
|
+
*/
|
|
275
|
+
export interface RefreshTokenRequest {
|
|
276
|
+
/** Refresh token */
|
|
277
|
+
refresh_token: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Refresh token response
|
|
282
|
+
*/
|
|
283
|
+
export interface RefreshTokenResponse {
|
|
284
|
+
/** New access token */
|
|
285
|
+
access_token: string;
|
|
286
|
+
/** Token expiration in seconds */
|
|
287
|
+
expires_in: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Publish response from POST /tools/{name} (v2 - single POST)
|
|
292
|
+
*/
|
|
293
|
+
export interface PublishResponse {
|
|
294
|
+
/** Tool name */
|
|
295
|
+
name: string;
|
|
296
|
+
/** Published version */
|
|
297
|
+
version: string;
|
|
298
|
+
/** Bundle hash */
|
|
299
|
+
bundle_hash: string;
|
|
300
|
+
/** Bundle size */
|
|
301
|
+
bundle_size: number;
|
|
302
|
+
/** Download URL */
|
|
303
|
+
download_url: string;
|
|
304
|
+
/** Publication timestamp */
|
|
305
|
+
published_at: string;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Yank version request
|
|
310
|
+
*/
|
|
311
|
+
export interface YankVersionRequest {
|
|
312
|
+
/** Reason for yanking */
|
|
313
|
+
reason?: string | undefined;
|
|
314
|
+
/** Replacement version to recommend */
|
|
315
|
+
replacement_version?: string | undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Yank version response
|
|
320
|
+
*/
|
|
321
|
+
export interface YankVersionResponse {
|
|
322
|
+
/** Whether version is yanked */
|
|
323
|
+
yanked: true;
|
|
324
|
+
/** Version that was yanked */
|
|
325
|
+
version: string;
|
|
326
|
+
/** Reason for yanking */
|
|
327
|
+
reason?: string | undefined;
|
|
328
|
+
/** Replacement version */
|
|
329
|
+
replacement_version?: string | undefined;
|
|
330
|
+
/** When it was yanked */
|
|
331
|
+
yanked_at: string;
|
|
332
|
+
/** Informational message */
|
|
333
|
+
message?: string | undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Unyank version response
|
|
338
|
+
*/
|
|
339
|
+
export interface UnyankVersionResponse {
|
|
340
|
+
/** Whether version is yanked */
|
|
341
|
+
yanked: false;
|
|
342
|
+
/** Version that was unyanked */
|
|
343
|
+
version: string;
|
|
344
|
+
/** When it was unyanked */
|
|
345
|
+
unyanked_at: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Submit attestation response (v2)
|
|
350
|
+
*/
|
|
351
|
+
export interface AttestationResponse {
|
|
352
|
+
/** Auditor email */
|
|
353
|
+
auditor: string;
|
|
354
|
+
/** OAuth provider */
|
|
355
|
+
auditor_provider: string;
|
|
356
|
+
/** Signing timestamp */
|
|
357
|
+
signed_at: string;
|
|
358
|
+
/** Rekor log ID */
|
|
359
|
+
rekor_log_id: string;
|
|
360
|
+
/** Rekor log index */
|
|
361
|
+
rekor_log_index?: number | undefined;
|
|
362
|
+
/** Verification result */
|
|
363
|
+
verification: {
|
|
364
|
+
verified: boolean;
|
|
365
|
+
verified_at: string;
|
|
366
|
+
rekor_verified: boolean;
|
|
367
|
+
certificate_verified: boolean;
|
|
368
|
+
signature_verified: boolean;
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Revoke attestation response
|
|
374
|
+
*/
|
|
375
|
+
export interface RevokeAttestationResponse {
|
|
376
|
+
/** Auditor email */
|
|
377
|
+
auditor: string;
|
|
378
|
+
/** Whether revocation succeeded */
|
|
379
|
+
revoked: true;
|
|
380
|
+
/** When it was revoked */
|
|
381
|
+
revoked_at: string;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Trusted auditor entry
|
|
386
|
+
*/
|
|
387
|
+
export interface TrustedAuditor {
|
|
388
|
+
/** Auditor identity (email) */
|
|
389
|
+
identity: string;
|
|
390
|
+
/** When they were added to trust list */
|
|
391
|
+
added_at: string;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* User trust configuration
|
|
396
|
+
*/
|
|
397
|
+
export interface UserTrustConfig {
|
|
398
|
+
/** Username */
|
|
399
|
+
username: string;
|
|
400
|
+
/** List of trusted auditors */
|
|
401
|
+
trusted_auditors: TrustedAuditor[];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Update trust configuration request
|
|
406
|
+
*/
|
|
407
|
+
export interface UpdateTrustConfigRequest {
|
|
408
|
+
/** Array of auditor emails to trust */
|
|
409
|
+
trusted_auditors: string[];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Update trust configuration response
|
|
414
|
+
*/
|
|
415
|
+
export interface UpdateTrustConfigResponse {
|
|
416
|
+
/** Updated list of trusted auditors */
|
|
417
|
+
trusted_auditors: TrustedAuditor[];
|
|
418
|
+
/** When the config was updated */
|
|
419
|
+
updated_at: string;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Rate limit info from response headers
|
|
424
|
+
*/
|
|
425
|
+
export interface RateLimitInfo {
|
|
426
|
+
/** Max requests per window */
|
|
427
|
+
limit: number;
|
|
428
|
+
/** Remaining requests */
|
|
429
|
+
remaining: number;
|
|
430
|
+
/** Unix timestamp when limit resets */
|
|
431
|
+
reset: number;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* HTTP error codes from API v2
|
|
436
|
+
*/
|
|
437
|
+
export type ApiErrorCode =
|
|
438
|
+
| "BAD_REQUEST"
|
|
439
|
+
| "UNAUTHORIZED"
|
|
440
|
+
| "FORBIDDEN"
|
|
441
|
+
| "NOT_FOUND"
|
|
442
|
+
| "CONFLICT"
|
|
443
|
+
| "VERSION_YANKED"
|
|
444
|
+
| "BUNDLE_TOO_LARGE"
|
|
445
|
+
| "VALIDATION_ERROR"
|
|
446
|
+
| "ATTESTATION_VERIFICATION_FAILED"
|
|
447
|
+
| "RATE_LIMITED"
|
|
448
|
+
| "INTERNAL_ERROR";
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* HTTP status codes
|
|
452
|
+
*/
|
|
453
|
+
export const HTTP_STATUS = {
|
|
454
|
+
OK: 200,
|
|
455
|
+
CREATED: 201,
|
|
456
|
+
NO_CONTENT: 204,
|
|
457
|
+
REDIRECT: 302,
|
|
458
|
+
BAD_REQUEST: 400,
|
|
459
|
+
UNAUTHORIZED: 401,
|
|
460
|
+
FORBIDDEN: 403,
|
|
461
|
+
NOT_FOUND: 404,
|
|
462
|
+
CONFLICT: 409,
|
|
463
|
+
GONE: 410,
|
|
464
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
465
|
+
VALIDATION_ERROR: 422,
|
|
466
|
+
RATE_LIMITED: 429,
|
|
467
|
+
INTERNAL_ERROR: 500,
|
|
468
|
+
} as const;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for API package
|
|
3
|
+
* Copied from @enactprotocol/shared to avoid circular browser dependency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert OIDC issuer URL to provider name
|
|
8
|
+
*/
|
|
9
|
+
function issuerToProvider(issuer: string): string | undefined {
|
|
10
|
+
if (issuer.includes("github.com")) return "github";
|
|
11
|
+
if (issuer.includes("accounts.google.com")) return "google";
|
|
12
|
+
if (issuer.includes("login.microsoftonline.com")) return "microsoft";
|
|
13
|
+
if (issuer.includes("gitlab.com")) return "gitlab";
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert OIDC identity to provider:identity format
|
|
19
|
+
* @param email - Email from Sigstore certificate
|
|
20
|
+
* @param issuer - OIDC issuer URL (optional, improves accuracy)
|
|
21
|
+
* @param username - Provider username if known (optional)
|
|
22
|
+
* @returns Identity in provider:identity format (e.g., github:keithagroves)
|
|
23
|
+
*/
|
|
24
|
+
export function emailToProviderIdentity(email: string, issuer?: string, username?: string): string {
|
|
25
|
+
// If we have a username and can determine the provider, use that
|
|
26
|
+
if (username && issuer) {
|
|
27
|
+
const provider = issuerToProvider(issuer);
|
|
28
|
+
if (provider) {
|
|
29
|
+
return `${provider}:${username}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Determine provider from issuer URL if available
|
|
34
|
+
if (issuer) {
|
|
35
|
+
const provider = issuerToProvider(issuer);
|
|
36
|
+
if (provider) {
|
|
37
|
+
// Try to extract username from email for GitHub
|
|
38
|
+
if (provider === "github" && email.endsWith("@users.noreply.github.com")) {
|
|
39
|
+
// GitHub noreply format: "123456+username@users.noreply.github.com"
|
|
40
|
+
// or just "username@users.noreply.github.com"
|
|
41
|
+
const localPart = email.replace("@users.noreply.github.com", "");
|
|
42
|
+
const plusIndex = localPart.indexOf("+");
|
|
43
|
+
const extractedUsername = plusIndex >= 0 ? localPart.slice(plusIndex + 1) : localPart;
|
|
44
|
+
return `github:${extractedUsername}`;
|
|
45
|
+
}
|
|
46
|
+
// Use email as the identity since we don't have username
|
|
47
|
+
return `${provider}:${email}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Common OIDC providers and their email domains (fallback)
|
|
52
|
+
const providerMap: Record<string, string> = {
|
|
53
|
+
"@users.noreply.github.com": "github",
|
|
54
|
+
"@github.com": "github",
|
|
55
|
+
"@gmail.com": "google",
|
|
56
|
+
"@googlemail.com": "google",
|
|
57
|
+
"@outlook.com": "microsoft",
|
|
58
|
+
"@hotmail.com": "microsoft",
|
|
59
|
+
"@live.com": "microsoft",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Try to match provider by email domain
|
|
63
|
+
for (const [domain, provider] of Object.entries(providerMap)) {
|
|
64
|
+
if (email.endsWith(domain)) {
|
|
65
|
+
let extractedUsername = email.substring(0, email.length - domain.length);
|
|
66
|
+
// Handle GitHub noreply format: "123456+username@users.noreply.github.com"
|
|
67
|
+
if (provider === "github" && domain === "@users.noreply.github.com") {
|
|
68
|
+
const plusIndex = extractedUsername.indexOf("+");
|
|
69
|
+
if (plusIndex >= 0) {
|
|
70
|
+
extractedUsername = extractedUsername.slice(plusIndex + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return `${provider}:${extractedUsername}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If no match, check for GitHub workflow identity
|
|
78
|
+
// Format: https://github.com/{org}/{workflow}
|
|
79
|
+
if (email.startsWith("https://github.com/")) {
|
|
80
|
+
const path = email.replace("https://github.com/", "");
|
|
81
|
+
return `github:${path}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fall back to email as-is
|
|
85
|
+
return email;
|
|
86
|
+
}
|