@nitida/sdk 0.27.0 → 0.27.1
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/AGENTS.md +16 -7
- package/dist/index.d.ts +17 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +5 -0
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +16 -7
- package/src/index.ts +20 -0
- package/src/server/index.ts +1 -0
- package/src/web/index.ts +1 -0
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/audio-compat.ts","../src/server/index.ts"],"sourcesContent":["/**\n * @nitida/sdk — universal client for the aquienpz multi-tenant asset\n * platform.\n *\n * One ergonomic facade over the underlying packages\n * (`@nitida/asset-client` URL builders + `@aquienpz/asset-uploader-web`\n * + the slot resolver). Auth is a bearer API key (`amk_rt_*`), issued\n * per tenant when the tenant is created; tenant scope comes from\n * the key's metadata (`X-Tenant-Code` is log-only).\n *\n * Usage:\n *\n * import { NitidaClient } from \"@nitida/sdk\";\n *\n * const aq = new NitidaClient({\n * endpoint: \"https://api.nitida.gofuture.space\",\n * apiKey: process.env.AQUIENPZ_API_KEY!, // amk_rt_* — server-only\n * tenantCode: \"acme-co\",\n * cdnBase: \"https://8ok.uk\", // optional override\n * tenantId: 4, // required for tenant-prefixed URLs\n * });\n *\n * // Slot system (recommended — admin can rebind without redeploys).\n * const hero = await aq.slots.resolve(\"storefront.cr.hero\");\n * const set = await aq.slots.resolveMany([\"a\", \"b\", \"c\"]);\n *\n * // Lower-level asset operations.\n * const asset = await aq.assets.byHash(sha);\n * const list = await aq.assets.list({ limit: 50 });\n *\n * // Upload bytes / files.\n * const up = await aq.upload(file, { fileName: \"cover.jpg\" });\n *\n * For React, see `@nitida/sdk/react` (useSlot, useSlots, useNitidaClient).\n * @module @nitida/sdk\n */\n\nimport {\n type AssetDTO,\n type AssetVariant,\n configureSlotResolver,\n getAssetSrcSet,\n getAssetUrl,\n getHlsStreamingUrl,\n getSignedTransformUrl,\n getTransformSrcSet,\n getTransformUrl,\n getVideoTransformUrl,\n hasPreset,\n invalidateSlotCache,\n type RequestablePreset,\n type ResolveSlotOptions,\n resolveSlot,\n resolveSlots,\n type SignedTransformOptions,\n type SlotDTO,\n type SlotResolution,\n setCdnBase,\n setTenantId,\n type TransformOptions,\n type VariantEntryPreset,\n type VariantPreset,\n} from \"@nitida/asset-client\";\nimport { isUniversallyPlayableAudio } from \"./audio-compat\";\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\n/**\n * Permissive constructor options for the root `NitidaClient`.\n *\n * App code should NOT import this type directly — prefer the strict\n * variants from the subpaths:\n *\n * - `WebClientOptions` from `@nitida/sdk/web` (no `apiKey`)\n * - `ServerClientOptions` from `@nitida/sdk/server` (`apiKey` required)\n *\n * This root type is the union both modes resolve to; the underlying class\n * accepts both shapes so subpath wrappers can extend without duplication.\n */\nexport type NitidaClientOptions = {\n /**\n * Base URL of the nitida API (e.g. `https://api.nitida.gofuture.space`).\n *\n * May be relative (e.g. `/api/am`) ONLY in browser contexts where the\n * SDK resolves it against `window.location.origin`. Node/Bun consumers\n * must always pass an absolute URL.\n */\n endpoint: string;\n /**\n * Bearer API key with the `amk_rt_*` prefix.\n *\n * **Server-only.** Omit when constructing from `@nitida/sdk/web` —\n * your BFF / route handler injects the bearer header in proxy mode.\n */\n apiKey?: string;\n /**\n * Extra headers merged into every request. The documented way for\n * mobile/Expo clients to authenticate a BFF that gates on the Better Auth\n * session: they can't send cookies automatically, so they pass\n * `{ Cookie: authClient.getCookie() }` here (see Better Auth Expo docs,\n * \"Making Authenticated Requests to Your Server\"). Web/server consumers omit\n * this — browsers attach the same-origin cookie and servers pass `apiKey`.\n */\n headers?: Record<string, string>;\n /** Tenant code — sent as `X-Tenant-Code` (log-only). Authoritative scope is the key's metadata.tenantId. */\n tenantCode: string;\n /** Numeric tenant id — used to build tenant-prefixed CDN URLs `<cdn>/<tenantId b36>/v/<sha>-<preset>.<ext>`. */\n tenantId: number;\n /** Override the public CDN base. Defaults to `https://8ok.uk`. */\n cdnBase?: string;\n /**\n * Tenant's HMAC signing key for transform URLs (Phase 3). Required\n * only when calling `aq.transform(asset, opts, { sign: true })`.\n *\n * 32 random bytes, generated server-side on tenant creation; fetch\n * via `POST /admin/projects/:code/rotate-signing-key`, which needs a\n * SYSTEM-scope credential the platform operator holds — your own admin key\n * answers `403 SYSTEM_KEY_REQUIRED`. Ask for it. **Keep it\n * server-side only** — do not ship in `NEXT_PUBLIC_*` env vars. Sign\n * URLs from a BFF route handler, or pre-sign at build time.\n */\n signingKey?: string;\n};\n\n// ---------------------------------------------------------------------------\n// URL construction\n// ---------------------------------------------------------------------------\n\n/**\n * Build a fully-qualified URL for an aquienpz endpoint path.\n *\n * Accepts both absolute endpoints (`https://api.nitida.gofuture.space`) and\n * relative ones (`/api/am`) — the latter only works in browser contexts\n * (resolved against `window.location.origin`). Node/Bun throws a clear\n * error if a relative endpoint is configured.\n *\n * The native `URL` constructor throws on relative inputs, so every fetch\n * site in the SDK must go through this helper instead of `new URL(...)`.\n */\nfunction endpointUrl(\n opts: Pick<NitidaClientOptions, \"endpoint\">,\n path: string,\n searchParams?: Record<string, string | number | boolean | undefined>,\n): URL {\n const endpoint = opts.endpoint.replace(/\\/+$/, \"\");\n const isAbsolute = /^https?:\\/\\//i.test(endpoint);\n let base: string;\n if (isAbsolute) {\n base = `${endpoint}${path}`;\n } else if (typeof window !== \"undefined\" && window.location?.origin) {\n base = `${window.location.origin}${endpoint}${path}`;\n } else {\n throw new Error(\n `[@nitida/sdk] relative endpoint \"${endpoint}\" requires a browser; ` +\n \"pass an absolute URL when using the SDK from Node/Bun.\",\n );\n }\n const u = new URL(base);\n if (searchParams) {\n for (const [k, v] of Object.entries(searchParams)) {\n if (v !== undefined && v !== null && v !== \"\") {\n u.searchParams.set(k, String(v));\n }\n }\n }\n return u;\n}\n\n/** Build a request URL as a plain string (no search params). */\nfunction endpointHref(\n opts: Pick<NitidaClientOptions, \"endpoint\">,\n path: string,\n): string {\n return endpointUrl(opts, path).toString();\n}\n\n/**\n * Auth headers — conditionally includes `Authorization` only when an\n * `apiKey` is present. In BFF-proxy mode (browser via `/web`) the\n * proxy injects the real bearer header, so we omit it here.\n */\nfunction authHeaders(opts: NitidaClientOptions): Record<string, string> {\n const h: Record<string, string> = {\n // Caller-supplied headers first; `X-Tenant-Code` stays authoritative below.\n ...opts.headers,\n \"X-Tenant-Code\": opts.tenantCode,\n };\n if (opts.apiKey) h.Authorization = `Bearer ${opts.apiKey}`;\n return h;\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports (so consumers don't double-import from asset-client)\n// ---------------------------------------------------------------------------\n\n// This barrel is COMPLETE on purpose: every public symbol of\n// `@nitida/asset-client` is reachable from `@nitida/sdk`, so a consumer never\n// has to know which of the two packages a helper happens to live in. It had\n// drifted to 30 of 53 — the gap silently included `TRANSFORM_WIDTHS` and\n// `TransformWidth`, which both READMEs tell you to import precisely so an\n// off-ladder width fails at compile time instead of returning 400, and the\n// palette helpers the README imports from here by name.\nexport type {\n AssetDTO,\n AssetPalette,\n AssetVariant,\n HlsRung,\n PaletteSwatch,\n RequestablePreset,\n ResolveSlotOptions,\n SignAccessOptions,\n SignedTransformOptions,\n SlotDTO,\n SlotResolution,\n TransformEffect,\n TransformFit,\n TransformFormat,\n TransformGravity,\n TransformOptions,\n TransformWidth,\n VariantEntryPreset,\n VariantPreset,\n VisibilityHint,\n} from \"@nitida/asset-client\";\nexport {\n accessMessage,\n assertPublic,\n bestTextContrast,\n computeVariantDimensions,\n configureSlotResolver,\n contrastRatio,\n deriveAccessKey,\n extractAssetSha,\n getAmbientGradient,\n getAssetDimensions,\n getAssetSrcSet,\n getAssetUrl,\n getCdnBase,\n getHlsLadder,\n getHlsStreamingUrl,\n getPaletteBlurBackground,\n getPaletteCssVars,\n getPrivateAssetUrl,\n getPrivateTransformUrl,\n getSignedTransformUrl,\n getTenantId,\n getTextColorForBackground,\n getTransformSrcSet,\n getTransformUrl,\n getVideoTransformUrl,\n hasPreset,\n hlsLadderAlignment,\n invalidateSlotCache,\n iteratePaletteSwatches,\n PRESET_EXT,\n PRESET_LONG,\n PRESET_MAX_DIM,\n PRESET_SHORT,\n pickAmbientBackground,\n relativeLuminance,\n resolveSlot,\n resolveSlots,\n serializeTransform,\n setCdnBase,\n setTenantId,\n signAccessUrl,\n signTransformUrl,\n TRANSFORM_WIDTHS,\n} from \"@nitida/asset-client\";\n\n// ---------------------------------------------------------------------------\n// Sub-namespaces\n// ---------------------------------------------------------------------------\n\nclass SlotsApi {\n constructor(private readonly opts: NitidaClientOptions) {}\n\n /** Resolve one slot — returns `{slot, preset, url}` or `{slot: null, url: null}` when unbound. */\n resolve(\n slotKey: string,\n options: ResolveSlotOptions = {},\n ): Promise<SlotResolution> {\n return resolveSlot(slotKey, options);\n }\n\n /** Bulk-resolve N slots in one HTTP round-trip. */\n resolveMany(\n slotKeys: string[],\n options: ResolveSlotOptions = {},\n ): Promise<Record<string, SlotResolution>> {\n return resolveSlots(slotKeys, options);\n }\n\n /** List slots for the tenant (admin). Optional prefix filter for tree views. */\n async list(\n opts: { prefix?: string; limit?: number } = {},\n ): Promise<SlotDTO[]> {\n const u = endpointUrl(this.opts, \"/slots\", {\n prefix: opts.prefix,\n limit: opts.limit,\n });\n const r = await fetch(u, { headers: this.headers() });\n if (!r.ok) throw new Error(`slots list ${r.status}: ${await r.text()}`);\n const body = (await r.json()) as { slots: SlotDTO[] };\n return body.slots;\n }\n\n /** Bind / rebind a slot to an asset. Admin-only operation. */\n async bind(\n slotKey: string,\n body: {\n assetId: string;\n preset?: VariantPreset;\n description?: string;\n updatedBy?: string;\n },\n ): Promise<{ ok: true; slotKey: string; assetId: string }> {\n const r = await fetch(\n endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),\n {\n method: \"PUT\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n },\n );\n if (!r.ok) throw new Error(`slot bind ${r.status}: ${await r.text()}`);\n return (await r.json()) as { ok: true; slotKey: string; assetId: string };\n }\n\n /**\n * Recent bindings for a slot. Lets the admin audit who changed\n * what and restore a previous binding without remembering the\n * asset id. Default limit 20, max 100.\n */\n async history(\n slotKey: string,\n opts: { limit?: number } = {},\n ): Promise<SlotHistoryEntry[]> {\n const u = endpointUrl(\n this.opts,\n `/slots/${encodeURIComponent(slotKey)}/history`,\n { limit: opts.limit },\n );\n const r = await fetch(u, { headers: this.headers() });\n if (!r.ok) throw new Error(`slots history ${r.status}: ${await r.text()}`);\n const body = (await r.json()) as { history: SlotHistoryEntry[] };\n return body.history;\n }\n\n /**\n * Restore the slot to a previous binding. Equivalent to\n * `bind(key, { assetId: previous.assetId, action: \"restore\" })`\n * — the audit row is tagged `restore` instead of `bind`.\n */\n async restore(\n slotKey: string,\n args: { assetId: string; preset?: VariantPreset; updatedBy?: string },\n ): Promise<{ ok: true; slotKey: string; assetId: string }> {\n const r = await fetch(\n endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),\n {\n method: \"PUT\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ ...args, action: \"restore\" }),\n },\n );\n if (!r.ok) throw new Error(`slot restore ${r.status}: ${await r.text()}`);\n return (await r.json()) as { ok: true; slotKey: string; assetId: string };\n }\n\n /** Remove a slot binding. The asset itself is left alone. */\n async unbind(slotKey: string): Promise<{ ok: true; removed: number }> {\n const r = await fetch(\n endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),\n {\n method: \"DELETE\",\n headers: this.headers(),\n },\n );\n if (!r.ok) throw new Error(`slot unbind ${r.status}: ${await r.text()}`);\n return (await r.json()) as { ok: true; removed: number };\n }\n\n /** Invalidate the in-process cache after a slot rebind. */\n invalidateCache(slotKey?: string): void {\n invalidateSlotCache(slotKey);\n }\n\n private headers(): Record<string, string> {\n return authHeaders(this.opts);\n }\n}\n\n/**\n * Returned by `aq.assets.regenerate(...)`. The shape varies by kind —\n * images return immediately with the merged variant list; videos\n * return a dispatch handle (the actual transcode runs in a background\n * job and finishes async).\n */\nexport type RegenerateResult =\n | {\n ok: true;\n kind: \"image\";\n /** Full variant set after the merge. */\n variants: AssetVariant[];\n /** Presets newly written this run. Useful for showing \"added X\". */\n newVariants: VariantPreset[];\n /**\n * Which source the server read to derive the new variants:\n * - `\"original\"` / `\"raw\"` → lossless source bytes (best)\n * - `\"xl\"` / `\"lg\"` / `\"md\"` / `\"sm\"` / `\"thumb\"` → a previously\n * encoded WebP variant was used as the source. Output is\n * re-encoded WebP — fine for thumb/sm from lg, lossier when\n * working from already-small sources.\n *\n * The no-upscale clamp still applies: deriving `lg` (1920) from\n * a 640 `sm` source produces a 640-side `lg` variant, not a\n * stretched 1920.\n */\n sourceUsed: VariantPreset | \"raw\";\n }\n | {\n ok: true;\n kind: \"video\";\n dispatch: unknown;\n regenerated: string[] | \"default\";\n };\n\n/**\n * Wire shape returned by `POST /assets/upload-url`. Either the server\n * resolves the upload synchronously via dedup (`deduped: true` + existing\n * asset DTO) or it returns a presigned storage PUT URL plus a `process` payload\n * the caller must POST to `/assets/process` after the PUT lands.\n */\nexport type UploadUrlResult =\n | { deduped: true; asset: AssetDTO }\n | {\n deduped: false;\n upload: { url: string; headers?: Record<string, string> };\n process: { url: string; body: Record<string, unknown> };\n };\n\n/**\n * VIDEO-only delivery knobs threaded into `/assets/process`. Ignored for\n * image / audio / other uploads. Both fields default to today's behavior when\n * omitted, so existing callers are unaffected.\n */\nexport type UploadVideoOptions = {\n /**\n * `false` → skip the auto-dispatched HLS adaptive ladder (240p–2160p). Use\n * for download-only assets served as a progressive `-v.mp4` and never\n * streamed (e.g. share-video reels) — it avoids a second background job no\n * one watches. Default/absent → the ladder is generated as before.\n */\n hls?: boolean;\n /**\n * `true` → when the uploaded MP4 is ALREADY web-safe (H.264 + yuv420p),\n * re-mux the `video` variant instead of re-encoding it. Use for\n * delivery-ready uploads (the bytes are already H.264 High / yuv420p /\n * +faststart / capped bitrate) to skip a wasteful re-encode + generational\n * quality loss. Falls back to a full re-encode automatically when the source\n * is not web-safe. Default/absent → unconditional re-encode (today's path).\n */\n passthrough?: boolean;\n};\n\n/** Input shape accepted by `aq.assets.presignUploadUrl(...)`. */\nexport type PresignUploadUrlOptions = {\n /** Full sha256 (64 hex) of the bytes that will be PUT to storage. */\n sha256: string;\n /** MIME type of the bytes (e.g. `image/jpeg`, `video/mp4`). */\n mime: string;\n /** Byte length of the upload payload. */\n bytes: number;\n /** Suggested file name; surfaces in admin dashboards + extension fallback. */\n fileName: string;\n /**\n * Variant ladder to generate after `/assets/process`. Defaults to\n * `[\"original\"]` server-side when omitted — same contract as `aq.upload`.\n *\n * {@link RequestablePreset}, not {@link VariantPreset}: `hls` and `mp3` are\n * things a variant can BE, never things you can ask for, and asking is a 400.\n */\n presets?: RequestablePreset[];\n /**\n * Pre-compression size of the source (useful when the browser ran\n * compressorjs / heic2any before computing `bytes`). Recorded\n * server-side, so the savings show up in the admin usage dashboards.\n */\n clientOriginalBytes?: number;\n /** VIDEO-only delivery knobs forwarded into `/assets/process`. See {@link UploadVideoOptions}. */\n video?: UploadVideoOptions;\n};\n\n/** Input shape accepted by `aq.assets.composeMarketing(...)`. */\nexport type ComposeMarketingSegment = {\n /** Public URL of the source clip (typically a `/t/.../video.mp4` transform). */\n sourceUrl: string;\n /** Optional clip duration in seconds (cap for that segment). */\n durationSec?: number;\n};\n\nexport type ComposeMarketingComposition = {\n /** Transition between consecutive segments. Default `\"cut\"`. */\n transition?: \"cut\" | \"fade\";\n /** Optional audio track to mix on top of the final composition. */\n audioTrack?: { url: string };\n /** Final composition length in seconds (server may clamp). */\n finalDurationSec?: number;\n};\n\nexport type ComposeMarketingOptions = {\n /** Marketing-kit id this composition belongs to (server uses it for naming + dedup). */\n marketingKitId: string;\n /** Ordered clip segments to stitch. */\n segments: ComposeMarketingSegment[];\n /** Optional composition-level knobs (transitions, audio, duration). */\n composition?: ComposeMarketingComposition;\n};\n\nexport type ComposeMarketingResult = {\n /** Aquienpz asset id of the in-flight composition. Poll `aq.assets.waitReady(id)`. */\n assetId: string;\n /** Asset status at dispatch time — usually `\"processing\"`. */\n status: \"processing\" | \"ready\" | \"failed\";\n};\n\nclass AssetsApi {\n constructor(private readonly opts: NitidaClientOptions) {}\n\n /** Look up an asset by full sha256 (64 hex). Returns null on 404. */\n async byHash(sha256: string): Promise<AssetDTO | null> {\n const r = await fetch(\n endpointHref(this.opts, `/assets/by-hash/${sha256}`),\n { headers: this.headers() },\n );\n if (r.status === 404) return null;\n if (!r.ok) throw new Error(`assets byHash ${r.status}: ${await r.text()}`);\n return (await r.json()) as AssetDTO;\n }\n\n /** Bulk lookup by sha256s. */\n async byHashes(\n hashes: string[],\n ): Promise<{ existing: AssetDTO[]; missing: string[] }> {\n const r = await fetch(endpointHref(this.opts, \"/assets/by-hashes\"), {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ hashes }),\n });\n if (!r.ok)\n throw new Error(`assets byHashes ${r.status}: ${await r.text()}`);\n return (await r.json()) as { existing: AssetDTO[]; missing: string[] };\n }\n\n /** Paginated list of recent assets for the tenant. */\n async list(\n opts: { limit?: number; cursor?: string; includeDeleted?: boolean } = {},\n ): Promise<{\n assets: AssetDTO[];\n nextCursor: string | null;\n }> {\n const u = endpointUrl(this.opts, \"/assets\", {\n limit: opts.limit,\n cursor: opts.cursor,\n include_deleted: opts.includeDeleted ? \"true\" : undefined,\n });\n const r = await fetch(u, { headers: this.headers() });\n if (!r.ok) throw new Error(`assets list ${r.status}: ${await r.text()}`);\n return (await r.json()) as {\n assets: AssetDTO[];\n nextCursor: string | null;\n };\n }\n\n /** Full DTO for an asset (admin view — includes audit-only fields). */\n async get(assetId: string): Promise<AssetDTO & Record<string, unknown>> {\n const r = await fetch(endpointHref(this.opts, `/assets/${assetId}`), {\n headers: this.headers(),\n });\n if (!r.ok) throw new Error(`asset get ${r.status}: ${await r.text()}`);\n return (await r.json()) as AssetDTO & Record<string, unknown>;\n }\n\n /**\n * Slot bindings pointing at an asset. Use this before deleting an\n * asset so the admin sees which storefront slots would suddenly\n * resolve to nothing.\n */\n async bindings(assetId: string): Promise<\n Array<{\n slotKey: string;\n preset: VariantPreset | null;\n description: string | null;\n updatedAt: string;\n updatedBy: string | null;\n }>\n > {\n const r = await fetch(endpointHref(this.opts, `/assets/${assetId}/slots`), {\n headers: this.headers(),\n });\n if (!r.ok) throw new Error(`asset bindings ${r.status}: ${await r.text()}`);\n const body = (await r.json()) as {\n slots: Array<{\n slotKey: string;\n preset: VariantPreset | null;\n description: string | null;\n updatedAt: string;\n updatedBy: string | null;\n }>;\n };\n return body.slots;\n }\n\n /**\n * Full variant list for an asset — preset, URL, dimensions, bytes.\n * Stronger-typed wrapper around `get()` that exposes only the\n * `variants` field with the proper `AssetVariant[]` shape.\n *\n * const v = await aq.assets.variants(logoId);\n * v.map((x) => x.preset); // → (\"thumb\" | \"sm\" | … | \"original\")[]\n *\n * ⚠️ Returns `[]` — not an error — against a server older than the\n * 2026-08-17 release, which never sent the field at all. An empty\n * array is therefore \"no variants OR old server\". For a plain existence\n * check prefer `hasPreset(dto, preset)` on `dto.presets`, which every server\n * version sends; use this when you need the URLs and sizes.\n */\n async variants(assetId: string): Promise<AssetVariant[]> {\n const dto = await this.get(assetId);\n return Array.isArray((dto as { variants?: unknown }).variants)\n ? (dto as { variants: AssetVariant[] }).variants\n : [];\n }\n\n /**\n * Add or rebuild variants on an existing asset. Presets are MERGED\n * with what's there, for images AND for video — passing\n * `{ presets: [\"thumb\"] }` adds the thumb variant without touching\n * `lg`, `sm`, `original`, etc.\n *\n * // Day 0: upload original-only logo\n * const { assetId } = await aq.upload(logoFile); // defaults to [\"original\"]\n *\n * // Day 7: need a thumb without re-uploading\n * await aq.assets.regenerate(assetId, { presets: [\"thumb\"] });\n *\n * const after = await aq.assets.variants(assetId);\n * after.map((v) => v.preset); // → [\"original\", \"thumb\"]\n *\n * Passing no presets re-runs the FULL default pipeline for that\n * asset's kind (thumb+sm+md+lg for images, poster+video for video).\n *\n * If the asset was uploaded original-only and the ~24 h grace window\n * on the uploaded bytes has already closed, the route falls back to\n * reading the source bytes from the permanent `original` variant —\n * no need to re-upload.\n *\n * Video presets are filtered to `[\"poster\",\"video\",\"aiproxy\",\"probe\"]`\n * and dispatched to a background job (the call returns immediately\n * with a dispatch handle; poll `aq.assets.get(id).status` for\n * completion).\n *\n * ⚠️ Before 2026-08-22 the video path REPLACED the whole variant\n * registry instead of merging, so a partial regenerate silently\n * deregistered `poster`, `video` and — irrecoverably — `hls`, which\n * is not a {@link RequestablePreset} and therefore cannot be asked\n * for again. The objects kept serving from the CDN; only the\n * registry died. Fixed server-side; a client on an older server\n * still loses them.\n */\n async regenerate(\n assetId: string,\n opts: { presets?: RequestablePreset[] } = {},\n ): Promise<RegenerateResult> {\n const r = await fetch(\n endpointHref(this.opts, `/assets/${assetId}/regenerate`),\n {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(opts.presets ? { presets: opts.presets } : {}),\n },\n );\n if (!r.ok)\n throw new Error(`asset regenerate ${r.status}: ${await r.text()}`);\n return (await r.json()) as RegenerateResult;\n }\n\n /** Merge metadata into an asset (role / slot / description / tags). */\n async patchMetadata(\n assetId: string,\n metadata: Record<string, unknown>,\n ): Promise<{ ok: true; metadata: Record<string, unknown> }> {\n const r = await fetch(endpointHref(this.opts, `/assets/${assetId}`), {\n method: \"PATCH\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ metadata }),\n });\n if (!r.ok) throw new Error(`asset patch ${r.status}: ${await r.text()}`);\n return (await r.json()) as { ok: true; metadata: Record<string, unknown> };\n }\n\n /**\n * Request a presigned storage PUT URL for direct browser-side uploads.\n *\n * Mirrors the first half of `aq.upload()` — the caller (typically a\n * BFF / share-link dropzone) computes sha256 in the browser, then\n * uploads bytes straight to object storage with the returned `upload.url`,\n * then POSTs `process.body` to `/assets/process` (see {@link processAndWait})\n * once storage has the bytes.\n *\n * If the sha is already known to the tenant the server short-circuits\n * with `{ deduped: true, asset }` — no PUT needed.\n *\n * @example The browser-direct flow, in full\n * ```ts\n * // SERVER (holds the amk_rt_* key — never the browser):\n * const presign = await aq.assets.presignUploadUrl({ sha256, mime, bytes, fileName, presets });\n * if (presign.deduped) return presign.asset; // those bytes already exist; none fly\n *\n * // BROWSER: PUT straight to presign.upload.url — the bytes never touch your server.\n * // ⚠️ The STORAGE BUCKET answers that preflight itself, so your origin must be in its CORS\n * // policy. Symptom when it is not: \"PUT failed: network error\" with every earlier step\n * // green — and it cannot be fixed in this SDK, in your app, or by the API's allowed origins.\n *\n * // SERVER again, forwarding presign.process.body VERBATIM:\n * const asset = await aq.assets.processAndWait(presign.process.body, { timeoutMs: 300_000 });\n * ```\n *\n * Works for images AND video. A video answers immediately with\n * `{ assetId, status: \"processing\" }` while a background job transcodes, so\n * give `processAndWait` a bigger `timeoutMs` (a transcode + HLS ladder runs\n * 1–2 min; 300_000 is a sane floor).\n */\n async presignUploadUrl(\n opts: PresignUploadUrlOptions,\n ): Promise<UploadUrlResult> {\n const body: Record<string, unknown> = {\n sha256: opts.sha256,\n mime: opts.mime,\n bytes: opts.bytes,\n fileName: opts.fileName,\n };\n if (opts.presets && opts.presets.length > 0) body.presets = opts.presets;\n if (opts.clientOriginalBytes != null)\n body.clientOriginalBytes = opts.clientOriginalBytes;\n if (opts.video != null) body.video = opts.video;\n\n const r = await fetch(endpointHref(this.opts, \"/assets/upload-url\"), {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n if (!r.ok) throw new Error(`upload-url ${r.status}: ${await r.text()}`);\n return (await r.json()) as UploadUrlResult;\n }\n\n /**\n * Dispatch `/assets/process` with the body returned by a prior\n * {@link presignUploadUrl} call, then poll until the asset transitions\n * to `ready` or `failed`. Throws on `failed` or timeout.\n *\n * Use this when bytes were uploaded directly from the browser to storage —\n * `aq.upload()` already does presign + PUT + process + wait in one\n * step when the server holds the bytes.\n */\n async processAndWait(\n processBody: Record<string, unknown>,\n opts: { timeoutMs?: number } = {},\n ): Promise<AssetDTO> {\n const r = await fetch(endpointHref(this.opts, \"/assets/process\"), {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(processBody),\n });\n if (!r.ok) throw new Error(`process ${r.status}: ${await r.text()}`);\n const proc = (await r.json()) as { assetId?: string };\n if (!proc.assetId) throw new Error(\"process returned no assetId\");\n const final = await this.waitReady(proc.assetId, opts.timeoutMs);\n if (final.status === \"failed\") {\n throw new Error(\n `processAndWait: asset ${proc.assetId} ended status=failed`,\n );\n }\n return final;\n }\n\n /**\n * Poll `GET /assets/:id` until the asset transitions to `ready` or\n * `failed`. Returns the final DTO (whether ready OR failed — callers\n * decide whether to throw on `failed`). Throws on timeout.\n *\n * Default timeout is 5 minutes; videos / HLS ladders may need a\n * higher cap (pass `10 * 60_000` for compositions, transcodes).\n */\n async waitReady(assetId: string, timeoutMs = 5 * 60_000): Promise<AssetDTO> {\n const start = Date.now();\n let delay = 500;\n let last: AssetDTO | null = null;\n while (Date.now() - start < timeoutMs) {\n last = (await this.get(assetId)) as AssetDTO;\n if (last.status === \"ready\" || last.status === \"failed\") return last;\n await new Promise((r) => setTimeout(r, delay));\n delay = Math.min(delay * 1.5, 5_000);\n }\n if (!last) throw new Error(`waitReady: no asset ${assetId}`);\n throw new Error(`waitReady timeout for ${assetId}`);\n }\n\n /**\n * Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip\n * segments into a single MP4 composition. Returns the processing asset\n * id immediately — does NOT block on completion. Callers poll via\n * {@link waitReady} (typical timeout: 10 min for multi-segment kits).\n *\n * Tenant scope is inherited from the SDK client; `tenantCode` is added\n * to the request body so the background job can resolve it without\n * re-reading the header.\n */\n async composeMarketing(\n opts: ComposeMarketingOptions,\n ): Promise<ComposeMarketingResult> {\n const r = await fetch(\n endpointHref(this.opts, \"/assets/compose-marketing\"),\n {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n tenantCode: this.opts.tenantCode,\n marketingKitId: opts.marketingKitId,\n segments: opts.segments,\n composition: opts.composition ?? {},\n }),\n },\n );\n if (!r.ok) {\n throw new Error(`compose-marketing ${r.status}: ${await r.text()}`);\n }\n const body = (await r.json()) as {\n asset?: { id: string; status: \"processing\" | \"ready\" | \"failed\" };\n assetId?: string;\n status?: \"processing\" | \"ready\" | \"failed\";\n };\n // Server returns `{ asset: { id, status }, dispatch? }`; some older\n // builds returned `{ assetId, status }` directly. Normalize both.\n const assetId = body.asset?.id ?? body.assetId;\n const status = body.asset?.status ?? body.status ?? \"processing\";\n if (!assetId) {\n throw new Error(\"compose-marketing: response missing assetId\");\n }\n return { assetId, status };\n }\n\n private headers(): Record<string, string> {\n return authHeaders(this.opts);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Upload helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Best-effort MIME from a file name's extension. Used as a fallback for `Uint8Array`\n * uploads (which carry no inherent type) so they still get classified correctly instead\n * of silently becoming `application/octet-stream` → `kind:\"other\"`. Returns `null` when\n * the extension is unknown.\n */\nconst MIME_BY_EXT: Record<string, string> = {\n webp: \"image/webp\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n heic: \"image/heic\",\n heif: \"image/heif\",\n svg: \"image/svg+xml\",\n bmp: \"image/bmp\",\n tiff: \"image/tiff\",\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n mov: \"video/quicktime\",\n m4v: \"video/x-m4v\",\n pdf: \"application/pdf\",\n};\nexport function mimeFromFileName(fileName: string | undefined): string | null {\n if (!fileName) return null;\n const ext = fileName.split(\".\").pop()?.toLowerCase();\n return ext ? (MIME_BY_EXT[ext] ?? null) : null;\n}\n\n/**\n * Subset of compressorjs options exposed through the SDK. Re-imported here\n * to avoid a hard import dependency on `./web` from this top-level module\n * (the /web subpath uses browser-only APIs). The runtime `compress`\n * implementation is lazy-loaded so Node/Bun callers don't pay the bundle\n * cost — see `aq.upload` below.\n */\nexport type CompressOptions = {\n quality?: number;\n mimeType?: \"image/jpeg\" | \"image/webp\";\n maxWidth?: number;\n maxHeight?: number;\n convertSize?: number;\n strict?: boolean;\n keepOriginalDimensions?: boolean;\n convertHeic?: boolean;\n onProgress?: (\n stage: \"convertingHeic\" | \"compressing\" | \"compressingKeepingDimensions\",\n ) => void;\n};\n\nexport type UploadOptions = {\n fileName?: string;\n /**\n * MIME type of the bytes. **Only needed for a `Uint8Array` input** — a `File`/`Blob`\n * already carries its `.type`. Raw bytes have no inherent MIME, so without this (and\n * without an extension on `fileName` to infer from) they upload as\n * `application/octet-stream`, which the server classifies as `kind:\"other\"` —\n * meaning NO image/video variants are generated and `regenerate()` is unsupported.\n * Resolution order for the effective MIME: `Blob.type` → `contentType` →\n * inferred from `fileName`'s extension → `application/octet-stream`.\n *\n * aq.upload(bytes, { fileName: \"cover.webp\" }) // inferred → image/webp ✓\n * aq.upload(bytes, { contentType: \"image/webp\" }) // explicit ✓\n * aq.upload(bytes) // octet-stream → kind:\"other\" ⚠\n */\n contentType?: string;\n /** Computed sha256 of bytes. Skip to compute locally with WebCrypto (browser only). */\n sha256?: string;\n /**\n * Client-side compression before upload. Saves user bandwidth — typical\n * 5–10× reduction for raw phone photos. Browser-only; in Node/Bun this\n * silently no-ops with a console.warn and the raw bytes upload as-is.\n *\n * - `true` → use SDK `DEFAULT_COMPRESSION_OPTIONS` (webapp-tuned)\n * - `CompressOptions` → merge over defaults\n * - `false` / omit → no compression (current default behavior)\n *\n * Implementation is lazy-imported from `@nitida/sdk/web` so callers\n * that never set `compress` don't pay the compressorjs + heic2any\n * bundle cost. Skipped for non-image MIMEs (video, PDF) regardless of\n * this option — those go to the upload pipeline raw.\n *\n * @see {@link CompressOptions}\n * @see https://nitida.gofuture.space/guides/advanced/ — client-side compression\n */\n compress?: boolean | CompressOptions;\n /**\n * Variant set to generate. **Defaults to `[\"original\"]`** —\n * if you omit this option, only the raw bytes land on the CDN\n * under the `o` path. Pass an explicit array to request more.\n *\n * Image presets (`thumb` 256 · `sm` 640 · `md` 1280 · `lg` 1920 ·\n * `xl` 3840 · `original`):\n * - `[\"original\"]` (default) → just the raw bytes. Right call for\n * logos / SVGs / anything you'll resize browser-side or via\n * `aq.assets.regenerate(id, { presets: [\"thumb\"] })` later.\n * - `[\"thumb\",\"sm\",\"md\",\"lg\"]` → the classic responsive ladder.\n * - `[\"thumb\",\"sm\",\"md\",\"lg\",\"xl\"]` → add 4K.\n *\n * Video presets (`poster`, `video`, `aiproxy`): omit `aiproxy` if\n * the tenant doesn't need the low-res transcode for AI captioning.\n *\n * No upscaling. Each size preset is a **ceiling**; a 1080×720 source\n * asked for `xl` (3840) yields a 1080×720 xl variant, not a stretched\n * 3840-wide image.\n *\n * Idempotent: you can always add missing variants later via\n * `aq.assets.regenerate(id, { presets: [...] })`. The platform\n * stores the source so regeneration doesn't require re-uploading.\n *\n * {@link RequestablePreset}, not {@link VariantPreset}. `hls` and `mp3` are\n * produced FOR you — the ladder when a video transcodes, the mp3 alongside\n * any audio original — and asking for either is a 400.\n */\n presets?: RequestablePreset[];\n /**\n * Max time to wait for the asset to transition to `ready` (or `failed`)\n * after dispatch. Default `5 * 60_000` (5 min). Bump higher for large\n * videos / HLS transcodes — processing time scales with input size,\n * and with how much other work the platform is doing at that moment.\n *\n * Throws `Error(\"waitReady timeout for <id>\")` if the deadline passes\n * without the asset transitioning. The asset row stays in aquienpz\n * (status=\"processing\") and the next byHash lookup will return it once\n * processing completes; the caller can resume with their own poll.\n */\n timeoutMs?: number;\n /**\n * VIDEO-only delivery knobs forwarded into `/assets/process`. See\n * {@link UploadVideoOptions}. Ignored for non-video uploads.\n *\n * // A delivery-ready reel: skip the unused HLS ladder + skip re-encode.\n * await aq.upload(mp4Bytes, {\n * fileName: \"reel.mp4\",\n * presets: [\"poster\", \"video\"],\n * video: { hls: false, passthrough: true },\n * });\n */\n video?: UploadVideoOptions;\n};\n\n/**\n * Default preset set the SDK sends to `/assets/upload-url` when the caller\n * omits `presets`.\n *\n * Typed `RequestablePreset[]`, not `VariantPreset[]` — it is a REQUEST. The\n * distinction caught this very line the moment it was introduced: it was the\n * wrong type here, and a `VariantPreset[]` default could have carried `hls`\n * into a request that answers 400.\n */\nconst DEFAULT_UPLOAD_PRESETS: RequestablePreset[] = [\"original\"];\n\nexport type UploadResult = {\n assetId: string;\n sha256: string;\n cdnUrl: string;\n};\n\nasync function computeSha256(\n bytes: ArrayBuffer | Uint8Array | Blob,\n): Promise<string> {\n const buf =\n bytes instanceof Blob\n ? await bytes.arrayBuffer()\n : bytes instanceof Uint8Array\n ? (bytes.buffer as ArrayBuffer)\n : bytes;\n const digest = await crypto.subtle.digest(\"SHA-256\", buf);\n return [...new Uint8Array(digest)]\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n// ---------------------------------------------------------------------------\n// Main facade\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Usage (Cloudinary-style consumption dashboard data)\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Slot history\n// ---------------------------------------------------------------------------\n\nexport type SlotHistoryEntry = {\n id: string;\n action: \"bind\" | \"unbind\" | \"restore\";\n preset: VariantPreset | null;\n description: string | null;\n updatedAt: string;\n updatedBy: string | null;\n assetId: string | null;\n /** Resolved DTO when the asset still exists; `null` after delete / 404. */\n asset: AssetDTO | null;\n};\n\nexport type UsageSnapshot = {\n tenant: { id: number; code: string };\n storage: { totalBytes: number; assetCount: number };\n today: UsageWindow;\n last30Days: UsageWindow;\n};\n\nexport type UsageWindow = {\n reads: number;\n writes: number;\n lists: number;\n deletes: number;\n admins: number;\n upscales: number;\n processes: number;\n /**\n * Minutes of video actually TRANSCODED in the window — source minutes ×\n * encode passes, so a 7-rung HLS ladder over a 2-minute clip books 14.\n *\n * A re-encode of a video already ingested counts AGAIN. That is the point:\n * an asset count cannot report it, because re-encoding an asset that already\n * exists creates no new asset.\n *\n * A stream copy (`video: { passthrough: true }`, or an idempotent cache skip)\n * books nothing, so a tenant that ran no encoder reads `0` — and that `0` is real.\n */\n videoMinutes: number;\n /** Encode passes behind `videoMinutes`. 0 when nothing was encoded. */\n videoEncodes: number;\n};\n\n/**\n * ⚠️ REMOVED 2026-08-17: `bytesIn` on `UsageWindow`/`UsageDailyPoint` and\n * `bytesTotal` on `UsagePerKey`.\n *\n * All three were structurally zero: the field behind them was never populated,\n * on any row. They could not be instrumented in place either — uploads go to\n * object storage through presigned URLs, so the payload never passes through\n * the API, and the only figure measurable there is a JSON envelope of a few\n * hundred bytes.\n *\n * A field that always reads `0` is worse than an absent field, because absent\n * is honest — a `0` next to a real `storage.totalBytes` reads as a measurement.\n * The byte number that IS true is still served: `storage.totalBytes`.\n */\n\nexport type UsageDailyPoint = {\n date: string;\n reads: number;\n writes: number;\n lists: number;\n deletes: number;\n processes: number;\n bytesStored: number;\n /** See `UsageWindow.videoMinutes`. Minutes transcoded on this day. */\n videoMinutes: number;\n videoEncodes: number;\n};\n\nexport type UsagePerKey = {\n apiKeyId: string;\n prefix: string | null;\n name: string | null;\n opsTotal: number;\n lastSeen: string;\n};\n\nclass UsageApi {\n constructor(private readonly opts: NitidaClientOptions) {}\n\n /** Snapshot for the active tenant — storage + today + last 30 days totals. */\n async snapshot(): Promise<UsageSnapshot> {\n const r = await fetch(endpointHref(this.opts, \"/usage\"), {\n headers: this.headers(),\n });\n if (!r.ok) throw new Error(`usage snapshot ${r.status}: ${await r.text()}`);\n return (await r.json()) as UsageSnapshot;\n }\n\n /** Daily rollup for charts — 1..365 days, default 30. */\n async timeseries(days = 30): Promise<{\n tenant: { id: number; code: string };\n days: UsageDailyPoint[];\n }> {\n const r = await fetch(\n endpointUrl(this.opts, \"/usage/timeseries\", { days }),\n { headers: this.headers() },\n );\n if (!r.ok)\n throw new Error(`usage timeseries ${r.status}: ${await r.text()}`);\n return (await r.json()) as {\n tenant: { id: number; code: string };\n days: UsageDailyPoint[];\n };\n }\n\n /** Per-API-key breakdown for the current month. */\n async keys(): Promise<{\n tenant: { id: number; code: string };\n monthStart: string;\n keys: UsagePerKey[];\n }> {\n const r = await fetch(endpointHref(this.opts, \"/usage/keys\"), {\n headers: this.headers(),\n });\n if (!r.ok) throw new Error(`usage keys ${r.status}: ${await r.text()}`);\n return (await r.json()) as {\n tenant: { id: number; code: string };\n monthStart: string;\n keys: UsagePerKey[];\n };\n }\n\n private headers(): Record<string, string> {\n return authHeaders(this.opts);\n }\n}\n\nexport { isUniversallyPlayableAudio } from \"./audio-compat\";\n\nexport class NitidaClient {\n readonly slots: SlotsApi;\n readonly assets: AssetsApi;\n readonly usage: UsageApi;\n /**\n * Effective options — read-only. Exposed so the `/web` and `/expo`\n * subpaths can inherit endpoint / apiKey / tenant scope from the\n * configured client without re-passing them per call site.\n */\n readonly opts: NitidaClientOptions;\n\n constructor(opts: NitidaClientOptions) {\n this.opts = opts;\n const cdn = opts.cdnBase ?? \"https://8ok.uk\";\n setCdnBase(cdn);\n // Configure the process-global tenant so every variant URL builder\n // (`getAssetUrl`, `urlFor`, `srcSetFor`, upload `cdnUrl`) emits the\n // tenant-prefixed path `<cdn>/<tid b36>/v/<sha>-<preset>.<ext>`.\n setTenantId(opts.tenantId);\n configureSlotResolver({\n endpoint: opts.endpoint,\n apiKey: opts.apiKey,\n tenantCode: opts.tenantCode,\n });\n this.slots = new SlotsApi(opts);\n this.assets = new AssetsApi(opts);\n this.usage = new UsageApi(opts);\n }\n\n /** Tenant id as base36 path segment (e.g. tenantId=4 → \"4/v/\"). */\n get tenantSegment(): string {\n return `${this.opts.tenantId.toString(36)}/v/`;\n }\n\n /** Build the canonical CDN URL deterministically from sha + preset. */\n urlFor(asset: Pick<AssetDTO, \"sha\">, preset: VariantPreset = \"lg\"): string {\n return getAssetUrl(asset, preset);\n }\n\n /** Build a responsive srcSet across the available image presets. */\n srcSetFor(asset: Pick<AssetDTO, \"sha\" | \"presets\">): string {\n return getAssetSrcSet(asset);\n }\n\n /**\n * Build an on-the-fly transform URL — `<cdn>/t/<dsl>/<sha>.<ext>`.\n *\n * ## URL CONVENTION — transforms are NOT tenant-prefixed (variants are)\n * Two distinct delivery paths, by design:\n * - **Variants / presets** (`urlFor`, `srcSetFor`, upload `cdnUrl`):\n * `<cdn>/<tenantId b36>/v/<sha>-<preset>.<ext>` ← tenant-scoped (e.g. `/4/v/<sha>-lg.webp`)\n * - **On-the-fly transforms** (`transform`, `transformSrcSet`):\n * `<cdn>/t/<dsl>/<sha>.<ext>` ← GLOBAL, no tenant segment (`/t/...`)\n * The transform service is content-addressed by sha + resizes from the source on demand,\n * so it needs no tenant in the path. Prefixing a transform URL with `/<tenant>/t/...` 404s.\n * Consumers that build URLs by hand must NOT add the tenant segment to `/t/` URLs.\n *\n * Returns the canonical `lg` variant URL when called with empty options,\n * so callers can swap `urlFor()` for `transform()` without thinking.\n *\n * URLs with the same params in different order produce the same\n * cache entry (the server canonicalizes both sides). Safe to use as\n * stable cache keys.\n *\n * <Image\n * src={aq.transform(asset, { width: 1280 })}\n * srcSet={aq.transformSrcSet(asset, [640, 960, 1280, 1920])}\n * sizes=\"(max-width: 768px) 100vw, 50vw\"\n * />\n *\n * @see {@link TransformOptions} for the full param matrix.\n */\n // Overload 1: no signing — synchronous, on-ladder width only (strict).\n transform(asset: Pick<AssetDTO, \"sha\">, opts?: TransformOptions): string;\n // Overload 2: with { sign: true } — async; returns `?sig=<hmac>` URL.\n // Accepts SignedTransformOptions so a signed URL may carry an off-ladder\n // custom width (the signature earns the edge whitelist bypass).\n transform(\n asset: Pick<AssetDTO, \"sha\">,\n opts: SignedTransformOptions,\n signOpts: { sign: true },\n ): Promise<string>;\n transform(\n asset: Pick<AssetDTO, \"sha\">,\n opts: SignedTransformOptions = {},\n signOpts?: { sign: true },\n ): string | Promise<string> {\n if (!signOpts?.sign) {\n // Unsigned path. Overload 1 constrains `width` to the ladder at every\n // public call site, so an off-ladder width can't reach here through the\n // typed API — narrow back to TransformOptions for the strict builder.\n return (\n getTransformUrl(asset, opts as TransformOptions) ??\n this.urlFor(asset, \"lg\")\n );\n }\n if (!this.opts.signingKey) {\n throw new Error(\n \"aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. \" +\n \"No signingKey on this client. The key is minted by POST /admin/projects/:code/rotate-signing-key, which needs a system-scope credential the platform operator holds — your own admin key gets 403 SYSTEM_KEY_REQUIRED, so ask for it. Then pass it to the SDK constructor on a SERVER-side instance only.\",\n );\n }\n // Signed path — custom (off-ladder) widths allowed. Empty opts → no\n // transform DSL, fall back to the unsigned `lg` variant URL.\n return (\n getSignedTransformUrl(asset, opts, this.opts.signingKey) ??\n Promise.resolve(this.urlFor(asset, \"lg\"))\n );\n }\n\n /**\n * Build a responsive `srcSet` string. One transform URL per width; all\n * other options apply to every URL.\n *\n * Pass `{ sign: true }` to return signed URLs (async). Without it, the\n * call stays synchronous as before.\n */\n transformSrcSet(\n asset: Pick<AssetDTO, \"sha\">,\n widths: number[],\n extraOpts?: Omit<TransformOptions, \"width\">,\n ): string;\n transformSrcSet(\n asset: Pick<AssetDTO, \"sha\">,\n widths: number[],\n extraOpts: Omit<TransformOptions, \"width\">,\n signOpts: { sign: true },\n ): Promise<string>;\n transformSrcSet(\n asset: Pick<AssetDTO, \"sha\">,\n widths: number[],\n extraOpts: Omit<TransformOptions, \"width\"> = {},\n signOpts?: { sign: true },\n ): string | Promise<string> {\n if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);\n if (!this.opts.signingKey) {\n throw new Error(\n \"aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions.\",\n );\n }\n const key = this.opts.signingKey;\n return Promise.all(\n widths.map(async (w) => {\n // Signed path → off-ladder widths allowed; build+sign via the\n // custom-width helper (the strict `getTransformUrl` would reject a\n // raw `number` width).\n const signed = await getSignedTransformUrl(\n asset,\n { ...extraOpts, width: w },\n key,\n );\n return signed ? `${signed} ${w}w` : null;\n }),\n ).then((parts) => parts.filter((s): s is string => s != null).join(\", \"));\n }\n\n /**\n * Build an on-the-fly VIDEO transform URL — Phase 4.\n *\n * Same DSL shape as `transform()` but the URL has a `.mp4` (default)\n * or `.webm` extension and the server routes the request to a\n * background job for video encoding (vs the inline pipeline for\n * images).\n *\n * On the first request the route returns **202 Accepted** with\n * `Retry-After: 10` while the encode runs (typically 5-30 s for a\n * short clip). The response body includes `outputUrl` which is the\n * eventual CDN URL — poll the same transform URL after the\n * retry-after window to get a 302 redirect to it.\n *\n * const url = aq.transformVideo(asset, {\n * width: 1080, height: 1920, fit: \"cover\",\n * start: 0, duration: 15,\n * });\n * // Pass to Video.js / <video src={url}>; on the first load it\n * // gets 202 + body.outputUrl; subsequent loads hit cache → 302.\n *\n * Video-specific DSL params:\n * - `start` (seconds, decimal OK)\n * - `duration` (seconds, 1..300)\n * - `format`: \"mp4\" (default) or \"webm\"\n *\n * The other params (`width`, `height`, `fit`) work identically to\n * image transforms. `gravity`, `quality`, `effect`, `dpr` are\n * accepted by the DSL but currently ignored on the video path.\n */\n transformVideo(\n asset: Pick<AssetDTO, \"sha\">,\n opts: TransformOptions = {},\n ): string {\n return getVideoTransformUrl(asset, opts) ?? this.urlFor(asset, \"lg\");\n }\n\n /**\n * Build the HLS master playlist URL for a VIDEO asset (Phase 5).\n *\n * Returns `<cdn>/t/format=hls(,start=…,duration=…)/<sha>.m3u8`. Pass\n * to an HLS-aware player:\n *\n * <video\n * src={aq.streamingUrl(asset)}\n * controls playsInline\n * // Video.js v10's @videojs/http-streaming ships native HLS —\n * // no plugin needed.\n * />\n *\n * On the first request the server returns **202 Accepted** while a\n * background job builds the multi-rung ladder (typically 1-3 min for\n * a 90 s source — five rungs of 240p/360p/480p/720p/1080p @ AAC).\n * Subsequent requests hit the cache → **302** to the master.m3u8.\n *\n * Supports `start` + `duration` to ladder a sub-clip. Other DSL\n * params (width, height, fit) are ignored on the HLS path because\n * the rungs determine resolution.\n */\n streamingUrl(\n asset: Pick<AssetDTO, \"sha\">,\n opts: Omit<TransformOptions, \"format\"> = {},\n ): string {\n return getHlsStreamingUrl(asset, opts);\n }\n\n /**\n * Upload a file or raw bytes. Returns the new asset id + canonical\n * URL. Hash-deduped — uploading the same bytes twice returns the\n * existing asset.\n *\n * Browser-first: uses `Blob` + WebCrypto. For Node 20+, pass a\n * Uint8Array and a precomputed `sha256` (since `crypto.subtle` works\n * but isn't always available depending on the runtime).\n */\n /**\n * Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-storage PUT**\n * → `/assets/process` → wait until the asset is ready.\n *\n * ⚠️ `presets` decides what exists FOREVER. Omit it and only `original` is written; ask for\n * `[\"thumb\"]` and the bytes you just uploaded are **not retrievable**. A variant not requested in\n * this first ingest cannot be added later once the ~24 h grace window on the uploaded bytes\n * closes — measured once as \"97 files archived successfully, zero recoverable\".\n *\n * @example Deliver an image on a site (the responsive ladder)\n * ```ts\n * import { NitidaClient } from \"@nitida/sdk/server\";\n *\n * const aq = new NitidaClient({ endpoint, apiKey, tenantCode, tenantId });\n * const { assetId, sha256 } = await aq.upload(file, {\n * fileName: file.name,\n * presets: [\"thumb\", \"sm\", \"md\", \"lg\"],\n * });\n * ```\n *\n * @example ARCHIVE a file — you must ask for `original`\n * ```ts\n * await aq.upload(bytes, {\n * fileName: \"contrato.pdf\",\n * contentType: \"application/pdf\",\n * presets: [\"original\"], // without this the bytes are unrecoverable\n * });\n * ```\n *\n * @example Raw bytes need an explicit MIME\n * ```ts\n * await aq.upload(bytes, { fileName: \"track.mp3\", contentType: \"audio/mpeg\" });\n * // Without either, it stores as kind:\"other\" — no variants, and regenerate() is unsupported.\n * ```\n *\n * @example Video — and what does NOT work there\n * ```ts\n * // `original` is accepted and then silently DROPPED: /assets/process filters video presets to\n * // {poster, video, aiproxy, probe} before dispatching the background transcode.\n * await aq.upload(clip, { fileName: \"tour.mp4\", presets: [\"poster\", \"video\"] });\n *\n * // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost encode\n * // time and permanent stored objects that nothing else reads.\n * ```\n */\n async upload(\n input: File | Blob | Uint8Array,\n opts: UploadOptions = {},\n ): Promise<UploadResult> {\n // 1. Resolve incoming bytes + mime first (the source of truth for\n // \"should we compress this?\")\n const sourceIsBlob = input instanceof File || input instanceof Blob;\n // A Uint8Array has no inherent MIME. Fall back to an explicit `contentType`, then to\n // the file extension, before octet-stream (which the server files as\n // `kind:\"other\"` — no variants). A File/Blob's own `.type` always wins when present.\n const sourceMime =\n (sourceIsBlob ? input.type : \"\") ||\n opts.contentType ||\n mimeFromFileName(opts.fileName) ||\n \"application/octet-stream\";\n if (!sourceIsBlob && sourceMime === \"application/octet-stream\") {\n console.warn(\n \"[@nitida/sdk] upload(Uint8Array): no MIME resolved (no `contentType`, no recognizable \" +\n '`fileName` extension) — the asset will be stored as kind:\"other\" with NO image/video ' +\n \"variants and regenerate() unsupported. Pass `contentType` or a `fileName` with an extension.\",\n );\n }\n\n // 2. Optional client-side compression. Image MIMEs only; non-image\n // sources pass through unchanged so a video or PDF upload still\n // works when the caller sets `compress: true` blanket-fashion.\n let bytes: Uint8Array;\n let effectiveMime: string;\n let clientOriginalBytes: number | undefined;\n\n const wantCompression =\n !!opts.compress &&\n sourceIsBlob &&\n typeof window !== \"undefined\" &&\n sourceMime.startsWith(\"image/\");\n\n if (wantCompression) {\n const compressOpts =\n opts.compress === true ? {} : (opts.compress as CompressOptions);\n // Lazy: keeps compressorjs/heic2any out of bundles that don't use it.\n //\n // Resolve `./web` against `import.meta.url` at runtime so no static\n // analyzer — esbuild, tsup, `bun build --compile`, vite, rollup —\n // can follow the specifier. Holding the path in a plain `const`\n // was not enough: `bun build --compile` constant-folds simple\n // strings and still eagerly bundled `./web.js`, which\n // top-level-imports the browser-only peer deps\n // `@aquienpz/asset-uploader-web` / `@nitida/asset-compressor-web`\n // — neither installed on server consumers — crashing the\n // single-binary on boot with `Cannot find module\n // '@aquienpz/asset-uploader-web'`.\n //\n // The earlier `new URL(\"./web.js\", import.meta.url)` + `await import(URL)`\n // dance survived bun-compile but Turbopack still tracks the URL literal\n // and tries to resolve `./web.js` at build time (no such file exists in\n // src/, only src/web/index.ts), failing with \"Module not found\".\n //\n // Indirect-eval via the Function constructor is opaque to BOTH static\n // analyzers — neither bun-compile nor Turbopack can follow the string\n // back to a module specifier. The browser `typeof window` guard above\n // ensures the branch never runs on the server, so the indirection is\n // safe at runtime.\n // Build the URL inside the Function body too — Turbopack still tracks\n // `new URL(<any expr>, import.meta.url)` as a resolution pattern even\n // when the first arg isn't a literal, so we have to hide both the URL\n // construction AND the import() behind indirect eval.\n const dynImport = new Function(\n \"base\",\n \"return import(new URL('./' + 'web' + '.js', base).href)\",\n ) as (base: string) => Promise<typeof import(\"./web\")>;\n const { compressImage } = await dynImport(import.meta.url);\n const result = await compressImage(input as File | Blob, compressOpts);\n bytes = new Uint8Array(await result.blob.arrayBuffer());\n clientOriginalBytes = result.originalBytes;\n effectiveMime = result.blob.type || sourceMime;\n } else {\n if (opts.compress && !sourceIsBlob) {\n console.warn(\n \"[@nitida/sdk] compress: true requires a File or Blob input; got Uint8Array — uploading raw bytes.\",\n );\n } else if (opts.compress && typeof window === \"undefined\") {\n console.warn(\n \"[@nitida/sdk] compress: true is browser-only — uploading raw bytes.\",\n );\n } else if (opts.compress && !sourceMime.startsWith(\"image/\")) {\n // Silent: callers can set `compress: true` for batched mixed\n // media and not have to special-case images vs videos.\n }\n bytes =\n input instanceof Uint8Array\n ? input\n : new Uint8Array(await input.arrayBuffer());\n effectiveMime = sourceMime;\n }\n\n const sha = opts.sha256 ?? (await computeSha256(bytes));\n const mime = effectiveMime;\n const fileName =\n opts.fileName ??\n (input instanceof File ? input.name : `upload-${sha.slice(0, 8)}.bin`);\n\n // Dedup probe.\n const existing = await this.assets.byHash(sha);\n if (existing && existing.status === \"ready\") {\n return {\n assetId: existing.id,\n sha256: sha,\n cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime)),\n };\n }\n\n // Presign + PUT + process. Delegates to `assets.presignUploadUrl()` so\n // the public method and `aq.upload()` share one code path; always\n // threads an explicit preset list so the default matches the SDK\n // contract (`[\"original\"]`) instead of inheriting the server's older\n // default (`thumb+sm+md+lg`).\n const presign = await this.assets.presignUploadUrl({\n sha256: sha,\n mime,\n bytes: bytes.byteLength,\n fileName,\n presets:\n opts.presets && opts.presets.length > 0\n ? opts.presets\n : DEFAULT_UPLOAD_PRESETS,\n ...(clientOriginalBytes != null && { clientOriginalBytes }),\n ...(opts.video != null && { video: opts.video }),\n });\n if (presign.deduped) {\n return {\n assetId: presign.asset.id,\n sha256: sha,\n cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime)),\n };\n }\n\n const putR = await fetch(presign.upload.url, {\n method: \"PUT\",\n headers: { \"Content-Type\": mime, ...(presign.upload.headers ?? {}) },\n body: new Blob([bytes as unknown as ArrayBuffer], { type: mime }),\n });\n if (!putR.ok)\n throw new Error(`Storage PUT ${putR.status}: ${await putR.text()}`);\n\n const procR = await fetch(endpointHref(this.opts, presign.process.url), {\n method: \"POST\",\n headers: {\n ...authHeaders(this.opts),\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(presign.process.body),\n });\n if (!procR.ok)\n throw new Error(`process ${procR.status}: ${await procR.text()}`);\n const proc = (await procR.json()) as { assetId?: string; kind?: string };\n\n let assetId = proc.assetId;\n if (!assetId && proc.kind === \"video\") {\n // Videos are async — poll by-hash until the row lands.\n const start = Date.now();\n let delay = 1000;\n while (Date.now() - start < 60_000) {\n const dto = await this.assets.byHash(sha);\n if (dto?.id) {\n assetId = dto.id;\n break;\n }\n await new Promise((r) => setTimeout(r, delay));\n delay = Math.min(delay * 1.5, 5_000);\n }\n }\n if (!assetId) throw new Error(\"upload: process returned no assetId\");\n\n // Wait until variants are ready so the URL works immediately.\n // Passing `undefined` keeps waitReady's own default (5 min). Callers\n // uploading large videos can opt in to a longer deadline via opts.timeoutMs.\n const final = await this.assets.waitReady(assetId, opts.timeoutMs);\n if (final.status !== \"ready\")\n throw new Error(`upload: asset ended status=${final.status}`);\n return {\n assetId,\n sha256: sha,\n cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime)),\n };\n }\n\n private defaultPresetForMime(mime: string): VariantPreset {\n if (mime.startsWith(\"video/\")) return \"video\";\n // The `mp3` variant only exists AFTER /process transcodes it; at presign\n // time only the original is guaranteed to be playable, so bind to it.\n if (mime.startsWith(\"audio/\")) return \"original\";\n return \"lg\";\n }\n\n /**\n * Pick a sensible preset to build a URL for, given the asset's actual\n * `presets` string. Falls back through the preference order\n * lg → md → sm → thumb → original (for images)\n * video → poster (for videos)\n * original → mp3 (for audio ALREADY playable everywhere)\n * mp3 → original (for any other audio)\n * so an upload that was processed with e.g. `[\"original\"]` still\n * returns a non-404 URL in `aq.upload`'s result.\n *\n * ⭐ Why audio branches on the source mime (changed 2026-08-22).\n *\n * It used to be `mp3 → original` unconditionally, so `upload().cdnUrl`\n * handed back the server's auto-generated mp3 — libmp3lame, mono ~96 kbps —\n * even when the caller had uploaded an MP3 or an AAC that already plays in\n * every target browser. A consumer asking for \"my file\" silently received a\n * re-encoded, lower-quality one, with no error to notice.\n *\n * Measured across the platform: of 126 audio assets carrying an mp3 variant,\n * **105 had an `audio/mpeg` source** — an MP3 re-encoded into an MP3, for\n * zero compatibility gain.\n *\n * The mp3 still wins for `audio/webm`/Opus and anything exotic, which is the\n * case it was built for: Chrome records webm/Opus, which iOS Safari cannot\n * decode. That guarantee is preserved exactly; only the needless downgrade\n * is gone.\n */\n private bestPresetForAsset(asset: AssetDTO, mime: string): VariantPreset {\n const order: VariantPreset[] = mime.startsWith(\"video/\")\n ? [\"video\", \"poster\"]\n : mime.startsWith(\"audio/\")\n ? isUniversallyPlayableAudio(mime)\n ? [\"original\", \"mp3\"] // the upload already plays everywhere — don't hand back a re-encode\n : [\"mp3\", \"original\"] // exotic codec: the cross-browser mp3 earns its place\n : [\"lg\", \"md\", \"sm\", \"thumb\", \"xl\", \"original\"];\n return (\n order.find((p) => hasPreset(asset, p)) ?? this.defaultPresetForMime(mime)\n );\n }\n}\n","/**\n * Which audio uploads already play everywhere — the single source of truth for\n * both the server's transcode guard and the client's delivery-preset choice.\n *\n * It lives in its own module, rather than beside the preset order that uses it,\n * for a mechanical reason: `apps/asset-manager` imports it by relative path.\n * The SDK is a PUBLISHED package, so its `exports` point at `dist/`, and\n * `dist/` is gitignored — it does not exist inside the Docker image, which\n * copies `packages/` as source. A bare `@nitida/sdk` import type-checks on any\n * machine that built the package once and then fails the image build. (It did,\n * on 2026-08-22, with `@nitida/asset-client`.) A single small file keeps that\n * relative import from dragging the whole client into the server.\n *\n * Two copies of this list drifting apart is how a platform ends up generating\n * a variant its own client refuses to use — which is exactly the bug this\n * predicate was introduced to end.\n */\n\n/**\n * `true` when a browser can play these bytes as uploaded, so re-encoding them\n * to MP3 buys nothing.\n *\n * - `audio/mpeg` — unambiguous.\n * - `audio/mp4` / `audio/aac` — plays in Safari, Chrome, Firefox and Edge on\n * desktop and mobile. The historical worry was old AOSP builds without\n * proprietary codecs; the call to treat AAC as universal was made with the\n * platform's one real AAC consumer, whose viewer is WebGL — a device that\n * cannot decode AAC cannot run that product at all, so the fallback would\n * only ever protect a device that had already lost.\n *\n * Deliberately NOT here: `audio/webm` and `audio/ogg` (Opus). iOS Safari\n * cannot decode them, and that is the case the MP3 fallback exists for.\n */\nexport function isUniversallyPlayableAudio(mime: string): boolean {\n const base = (mime.split(\";\")[0] ?? \"\").trim().toLowerCase();\n return base === \"audio/mpeg\" || base === \"audio/mp4\" || base === \"audio/aac\";\n}\n","/**\n * @nitida/sdk/server — server-safe entry point.\n *\n * Use this subpath from Node.js, Bun, Cloud Run, Lambda, Vercel Functions,\n * edge runtimes, agents, cron jobs, BFFs — anywhere there's no `window`\n * and you want a hard guarantee that no browser-only code lands in your\n * bundle. The constructor REQUIRES `apiKey`; the type from `/web` omits\n * it, so the two modes never confuse each other.\n *\n * import { NitidaClient } from \"@nitida/sdk/server\";\n *\n * const aq = new NitidaClient({\n * endpoint: process.env.AQUIENPZ_URL!,\n * apiKey: process.env.AQUIENPZ_API_KEY!, // <- required\n * tenantCode: \"acme-co\",\n * tenantId: 1,\n * // signingKey: optional, only for `aq.transform(..., { sign: true })`\n * });\n *\n * const asset = await aq.assets.byHash(sha256);\n * const hero = aq.transform(asset, { width: 1920 });\n *\n * // Typical BFF use: proxy a browser request through to aquienpz.\n * // The browser side calls `@nitida/sdk/web` against `/api/am/...`\n * // and your route handler forwards here with the real API key.\n *\n * What you get:\n * - `NitidaClient` (slots/assets/usage APIs over plain fetch)\n * - URL builders: `getAssetUrl`, `getTransformUrl`, `getTransformSrcSet`,\n * `getHlsStreamingUrl`, `extractAssetSha`, `signTransformUrl`\n * - `aq.upload(bytes)` works with `Uint8Array` (Node 18+ / Bun ship Blob\n * globally; File-API workflows are documented on the /web subpath instead)\n *\n * What's NOT here (use `@nitida/sdk/web` instead):\n * - `compressImage` (browser-side compressorjs + heic2any)\n *\n * And what exists nowhere: there is no `createWebUploader`. Multipart is not\n * exposed from any subpath — `aq.upload()` is the supported path.\n *\n * Stripe/Cloudinary historically shipped two separate packages\n * (`stripe` vs `@stripe/stripe-js`, `cloudinary` vs `@cloudinary/url-gen`)\n * for this split. Modern providers (Vercel Blob, Uploadthing, Better\n * Auth, AI SDK) use subpaths within one package — same tree-shaking\n * guarantees, single version, no drift. We follow that pattern.\n * @module @nitida/sdk/server\n */\n\nimport { NitidaClient as BaseNitidaClient, type NitidaClientOptions } from \"..\";\n\n/**\n * Server-side constructor options — `apiKey` is REQUIRED here. Use this\n * type whenever you build a client behind a process boundary (Node, Bun,\n * Cloud Run, Vercel Functions, edge runtimes, BFFs).\n *\n * const aq = new NitidaClient({\n * endpoint: process.env.AQUIENPZ_URL!,\n * apiKey: process.env.AQUIENPZ_API_KEY!,\n * tenantCode: \"acme-co\",\n * tenantId: 1,\n * });\n */\nexport type ServerClientOptions = Required<\n Pick<NitidaClientOptions, \"endpoint\" | \"apiKey\" | \"tenantCode\" | \"tenantId\">\n> &\n Pick<NitidaClientOptions, \"cdnBase\" | \"signingKey\">;\n\n/**\n * Server-safe `NitidaClient` — same runtime as the root class, but the\n * constructor type enforces `apiKey` so misconfiguration is a TS build\n * error, not a runtime 401.\n */\nexport class NitidaClient extends BaseNitidaClient {\n constructor(opts: ServerClientOptions) {\n super(opts);\n }\n}\n\n// ---------------------------------------------------------------------------\n// This subpath MIRRORS THE ROOT. Every export of `@nitida/sdk` is here.\n//\n// It is a COMPLETE entry point, not an additive module — a Node consumer is\n// told to import from here and must never have to reach past it. It did:\n// `getHlsLadder` was on the root and missing here, an example in the docs\n// imported it from `/server`, and an agent evaluating the SDK got a\n// SyntaxError at RUNTIME. Measured on 2026-08-21, this list was short by 28\n// of the root's 72 — the whole palette family, every slot helper, the HLS\n// ladder helpers and the preset constants.\n//\n// The completeness is now ASSERTED by scripts/check-published-doc-symbols.ts,\n// which also owns the deny list: an omission has to be justified there, in\n// writing, or the build fails. Do not hand-edit this list to be shorter.\n// ---------------------------------------------------------------------------\nexport {\n type AssetDTO,\n type AssetPalette,\n type AssetVariant,\n accessMessage,\n assertPublic,\n bestTextContrast,\n type ComposeMarketingComposition,\n type ComposeMarketingOptions,\n type ComposeMarketingResult,\n type ComposeMarketingSegment,\n type CompressOptions,\n computeVariantDimensions,\n configureSlotResolver,\n contrastRatio,\n deriveAccessKey,\n extractAssetSha,\n getAmbientGradient,\n getAssetDimensions,\n getAssetSrcSet,\n getAssetUrl,\n getCdnBase,\n getHlsLadder,\n getHlsStreamingUrl,\n getPaletteBlurBackground,\n getPaletteCssVars,\n getPrivateAssetUrl,\n getPrivateTransformUrl,\n getSignedTransformUrl,\n getTenantId,\n getTextColorForBackground,\n getTransformSrcSet,\n getTransformUrl,\n getVideoTransformUrl,\n type HlsRung,\n hasPreset,\n hlsLadderAlignment,\n invalidateSlotCache,\n isUniversallyPlayableAudio,\n iteratePaletteSwatches,\n mimeFromFileName,\n type NitidaClientOptions,\n type PaletteSwatch,\n PRESET_EXT,\n PRESET_LONG,\n PRESET_MAX_DIM,\n PRESET_SHORT,\n type PresignUploadUrlOptions,\n pickAmbientBackground,\n type RegenerateResult,\n type RequestablePreset,\n type ResolveSlotOptions,\n relativeLuminance,\n resolveSlot,\n resolveSlots,\n type SignAccessOptions,\n type SignedTransformOptions,\n type SlotDTO,\n type SlotHistoryEntry,\n type SlotResolution,\n serializeTransform,\n setCdnBase,\n setTenantId,\n signAccessUrl,\n signTransformUrl,\n TRANSFORM_WIDTHS,\n type TransformEffect,\n type TransformFit,\n type TransformFormat,\n type TransformGravity,\n type TransformOptions,\n type TransformWidth,\n type UploadOptions,\n type UploadResult,\n type UploadUrlResult,\n type UploadVideoOptions,\n type UsageDailyPoint,\n type UsagePerKey,\n type UsageSnapshot,\n type UsageWindow,\n type VariantEntryPreset,\n type VariantPreset,\n type VisibilityHint,\n} from \"..\";\n"],"mappings":"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","names":["configureSlotResolver","getAssetSrcSet","getAssetUrl","getHlsStreamingUrl","getSignedTransformUrl","getTransformSrcSet","getTransformUrl","getVideoTransformUrl","hasPreset","invalidateSlotCache","resolveSlot","resolveSlots","setCdnBase","setTenantId","NitidaClient"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/audio-compat.ts","../src/server/index.ts"],"sourcesContent":["/**\n * @nitida/sdk — universal client for the aquienpz multi-tenant asset\n * platform.\n *\n * One ergonomic facade over the underlying packages\n * (`@nitida/asset-client` URL builders + `@aquienpz/asset-uploader-web`\n * + the slot resolver). Auth is a bearer API key (`amk_rt_*`), issued\n * per tenant when the tenant is created; tenant scope comes from\n * the key's metadata (`X-Tenant-Code` is log-only).\n *\n * Usage:\n *\n * import { NitidaClient } from \"@nitida/sdk\";\n *\n * const aq = new NitidaClient({\n * endpoint: \"https://api.nitida.gofuture.space\",\n * apiKey: process.env.AQUIENPZ_API_KEY!, // amk_rt_* — server-only\n * tenantCode: \"acme-co\",\n * cdnBase: \"https://8ok.uk\", // optional override\n * tenantId: 4, // required for tenant-prefixed URLs\n * });\n *\n * // Slot system (recommended — admin can rebind without redeploys).\n * const hero = await aq.slots.resolve(\"storefront.cr.hero\");\n * const set = await aq.slots.resolveMany([\"a\", \"b\", \"c\"]);\n *\n * // Lower-level asset operations.\n * const asset = await aq.assets.byHash(sha);\n * const list = await aq.assets.list({ limit: 50 });\n *\n * // Upload bytes / files.\n * const up = await aq.upload(file, { fileName: \"cover.jpg\" });\n *\n * For React, see `@nitida/sdk/react` (useSlot, useSlots, useNitidaClient).\n * @module @nitida/sdk\n */\n\nimport {\n type AssetDTO,\n type AssetVariant,\n configureSlotResolver,\n getAssetSrcSet,\n getAssetUrl,\n getHlsStreamingUrl,\n getSignedTransformUrl,\n getTransformSrcSet,\n getTransformUrl,\n getVideoTransformUrl,\n hasPreset,\n invalidateSlotCache,\n type RequestablePreset,\n type ResolveSlotOptions,\n resolveSlot,\n resolveSlots,\n type SignedTransformOptions,\n type SlotDTO,\n type SlotResolution,\n setCdnBase,\n setTenantId,\n type TransformOptions,\n type VariantEntryPreset,\n type VariantPreset,\n} from \"@nitida/asset-client\";\nimport { isUniversallyPlayableAudio } from \"./audio-compat\";\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\n/**\n * Permissive constructor options for the root `NitidaClient`.\n *\n * App code should NOT import this type directly — prefer the strict\n * variants from the subpaths:\n *\n * - `WebClientOptions` from `@nitida/sdk/web` (no `apiKey`)\n * - `ServerClientOptions` from `@nitida/sdk/server` (`apiKey` required)\n *\n * This root type is the union both modes resolve to; the underlying class\n * accepts both shapes so subpath wrappers can extend without duplication.\n */\nexport type NitidaClientOptions = {\n /**\n * Base URL of the nitida API (e.g. `https://api.nitida.gofuture.space`).\n *\n * May be relative (e.g. `/api/am`) ONLY in browser contexts where the\n * SDK resolves it against `window.location.origin`. Node/Bun consumers\n * must always pass an absolute URL.\n */\n endpoint: string;\n /**\n * Bearer API key with the `amk_rt_*` prefix.\n *\n * **Server-only.** Omit when constructing from `@nitida/sdk/web` —\n * your BFF / route handler injects the bearer header in proxy mode.\n */\n apiKey?: string;\n /**\n * Extra headers merged into every request. The documented way for\n * mobile/Expo clients to authenticate a BFF that gates on the Better Auth\n * session: they can't send cookies automatically, so they pass\n * `{ Cookie: authClient.getCookie() }` here (see Better Auth Expo docs,\n * \"Making Authenticated Requests to Your Server\"). Web/server consumers omit\n * this — browsers attach the same-origin cookie and servers pass `apiKey`.\n */\n headers?: Record<string, string>;\n /** Tenant code — sent as `X-Tenant-Code` (log-only). Authoritative scope is the key's metadata.tenantId. */\n tenantCode: string;\n /** Numeric tenant id — used to build tenant-prefixed CDN URLs `<cdn>/<tenantId b36>/v/<sha>-<preset>.<ext>`. */\n tenantId: number;\n /** Override the public CDN base. Defaults to `https://8ok.uk`. */\n cdnBase?: string;\n /**\n * Tenant's HMAC signing key for transform URLs (Phase 3). Required\n * only when calling `aq.transform(asset, opts, { sign: true })`.\n *\n * 32 random bytes, generated server-side on tenant creation; fetch\n * via `POST /admin/projects/:code/rotate-signing-key`, which needs a\n * SYSTEM-scope credential the platform operator holds — your own admin key\n * answers `403 SYSTEM_KEY_REQUIRED`. Ask for it. **Keep it\n * server-side only** — do not ship in `NEXT_PUBLIC_*` env vars. Sign\n * URLs from a BFF route handler, or pre-sign at build time.\n */\n signingKey?: string;\n};\n\n// ---------------------------------------------------------------------------\n// URL construction\n// ---------------------------------------------------------------------------\n\n/**\n * Build a fully-qualified URL for an aquienpz endpoint path.\n *\n * Accepts both absolute endpoints (`https://api.nitida.gofuture.space`) and\n * relative ones (`/api/am`) — the latter only works in browser contexts\n * (resolved against `window.location.origin`). Node/Bun throws a clear\n * error if a relative endpoint is configured.\n *\n * The native `URL` constructor throws on relative inputs, so every fetch\n * site in the SDK must go through this helper instead of `new URL(...)`.\n */\nfunction endpointUrl(\n opts: Pick<NitidaClientOptions, \"endpoint\">,\n path: string,\n searchParams?: Record<string, string | number | boolean | undefined>,\n): URL {\n const endpoint = opts.endpoint.replace(/\\/+$/, \"\");\n const isAbsolute = /^https?:\\/\\//i.test(endpoint);\n let base: string;\n if (isAbsolute) {\n base = `${endpoint}${path}`;\n } else if (typeof window !== \"undefined\" && window.location?.origin) {\n base = `${window.location.origin}${endpoint}${path}`;\n } else {\n throw new Error(\n `[@nitida/sdk] relative endpoint \"${endpoint}\" requires a browser; ` +\n \"pass an absolute URL when using the SDK from Node/Bun.\",\n );\n }\n const u = new URL(base);\n if (searchParams) {\n for (const [k, v] of Object.entries(searchParams)) {\n if (v !== undefined && v !== null && v !== \"\") {\n u.searchParams.set(k, String(v));\n }\n }\n }\n return u;\n}\n\n/** Build a request URL as a plain string (no search params). */\nfunction endpointHref(\n opts: Pick<NitidaClientOptions, \"endpoint\">,\n path: string,\n): string {\n return endpointUrl(opts, path).toString();\n}\n\n/**\n * Auth headers — conditionally includes `Authorization` only when an\n * `apiKey` is present. In BFF-proxy mode (browser via `/web`) the\n * proxy injects the real bearer header, so we omit it here.\n */\nfunction authHeaders(opts: NitidaClientOptions): Record<string, string> {\n const h: Record<string, string> = {\n // Caller-supplied headers first; `X-Tenant-Code` stays authoritative below.\n ...opts.headers,\n \"X-Tenant-Code\": opts.tenantCode,\n };\n if (opts.apiKey) h.Authorization = `Bearer ${opts.apiKey}`;\n return h;\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports (so consumers don't double-import from asset-client)\n// ---------------------------------------------------------------------------\n\n// This barrel is COMPLETE on purpose: every public symbol of\n// `@nitida/asset-client` is reachable from `@nitida/sdk`, so a consumer never\n// has to know which of the two packages a helper happens to live in. It had\n// drifted to 30 of 53 — the gap silently included `TRANSFORM_WIDTHS` and\n// `TransformWidth`, which both READMEs tell you to import precisely so an\n// off-ladder width fails at compile time instead of returning 400, and the\n// palette helpers the README imports from here by name.\nexport type {\n AssetDTO,\n AssetPalette,\n AssetVariant,\n HlsRung,\n PaletteSwatch,\n RequestablePreset,\n ResolveSlotOptions,\n SignAccessOptions,\n SignedTransformOptions,\n SlotDTO,\n SlotResolution,\n TransformEffect,\n TransformFit,\n TransformFormat,\n TransformGravity,\n TransformOptions,\n TransformWidth,\n VariantEntryPreset,\n VariantPreset,\n VisibilityHint,\n} from \"@nitida/asset-client\";\nexport {\n accessMessage,\n assertPublic,\n assertSha,\n bestTextContrast,\n computeVariantDimensions,\n configureSlotResolver,\n contrastRatio,\n deriveAccessKey,\n extractAssetSha,\n getAmbientGradient,\n getAssetDimensions,\n getAssetSrcSet,\n getAssetUrl,\n getCdnBase,\n getHlsLadder,\n getHlsStreamingUrl,\n getPaletteBlurBackground,\n getPaletteCssVars,\n getPrivateAssetUrl,\n getPrivateTransformUrl,\n getSignedTransformUrl,\n getTenantId,\n getTextColorForBackground,\n getTransformSrcSet,\n getTransformUrl,\n getVideoTransformUrl,\n hasPreset,\n hlsLadderAlignment,\n invalidateSlotCache,\n iteratePaletteSwatches,\n PRESET_EXT,\n PRESET_LONG,\n PRESET_MAX_DIM,\n PRESET_SHORT,\n pickAmbientBackground,\n relativeLuminance,\n resolveSlot,\n resolveSlots,\n serializeTransform,\n setCdnBase,\n setTenantId,\n signAccessUrl,\n signTransformUrl,\n TRANSFORM_WIDTHS,\n} from \"@nitida/asset-client\";\n\n// ---------------------------------------------------------------------------\n// Sub-namespaces\n// ---------------------------------------------------------------------------\n\nclass SlotsApi {\n constructor(private readonly opts: NitidaClientOptions) {}\n\n /** Resolve one slot — returns `{slot, preset, url}` or `{slot: null, url: null}` when unbound. */\n resolve(\n slotKey: string,\n options: ResolveSlotOptions = {},\n ): Promise<SlotResolution> {\n return resolveSlot(slotKey, options);\n }\n\n /** Bulk-resolve N slots in one HTTP round-trip. */\n resolveMany(\n slotKeys: string[],\n options: ResolveSlotOptions = {},\n ): Promise<Record<string, SlotResolution>> {\n return resolveSlots(slotKeys, options);\n }\n\n /** List slots for the tenant (admin). Optional prefix filter for tree views. */\n async list(\n opts: { prefix?: string; limit?: number } = {},\n ): Promise<SlotDTO[]> {\n const u = endpointUrl(this.opts, \"/slots\", {\n prefix: opts.prefix,\n limit: opts.limit,\n });\n const r = await fetch(u, { headers: this.headers() });\n if (!r.ok) throw new Error(`slots list ${r.status}: ${await r.text()}`);\n const body = (await r.json()) as { slots: SlotDTO[] };\n return body.slots;\n }\n\n /** Bind / rebind a slot to an asset. Admin-only operation. */\n async bind(\n slotKey: string,\n body: {\n assetId: string;\n preset?: VariantPreset;\n description?: string;\n updatedBy?: string;\n },\n ): Promise<{ ok: true; slotKey: string; assetId: string }> {\n const r = await fetch(\n endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),\n {\n method: \"PUT\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n },\n );\n if (!r.ok) throw new Error(`slot bind ${r.status}: ${await r.text()}`);\n return (await r.json()) as { ok: true; slotKey: string; assetId: string };\n }\n\n /**\n * Recent bindings for a slot. Lets the admin audit who changed\n * what and restore a previous binding without remembering the\n * asset id. Default limit 20, max 100.\n */\n async history(\n slotKey: string,\n opts: { limit?: number } = {},\n ): Promise<SlotHistoryEntry[]> {\n const u = endpointUrl(\n this.opts,\n `/slots/${encodeURIComponent(slotKey)}/history`,\n { limit: opts.limit },\n );\n const r = await fetch(u, { headers: this.headers() });\n if (!r.ok) throw new Error(`slots history ${r.status}: ${await r.text()}`);\n const body = (await r.json()) as { history: SlotHistoryEntry[] };\n return body.history;\n }\n\n /**\n * Restore the slot to a previous binding. Equivalent to\n * `bind(key, { assetId: previous.assetId, action: \"restore\" })`\n * — the audit row is tagged `restore` instead of `bind`.\n */\n async restore(\n slotKey: string,\n args: { assetId: string; preset?: VariantPreset; updatedBy?: string },\n ): Promise<{ ok: true; slotKey: string; assetId: string }> {\n const r = await fetch(\n endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),\n {\n method: \"PUT\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ ...args, action: \"restore\" }),\n },\n );\n if (!r.ok) throw new Error(`slot restore ${r.status}: ${await r.text()}`);\n return (await r.json()) as { ok: true; slotKey: string; assetId: string };\n }\n\n /** Remove a slot binding. The asset itself is left alone. */\n async unbind(slotKey: string): Promise<{ ok: true; removed: number }> {\n const r = await fetch(\n endpointHref(this.opts, `/slots/${encodeURIComponent(slotKey)}`),\n {\n method: \"DELETE\",\n headers: this.headers(),\n },\n );\n if (!r.ok) throw new Error(`slot unbind ${r.status}: ${await r.text()}`);\n return (await r.json()) as { ok: true; removed: number };\n }\n\n /** Invalidate the in-process cache after a slot rebind. */\n invalidateCache(slotKey?: string): void {\n invalidateSlotCache(slotKey);\n }\n\n private headers(): Record<string, string> {\n return authHeaders(this.opts);\n }\n}\n\n/**\n * Returned by `aq.assets.regenerate(...)`. The shape varies by kind —\n * images return immediately with the merged variant list; videos\n * return a dispatch handle (the actual transcode runs in a background\n * job and finishes async).\n */\nexport type RegenerateResult =\n | {\n ok: true;\n kind: \"image\";\n /** Full variant set after the merge. */\n variants: AssetVariant[];\n /** Presets newly written this run. Useful for showing \"added X\". */\n newVariants: VariantPreset[];\n /**\n * Which source the server read to derive the new variants:\n * - `\"original\"` / `\"raw\"` → lossless source bytes (best)\n * - `\"xl\"` / `\"lg\"` / `\"md\"` / `\"sm\"` / `\"thumb\"` → a previously\n * encoded WebP variant was used as the source. Output is\n * re-encoded WebP — fine for thumb/sm from lg, lossier when\n * working from already-small sources.\n *\n * The no-upscale clamp still applies: deriving `lg` (1920) from\n * a 640 `sm` source produces a 640-side `lg` variant, not a\n * stretched 1920.\n */\n sourceUsed: VariantPreset | \"raw\";\n }\n | {\n ok: true;\n kind: \"video\";\n dispatch: unknown;\n regenerated: string[] | \"default\";\n };\n\n/**\n * Wire shape returned by `POST /assets/upload-url`. Either the server\n * resolves the upload synchronously via dedup (`deduped: true` + existing\n * asset DTO) or it returns a presigned storage PUT URL plus a `process` payload\n * the caller must POST to `/assets/process` after the PUT lands.\n */\nexport type UploadUrlResult =\n | { deduped: true; asset: AssetDTO }\n | {\n deduped: false;\n upload: { url: string; headers?: Record<string, string> };\n process: { url: string; body: Record<string, unknown> };\n };\n\n/**\n * VIDEO-only delivery knobs threaded into `/assets/process`. Ignored for\n * image / audio / other uploads. Both fields default to today's behavior when\n * omitted, so existing callers are unaffected.\n */\nexport type UploadVideoOptions = {\n /**\n * `false` → skip the auto-dispatched HLS adaptive ladder (240p–2160p). Use\n * for download-only assets served as a progressive `-v.mp4` and never\n * streamed (e.g. share-video reels) — it avoids a second background job no\n * one watches. Default/absent → the ladder is generated as before.\n */\n hls?: boolean;\n /**\n * `true` → when the uploaded MP4 is ALREADY web-safe (H.264 + yuv420p),\n * re-mux the `video` variant instead of re-encoding it. Use for\n * delivery-ready uploads (the bytes are already H.264 High / yuv420p /\n * +faststart / capped bitrate) to skip a wasteful re-encode + generational\n * quality loss. Falls back to a full re-encode automatically when the source\n * is not web-safe. Default/absent → unconditional re-encode (today's path).\n */\n passthrough?: boolean;\n};\n\n/** Input shape accepted by `aq.assets.presignUploadUrl(...)`. */\nexport type PresignUploadUrlOptions = {\n /** Full sha256 (64 hex) of the bytes that will be PUT to storage. */\n sha256: string;\n /** MIME type of the bytes (e.g. `image/jpeg`, `video/mp4`). */\n mime: string;\n /** Byte length of the upload payload. */\n bytes: number;\n /** Suggested file name; surfaces in admin dashboards + extension fallback. */\n fileName: string;\n /**\n * Variant ladder to generate after `/assets/process`. Defaults to\n * `[\"original\"]` server-side when omitted — same contract as `aq.upload`.\n *\n * {@link RequestablePreset}, not {@link VariantPreset}: `hls` and `mp3` are\n * things a variant can BE, never things you can ask for, and asking is a 400.\n */\n presets?: RequestablePreset[];\n /**\n * Pre-compression size of the source (useful when the browser ran\n * compressorjs / heic2any before computing `bytes`). Recorded\n * server-side, so the savings show up in the admin usage dashboards.\n */\n clientOriginalBytes?: number;\n /** VIDEO-only delivery knobs forwarded into `/assets/process`. See {@link UploadVideoOptions}. */\n video?: UploadVideoOptions;\n};\n\n/** Input shape accepted by `aq.assets.composeMarketing(...)`. */\nexport type ComposeMarketingSegment = {\n /** Public URL of the source clip (typically a `/t/.../video.mp4` transform). */\n sourceUrl: string;\n /** Optional clip duration in seconds (cap for that segment). */\n durationSec?: number;\n};\n\nexport type ComposeMarketingComposition = {\n /** Transition between consecutive segments. Default `\"cut\"`. */\n transition?: \"cut\" | \"fade\";\n /** Optional audio track to mix on top of the final composition. */\n audioTrack?: { url: string };\n /** Final composition length in seconds (server may clamp). */\n finalDurationSec?: number;\n};\n\nexport type ComposeMarketingOptions = {\n /** Marketing-kit id this composition belongs to (server uses it for naming + dedup). */\n marketingKitId: string;\n /** Ordered clip segments to stitch. */\n segments: ComposeMarketingSegment[];\n /** Optional composition-level knobs (transitions, audio, duration). */\n composition?: ComposeMarketingComposition;\n};\n\nexport type ComposeMarketingResult = {\n /** Aquienpz asset id of the in-flight composition. Poll `aq.assets.waitReady(id)`. */\n assetId: string;\n /** Asset status at dispatch time — usually `\"processing\"`. */\n status: \"processing\" | \"ready\" | \"failed\";\n};\n\nclass AssetsApi {\n constructor(private readonly opts: NitidaClientOptions) {}\n\n /** Look up an asset by full sha256 (64 hex). Returns null on 404. */\n async byHash(sha256: string): Promise<AssetDTO | null> {\n const r = await fetch(\n endpointHref(this.opts, `/assets/by-hash/${sha256}`),\n { headers: this.headers() },\n );\n if (r.status === 404) return null;\n if (!r.ok) throw new Error(`assets byHash ${r.status}: ${await r.text()}`);\n return (await r.json()) as AssetDTO;\n }\n\n /** Bulk lookup by sha256s. */\n async byHashes(\n hashes: string[],\n ): Promise<{ existing: AssetDTO[]; missing: string[] }> {\n const r = await fetch(endpointHref(this.opts, \"/assets/by-hashes\"), {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ hashes }),\n });\n if (!r.ok)\n throw new Error(`assets byHashes ${r.status}: ${await r.text()}`);\n return (await r.json()) as { existing: AssetDTO[]; missing: string[] };\n }\n\n /** Paginated list of recent assets for the tenant. */\n async list(\n opts: { limit?: number; cursor?: string; includeDeleted?: boolean } = {},\n ): Promise<{\n assets: AssetDTO[];\n nextCursor: string | null;\n }> {\n const u = endpointUrl(this.opts, \"/assets\", {\n limit: opts.limit,\n cursor: opts.cursor,\n include_deleted: opts.includeDeleted ? \"true\" : undefined,\n });\n const r = await fetch(u, { headers: this.headers() });\n if (!r.ok) throw new Error(`assets list ${r.status}: ${await r.text()}`);\n return (await r.json()) as {\n assets: AssetDTO[];\n nextCursor: string | null;\n };\n }\n\n /** Full DTO for an asset (admin view — includes audit-only fields). */\n async get(assetId: string): Promise<AssetDTO & Record<string, unknown>> {\n const r = await fetch(endpointHref(this.opts, `/assets/${assetId}`), {\n headers: this.headers(),\n });\n if (!r.ok) throw new Error(`asset get ${r.status}: ${await r.text()}`);\n return (await r.json()) as AssetDTO & Record<string, unknown>;\n }\n\n /**\n * Slot bindings pointing at an asset. Use this before deleting an\n * asset so the admin sees which storefront slots would suddenly\n * resolve to nothing.\n */\n async bindings(assetId: string): Promise<\n Array<{\n slotKey: string;\n preset: VariantPreset | null;\n description: string | null;\n updatedAt: string;\n updatedBy: string | null;\n }>\n > {\n const r = await fetch(endpointHref(this.opts, `/assets/${assetId}/slots`), {\n headers: this.headers(),\n });\n if (!r.ok) throw new Error(`asset bindings ${r.status}: ${await r.text()}`);\n const body = (await r.json()) as {\n slots: Array<{\n slotKey: string;\n preset: VariantPreset | null;\n description: string | null;\n updatedAt: string;\n updatedBy: string | null;\n }>;\n };\n return body.slots;\n }\n\n /**\n * Full variant list for an asset — preset, URL, dimensions, bytes.\n * Stronger-typed wrapper around `get()` that exposes only the\n * `variants` field with the proper `AssetVariant[]` shape.\n *\n * const v = await aq.assets.variants(logoId);\n * v.map((x) => x.preset); // → (\"thumb\" | \"sm\" | … | \"original\")[]\n *\n * ⚠️ Returns `[]` — not an error — against a server older than the\n * 2026-08-17 release, which never sent the field at all. An empty\n * array is therefore \"no variants OR old server\". For a plain existence\n * check prefer `hasPreset(dto, preset)` on `dto.presets`, which every server\n * version sends; use this when you need the URLs and sizes.\n */\n async variants(assetId: string): Promise<AssetVariant[]> {\n const dto = await this.get(assetId);\n return Array.isArray((dto as { variants?: unknown }).variants)\n ? (dto as { variants: AssetVariant[] }).variants\n : [];\n }\n\n /**\n * Add or rebuild variants on an existing asset. Presets are MERGED\n * with what's there, for images AND for video — passing\n * `{ presets: [\"thumb\"] }` adds the thumb variant without touching\n * `lg`, `sm`, `original`, etc.\n *\n * // Day 0: upload original-only logo\n * const { assetId } = await aq.upload(logoFile); // defaults to [\"original\"]\n *\n * // Day 7: need a thumb without re-uploading\n * await aq.assets.regenerate(assetId, { presets: [\"thumb\"] });\n *\n * const after = await aq.assets.variants(assetId);\n * after.map((v) => v.preset); // → [\"original\", \"thumb\"]\n *\n * Passing no presets re-runs the FULL default pipeline for that\n * asset's kind (thumb+sm+md+lg for images, poster+video for video).\n *\n * If the asset was uploaded original-only and the ~24 h grace window\n * on the uploaded bytes has already closed, the route falls back to\n * reading the source bytes from the permanent `original` variant —\n * no need to re-upload.\n *\n * Video presets are filtered to `[\"poster\",\"video\",\"aiproxy\",\"probe\"]`\n * and dispatched to a background job (the call returns immediately\n * with a dispatch handle; poll `aq.assets.get(id).status` for\n * completion).\n *\n * ⚠️ Before 2026-08-22 the video path REPLACED the whole variant\n * registry instead of merging, so a partial regenerate silently\n * deregistered `poster`, `video` and — irrecoverably — `hls`, which\n * is not a {@link RequestablePreset} and therefore cannot be asked\n * for again. The objects kept serving from the CDN; only the\n * registry died. Fixed server-side; a client on an older server\n * still loses them.\n */\n async regenerate(\n assetId: string,\n opts: { presets?: RequestablePreset[] } = {},\n ): Promise<RegenerateResult> {\n const r = await fetch(\n endpointHref(this.opts, `/assets/${assetId}/regenerate`),\n {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(opts.presets ? { presets: opts.presets } : {}),\n },\n );\n if (!r.ok)\n throw new Error(`asset regenerate ${r.status}: ${await r.text()}`);\n return (await r.json()) as RegenerateResult;\n }\n\n /** Merge metadata into an asset (role / slot / description / tags). */\n async patchMetadata(\n assetId: string,\n metadata: Record<string, unknown>,\n ): Promise<{ ok: true; metadata: Record<string, unknown> }> {\n const r = await fetch(endpointHref(this.opts, `/assets/${assetId}`), {\n method: \"PATCH\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ metadata }),\n });\n if (!r.ok) throw new Error(`asset patch ${r.status}: ${await r.text()}`);\n return (await r.json()) as { ok: true; metadata: Record<string, unknown> };\n }\n\n /**\n * Request a presigned storage PUT URL for direct browser-side uploads.\n *\n * Mirrors the first half of `aq.upload()` — the caller (typically a\n * BFF / share-link dropzone) computes sha256 in the browser, then\n * uploads bytes straight to object storage with the returned `upload.url`,\n * then POSTs `process.body` to `/assets/process` (see {@link processAndWait})\n * once storage has the bytes.\n *\n * If the sha is already known to the tenant the server short-circuits\n * with `{ deduped: true, asset }` — no PUT needed.\n *\n * @example The browser-direct flow, in full\n * ```ts\n * // SERVER (holds the amk_rt_* key — never the browser):\n * const presign = await aq.assets.presignUploadUrl({ sha256, mime, bytes, fileName, presets });\n * if (presign.deduped) return presign.asset; // those bytes already exist; none fly\n *\n * // BROWSER: PUT straight to presign.upload.url — the bytes never touch your server.\n * // ⚠️ The STORAGE BUCKET answers that preflight itself, so your origin must be in its CORS\n * // policy. Symptom when it is not: \"PUT failed: network error\" with every earlier step\n * // green — and it cannot be fixed in this SDK, in your app, or by the API's allowed origins.\n *\n * // SERVER again, forwarding presign.process.body VERBATIM:\n * const asset = await aq.assets.processAndWait(presign.process.body, { timeoutMs: 300_000 });\n * ```\n *\n * Works for images AND video. A video answers immediately with\n * `{ assetId, status: \"processing\" }` while a background job transcodes, so\n * give `processAndWait` a bigger `timeoutMs` (a transcode + HLS ladder runs\n * 1–2 min; 300_000 is a sane floor).\n */\n async presignUploadUrl(\n opts: PresignUploadUrlOptions,\n ): Promise<UploadUrlResult> {\n const body: Record<string, unknown> = {\n sha256: opts.sha256,\n mime: opts.mime,\n bytes: opts.bytes,\n fileName: opts.fileName,\n };\n if (opts.presets && opts.presets.length > 0) body.presets = opts.presets;\n if (opts.clientOriginalBytes != null)\n body.clientOriginalBytes = opts.clientOriginalBytes;\n if (opts.video != null) body.video = opts.video;\n\n const r = await fetch(endpointHref(this.opts, \"/assets/upload-url\"), {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n if (!r.ok) throw new Error(`upload-url ${r.status}: ${await r.text()}`);\n return (await r.json()) as UploadUrlResult;\n }\n\n /**\n * Dispatch `/assets/process` with the body returned by a prior\n * {@link presignUploadUrl} call, then poll until the asset transitions\n * to `ready` or `failed`. Throws on `failed` or timeout.\n *\n * Use this when bytes were uploaded directly from the browser to storage —\n * `aq.upload()` already does presign + PUT + process + wait in one\n * step when the server holds the bytes.\n */\n async processAndWait(\n processBody: Record<string, unknown>,\n opts: { timeoutMs?: number } = {},\n ): Promise<AssetDTO> {\n const r = await fetch(endpointHref(this.opts, \"/assets/process\"), {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify(processBody),\n });\n if (!r.ok) throw new Error(`process ${r.status}: ${await r.text()}`);\n const proc = (await r.json()) as { assetId?: string };\n if (!proc.assetId) throw new Error(\"process returned no assetId\");\n const final = await this.waitReady(proc.assetId, opts.timeoutMs);\n if (final.status === \"failed\") {\n throw new Error(\n `processAndWait: asset ${proc.assetId} ended status=failed`,\n );\n }\n return final;\n }\n\n /**\n * Poll `GET /assets/:id` until the asset transitions to `ready` or\n * `failed`. Returns the final DTO (whether ready OR failed — callers\n * decide whether to throw on `failed`). Throws on timeout.\n *\n * Default timeout is 5 minutes; videos / HLS ladders may need a\n * higher cap (pass `10 * 60_000` for compositions, transcodes).\n */\n async waitReady(assetId: string, timeoutMs = 5 * 60_000): Promise<AssetDTO> {\n const start = Date.now();\n let delay = 500;\n let last: AssetDTO | null = null;\n while (Date.now() - start < timeoutMs) {\n last = (await this.get(assetId)) as AssetDTO;\n if (last.status === \"ready\" || last.status === \"failed\") return last;\n await new Promise((r) => setTimeout(r, delay));\n delay = Math.min(delay * 1.5, 5_000);\n }\n if (!last) throw new Error(`waitReady: no asset ${assetId}`);\n throw new Error(`waitReady timeout for ${assetId}`);\n }\n\n /**\n * Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip\n * segments into a single MP4 composition. Returns the processing asset\n * id immediately — does NOT block on completion. Callers poll via\n * {@link waitReady} (typical timeout: 10 min for multi-segment kits).\n *\n * Tenant scope is inherited from the SDK client; `tenantCode` is added\n * to the request body so the background job can resolve it without\n * re-reading the header.\n */\n async composeMarketing(\n opts: ComposeMarketingOptions,\n ): Promise<ComposeMarketingResult> {\n const r = await fetch(\n endpointHref(this.opts, \"/assets/compose-marketing\"),\n {\n method: \"POST\",\n headers: { ...this.headers(), \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n tenantCode: this.opts.tenantCode,\n marketingKitId: opts.marketingKitId,\n segments: opts.segments,\n composition: opts.composition ?? {},\n }),\n },\n );\n if (!r.ok) {\n throw new Error(`compose-marketing ${r.status}: ${await r.text()}`);\n }\n const body = (await r.json()) as {\n asset?: { id: string; status: \"processing\" | \"ready\" | \"failed\" };\n assetId?: string;\n status?: \"processing\" | \"ready\" | \"failed\";\n };\n // Server returns `{ asset: { id, status }, dispatch? }`; some older\n // builds returned `{ assetId, status }` directly. Normalize both.\n const assetId = body.asset?.id ?? body.assetId;\n const status = body.asset?.status ?? body.status ?? \"processing\";\n if (!assetId) {\n throw new Error(\"compose-marketing: response missing assetId\");\n }\n return { assetId, status };\n }\n\n private headers(): Record<string, string> {\n return authHeaders(this.opts);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Upload helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Best-effort MIME from a file name's extension. Used as a fallback for `Uint8Array`\n * uploads (which carry no inherent type) so they still get classified correctly instead\n * of silently becoming `application/octet-stream` → `kind:\"other\"`. Returns `null` when\n * the extension is unknown.\n */\nconst MIME_BY_EXT: Record<string, string> = {\n webp: \"image/webp\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n heic: \"image/heic\",\n heif: \"image/heif\",\n svg: \"image/svg+xml\",\n bmp: \"image/bmp\",\n tiff: \"image/tiff\",\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n mov: \"video/quicktime\",\n m4v: \"video/x-m4v\",\n pdf: \"application/pdf\",\n};\nexport function mimeFromFileName(fileName: string | undefined): string | null {\n if (!fileName) return null;\n const ext = fileName.split(\".\").pop()?.toLowerCase();\n return ext ? (MIME_BY_EXT[ext] ?? null) : null;\n}\n\n/**\n * Subset of compressorjs options exposed through the SDK. Re-imported here\n * to avoid a hard import dependency on `./web` from this top-level module\n * (the /web subpath uses browser-only APIs). The runtime `compress`\n * implementation is lazy-loaded so Node/Bun callers don't pay the bundle\n * cost — see `aq.upload` below.\n */\nexport type CompressOptions = {\n quality?: number;\n mimeType?: \"image/jpeg\" | \"image/webp\";\n maxWidth?: number;\n maxHeight?: number;\n convertSize?: number;\n strict?: boolean;\n keepOriginalDimensions?: boolean;\n convertHeic?: boolean;\n onProgress?: (\n stage: \"convertingHeic\" | \"compressing\" | \"compressingKeepingDimensions\",\n ) => void;\n};\n\nexport type UploadOptions = {\n fileName?: string;\n /**\n * MIME type of the bytes. **Only needed for a `Uint8Array` input** — a `File`/`Blob`\n * already carries its `.type`. Raw bytes have no inherent MIME, so without this (and\n * without an extension on `fileName` to infer from) they upload as\n * `application/octet-stream`, which the server classifies as `kind:\"other\"` —\n * meaning NO image/video variants are generated and `regenerate()` is unsupported.\n * Resolution order for the effective MIME: `Blob.type` → `contentType` →\n * inferred from `fileName`'s extension → `application/octet-stream`.\n *\n * aq.upload(bytes, { fileName: \"cover.webp\" }) // inferred → image/webp ✓\n * aq.upload(bytes, { contentType: \"image/webp\" }) // explicit ✓\n * aq.upload(bytes) // octet-stream → kind:\"other\" ⚠\n */\n contentType?: string;\n /** Computed sha256 of bytes. Skip to compute locally with WebCrypto (browser only). */\n sha256?: string;\n /**\n * Client-side compression before upload. Saves user bandwidth — typical\n * 5–10× reduction for raw phone photos. Browser-only; in Node/Bun this\n * silently no-ops with a console.warn and the raw bytes upload as-is.\n *\n * - `true` → use SDK `DEFAULT_COMPRESSION_OPTIONS` (webapp-tuned)\n * - `CompressOptions` → merge over defaults\n * - `false` / omit → no compression (current default behavior)\n *\n * Implementation is lazy-imported from `@nitida/sdk/web` so callers\n * that never set `compress` don't pay the compressorjs + heic2any\n * bundle cost. Skipped for non-image MIMEs (video, PDF) regardless of\n * this option — those go to the upload pipeline raw.\n *\n * @see {@link CompressOptions}\n * @see https://nitida.gofuture.space/guides/advanced/ — client-side compression\n */\n compress?: boolean | CompressOptions;\n /**\n * Variant set to generate. **Defaults to `[\"original\"]`** —\n * if you omit this option, only the raw bytes land on the CDN\n * under the `o` path. Pass an explicit array to request more.\n *\n * Image presets (`thumb` 256 · `sm` 640 · `md` 1280 · `lg` 1920 ·\n * `xl` 3840 · `original`):\n * - `[\"original\"]` (default) → just the raw bytes. Right call for\n * logos / SVGs / anything you'll resize browser-side or via\n * `aq.assets.regenerate(id, { presets: [\"thumb\"] })` later.\n * - `[\"thumb\",\"sm\",\"md\",\"lg\"]` → the classic responsive ladder.\n * - `[\"thumb\",\"sm\",\"md\",\"lg\",\"xl\"]` → add 4K.\n *\n * Video presets (`poster`, `video`, `aiproxy`): omit `aiproxy` if\n * the tenant doesn't need the low-res transcode for AI captioning.\n *\n * No upscaling. Each size preset is a **ceiling**; a 1080×720 source\n * asked for `xl` (3840) yields a 1080×720 xl variant, not a stretched\n * 3840-wide image.\n *\n * Idempotent: you can always add missing variants later via\n * `aq.assets.regenerate(id, { presets: [...] })`. The platform\n * stores the source so regeneration doesn't require re-uploading.\n *\n * {@link RequestablePreset}, not {@link VariantPreset}. `hls` and `mp3` are\n * produced FOR you — the ladder when a video transcodes, the mp3 alongside\n * any audio original — and asking for either is a 400.\n */\n presets?: RequestablePreset[];\n /**\n * Max time to wait for the asset to transition to `ready` (or `failed`)\n * after dispatch. Default `5 * 60_000` (5 min). Bump higher for large\n * videos / HLS transcodes — processing time scales with input size,\n * and with how much other work the platform is doing at that moment.\n *\n * Throws `Error(\"waitReady timeout for <id>\")` if the deadline passes\n * without the asset transitioning. The asset row stays in aquienpz\n * (status=\"processing\") and the next byHash lookup will return it once\n * processing completes; the caller can resume with their own poll.\n */\n timeoutMs?: number;\n /**\n * VIDEO-only delivery knobs forwarded into `/assets/process`. See\n * {@link UploadVideoOptions}. Ignored for non-video uploads.\n *\n * // A delivery-ready reel: skip the unused HLS ladder + skip re-encode.\n * await aq.upload(mp4Bytes, {\n * fileName: \"reel.mp4\",\n * presets: [\"poster\", \"video\"],\n * video: { hls: false, passthrough: true },\n * });\n */\n video?: UploadVideoOptions;\n};\n\n/**\n * Default preset set the SDK sends to `/assets/upload-url` when the caller\n * omits `presets`.\n *\n * Typed `RequestablePreset[]`, not `VariantPreset[]` — it is a REQUEST. The\n * distinction caught this very line the moment it was introduced: it was the\n * wrong type here, and a `VariantPreset[]` default could have carried `hls`\n * into a request that answers 400.\n */\nconst DEFAULT_UPLOAD_PRESETS: RequestablePreset[] = [\"original\"];\n\nexport type UploadResult = {\n assetId: string;\n sha256: string;\n /**\n * The SAME 16-hex prefix an `AssetDTO` carries, so the result of an upload can\n * be handed straight to any URL builder.\n *\n * ⭐ It exists because it did not, and that cost a real 404. A Haiku agent\n * evaluating the SDK on 2026-08-23 did the most natural thing there is —\n * `transform(await upload(file), { width: 1280 })` — and got\n * `https://8ok.uk/t/width=1280/undefined.webp`. The builders read `sha`; this\n * type only had `sha256`. TypeScript caught it; running through `bun`, or in\n * plain JS, nothing did.\n *\n * The guard (`assertSha`) is the backstop. This field is the actual fix: the\n * obvious call is now the correct one, which is worth more than a good error\n * message about the wrong one.\n */\n sha: string;\n cdnUrl: string;\n};\n\nasync function computeSha256(\n bytes: ArrayBuffer | Uint8Array | Blob,\n): Promise<string> {\n const buf =\n bytes instanceof Blob\n ? await bytes.arrayBuffer()\n : bytes instanceof Uint8Array\n ? (bytes.buffer as ArrayBuffer)\n : bytes;\n const digest = await crypto.subtle.digest(\"SHA-256\", buf);\n return [...new Uint8Array(digest)]\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n// ---------------------------------------------------------------------------\n// Main facade\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Usage (Cloudinary-style consumption dashboard data)\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Slot history\n// ---------------------------------------------------------------------------\n\nexport type SlotHistoryEntry = {\n id: string;\n action: \"bind\" | \"unbind\" | \"restore\";\n preset: VariantPreset | null;\n description: string | null;\n updatedAt: string;\n updatedBy: string | null;\n assetId: string | null;\n /** Resolved DTO when the asset still exists; `null` after delete / 404. */\n asset: AssetDTO | null;\n};\n\nexport type UsageSnapshot = {\n tenant: { id: number; code: string };\n storage: { totalBytes: number; assetCount: number };\n today: UsageWindow;\n last30Days: UsageWindow;\n};\n\nexport type UsageWindow = {\n reads: number;\n writes: number;\n lists: number;\n deletes: number;\n admins: number;\n upscales: number;\n processes: number;\n /**\n * Minutes of video actually TRANSCODED in the window — source minutes ×\n * encode passes, so a 7-rung HLS ladder over a 2-minute clip books 14.\n *\n * A re-encode of a video already ingested counts AGAIN. That is the point:\n * an asset count cannot report it, because re-encoding an asset that already\n * exists creates no new asset.\n *\n * A stream copy (`video: { passthrough: true }`, or an idempotent cache skip)\n * books nothing, so a tenant that ran no encoder reads `0` — and that `0` is real.\n */\n videoMinutes: number;\n /** Encode passes behind `videoMinutes`. 0 when nothing was encoded. */\n videoEncodes: number;\n};\n\n/**\n * ⚠️ REMOVED 2026-08-17: `bytesIn` on `UsageWindow`/`UsageDailyPoint` and\n * `bytesTotal` on `UsagePerKey`.\n *\n * All three were structurally zero: the field behind them was never populated,\n * on any row. They could not be instrumented in place either — uploads go to\n * object storage through presigned URLs, so the payload never passes through\n * the API, and the only figure measurable there is a JSON envelope of a few\n * hundred bytes.\n *\n * A field that always reads `0` is worse than an absent field, because absent\n * is honest — a `0` next to a real `storage.totalBytes` reads as a measurement.\n * The byte number that IS true is still served: `storage.totalBytes`.\n */\n\nexport type UsageDailyPoint = {\n date: string;\n reads: number;\n writes: number;\n lists: number;\n deletes: number;\n processes: number;\n bytesStored: number;\n /** See `UsageWindow.videoMinutes`. Minutes transcoded on this day. */\n videoMinutes: number;\n videoEncodes: number;\n};\n\nexport type UsagePerKey = {\n apiKeyId: string;\n prefix: string | null;\n name: string | null;\n opsTotal: number;\n lastSeen: string;\n};\n\nclass UsageApi {\n constructor(private readonly opts: NitidaClientOptions) {}\n\n /** Snapshot for the active tenant — storage + today + last 30 days totals. */\n async snapshot(): Promise<UsageSnapshot> {\n const r = await fetch(endpointHref(this.opts, \"/usage\"), {\n headers: this.headers(),\n });\n if (!r.ok) throw new Error(`usage snapshot ${r.status}: ${await r.text()}`);\n return (await r.json()) as UsageSnapshot;\n }\n\n /** Daily rollup for charts — 1..365 days, default 30. */\n async timeseries(days = 30): Promise<{\n tenant: { id: number; code: string };\n days: UsageDailyPoint[];\n }> {\n const r = await fetch(\n endpointUrl(this.opts, \"/usage/timeseries\", { days }),\n { headers: this.headers() },\n );\n if (!r.ok)\n throw new Error(`usage timeseries ${r.status}: ${await r.text()}`);\n return (await r.json()) as {\n tenant: { id: number; code: string };\n days: UsageDailyPoint[];\n };\n }\n\n /** Per-API-key breakdown for the current month. */\n async keys(): Promise<{\n tenant: { id: number; code: string };\n monthStart: string;\n keys: UsagePerKey[];\n }> {\n const r = await fetch(endpointHref(this.opts, \"/usage/keys\"), {\n headers: this.headers(),\n });\n if (!r.ok) throw new Error(`usage keys ${r.status}: ${await r.text()}`);\n return (await r.json()) as {\n tenant: { id: number; code: string };\n monthStart: string;\n keys: UsagePerKey[];\n };\n }\n\n private headers(): Record<string, string> {\n return authHeaders(this.opts);\n }\n}\n\nexport { isUniversallyPlayableAudio } from \"./audio-compat\";\n\nexport class NitidaClient {\n readonly slots: SlotsApi;\n readonly assets: AssetsApi;\n readonly usage: UsageApi;\n /**\n * Effective options — read-only. Exposed so the `/web` and `/expo`\n * subpaths can inherit endpoint / apiKey / tenant scope from the\n * configured client without re-passing them per call site.\n */\n readonly opts: NitidaClientOptions;\n\n constructor(opts: NitidaClientOptions) {\n this.opts = opts;\n const cdn = opts.cdnBase ?? \"https://8ok.uk\";\n setCdnBase(cdn);\n // Configure the process-global tenant so every variant URL builder\n // (`getAssetUrl`, `urlFor`, `srcSetFor`, upload `cdnUrl`) emits the\n // tenant-prefixed path `<cdn>/<tid b36>/v/<sha>-<preset>.<ext>`.\n setTenantId(opts.tenantId);\n configureSlotResolver({\n endpoint: opts.endpoint,\n apiKey: opts.apiKey,\n tenantCode: opts.tenantCode,\n });\n this.slots = new SlotsApi(opts);\n this.assets = new AssetsApi(opts);\n this.usage = new UsageApi(opts);\n }\n\n /** Tenant id as base36 path segment (e.g. tenantId=4 → \"4/v/\"). */\n get tenantSegment(): string {\n return `${this.opts.tenantId.toString(36)}/v/`;\n }\n\n /** Build the canonical CDN URL deterministically from sha + preset. */\n urlFor(asset: Pick<AssetDTO, \"sha\">, preset: VariantPreset = \"lg\"): string {\n return getAssetUrl(asset, preset);\n }\n\n /** Build a responsive srcSet across the available image presets. */\n srcSetFor(asset: Pick<AssetDTO, \"sha\" | \"presets\">): string {\n return getAssetSrcSet(asset);\n }\n\n /**\n * Build an on-the-fly transform URL — `<cdn>/t/<dsl>/<sha>.<ext>`.\n *\n * ## URL CONVENTION — transforms are NOT tenant-prefixed (variants are)\n * Two distinct delivery paths, by design:\n * - **Variants / presets** (`urlFor`, `srcSetFor`, upload `cdnUrl`):\n * `<cdn>/<tenantId b36>/v/<sha>-<preset>.<ext>` ← tenant-scoped (e.g. `/4/v/<sha>-lg.webp`)\n * - **On-the-fly transforms** (`transform`, `transformSrcSet`):\n * `<cdn>/t/<dsl>/<sha>.<ext>` ← GLOBAL, no tenant segment (`/t/...`)\n * The transform service is content-addressed by sha + resizes from the source on demand,\n * so it needs no tenant in the path. Prefixing a transform URL with `/<tenant>/t/...` 404s.\n * Consumers that build URLs by hand must NOT add the tenant segment to `/t/` URLs.\n *\n * Returns the canonical `lg` variant URL when called with empty options,\n * so callers can swap `urlFor()` for `transform()` without thinking.\n *\n * URLs with the same params in different order produce the same\n * cache entry (the server canonicalizes both sides). Safe to use as\n * stable cache keys.\n *\n * <Image\n * src={aq.transform(asset, { width: 1280 })}\n * srcSet={aq.transformSrcSet(asset, [640, 960, 1280, 1920])}\n * sizes=\"(max-width: 768px) 100vw, 50vw\"\n * />\n *\n * @see {@link TransformOptions} for the full param matrix.\n */\n // Overload 1: no signing — synchronous, on-ladder width only (strict).\n transform(asset: Pick<AssetDTO, \"sha\">, opts?: TransformOptions): string;\n // Overload 2: with { sign: true } — async; returns `?sig=<hmac>` URL.\n // Accepts SignedTransformOptions so a signed URL may carry an off-ladder\n // custom width (the signature earns the edge whitelist bypass).\n transform(\n asset: Pick<AssetDTO, \"sha\">,\n opts: SignedTransformOptions,\n signOpts: { sign: true },\n ): Promise<string>;\n transform(\n asset: Pick<AssetDTO, \"sha\">,\n opts: SignedTransformOptions = {},\n signOpts?: { sign: true },\n ): string | Promise<string> {\n if (!signOpts?.sign) {\n // Unsigned path. Overload 1 constrains `width` to the ladder at every\n // public call site, so an off-ladder width can't reach here through the\n // typed API — narrow back to TransformOptions for the strict builder.\n return (\n getTransformUrl(asset, opts as TransformOptions) ??\n this.urlFor(asset, \"lg\")\n );\n }\n if (!this.opts.signingKey) {\n throw new Error(\n \"aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. \" +\n \"No signingKey on this client. The key is minted by POST /admin/projects/:code/rotate-signing-key, which needs a system-scope credential the platform operator holds — your own admin key gets 403 SYSTEM_KEY_REQUIRED, so ask for it. Then pass it to the SDK constructor on a SERVER-side instance only.\",\n );\n }\n // Signed path — custom (off-ladder) widths allowed. Empty opts → no\n // transform DSL, fall back to the unsigned `lg` variant URL.\n return (\n getSignedTransformUrl(asset, opts, this.opts.signingKey) ??\n Promise.resolve(this.urlFor(asset, \"lg\"))\n );\n }\n\n /**\n * Build a responsive `srcSet` string. One transform URL per width; all\n * other options apply to every URL.\n *\n * Pass `{ sign: true }` to return signed URLs (async). Without it, the\n * call stays synchronous as before.\n */\n transformSrcSet(\n asset: Pick<AssetDTO, \"sha\">,\n widths: number[],\n extraOpts?: Omit<TransformOptions, \"width\">,\n ): string;\n transformSrcSet(\n asset: Pick<AssetDTO, \"sha\">,\n widths: number[],\n extraOpts: Omit<TransformOptions, \"width\">,\n signOpts: { sign: true },\n ): Promise<string>;\n transformSrcSet(\n asset: Pick<AssetDTO, \"sha\">,\n widths: number[],\n extraOpts: Omit<TransformOptions, \"width\"> = {},\n signOpts?: { sign: true },\n ): string | Promise<string> {\n if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);\n if (!this.opts.signingKey) {\n throw new Error(\n \"aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions.\",\n );\n }\n const key = this.opts.signingKey;\n return Promise.all(\n widths.map(async (w) => {\n // Signed path → off-ladder widths allowed; build+sign via the\n // custom-width helper (the strict `getTransformUrl` would reject a\n // raw `number` width).\n const signed = await getSignedTransformUrl(\n asset,\n { ...extraOpts, width: w },\n key,\n );\n return signed ? `${signed} ${w}w` : null;\n }),\n ).then((parts) => parts.filter((s): s is string => s != null).join(\", \"));\n }\n\n /**\n * Build an on-the-fly VIDEO transform URL — Phase 4.\n *\n * Same DSL shape as `transform()` but the URL has a `.mp4` (default)\n * or `.webm` extension and the server routes the request to a\n * background job for video encoding (vs the inline pipeline for\n * images).\n *\n * On the first request the route returns **202 Accepted** with\n * `Retry-After: 10` while the encode runs (typically 5-30 s for a\n * short clip). The response body includes `outputUrl` which is the\n * eventual CDN URL — poll the same transform URL after the\n * retry-after window to get a 302 redirect to it.\n *\n * const url = aq.transformVideo(asset, {\n * width: 1080, height: 1920, fit: \"cover\",\n * start: 0, duration: 15,\n * });\n * // Pass to Video.js / <video src={url}>; on the first load it\n * // gets 202 + body.outputUrl; subsequent loads hit cache → 302.\n *\n * Video-specific DSL params:\n * - `start` (seconds, decimal OK)\n * - `duration` (seconds, 1..300)\n * - `format`: \"mp4\" (default) or \"webm\"\n *\n * The other params (`width`, `height`, `fit`) work identically to\n * image transforms. `gravity`, `quality`, `effect`, `dpr` are\n * accepted by the DSL but currently ignored on the video path.\n */\n transformVideo(\n asset: Pick<AssetDTO, \"sha\">,\n opts: TransformOptions = {},\n ): string {\n return getVideoTransformUrl(asset, opts) ?? this.urlFor(asset, \"lg\");\n }\n\n /**\n * Build the HLS master playlist URL for a VIDEO asset (Phase 5).\n *\n * Returns `<cdn>/t/format=hls(,start=…,duration=…)/<sha>.m3u8`. Pass\n * to an HLS-aware player:\n *\n * <video\n * src={aq.streamingUrl(asset)}\n * controls playsInline\n * // Video.js v10's @videojs/http-streaming ships native HLS —\n * // no plugin needed.\n * />\n *\n * On the first request the server returns **202 Accepted** while a\n * background job builds the multi-rung ladder (typically 1-3 min for\n * a 90 s source — five rungs of 240p/360p/480p/720p/1080p @ AAC).\n * Subsequent requests hit the cache → **302** to the master.m3u8.\n *\n * Supports `start` + `duration` to ladder a sub-clip. Other DSL\n * params (width, height, fit) are ignored on the HLS path because\n * the rungs determine resolution.\n */\n streamingUrl(\n asset: Pick<AssetDTO, \"sha\">,\n opts: Omit<TransformOptions, \"format\"> = {},\n ): string {\n return getHlsStreamingUrl(asset, opts);\n }\n\n /**\n * Upload a file or raw bytes. Returns the new asset id + canonical\n * URL. Hash-deduped — uploading the same bytes twice returns the\n * existing asset.\n *\n * Browser-first: uses `Blob` + WebCrypto. For Node 20+, pass a\n * Uint8Array and a precomputed `sha256` (since `crypto.subtle` works\n * but isn't always available depending on the runtime).\n */\n /**\n * Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-storage PUT**\n * → `/assets/process` → wait until the asset is ready.\n *\n * ⚠️ `presets` decides what exists FOREVER. Omit it and only `original` is written; ask for\n * `[\"thumb\"]` and the bytes you just uploaded are **not retrievable**. A variant not requested in\n * this first ingest cannot be added later once the ~24 h grace window on the uploaded bytes\n * closes — measured once as \"97 files archived successfully, zero recoverable\".\n *\n * @example Deliver an image on a site (the responsive ladder)\n * ```ts\n * import { NitidaClient } from \"@nitida/sdk/server\";\n *\n * const aq = new NitidaClient({ endpoint, apiKey, tenantCode, tenantId });\n * const { assetId, sha256 } = await aq.upload(file, {\n * fileName: file.name,\n * presets: [\"thumb\", \"sm\", \"md\", \"lg\"],\n * });\n * ```\n *\n * @example ARCHIVE a file — you must ask for `original`\n * ```ts\n * await aq.upload(bytes, {\n * fileName: \"contrato.pdf\",\n * contentType: \"application/pdf\",\n * presets: [\"original\"], // without this the bytes are unrecoverable\n * });\n * ```\n *\n * @example Raw bytes need an explicit MIME\n * ```ts\n * await aq.upload(bytes, { fileName: \"track.mp3\", contentType: \"audio/mpeg\" });\n * // Without either, it stores as kind:\"other\" — no variants, and regenerate() is unsupported.\n * ```\n *\n * @example Video — and what does NOT work there\n * ```ts\n * // `original` is accepted and then silently DROPPED: /assets/process filters video presets to\n * // {poster, video, aiproxy, probe} before dispatching the background transcode.\n * await aq.upload(clip, { fileName: \"tour.mp4\", presets: [\"poster\", \"video\"] });\n *\n * // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost encode\n * // time and permanent stored objects that nothing else reads.\n * ```\n */\n async upload(\n input: File | Blob | Uint8Array,\n opts: UploadOptions = {},\n ): Promise<UploadResult> {\n // 1. Resolve incoming bytes + mime first (the source of truth for\n // \"should we compress this?\")\n const sourceIsBlob = input instanceof File || input instanceof Blob;\n // A Uint8Array has no inherent MIME. Fall back to an explicit `contentType`, then to\n // the file extension, before octet-stream (which the server files as\n // `kind:\"other\"` — no variants). A File/Blob's own `.type` always wins when present.\n const sourceMime =\n (sourceIsBlob ? input.type : \"\") ||\n opts.contentType ||\n mimeFromFileName(opts.fileName) ||\n \"application/octet-stream\";\n if (!sourceIsBlob && sourceMime === \"application/octet-stream\") {\n console.warn(\n \"[@nitida/sdk] upload(Uint8Array): no MIME resolved (no `contentType`, no recognizable \" +\n '`fileName` extension) — the asset will be stored as kind:\"other\" with NO image/video ' +\n \"variants and regenerate() unsupported. Pass `contentType` or a `fileName` with an extension.\",\n );\n }\n\n // 2. Optional client-side compression. Image MIMEs only; non-image\n // sources pass through unchanged so a video or PDF upload still\n // works when the caller sets `compress: true` blanket-fashion.\n let bytes: Uint8Array;\n let effectiveMime: string;\n let clientOriginalBytes: number | undefined;\n\n const wantCompression =\n !!opts.compress &&\n sourceIsBlob &&\n typeof window !== \"undefined\" &&\n sourceMime.startsWith(\"image/\");\n\n if (wantCompression) {\n const compressOpts =\n opts.compress === true ? {} : (opts.compress as CompressOptions);\n // Lazy: keeps compressorjs/heic2any out of bundles that don't use it.\n //\n // Resolve `./web` against `import.meta.url` at runtime so no static\n // analyzer — esbuild, tsup, `bun build --compile`, vite, rollup —\n // can follow the specifier. Holding the path in a plain `const`\n // was not enough: `bun build --compile` constant-folds simple\n // strings and still eagerly bundled `./web.js`, which\n // top-level-imports the browser-only peer deps\n // `@aquienpz/asset-uploader-web` / `@nitida/asset-compressor-web`\n // — neither installed on server consumers — crashing the\n // single-binary on boot with `Cannot find module\n // '@aquienpz/asset-uploader-web'`.\n //\n // The earlier `new URL(\"./web.js\", import.meta.url)` + `await import(URL)`\n // dance survived bun-compile but Turbopack still tracks the URL literal\n // and tries to resolve `./web.js` at build time (no such file exists in\n // src/, only src/web/index.ts), failing with \"Module not found\".\n //\n // Indirect-eval via the Function constructor is opaque to BOTH static\n // analyzers — neither bun-compile nor Turbopack can follow the string\n // back to a module specifier. The browser `typeof window` guard above\n // ensures the branch never runs on the server, so the indirection is\n // safe at runtime.\n // Build the URL inside the Function body too — Turbopack still tracks\n // `new URL(<any expr>, import.meta.url)` as a resolution pattern even\n // when the first arg isn't a literal, so we have to hide both the URL\n // construction AND the import() behind indirect eval.\n const dynImport = new Function(\n \"base\",\n \"return import(new URL('./' + 'web' + '.js', base).href)\",\n ) as (base: string) => Promise<typeof import(\"./web\")>;\n const { compressImage } = await dynImport(import.meta.url);\n const result = await compressImage(input as File | Blob, compressOpts);\n bytes = new Uint8Array(await result.blob.arrayBuffer());\n clientOriginalBytes = result.originalBytes;\n effectiveMime = result.blob.type || sourceMime;\n } else {\n if (opts.compress && !sourceIsBlob) {\n console.warn(\n \"[@nitida/sdk] compress: true requires a File or Blob input; got Uint8Array — uploading raw bytes.\",\n );\n } else if (opts.compress && typeof window === \"undefined\") {\n console.warn(\n \"[@nitida/sdk] compress: true is browser-only — uploading raw bytes.\",\n );\n } else if (opts.compress && !sourceMime.startsWith(\"image/\")) {\n // Silent: callers can set `compress: true` for batched mixed\n // media and not have to special-case images vs videos.\n }\n bytes =\n input instanceof Uint8Array\n ? input\n : new Uint8Array(await input.arrayBuffer());\n effectiveMime = sourceMime;\n }\n\n const sha = opts.sha256 ?? (await computeSha256(bytes));\n const mime = effectiveMime;\n const fileName =\n opts.fileName ??\n (input instanceof File ? input.name : `upload-${sha.slice(0, 8)}.bin`);\n\n // Dedup probe.\n const existing = await this.assets.byHash(sha);\n if (existing && existing.status === \"ready\") {\n return {\n assetId: existing.id,\n sha256: sha,\n sha: sha.slice(0, 16),\n cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime)),\n };\n }\n\n // Presign + PUT + process. Delegates to `assets.presignUploadUrl()` so\n // the public method and `aq.upload()` share one code path; always\n // threads an explicit preset list so the default matches the SDK\n // contract (`[\"original\"]`) instead of inheriting the server's older\n // default (`thumb+sm+md+lg`).\n const presign = await this.assets.presignUploadUrl({\n sha256: sha,\n mime,\n bytes: bytes.byteLength,\n fileName,\n presets:\n opts.presets && opts.presets.length > 0\n ? opts.presets\n : DEFAULT_UPLOAD_PRESETS,\n ...(clientOriginalBytes != null && { clientOriginalBytes }),\n ...(opts.video != null && { video: opts.video }),\n });\n if (presign.deduped) {\n return {\n assetId: presign.asset.id,\n sha256: sha,\n sha: sha.slice(0, 16),\n cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime)),\n };\n }\n\n const putR = await fetch(presign.upload.url, {\n method: \"PUT\",\n headers: { \"Content-Type\": mime, ...(presign.upload.headers ?? {}) },\n body: new Blob([bytes as unknown as ArrayBuffer], { type: mime }),\n });\n if (!putR.ok)\n throw new Error(`Storage PUT ${putR.status}: ${await putR.text()}`);\n\n const procR = await fetch(endpointHref(this.opts, presign.process.url), {\n method: \"POST\",\n headers: {\n ...authHeaders(this.opts),\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(presign.process.body),\n });\n if (!procR.ok)\n throw new Error(`process ${procR.status}: ${await procR.text()}`);\n const proc = (await procR.json()) as { assetId?: string; kind?: string };\n\n let assetId = proc.assetId;\n if (!assetId && proc.kind === \"video\") {\n // Videos are async — poll by-hash until the row lands.\n const start = Date.now();\n let delay = 1000;\n while (Date.now() - start < 60_000) {\n const dto = await this.assets.byHash(sha);\n if (dto?.id) {\n assetId = dto.id;\n break;\n }\n await new Promise((r) => setTimeout(r, delay));\n delay = Math.min(delay * 1.5, 5_000);\n }\n }\n if (!assetId) throw new Error(\"upload: process returned no assetId\");\n\n // Wait until variants are ready so the URL works immediately.\n // Passing `undefined` keeps waitReady's own default (5 min). Callers\n // uploading large videos can opt in to a longer deadline via opts.timeoutMs.\n const final = await this.assets.waitReady(assetId, opts.timeoutMs);\n if (final.status !== \"ready\")\n throw new Error(`upload: asset ended status=${final.status}`);\n return {\n assetId,\n sha256: sha,\n sha: sha.slice(0, 16),\n cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime)),\n };\n }\n\n private defaultPresetForMime(mime: string): VariantPreset {\n if (mime.startsWith(\"video/\")) return \"video\";\n // The `mp3` variant only exists AFTER /process transcodes it; at presign\n // time only the original is guaranteed to be playable, so bind to it.\n if (mime.startsWith(\"audio/\")) return \"original\";\n return \"lg\";\n }\n\n /**\n * Pick a sensible preset to build a URL for, given the asset's actual\n * `presets` string. Falls back through the preference order\n * lg → md → sm → thumb → original (for images)\n * video → poster (for videos)\n * original → mp3 (for audio ALREADY playable everywhere)\n * mp3 → original (for any other audio)\n * so an upload that was processed with e.g. `[\"original\"]` still\n * returns a non-404 URL in `aq.upload`'s result.\n *\n * ⭐ Why audio branches on the source mime (changed 2026-08-22).\n *\n * It used to be `mp3 → original` unconditionally, so `upload().cdnUrl`\n * handed back the server's auto-generated mp3 — libmp3lame, mono ~96 kbps —\n * even when the caller had uploaded an MP3 or an AAC that already plays in\n * every target browser. A consumer asking for \"my file\" silently received a\n * re-encoded, lower-quality one, with no error to notice.\n *\n * Measured across the platform: of 126 audio assets carrying an mp3 variant,\n * **105 had an `audio/mpeg` source** — an MP3 re-encoded into an MP3, for\n * zero compatibility gain.\n *\n * The mp3 still wins for `audio/webm`/Opus and anything exotic, which is the\n * case it was built for: Chrome records webm/Opus, which iOS Safari cannot\n * decode. That guarantee is preserved exactly; only the needless downgrade\n * is gone.\n */\n private bestPresetForAsset(asset: AssetDTO, mime: string): VariantPreset {\n const order: VariantPreset[] = mime.startsWith(\"video/\")\n ? [\"video\", \"poster\"]\n : mime.startsWith(\"audio/\")\n ? isUniversallyPlayableAudio(mime)\n ? [\"original\", \"mp3\"] // the upload already plays everywhere — don't hand back a re-encode\n : [\"mp3\", \"original\"] // exotic codec: the cross-browser mp3 earns its place\n : [\"lg\", \"md\", \"sm\", \"thumb\", \"xl\", \"original\"];\n return (\n order.find((p) => hasPreset(asset, p)) ?? this.defaultPresetForMime(mime)\n );\n }\n}\n","/**\n * Which audio uploads already play everywhere — the single source of truth for\n * both the server's transcode guard and the client's delivery-preset choice.\n *\n * It lives in its own module, rather than beside the preset order that uses it,\n * for a mechanical reason: `apps/asset-manager` imports it by relative path.\n * The SDK is a PUBLISHED package, so its `exports` point at `dist/`, and\n * `dist/` is gitignored — it does not exist inside the Docker image, which\n * copies `packages/` as source. A bare `@nitida/sdk` import type-checks on any\n * machine that built the package once and then fails the image build. (It did,\n * on 2026-08-22, with `@nitida/asset-client`.) A single small file keeps that\n * relative import from dragging the whole client into the server.\n *\n * Two copies of this list drifting apart is how a platform ends up generating\n * a variant its own client refuses to use — which is exactly the bug this\n * predicate was introduced to end.\n */\n\n/**\n * `true` when a browser can play these bytes as uploaded, so re-encoding them\n * to MP3 buys nothing.\n *\n * - `audio/mpeg` — unambiguous.\n * - `audio/mp4` / `audio/aac` — plays in Safari, Chrome, Firefox and Edge on\n * desktop and mobile. The historical worry was old AOSP builds without\n * proprietary codecs; the call to treat AAC as universal was made with the\n * platform's one real AAC consumer, whose viewer is WebGL — a device that\n * cannot decode AAC cannot run that product at all, so the fallback would\n * only ever protect a device that had already lost.\n *\n * Deliberately NOT here: `audio/webm` and `audio/ogg` (Opus). iOS Safari\n * cannot decode them, and that is the case the MP3 fallback exists for.\n */\nexport function isUniversallyPlayableAudio(mime: string): boolean {\n const base = (mime.split(\";\")[0] ?? \"\").trim().toLowerCase();\n return base === \"audio/mpeg\" || base === \"audio/mp4\" || base === \"audio/aac\";\n}\n","/**\n * @nitida/sdk/server — server-safe entry point.\n *\n * Use this subpath from Node.js, Bun, Cloud Run, Lambda, Vercel Functions,\n * edge runtimes, agents, cron jobs, BFFs — anywhere there's no `window`\n * and you want a hard guarantee that no browser-only code lands in your\n * bundle. The constructor REQUIRES `apiKey`; the type from `/web` omits\n * it, so the two modes never confuse each other.\n *\n * import { NitidaClient } from \"@nitida/sdk/server\";\n *\n * const aq = new NitidaClient({\n * endpoint: process.env.AQUIENPZ_URL!,\n * apiKey: process.env.AQUIENPZ_API_KEY!, // <- required\n * tenantCode: \"acme-co\",\n * tenantId: 1,\n * // signingKey: optional, only for `aq.transform(..., { sign: true })`\n * });\n *\n * const asset = await aq.assets.byHash(sha256);\n * const hero = aq.transform(asset, { width: 1920 });\n *\n * // Typical BFF use: proxy a browser request through to aquienpz.\n * // The browser side calls `@nitida/sdk/web` against `/api/am/...`\n * // and your route handler forwards here with the real API key.\n *\n * What you get:\n * - `NitidaClient` (slots/assets/usage APIs over plain fetch)\n * - URL builders: `getAssetUrl`, `getTransformUrl`, `getTransformSrcSet`,\n * `getHlsStreamingUrl`, `extractAssetSha`, `signTransformUrl`\n * - `aq.upload(bytes)` works with `Uint8Array` (Node 18+ / Bun ship Blob\n * globally; File-API workflows are documented on the /web subpath instead)\n *\n * What's NOT here (use `@nitida/sdk/web` instead):\n * - `compressImage` (browser-side compressorjs + heic2any)\n *\n * And what exists nowhere: there is no `createWebUploader`. Multipart is not\n * exposed from any subpath — `aq.upload()` is the supported path.\n *\n * Stripe/Cloudinary historically shipped two separate packages\n * (`stripe` vs `@stripe/stripe-js`, `cloudinary` vs `@cloudinary/url-gen`)\n * for this split. Modern providers (Vercel Blob, Uploadthing, Better\n * Auth, AI SDK) use subpaths within one package — same tree-shaking\n * guarantees, single version, no drift. We follow that pattern.\n * @module @nitida/sdk/server\n */\n\nimport { NitidaClient as BaseNitidaClient, type NitidaClientOptions } from \"..\";\n\n/**\n * Server-side constructor options — `apiKey` is REQUIRED here. Use this\n * type whenever you build a client behind a process boundary (Node, Bun,\n * Cloud Run, Vercel Functions, edge runtimes, BFFs).\n *\n * const aq = new NitidaClient({\n * endpoint: process.env.AQUIENPZ_URL!,\n * apiKey: process.env.AQUIENPZ_API_KEY!,\n * tenantCode: \"acme-co\",\n * tenantId: 1,\n * });\n */\nexport type ServerClientOptions = Required<\n Pick<NitidaClientOptions, \"endpoint\" | \"apiKey\" | \"tenantCode\" | \"tenantId\">\n> &\n Pick<NitidaClientOptions, \"cdnBase\" | \"signingKey\">;\n\n/**\n * Server-safe `NitidaClient` — same runtime as the root class, but the\n * constructor type enforces `apiKey` so misconfiguration is a TS build\n * error, not a runtime 401.\n */\nexport class NitidaClient extends BaseNitidaClient {\n constructor(opts: ServerClientOptions) {\n super(opts);\n }\n}\n\n// ---------------------------------------------------------------------------\n// This subpath MIRRORS THE ROOT. Every export of `@nitida/sdk` is here.\n//\n// It is a COMPLETE entry point, not an additive module — a Node consumer is\n// told to import from here and must never have to reach past it. It did:\n// `getHlsLadder` was on the root and missing here, an example in the docs\n// imported it from `/server`, and an agent evaluating the SDK got a\n// SyntaxError at RUNTIME. Measured on 2026-08-21, this list was short by 28\n// of the root's 72 — the whole palette family, every slot helper, the HLS\n// ladder helpers and the preset constants.\n//\n// The completeness is now ASSERTED by scripts/check-published-doc-symbols.ts,\n// which also owns the deny list: an omission has to be justified there, in\n// writing, or the build fails. Do not hand-edit this list to be shorter.\n// ---------------------------------------------------------------------------\nexport {\n type AssetDTO,\n type AssetPalette,\n type AssetVariant,\n accessMessage,\n assertPublic,\n assertSha,\n bestTextContrast,\n type ComposeMarketingComposition,\n type ComposeMarketingOptions,\n type ComposeMarketingResult,\n type ComposeMarketingSegment,\n type CompressOptions,\n computeVariantDimensions,\n configureSlotResolver,\n contrastRatio,\n deriveAccessKey,\n extractAssetSha,\n getAmbientGradient,\n getAssetDimensions,\n getAssetSrcSet,\n getAssetUrl,\n getCdnBase,\n getHlsLadder,\n getHlsStreamingUrl,\n getPaletteBlurBackground,\n getPaletteCssVars,\n getPrivateAssetUrl,\n getPrivateTransformUrl,\n getSignedTransformUrl,\n getTenantId,\n getTextColorForBackground,\n getTransformSrcSet,\n getTransformUrl,\n getVideoTransformUrl,\n type HlsRung,\n hasPreset,\n hlsLadderAlignment,\n invalidateSlotCache,\n isUniversallyPlayableAudio,\n iteratePaletteSwatches,\n mimeFromFileName,\n type NitidaClientOptions,\n type PaletteSwatch,\n PRESET_EXT,\n PRESET_LONG,\n PRESET_MAX_DIM,\n PRESET_SHORT,\n type PresignUploadUrlOptions,\n pickAmbientBackground,\n type RegenerateResult,\n type RequestablePreset,\n type ResolveSlotOptions,\n relativeLuminance,\n resolveSlot,\n resolveSlots,\n type SignAccessOptions,\n type SignedTransformOptions,\n type SlotDTO,\n type SlotHistoryEntry,\n type SlotResolution,\n serializeTransform,\n setCdnBase,\n setTenantId,\n signAccessUrl,\n signTransformUrl,\n TRANSFORM_WIDTHS,\n type TransformEffect,\n type TransformFit,\n type TransformFormat,\n type TransformGravity,\n type TransformOptions,\n type TransformWidth,\n type UploadOptions,\n type UploadResult,\n type UploadUrlResult,\n type UploadVideoOptions,\n type UsageDailyPoint,\n type UsagePerKey,\n type UsageSnapshot,\n type UsageWindow,\n type VariantEntryPreset,\n type VariantPreset,\n type VisibilityHint,\n} from \"..\";\n"],"mappings":"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","names":["configureSlotResolver","getAssetSrcSet","getAssetUrl","getHlsStreamingUrl","getSignedTransformUrl","getTransformSrcSet","getTransformUrl","getVideoTransformUrl","hasPreset","invalidateSlotCache","resolveSlot","resolveSlots","setCdnBase","setTenantId","NitidaClient"]}
|
package/dist/web.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NitidaClient as NitidaClient$1, NitidaClientOptions } from './index.js';
|
|
2
2
|
export { CompressOptions as ClientCompressOptions, ComposeMarketingComposition, ComposeMarketingOptions, ComposeMarketingResult, ComposeMarketingSegment, PresignUploadUrlOptions, RegenerateResult, SlotHistoryEntry, UploadOptions, UploadResult, UploadUrlResult, UploadVideoOptions, UsageDailyPoint, UsagePerKey, UsageSnapshot, UsageWindow, isUniversallyPlayableAudio, mimeFromFileName } from './index.js';
|
|
3
|
-
export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, RequestablePreset, ResolveSlotOptions, SignAccessOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl } from '@nitida/asset-client';
|
|
3
|
+
export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, RequestablePreset, ResolveSlotOptions, SignAccessOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl } from '@nitida/asset-client';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @nitida/sdk/web — browser entry point (BFF-proxy mode).
|
package/dist/web.js
CHANGED
|
@@ -26,6 +26,7 @@ function isUniversallyPlayableAudio(mime) {
|
|
|
26
26
|
import {
|
|
27
27
|
accessMessage,
|
|
28
28
|
assertPublic,
|
|
29
|
+
assertSha,
|
|
29
30
|
bestTextContrast,
|
|
30
31
|
computeVariantDimensions,
|
|
31
32
|
configureSlotResolver as configureSlotResolver2,
|
|
@@ -752,6 +753,7 @@ var NitidaClient = class {
|
|
|
752
753
|
return {
|
|
753
754
|
assetId: existing.id,
|
|
754
755
|
sha256: sha,
|
|
756
|
+
sha: sha.slice(0, 16),
|
|
755
757
|
cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime))
|
|
756
758
|
};
|
|
757
759
|
}
|
|
@@ -768,6 +770,7 @@ var NitidaClient = class {
|
|
|
768
770
|
return {
|
|
769
771
|
assetId: presign.asset.id,
|
|
770
772
|
sha256: sha,
|
|
773
|
+
sha: sha.slice(0, 16),
|
|
771
774
|
cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime))
|
|
772
775
|
};
|
|
773
776
|
}
|
|
@@ -810,6 +813,7 @@ var NitidaClient = class {
|
|
|
810
813
|
return {
|
|
811
814
|
assetId,
|
|
812
815
|
sha256: sha,
|
|
816
|
+
sha: sha.slice(0, 16),
|
|
813
817
|
cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime))
|
|
814
818
|
};
|
|
815
819
|
}
|
|
@@ -922,6 +926,7 @@ export {
|
|
|
922
926
|
TRANSFORM_WIDTHS,
|
|
923
927
|
accessMessage,
|
|
924
928
|
assertPublic,
|
|
929
|
+
assertSha,
|
|
925
930
|
bestTextContrast,
|
|
926
931
|
compressImage,
|
|
927
932
|
compressImages,
|