@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.
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Tool download functionality
3
+ * Implements bundle download and tool info retrieval
4
+ */
5
+
6
+ import type { EnactApiClient } from "./client";
7
+ import type { ToolMetadata, ToolVersionDetails } from "./types";
8
+
9
+ /**
10
+ * Download options
11
+ */
12
+ export interface DownloadOptions {
13
+ /** Tool name (e.g., "alice/utils/greeter") */
14
+ name: string;
15
+ /** Tool version (e.g., "1.2.0") */
16
+ version: string;
17
+ /** Verify bundle hash after download */
18
+ verify?: boolean | undefined;
19
+ }
20
+
21
+ /**
22
+ * Download result
23
+ */
24
+ export interface DownloadResult {
25
+ /** Downloaded bundle data */
26
+ data: ArrayBuffer;
27
+ /** Bundle hash (sha256) */
28
+ hash: string;
29
+ /** Content length in bytes */
30
+ size: number;
31
+ /** Content type */
32
+ contentType: string;
33
+ }
34
+
35
+ /**
36
+ * Tool info (metadata) v2
37
+ */
38
+ export interface ToolInfo {
39
+ /** Tool name */
40
+ name: string;
41
+ /** Tool description */
42
+ description: string;
43
+ /** Tool tags */
44
+ tags: string[];
45
+ /** SPDX license */
46
+ license: string;
47
+ /** Author info */
48
+ author: {
49
+ username: string;
50
+ avatarUrl?: string | undefined;
51
+ };
52
+ /** Repository URL */
53
+ repository?: string | undefined;
54
+ /** Creation timestamp */
55
+ createdAt: Date;
56
+ /** Last update timestamp */
57
+ updatedAt: Date;
58
+ /** Latest version */
59
+ latestVersion: string;
60
+ /** All available versions (paginated) */
61
+ versions: Array<{
62
+ version: string;
63
+ publishedAt: Date;
64
+ downloads: number;
65
+ bundleHash: string;
66
+ yanked: boolean;
67
+ }>;
68
+ /** Total number of versions */
69
+ versionsTotal: number;
70
+ /** Total downloads */
71
+ totalDownloads: number;
72
+ }
73
+
74
+ /**
75
+ * Version-specific tool info v2
76
+ */
77
+ export interface ToolVersionInfo {
78
+ /** Tool name */
79
+ name: string;
80
+ /** Version */
81
+ version: string;
82
+ /** Description */
83
+ description: string;
84
+ /** License */
85
+ license: string;
86
+ /** Whether yanked */
87
+ yanked: boolean;
88
+ /** Yank reason */
89
+ yankReason?: string | undefined;
90
+ /** Replacement version */
91
+ yankReplacement?: string | undefined;
92
+ /** Yanked timestamp */
93
+ yankedAt?: Date | undefined;
94
+ /** Full manifest */
95
+ manifest: Record<string, unknown>;
96
+ /** Bundle info */
97
+ bundle: {
98
+ hash: string;
99
+ size: number;
100
+ downloadUrl: string;
101
+ };
102
+ /** Attestations */
103
+ attestations: Array<{
104
+ auditor: string;
105
+ auditorProvider: string;
106
+ signedAt: Date;
107
+ rekorLogId: string;
108
+ verified: boolean;
109
+ }>;
110
+ /** Published by */
111
+ publishedBy: {
112
+ username: string;
113
+ avatarUrl?: string | undefined;
114
+ };
115
+ /** Publication timestamp */
116
+ publishedAt: Date;
117
+ /** Download count */
118
+ downloads: number;
119
+ }
120
+
121
+ /**
122
+ * Convert raw tool metadata to ToolInfo
123
+ */
124
+ function toToolInfo(raw: ToolMetadata): ToolInfo {
125
+ return {
126
+ name: raw.name,
127
+ description: raw.description,
128
+ tags: raw.tags,
129
+ license: raw.license,
130
+ author: {
131
+ username: raw.author.username,
132
+ avatarUrl: raw.author.avatar_url,
133
+ },
134
+ repository: raw.repository,
135
+ createdAt: new Date(raw.created_at),
136
+ updatedAt: new Date(raw.updated_at),
137
+ latestVersion: raw.latest_version,
138
+ versions: raw.versions.map((v) => ({
139
+ version: v.version,
140
+ publishedAt: new Date(v.published_at),
141
+ downloads: v.downloads,
142
+ bundleHash: v.bundle_hash,
143
+ yanked: v.yanked,
144
+ })),
145
+ versionsTotal: raw.versions_total,
146
+ totalDownloads: raw.total_downloads,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Convert raw version details to ToolVersionInfo
152
+ */
153
+ function toToolVersionInfo(raw: ToolVersionDetails): ToolVersionInfo {
154
+ return {
155
+ name: raw.name,
156
+ version: raw.version,
157
+ description: raw.description,
158
+ license: raw.license,
159
+ yanked: raw.yanked,
160
+ yankReason: raw.yank_reason,
161
+ yankReplacement: raw.yank_replacement,
162
+ yankedAt: raw.yanked_at ? new Date(raw.yanked_at) : undefined,
163
+ manifest: raw.manifest,
164
+ bundle: {
165
+ hash: raw.bundle.hash,
166
+ size: raw.bundle.size,
167
+ downloadUrl: raw.bundle.download_url,
168
+ },
169
+ attestations: raw.attestations.map((a) => ({
170
+ auditor: a.auditor,
171
+ auditorProvider: a.auditor_provider,
172
+ signedAt: new Date(a.signed_at),
173
+ rekorLogId: a.rekor_log_id,
174
+ verified: a.verification?.verified ?? false,
175
+ })),
176
+ publishedBy: {
177
+ username: raw.published_by.username,
178
+ avatarUrl: raw.published_by.avatar_url,
179
+ },
180
+ publishedAt: new Date(raw.published_at),
181
+ downloads: raw.downloads,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Get tool metadata
187
+ *
188
+ * @param client - API client instance
189
+ * @param name - Tool name (e.g., "alice/utils/greeter")
190
+ * @returns Tool metadata
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * const info = await getToolInfo(client, "alice/utils/greeter");
195
+ * console.log(info.latestVersion);
196
+ * ```
197
+ */
198
+ export async function getToolInfo(client: EnactApiClient, name: string): Promise<ToolInfo> {
199
+ const response = await client.get<ToolMetadata>(`/tools/${name}`);
200
+ return toToolInfo(response.data);
201
+ }
202
+
203
+ /**
204
+ * Get version-specific tool information
205
+ *
206
+ * @param client - API client instance
207
+ * @param name - Tool name
208
+ * @param version - Tool version
209
+ * @returns Version-specific tool info
210
+ */
211
+ export async function getToolVersion(
212
+ client: EnactApiClient,
213
+ name: string,
214
+ version: string
215
+ ): Promise<ToolVersionInfo> {
216
+ const response = await client.get<ToolVersionDetails>(`/tools/${name}/versions/${version}`);
217
+ return toToolVersionInfo(response.data);
218
+ }
219
+
220
+ /**
221
+ * Get attestations for a tool version
222
+ *
223
+ * @param client - API client instance
224
+ * @param name - Tool name
225
+ * @param version - Tool version
226
+ * @returns Attestation list
227
+ */
228
+ export async function getAttestations(
229
+ client: EnactApiClient,
230
+ name: string,
231
+ version: string
232
+ ): Promise<
233
+ Array<{
234
+ auditor: string;
235
+ auditorProvider: string;
236
+ signedAt: Date;
237
+ rekorLogId: string;
238
+ verified: boolean;
239
+ }>
240
+ > {
241
+ const response = await client.get<ToolVersionDetails>(`/tools/${name}/versions/${version}`);
242
+ return response.data.attestations.map((a) => ({
243
+ auditor: a.auditor,
244
+ auditorProvider: a.auditor_provider,
245
+ signedAt: new Date(a.signed_at),
246
+ rekorLogId: a.rekor_log_id,
247
+ verified: a.verification?.verified ?? false,
248
+ }));
249
+ }
250
+
251
+ /**
252
+ * Download a tool bundle (v2 with yank handling)
253
+ *
254
+ * @param client - API client instance
255
+ * @param options - Download options
256
+ * @returns Downloaded bundle data with metadata
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const result = await downloadBundle(client, {
261
+ * name: "alice/utils/greeter",
262
+ * version: "1.2.0",
263
+ * verify: true
264
+ * });
265
+ * await Bun.write("bundle.tar.gz", result.data);
266
+ * ```
267
+ */
268
+ export async function downloadBundle(
269
+ client: EnactApiClient,
270
+ options: DownloadOptions & { acknowledgeYanked?: boolean }
271
+ ): Promise<DownloadResult> {
272
+ const { name, version, acknowledgeYanked } = options;
273
+
274
+ // Build query params for yanked version acknowledgment
275
+ const params = acknowledgeYanked ? "?acknowledge_yanked=true" : "";
276
+ const response = await client.download(`/tools/${name}/versions/${version}/download${params}`);
277
+
278
+ const data = await response.arrayBuffer();
279
+ const contentType = response.headers.get("Content-Type") ?? "application/gzip";
280
+ const size = data.byteLength;
281
+
282
+ // Extract hash from ETag or compute it
283
+ let hash = response.headers.get("ETag")?.replace(/"/g, "") ?? "";
284
+
285
+ // If verify is true and we need to compute hash, do it
286
+ if (options.verify && !hash.startsWith("sha256:")) {
287
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
288
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
289
+ hash = `sha256:${hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")}`;
290
+ }
291
+
292
+ return {
293
+ data,
294
+ hash,
295
+ size,
296
+ contentType,
297
+ };
298
+ }
package/src/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @enactprotocol/api - Registry API client for Enact tools (v2)
3
+ *
4
+ * This package provides HTTP client functionality for interacting with
5
+ * the Enact registry v2, including:
6
+ * - OAuth authentication
7
+ * - Tool search and discovery
8
+ * - Bundle download with yank handling
9
+ * - Tool publishing (multipart upload)
10
+ * - Attestation management
11
+ * - Trust configuration
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
16
+ export const VERSION = "0.2.0";
17
+
18
+ // =============================================================================
19
+ // Types
20
+ // =============================================================================
21
+
22
+ export * from "./types";
23
+
24
+ // =============================================================================
25
+ // Client
26
+ // =============================================================================
27
+
28
+ export { EnactApiClient, createApiClient } from "./client";
29
+ export type { ApiClientOptions } from "./client";
30
+
31
+ // =============================================================================
32
+ // Search
33
+ // =============================================================================
34
+
35
+ export { searchTools } from "./search";
36
+ export type { SearchOptions, SearchResult, SearchResponse } from "./search";
37
+
38
+ // =============================================================================
39
+ // Download
40
+ // =============================================================================
41
+
42
+ export { downloadBundle, getToolInfo, getToolVersion, getAttestations } from "./download";
43
+ export type { DownloadOptions, DownloadResult, ToolInfo, ToolVersionInfo } from "./download";
44
+
45
+ // =============================================================================
46
+ // Publish
47
+ // =============================================================================
48
+
49
+ export {
50
+ publishTool,
51
+ createBundle,
52
+ submitAttestation,
53
+ yankVersion,
54
+ unyankVersion,
55
+ deleteTool,
56
+ } from "./publish";
57
+ export type {
58
+ PublishOptions,
59
+ PublishResult,
60
+ BundleInfo,
61
+ SubmitAttestationOptions,
62
+ AttestationResult,
63
+ } from "./publish";
64
+
65
+ // =============================================================================
66
+ // Auth (v2 OAuth)
67
+ // =============================================================================
68
+
69
+ export {
70
+ initiateLogin,
71
+ exchangeCodeForToken,
72
+ refreshAccessToken,
73
+ getCurrentUser,
74
+ authenticate,
75
+ logout,
76
+ getAuthStatus,
77
+ getUserProfile,
78
+ submitFeedback,
79
+ } from "./auth";
80
+ export type { AuthResult, AuthStatus } from "./auth";
81
+
82
+ // =============================================================================
83
+ // Attestations (v2)
84
+ // =============================================================================
85
+
86
+ export {
87
+ getAttestations as getAttestationList,
88
+ submitAttestation as submitAttestationToRegistry,
89
+ revokeAttestation,
90
+ hasAttestation,
91
+ getAttestationBundle,
92
+ verifyAttestationLocally,
93
+ verifyAllAttestations,
94
+ hasTrustedAttestation,
95
+ } from "./attestations";
96
+ export type { AttestationListResponse, VerifiedAuditor } from "./attestations";
97
+
98
+ // =============================================================================
99
+ // Trust (v2)
100
+ // =============================================================================
101
+
102
+ export {
103
+ getUserTrust,
104
+ updateMyTrust,
105
+ addTrustedAuditor,
106
+ removeTrustedAuditor,
107
+ userTrustsAuditor,
108
+ getMyTrustedAuditors,
109
+ } from "./trust";
package/src/publish.ts ADDED
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Tool publishing functionality (v2)
3
+ * Implements bundle creation and publishing to registry
4
+ */
5
+
6
+ import type { EnactApiClient } from "./client";
7
+ import type { AttestationResponse, PublishResponse } from "./types";
8
+
9
+ /**
10
+ * Publish options
11
+ */
12
+ export interface PublishOptions {
13
+ /** Tool name (e.g., "alice/utils/greeter") */
14
+ name: string;
15
+ /** Tool version (e.g., "1.2.0") */
16
+ version: string;
17
+ /** Bundle data (tar.gz) */
18
+ bundle: ArrayBuffer | Uint8Array;
19
+ }
20
+
21
+ /**
22
+ * Publish result
23
+ */
24
+ export interface PublishResult {
25
+ /** Tool name */
26
+ name: string;
27
+ /** Published version */
28
+ version: string;
29
+ /** Publication timestamp */
30
+ publishedAt: Date;
31
+ /** Bundle hash */
32
+ bundleHash: string;
33
+ }
34
+
35
+ /**
36
+ * Bundle info (for createBundle)
37
+ */
38
+ export interface BundleInfo {
39
+ /** Bundle data */
40
+ data: Uint8Array;
41
+ /** SHA-256 hash */
42
+ hash: string;
43
+ /** Size in bytes */
44
+ size: number;
45
+ }
46
+
47
+ /**
48
+ * Attestation submission options (v2)
49
+ */
50
+ export interface SubmitAttestationOptions {
51
+ /** Tool name */
52
+ name: string;
53
+ /** Tool version */
54
+ version: string;
55
+ /** Sigstore bundle (full bundle object) */
56
+ sigstoreBundle: Record<string, unknown>;
57
+ }
58
+
59
+ /**
60
+ * Attestation submission result (v2)
61
+ */
62
+ export interface AttestationResult {
63
+ /** Auditor email */
64
+ auditor: string;
65
+ /** OAuth provider */
66
+ auditorProvider: string;
67
+ /** Signing timestamp */
68
+ signedAt: Date;
69
+ /** Rekor log ID */
70
+ rekorLogId: string;
71
+ /** Rekor log index */
72
+ rekorLogIndex?: number | undefined;
73
+ /** Verification result */
74
+ verification: {
75
+ verified: boolean;
76
+ verifiedAt: Date;
77
+ rekorVerified: boolean;
78
+ certificateVerified: boolean;
79
+ signatureVerified: boolean;
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Create a bundle from tool directory
85
+ *
86
+ * This is a placeholder that would typically:
87
+ * 1. Read all files from the tool directory
88
+ * 2. Create a tar.gz archive
89
+ * 3. Compute SHA-256 hash
90
+ *
91
+ * @param toolDir - Path to tool directory
92
+ * @returns Bundle info with data and hash
93
+ */
94
+ export async function createBundle(toolDir: string): Promise<BundleInfo> {
95
+ // This would use @enactprotocol/shared utilities to:
96
+ // 1. Read files from toolDir
97
+ // 2. Create tarball
98
+ // 3. Compute hash
99
+
100
+ // For now, this is a placeholder that throws
101
+ // The actual implementation would integrate with the shared package
102
+ throw new Error(`createBundle not yet implemented for: ${toolDir}`);
103
+ }
104
+
105
+ /**
106
+ * Publish a tool to the registry (v2 - multipart upload)
107
+ *
108
+ * @param client - API client instance (must be authenticated)
109
+ * @param options - Publish options with manifest and bundle
110
+ * @returns Publish result
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const bundle = await createBundle("./my-tool");
115
+ * const result = await publishTool(client, {
116
+ * name: "alice/utils/greeter",
117
+ * manifest: { enact: "2.0.0", name: "alice/utils/greeter", version: "1.2.0", ... },
118
+ * bundle: bundle.data,
119
+ * readme: "# My Tool\n\nDescription..."
120
+ * });
121
+ * console.log(`Published: ${result.bundleHash}`);
122
+ * ```
123
+ */
124
+ export async function publishTool(
125
+ client: EnactApiClient,
126
+ options: {
127
+ name: string;
128
+ manifest: Record<string, unknown>;
129
+ bundle: ArrayBuffer | Uint8Array;
130
+ readme?: string | undefined;
131
+ }
132
+ ): Promise<PublishResult> {
133
+ const { name, manifest, bundle, readme } = options;
134
+
135
+ // Create FormData for multipart upload
136
+ const formData = new FormData();
137
+
138
+ // Add manifest as JSON
139
+ formData.append("manifest", JSON.stringify(manifest));
140
+
141
+ // Add bundle as file
142
+ const bundleBlob =
143
+ bundle instanceof ArrayBuffer
144
+ ? new Blob([bundle], { type: "application/gzip" })
145
+ : new Blob([bundle], { type: "application/gzip" });
146
+ formData.append("bundle", bundleBlob, "bundle.tar.gz");
147
+
148
+ // Add optional readme
149
+ if (readme) {
150
+ formData.append("readme", readme);
151
+ }
152
+
153
+ // Make multipart request (v2 endpoint is POST /tools/{name})
154
+ const response = await fetch(`${client.getBaseUrl()}/tools/${name}`, {
155
+ method: "POST",
156
+ headers: {
157
+ Authorization: client.getAuthToken() ? `Bearer ${client.getAuthToken()}` : "",
158
+ "User-Agent": client.getUserAgent() || "enact-cli/0.1.0",
159
+ },
160
+ body: formData,
161
+ });
162
+
163
+ if (!response.ok) {
164
+ const errorText = await response.text();
165
+ throw new Error(`Publish failed: ${response.status} - ${errorText}`);
166
+ }
167
+
168
+ const json = (await response.json()) as { data: PublishResponse } | PublishResponse;
169
+ // Handle both wrapped and unwrapped responses
170
+ const data = "data" in json ? json.data : json;
171
+
172
+ return {
173
+ name: data.name,
174
+ version: data.version,
175
+ publishedAt: new Date(data.published_at),
176
+ bundleHash: data.bundle_hash,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Submit an attestation for a tool version (v2)
182
+ *
183
+ * @param client - API client instance (must be authenticated via OIDC)
184
+ * @param options - Attestation options
185
+ * @returns Attestation result
186
+ */
187
+ export async function submitAttestation(
188
+ client: EnactApiClient,
189
+ options: SubmitAttestationOptions
190
+ ): Promise<AttestationResult> {
191
+ const { name, version, sigstoreBundle } = options;
192
+
193
+ const response = await client.post<AttestationResponse>(
194
+ `/tools/${name}/versions/${version}/attestations`,
195
+ {
196
+ bundle: sigstoreBundle,
197
+ }
198
+ );
199
+
200
+ return {
201
+ auditor: response.data.auditor,
202
+ auditorProvider: response.data.auditor_provider,
203
+ signedAt: new Date(response.data.signed_at),
204
+ rekorLogId: response.data.rekor_log_id,
205
+ rekorLogIndex: response.data.rekor_log_index,
206
+ verification: {
207
+ verified: response.data.verification.verified,
208
+ verifiedAt: new Date(response.data.verification.verified_at),
209
+ rekorVerified: response.data.verification.rekor_verified,
210
+ certificateVerified: response.data.verification.certificate_verified,
211
+ signatureVerified: response.data.verification.signature_verified,
212
+ },
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Yank a tool version (v2)
218
+ *
219
+ * Yanked versions remain downloadable but are excluded from version listings
220
+ * by default and show warnings to users.
221
+ *
222
+ * @param client - API client instance (must be authenticated and owner)
223
+ * @param name - Tool name
224
+ * @param version - Version to yank
225
+ * @param options - Yank options (reason, replacement)
226
+ * @returns Yank result
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * const result = await yankVersion(client, "alice/utils/greeter", "1.1.0", {
231
+ * reason: "Security vulnerability CVE-2025-1234",
232
+ * replacementVersion: "1.2.0"
233
+ * });
234
+ * ```
235
+ */
236
+ export async function yankVersion(
237
+ client: EnactApiClient,
238
+ name: string,
239
+ version: string,
240
+ options?: {
241
+ reason?: string | undefined;
242
+ replacementVersion?: string | undefined;
243
+ }
244
+ ): Promise<{
245
+ yanked: true;
246
+ version: string;
247
+ reason?: string | undefined;
248
+ replacementVersion?: string | undefined;
249
+ yankedAt: Date;
250
+ }> {
251
+ const response = await client.post<{
252
+ yanked: true;
253
+ version: string;
254
+ reason?: string | undefined;
255
+ replacement_version?: string | undefined;
256
+ yanked_at: string;
257
+ }>(`/tools/${name}/versions/${version}/yank`, {
258
+ reason: options?.reason,
259
+ replacement_version: options?.replacementVersion,
260
+ });
261
+
262
+ return {
263
+ yanked: response.data.yanked,
264
+ version: response.data.version,
265
+ reason: response.data.reason,
266
+ replacementVersion: response.data.replacement_version,
267
+ yankedAt: new Date(response.data.yanked_at),
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Unyank a tool version (v2)
273
+ *
274
+ * Restore a previously yanked version.
275
+ *
276
+ * @param client - API client instance (must be authenticated and owner)
277
+ * @param name - Tool name
278
+ * @param version - Version to unyank
279
+ * @returns Unyank result
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * const result = await unyankVersion(client, "alice/utils/greeter", "1.1.0");
284
+ * ```
285
+ */
286
+ export async function unyankVersion(
287
+ client: EnactApiClient,
288
+ name: string,
289
+ version: string
290
+ ): Promise<{
291
+ yanked: false;
292
+ version: string;
293
+ unyankedAt: Date;
294
+ }> {
295
+ const response = await client.post<{
296
+ yanked: false;
297
+ version: string;
298
+ unyanked_at: string;
299
+ }>(`/tools/${name}/versions/${version}/unyank`);
300
+
301
+ return {
302
+ yanked: response.data.yanked,
303
+ version: response.data.version,
304
+ unyankedAt: new Date(response.data.unyanked_at),
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Delete a tool from the registry
310
+ *
311
+ * @param client - API client instance (must be authenticated and owner)
312
+ * @param name - Tool name to delete
313
+ */
314
+ export async function deleteTool(client: EnactApiClient, name: string): Promise<void> {
315
+ await client.delete(`/tools/${name}`);
316
+ }