@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 ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@enactprotocol/api",
3
+ "version": "2.0.0",
4
+ "description": "Enact registry API client v2 - OAuth auth, search, download, publish, attestations, and trust",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc --build",
17
+ "dev": "bun run --watch src/index.ts",
18
+ "test": "bun test",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "dependencies": {},
22
+ "devDependencies": {
23
+ "@types/bun": "latest",
24
+ "@types/node": "^22.10.1",
25
+ "typescript": "^5.7.2"
26
+ },
27
+ "peerDependencies": {
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "files": ["dist", "src"],
31
+ "keywords": ["enact", "api", "registry", "tools"],
32
+ "author": "Enact Protocol",
33
+ "license": "Apache-2.0"
34
+ }
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Attestation management (v2)
3
+ * Functions for managing auditor attestations
4
+ */
5
+
6
+ import { extractIdentityFromBundle, verifyBundle } from "@enactprotocol/trust";
7
+ import type { OIDCIdentity, SigstoreBundle } from "@enactprotocol/trust";
8
+ import type { EnactApiClient } from "./client";
9
+ import type { Attestation, AttestationResponse, RevokeAttestationResponse } from "./types";
10
+ import { emailToProviderIdentity } from "./utils";
11
+
12
+ /**
13
+ * Verified auditor info with full identity details
14
+ */
15
+ export interface VerifiedAuditor {
16
+ /** Email from attestation */
17
+ email: string;
18
+ /** Full identity from verified certificate (may be undefined if not extractable) */
19
+ identity: OIDCIdentity | undefined;
20
+ /** Provider:identity format (e.g., github:keithagroves) */
21
+ providerIdentity: string;
22
+ }
23
+ /**
24
+ * Attestation list response
25
+ */
26
+ export interface AttestationListResponse {
27
+ attestations: Attestation[];
28
+ total: number;
29
+ limit: number;
30
+ offset: number;
31
+ }
32
+
33
+ /**
34
+ * Get all attestations for a tool version (v2)
35
+ *
36
+ * @param client - API client instance
37
+ * @param name - Tool name
38
+ * @param version - Tool version
39
+ * @param options - Pagination options
40
+ * @returns List of attestations with pagination info
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const result = await getAttestations(client, "alice/utils/greeter", "1.2.0", {
45
+ * limit: 10,
46
+ * offset: 0
47
+ * });
48
+ * console.log(`Found ${result.total} attestations`);
49
+ * ```
50
+ */
51
+ export async function getAttestations(
52
+ client: EnactApiClient,
53
+ name: string,
54
+ version: string,
55
+ options?: {
56
+ limit?: number | undefined;
57
+ offset?: number | undefined;
58
+ }
59
+ ): Promise<AttestationListResponse> {
60
+ const params = new URLSearchParams();
61
+
62
+ if (options?.limit !== undefined) {
63
+ params.set("limit", String(Math.min(options.limit, 100)));
64
+ }
65
+
66
+ if (options?.offset !== undefined) {
67
+ params.set("offset", String(options.offset));
68
+ }
69
+
70
+ const queryString = params.toString();
71
+ const path = `/tools/${name}/versions/${version}/attestations${queryString ? `?${queryString}` : ""}`;
72
+
73
+ const response = await client.get<AttestationListResponse>(path);
74
+ return response.data;
75
+ }
76
+
77
+ /**
78
+ * Submit an attestation for a tool version (v2)
79
+ *
80
+ * The server will verify the Sigstore bundle against the public Sigstore
81
+ * infrastructure (Rekor + Fulcio) before accepting.
82
+ *
83
+ * @param client - API client instance (must be authenticated)
84
+ * @param name - Tool name
85
+ * @param version - Tool version
86
+ * @param sigstoreBundle - Complete Sigstore bundle
87
+ * @returns Attestation response with verification result
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const result = await submitAttestation(client, "alice/utils/greeter", "1.2.0", {
92
+ * "$schema": "https://sigstore.dev/bundle/v1",
93
+ * "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
94
+ * "verificationMaterial": { ... },
95
+ * "messageSignature": { ... }
96
+ * });
97
+ *
98
+ * if (result.verification.verified) {
99
+ * console.log("Attestation verified and recorded!");
100
+ * }
101
+ * ```
102
+ */
103
+ export async function submitAttestation(
104
+ client: EnactApiClient,
105
+ name: string,
106
+ version: string,
107
+ sigstoreBundle: Record<string, unknown>
108
+ ): Promise<{
109
+ auditor: string;
110
+ auditorProvider: string;
111
+ signedAt: Date;
112
+ rekorLogId: string;
113
+ rekorLogIndex?: number | undefined;
114
+ verification: {
115
+ verified: boolean;
116
+ verifiedAt: Date;
117
+ rekorVerified: boolean;
118
+ certificateVerified: boolean;
119
+ signatureVerified: boolean;
120
+ };
121
+ }> {
122
+ const response = await client.post<AttestationResponse>(
123
+ `/tools/${name}/versions/${version}/attestations`,
124
+ {
125
+ bundle: sigstoreBundle,
126
+ }
127
+ );
128
+
129
+ return {
130
+ auditor: response.data.auditor,
131
+ auditorProvider: response.data.auditor_provider,
132
+ signedAt: new Date(response.data.signed_at),
133
+ rekorLogId: response.data.rekor_log_id,
134
+ rekorLogIndex: response.data.rekor_log_index,
135
+ verification: {
136
+ verified: response.data.verification.verified,
137
+ verifiedAt: new Date(response.data.verification.verified_at),
138
+ rekorVerified: response.data.verification.rekor_verified,
139
+ certificateVerified: response.data.verification.certificate_verified,
140
+ signatureVerified: response.data.verification.signature_verified,
141
+ },
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Revoke an attestation (v2)
147
+ *
148
+ * Only the original auditor can revoke their attestation.
149
+ *
150
+ * @param client - API client instance (must be authenticated)
151
+ * @param name - Tool name
152
+ * @param version - Tool version
153
+ * @param auditorEmail - Email of the auditor (from Sigstore certificate)
154
+ * @returns Revocation confirmation
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const result = await revokeAttestation(
159
+ * client,
160
+ * "alice/utils/greeter",
161
+ * "1.2.0",
162
+ * "security@example.com"
163
+ * );
164
+ * console.log(`Revoked at ${result.revokedAt}`);
165
+ * ```
166
+ */
167
+ export async function revokeAttestation(
168
+ client: EnactApiClient,
169
+ name: string,
170
+ version: string,
171
+ auditorEmail: string
172
+ ): Promise<{
173
+ auditor: string;
174
+ revoked: true;
175
+ revokedAt: Date;
176
+ }> {
177
+ const encodedEmail = encodeURIComponent(auditorEmail);
178
+ const response = await client.delete<RevokeAttestationResponse>(
179
+ `/tools/${name}/versions/${version}/attestations?auditor=${encodedEmail}`
180
+ );
181
+
182
+ return {
183
+ auditor: response.data.auditor,
184
+ revoked: response.data.revoked,
185
+ revokedAt: new Date(response.data.revoked_at),
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Check if a tool version has attestations from specific auditors
191
+ *
192
+ * @param client - API client instance
193
+ * @param name - Tool name
194
+ * @param version - Tool version
195
+ * @param trustedAuditors - List of trusted auditor emails
196
+ * @returns True if at least one trusted auditor has attested
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * const isTrusted = await hasAttestation(
201
+ * client,
202
+ * "alice/utils/greeter",
203
+ * "1.2.0",
204
+ * ["security@example.com", "bob@github.com"]
205
+ * );
206
+ *
207
+ * if (isTrusted) {
208
+ * console.log("Tool is trusted!");
209
+ * }
210
+ * ```
211
+ */
212
+ export async function hasAttestation(
213
+ client: EnactApiClient,
214
+ name: string,
215
+ version: string,
216
+ trustedAuditors: string[]
217
+ ): Promise<boolean> {
218
+ const result = await getAttestations(client, name, version);
219
+
220
+ return result.attestations.some(
221
+ (attestation) =>
222
+ trustedAuditors.includes(attestation.auditor) && attestation.verification?.verified === true
223
+ );
224
+ }
225
+
226
+ /**
227
+ * Get the full Sigstore bundle for a specific attestation
228
+ *
229
+ * This fetches the complete Sigstore bundle needed for local verification.
230
+ * Never trust the registry's verification status - always verify locally.
231
+ *
232
+ * @param client - API client instance
233
+ * @param name - Tool name
234
+ * @param version - Tool version
235
+ * @param auditor - Auditor email
236
+ * @returns Complete Sigstore bundle
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * const bundle = await getAttestationBundle(
241
+ * client,
242
+ * "alice/utils/greeter",
243
+ * "1.2.0",
244
+ * "security@example.com"
245
+ * );
246
+ * ```
247
+ */
248
+ export async function getAttestationBundle(
249
+ client: EnactApiClient,
250
+ name: string,
251
+ version: string,
252
+ auditor: string
253
+ ): Promise<SigstoreBundle> {
254
+ const encodedAuditor = encodeURIComponent(auditor);
255
+ const response = await client.get<SigstoreBundle>(
256
+ `/tools/${name}/versions/${version}/trust/attestations/${encodedAuditor}`
257
+ );
258
+ return response.data;
259
+ }
260
+
261
+ /**
262
+ * Verify an attestation locally using Sigstore (never trust registry)
263
+ *
264
+ * This performs cryptographic verification against Rekor transparency log,
265
+ * Fulcio certificate authority, and validates the signature. The registry's
266
+ * verification status is NEVER trusted - we always verify locally.
267
+ *
268
+ * @param client - API client instance
269
+ * @param name - Tool name
270
+ * @param version - Tool version
271
+ * @param attestation - Attestation metadata from registry
272
+ * @param bundleHash - Bundle hash to verify against (sha256:...)
273
+ * @returns True if attestation is cryptographically valid
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * const attestations = await getAttestations(client, "alice/utils/greeter", "1.2.0");
278
+ * const attestation = attestations.attestations[0];
279
+ *
280
+ * const isValid = await verifyAttestationLocally(
281
+ * client,
282
+ * "alice/utils/greeter",
283
+ * "1.2.0",
284
+ * attestation,
285
+ * "sha256:abc123..."
286
+ * );
287
+ *
288
+ * if (isValid) {
289
+ * console.log("Attestation cryptographically verified!");
290
+ * }
291
+ * ```
292
+ */
293
+ export async function verifyAttestationLocally(
294
+ client: EnactApiClient,
295
+ name: string,
296
+ version: string,
297
+ attestation: Attestation,
298
+ bundleHash: string
299
+ ): Promise<boolean> {
300
+ try {
301
+ // Fetch the full Sigstore bundle from registry
302
+ const bundle = await getAttestationBundle(client, name, version, attestation.auditor);
303
+
304
+ // Convert bundle hash to Buffer for verification
305
+ const hashWithoutPrefix = bundleHash.replace("sha256:", "");
306
+ const artifactHash = Buffer.from(hashWithoutPrefix, "hex");
307
+
308
+ // Verify using @enactprotocol/trust package (checks Rekor, Fulcio, signatures)
309
+ const result = await verifyBundle(bundle, artifactHash, {
310
+ expectedIdentity: {
311
+ subjectAlternativeName: attestation.auditor,
312
+ },
313
+ });
314
+
315
+ return result.verified;
316
+ } catch (error) {
317
+ // Verification failed - log error and return false
318
+ console.error(`Attestation verification failed for ${attestation.auditor}:`, error);
319
+ return false;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Verify all attestations for a tool and return verified auditors
325
+ *
326
+ * This verifies all attestations locally and returns only those that pass
327
+ * cryptographic verification. Never trusts the registry's verification status.
328
+ *
329
+ * @param client - API client instance
330
+ * @param name - Tool name
331
+ * @param version - Tool version
332
+ * @param bundleHash - Bundle hash to verify against
333
+ * @returns Array of verified auditor emails
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * const verifiedAuditors = await verifyAllAttestations(
338
+ * client,
339
+ * "alice/utils/greeter",
340
+ * "1.2.0",
341
+ * "sha256:abc123..."
342
+ * );
343
+ *
344
+ * console.log(`Verified auditors: ${verifiedAuditors.join(", ")}`);
345
+ * ```
346
+ */
347
+ export async function verifyAllAttestations(
348
+ client: EnactApiClient,
349
+ name: string,
350
+ version: string,
351
+ bundleHash: string
352
+ ): Promise<VerifiedAuditor[]> {
353
+ const { attestations } = await getAttestations(client, name, version);
354
+
355
+ // Verify all attestations in parallel
356
+ const verificationResults = await Promise.all(
357
+ attestations.map(async (attestation) => {
358
+ try {
359
+ // Fetch the full Sigstore bundle
360
+ const bundle = await getAttestationBundle(client, name, version, attestation.auditor);
361
+
362
+ // Convert bundle hash to Buffer for verification
363
+ const hashWithoutPrefix = bundleHash.replace("sha256:", "");
364
+ const artifactHash = Buffer.from(hashWithoutPrefix, "hex");
365
+
366
+ // Verify the bundle
367
+ const result = await verifyBundle(bundle, artifactHash, {
368
+ expectedIdentity: {
369
+ subjectAlternativeName: attestation.auditor,
370
+ },
371
+ });
372
+
373
+ if (result.verified) {
374
+ // Extract full identity from the verified bundle
375
+ const identity = extractIdentityFromBundle(bundle);
376
+
377
+ // Build provider:identity format using issuer info
378
+ const providerIdentity = emailToProviderIdentity(
379
+ attestation.auditor,
380
+ identity?.issuer,
381
+ identity?.username
382
+ );
383
+
384
+ return {
385
+ auditor: attestation.auditor,
386
+ identity,
387
+ providerIdentity,
388
+ isValid: true,
389
+ };
390
+ }
391
+ return { auditor: attestation.auditor, isValid: false };
392
+ } catch {
393
+ return { auditor: attestation.auditor, isValid: false };
394
+ }
395
+ })
396
+ );
397
+
398
+ // Return only verified auditors with full identity info
399
+ return verificationResults
400
+ .filter(
401
+ (
402
+ result
403
+ ): result is {
404
+ auditor: string;
405
+ identity: OIDCIdentity | undefined;
406
+ providerIdentity: string;
407
+ isValid: true;
408
+ } => result.isValid
409
+ )
410
+ .map((result) => ({
411
+ email: result.auditor,
412
+ identity: result.identity,
413
+ providerIdentity: result.providerIdentity,
414
+ }));
415
+ }
416
+
417
+ /**
418
+ * Check if a tool has a trusted attestation (with local verification)
419
+ *
420
+ * This checks if at least one attestation exists from a trusted auditor
421
+ * AND verifies it locally. Never trusts the registry's verification status.
422
+ *
423
+ * @param client - API client instance
424
+ * @param name - Tool name
425
+ * @param version - Tool version
426
+ * @param bundleHash - Bundle hash to verify against
427
+ * @param trustedAuditors - List of trusted auditor emails
428
+ * @returns True if at least one trusted auditor's attestation is verified
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * const isTrusted = await hasTrustedAttestation(
433
+ * client,
434
+ * "alice/utils/greeter",
435
+ * "1.2.0",
436
+ * "sha256:abc123...",
437
+ * ["security@example.com", "bob@github.com"]
438
+ * );
439
+ *
440
+ * if (isTrusted) {
441
+ * console.log("Tool is trusted and verified!");
442
+ * } else {
443
+ * console.log("No trusted attestations found - use 'enact inspect' to review");
444
+ * }
445
+ * ```
446
+ */
447
+ export async function hasTrustedAttestation(
448
+ client: EnactApiClient,
449
+ name: string,
450
+ version: string,
451
+ bundleHash: string,
452
+ trustedAuditors: string[]
453
+ ): Promise<boolean> {
454
+ const verifiedAuditors = await verifyAllAttestations(client, name, version, bundleHash);
455
+
456
+ // Check if any verified auditor's providerIdentity matches a trusted auditor
457
+ // providerIdentity is in format "github:username" or "github:email@domain.com"
458
+ return verifiedAuditors.some((auditor) => {
459
+ return trustedAuditors.includes(auditor.providerIdentity);
460
+ });
461
+ }