import { ResolveSlotOptions, SlotResolution, SlotDTO, VariantPreset, AssetDTO, AssetVariant, TransformOptions, SignedTransformOptions } from '@aquienpz/asset-client'; export { AssetDTO, AssetVariant, ResolveSlotOptions, SignedTransformOptions, SlotDTO, SlotResolution, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, VariantPreset, computeVariantDimensions, extractAssetSha, getAssetDimensions, getAssetSrcSet, getAssetUrl, getHlsStreamingUrl, getSignedTransformUrl, getTenantId, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, serializeTransform, setTenantId, signTransformUrl } from '@aquienpz/asset-client'; /** * @aquienpz/sdk — universal client for the aquienpz multi-tenant asset * platform. * * One ergonomic facade over the underlying packages * (`@aquienpz/asset-client` URL builders + `@aquienpz/asset-uploader-web` * + the slot resolver). Auth is a Better Auth API key (`amk_rt_*`) * issued by aquienpz `bootstrap-project.ts`; tenant scope comes from * the key's metadata (`X-Tenant-Code` is log-only). * * Usage: * * import { AquienpzClient } from "@aquienpz/sdk"; * * const aq = new AquienpzClient({ * endpoint: "https://aquienpz-asset-manager-xxx.run.app", * apiKey: process.env.ASSET_MANAGER_RUNTIME_KEY!, * tenantCode: "realtyone-cr", * cdnBase: "https://8ok.uk", // optional override * tenantId: 4, // required for tenant-prefixed URLs * }); * * // Slot system (recommended — admin can rebind without redeploys). * const hero = await aq.slots.resolve("storefront.cr.hero"); * const set = await aq.slots.resolveMany(["a", "b", "c"]); * * // Lower-level asset operations. * const asset = await aq.assets.byHash(sha); * const list = await aq.assets.list({ limit: 50 }); * * // Upload bytes / files. * const up = await aq.upload(file, { fileName: "cover.jpg" }); * * For React, see `@aquienpz/sdk/react` (useSlot, useSlots, useAsset). */ /** * Permissive constructor options for the root `AquienpzClient`. * * App code should NOT import this type directly — prefer the strict * variants from the subpaths: * * - `WebClientOptions` from `@aquienpz/sdk/web` (no `apiKey`) * - `ServerClientOptions` from `@aquienpz/sdk/server` (`apiKey` required) * * This root type is the union both modes resolve to; the underlying class * accepts both shapes so subpath wrappers can extend without duplication. */ type AquienpzClientOptions = { /** * Base URL of the aquienpz asset-manager (Cloud Run service URL). * * May be relative (e.g. `/api/am`) ONLY in browser contexts where the * SDK resolves it against `window.location.origin`. Node/Bun consumers * must always pass an absolute URL. */ endpoint: string; /** * Better Auth API key with the `amk_rt_*` prefix. * * **Server-only.** Omit when constructing from `@aquienpz/sdk/web` — * your BFF / route handler injects the bearer header in proxy mode. */ apiKey?: string; /** * Extra headers merged into every request. The documented way for * mobile/Expo clients to authenticate a BFF that gates on the Better Auth * session: they can't send cookies automatically, so they pass * `{ Cookie: authClient.getCookie() }` here (see Better Auth Expo docs, * "Making Authenticated Requests to Your Server"). Web/server consumers omit * this — browsers attach the same-origin cookie and servers pass `apiKey`. */ headers?: Record; /** Tenant code — sent as `X-Tenant-Code` (log-only). Authoritative scope is the key's metadata.tenantId. */ tenantCode: string; /** Numeric tenant id — used to build tenant-prefixed CDN URLs `//v/-.`. */ tenantId: number; /** Override the public CDN base. Defaults to `https://8ok.uk`. */ cdnBase?: string; /** * Tenant's HMAC signing key for transform URLs (Phase 3). Required * only when calling `aq.transform(asset, opts, { sign: true })`. * * Generated server-side per tenant (see `infra/sql/tenants_signed_transforms.sql`); * fetch via `GET /admin/tenants/:id` with an admin key. **Keep it * server-side only** — do not ship in `NEXT_PUBLIC_*` env vars. Sign * URLs from a BFF route handler, or pre-sign at build time. */ signingKey?: string; }; declare class SlotsApi { private readonly opts; constructor(opts: AquienpzClientOptions); /** Resolve one slot — returns `{slot, preset, url}` or `{slot: null, url: null}` when unbound. */ resolve(slotKey: string, options?: ResolveSlotOptions): Promise; /** Bulk-resolve N slots in one HTTP round-trip. */ resolveMany(slotKeys: string[], options?: ResolveSlotOptions): Promise>; /** List slots for the tenant (admin). Optional prefix filter for tree views. */ list(opts?: { prefix?: string; limit?: number; }): Promise; /** Bind / rebind a slot to an asset. Admin-only operation. */ bind(slotKey: string, body: { assetId: string; preset?: VariantPreset; description?: string; updatedBy?: string; }): Promise<{ ok: true; slotKey: string; assetId: string; }>; /** * Recent bindings for a slot. Lets the admin audit who changed * what and restore a previous binding without remembering the * asset id. Default limit 20, max 100. */ history(slotKey: string, opts?: { limit?: number; }): Promise; /** * Restore the slot to a previous binding. Equivalent to * `bind(key, { assetId: previous.assetId, action: "restore" })` * — the audit row is tagged `restore` instead of `bind`. */ restore(slotKey: string, args: { assetId: string; preset?: VariantPreset; updatedBy?: string; }): Promise<{ ok: true; slotKey: string; assetId: string; }>; /** Remove a slot binding. The asset itself is left alone. */ unbind(slotKey: string): Promise<{ ok: true; removed: number; }>; /** Invalidate the in-process cache after a slot rebind. */ invalidateCache(slotKey?: string): void; private headers; } /** * Returned by `aq.assets.regenerate(...)`. The shape varies by kind — * images return immediately with the merged variant list; videos * return a dispatch handle (the actual transcode runs in a Cloud Run * Job and finishes async). */ type RegenerateResult = { ok: true; kind: "image"; /** Full variant set after the merge. */ variants: AssetVariant[]; /** Presets newly written this run. Useful for showing "added X". */ newVariants: VariantPreset[]; /** * Which source the server read to derive the new variants: * - `"original"` / `"raw"` → lossless source bytes (best) * - `"xl"` / `"lg"` / `"md"` / `"sm"` / `"thumb"` → a previously * encoded WebP variant was used as the source. Output is * re-encoded WebP — fine for thumb/sm from lg, lossier when * working from already-small sources. * * The no-upscale clamp still applies: deriving `lg` (1920) from * a 640 `sm` source produces a 640-side `lg` variant, not a * stretched 1920. */ sourceUsed: VariantPreset | "raw"; } | { ok: true; kind: "video"; dispatch: unknown; regenerated: string[] | "default"; }; /** * Wire shape returned by `POST /assets/upload-url`. Either the server * resolves the upload synchronously via dedup (`deduped: true` + existing * asset DTO) or it returns a presigned R2 PUT URL plus a `process` payload * the caller must POST to `/assets/process` after the PUT lands. */ type UploadUrlResult = { deduped: true; asset: AssetDTO; } | { deduped: false; upload: { url: string; headers?: Record; }; process: { url: string; body: Record; }; }; /** * VIDEO-only delivery knobs threaded into `/assets/process`. Ignored for * image / audio / other uploads. Both fields default to today's behavior when * omitted, so existing callers are unaffected. */ type UploadVideoOptions = { /** * `false` → skip the auto-dispatched HLS adaptive ladder (240p–2160p). Use * for download-only assets served as a progressive `-v.mp4` and never * streamed (e.g. share-video reels) — it avoids a second Cloud Run Job no * one watches. Default/absent → the ladder is generated as before. */ hls?: boolean; /** * `true` → when the uploaded MP4 is ALREADY web-safe (H.264 + yuv420p), * re-mux the `video` variant with `-c copy` instead of re-encoding. Use for * delivery-ready uploads (the bytes are already H.264 High / yuv420p / * +faststart / capped bitrate) to skip a wasteful re-encode + generational * quality loss. Falls back to a full re-encode automatically when the source * is not web-safe. Default/absent → unconditional re-encode (today's path). */ passthrough?: boolean; }; /** Input shape accepted by `aq.assets.presignUploadUrl(...)`. */ type PresignUploadUrlOptions = { /** Full sha256 (64 hex) of the bytes that will be PUT to R2. */ sha256: string; /** MIME type of the bytes (e.g. `image/jpeg`, `video/mp4`). */ mime: string; /** Byte length of the upload payload. */ bytes: number; /** Suggested file name; surfaces in admin dashboards + extension fallback. */ fileName: string; /** * Variant ladder to generate after `/assets/process`. Defaults to * `["original"]` server-side when omitted — same contract as `aq.upload`. */ presets?: VariantPreset[]; /** * Pre-compression size of the source (useful when the browser ran * compressorjs / heic2any before computing `bytes`). Surfaces in admin * dashboards under `assets.client_original_bytes`. */ clientOriginalBytes?: number; /** VIDEO-only delivery knobs forwarded into `/assets/process`. See {@link UploadVideoOptions}. */ video?: UploadVideoOptions; }; /** Input shape accepted by `aq.assets.composeMarketing(...)`. */ type ComposeMarketingSegment = { /** Public URL of the source clip (typically a `/t/.../video.mp4` transform). */ sourceUrl: string; /** Optional clip duration in seconds (cap for that segment). */ durationSec?: number; }; type ComposeMarketingComposition = { /** Transition between consecutive segments. Default `"cut"`. */ transition?: "cut" | "fade"; /** Optional audio track to mix on top of the final composition. */ audioTrack?: { url: string; }; /** Final composition length in seconds (server may clamp). */ finalDurationSec?: number; }; type ComposeMarketingOptions = { /** Marketing-kit id this composition belongs to (server uses it for naming + dedup). */ marketingKitId: string; /** Ordered clip segments to stitch. */ segments: ComposeMarketingSegment[]; /** Optional composition-level knobs (transitions, audio, duration). */ composition?: ComposeMarketingComposition; }; type ComposeMarketingResult = { /** Aquienpz asset id of the in-flight composition. Poll `aq.assets.waitReady(id)`. */ assetId: string; /** Asset status at dispatch time — usually `"processing"`. */ status: "processing" | "ready" | "failed"; }; declare class AssetsApi { private readonly opts; constructor(opts: AquienpzClientOptions); /** Look up an asset by full sha256 (64 hex). Returns null on 404. */ byHash(sha256: string): Promise; /** Bulk lookup by sha256s. */ byHashes(hashes: string[]): Promise<{ existing: AssetDTO[]; missing: string[]; }>; /** Paginated list of recent assets for the tenant. */ list(opts?: { limit?: number; cursor?: string; includeDeleted?: boolean; }): Promise<{ assets: AssetDTO[]; nextCursor: string | null; }>; /** Full DTO for an asset (admin view — includes audit-only fields). */ get(assetId: string): Promise>; /** * Slot bindings pointing at an asset. Use this before deleting an * asset so the admin sees which storefront slots would suddenly * resolve to nothing. */ bindings(assetId: string): Promise>; /** * Full variant list for an asset — preset, URL, dimensions, bytes. * Stronger-typed wrapper around `get()` that exposes only the * `variants` field with the proper `AssetVariant[]` shape. * * const v = await aq.assets.variants(logoId); * v.map((x) => x.preset); // → ("thumb" | "sm" | … | "original")[] */ variants(assetId: string): Promise; /** * Add or rebuild variants on an existing asset. Image presets are * MERGED with what's there — passing `{ presets: ["thumb"] }` adds * the thumb variant without touching `lg`, `sm`, `original`, etc. * * // Day 0: upload original-only logo * const { assetId } = await aq.upload(logoFile); // defaults to ["original"] * * // Day 7: need a thumb without re-uploading * await aq.assets.regenerate(assetId, { presets: ["thumb"] }); * * const after = await aq.assets.variants(assetId); * after.map((v) => v.preset); // → ["original", "thumb"] * * Passing no presets re-runs the FULL default pipeline for that * asset's kind (thumb+sm+md+lg for images, poster+video for video). * * If the asset was uploaded original-only and the cleanup job has * already reaped `raw/`, the route falls back to reading the source * bytes from `variants/o.` — no need to re-upload. * * Video presets are filtered to `["poster","video","aiproxy"]` and * dispatched to the Cloud Run Job (the call returns immediately * with a dispatch handle; poll `aq.assets.get(id).status` for * completion). */ regenerate(assetId: string, opts?: { presets?: VariantPreset[]; }): Promise; /** Merge metadata into an asset (role / slot / description / tags). */ patchMetadata(assetId: string, metadata: Record): Promise<{ ok: true; metadata: Record; }>; /** * Request a presigned R2 PUT URL for direct browser-side uploads. * * Mirrors the first half of `aq.upload()` — the caller (typically a * BFF / share-link dropzone) computes sha256 in the browser, then * uploads bytes straight to R2 with the returned `upload.url`, then * POSTs `process.body` to `/assets/process` (see {@link processAndWait}) * once R2 has the bytes. * * If the sha is already known to the tenant the server short-circuits * with `{ deduped: true, asset }` — no PUT needed. */ presignUploadUrl(opts: PresignUploadUrlOptions): Promise; /** * Dispatch `/assets/process` with the body returned by a prior * {@link presignUploadUrl} call, then poll until the asset transitions * to `ready` or `failed`. Throws on `failed` or timeout. * * Use this when bytes were uploaded directly from the browser to R2 — * `aq.upload()` already does presign + PUT + process + wait in one * step when the server holds the bytes. */ processAndWait(processBody: Record, opts?: { timeoutMs?: number; }): Promise; /** * Poll `GET /assets/:id` until the asset transitions to `ready` or * `failed`. Returns the final DTO (whether ready OR failed — callers * decide whether to throw on `failed`). Throws on timeout. * * Default timeout is 5 minutes; videos / HLS ladders may need a * higher cap (pass `10 * 60_000` for compositions, transcodes). */ waitReady(assetId: string, timeoutMs?: number): Promise; /** * Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip * segments into a single MP4 composition. Returns the processing asset * id immediately — does NOT block on completion. Callers poll via * {@link waitReady} (typical timeout: 10 min for multi-segment kits). * * Tenant scope is inherited from the SDK client; `tenantCode` is added * to the request body so the Cloud Run Job can resolve it without * re-reading the header. */ composeMarketing(opts: ComposeMarketingOptions): Promise; private headers; } declare function mimeFromFileName(fileName: string | undefined): string | null; /** * Subset of compressorjs options exposed through the SDK. Re-imported here * to avoid a hard import dependency on `./web` from this top-level module * (the /web subpath uses browser-only APIs). The runtime `compress` * implementation is lazy-loaded so Node/Bun callers don't pay the bundle * cost — see `aq.upload` below. */ type CompressOptions = { quality?: number; mimeType?: "image/jpeg" | "image/webp"; maxWidth?: number; maxHeight?: number; convertSize?: number; strict?: boolean; keepOriginalDimensions?: boolean; convertHeic?: boolean; onProgress?: (stage: "convertingHeic" | "compressing" | "compressingKeepingDimensions") => void; }; type UploadOptions = { fileName?: string; /** * MIME type of the bytes. **Only needed for a `Uint8Array` input** — a `File`/`Blob` * already carries its `.type`. Raw bytes have no inherent MIME, so without this (and * without an extension on `fileName` to infer from) they upload as * `application/octet-stream`, which the asset-manager classifies as `kind:"other"` — * meaning NO image/video variants are generated and `regenerate()` is unsupported. * Resolution order for the effective MIME: `Blob.type` → `contentType` → * inferred from `fileName`'s extension → `application/octet-stream`. * * aq.upload(bytes, { fileName: "cover.webp" }) // inferred → image/webp ✓ * aq.upload(bytes, { contentType: "image/webp" }) // explicit ✓ * aq.upload(bytes) // octet-stream → kind:"other" ⚠ */ contentType?: string; /** Computed sha256 of bytes. Skip to compute locally with WebCrypto (browser only). */ sha256?: string; /** * Client-side compression before upload. Saves user bandwidth — typical * 5–10× reduction for raw phone photos. Browser-only; in Node/Bun this * silently no-ops with a console.warn and the raw bytes upload as-is. * * - `true` → use SDK `DEFAULT_COMPRESSION_OPTIONS` (webapp-tuned) * - `CompressOptions` → merge over defaults * - `false` / omit → no compression (current default behavior) * * Implementation is lazy-imported from `@aquienpz/sdk/web` so callers * that never set `compress` don't pay the compressorjs + heic2any * bundle cost. Skipped for non-image MIMEs (video, PDF) regardless of * this option — those go to the upload pipeline raw. * * @see {@link CompressOptions} * @see https://github.com/espaciofuturoio/aquienpz/tree/main/packages/sdk#client-side-compression-browsers */ compress?: boolean | CompressOptions; /** * Variant set to generate. **Defaults to `["original"]`** — * if you omit this option, only the raw bytes land on the CDN * under the `o` path. Pass an explicit array to request more. * * Image presets (`thumb` 256 · `sm` 640 · `md` 1280 · `lg` 1920 · * `xl` 3840 · `original`): * - `["original"]` (default) → just the raw bytes. Right call for * logos / SVGs / anything you'll resize browser-side or via * `aq.assets.regenerate(id, { presets: ["thumb"] })` later. * - `["thumb","sm","md","lg"]` → the classic responsive ladder. * - `["thumb","sm","md","lg","xl"]` → add 4K. * * Video presets (`poster`, `video`, `aiproxy`): omit `aiproxy` if * the tenant doesn't need the low-res transcode for AI captioning. * * No upscaling. Each size preset is a **ceiling**; a 1080×720 source * asked for `xl` (3840) yields a 1080×720 xl variant, not a stretched * 3840-wide image. * * Idempotent: you can always add missing variants later via * `aq.assets.regenerate(id, { presets: [...] })`. The platform * stores the source so regeneration doesn't require re-uploading. */ presets?: VariantPreset[]; /** * Max time to wait for the asset to transition to `ready` (or `failed`) * after dispatch. Default `5 * 60_000` (5 min). Bump higher for large * videos / HLS transcodes — aquienpz processing time scales with input * size and per-instance CPU. * * Throws `Error("waitReady timeout for ")` if the deadline passes * without the asset transitioning. The asset row stays in aquienpz * (status="processing") and the next byHash lookup will return it once * processing completes; the caller can resume with their own poll. */ timeoutMs?: number; /** * VIDEO-only delivery knobs forwarded into `/assets/process`. See * {@link UploadVideoOptions}. Ignored for non-video uploads. * * // A delivery-ready reel: skip the unused HLS ladder + skip re-encode. * await aq.upload(mp4Bytes, { * fileName: "reel.mp4", * presets: ["poster", "video"], * video: { hls: false, passthrough: true }, * }); */ video?: UploadVideoOptions; }; type UploadResult = { assetId: string; sha256: string; cdnUrl: string; }; type SlotHistoryEntry = { id: string; action: "bind" | "unbind" | "restore"; preset: VariantPreset | null; description: string | null; updatedAt: string; updatedBy: string | null; assetId: string | null; /** Resolved DTO when the asset still exists; `null` after delete / 404. */ asset: AssetDTO | null; }; type UsageSnapshot = { tenant: { id: number; code: string; }; storage: { totalBytes: number; assetCount: number; }; today: UsageWindow; last30Days: UsageWindow; }; type UsageWindow = { reads: number; writes: number; lists: number; deletes: number; admins: number; upscales: number; processes: number; bytesIn: number; }; type UsageDailyPoint = { date: string; reads: number; writes: number; lists: number; deletes: number; processes: number; bytesIn: number; bytesStored: number; }; type UsagePerKey = { apiKeyId: string; prefix: string | null; name: string | null; opsTotal: number; bytesTotal: number; lastSeen: string; }; declare class UsageApi { private readonly opts; constructor(opts: AquienpzClientOptions); /** Snapshot for the active tenant — storage + today + last 30 days totals. */ snapshot(): Promise; /** Daily rollup for charts — 1..365 days, default 30. */ timeseries(days?: number): Promise<{ tenant: { id: number; code: string; }; days: UsageDailyPoint[]; }>; /** Per-API-key breakdown for the current month. */ keys(): Promise<{ tenant: { id: number; code: string; }; monthStart: string; keys: UsagePerKey[]; }>; private headers; } declare class AquienpzClient { readonly slots: SlotsApi; readonly assets: AssetsApi; readonly usage: UsageApi; /** * Effective options — read-only. Exposed so the `/web` and `/expo` * subpaths can inherit endpoint / apiKey / tenant scope from the * configured client without re-passing them per call site. */ readonly opts: AquienpzClientOptions; constructor(opts: AquienpzClientOptions); /** Tenant id as base36 path segment (e.g. tenantId=4 → "4/v/"). */ get tenantSegment(): string; /** Build the canonical CDN URL deterministically from sha + preset. */ urlFor(asset: Pick, preset?: VariantPreset): string; /** Build a responsive srcSet across the available image presets. */ srcSetFor(asset: Pick): string; /** * Build an on-the-fly transform URL — `/t//.`. * * ## URL CONVENTION — transforms are NOT tenant-prefixed (variants are) * Two distinct delivery paths, by design: * - **Variants / presets** (`urlFor`, `srcSetFor`, upload `cdnUrl`): * `//v/-.` ← tenant-scoped (e.g. `/4/v/-lg.webp`) * - **On-the-fly transforms** (`transform`, `transformSrcSet`): * `/t//.` ← GLOBAL, no tenant segment (`/t/...`) * The transform service is content-addressed by sha + resizes from the source on demand, * so it needs no tenant in the path. Prefixing a transform URL with `//t/...` 404s. * Consumers that build URLs by hand must NOT add the tenant segment to `/t/` URLs. * * Returns the canonical `lg` variant URL when called with empty options, * so callers can swap `urlFor()` for `transform()` without thinking. * * URLs with the same params in different order produce the same R2 * cache entry (the server canonicalizes both sides). Safe to use as * stable cache keys. * * * * @see {@link TransformOptions} for the full param matrix. */ transform(asset: Pick, opts?: TransformOptions): string; transform(asset: Pick, opts: SignedTransformOptions, signOpts: { sign: true; }): Promise; /** * Build a responsive `srcSet` string. One transform URL per width; all * other options apply to every URL. * * Pass `{ sign: true }` to return signed URLs (async). Without it, the * call stays synchronous as before. */ transformSrcSet(asset: Pick, widths: number[], extraOpts?: Omit): string; transformSrcSet(asset: Pick, widths: number[], extraOpts: Omit, signOpts: { sign: true; }): Promise; /** * Build an on-the-fly VIDEO transform URL — Phase 4. * * Same DSL shape as `transform()` but the URL has a `.mp4` (default) * or `.webm` extension and the server routes the request to a Cloud * Run Job for ffmpeg encoding (vs the inline sharp pipeline for * images). * * On the first request the route returns **202 Accepted** with * `Retry-After: 10` while the Job runs (typically 5-30 s for a * short clip). The response body includes `outputUrl` which is the * eventual CDN URL — poll the same transform URL after the * retry-after window to get a 302 redirect to it. * * const url = aq.transformVideo(asset, { * width: 1080, height: 1920, fit: "cover", * start: 0, duration: 15, * }); * // Pass to Video.js /