@nitida/sdk 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,790 @@
1
+ import { ResolveSlotOptions, SlotResolution, SlotDTO, VariantPreset, AssetDTO, AssetVariant, TransformOptions, SignedTransformOptions } from '@nitida/asset-client';
2
+ export { AssetDTO, AssetVariant, ResolveSlotOptions, SignedTransformOptions, SlotDTO, SlotResolution, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, VariantPreset, computeVariantDimensions, extractAssetSha, getAssetDimensions, getAssetSrcSet, getAssetUrl, getHlsStreamingUrl, getSignedTransformUrl, getTenantId, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, serializeTransform, setTenantId, signTransformUrl } from '@nitida/asset-client';
3
+
4
+ /**
5
+ * @nitida/sdk — universal client for the aquienpz multi-tenant asset
6
+ * platform.
7
+ *
8
+ * One ergonomic facade over the underlying packages
9
+ * (`@nitida/asset-client` URL builders + `@aquienpz/asset-uploader-web`
10
+ * + the slot resolver). Auth is a Better Auth API key (`amk_rt_*`)
11
+ * issued by aquienpz `bootstrap-project.ts`; tenant scope comes from
12
+ * the key's metadata (`X-Tenant-Code` is log-only).
13
+ *
14
+ * Usage:
15
+ *
16
+ * import { AquienpzClient } from "@nitida/sdk";
17
+ *
18
+ * const aq = new AquienpzClient({
19
+ * endpoint: "https://aquienpz-asset-manager-xxx.run.app",
20
+ * apiKey: process.env.ASSET_MANAGER_RUNTIME_KEY!,
21
+ * tenantCode: "realtyone-cr",
22
+ * cdnBase: "https://8ok.uk", // optional override
23
+ * tenantId: 4, // required for tenant-prefixed URLs
24
+ * });
25
+ *
26
+ * // Slot system (recommended — admin can rebind without redeploys).
27
+ * const hero = await aq.slots.resolve("storefront.cr.hero");
28
+ * const set = await aq.slots.resolveMany(["a", "b", "c"]);
29
+ *
30
+ * // Lower-level asset operations.
31
+ * const asset = await aq.assets.byHash(sha);
32
+ * const list = await aq.assets.list({ limit: 50 });
33
+ *
34
+ * // Upload bytes / files.
35
+ * const up = await aq.upload(file, { fileName: "cover.jpg" });
36
+ *
37
+ * For React, see `@nitida/sdk/react` (useSlot, useSlots, useAsset).
38
+ * @module @nitida/sdk
39
+ */
40
+
41
+ /**
42
+ * Permissive constructor options for the root `AquienpzClient`.
43
+ *
44
+ * App code should NOT import this type directly — prefer the strict
45
+ * variants from the subpaths:
46
+ *
47
+ * - `WebClientOptions` from `@nitida/sdk/web` (no `apiKey`)
48
+ * - `ServerClientOptions` from `@nitida/sdk/server` (`apiKey` required)
49
+ *
50
+ * This root type is the union both modes resolve to; the underlying class
51
+ * accepts both shapes so subpath wrappers can extend without duplication.
52
+ */
53
+ type AquienpzClientOptions = {
54
+ /**
55
+ * Base URL of the aquienpz asset-manager (Cloud Run service URL).
56
+ *
57
+ * May be relative (e.g. `/api/am`) ONLY in browser contexts where the
58
+ * SDK resolves it against `window.location.origin`. Node/Bun consumers
59
+ * must always pass an absolute URL.
60
+ */
61
+ endpoint: string;
62
+ /**
63
+ * Better Auth API key with the `amk_rt_*` prefix.
64
+ *
65
+ * **Server-only.** Omit when constructing from `@nitida/sdk/web` —
66
+ * your BFF / route handler injects the bearer header in proxy mode.
67
+ */
68
+ apiKey?: string;
69
+ /**
70
+ * Extra headers merged into every request. The documented way for
71
+ * mobile/Expo clients to authenticate a BFF that gates on the Better Auth
72
+ * session: they can't send cookies automatically, so they pass
73
+ * `{ Cookie: authClient.getCookie() }` here (see Better Auth Expo docs,
74
+ * "Making Authenticated Requests to Your Server"). Web/server consumers omit
75
+ * this — browsers attach the same-origin cookie and servers pass `apiKey`.
76
+ */
77
+ headers?: Record<string, string>;
78
+ /** Tenant code — sent as `X-Tenant-Code` (log-only). Authoritative scope is the key's metadata.tenantId. */
79
+ tenantCode: string;
80
+ /** Numeric tenant id — used to build tenant-prefixed CDN URLs `<cdn>/<tenantId b36>/v/<sha>-<preset>.<ext>`. */
81
+ tenantId: number;
82
+ /** Override the public CDN base. Defaults to `https://8ok.uk`. */
83
+ cdnBase?: string;
84
+ /**
85
+ * Tenant's HMAC signing key for transform URLs (Phase 3). Required
86
+ * only when calling `aq.transform(asset, opts, { sign: true })`.
87
+ *
88
+ * Generated server-side per tenant (see `infra/sql/tenants_signed_transforms.sql`);
89
+ * fetch via `GET /admin/tenants/:id` with an admin key. **Keep it
90
+ * server-side only** — do not ship in `NEXT_PUBLIC_*` env vars. Sign
91
+ * URLs from a BFF route handler, or pre-sign at build time.
92
+ */
93
+ signingKey?: string;
94
+ };
95
+
96
+ declare class SlotsApi {
97
+ private readonly opts;
98
+ constructor(opts: AquienpzClientOptions);
99
+ /** Resolve one slot — returns `{slot, preset, url}` or `{slot: null, url: null}` when unbound. */
100
+ resolve(slotKey: string, options?: ResolveSlotOptions): Promise<SlotResolution>;
101
+ /** Bulk-resolve N slots in one HTTP round-trip. */
102
+ resolveMany(slotKeys: string[], options?: ResolveSlotOptions): Promise<Record<string, SlotResolution>>;
103
+ /** List slots for the tenant (admin). Optional prefix filter for tree views. */
104
+ list(opts?: {
105
+ prefix?: string;
106
+ limit?: number;
107
+ }): Promise<SlotDTO[]>;
108
+ /** Bind / rebind a slot to an asset. Admin-only operation. */
109
+ bind(slotKey: string, body: {
110
+ assetId: string;
111
+ preset?: VariantPreset;
112
+ description?: string;
113
+ updatedBy?: string;
114
+ }): Promise<{
115
+ ok: true;
116
+ slotKey: string;
117
+ assetId: string;
118
+ }>;
119
+ /**
120
+ * Recent bindings for a slot. Lets the admin audit who changed
121
+ * what and restore a previous binding without remembering the
122
+ * asset id. Default limit 20, max 100.
123
+ */
124
+ history(slotKey: string, opts?: {
125
+ limit?: number;
126
+ }): Promise<SlotHistoryEntry[]>;
127
+ /**
128
+ * Restore the slot to a previous binding. Equivalent to
129
+ * `bind(key, { assetId: previous.assetId, action: "restore" })`
130
+ * — the audit row is tagged `restore` instead of `bind`.
131
+ */
132
+ restore(slotKey: string, args: {
133
+ assetId: string;
134
+ preset?: VariantPreset;
135
+ updatedBy?: string;
136
+ }): Promise<{
137
+ ok: true;
138
+ slotKey: string;
139
+ assetId: string;
140
+ }>;
141
+ /** Remove a slot binding. The asset itself is left alone. */
142
+ unbind(slotKey: string): Promise<{
143
+ ok: true;
144
+ removed: number;
145
+ }>;
146
+ /** Invalidate the in-process cache after a slot rebind. */
147
+ invalidateCache(slotKey?: string): void;
148
+ private headers;
149
+ }
150
+ /**
151
+ * Returned by `aq.assets.regenerate(...)`. The shape varies by kind —
152
+ * images return immediately with the merged variant list; videos
153
+ * return a dispatch handle (the actual transcode runs in a Cloud Run
154
+ * Job and finishes async).
155
+ */
156
+ type RegenerateResult = {
157
+ ok: true;
158
+ kind: "image";
159
+ /** Full variant set after the merge. */
160
+ variants: AssetVariant[];
161
+ /** Presets newly written this run. Useful for showing "added X". */
162
+ newVariants: VariantPreset[];
163
+ /**
164
+ * Which source the server read to derive the new variants:
165
+ * - `"original"` / `"raw"` → lossless source bytes (best)
166
+ * - `"xl"` / `"lg"` / `"md"` / `"sm"` / `"thumb"` → a previously
167
+ * encoded WebP variant was used as the source. Output is
168
+ * re-encoded WebP — fine for thumb/sm from lg, lossier when
169
+ * working from already-small sources.
170
+ *
171
+ * The no-upscale clamp still applies: deriving `lg` (1920) from
172
+ * a 640 `sm` source produces a 640-side `lg` variant, not a
173
+ * stretched 1920.
174
+ */
175
+ sourceUsed: VariantPreset | "raw";
176
+ } | {
177
+ ok: true;
178
+ kind: "video";
179
+ dispatch: unknown;
180
+ regenerated: string[] | "default";
181
+ };
182
+ /**
183
+ * Wire shape returned by `POST /assets/upload-url`. Either the server
184
+ * resolves the upload synchronously via dedup (`deduped: true` + existing
185
+ * asset DTO) or it returns a presigned R2 PUT URL plus a `process` payload
186
+ * the caller must POST to `/assets/process` after the PUT lands.
187
+ */
188
+ type UploadUrlResult = {
189
+ deduped: true;
190
+ asset: AssetDTO;
191
+ } | {
192
+ deduped: false;
193
+ upload: {
194
+ url: string;
195
+ headers?: Record<string, string>;
196
+ };
197
+ process: {
198
+ url: string;
199
+ body: Record<string, unknown>;
200
+ };
201
+ };
202
+ /**
203
+ * VIDEO-only delivery knobs threaded into `/assets/process`. Ignored for
204
+ * image / audio / other uploads. Both fields default to today's behavior when
205
+ * omitted, so existing callers are unaffected.
206
+ */
207
+ type UploadVideoOptions = {
208
+ /**
209
+ * `false` → skip the auto-dispatched HLS adaptive ladder (240p–2160p). Use
210
+ * for download-only assets served as a progressive `-v.mp4` and never
211
+ * streamed (e.g. share-video reels) — it avoids a second Cloud Run Job no
212
+ * one watches. Default/absent → the ladder is generated as before.
213
+ */
214
+ hls?: boolean;
215
+ /**
216
+ * `true` → when the uploaded MP4 is ALREADY web-safe (H.264 + yuv420p),
217
+ * re-mux the `video` variant with `-c copy` instead of re-encoding. Use for
218
+ * delivery-ready uploads (the bytes are already H.264 High / yuv420p /
219
+ * +faststart / capped bitrate) to skip a wasteful re-encode + generational
220
+ * quality loss. Falls back to a full re-encode automatically when the source
221
+ * is not web-safe. Default/absent → unconditional re-encode (today's path).
222
+ */
223
+ passthrough?: boolean;
224
+ };
225
+ /** Input shape accepted by `aq.assets.presignUploadUrl(...)`. */
226
+ type PresignUploadUrlOptions = {
227
+ /** Full sha256 (64 hex) of the bytes that will be PUT to R2. */
228
+ sha256: string;
229
+ /** MIME type of the bytes (e.g. `image/jpeg`, `video/mp4`). */
230
+ mime: string;
231
+ /** Byte length of the upload payload. */
232
+ bytes: number;
233
+ /** Suggested file name; surfaces in admin dashboards + extension fallback. */
234
+ fileName: string;
235
+ /**
236
+ * Variant ladder to generate after `/assets/process`. Defaults to
237
+ * `["original"]` server-side when omitted — same contract as `aq.upload`.
238
+ */
239
+ presets?: VariantPreset[];
240
+ /**
241
+ * Pre-compression size of the source (useful when the browser ran
242
+ * compressorjs / heic2any before computing `bytes`). Surfaces in admin
243
+ * dashboards under `assets.client_original_bytes`.
244
+ */
245
+ clientOriginalBytes?: number;
246
+ /** VIDEO-only delivery knobs forwarded into `/assets/process`. See {@link UploadVideoOptions}. */
247
+ video?: UploadVideoOptions;
248
+ };
249
+ /** Input shape accepted by `aq.assets.composeMarketing(...)`. */
250
+ type ComposeMarketingSegment = {
251
+ /** Public URL of the source clip (typically a `/t/.../video.mp4` transform). */
252
+ sourceUrl: string;
253
+ /** Optional clip duration in seconds (cap for that segment). */
254
+ durationSec?: number;
255
+ };
256
+ type ComposeMarketingComposition = {
257
+ /** Transition between consecutive segments. Default `"cut"`. */
258
+ transition?: "cut" | "fade";
259
+ /** Optional audio track to mix on top of the final composition. */
260
+ audioTrack?: {
261
+ url: string;
262
+ };
263
+ /** Final composition length in seconds (server may clamp). */
264
+ finalDurationSec?: number;
265
+ };
266
+ type ComposeMarketingOptions = {
267
+ /** Marketing-kit id this composition belongs to (server uses it for naming + dedup). */
268
+ marketingKitId: string;
269
+ /** Ordered clip segments to stitch. */
270
+ segments: ComposeMarketingSegment[];
271
+ /** Optional composition-level knobs (transitions, audio, duration). */
272
+ composition?: ComposeMarketingComposition;
273
+ };
274
+ type ComposeMarketingResult = {
275
+ /** Aquienpz asset id of the in-flight composition. Poll `aq.assets.waitReady(id)`. */
276
+ assetId: string;
277
+ /** Asset status at dispatch time — usually `"processing"`. */
278
+ status: "processing" | "ready" | "failed";
279
+ };
280
+ declare class AssetsApi {
281
+ private readonly opts;
282
+ constructor(opts: AquienpzClientOptions);
283
+ /** Look up an asset by full sha256 (64 hex). Returns null on 404. */
284
+ byHash(sha256: string): Promise<AssetDTO | null>;
285
+ /** Bulk lookup by sha256s. */
286
+ byHashes(hashes: string[]): Promise<{
287
+ existing: AssetDTO[];
288
+ missing: string[];
289
+ }>;
290
+ /** Paginated list of recent assets for the tenant. */
291
+ list(opts?: {
292
+ limit?: number;
293
+ cursor?: string;
294
+ includeDeleted?: boolean;
295
+ }): Promise<{
296
+ assets: AssetDTO[];
297
+ nextCursor: string | null;
298
+ }>;
299
+ /** Full DTO for an asset (admin view — includes audit-only fields). */
300
+ get(assetId: string): Promise<AssetDTO & Record<string, unknown>>;
301
+ /**
302
+ * Slot bindings pointing at an asset. Use this before deleting an
303
+ * asset so the admin sees which storefront slots would suddenly
304
+ * resolve to nothing.
305
+ */
306
+ bindings(assetId: string): Promise<Array<{
307
+ slotKey: string;
308
+ preset: VariantPreset | null;
309
+ description: string | null;
310
+ updatedAt: string;
311
+ updatedBy: string | null;
312
+ }>>;
313
+ /**
314
+ * Full variant list for an asset — preset, URL, dimensions, bytes.
315
+ * Stronger-typed wrapper around `get()` that exposes only the
316
+ * `variants` field with the proper `AssetVariant[]` shape.
317
+ *
318
+ * const v = await aq.assets.variants(logoId);
319
+ * v.map((x) => x.preset); // → ("thumb" | "sm" | … | "original")[]
320
+ */
321
+ variants(assetId: string): Promise<AssetVariant[]>;
322
+ /**
323
+ * Add or rebuild variants on an existing asset. Image presets are
324
+ * MERGED with what's there — passing `{ presets: ["thumb"] }` adds
325
+ * the thumb variant without touching `lg`, `sm`, `original`, etc.
326
+ *
327
+ * // Day 0: upload original-only logo
328
+ * const { assetId } = await aq.upload(logoFile); // defaults to ["original"]
329
+ *
330
+ * // Day 7: need a thumb without re-uploading
331
+ * await aq.assets.regenerate(assetId, { presets: ["thumb"] });
332
+ *
333
+ * const after = await aq.assets.variants(assetId);
334
+ * after.map((v) => v.preset); // → ["original", "thumb"]
335
+ *
336
+ * Passing no presets re-runs the FULL default pipeline for that
337
+ * asset's kind (thumb+sm+md+lg for images, poster+video for video).
338
+ *
339
+ * If the asset was uploaded original-only and the cleanup job has
340
+ * already reaped `raw/`, the route falls back to reading the source
341
+ * bytes from `variants/o.<ext>` — no need to re-upload.
342
+ *
343
+ * Video presets are filtered to `["poster","video","aiproxy"]` and
344
+ * dispatched to the Cloud Run Job (the call returns immediately
345
+ * with a dispatch handle; poll `aq.assets.get(id).status` for
346
+ * completion).
347
+ */
348
+ regenerate(assetId: string, opts?: {
349
+ presets?: VariantPreset[];
350
+ }): Promise<RegenerateResult>;
351
+ /** Merge metadata into an asset (role / slot / description / tags). */
352
+ patchMetadata(assetId: string, metadata: Record<string, unknown>): Promise<{
353
+ ok: true;
354
+ metadata: Record<string, unknown>;
355
+ }>;
356
+ /**
357
+ * Request a presigned R2 PUT URL for direct browser-side uploads.
358
+ *
359
+ * Mirrors the first half of `aq.upload()` — the caller (typically a
360
+ * BFF / share-link dropzone) computes sha256 in the browser, then
361
+ * uploads bytes straight to R2 with the returned `upload.url`, then
362
+ * POSTs `process.body` to `/assets/process` (see {@link processAndWait})
363
+ * once R2 has the bytes.
364
+ *
365
+ * If the sha is already known to the tenant the server short-circuits
366
+ * with `{ deduped: true, asset }` — no PUT needed.
367
+ *
368
+ * @example The browser-direct flow, in full
369
+ * ```ts
370
+ * // SERVER (holds the amk_rt_* key — never the browser):
371
+ * const presign = await aq.assets.presignUploadUrl({ sha256, mime, bytes, fileName, presets });
372
+ * if (presign.deduped) return presign.asset; // those bytes already exist; none fly
373
+ *
374
+ * // BROWSER: PUT straight to presign.upload.url — the bytes never touch your server.
375
+ * // ⚠️ R2 answers that preflight ITSELF, so your origin must be in the BUCKET's CORS policy.
376
+ * // Symptom when it is not: "PUT failed: network error" with every earlier step green —
377
+ * // and it cannot be fixed in this SDK, in your app, or in `storefront_origins`.
378
+ *
379
+ * // SERVER again, forwarding presign.process.body VERBATIM:
380
+ * const asset = await aq.assets.processAndWait(presign.process.body, { timeoutMs: 300_000 });
381
+ * ```
382
+ *
383
+ * Works for images AND video. A video answers immediately with
384
+ * `{ assetId, status: "processing" }` while a Cloud Run Job transcodes, so
385
+ * give `processAndWait` a bigger `timeoutMs` (a transcode + HLS ladder runs
386
+ * 1–2 min; 300_000 is a sane floor).
387
+ */
388
+ presignUploadUrl(opts: PresignUploadUrlOptions): Promise<UploadUrlResult>;
389
+ /**
390
+ * Dispatch `/assets/process` with the body returned by a prior
391
+ * {@link presignUploadUrl} call, then poll until the asset transitions
392
+ * to `ready` or `failed`. Throws on `failed` or timeout.
393
+ *
394
+ * Use this when bytes were uploaded directly from the browser to R2 —
395
+ * `aq.upload()` already does presign + PUT + process + wait in one
396
+ * step when the server holds the bytes.
397
+ */
398
+ processAndWait(processBody: Record<string, unknown>, opts?: {
399
+ timeoutMs?: number;
400
+ }): Promise<AssetDTO>;
401
+ /**
402
+ * Poll `GET /assets/:id` until the asset transitions to `ready` or
403
+ * `failed`. Returns the final DTO (whether ready OR failed — callers
404
+ * decide whether to throw on `failed`). Throws on timeout.
405
+ *
406
+ * Default timeout is 5 minutes; videos / HLS ladders may need a
407
+ * higher cap (pass `10 * 60_000` for compositions, transcodes).
408
+ */
409
+ waitReady(assetId: string, timeoutMs?: number): Promise<AssetDTO>;
410
+ /**
411
+ * Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip
412
+ * segments into a single MP4 composition. Returns the processing asset
413
+ * id immediately — does NOT block on completion. Callers poll via
414
+ * {@link waitReady} (typical timeout: 10 min for multi-segment kits).
415
+ *
416
+ * Tenant scope is inherited from the SDK client; `tenantCode` is added
417
+ * to the request body so the Cloud Run Job can resolve it without
418
+ * re-reading the header.
419
+ */
420
+ composeMarketing(opts: ComposeMarketingOptions): Promise<ComposeMarketingResult>;
421
+ private headers;
422
+ }
423
+ declare function mimeFromFileName(fileName: string | undefined): string | null;
424
+ /**
425
+ * Subset of compressorjs options exposed through the SDK. Re-imported here
426
+ * to avoid a hard import dependency on `./web` from this top-level module
427
+ * (the /web subpath uses browser-only APIs). The runtime `compress`
428
+ * implementation is lazy-loaded so Node/Bun callers don't pay the bundle
429
+ * cost — see `aq.upload` below.
430
+ */
431
+ type CompressOptions = {
432
+ quality?: number;
433
+ mimeType?: "image/jpeg" | "image/webp";
434
+ maxWidth?: number;
435
+ maxHeight?: number;
436
+ convertSize?: number;
437
+ strict?: boolean;
438
+ keepOriginalDimensions?: boolean;
439
+ convertHeic?: boolean;
440
+ onProgress?: (stage: "convertingHeic" | "compressing" | "compressingKeepingDimensions") => void;
441
+ };
442
+ type UploadOptions = {
443
+ fileName?: string;
444
+ /**
445
+ * MIME type of the bytes. **Only needed for a `Uint8Array` input** — a `File`/`Blob`
446
+ * already carries its `.type`. Raw bytes have no inherent MIME, so without this (and
447
+ * without an extension on `fileName` to infer from) they upload as
448
+ * `application/octet-stream`, which the asset-manager classifies as `kind:"other"` —
449
+ * meaning NO image/video variants are generated and `regenerate()` is unsupported.
450
+ * Resolution order for the effective MIME: `Blob.type` → `contentType` →
451
+ * inferred from `fileName`'s extension → `application/octet-stream`.
452
+ *
453
+ * aq.upload(bytes, { fileName: "cover.webp" }) // inferred → image/webp ✓
454
+ * aq.upload(bytes, { contentType: "image/webp" }) // explicit ✓
455
+ * aq.upload(bytes) // octet-stream → kind:"other" ⚠
456
+ */
457
+ contentType?: string;
458
+ /** Computed sha256 of bytes. Skip to compute locally with WebCrypto (browser only). */
459
+ sha256?: string;
460
+ /**
461
+ * Client-side compression before upload. Saves user bandwidth — typical
462
+ * 5–10× reduction for raw phone photos. Browser-only; in Node/Bun this
463
+ * silently no-ops with a console.warn and the raw bytes upload as-is.
464
+ *
465
+ * - `true` → use SDK `DEFAULT_COMPRESSION_OPTIONS` (webapp-tuned)
466
+ * - `CompressOptions` → merge over defaults
467
+ * - `false` / omit → no compression (current default behavior)
468
+ *
469
+ * Implementation is lazy-imported from `@nitida/sdk/web` so callers
470
+ * that never set `compress` don't pay the compressorjs + heic2any
471
+ * bundle cost. Skipped for non-image MIMEs (video, PDF) regardless of
472
+ * this option — those go to the upload pipeline raw.
473
+ *
474
+ * @see {@link CompressOptions}
475
+ * @see https://github.com/espaciofuturoio/aquienpz/tree/main/packages/sdk#client-side-compression-browsers
476
+ */
477
+ compress?: boolean | CompressOptions;
478
+ /**
479
+ * Variant set to generate. **Defaults to `["original"]`** —
480
+ * if you omit this option, only the raw bytes land on the CDN
481
+ * under the `o` path. Pass an explicit array to request more.
482
+ *
483
+ * Image presets (`thumb` 256 · `sm` 640 · `md` 1280 · `lg` 1920 ·
484
+ * `xl` 3840 · `original`):
485
+ * - `["original"]` (default) → just the raw bytes. Right call for
486
+ * logos / SVGs / anything you'll resize browser-side or via
487
+ * `aq.assets.regenerate(id, { presets: ["thumb"] })` later.
488
+ * - `["thumb","sm","md","lg"]` → the classic responsive ladder.
489
+ * - `["thumb","sm","md","lg","xl"]` → add 4K.
490
+ *
491
+ * Video presets (`poster`, `video`, `aiproxy`): omit `aiproxy` if
492
+ * the tenant doesn't need the low-res transcode for AI captioning.
493
+ *
494
+ * No upscaling. Each size preset is a **ceiling**; a 1080×720 source
495
+ * asked for `xl` (3840) yields a 1080×720 xl variant, not a stretched
496
+ * 3840-wide image.
497
+ *
498
+ * Idempotent: you can always add missing variants later via
499
+ * `aq.assets.regenerate(id, { presets: [...] })`. The platform
500
+ * stores the source so regeneration doesn't require re-uploading.
501
+ */
502
+ presets?: VariantPreset[];
503
+ /**
504
+ * Max time to wait for the asset to transition to `ready` (or `failed`)
505
+ * after dispatch. Default `5 * 60_000` (5 min). Bump higher for large
506
+ * videos / HLS transcodes — aquienpz processing time scales with input
507
+ * size and per-instance CPU.
508
+ *
509
+ * Throws `Error("waitReady timeout for <id>")` if the deadline passes
510
+ * without the asset transitioning. The asset row stays in aquienpz
511
+ * (status="processing") and the next byHash lookup will return it once
512
+ * processing completes; the caller can resume with their own poll.
513
+ */
514
+ timeoutMs?: number;
515
+ /**
516
+ * VIDEO-only delivery knobs forwarded into `/assets/process`. See
517
+ * {@link UploadVideoOptions}. Ignored for non-video uploads.
518
+ *
519
+ * // A delivery-ready reel: skip the unused HLS ladder + skip re-encode.
520
+ * await aq.upload(mp4Bytes, {
521
+ * fileName: "reel.mp4",
522
+ * presets: ["poster", "video"],
523
+ * video: { hls: false, passthrough: true },
524
+ * });
525
+ */
526
+ video?: UploadVideoOptions;
527
+ };
528
+ type UploadResult = {
529
+ assetId: string;
530
+ sha256: string;
531
+ cdnUrl: string;
532
+ };
533
+ type SlotHistoryEntry = {
534
+ id: string;
535
+ action: "bind" | "unbind" | "restore";
536
+ preset: VariantPreset | null;
537
+ description: string | null;
538
+ updatedAt: string;
539
+ updatedBy: string | null;
540
+ assetId: string | null;
541
+ /** Resolved DTO when the asset still exists; `null` after delete / 404. */
542
+ asset: AssetDTO | null;
543
+ };
544
+ type UsageSnapshot = {
545
+ tenant: {
546
+ id: number;
547
+ code: string;
548
+ };
549
+ storage: {
550
+ totalBytes: number;
551
+ assetCount: number;
552
+ };
553
+ today: UsageWindow;
554
+ last30Days: UsageWindow;
555
+ };
556
+ type UsageWindow = {
557
+ reads: number;
558
+ writes: number;
559
+ lists: number;
560
+ deletes: number;
561
+ admins: number;
562
+ upscales: number;
563
+ processes: number;
564
+ bytesIn: number;
565
+ };
566
+ type UsageDailyPoint = {
567
+ date: string;
568
+ reads: number;
569
+ writes: number;
570
+ lists: number;
571
+ deletes: number;
572
+ processes: number;
573
+ bytesIn: number;
574
+ bytesStored: number;
575
+ };
576
+ type UsagePerKey = {
577
+ apiKeyId: string;
578
+ prefix: string | null;
579
+ name: string | null;
580
+ opsTotal: number;
581
+ bytesTotal: number;
582
+ lastSeen: string;
583
+ };
584
+ declare class UsageApi {
585
+ private readonly opts;
586
+ constructor(opts: AquienpzClientOptions);
587
+ /** Snapshot for the active tenant — storage + today + last 30 days totals. */
588
+ snapshot(): Promise<UsageSnapshot>;
589
+ /** Daily rollup for charts — 1..365 days, default 30. */
590
+ timeseries(days?: number): Promise<{
591
+ tenant: {
592
+ id: number;
593
+ code: string;
594
+ };
595
+ days: UsageDailyPoint[];
596
+ }>;
597
+ /** Per-API-key breakdown for the current month. */
598
+ keys(): Promise<{
599
+ tenant: {
600
+ id: number;
601
+ code: string;
602
+ };
603
+ monthStart: string;
604
+ keys: UsagePerKey[];
605
+ }>;
606
+ private headers;
607
+ }
608
+ declare class AquienpzClient {
609
+ readonly slots: SlotsApi;
610
+ readonly assets: AssetsApi;
611
+ readonly usage: UsageApi;
612
+ /**
613
+ * Effective options — read-only. Exposed so the `/web` and `/expo`
614
+ * subpaths can inherit endpoint / apiKey / tenant scope from the
615
+ * configured client without re-passing them per call site.
616
+ */
617
+ readonly opts: AquienpzClientOptions;
618
+ constructor(opts: AquienpzClientOptions);
619
+ /** Tenant id as base36 path segment (e.g. tenantId=4 → "4/v/"). */
620
+ get tenantSegment(): string;
621
+ /** Build the canonical CDN URL deterministically from sha + preset. */
622
+ urlFor(asset: Pick<AssetDTO, "sha">, preset?: VariantPreset): string;
623
+ /** Build a responsive srcSet across the available image presets. */
624
+ srcSetFor(asset: Pick<AssetDTO, "sha" | "presets">): string;
625
+ /**
626
+ * Build an on-the-fly transform URL — `<cdn>/t/<dsl>/<sha>.<ext>`.
627
+ *
628
+ * ## URL CONVENTION — transforms are NOT tenant-prefixed (variants are)
629
+ * Two distinct delivery paths, by design:
630
+ * - **Variants / presets** (`urlFor`, `srcSetFor`, upload `cdnUrl`):
631
+ * `<cdn>/<tenantId b36>/v/<sha>-<preset>.<ext>` ← tenant-scoped (e.g. `/4/v/<sha>-lg.webp`)
632
+ * - **On-the-fly transforms** (`transform`, `transformSrcSet`):
633
+ * `<cdn>/t/<dsl>/<sha>.<ext>` ← GLOBAL, no tenant segment (`/t/...`)
634
+ * The transform service is content-addressed by sha + resizes from the source on demand,
635
+ * so it needs no tenant in the path. Prefixing a transform URL with `/<tenant>/t/...` 404s.
636
+ * Consumers that build URLs by hand must NOT add the tenant segment to `/t/` URLs.
637
+ *
638
+ * Returns the canonical `lg` variant URL when called with empty options,
639
+ * so callers can swap `urlFor()` for `transform()` without thinking.
640
+ *
641
+ * URLs with the same params in different order produce the same R2
642
+ * cache entry (the server canonicalizes both sides). Safe to use as
643
+ * stable cache keys.
644
+ *
645
+ * <Image
646
+ * src={aq.transform(asset, { width: 1280 })}
647
+ * srcSet={aq.transformSrcSet(asset, [640, 960, 1280, 1920])}
648
+ * sizes="(max-width: 768px) 100vw, 50vw"
649
+ * />
650
+ *
651
+ * @see {@link TransformOptions} for the full param matrix.
652
+ */
653
+ transform(asset: Pick<AssetDTO, "sha">, opts?: TransformOptions): string;
654
+ transform(asset: Pick<AssetDTO, "sha">, opts: SignedTransformOptions, signOpts: {
655
+ sign: true;
656
+ }): Promise<string>;
657
+ /**
658
+ * Build a responsive `srcSet` string. One transform URL per width; all
659
+ * other options apply to every URL.
660
+ *
661
+ * Pass `{ sign: true }` to return signed URLs (async). Without it, the
662
+ * call stays synchronous as before.
663
+ */
664
+ transformSrcSet(asset: Pick<AssetDTO, "sha">, widths: number[], extraOpts?: Omit<TransformOptions, "width">): string;
665
+ transformSrcSet(asset: Pick<AssetDTO, "sha">, widths: number[], extraOpts: Omit<TransformOptions, "width">, signOpts: {
666
+ sign: true;
667
+ }): Promise<string>;
668
+ /**
669
+ * Build an on-the-fly VIDEO transform URL — Phase 4.
670
+ *
671
+ * Same DSL shape as `transform()` but the URL has a `.mp4` (default)
672
+ * or `.webm` extension and the server routes the request to a Cloud
673
+ * Run Job for ffmpeg encoding (vs the inline sharp pipeline for
674
+ * images).
675
+ *
676
+ * On the first request the route returns **202 Accepted** with
677
+ * `Retry-After: 10` while the Job runs (typically 5-30 s for a
678
+ * short clip). The response body includes `outputUrl` which is the
679
+ * eventual CDN URL — poll the same transform URL after the
680
+ * retry-after window to get a 302 redirect to it.
681
+ *
682
+ * const url = aq.transformVideo(asset, {
683
+ * width: 1080, height: 1920, fit: "cover",
684
+ * start: 0, duration: 15,
685
+ * });
686
+ * // Pass to Video.js / <video src={url}>; on the first load it
687
+ * // gets 202 + body.outputUrl; subsequent loads hit cache → 302.
688
+ *
689
+ * Video-specific DSL params:
690
+ * - `start` (seconds, decimal OK)
691
+ * - `duration` (seconds, 1..300)
692
+ * - `format`: "mp4" (default) or "webm"
693
+ *
694
+ * The other params (`width`, `height`, `fit`) work identically to
695
+ * image transforms. `gravity`, `quality`, `effect`, `dpr` are
696
+ * accepted by the DSL but currently ignored on the video path.
697
+ */
698
+ transformVideo(asset: Pick<AssetDTO, "sha">, opts?: TransformOptions): string;
699
+ /**
700
+ * Build the HLS master playlist URL for a VIDEO asset (Phase 5).
701
+ *
702
+ * Returns `<cdn>/t/format=hls(,start=…,duration=…)/<sha>.m3u8`. Pass
703
+ * to an HLS-aware player:
704
+ *
705
+ * <video
706
+ * src={aq.streamingUrl(asset)}
707
+ * controls playsInline
708
+ * // Video.js v10's @videojs/http-streaming ships native HLS —
709
+ * // no plugin needed.
710
+ * />
711
+ *
712
+ * On the first request the server returns **202 Accepted** while a
713
+ * Cloud Run Job builds the multi-rung ladder (typically 1-3 min for
714
+ * a 90 s source — five rungs of 240p/360p/480p/720p/1080p @ AAC).
715
+ * Subsequent requests hit the cache → **302** to the master.m3u8.
716
+ *
717
+ * Supports `start` + `duration` to ladder a sub-clip. Other DSL
718
+ * params (width, height, fit) are ignored on the HLS path because
719
+ * the rungs determine resolution.
720
+ */
721
+ streamingUrl(asset: Pick<AssetDTO, "sha">, opts?: Omit<TransformOptions, "format">): string;
722
+ /**
723
+ * Upload a file or raw bytes. Returns the new asset id + canonical
724
+ * URL. Hash-deduped — uploading the same bytes twice returns the
725
+ * existing asset.
726
+ *
727
+ * Browser-first: uses `Blob` + WebCrypto. For Node 20+, pass a
728
+ * Uint8Array and a precomputed `sha256` (since `crypto.subtle` works
729
+ * but isn't always available depending on the runtime).
730
+ */
731
+ /**
732
+ * Upload bytes end to end: optional client compression → sha256 → presign → **direct-to-R2 PUT**
733
+ * → `/assets/process` → wait until the asset is ready.
734
+ *
735
+ * ⚠️ `presets` decides what exists FOREVER. Omit it and only `original` is written; ask for
736
+ * `["thumb"]` and the bytes you just uploaded are **not retrievable**. A variant not requested in
737
+ * this first ingest cannot be added later once the cleanup job reaps `raw/` — measured once as
738
+ * "97 files archived successfully, zero recoverable".
739
+ *
740
+ * @example Deliver an image on a site (the responsive ladder)
741
+ * ```ts
742
+ * import { AquienpzClient } from "@nitida/sdk/server";
743
+ *
744
+ * const aq = new AquienpzClient({ endpoint, apiKey, tenantCode, tenantId });
745
+ * const { assetId, sha256 } = await aq.upload(file, {
746
+ * fileName: file.name,
747
+ * presets: ["thumb", "sm", "md", "lg"],
748
+ * });
749
+ * ```
750
+ *
751
+ * @example ARCHIVE a file — you must ask for `original`
752
+ * ```ts
753
+ * await aq.upload(bytes, {
754
+ * fileName: "contrato.pdf",
755
+ * contentType: "application/pdf",
756
+ * presets: ["original"], // without this the bytes are unrecoverable
757
+ * });
758
+ * ```
759
+ *
760
+ * @example Raw bytes need an explicit MIME
761
+ * ```ts
762
+ * await aq.upload(bytes, { fileName: "track.mp3", contentType: "audio/mpeg" });
763
+ * // Without either, it stores as kind:"other" — no variants, and regenerate() is unsupported.
764
+ * ```
765
+ *
766
+ * @example Video — and what does NOT work there
767
+ * ```ts
768
+ * // `original` is accepted and then silently DROPPED: /assets/process filters video presets to
769
+ * // {poster, video, aiproxy, probe} before dispatching the transcode Job.
770
+ * await aq.upload(clip, { fileName: "tour.mp4", presets: ["poster", "video"] });
771
+ *
772
+ * // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost Job time
773
+ * // and permanent R2 objects that nothing else reads.
774
+ * ```
775
+ */
776
+ upload(input: File | Blob | Uint8Array, opts?: UploadOptions): Promise<UploadResult>;
777
+ private defaultPresetForMime;
778
+ /**
779
+ * Pick a sensible preset to build a URL for, given the asset's actual
780
+ * `presets` string. Falls back through the preference order
781
+ * lg → md → sm → thumb → original (for images)
782
+ * video → poster (for videos)
783
+ * mp3 → original (for audio)
784
+ * so an upload that was processed with e.g. `["original"]` still
785
+ * returns a non-404 URL in `aq.upload`'s result.
786
+ */
787
+ private bestPresetForAsset;
788
+ }
789
+
790
+ export { AquienpzClient, type AquienpzClientOptions, type ComposeMarketingComposition, type ComposeMarketingOptions, type ComposeMarketingResult, type ComposeMarketingSegment, type CompressOptions, type PresignUploadUrlOptions, type RegenerateResult, type SlotHistoryEntry, type UploadOptions, type UploadResult, type UploadUrlResult, type UploadVideoOptions, type UsageDailyPoint, type UsagePerKey, type UsageSnapshot, type UsageWindow, mimeFromFileName };