@nitida/sdk 0.24.1 → 0.25.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.
@@ -122,7 +122,7 @@ speed/quality argument, as a number.
122
122
  | Full-bleed retina hero | `2560` / `3840` — **only if the master reaches it** |
123
123
 
124
124
  ```tsx
125
- // Sin `quality`: auto-quality sondea el MÁSTERuna sola compresión.
125
+ // No `quality`: auto-quality probes the MASTERa single compression.
126
126
  <img
127
127
  src={getTransformUrl(asset, { format: "webp", width: 800 })}
128
128
  srcSet={getTransformSrcSet(asset, [400, 800, 1200, 1600], { format: "webp" })}
@@ -130,10 +130,10 @@ speed/quality argument, as a number.
130
130
  />
131
131
  ```
132
132
 
133
- ⚠️ **Fijate que NO lleva `quality`.** Un `quality` numérico activa el atajo que
134
- lee una variante ya comprimidaver la advertencia de abajo. Si de verdad
135
- necesitás fijarlo (un presupuesto de bytes duro), es un opt-in consciente, no el
136
- default que se copia sin leer.
133
+ ⚠️ **Note there is NO `quality`.** A numeric `quality` switches on the shortcut
134
+ that reads an already-compressed variantsee the warning below. If you really
135
+ do need to pin it (a hard byte budget), make that a deliberate opt-in, not a
136
+ default copied without reading.
137
137
 
138
138
  **`sizes` is what makes the phone pick 800 and the desktop 1600 from the same
139
139
  tag.** Omit it and the browser assumes `100vw`, fetches the large candidate for
@@ -218,21 +218,19 @@ Output: `https://8ok.uk/<tenantId.toString(36)>/v/<sha16>-v.mp4`.
218
218
 
219
219
  **Audio IS supported (updated 2026-07-01 — verify against the SDK types, this used to say "not supported").** The platform recognizes `kind: "image" | "video" | "document" | "audio" | "other"` and ships an **`mp3`** variant preset (`VariantPreset` in `@nitida/asset-client`). Uploads are hash-deduped (byte-identical re-uploads return the existing sha — that's *byte* dedup, NOT semantic "find a similar track"). `nt.upload` also accepts an `audioTrack` on video-composition calls. Serve via the `mp3` preset / `original`. Confirm the current preset/kind list in `node_modules/@nitida/asset-client/dist/index.d.ts` before relying on a specific ext.
220
220
 
221
- > ✅ **FIXED 2026-08-17 (#230/#231): you no longer have to pass `contentType` to avoid
222
- > `kind:"other"`.** This section used to insist on
223
- > `upload(bytes, { contentType: "audio/mpeg", fileName: "track.mp3" })` as a *defence*. Two
224
- > independent holes made that necessary and both are closed:
221
+ > ✅ **Fixed in the 2026-08-17 deploy: you no longer have to pass `contentType` to avoid
222
+ > `kind:"other"`.** Passing `upload(bytes, { contentType: "audio/mpeg", fileName: "track.mp3" })`
223
+ > used to be a *defence*. Two independent holes made that necessary and both are closed:
225
224
  >
226
225
  > - **The mime round-trip lost the type.** Presign derives the stored extension with
227
226
  > `mime.extension(body.mime)`, so `audio/mpeg` became `.mpga` — and the server's hand-written
228
227
  > ext→mime `switch` did not know `.mpga`, so it came back as `application/octet-stream` ⇒
229
- > `kind:"other"` ⇒ no variants. That swallowed **104 MP3 uploads**. `detectMime` now derives from
230
- > the same `mime-types` table that produced the extension, closing the whole class by construction
231
- > (7 broken round-trips fixed: `mpga`, `heif`, `adts`, `tif`, `bmp`, `svg`, `flac`), and
232
- > `test/mime-roundtrip.test.ts` keeps it closed.
228
+ > `kind:"other"` ⇒ no variants, silently, for every MP3 uploaded that way. The server now derives
229
+ > the mime from the same table that produced the extension, closing the whole class by
230
+ > construction `mpga`, `heif`, `adts`, `tif`, `bmp`, `svg` and `flac` all round-trip.
233
231
  > - **Genuinely untyped uploads are rescued by their bytes.** When the key implies
234
- > `application/octet-stream`, `/process` sniffs the magic bytes (`sniffMagicMime`) and processes
235
- > the file as what it actually is.
232
+ > `application/octet-stream`, the server sniffs the magic bytes and processes the file as what it
233
+ > actually is.
236
234
  >
237
235
  > Passing `contentType` + `fileName` is still *good practice* — it is the cheapest possible signal
238
236
  > and it decides the stored extension. It is no longer load-bearing.
@@ -264,7 +262,7 @@ If you are uploading to **archive** something (not just to serve it), three serv
264
262
 
265
263
  The client's `PRESET_EXT.original` is the literal `"bin"`, so `getAssetUrl({sha}, "original")` **without `mime`** emits `-o.bin` while a PNG is really stored at `-o.png` → 404, always.
266
264
 
267
- > ✅ **FIXED 2026-08-17 (doc 240 §4.3 + §4.4). Pass the whole DTO and it is simply right:**
265
+ > ✅ **Fixed in the 2026-08-17 deploy. Pass the whole DTO and it is simply right:**
268
266
  >
269
267
  > ```ts
270
268
  > const asset = await nt.assets.get(id);
@@ -294,32 +292,26 @@ The client's `PRESET_EXT.original` is the literal `"bin"`, so `getAssetUrl({sha}
294
292
  await nt.assets.regenerate(assetId, { presets: ["original"] });
295
293
  ```
296
294
 
297
- ⚠️ **Elegir mal `presets` NO destruye nadapero rompe una API y no la otra.** Verificado
298
- 2026-08-17 contra producción, sobre un asset del **16-jun** con `presets:"q"` (sólo miniatura):
299
- **su `raw` sigue existiendo (HTTP 200), dos meses después.**
295
+ ⚠️ **Choosing `presets` badly destroys nothingbut it breaks one API and not the other.**
296
+ Verified 2026-08-17 against a live asset ingested two months earlier with `presets:"q"` (thumbnail
297
+ only): its raw upload was still there, HTTP 200.
300
298
 
301
- La limpieza conserva el `raw` de **cualquier** sha referenciado por una fila de asset, incluidas las
302
- borradas en suave. **Mientras exista la fila, los bytes originales están.**
299
+ Cleanup keeps the raw bytes of **any** sha referenced by an asset row, soft-deleted rows included.
300
+ **While the row exists, the original bytes exist.**
303
301
 
304
- lo que realmente pasa si pediste `["thumb"]` y necesitabas la escalera:
302
+ what actually happens if you asked for `["thumb"]` and needed the ladder:
305
303
 
306
- | lo que llamás | qué obtenés |
304
+ | what you call | what you get |
307
305
  |---|---|
308
- | `getAssetUrl(asset, "md")` | **404** — esa variante nunca se generó |
309
- | `/t/…width=1280/<sha>.webp` | **200** — se genera al vuelo desde el raw |
310
- | `hasPreset(asset, "lg")` | `false`, correctamente |
306
+ | `getAssetUrl(asset, "md")` | **404** — that variant was never generated |
307
+ | `/t/…width=1280/<sha>.webp` | **200** — generated on the fly from the raw |
308
+ | `hasPreset(asset, "lg")` | `false`, correctly |
311
309
 
312
- Una API te dice que no existe y la otra te la entrega. **Eso** es lo caro: no la pérdida, la
313
- incoherenciamás un cómputo en frío la primera vez que alguien pide cada tamaño.
310
+ One API tells you it does not exist and the other hands it to you. **That** is the expensive part
311
+ not the loss, the incoherence plus a cold compute the first time anyone asks for each size.
314
312
 
315
- **Se arregla en cualquier momento** con `regenerate({presets:[...]})`, que lee del raw. No hay
316
- ventana que se cierre.
317
-
318
- > ⚠️ **CORREGIDO 2026-08-17. La versión anterior de este párrafo decía que «los bytes del raw pueden
319
- > ya ser irrecuperables» si la primera ingesta no pidió `original`.** Era falso, y venía de una
320
- > hipótesis escrita como diagnóstico: la corrida del 2026-08-07 observó que `regenerate` no producía
321
- > el `original` y concluyó *«consistent with `raw/` already being gone»*. Nadie comprobó el raw. Al
322
- > comprobarlo, está. Lo que sí sigue en pie de esa corrida es la disciplina de abajo.
313
+ **It is fixable at any time** with `regenerate({presets:[...]})`, which reads from the raw. No
314
+ window closes.
323
315
 
324
316
  **Verify, don't assume.** Round-trip every archived file: download the `original` URL from the API
325
317
  and compare its SHA-256 to the local file. "97 uploaded, 0 failed" was true and meaningless.
@@ -333,18 +325,19 @@ and compare its SHA-256 to the local file. "97 uploaded, 0 failed" was true and
333
325
 
334
326
  `presets` is documented as a concatenation of **one-character** codes, and membership is a
335
327
  one-character `includes`. It used to carry multi-character tokens as well (`transform-3da0019…`,
336
- `probe`), and every one of them answered `true` for presets that do not exist. Counted on live
337
- rows: **16 299** assets were told they had an `original` they did not have sending callers to
338
- exactly the 404 that `oext` came to remove — plus 16 479 phantom `aiproxy` and 487 phantom `sm`/`md`.
328
+ `probe`), and every one of them answered `true` for presets that do not exist. On a corpus written
329
+ by those servers the majority of rows are polluted, and a large minority are told they have an
330
+ `original` they do not — sending callers to exactly the 404 that `oext` came to remove. Phantom
331
+ `aiproxy` is affected at the same order of magnitude; `sm`/`md` far less.
339
332
 
340
- ✅ **Fixed 2026-08-17 (#228) on both sides:** the server emits only the 1-char vocabulary (plus the
333
+ ✅ **Fixed in the 2026-08-17 deploy, on both sides:** the server emits only the 1-char vocabulary (plus the
341
334
  sanctioned `mp3` token, which is deliberate and stays), and `hasPreset` strips multi-char tokens
342
335
  before the `includes`. **`hasPreset(dto, preset)` remains the recommended existence check** — it is
343
336
  the only field present on every response shape, including the slim list/resolver one that carries no
344
337
  `variants` at all.
345
338
 
346
- ⚠️ **Against a server deploy older than 2026-08-17 the contaminated strings are still being sent**
347
- (62 % of live rows had one). The client-side strip covers you; a hand-rolled
339
+ ⚠️ **Against a server deploy older than 2026-08-17 the contaminated strings are still being sent**,
340
+ on the majority of rows. The client-side strip covers you; a hand-rolled
348
341
  `dto.presets.includes("o")` does not.
349
342
 
350
343
  ⚠️ **`mp3` is a real, multi-char token in the vocabulary on purpose.** Any substring test you write
@@ -508,15 +501,15 @@ needed. Reproduce there before debugging any of these by hand.
508
501
  | **400** on an image URL | width not on `TRANSFORM_WIDTHS` ladder |
509
502
  | **410** on a video URL | used `/t/` (transform) for a stored video — use `getAssetUrl(...,'video')` |
510
503
  | **404** on a video URL | decimal tenant prefix (`/10/v/`) instead of base36 (`/a/v/`) — call `setTenantId` + `getAssetUrl`. Tenant 12 → `/c/v/` |
511
- | `process returned no assetId` | fixed 2026-08-16 — you are on a server deploy older than that, §7.2 |
504
+ | `process returned no assetId` | fixed in the 2026-08-16 deploy — you are on a server deploy older than that, §7.2 |
512
505
  | `waitReady timeout` on a video | a transcode + HLS ladder takes 1–2 min; the default `timeoutMs` is 5 min but a 4K source can beat it. Raise it, §7.2 |
513
506
  | the direct PUT fails with a bare network error | your origin is not in the storage bucket's CORS policy — ours to fix, §7.1 |
514
- | `nt.assets.variants()` returns `[]` | fixed 2026-08-17 (#225) — you are on a server deploy older than that. Existence from `dto.presets` + `hasPreset` works on every version |
515
- | `hasPreset` says a preset exists and its URL 404s | fixed 2026-08-17 (#228) — an old server sends multi-char tokens in `presets`; a current `@nitida/asset-client` strips them, a hand-rolled `includes` does not, §3b-bis |
507
+ | `nt.assets.variants()` returns `[]` | fixed in the 2026-08-17 deploy — you are on a server deploy older than that. Existence from `dto.presets` + `hasPreset` works on every version |
508
+ | `hasPreset` says a preset exists and its URL 404s | fixed in the 2026-08-17 deploy — an old server sends multi-char tokens in `presets`; a current `@nitida/asset-client` strips them, a hand-rolled `includes` does not, §3b-bis |
516
509
  | `tenant_not_found` from a service | tenant rows missing, or the 60s tenant-config cache is stale |
517
510
  | `original` 404s | (a) never written — `presets` omitted `"original"` at ingest, or the asset is a VIDEO (always dropped, §7.3); (b) `getAssetUrl` called with a bare `{sha}` → the `-o.bin` sentinel; (c) called with `{sha, mime}` against an old server, so it guessed the extension. Pass the whole DTO from `nt.assets.get(id)` — see §3b |
518
511
  | "uploaded OK" but nothing to restore | `["thumb"]`-style presets store no original; verify by SHA-256 round-trip, not by upload count — see §3b |
519
- | audio stored as `kind:"other"` | fixed 2026-08-17 (#230/#231) — the ext→mime round-trip lost `.mpga`, and untyped bytes are now sniffed. Against an older server deploy, upload with an explicit `contentType:"audio/mpeg"`. Rows misclassified **before** the fix stay that way by decision, §3 |
512
+ | audio stored as `kind:"other"` | fixed in the 2026-08-17 deploy — the ext→mime round-trip lost `.mpga`, and untyped bytes are now sniffed. Against an older server deploy, upload with an explicit `contentType:"audio/mpeg"`. Rows misclassified **before** the fix stay that way by decision, §3 |
520
513
 
521
514
  > Source of truth for deeper detail: the published docs — <https://nitida.gofuture.space/guides/advanced/>,
522
515
  > <https://nitida.gofuture.space/guides/troubleshooting/>, <https://nitida.gofuture.space/start/credentials/> —
package/src/index.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * One ergonomic facade over the underlying packages
6
6
  * (`@nitida/asset-client` URL builders + `@aquienpz/asset-uploader-web`
7
- * + the slot resolver). Auth is a Better Auth API key (`amk_rt_*`)
8
- * issued by aquienpz `bootstrap-project.ts`; tenant scope comes from
7
+ * + the slot resolver). Auth is a bearer API key (`amk_rt_*`), issued
8
+ * per tenant when the tenant is created; tenant scope comes from
9
9
  * the key's metadata (`X-Tenant-Code` is log-only).
10
10
  *
11
11
  * Usage:
@@ -14,8 +14,8 @@
14
14
  *
15
15
  * const aq = new NitidaClient({
16
16
  * endpoint: "https://api.nitida.gofuture.space",
17
- * apiKey: process.env.ASSET_MANAGER_RUNTIME_KEY!,
18
- * tenantCode: "realtyone-cr",
17
+ * apiKey: process.env.AQUIENPZ_API_KEY!, // amk_rt_* — server-only
18
+ * tenantCode: "acme-co",
19
19
  * cdnBase: "https://8ok.uk", // optional override
20
20
  * tenantId: 4, // required for tenant-prefixed URLs
21
21
  * });
@@ -86,7 +86,7 @@ export type NitidaClientOptions = {
86
86
  */
87
87
  endpoint: string;
88
88
  /**
89
- * Better Auth API key with the `amk_rt_*` prefix.
89
+ * Bearer API key with the `amk_rt_*` prefix.
90
90
  *
91
91
  * **Server-only.** Omit when constructing from `@nitida/sdk/web` —
92
92
  * your BFF / route handler injects the bearer header in proxy mode.
@@ -111,8 +111,8 @@ export type NitidaClientOptions = {
111
111
  * Tenant's HMAC signing key for transform URLs (Phase 3). Required
112
112
  * only when calling `aq.transform(asset, opts, { sign: true })`.
113
113
  *
114
- * Generated server-side per tenant (see `infra/sql/tenants_signed_transforms.sql`);
115
- * fetch via `GET /admin/tenants/:id` with an admin key. **Keep it
114
+ * 32 random bytes, generated server-side on tenant creation; fetch
115
+ * via `GET /admin/tenants/:id` with an admin key. **Keep it
116
116
  * server-side only** — do not ship in `NEXT_PUBLIC_*` env vars. Sign
117
117
  * URLs from a BFF route handler, or pre-sign at build time.
118
118
  */
@@ -190,9 +190,19 @@ function authHeaders(opts: NitidaClientOptions): Record<string, string> {
190
190
  // Re-exports (so consumers don't double-import from asset-client)
191
191
  // ---------------------------------------------------------------------------
192
192
 
193
+ // This barrel is COMPLETE on purpose: every public symbol of
194
+ // `@nitida/asset-client` is reachable from `@nitida/sdk`, so a consumer never
195
+ // has to know which of the two packages a helper happens to live in. It had
196
+ // drifted to 30 of 53 — the gap silently included `TRANSFORM_WIDTHS` and
197
+ // `TransformWidth`, which both READMEs tell you to import precisely so an
198
+ // off-ladder width fails at compile time instead of returning 400, and the
199
+ // palette helpers the README imports from here by name.
193
200
  export type {
194
201
  AssetDTO,
202
+ AssetPalette,
195
203
  AssetVariant,
204
+ HlsRung,
205
+ PaletteSwatch,
196
206
  ResolveSlotOptions,
197
207
  SignedTransformOptions,
198
208
  SlotDTO,
@@ -202,24 +212,47 @@ export type {
202
212
  TransformFormat,
203
213
  TransformGravity,
204
214
  TransformOptions,
215
+ TransformWidth,
205
216
  VariantPreset,
206
217
  } from "@nitida/asset-client";
207
218
  export {
219
+ bestTextContrast,
208
220
  computeVariantDimensions,
221
+ configureSlotResolver,
222
+ contrastRatio,
209
223
  extractAssetSha,
224
+ getAmbientGradient,
210
225
  getAssetDimensions,
211
226
  getAssetSrcSet,
212
227
  getAssetUrl,
228
+ getCdnBase,
229
+ getHlsLadder,
213
230
  getHlsStreamingUrl,
231
+ getPaletteBlurBackground,
232
+ getPaletteCssVars,
214
233
  getSignedTransformUrl,
215
234
  getTenantId,
235
+ getTextColorForBackground,
216
236
  getTransformSrcSet,
217
237
  getTransformUrl,
218
238
  getVideoTransformUrl,
219
239
  hasPreset,
240
+ hlsLadderAlignment,
241
+ invalidateSlotCache,
242
+ iteratePaletteSwatches,
243
+ PRESET_EXT,
244
+ PRESET_LONG,
245
+ PRESET_MAX_DIM,
246
+ PRESET_SHORT,
247
+ pickAmbientBackground,
248
+ relativeLuminance,
249
+ resolveSlot,
250
+ resolveSlots,
220
251
  serializeTransform,
252
+ setCdnBase,
221
253
  setTenantId,
222
254
  signTransformUrl,
255
+ TRANSFORM_WIDTHS,
223
256
  } from "@nitida/asset-client";
224
257
 
225
258
  // ---------------------------------------------------------------------------
@@ -409,7 +442,7 @@ export type UploadVideoOptions = {
409
442
  hls?: boolean;
410
443
  /**
411
444
  * `true` → when the uploaded MP4 is ALREADY web-safe (H.264 + yuv420p),
412
- * re-mux the `video` variant with `-c copy` instead of re-encoding. Use for
445
+ * re-mux the `video` variant instead of re-encoding it. Use for
413
446
  * delivery-ready uploads (the bytes are already H.264 High / yuv420p /
414
447
  * +faststart / capped bitrate) to skip a wasteful re-encode + generational
415
448
  * quality loss. Falls back to a full re-encode automatically when the source
@@ -435,8 +468,8 @@ export type PresignUploadUrlOptions = {
435
468
  presets?: VariantPreset[];
436
469
  /**
437
470
  * Pre-compression size of the source (useful when the browser ran
438
- * compressorjs / heic2any before computing `bytes`). Surfaces in admin
439
- * dashboards under `assets.client_original_bytes`.
471
+ * compressorjs / heic2any before computing `bytes`). Recorded
472
+ * server-side, so the savings show up in the admin usage dashboards.
440
473
  */
441
474
  clientOriginalBytes?: number;
442
475
  /** VIDEO-only delivery knobs forwarded into `/assets/process`. See {@link UploadVideoOptions}. */
@@ -571,8 +604,8 @@ class AssetsApi {
571
604
  * const v = await aq.assets.variants(logoId);
572
605
  * v.map((x) => x.preset); // → ("thumb" | "sm" | … | "original")[]
573
606
  *
574
- * ⚠️ Returns `[]` — not an error — against a server deploy older than the
575
- * 2026-08-17 deploy, which never sent the field (doc 240 §4.3). An empty
607
+ * ⚠️ Returns `[]` — not an error — against a server older than the
608
+ * 2026-08-17 release, which never sent the field at all. An empty
576
609
  * array is therefore "no variants OR old server". For a plain existence
577
610
  * check prefer `hasPreset(dto, preset)` on `dto.presets`, which every server
578
611
  * version sends; use this when you need the URLs and sizes.
@@ -601,12 +634,13 @@ class AssetsApi {
601
634
  * Passing no presets re-runs the FULL default pipeline for that
602
635
  * asset's kind (thumb+sm+md+lg for images, poster+video for video).
603
636
  *
604
- * If the asset was uploaded original-only and the cleanup job has
605
- * already reaped `raw/`, the route falls back to reading the source
606
- * bytes from `variants/o.<ext>` no need to re-upload.
637
+ * If the asset was uploaded original-only and the ~24 h grace window
638
+ * on the uploaded bytes has already closed, the route falls back to
639
+ * reading the source bytes from the permanent `original` variant
640
+ * no need to re-upload.
607
641
  *
608
642
  * Video presets are filtered to `["poster","video","aiproxy"]` and
609
- * dispatched to the background job (the call returns immediately
643
+ * dispatched to a background job (the call returns immediately
610
644
  * with a dispatch handle; poll `aq.assets.get(id).status` for
611
645
  * completion).
612
646
  */
@@ -916,8 +950,8 @@ export type UploadOptions = {
916
950
  /**
917
951
  * Max time to wait for the asset to transition to `ready` (or `failed`)
918
952
  * after dispatch. Default `5 * 60_000` (5 min). Bump higher for large
919
- * videos / HLS transcodes — aquienpz processing time scales with input
920
- * size and per-instance CPU.
953
+ * videos / HLS transcodes — processing time scales with input size,
954
+ * and with how much other work the platform is doing at that moment.
921
955
  *
922
956
  * Throws `Error("waitReady timeout for <id>")` if the deadline passes
923
957
  * without the asset transitioning. The asset row stays in aquienpz
@@ -1007,11 +1041,11 @@ export type UsageWindow = {
1007
1041
  * encode passes, so a 7-rung HLS ladder over a 2-minute clip books 14.
1008
1042
  *
1009
1043
  * A re-encode of a video already ingested counts AGAIN. That is the point:
1010
- * `assets.assets` cannot report it, because its insert is
1011
- * `ON CONFLICT DO UPDATE` and a re-encode creates no row there.
1044
+ * an asset count cannot report it, because re-encoding an asset that already
1045
+ * exists creates no new asset.
1012
1046
  *
1013
- * A stream copy (`-c copy` passthrough, an idempotent cache skip) books
1014
- * nothing, so a tenant that ran no encoder reads `0` — and that `0` is real.
1047
+ * A stream copy (`video: { passthrough: true }`, or an idempotent cache skip)
1048
+ * books nothing, so a tenant that ran no encoder reads `0` — and that `0` is real.
1015
1049
  */
1016
1050
  videoMinutes: number;
1017
1051
  /** Encode passes behind `videoMinutes`. 0 when nothing was encoded. */
@@ -1270,7 +1304,7 @@ export class NitidaClient {
1270
1304
  * images).
1271
1305
  *
1272
1306
  * On the first request the route returns **202 Accepted** with
1273
- * `Retry-After: 10` while the Job runs (typically 5-30 s for a
1307
+ * `Retry-After: 10` while the encode runs (typically 5-30 s for a
1274
1308
  * short clip). The response body includes `outputUrl` which is the
1275
1309
  * eventual CDN URL — poll the same transform URL after the
1276
1310
  * retry-after window to get a 302 redirect to it.
@@ -1342,8 +1376,8 @@ export class NitidaClient {
1342
1376
  *
1343
1377
  * ⚠️ `presets` decides what exists FOREVER. Omit it and only `original` is written; ask for
1344
1378
  * `["thumb"]` and the bytes you just uploaded are **not retrievable**. A variant not requested in
1345
- * this first ingest cannot be added later once the cleanup job reaps `raw/` measured once as
1346
- * "97 files archived successfully, zero recoverable".
1379
+ * this first ingest cannot be added later once the ~24 h grace window on the uploaded bytes
1380
+ * closes — measured once as "97 files archived successfully, zero recoverable".
1347
1381
  *
1348
1382
  * @example Deliver an image on a site (the responsive ladder)
1349
1383
  * ```ts
@@ -1374,7 +1408,7 @@ export class NitidaClient {
1374
1408
  * @example Video — and what does NOT work there
1375
1409
  * ```ts
1376
1410
  * // `original` is accepted and then silently DROPPED: /assets/process filters video presets to
1377
- * // {poster, video, aiproxy, probe} before dispatching the transcode Job.
1411
+ * // {poster, video, aiproxy, probe} before dispatching the background transcode.
1378
1412
  * await aq.upload(clip, { fileName: "tour.mp4", presets: ["poster", "video"] });
1379
1413
  *
1380
1414
  * // Omit `aiproxy`/`probe` unless the asset really goes to a vision model — they cost encode
@@ -10,9 +10,9 @@
10
10
  * import { NitidaClient } from "@nitida/sdk/server";
11
11
  *
12
12
  * const aq = new NitidaClient({
13
- * endpoint: process.env.ASSET_MANAGER_URL!,
14
- * apiKey: process.env.ASSET_MANAGER_API_KEY!, // <- required
15
- * tenantCode: "realtyone-cr",
13
+ * endpoint: process.env.AQUIENPZ_URL!,
14
+ * apiKey: process.env.AQUIENPZ_API_KEY!, // <- required
15
+ * tenantCode: "acme-co",
16
16
  * tenantId: 1,
17
17
  * // signingKey: optional, only for `aq.transform(..., { sign: true })`
18
18
  * });
@@ -32,9 +32,11 @@
32
32
  * globally; File-API workflows are documented on the /web subpath instead)
33
33
  *
34
34
  * What's NOT here (use `@nitida/sdk/web` instead):
35
- * - `createWebUploader` (multipart UploadTask with IndexedDB resume)
36
35
  * - `compressImage` (browser-side compressorjs + heic2any)
37
36
  *
37
+ * And what exists nowhere: there is no `createWebUploader`. Multipart is not
38
+ * exposed from any subpath — `aq.upload()` is the supported path.
39
+ *
38
40
  * Stripe/Cloudinary historically shipped two separate packages
39
41
  * (`stripe` vs `@stripe/stripe-js`, `cloudinary` vs `@cloudinary/url-gen`)
40
42
  * for this split. Modern providers (Vercel Blob, Uploadthing, Better
@@ -51,9 +53,9 @@ import { NitidaClient as BaseNitidaClient, type NitidaClientOptions } from "..";
51
53
  * Cloud Run, Vercel Functions, edge runtimes, BFFs).
52
54
  *
53
55
  * const aq = new NitidaClient({
54
- * endpoint: process.env.ASSET_MANAGER_URL!,
55
- * apiKey: process.env.ASSET_MANAGER_API_KEY!,
56
- * tenantCode: "realtyone-cr",
56
+ * endpoint: process.env.AQUIENPZ_URL!,
57
+ * apiKey: process.env.AQUIENPZ_API_KEY!,
58
+ * tenantCode: "acme-co",
57
59
  * tenantId: 1,
58
60
  * });
59
61
  */
package/src/web/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * // Point at your BFF route — the SDK calls
14
14
  * // `${endpoint}/assets/by-hash/...`, `${endpoint}/slots/...`, etc.
15
15
  * endpoint: "/api/am", // relative ⇒ same-origin proxy
16
- * tenantCode: "realtyone-cr",
16
+ * tenantCode: "acme-co",
17
17
  * tenantId: 1,
18
18
  * // NO apiKey — the type strips it. Your BFF injects the bearer.
19
19
  * });
@@ -28,7 +28,7 @@
28
28
  * return fetch(url, {
29
29
  * headers: {
30
30
  * Authorization: `Bearer ${process.env.AQUIENPZ_API_KEY!}`,
31
- * "X-Tenant-Code": "realtyone-cr",
31
+ * "X-Tenant-Code": "acme-co",
32
32
  * },
33
33
  * });
34
34
  * }
@@ -64,7 +64,7 @@ import { NitidaClient as BaseNitidaClient, type NitidaClientOptions } from "..";
64
64
  *
65
65
  * const aq = new NitidaClient({
66
66
  * endpoint: "/api/am", // OK: relative → same-origin BFF
67
- * tenantCode: "realtyone-cr",
67
+ * tenantCode: "acme-co",
68
68
  * tenantId: 1,
69
69
  * // apiKey: "amk_rt_...", // ERROR: TS error: not assignable
70
70
  * });
@@ -150,9 +150,9 @@ export {
150
150
  // ---------------------------------------------------------------------------
151
151
  //
152
152
  // Wraps `@nitida/asset-compressor-web` with the SDK's
153
- // `DEFAULT_COMPRESSION_OPTIONS` — values derived from the realtyone-cr
154
- // webapp's production tuning (LISTING_STANDARD_*: quality 0.80, max-edge
155
- // 3840px, WebP output, 5MB PNG→JPEG threshold, strict mode). Those defaults
153
+ // `DEFAULT_COMPRESSION_OPTIONS` — values tuned against a production
154
+ // photo-upload workload (quality 0.80, max-edge 3840px, WebP output,
155
+ // 5MB PNG→JPEG threshold, strict mode). Those defaults
156
156
  // differ from `@nitida/asset-compressor-web`'s own defaults (0.85 / 2880 /
157
157
  // convertSize=0) which target a slightly different audience; SDK callers
158
158
  // get the webapp-tuned values, package-direct callers keep theirs.
@@ -184,17 +184,16 @@ export type CompressStage =
184
184
 
185
185
  /**
186
186
  * Subset of compressorjs options exposed by the SDK. Matches the
187
- * webapp's `DEFAULT_COMPRESSION_OPTIONS` shape from the realtyone-cr
188
- * production back-office uploader.
187
+ * `DEFAULT_COMPRESSION_OPTIONS` shape below.
189
188
  */
190
189
  export type CompressOptions = {
191
- /** 0..1. Default 0.80 (`LISTING_STANDARD_IMAGE_QUALITY / 100`). */
190
+ /** 0..1. Default 0.80. */
192
191
  quality?: number;
193
- /** Output format. Default `"image/webp"` (`DEFAULT_IMAGE_TARGET_FORMAT`). */
192
+ /** Output format. Default `"image/webp"`. */
194
193
  mimeType?: "image/jpeg" | "image/webp";
195
- /** Max edge in pixels. Default 3840 (`LISTING_STANDARD_IMAGE_WIDTH`). */
194
+ /** Max edge in pixels. Default 3840. */
196
195
  maxWidth?: number;
197
- /** Default 3840 (`LISTING_STANDARD_IMAGE_HEIGHT`). */
196
+ /** Max edge in pixels. Default 3840. */
198
197
  maxHeight?: number;
199
198
  /**
200
199
  * compressorjs `convertSize`: PNG > this byte count auto-converts to
@@ -215,9 +214,9 @@ export type CompressOptions = {
215
214
  };
216
215
 
217
216
  /**
218
- * Webapp-tuned defaults (LISTING_STANDARD_*). These intentionally differ
219
- * from `@nitida/asset-compressor-web`'s package-level defaults — the
220
- * SDK overrides at call time.
217
+ * Defaults tuned against a production photo-upload workload. These
218
+ * intentionally differ from `@nitida/asset-compressor-web`'s
219
+ * package-level defaults — the SDK overrides at call time.
221
220
  */
222
221
  export const DEFAULT_COMPRESSION_OPTIONS: Required<
223
222
  Omit<CompressOptions, "keepOriginalDimensions" | "convertHeic" | "onProgress">
@@ -274,8 +273,8 @@ export async function compressImage(
274
273
  // String indirection defeats bundler static-analysis of dynamic
275
274
  // imports — Turbopack/webpack will leave this for the runtime to
276
275
  // resolve instead of failing the build when the optional peer dep
277
- // isn't installed. See feedback_bun_compile_dynamic_imports (same
278
- // trick used in reverse to opt OUT of bundling).
276
+ // isn't installed. (The same trick is used in reverse elsewhere in
277
+ // this SDK to opt OUT of bundling.)
279
278
  const compressorPkg = "@nitida/asset-compressor-web";
280
279
  const { compressImage: doCompress } = (await import(
281
280
  /* @vite-ignore */ /* webpackIgnore: true */ compressorPkg