import { z } from 'zod'; import { type APIClient, APIResponseSchema } from '@agentuity/api'; import { StorageListResponseSchema, StorageDeleteResponseSchema, StoragePresignResponseSchema, StorageStatsResponseSchema, StorageAnalyticsResponseSchema, type StorageListResponse, type StorageDeleteResponse, type StoragePresignResponse, type StorageStatsResponse, type StorageAnalyticsResponse, } from './types.ts'; import { StorageObjectsResponseError } from './util.ts'; export const StorageListAPIResponseSchema = APIResponseSchema(StorageListResponseSchema); export const StorageDeleteAPIResponseSchema = APIResponseSchema(StorageDeleteResponseSchema); export const StoragePresignAPIResponseSchema = APIResponseSchema(StoragePresignResponseSchema); export const StorageStatsAPIResponseSchema = APIResponseSchema(StorageStatsResponseSchema); export const StorageAnalyticsAPIResponseSchema = APIResponseSchema(StorageAnalyticsResponseSchema); export const ListStorageObjectsOptionsSchema = z.object({ prefix: z.string().optional().describe('Optional object key prefix filter'), limit: z.number().optional().describe('Optional maximum number of objects to return'), offset: z.number().optional().describe('Optional pagination offset'), }); export type ListStorageObjectsOptions = z.infer; /** * List objects in a storage bucket with optional prefix filtering and pagination. * * @param client - The API client to use for the request * @param bucketName - Name of the bucket to list objects from * @param options - Optional filtering/pagination options * @param extraHeaders - Optional extra headers (e.g. x-agentuity-orgid for CLI auth) * @returns Paginated list of objects with total count * @throws {StorageObjectsResponseError} If the request fails */ export async function listStorageObjects( client: APIClient, bucketName: string, options?: ListStorageObjectsOptions, extraHeaders?: Record ): Promise { const params = new URLSearchParams(); if (options?.prefix) params.set('prefix', options.prefix); if (options?.limit !== undefined) { if ( !Number.isFinite(options.limit) || !Number.isInteger(options.limit) || options.limit < 0 ) { throw new TypeError('limit must be a non-negative integer'); } params.set('limit', String(options.limit)); } if (options?.offset !== undefined) { if ( !Number.isFinite(options.offset) || !Number.isInteger(options.offset) || options.offset < 0 ) { throw new TypeError('offset must be a non-negative integer'); } params.set('offset', String(options.offset)); } const query = params.toString(); const url = `/storage/objects/${encodeURIComponent(bucketName)}${query ? `?${query}` : ''}`; const resp = await client.get>( url, StorageListAPIResponseSchema, undefined, extraHeaders ); if (resp.success) { return resp.data; } throw new StorageObjectsResponseError({ message: resp.message }); } export const DeleteStorageObjectsOptionsSchema = z.object({ key: z.string().optional().describe('Optional single object key to delete'), prefix: z.string().optional().describe('Optional prefix for bulk deletion'), }); export type DeleteStorageObjectsOptions = z.infer; /** * Delete objects from a storage bucket. * Provide either `key` (single object) or `prefix` (all matching objects), but not both. * * @param client - The API client to use for the request * @param bucketName - Name of the bucket to delete from * @param options - Must include either key or prefix (mutually exclusive) * @param extraHeaders - Optional extra headers (e.g. x-agentuity-orgid for CLI auth) * @returns The count of deleted objects * @throws {StorageObjectsResponseError} If the request fails */ export async function deleteStorageObjects( client: APIClient, bucketName: string, options: DeleteStorageObjectsOptions, extraHeaders?: Record ): Promise { if (!options.key && !options.prefix) { throw new StorageObjectsResponseError({ message: "Either 'key' or 'prefix' is required" }); } if (options.key && options.prefix) { throw new StorageObjectsResponseError({ message: "Provide either 'key' or 'prefix', not both", }); } const params = new URLSearchParams(); if (options.key) params.set('key', options.key); if (options.prefix) params.set('prefix', options.prefix); const url = `/storage/objects/${encodeURIComponent(bucketName)}?${params.toString()}`; const resp = await client.delete>( url, StorageDeleteAPIResponseSchema, undefined, extraHeaders ); if (resp.success) { return resp.data; } throw new StorageObjectsResponseError({ message: resp.message }); } /** * Generate a presigned URL for downloading or uploading an object. * * @param client - The API client to use for the request * @param bucketName - Name of the bucket * @param key - Object key * @param operation - 'download' (default) or 'upload' * @param extraHeaders - Optional extra headers (e.g. x-agentuity-orgid for CLI auth) * @returns Presigned URL and expiry info * @throws {StorageObjectsResponseError} If the request fails */ export async function presignStorageObject( client: APIClient, bucketName: string, key: string, operation: 'download' | 'upload' = 'download', extraHeaders?: Record ): Promise { if (!key) { throw new StorageObjectsResponseError({ message: "'key' must be a non-empty string" }); } const params = new URLSearchParams(); params.set('key', key); if (operation !== 'download') { params.set('operation', operation); } const url = `/storage/presign/${encodeURIComponent(bucketName)}?${params.toString()}`; const resp = await client.get>( url, StoragePresignAPIResponseSchema, undefined, extraHeaders ); if (resp.success) { return resp.data; } throw new StorageObjectsResponseError({ message: resp.message }); } /** * Get aggregate stats for a storage bucket (object count, total size). * * @param client - The API client to use for the request * @param bucketName - Name of the bucket * @param extraHeaders - Optional extra headers (e.g. x-agentuity-orgid for CLI auth) * @returns Bucket statistics * @throws {StorageObjectsResponseError} If the request fails */ export async function getStorageStats( client: APIClient, bucketName: string, extraHeaders?: Record ): Promise { const url = `/storage/stats/${encodeURIComponent(bucketName)}`; const resp = await client.get>( url, StorageStatsAPIResponseSchema, undefined, extraHeaders ); if (resp.success) { return resp.data; } throw new StorageObjectsResponseError({ message: resp.message }); } export const GetStorageAnalyticsOptionsSchema = z.object({ days: z.number().optional().describe('Optional number of days of analytics history to return'), }); export type GetStorageAnalyticsOptions = z.infer; /** * Get storage analytics for the org: summary totals, per-bucket breakdown, and daily snapshots. * * @param client - The API client to use for the request * @param options - Optional options (days for sparkline history, default 180) * @param extraHeaders - Optional extra headers (e.g. x-agentuity-orgid for CLI auth) * @returns Analytics data with summary, buckets, and daily snapshots * @throws {StorageObjectsResponseError} If the request fails */ export async function getStorageAnalytics( client: APIClient, options?: GetStorageAnalyticsOptions, extraHeaders?: Record ): Promise { const params = new URLSearchParams(); if (options?.days !== undefined) { if (!Number.isFinite(options.days) || !Number.isInteger(options.days) || options.days < 0) { throw new TypeError('days must be a non-negative integer'); } params.set('days', String(options.days)); } const query = params.toString(); const url = `/storage/analytics${query ? `?${query}` : ''}`; const resp = await client.get>( url, StorageAnalyticsAPIResponseSchema, undefined, extraHeaders ); if (resp.success) { return resp.data; } throw new StorageObjectsResponseError({ message: resp.message }); }