/** * Backend-hosted image storage client. * * Uploads raw image bytes to the Aexol backend so that sessions only ever * carry a stable URL (`/generated-images/`) instead of a base64 * payload. The three-step contract is: * * 1. `POST {backendUrl}/generated-images/upload-url` * -> `{ imageId, uploadUrl, url, expiresIn, requiredHeaders? }` * 2. `PUT ` (presigned, ~600s) * with `Content-Type` + every header from `requiredHeaders` * 3. `POST {backendUrl}/generated-images//confirm` * -> `{ id, size, url }` * * Downloading works the other way round and has two flavours: * * A. `GET {backendUrl}/generated-images/` (authenticated) * -> 302 redirect to a short-lived presigned S3 GET. * B. `GET {backendUrl}/generated-images//url` * (authenticated) -> `{ url, expiresIn }` where `url` is a presigned * S3 GET that needs NO headers at all. * * Flavour B is the one to hand to a third party that cannot authenticate * against our backend (an LLM provider fetching an image, an ``, * ...). See `resolveHostedImageUrl`. * * Errors from the backend have the shape * `{ "error": { "message": "...", "type": "..." } }` and are surfaced as a * `GeneratedImageUploadError` carrying the HTTP status and the step that * failed, so callers can decide whether to degrade gracefully. * * Shared by `image_generate` (Phase 1) and — in a later phase — user * screenshots/attachments, hence `kind` is a parameter rather than a constant. * * @module @spectral/backend/generated-images */ export type GeneratedImageKind = "generated" | "attachment"; export type UploadStep = "upload-url" | "put" | "confirm"; export interface UploadImageOptions { /** Backend base url, e.g. `https://api.aexol.ai` (no trailing slash needed). */ backendUrl: string; /** Machine JWT (`Authorization: Bearer `). */ token: string; /** Raw image bytes. */ bytes: Uint8Array; /** MIME type of the bytes, e.g. `image/png`. */ contentType: string; /** Storage bucket/kind on the backend. Defaults to `"generated"`. */ kind?: GeneratedImageKind; /** Intrinsic pixel width, when known (optional metadata). */ width?: number; /** Intrinsic pixel height, when known (optional metadata). */ height?: number; /** Injectable for tests / alternative runtimes. Defaults to global `fetch`. */ fetchImpl?: typeof fetch; /** Abort the whole sequence (e.g. tool cancellation). */ signal?: AbortSignal; /** Per-request timeout. Defaults to 60s. */ timeoutMs?: number; } export interface UploadImageResult { imageId: string; /** Backend-relative url (`/generated-images/`) — safe to persist in a session. */ url: string; size: number; } export interface UploadUrlResponse { imageId: string; uploadUrl: string; url: string; expiresIn?: number; requiredHeaders?: Record; } export interface ConfirmResponse { id: string; size: number; url: string; } export interface ResolveHostedImageUrlOptions { /** Backend base url, e.g. `https://api.aexol.ai`. */ backendUrl: string; /** Machine JWT (`Authorization: Bearer `). */ token: string; /** Image id. Optional when `url` is given (it is parsed from there). */ imageId?: string; /** Backend-relative (`/generated-images/`) or absolute hosted url. */ url?: string; /** Injectable for tests / alternative runtimes. Defaults to global `fetch`. */ fetchImpl?: typeof fetch; signal?: AbortSignal; /** Per-request timeout. Defaults to 60s. */ timeoutMs?: number; /** Diagnostics sink — failures are silent unless this is provided. */ onWarn?: (message: string) => void; } export interface HostedImageUrlResult { /** * Presigned S3 GET url that is fetchable WITHOUT any Authorization header. * Valid for `expiresIn` seconds (typically 3600). */ url: string; expiresIn: number; } /** * Extract the hosted image id from a backend-relative path * (`/generated-images/`) or from an absolute url * (`https://api.aexol.ai/generated-images/?x=1`). * * Only the PATH is inspected: a query/fragment that happens to contain * `/generated-images/` does not make the url one of ours. * * Returns `null` when the value is not a hosted backend image reference, so * callers can use it as a cheap "is this ours?" predicate. */ export declare function extractHostedImageId(urlOrPath: string): string | null; /** * The hosted image id carried by `url`, or `null` when `url` is not a hosted * backend image reference. Alias of `extractHostedImageId` kept for callers * that read better with a url-oriented name. */ export declare function hostedImageIdFromUrl(url: string): string | null; /** * True when `url` points at a backend-hosted image * (`/generated-images/`), either backend-relative or absolute. * * SECURITY: an absolute url is only "ours" when it actually points at OUR * backend. `backendUrl` is therefore REQUIRED for absolute urls — without it * there is nothing to compare against and the answer is `false`, so callers * never attach the machine JWT to a host they cannot vouch for. Relative urls * always match (they can only ever be resolved against the backend base url). */ export declare function isHostedImageUrl(url: string, backendUrl?: string): boolean; /** * Drop cached presigned urls. Omit `imageId` to clear the whole cache (used * by tests and after a logout/credential change). */ export declare function invalidateHostedImageUrl(imageId?: string, backendUrl?: string): void; /** * Turn a hosted image reference into a presigned url that a third party (LLM * provider, browser) can fetch WITHOUT authenticating against our backend. * * `GET {backendUrl}/generated-images//url` with the machine JWT returns * `{ url, expiresIn }`; the plain `GET /generated-images/` endpoint is * auth-gated and 302-redirects, so handing it to a provider only produces a * 401 and a silently dropped image. * * Results are cached per `|` until * `PRESIGNED_REFRESH_MARGIN_MS` before their expiry (see * `invalidateHostedImageUrl`), so repeated requests for the same image do not * each pay a round trip. * * NEVER throws and never logs: returns `null` on any failure (missing token, * non-2xx, network error, malformed body) so callers can fall back to their * previous behaviour. Pass `onWarn` to see why. */ export declare function resolveHostedImageUrl(options: ResolveHostedImageUrlOptions): Promise; /** * Thrown for every failure of the upload sequence (including presigned PUT * failures). `status` is `undefined` for network-level errors. */ export declare class GeneratedImageUploadError extends Error { readonly step: UploadStep; readonly status?: number; readonly type?: string; constructor(message: string, options: { step: UploadStep; status?: number; type?: string; cause?: unknown; }); } /** * Upload raw image bytes to backend storage and confirm them. * * @returns the image id, the backend-relative url and the confirmed byte size. * @throws {GeneratedImageUploadError} on any HTTP or network failure, with a * human-readable message taken from the backend `error.message` when present. */ export declare function uploadImage(options: UploadImageOptions): Promise; //# sourceMappingURL=generated-images.d.ts.map