@nitida/sdk 0.26.0 → 0.27.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/AGENTS.md +62 -0
- package/README.md +63 -2
- package/dist/index.d.ts +73 -9
- package/dist/index.js +56 -8
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +56 -8
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +2 -2
- package/dist/web.js +56 -8
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +71 -5
- package/src/audio-compat.ts +37 -0
- package/src/index.ts +51 -9
- package/src/server/index.ts +9 -0
- package/src/web/index.ts +9 -0
package/AGENTS.md
CHANGED
|
@@ -74,6 +74,68 @@ version of this file, verify against the `.d.ts` you actually installed.
|
|
|
74
74
|
ASYNCHRONOUSLY: the call answers `{ assetId, status: "processing" }` and a background job flips
|
|
75
75
|
it to `ready` 1–2 min later, so `processAndWait` needs a `timeoutMs` of at least `300_000`.
|
|
76
76
|
|
|
77
|
+
|
|
78
|
+
## Private assets — `visibility`
|
|
79
|
+
|
|
80
|
+
Default is `"public"`. Set `private` and **every public door answers 404** —
|
|
81
|
+
stored variants, the raw original, the HLS ladder, and `/t/` (including the
|
|
82
|
+
poster frame of a private video). The bytes come back only through a signed URL
|
|
83
|
+
that expires.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { getPrivateAssetUrl, getPrivateTransformUrl } from "@nitida/sdk";
|
|
87
|
+
// ON YOUR BACKEND, once you decided this viewer may see it:
|
|
88
|
+
await getPrivateAssetUrl(asset, "lg", signingKey, { expiresInSeconds: 300 });
|
|
89
|
+
await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeconds: 300 });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **A 404 on a private asset is NOT a missing file.** Check `visibility` on the
|
|
93
|
+
DTO before you check storage. It is the number-one support question.
|
|
94
|
+
- **Seven URL builders throw** rather than hand you a doomed URL — `getAssetUrl`,
|
|
95
|
+
`getAssetSrcSet`, `getTransformUrl`, `getTransformSrcSet`,
|
|
96
|
+
`getVideoTransformUrl`, `getHlsStreamingUrl`, `getSignedTransformUrl` — but
|
|
97
|
+
only when the value you pass carries `visibility`. `{ sha }` alone is never
|
|
98
|
+
refused.
|
|
99
|
+
- **`exp` is mandatory**; revocation is *"within a minute"* (60 s TTL at the edge).
|
|
100
|
+
- **The signing key is a backend secret** — it mints URLs for every private
|
|
101
|
+
asset the tenant owns.
|
|
102
|
+
|
|
103
|
+
## Two ways an image gets smaller, and only one of them is yours to call
|
|
104
|
+
|
|
105
|
+
This is the question every programmatic caller gets wrong, so it is stated flat:
|
|
106
|
+
|
|
107
|
+
| | who does it | what it shrinks |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| **Client compressor** (browser / Expo) | the UI, before the PUT | the **upload**: quality 0.85, max 2880 px, WebP. iPhone 9.1 MB → 1.8 MB |
|
|
110
|
+
| **Variants + `/t/`** | the backend, on request | the **delivery**: 19 MB JPEG → 170 kB WebP at 1920 |
|
|
111
|
+
|
|
112
|
+
**The backend never recompresses the raw. Ever.** That is deliberate: the raw
|
|
113
|
+
has to stay pristine so variants are *regenerable* — the day you add AVIF or
|
|
114
|
+
raise the max dimension, the pipeline re-runs against it. A client→server lossy
|
|
115
|
+
chain bakes artifacts in forever.
|
|
116
|
+
|
|
117
|
+
So for an API/agent upload there is nothing to "turn on": you were never going
|
|
118
|
+
to compress the raw, and delivery is already optimised two ways.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// Ask for the rungs you will actually render…
|
|
122
|
+
await aq.upload(file, { presets: ["original", "thumb", "md", "lg"] });
|
|
123
|
+
|
|
124
|
+
// …or upload bare and let the transform route make them on demand:
|
|
125
|
+
aq.transform(asset, { width: 1280, format: "webp" }); // → /t/…, cached after the first hit
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**`presets` defaults to `["original"]`** — a bare upload stores the raw and no
|
|
129
|
+
rendition. Since 2026-08-22 that is no longer a trap: when the DTO says a preset
|
|
130
|
+
was never materialised, `getAssetUrl` / `urlFor` **fall back to `/t/`** instead
|
|
131
|
+
of returning a URL that 404s. You still get optimised bytes; you just pay the
|
|
132
|
+
first encode. Prefer naming the presets when you know what you will render.
|
|
133
|
+
|
|
134
|
+
⚠️ `getAssetSrcSet` deliberately does **not** fall back — a srcSet promises
|
|
135
|
+
pixel widths and a transform cannot keep that promise on a source smaller than
|
|
136
|
+
the rung, because the pipeline never enlarges. An empty srcSet degrades to `src`; a lying one
|
|
137
|
+
degrades to a wrong choice.
|
|
138
|
+
|
|
77
139
|
## Related packages
|
|
78
140
|
|
|
79
141
|
| Package | Job |
|
package/README.md
CHANGED
|
@@ -832,8 +832,9 @@ const asset = await aq.assets.byHash(sha256);
|
|
|
832
832
|
| `dpr` | `1` / `2` / `3` | `1` |
|
|
833
833
|
|
|
834
834
|
> **`width` is strongly typed.** `TransformOptions.width` is a **`TransformWidth`** — the
|
|
835
|
-
> predefined CDN ladder (`160, 240, 256, 320, 400, 480, 600, 640, 800, 960,
|
|
836
|
-
> 1280, 1440, 1600, 1920, 2560, 3840
|
|
835
|
+
> predefined CDN ladder (`96, 128, 160, 240, 256, 320, 400, 480, 600, 640, 800, 960,
|
|
836
|
+
> 1080, 1200, 1280, 1440, 1600, 1920, 2560, 3840` — 20 widths, exported as
|
|
837
|
+
> `TRANSFORM_WIDTHS`). An off-ladder
|
|
837
838
|
> width is a **compile error**: the edge whitelists exactly these as a DoS guard and
|
|
838
839
|
> HTTP 400s anything else on unsigned URLs. Need a custom off-ladder width? **Sign it** —
|
|
839
840
|
> `aq.transform(asset, { width: 1490 }, { sign: true })` and `getSignedTransformUrl` take
|
|
@@ -926,6 +927,66 @@ hex-encoded. The server canonicalizes the URL the same way the SDK does
|
|
|
926
927
|
(sort keys, lowercase strings), so two URLs with the same params in
|
|
927
928
|
different order accept the same signature.
|
|
928
929
|
|
|
930
|
+
## Private assets — `visibility`
|
|
931
|
+
|
|
932
|
+
Every asset carries `visibility`, and the default is `"public"`.
|
|
933
|
+
|
|
934
|
+
| | `"public"` | `"private"` |
|
|
935
|
+
|---|---|---|
|
|
936
|
+
| stored variants, raw, HLS ladder, transforms | served to anyone with the URL | **404**, to everyone |
|
|
937
|
+
| how the bytes come back | the URL | a signed URL under `/a/{tenant}/…?exp&sig` |
|
|
938
|
+
| expiry | none | mandatory |
|
|
939
|
+
| edge caching | shared, effectively free | `private`, per-viewer |
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
// Flip it (write scope):
|
|
943
|
+
await fetch(`${endpoint}/assets/${assetId}/visibility`, {
|
|
944
|
+
method: "PATCH",
|
|
945
|
+
headers: { Authorization: `Bearer ${apiKey}`, "X-Tenant-Code": code,
|
|
946
|
+
"Content-Type": "application/json" },
|
|
947
|
+
body: JSON.stringify({ visibility: "private" }),
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Hand a viewer the bytes — ON YOUR BACKEND:
|
|
951
|
+
import { getPrivateAssetUrl, getPrivateTransformUrl } from "@nitida/sdk";
|
|
952
|
+
|
|
953
|
+
const url = await getPrivateAssetUrl(asset, "lg", signingKey, {
|
|
954
|
+
expiresInSeconds: 300,
|
|
955
|
+
});
|
|
956
|
+
// → https://8ok.uk/a/5/v/<sha>-l.webp?exp=…&sig=…
|
|
957
|
+
|
|
958
|
+
// …or any width/crop/format, not just the materialised ones:
|
|
959
|
+
const resized = await getPrivateTransformUrl(
|
|
960
|
+
asset,
|
|
961
|
+
{ width: 1280, format: "webp" },
|
|
962
|
+
signingKey,
|
|
963
|
+
{ expiresInSeconds: 300 },
|
|
964
|
+
);
|
|
965
|
+
// → https://8ok.uk/a/5/t/format=webp,width=1280/<sha>.webp?exp=…&sig=…
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
Nothing moves when you flip it: one row changes and the edge cache for that
|
|
969
|
+
asset is purged, so a 1 GB video flips as fast as a thumbnail. It is the same
|
|
970
|
+
single copy behind both doors.
|
|
971
|
+
|
|
972
|
+
**Four things worth knowing before you rely on it:**
|
|
973
|
+
|
|
974
|
+
1. **A 404 on a private asset is the feature, not a missing file.** Everywhere
|
|
975
|
+
else here a 404 means the object was never written. Check `visibility` on
|
|
976
|
+
the DTO before you check storage.
|
|
977
|
+
2. **The SDK refuses instead of handing you a URL that 404s.** `getAssetUrl`,
|
|
978
|
+
`getAssetSrcSet`, `getTransformUrl`, `getTransformSrcSet`,
|
|
979
|
+
`getVideoTransformUrl` and `getHlsStreamingUrl` all throw when the value you
|
|
980
|
+
pass says `visibility: "private"` — with a message naming
|
|
981
|
+
`getPrivateAssetUrl`. Pass only `{ sha }` and there is nothing to check.
|
|
982
|
+
3. **Revocation is "within a minute".** Flipping back to `private` really does
|
|
983
|
+
kill URLs already handed out — the edge is purged — but the CDN refreshes
|
|
984
|
+
its list of private assets on a 60-second TTL.
|
|
985
|
+
4. **The signing key is a backend secret.** It mints URLs for every private
|
|
986
|
+
asset the tenant owns. It is a different claim from the transform `?sig=`
|
|
987
|
+
above, computed under a separately derived key, so a signature minted to
|
|
988
|
+
resize can never be replayed as one to enter.
|
|
989
|
+
|
|
929
990
|
### ⚠️ Admin operations need the SYSTEM key, not the admin key in your triplet
|
|
930
991
|
|
|
931
992
|
Both are `amk_ad_*`, and that is the whole trap. The `admin` key issued with
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { ResolveSlotOptions, SlotResolution, SlotDTO, VariantPreset, AssetDTO, AssetVariant, RequestablePreset, TransformOptions, SignedTransformOptions } from '@nitida/asset-client';
|
|
2
|
-
export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, RequestablePreset, ResolveSlotOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signTransformUrl } from '@nitida/asset-client';
|
|
2
|
+
export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, RequestablePreset, ResolveSlotOptions, SignAccessOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl } from '@nitida/asset-client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which audio uploads already play everywhere — the single source of truth for
|
|
6
|
+
* both the server's transcode guard and the client's delivery-preset choice.
|
|
7
|
+
*
|
|
8
|
+
* It lives in its own module, rather than beside the preset order that uses it,
|
|
9
|
+
* for a mechanical reason: `apps/asset-manager` imports it by relative path.
|
|
10
|
+
* The SDK is a PUBLISHED package, so its `exports` point at `dist/`, and
|
|
11
|
+
* `dist/` is gitignored — it does not exist inside the Docker image, which
|
|
12
|
+
* copies `packages/` as source. A bare `@nitida/sdk` import type-checks on any
|
|
13
|
+
* machine that built the package once and then fails the image build. (It did,
|
|
14
|
+
* on 2026-08-22, with `@nitida/asset-client`.) A single small file keeps that
|
|
15
|
+
* relative import from dragging the whole client into the server.
|
|
16
|
+
*
|
|
17
|
+
* Two copies of this list drifting apart is how a platform ends up generating
|
|
18
|
+
* a variant its own client refuses to use — which is exactly the bug this
|
|
19
|
+
* predicate was introduced to end.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* `true` when a browser can play these bytes as uploaded, so re-encoding them
|
|
23
|
+
* to MP3 buys nothing.
|
|
24
|
+
*
|
|
25
|
+
* - `audio/mpeg` — unambiguous.
|
|
26
|
+
* - `audio/mp4` / `audio/aac` — plays in Safari, Chrome, Firefox and Edge on
|
|
27
|
+
* desktop and mobile. The historical worry was old AOSP builds without
|
|
28
|
+
* proprietary codecs; the call to treat AAC as universal was made with the
|
|
29
|
+
* platform's one real AAC consumer, whose viewer is WebGL — a device that
|
|
30
|
+
* cannot decode AAC cannot run that product at all, so the fallback would
|
|
31
|
+
* only ever protect a device that had already lost.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately NOT here: `audio/webm` and `audio/ogg` (Opus). iOS Safari
|
|
34
|
+
* cannot decode them, and that is the case the MP3 fallback exists for.
|
|
35
|
+
*/
|
|
36
|
+
declare function isUniversallyPlayableAudio(mime: string): boolean;
|
|
3
37
|
|
|
4
38
|
/**
|
|
5
39
|
* @nitida/sdk — universal client for the aquienpz multi-tenant asset
|
|
@@ -86,7 +120,9 @@ type NitidaClientOptions = {
|
|
|
86
120
|
* only when calling `aq.transform(asset, opts, { sign: true })`.
|
|
87
121
|
*
|
|
88
122
|
* 32 random bytes, generated server-side on tenant creation; fetch
|
|
89
|
-
* via `
|
|
123
|
+
* via `POST /admin/projects/:code/rotate-signing-key`, which needs a
|
|
124
|
+
* SYSTEM-scope credential the platform operator holds — your own admin key
|
|
125
|
+
* answers `403 SYSTEM_KEY_REQUIRED`. Ask for it. **Keep it
|
|
90
126
|
* server-side only** — do not ship in `NEXT_PUBLIC_*` env vars. Sign
|
|
91
127
|
* URLs from a BFF route handler, or pre-sign at build time.
|
|
92
128
|
*/
|
|
@@ -329,9 +365,10 @@ declare class AssetsApi {
|
|
|
329
365
|
*/
|
|
330
366
|
variants(assetId: string): Promise<AssetVariant[]>;
|
|
331
367
|
/**
|
|
332
|
-
* Add or rebuild variants on an existing asset.
|
|
333
|
-
*
|
|
334
|
-
* the thumb variant without touching
|
|
368
|
+
* Add or rebuild variants on an existing asset. Presets are MERGED
|
|
369
|
+
* with what's there, for images AND for video — passing
|
|
370
|
+
* `{ presets: ["thumb"] }` adds the thumb variant without touching
|
|
371
|
+
* `lg`, `sm`, `original`, etc.
|
|
335
372
|
*
|
|
336
373
|
* // Day 0: upload original-only logo
|
|
337
374
|
* const { assetId } = await aq.upload(logoFile); // defaults to ["original"]
|
|
@@ -350,10 +387,18 @@ declare class AssetsApi {
|
|
|
350
387
|
* reading the source bytes from the permanent `original` variant —
|
|
351
388
|
* no need to re-upload.
|
|
352
389
|
*
|
|
353
|
-
* Video presets are filtered to `["poster","video","aiproxy"]`
|
|
354
|
-
* dispatched to a background job (the call returns immediately
|
|
390
|
+
* Video presets are filtered to `["poster","video","aiproxy","probe"]`
|
|
391
|
+
* and dispatched to a background job (the call returns immediately
|
|
355
392
|
* with a dispatch handle; poll `aq.assets.get(id).status` for
|
|
356
393
|
* completion).
|
|
394
|
+
*
|
|
395
|
+
* ⚠️ Before 2026-08-22 the video path REPLACED the whole variant
|
|
396
|
+
* registry instead of merging, so a partial regenerate silently
|
|
397
|
+
* deregistered `poster`, `video` and — irrecoverably — `hls`, which
|
|
398
|
+
* is not a {@link RequestablePreset} and therefore cannot be asked
|
|
399
|
+
* for again. The objects kept serving from the CDN; only the
|
|
400
|
+
* registry died. Fixed server-side; a client on an older server
|
|
401
|
+
* still loses them.
|
|
357
402
|
*/
|
|
358
403
|
regenerate(assetId: string, opts?: {
|
|
359
404
|
presets?: RequestablePreset[];
|
|
@@ -647,6 +692,7 @@ declare class UsageApi {
|
|
|
647
692
|
}>;
|
|
648
693
|
private headers;
|
|
649
694
|
}
|
|
695
|
+
|
|
650
696
|
declare class NitidaClient {
|
|
651
697
|
readonly slots: SlotsApi;
|
|
652
698
|
readonly assets: AssetsApi;
|
|
@@ -822,11 +868,29 @@ declare class NitidaClient {
|
|
|
822
868
|
* `presets` string. Falls back through the preference order
|
|
823
869
|
* lg → md → sm → thumb → original (for images)
|
|
824
870
|
* video → poster (for videos)
|
|
825
|
-
*
|
|
871
|
+
* original → mp3 (for audio ALREADY playable everywhere)
|
|
872
|
+
* mp3 → original (for any other audio)
|
|
826
873
|
* so an upload that was processed with e.g. `["original"]` still
|
|
827
874
|
* returns a non-404 URL in `aq.upload`'s result.
|
|
875
|
+
*
|
|
876
|
+
* ⭐ Why audio branches on the source mime (changed 2026-08-22).
|
|
877
|
+
*
|
|
878
|
+
* It used to be `mp3 → original` unconditionally, so `upload().cdnUrl`
|
|
879
|
+
* handed back the server's auto-generated mp3 — libmp3lame, mono ~96 kbps —
|
|
880
|
+
* even when the caller had uploaded an MP3 or an AAC that already plays in
|
|
881
|
+
* every target browser. A consumer asking for "my file" silently received a
|
|
882
|
+
* re-encoded, lower-quality one, with no error to notice.
|
|
883
|
+
*
|
|
884
|
+
* Measured across the platform: of 126 audio assets carrying an mp3 variant,
|
|
885
|
+
* **105 had an `audio/mpeg` source** — an MP3 re-encoded into an MP3, for
|
|
886
|
+
* zero compatibility gain.
|
|
887
|
+
*
|
|
888
|
+
* The mp3 still wins for `audio/webm`/Opus and anything exotic, which is the
|
|
889
|
+
* case it was built for: Chrome records webm/Opus, which iOS Safari cannot
|
|
890
|
+
* decode. That guarantee is preserved exactly; only the needless downgrade
|
|
891
|
+
* is gone.
|
|
828
892
|
*/
|
|
829
893
|
private bestPresetForAsset;
|
|
830
894
|
}
|
|
831
895
|
|
|
832
|
-
export { type ComposeMarketingComposition, type ComposeMarketingOptions, type ComposeMarketingResult, type ComposeMarketingSegment, type CompressOptions, NitidaClient, type NitidaClientOptions, type PresignUploadUrlOptions, type RegenerateResult, type SlotHistoryEntry, type UploadOptions, type UploadResult, type UploadUrlResult, type UploadVideoOptions, type UsageDailyPoint, type UsagePerKey, type UsageSnapshot, type UsageWindow, mimeFromFileName };
|
|
896
|
+
export { type ComposeMarketingComposition, type ComposeMarketingOptions, type ComposeMarketingResult, type ComposeMarketingSegment, type CompressOptions, NitidaClient, type NitidaClientOptions, type PresignUploadUrlOptions, type RegenerateResult, type SlotHistoryEntry, type UploadOptions, type UploadResult, type UploadUrlResult, type UploadVideoOptions, type UsageDailyPoint, type UsagePerKey, type UsageSnapshot, type UsageWindow, isUniversallyPlayableAudio, mimeFromFileName };
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,22 @@ import {
|
|
|
15
15
|
setCdnBase,
|
|
16
16
|
setTenantId
|
|
17
17
|
} from "@nitida/asset-client";
|
|
18
|
+
|
|
19
|
+
// src/audio-compat.ts
|
|
20
|
+
function isUniversallyPlayableAudio(mime) {
|
|
21
|
+
const base = (mime.split(";")[0] ?? "").trim().toLowerCase();
|
|
22
|
+
return base === "audio/mpeg" || base === "audio/mp4" || base === "audio/aac";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/index.ts
|
|
18
26
|
import {
|
|
27
|
+
accessMessage,
|
|
28
|
+
assertPublic,
|
|
19
29
|
bestTextContrast,
|
|
20
30
|
computeVariantDimensions,
|
|
21
31
|
configureSlotResolver as configureSlotResolver2,
|
|
22
32
|
contrastRatio,
|
|
33
|
+
deriveAccessKey,
|
|
23
34
|
extractAssetSha,
|
|
24
35
|
getAmbientGradient,
|
|
25
36
|
getAssetDimensions,
|
|
@@ -30,6 +41,8 @@ import {
|
|
|
30
41
|
getHlsStreamingUrl as getHlsStreamingUrl2,
|
|
31
42
|
getPaletteBlurBackground,
|
|
32
43
|
getPaletteCssVars,
|
|
44
|
+
getPrivateAssetUrl,
|
|
45
|
+
getPrivateTransformUrl,
|
|
33
46
|
getSignedTransformUrl as getSignedTransformUrl2,
|
|
34
47
|
getTenantId,
|
|
35
48
|
getTextColorForBackground,
|
|
@@ -51,6 +64,7 @@ import {
|
|
|
51
64
|
serializeTransform,
|
|
52
65
|
setCdnBase as setCdnBase2,
|
|
53
66
|
setTenantId as setTenantId2,
|
|
67
|
+
signAccessUrl,
|
|
54
68
|
signTransformUrl,
|
|
55
69
|
TRANSFORM_WIDTHS
|
|
56
70
|
} from "@nitida/asset-client";
|
|
@@ -256,9 +270,10 @@ var AssetsApi = class {
|
|
|
256
270
|
return Array.isArray(dto.variants) ? dto.variants : [];
|
|
257
271
|
}
|
|
258
272
|
/**
|
|
259
|
-
* Add or rebuild variants on an existing asset.
|
|
260
|
-
*
|
|
261
|
-
* the thumb variant without touching
|
|
273
|
+
* Add or rebuild variants on an existing asset. Presets are MERGED
|
|
274
|
+
* with what's there, for images AND for video — passing
|
|
275
|
+
* `{ presets: ["thumb"] }` adds the thumb variant without touching
|
|
276
|
+
* `lg`, `sm`, `original`, etc.
|
|
262
277
|
*
|
|
263
278
|
* // Day 0: upload original-only logo
|
|
264
279
|
* const { assetId } = await aq.upload(logoFile); // defaults to ["original"]
|
|
@@ -277,10 +292,18 @@ var AssetsApi = class {
|
|
|
277
292
|
* reading the source bytes from the permanent `original` variant —
|
|
278
293
|
* no need to re-upload.
|
|
279
294
|
*
|
|
280
|
-
* Video presets are filtered to `["poster","video","aiproxy"]`
|
|
281
|
-
* dispatched to a background job (the call returns immediately
|
|
295
|
+
* Video presets are filtered to `["poster","video","aiproxy","probe"]`
|
|
296
|
+
* and dispatched to a background job (the call returns immediately
|
|
282
297
|
* with a dispatch handle; poll `aq.assets.get(id).status` for
|
|
283
298
|
* completion).
|
|
299
|
+
*
|
|
300
|
+
* ⚠️ Before 2026-08-22 the video path REPLACED the whole variant
|
|
301
|
+
* registry instead of merging, so a partial regenerate silently
|
|
302
|
+
* deregistered `poster`, `video` and — irrecoverably — `hls`, which
|
|
303
|
+
* is not a {@link RequestablePreset} and therefore cannot be asked
|
|
304
|
+
* for again. The objects kept serving from the CDN; only the
|
|
305
|
+
* registry died. Fixed server-side; a client on an older server
|
|
306
|
+
* still loses them.
|
|
284
307
|
*/
|
|
285
308
|
async regenerate(assetId, opts = {}) {
|
|
286
309
|
const r = await fetch(
|
|
@@ -548,7 +571,7 @@ var NitidaClient = class {
|
|
|
548
571
|
}
|
|
549
572
|
if (!this.opts.signingKey) {
|
|
550
573
|
throw new Error(
|
|
551
|
-
"aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions.
|
|
574
|
+
"aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. No signingKey on this client. The key is minted by POST /admin/projects/:code/rotate-signing-key, which needs a system-scope credential the platform operator holds \u2014 your own admin key gets 403 SYSTEM_KEY_REQUIRED, so ask for it. Then pass it to the SDK constructor on a SERVER-side instance only."
|
|
552
575
|
);
|
|
553
576
|
}
|
|
554
577
|
return getSignedTransformUrl(asset, opts, this.opts.signingKey) ?? Promise.resolve(this.urlFor(asset, "lg"));
|
|
@@ -800,12 +823,30 @@ var NitidaClient = class {
|
|
|
800
823
|
* `presets` string. Falls back through the preference order
|
|
801
824
|
* lg → md → sm → thumb → original (for images)
|
|
802
825
|
* video → poster (for videos)
|
|
803
|
-
*
|
|
826
|
+
* original → mp3 (for audio ALREADY playable everywhere)
|
|
827
|
+
* mp3 → original (for any other audio)
|
|
804
828
|
* so an upload that was processed with e.g. `["original"]` still
|
|
805
829
|
* returns a non-404 URL in `aq.upload`'s result.
|
|
830
|
+
*
|
|
831
|
+
* ⭐ Why audio branches on the source mime (changed 2026-08-22).
|
|
832
|
+
*
|
|
833
|
+
* It used to be `mp3 → original` unconditionally, so `upload().cdnUrl`
|
|
834
|
+
* handed back the server's auto-generated mp3 — libmp3lame, mono ~96 kbps —
|
|
835
|
+
* even when the caller had uploaded an MP3 or an AAC that already plays in
|
|
836
|
+
* every target browser. A consumer asking for "my file" silently received a
|
|
837
|
+
* re-encoded, lower-quality one, with no error to notice.
|
|
838
|
+
*
|
|
839
|
+
* Measured across the platform: of 126 audio assets carrying an mp3 variant,
|
|
840
|
+
* **105 had an `audio/mpeg` source** — an MP3 re-encoded into an MP3, for
|
|
841
|
+
* zero compatibility gain.
|
|
842
|
+
*
|
|
843
|
+
* The mp3 still wins for `audio/webm`/Opus and anything exotic, which is the
|
|
844
|
+
* case it was built for: Chrome records webm/Opus, which iOS Safari cannot
|
|
845
|
+
* decode. That guarantee is preserved exactly; only the needless downgrade
|
|
846
|
+
* is gone.
|
|
806
847
|
*/
|
|
807
848
|
bestPresetForAsset(asset, mime) {
|
|
808
|
-
const order = mime.startsWith("video/") ? ["video", "poster"] : mime.startsWith("audio/") ? ["mp3", "original"] : ["lg", "md", "sm", "thumb", "xl", "original"];
|
|
849
|
+
const order = mime.startsWith("video/") ? ["video", "poster"] : mime.startsWith("audio/") ? isUniversallyPlayableAudio(mime) ? ["original", "mp3"] : ["mp3", "original"] : ["lg", "md", "sm", "thumb", "xl", "original"];
|
|
809
850
|
return order.find((p) => hasPreset(asset, p)) ?? this.defaultPresetForMime(mime);
|
|
810
851
|
}
|
|
811
852
|
};
|
|
@@ -816,10 +857,13 @@ export {
|
|
|
816
857
|
PRESET_MAX_DIM,
|
|
817
858
|
PRESET_SHORT,
|
|
818
859
|
TRANSFORM_WIDTHS,
|
|
860
|
+
accessMessage,
|
|
861
|
+
assertPublic,
|
|
819
862
|
bestTextContrast,
|
|
820
863
|
computeVariantDimensions,
|
|
821
864
|
configureSlotResolver2 as configureSlotResolver,
|
|
822
865
|
contrastRatio,
|
|
866
|
+
deriveAccessKey,
|
|
823
867
|
extractAssetSha,
|
|
824
868
|
getAmbientGradient,
|
|
825
869
|
getAssetDimensions,
|
|
@@ -830,6 +874,8 @@ export {
|
|
|
830
874
|
getHlsStreamingUrl2 as getHlsStreamingUrl,
|
|
831
875
|
getPaletteBlurBackground,
|
|
832
876
|
getPaletteCssVars,
|
|
877
|
+
getPrivateAssetUrl,
|
|
878
|
+
getPrivateTransformUrl,
|
|
833
879
|
getSignedTransformUrl2 as getSignedTransformUrl,
|
|
834
880
|
getTenantId,
|
|
835
881
|
getTextColorForBackground,
|
|
@@ -839,6 +885,7 @@ export {
|
|
|
839
885
|
hasPreset2 as hasPreset,
|
|
840
886
|
hlsLadderAlignment,
|
|
841
887
|
invalidateSlotCache2 as invalidateSlotCache,
|
|
888
|
+
isUniversallyPlayableAudio,
|
|
842
889
|
iteratePaletteSwatches,
|
|
843
890
|
mimeFromFileName,
|
|
844
891
|
pickAmbientBackground,
|
|
@@ -848,6 +895,7 @@ export {
|
|
|
848
895
|
serializeTransform,
|
|
849
896
|
setCdnBase2 as setCdnBase,
|
|
850
897
|
setTenantId2 as setTenantId,
|
|
898
|
+
signAccessUrl,
|
|
851
899
|
signTransformUrl
|
|
852
900
|
};
|
|
853
901
|
//# sourceMappingURL=index.js.map
|