@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.
- package/README.md +128 -129
- package/dist/index.d.ts +28 -27
- package/dist/index.js +56 -11
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +9 -7
- package/dist/server.js +34 -11
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +11 -12
- package/dist/web.js +34 -11
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +39 -46
- package/src/index.ts +60 -26
- package/src/server/index.ts +9 -7
- package/src/web/index.ts +16 -17
|
@@ -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
|
-
//
|
|
125
|
+
// No `quality`: auto-quality probes the MASTER ⇒ a 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
|
-
⚠️ **
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
default
|
|
133
|
+
⚠️ **Note there is NO `quality`.** A numeric `quality` switches on the shortcut
|
|
134
|
+
that reads an already-compressed variant — see 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
|
-
> ✅ **
|
|
222
|
-
> `kind:"other"`.**
|
|
223
|
-
>
|
|
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
|
|
230
|
-
> the same
|
|
231
|
-
>
|
|
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`,
|
|
235
|
-
>
|
|
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
|
-
> ✅ **
|
|
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
|
-
⚠️ **
|
|
298
|
-
2026-08-17
|
|
299
|
-
|
|
295
|
+
⚠️ **Choosing `presets` badly destroys nothing — but 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
|
-
|
|
302
|
-
|
|
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
|
-
⇒
|
|
302
|
+
⇒ what actually happens if you asked for `["thumb"]` and needed the ladder:
|
|
305
303
|
|
|
306
|
-
|
|
|
304
|
+
| what you call | what you get |
|
|
307
305
|
|---|---|
|
|
308
|
-
| `getAssetUrl(asset, "md")` | **404** —
|
|
309
|
-
| `/t/…width=1280/<sha>.webp` | **200** —
|
|
310
|
-
| `hasPreset(asset, "lg")` | `false`,
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
**
|
|
316
|
-
|
|
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.
|
|
337
|
-
|
|
338
|
-
exactly the 404 that `oext` came to remove
|
|
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
|
|
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
|
-
|
|
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
|
|
515
|
-
| `hasPreset` says a preset exists and its URL 404s | fixed 2026-08-17
|
|
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
|
|
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
|
|
8
|
-
*
|
|
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.
|
|
18
|
-
* tenantCode: "
|
|
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
|
-
*
|
|
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
|
-
*
|
|
115
|
-
*
|
|
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
|
|
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`).
|
|
439
|
-
*
|
|
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
|
|
575
|
-
* 2026-08-17
|
|
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
|
|
605
|
-
* already
|
|
606
|
-
* bytes from
|
|
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
|
|
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 —
|
|
920
|
-
*
|
|
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
|
-
*
|
|
1011
|
-
*
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
package/src/server/index.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* import { NitidaClient } from "@nitida/sdk/server";
|
|
11
11
|
*
|
|
12
12
|
* const aq = new NitidaClient({
|
|
13
|
-
* endpoint: process.env.
|
|
14
|
-
* apiKey: process.env.
|
|
15
|
-
* tenantCode: "
|
|
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.
|
|
55
|
-
* apiKey: process.env.
|
|
56
|
-
* tenantCode: "
|
|
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: "
|
|
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": "
|
|
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: "
|
|
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
|
|
154
|
-
//
|
|
155
|
-
//
|
|
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
|
-
*
|
|
188
|
-
* production back-office uploader.
|
|
187
|
+
* `DEFAULT_COMPRESSION_OPTIONS` shape below.
|
|
189
188
|
*/
|
|
190
189
|
export type CompressOptions = {
|
|
191
|
-
/** 0..1. Default 0.80
|
|
190
|
+
/** 0..1. Default 0.80. */
|
|
192
191
|
quality?: number;
|
|
193
|
-
/** Output format. Default `"image/webp"
|
|
192
|
+
/** Output format. Default `"image/webp"`. */
|
|
194
193
|
mimeType?: "image/jpeg" | "image/webp";
|
|
195
|
-
/** Max edge in pixels. Default 3840
|
|
194
|
+
/** Max edge in pixels. Default 3840. */
|
|
196
195
|
maxWidth?: number;
|
|
197
|
-
/** Default 3840
|
|
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
|
-
*
|
|
219
|
-
* from `@nitida/asset-compressor-web`'s
|
|
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.
|
|
278
|
-
//
|
|
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
|