@nitida/sdk 0.26.0 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +71 -0
- package/README.md +63 -2
- package/dist/index.d.ts +89 -9
- package/dist/index.js +61 -8
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +61 -8
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +2 -2
- package/dist/web.js +61 -8
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/skills/nitida-sdk/SKILL.md +80 -5
- package/src/audio-compat.ts +37 -0
- package/src/index.ts +71 -9
- package/src/server/index.ts +10 -0
- package/src/web/index.ts +10 -0
package/AGENTS.md
CHANGED
|
@@ -74,6 +74,77 @@ 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 says `visibility: "private"`. A DTO that says
|
|
98
|
+
`"public"`, or a bare `{ sha }`, is never refused — and this is a DIFFERENT
|
|
99
|
+
rule from the missing-preset fallback below, which is about presets, not
|
|
100
|
+
privacy.
|
|
101
|
+
- **`exp` is mandatory**; revocation is *"within a minute"* (60 s TTL at the edge).
|
|
102
|
+
- **The signing key is a backend secret** — it mints URLs for every private
|
|
103
|
+
asset the tenant owns.
|
|
104
|
+
|
|
105
|
+
## Two ways an image gets smaller, and only one of them is yours to call
|
|
106
|
+
|
|
107
|
+
This is the question every programmatic caller gets wrong, so it is stated flat:
|
|
108
|
+
|
|
109
|
+
| | who does it | what it shrinks |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| **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 |
|
|
112
|
+
| **Variants + `/t/`** | the backend, on request | the **delivery**: 19 MB JPEG → 170 kB WebP at 1920 |
|
|
113
|
+
|
|
114
|
+
**The backend never recompresses the raw. Ever.** That is deliberate: the raw
|
|
115
|
+
has to stay pristine so variants are *regenerable* — the day you add AVIF or
|
|
116
|
+
raise the max dimension, the pipeline re-runs against it. A client→server lossy
|
|
117
|
+
chain bakes artifacts in forever.
|
|
118
|
+
|
|
119
|
+
So for an API/agent upload there is nothing to "turn on": you were never going
|
|
120
|
+
to compress the raw, and delivery is already optimised two ways.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
await aq.upload(file);
|
|
124
|
+
aq.transform(asset, { width: 1280, format: "webp" }); // → /t/…, generated once, cached after
|
|
125
|
+
|
|
126
|
+
// Only for a rung you KNOW will be rendered over and over:
|
|
127
|
+
await aq.upload(file, { presets: ["original", "thumb"] });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**`presets` defaults to `["original"]`** — a bare upload stores the raw and no
|
|
131
|
+
rendition. Since 2026-08-22 that is no longer a trap: when the DTO says a preset
|
|
132
|
+
was never materialised, `getAssetUrl` / `urlFor` **fall back to `/t/`** instead
|
|
133
|
+
of returning a URL that 404s. You still get optimised bytes; you just pay the
|
|
134
|
+
first encode.
|
|
135
|
+
|
|
136
|
+
**On demand is the default, and it is the right one.** A variant exists because
|
|
137
|
+
somebody asked for it; we do not manufacture sizes on the chance that they might
|
|
138
|
+
be wanted. Name presets at upload only for a rung you *know* will be rendered
|
|
139
|
+
over and over — a card thumbnail on every listing page — where paying the encode
|
|
140
|
+
once up front beats paying it once lazily. For everything else, upload bare and
|
|
141
|
+
let `/t/` do it.
|
|
142
|
+
|
|
143
|
+
⚠️ `getAssetSrcSet` deliberately does **not** fall back — a srcSet promises
|
|
144
|
+
pixel widths and a transform cannot keep that promise on a source smaller than
|
|
145
|
+
the rung, because the pipeline never enlarges. An empty srcSet degrades to `src`; a lying one
|
|
146
|
+
degrades to a wrong choice.
|
|
147
|
+
|
|
77
148
|
## Related packages
|
|
78
149
|
|
|
79
150
|
| 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, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl } from '@nitida/asset-client';
|
|
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[];
|
|
@@ -542,6 +587,22 @@ type UploadOptions = {
|
|
|
542
587
|
type UploadResult = {
|
|
543
588
|
assetId: string;
|
|
544
589
|
sha256: string;
|
|
590
|
+
/**
|
|
591
|
+
* The SAME 16-hex prefix an `AssetDTO` carries, so the result of an upload can
|
|
592
|
+
* be handed straight to any URL builder.
|
|
593
|
+
*
|
|
594
|
+
* ⭐ It exists because it did not, and that cost a real 404. A Haiku agent
|
|
595
|
+
* evaluating the SDK on 2026-08-23 did the most natural thing there is —
|
|
596
|
+
* `transform(await upload(file), { width: 1280 })` — and got
|
|
597
|
+
* `https://8ok.uk/t/width=1280/undefined.webp`. The builders read `sha`; this
|
|
598
|
+
* type only had `sha256`. TypeScript caught it; running through `bun`, or in
|
|
599
|
+
* plain JS, nothing did.
|
|
600
|
+
*
|
|
601
|
+
* The guard (`assertSha`) is the backstop. This field is the actual fix: the
|
|
602
|
+
* obvious call is now the correct one, which is worth more than a good error
|
|
603
|
+
* message about the wrong one.
|
|
604
|
+
*/
|
|
605
|
+
sha: string;
|
|
545
606
|
cdnUrl: string;
|
|
546
607
|
};
|
|
547
608
|
type SlotHistoryEntry = {
|
|
@@ -647,6 +708,7 @@ declare class UsageApi {
|
|
|
647
708
|
}>;
|
|
648
709
|
private headers;
|
|
649
710
|
}
|
|
711
|
+
|
|
650
712
|
declare class NitidaClient {
|
|
651
713
|
readonly slots: SlotsApi;
|
|
652
714
|
readonly assets: AssetsApi;
|
|
@@ -822,11 +884,29 @@ declare class NitidaClient {
|
|
|
822
884
|
* `presets` string. Falls back through the preference order
|
|
823
885
|
* lg → md → sm → thumb → original (for images)
|
|
824
886
|
* video → poster (for videos)
|
|
825
|
-
*
|
|
887
|
+
* original → mp3 (for audio ALREADY playable everywhere)
|
|
888
|
+
* mp3 → original (for any other audio)
|
|
826
889
|
* so an upload that was processed with e.g. `["original"]` still
|
|
827
890
|
* returns a non-404 URL in `aq.upload`'s result.
|
|
891
|
+
*
|
|
892
|
+
* ⭐ Why audio branches on the source mime (changed 2026-08-22).
|
|
893
|
+
*
|
|
894
|
+
* It used to be `mp3 → original` unconditionally, so `upload().cdnUrl`
|
|
895
|
+
* handed back the server's auto-generated mp3 — libmp3lame, mono ~96 kbps —
|
|
896
|
+
* even when the caller had uploaded an MP3 or an AAC that already plays in
|
|
897
|
+
* every target browser. A consumer asking for "my file" silently received a
|
|
898
|
+
* re-encoded, lower-quality one, with no error to notice.
|
|
899
|
+
*
|
|
900
|
+
* Measured across the platform: of 126 audio assets carrying an mp3 variant,
|
|
901
|
+
* **105 had an `audio/mpeg` source** — an MP3 re-encoded into an MP3, for
|
|
902
|
+
* zero compatibility gain.
|
|
903
|
+
*
|
|
904
|
+
* The mp3 still wins for `audio/webm`/Opus and anything exotic, which is the
|
|
905
|
+
* case it was built for: Chrome records webm/Opus, which iOS Safari cannot
|
|
906
|
+
* decode. That guarantee is preserved exactly; only the needless downgrade
|
|
907
|
+
* is gone.
|
|
828
908
|
*/
|
|
829
909
|
private bestPresetForAsset;
|
|
830
910
|
}
|
|
831
911
|
|
|
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 };
|
|
912
|
+
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,23 @@ 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,
|
|
29
|
+
assertSha,
|
|
19
30
|
bestTextContrast,
|
|
20
31
|
computeVariantDimensions,
|
|
21
32
|
configureSlotResolver as configureSlotResolver2,
|
|
22
33
|
contrastRatio,
|
|
34
|
+
deriveAccessKey,
|
|
23
35
|
extractAssetSha,
|
|
24
36
|
getAmbientGradient,
|
|
25
37
|
getAssetDimensions,
|
|
@@ -30,6 +42,8 @@ import {
|
|
|
30
42
|
getHlsStreamingUrl as getHlsStreamingUrl2,
|
|
31
43
|
getPaletteBlurBackground,
|
|
32
44
|
getPaletteCssVars,
|
|
45
|
+
getPrivateAssetUrl,
|
|
46
|
+
getPrivateTransformUrl,
|
|
33
47
|
getSignedTransformUrl as getSignedTransformUrl2,
|
|
34
48
|
getTenantId,
|
|
35
49
|
getTextColorForBackground,
|
|
@@ -51,6 +65,7 @@ import {
|
|
|
51
65
|
serializeTransform,
|
|
52
66
|
setCdnBase as setCdnBase2,
|
|
53
67
|
setTenantId as setTenantId2,
|
|
68
|
+
signAccessUrl,
|
|
54
69
|
signTransformUrl,
|
|
55
70
|
TRANSFORM_WIDTHS
|
|
56
71
|
} from "@nitida/asset-client";
|
|
@@ -256,9 +271,10 @@ var AssetsApi = class {
|
|
|
256
271
|
return Array.isArray(dto.variants) ? dto.variants : [];
|
|
257
272
|
}
|
|
258
273
|
/**
|
|
259
|
-
* Add or rebuild variants on an existing asset.
|
|
260
|
-
*
|
|
261
|
-
* the thumb variant without touching
|
|
274
|
+
* Add or rebuild variants on an existing asset. Presets are MERGED
|
|
275
|
+
* with what's there, for images AND for video — passing
|
|
276
|
+
* `{ presets: ["thumb"] }` adds the thumb variant without touching
|
|
277
|
+
* `lg`, `sm`, `original`, etc.
|
|
262
278
|
*
|
|
263
279
|
* // Day 0: upload original-only logo
|
|
264
280
|
* const { assetId } = await aq.upload(logoFile); // defaults to ["original"]
|
|
@@ -277,10 +293,18 @@ var AssetsApi = class {
|
|
|
277
293
|
* reading the source bytes from the permanent `original` variant —
|
|
278
294
|
* no need to re-upload.
|
|
279
295
|
*
|
|
280
|
-
* Video presets are filtered to `["poster","video","aiproxy"]`
|
|
281
|
-
* dispatched to a background job (the call returns immediately
|
|
296
|
+
* Video presets are filtered to `["poster","video","aiproxy","probe"]`
|
|
297
|
+
* and dispatched to a background job (the call returns immediately
|
|
282
298
|
* with a dispatch handle; poll `aq.assets.get(id).status` for
|
|
283
299
|
* completion).
|
|
300
|
+
*
|
|
301
|
+
* ⚠️ Before 2026-08-22 the video path REPLACED the whole variant
|
|
302
|
+
* registry instead of merging, so a partial regenerate silently
|
|
303
|
+
* deregistered `poster`, `video` and — irrecoverably — `hls`, which
|
|
304
|
+
* is not a {@link RequestablePreset} and therefore cannot be asked
|
|
305
|
+
* for again. The objects kept serving from the CDN; only the
|
|
306
|
+
* registry died. Fixed server-side; a client on an older server
|
|
307
|
+
* still loses them.
|
|
284
308
|
*/
|
|
285
309
|
async regenerate(assetId, opts = {}) {
|
|
286
310
|
const r = await fetch(
|
|
@@ -548,7 +572,7 @@ var NitidaClient = class {
|
|
|
548
572
|
}
|
|
549
573
|
if (!this.opts.signingKey) {
|
|
550
574
|
throw new Error(
|
|
551
|
-
"aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions.
|
|
575
|
+
"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
576
|
);
|
|
553
577
|
}
|
|
554
578
|
return getSignedTransformUrl(asset, opts, this.opts.signingKey) ?? Promise.resolve(this.urlFor(asset, "lg"));
|
|
@@ -729,6 +753,7 @@ var NitidaClient = class {
|
|
|
729
753
|
return {
|
|
730
754
|
assetId: existing.id,
|
|
731
755
|
sha256: sha,
|
|
756
|
+
sha: sha.slice(0, 16),
|
|
732
757
|
cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime))
|
|
733
758
|
};
|
|
734
759
|
}
|
|
@@ -745,6 +770,7 @@ var NitidaClient = class {
|
|
|
745
770
|
return {
|
|
746
771
|
assetId: presign.asset.id,
|
|
747
772
|
sha256: sha,
|
|
773
|
+
sha: sha.slice(0, 16),
|
|
748
774
|
cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime))
|
|
749
775
|
};
|
|
750
776
|
}
|
|
@@ -787,6 +813,7 @@ var NitidaClient = class {
|
|
|
787
813
|
return {
|
|
788
814
|
assetId,
|
|
789
815
|
sha256: sha,
|
|
816
|
+
sha: sha.slice(0, 16),
|
|
790
817
|
cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime))
|
|
791
818
|
};
|
|
792
819
|
}
|
|
@@ -800,12 +827,30 @@ var NitidaClient = class {
|
|
|
800
827
|
* `presets` string. Falls back through the preference order
|
|
801
828
|
* lg → md → sm → thumb → original (for images)
|
|
802
829
|
* video → poster (for videos)
|
|
803
|
-
*
|
|
830
|
+
* original → mp3 (for audio ALREADY playable everywhere)
|
|
831
|
+
* mp3 → original (for any other audio)
|
|
804
832
|
* so an upload that was processed with e.g. `["original"]` still
|
|
805
833
|
* returns a non-404 URL in `aq.upload`'s result.
|
|
834
|
+
*
|
|
835
|
+
* ⭐ Why audio branches on the source mime (changed 2026-08-22).
|
|
836
|
+
*
|
|
837
|
+
* It used to be `mp3 → original` unconditionally, so `upload().cdnUrl`
|
|
838
|
+
* handed back the server's auto-generated mp3 — libmp3lame, mono ~96 kbps —
|
|
839
|
+
* even when the caller had uploaded an MP3 or an AAC that already plays in
|
|
840
|
+
* every target browser. A consumer asking for "my file" silently received a
|
|
841
|
+
* re-encoded, lower-quality one, with no error to notice.
|
|
842
|
+
*
|
|
843
|
+
* Measured across the platform: of 126 audio assets carrying an mp3 variant,
|
|
844
|
+
* **105 had an `audio/mpeg` source** — an MP3 re-encoded into an MP3, for
|
|
845
|
+
* zero compatibility gain.
|
|
846
|
+
*
|
|
847
|
+
* The mp3 still wins for `audio/webm`/Opus and anything exotic, which is the
|
|
848
|
+
* case it was built for: Chrome records webm/Opus, which iOS Safari cannot
|
|
849
|
+
* decode. That guarantee is preserved exactly; only the needless downgrade
|
|
850
|
+
* is gone.
|
|
806
851
|
*/
|
|
807
852
|
bestPresetForAsset(asset, mime) {
|
|
808
|
-
const order = mime.startsWith("video/") ? ["video", "poster"] : mime.startsWith("audio/") ? ["mp3", "original"] : ["lg", "md", "sm", "thumb", "xl", "original"];
|
|
853
|
+
const order = mime.startsWith("video/") ? ["video", "poster"] : mime.startsWith("audio/") ? isUniversallyPlayableAudio(mime) ? ["original", "mp3"] : ["mp3", "original"] : ["lg", "md", "sm", "thumb", "xl", "original"];
|
|
809
854
|
return order.find((p) => hasPreset(asset, p)) ?? this.defaultPresetForMime(mime);
|
|
810
855
|
}
|
|
811
856
|
};
|
|
@@ -816,10 +861,14 @@ export {
|
|
|
816
861
|
PRESET_MAX_DIM,
|
|
817
862
|
PRESET_SHORT,
|
|
818
863
|
TRANSFORM_WIDTHS,
|
|
864
|
+
accessMessage,
|
|
865
|
+
assertPublic,
|
|
866
|
+
assertSha,
|
|
819
867
|
bestTextContrast,
|
|
820
868
|
computeVariantDimensions,
|
|
821
869
|
configureSlotResolver2 as configureSlotResolver,
|
|
822
870
|
contrastRatio,
|
|
871
|
+
deriveAccessKey,
|
|
823
872
|
extractAssetSha,
|
|
824
873
|
getAmbientGradient,
|
|
825
874
|
getAssetDimensions,
|
|
@@ -830,6 +879,8 @@ export {
|
|
|
830
879
|
getHlsStreamingUrl2 as getHlsStreamingUrl,
|
|
831
880
|
getPaletteBlurBackground,
|
|
832
881
|
getPaletteCssVars,
|
|
882
|
+
getPrivateAssetUrl,
|
|
883
|
+
getPrivateTransformUrl,
|
|
833
884
|
getSignedTransformUrl2 as getSignedTransformUrl,
|
|
834
885
|
getTenantId,
|
|
835
886
|
getTextColorForBackground,
|
|
@@ -839,6 +890,7 @@ export {
|
|
|
839
890
|
hasPreset2 as hasPreset,
|
|
840
891
|
hlsLadderAlignment,
|
|
841
892
|
invalidateSlotCache2 as invalidateSlotCache,
|
|
893
|
+
isUniversallyPlayableAudio,
|
|
842
894
|
iteratePaletteSwatches,
|
|
843
895
|
mimeFromFileName,
|
|
844
896
|
pickAmbientBackground,
|
|
@@ -848,6 +900,7 @@ export {
|
|
|
848
900
|
serializeTransform,
|
|
849
901
|
setCdnBase2 as setCdnBase,
|
|
850
902
|
setTenantId2 as setTenantId,
|
|
903
|
+
signAccessUrl,
|
|
851
904
|
signTransformUrl
|
|
852
905
|
};
|
|
853
906
|
//# sourceMappingURL=index.js.map
|