@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.
@@ -87,7 +87,7 @@ const COVER_CROP = { fit: "cover", gravity: "auto" } as const;
87
87
 
88
88
  Output URL shape: `https://8ok.uk/t/format=webp,width=640/<sha16>.webp`.
89
89
 
90
- ⚠️ **Widths MUST be on the unsigned ladder** `TRANSFORM_WIDTHS` (`160,240,256,320,400,480,600,640,800,960,1080,1200,1280,1440,1600,1920,2560,3840`). Any other width → **HTTP 400** at the edge (DoS guard). The `TransformWidth` type makes an off-ladder width a compile error — import the type, don't hardcode magic numbers. (For a one-off custom width you'd need signed URLs; not used here.)
90
+ ⚠️ **Widths MUST be on the unsigned ladder** `TRANSFORM_WIDTHS` (`96,128,160,240,256,320,400,480,600,640,800,960,1080,1200,1280,1440,1600,1920,2560,3840` — 20 widths). Any other width → **HTTP 400** at the edge (DoS guard). The `TransformWidth` type makes an off-ladder width a compile error — import the type, don't hardcode magic numbers. (For a one-off custom width you'd need signed URLs; not used here.)
91
91
 
92
92
  ## 2b. A responsive gallery — the widths, with their measured weight
93
93
 
@@ -221,7 +221,7 @@ export function videoUrl(sha: string, tenantId = TENANT_ID): string {
221
221
  Output: `https://8ok.uk/<tenantId.toString(36)>/v/<sha16>-v.mp4`.
222
222
 
223
223
  **Do NOT:**
224
- - ❌ Use `getVideoTransformUrl` — that builds a `/t/...` transform URL, which **410s** for a stored video sha. (`getVideoTransformUrl` is for on-the-fly re-encodes, a different feature.)
224
+ - ❌ Use `getVideoTransformUrl` — that builds a `/t/...` transform URL, which never returns playable video. On a video sha `/t/` transforms the **poster frame**: `200 image/webp` with `x-transform-source: poster` when a poster exists, `410` when it does not. (`getVideoTransformUrl` is for on-the-fly re-encodes, a different feature.)
225
225
  - ❌ Hand-roll the path with the decimal tenant id. The path segment is **base36**: `tenantId.toString(36)`. **Tenant 10 → `/a/v/`**, and the decimal `/10/v/` **404s**. This is invisible for tenants ≤ 9 (`8`→`8`, `9`→`9`) and bit a real migration only at tenant 10. Always delegate to `getAssetUrl` so the encoding can't drift.
226
226
 
227
227
  **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.
@@ -384,7 +384,11 @@ hls.js, which cannot start there, and fall through to the MP4. The `|| !MediaSou
384
384
  covers it: native if the engine is Apple's **OR** if there is no MSE to fall back on.
385
385
 
386
386
  **When you use hls.js, stop it guessing** — the defaults are how a fast connection still opens at
387
- 240p: `{ startLevel: -1, testBandwidth: true, abrEwmaDefaultEstimate: 1_000_000 }`.
387
+ 240p. ⚠️ **Do NOT pass `startLevel: -1` together with `testBandwidth: true`** measured,
388
+ that pair is the *cause*: on a clip short enough to be one segment the bandwidth probe IS
389
+ the whole video, so it plays at the bottom rung start to finish (a 5.04 s 4K asset was
390
+ delivered at 426×240 because of it). Leave the start level unset and raise the estimate:
391
+ `{ abrEwmaDefaultEstimate: 5_000_000 }`.
388
392
 
389
393
  **The first request can answer `202`** while the background job builds the ladder (1–3 min for a
390
394
  90 s source), then `302`s to the cached master. Keep the progressive MP4 as the fallback `<source>`
@@ -398,7 +402,7 @@ What it does NOT do is prefer Apple's engine where both work: a modern iPhone ge
398
402
  hls.js tuning goes in the `config` prop:
399
403
 
400
404
  ```tsx
401
- const HLS_CONFIG = { capLevelToPlayerSize: false, startLevel: -1, testBandwidth: true, abrEwmaDefaultEstimate: 1_000_000 };
405
+ const HLS_CONFIG = { capLevelToPlayerSize: false, abrEwmaDefaultEstimate: 5_000_000 };
402
406
  <HlsVideo src={getHlsStreamingUrl(asset)} config={HLS_CONFIG} poster={poster} playsInline crossOrigin="anonymous" />
403
407
  ```
404
408
 
@@ -475,6 +479,77 @@ That replaces the older hand-run provisioning scripts and SQL that used to live
475
479
 
476
480
  Variant preset short codes: `thumb=q, sm=s, md=m, lg=l, xl=x, original=o, poster=p, video=v`. Exts: images `webp`, video `mp4`.
477
481
 
482
+
483
+ ## Private assets — `visibility`
484
+
485
+ Default is `"public"`. Set `private` and **every public door answers 404** —
486
+ stored variants, the raw original, the HLS ladder, and `/t/` (including the
487
+ poster frame of a private video). The bytes come back only through a signed URL
488
+ that expires.
489
+
490
+ ```ts
491
+ import { getPrivateAssetUrl, getPrivateTransformUrl } from "@nitida/sdk";
492
+ // ON YOUR BACKEND, once you decided this viewer may see it:
493
+ await getPrivateAssetUrl(asset, "lg", signingKey, { expiresInSeconds: 300 });
494
+ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeconds: 300 });
495
+ ```
496
+
497
+ - **A 404 on a private asset is NOT a missing file.** Check `visibility` on the
498
+ DTO before you check storage. It is the number-one support question.
499
+ - **Seven URL builders throw** rather than hand you a doomed URL — `getAssetUrl`,
500
+ `getAssetSrcSet`, `getTransformUrl`, `getTransformSrcSet`,
501
+ `getVideoTransformUrl`, `getHlsStreamingUrl`, `getSignedTransformUrl` — but
502
+ only when the value you pass says `visibility: "private"`. A DTO that says
503
+ `"public"`, or a bare `{ sha }`, is never refused — and this is a DIFFERENT
504
+ rule from the missing-preset fallback below, which is about presets, not
505
+ privacy.
506
+ - **`exp` is mandatory**; revocation is *"within a minute"* (60 s TTL at the edge).
507
+ - **The signing key is a backend secret** — it mints URLs for every private
508
+ asset the tenant owns.
509
+
510
+ ## Two ways an image gets smaller, and only one of them is yours to call
511
+
512
+ This is the question every programmatic caller gets wrong, so it is stated flat:
513
+
514
+ | | who does it | what it shrinks |
515
+ |---|---|---|
516
+ | **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 |
517
+ | **Variants + `/t/`** | the backend, on request | the **delivery**: 19 MB JPEG → 170 kB WebP at 1920 |
518
+
519
+ **The backend never recompresses the raw. Ever.** That is deliberate: the raw
520
+ has to stay pristine so variants are *regenerable* — the day you add AVIF or
521
+ raise the max dimension, the pipeline re-runs against it. A client→server lossy
522
+ chain bakes artifacts in forever.
523
+
524
+ So for an API/agent upload there is nothing to "turn on": you were never going
525
+ to compress the raw, and delivery is already optimised two ways.
526
+
527
+ ```ts
528
+ await aq.upload(file);
529
+ aq.transform(asset, { width: 1280, format: "webp" }); // → /t/…, generated once, cached after
530
+
531
+ // Only for a rung you KNOW will be rendered over and over:
532
+ await aq.upload(file, { presets: ["original", "thumb"] });
533
+ ```
534
+
535
+ **`presets` defaults to `["original"]`** — a bare upload stores the raw and no
536
+ rendition. Since 2026-08-22 that is no longer a trap: when the DTO says a preset
537
+ was never materialised, `getAssetUrl` / `urlFor` **fall back to `/t/`** instead
538
+ of returning a URL that 404s. You still get optimised bytes; you just pay the
539
+ first encode.
540
+
541
+ **On demand is the default, and it is the right one.** A variant exists because
542
+ somebody asked for it; we do not manufacture sizes on the chance that they might
543
+ be wanted. Name presets at upload only for a rung you *know* will be rendered
544
+ over and over — a card thumbnail on every listing page — where paying the encode
545
+ once up front beats paying it once lazily. For everything else, upload bare and
546
+ let `/t/` do it.
547
+
548
+ ⚠️ `getAssetSrcSet` deliberately does **not** fall back — a srcSet promises
549
+ pixel widths and a transform cannot keep that promise on a source smaller than
550
+ the rung, because the pipeline never enlarges. An empty srcSet degrades to `src`; a lying one
551
+ degrades to a wrong choice.
552
+
478
553
  ## 7. Browser-direct uploads — three things that only fail in a real browser
479
554
 
480
555
  Measured 2026-08-15 on the public bench at <https://media-harness.vercel.app> — no credentials
@@ -507,7 +582,7 @@ needed. Reproduce there before debugging any of these by hand.
507
582
  | Symptom | Cause |
508
583
  |---|---|
509
584
  | **400** on an image URL | width not on `TRANSFORM_WIDTHS` ladder |
510
- | **410** on a video URL | used `/t/` (transform) for a stored video — use `getAssetUrl(...,'video')` |
585
+ | **410** on a video `/t/` URL | that video has no `poster`. With one, `/t/` returns the poster as an image, never the video — use `getAssetUrl(...,'video')` |
511
586
  | **404** on a video URL | decimal tenant prefix (`/10/v/`) instead of base36 (`/a/v/`) — call `setTenantId` + `getAssetUrl`. Tenant 12 → `/c/v/` |
512
587
  | `process returned no assetId` | fixed in the 2026-08-16 deploy — you are on a server deploy older than that, §7.2 |
513
588
  | `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 |
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Which audio uploads already play everywhere — the single source of truth for
3
+ * both the server's transcode guard and the client's delivery-preset choice.
4
+ *
5
+ * It lives in its own module, rather than beside the preset order that uses it,
6
+ * for a mechanical reason: `apps/asset-manager` imports it by relative path.
7
+ * The SDK is a PUBLISHED package, so its `exports` point at `dist/`, and
8
+ * `dist/` is gitignored — it does not exist inside the Docker image, which
9
+ * copies `packages/` as source. A bare `@nitida/sdk` import type-checks on any
10
+ * machine that built the package once and then fails the image build. (It did,
11
+ * on 2026-08-22, with `@nitida/asset-client`.) A single small file keeps that
12
+ * relative import from dragging the whole client into the server.
13
+ *
14
+ * Two copies of this list drifting apart is how a platform ends up generating
15
+ * a variant its own client refuses to use — which is exactly the bug this
16
+ * predicate was introduced to end.
17
+ */
18
+
19
+ /**
20
+ * `true` when a browser can play these bytes as uploaded, so re-encoding them
21
+ * to MP3 buys nothing.
22
+ *
23
+ * - `audio/mpeg` — unambiguous.
24
+ * - `audio/mp4` / `audio/aac` — plays in Safari, Chrome, Firefox and Edge on
25
+ * desktop and mobile. The historical worry was old AOSP builds without
26
+ * proprietary codecs; the call to treat AAC as universal was made with the
27
+ * platform's one real AAC consumer, whose viewer is WebGL — a device that
28
+ * cannot decode AAC cannot run that product at all, so the fallback would
29
+ * only ever protect a device that had already lost.
30
+ *
31
+ * Deliberately NOT here: `audio/webm` and `audio/ogg` (Opus). iOS Safari
32
+ * cannot decode them, and that is the case the MP3 fallback exists for.
33
+ */
34
+ export function isUniversallyPlayableAudio(mime: string): boolean {
35
+ const base = (mime.split(";")[0] ?? "").trim().toLowerCase();
36
+ return base === "audio/mpeg" || base === "audio/mp4" || base === "audio/aac";
37
+ }
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  type VariantEntryPreset,
62
62
  type VariantPreset,
63
63
  } from "@nitida/asset-client";
64
+ import { isUniversallyPlayableAudio } from "./audio-compat";
64
65
 
65
66
  // ---------------------------------------------------------------------------
66
67
  // Config
@@ -114,7 +115,9 @@ export type NitidaClientOptions = {
114
115
  * only when calling `aq.transform(asset, opts, { sign: true })`.
115
116
  *
116
117
  * 32 random bytes, generated server-side on tenant creation; fetch
117
- * via `GET /admin/tenants/:id` with an admin key. **Keep it
118
+ * via `POST /admin/projects/:code/rotate-signing-key`, which needs a
119
+ * SYSTEM-scope credential the platform operator holds — your own admin key
120
+ * answers `403 SYSTEM_KEY_REQUIRED`. Ask for it. **Keep it
118
121
  * server-side only** — do not ship in `NEXT_PUBLIC_*` env vars. Sign
119
122
  * URLs from a BFF route handler, or pre-sign at build time.
120
123
  */
@@ -207,6 +210,7 @@ export type {
207
210
  PaletteSwatch,
208
211
  RequestablePreset,
209
212
  ResolveSlotOptions,
213
+ SignAccessOptions,
210
214
  SignedTransformOptions,
211
215
  SlotDTO,
212
216
  SlotResolution,
@@ -218,12 +222,17 @@ export type {
218
222
  TransformWidth,
219
223
  VariantEntryPreset,
220
224
  VariantPreset,
225
+ VisibilityHint,
221
226
  } from "@nitida/asset-client";
222
227
  export {
228
+ accessMessage,
229
+ assertPublic,
230
+ assertSha,
223
231
  bestTextContrast,
224
232
  computeVariantDimensions,
225
233
  configureSlotResolver,
226
234
  contrastRatio,
235
+ deriveAccessKey,
227
236
  extractAssetSha,
228
237
  getAmbientGradient,
229
238
  getAssetDimensions,
@@ -234,6 +243,8 @@ export {
234
243
  getHlsStreamingUrl,
235
244
  getPaletteBlurBackground,
236
245
  getPaletteCssVars,
246
+ getPrivateAssetUrl,
247
+ getPrivateTransformUrl,
237
248
  getSignedTransformUrl,
238
249
  getTenantId,
239
250
  getTextColorForBackground,
@@ -255,6 +266,7 @@ export {
255
266
  serializeTransform,
256
267
  setCdnBase,
257
268
  setTenantId,
269
+ signAccessUrl,
258
270
  signTransformUrl,
259
271
  TRANSFORM_WIDTHS,
260
272
  } from "@nitida/asset-client";
@@ -625,9 +637,10 @@ class AssetsApi {
625
637
  }
626
638
 
627
639
  /**
628
- * Add or rebuild variants on an existing asset. Image presets are
629
- * MERGED with what's there passing `{ presets: ["thumb"] }` adds
630
- * the thumb variant without touching `lg`, `sm`, `original`, etc.
640
+ * Add or rebuild variants on an existing asset. Presets are MERGED
641
+ * with what's there, for images AND for video passing
642
+ * `{ presets: ["thumb"] }` adds the thumb variant without touching
643
+ * `lg`, `sm`, `original`, etc.
631
644
  *
632
645
  * // Day 0: upload original-only logo
633
646
  * const { assetId } = await aq.upload(logoFile); // defaults to ["original"]
@@ -646,10 +659,18 @@ class AssetsApi {
646
659
  * reading the source bytes from the permanent `original` variant —
647
660
  * no need to re-upload.
648
661
  *
649
- * Video presets are filtered to `["poster","video","aiproxy"]` and
650
- * dispatched to a background job (the call returns immediately
662
+ * Video presets are filtered to `["poster","video","aiproxy","probe"]`
663
+ * and dispatched to a background job (the call returns immediately
651
664
  * with a dispatch handle; poll `aq.assets.get(id).status` for
652
665
  * completion).
666
+ *
667
+ * ⚠️ Before 2026-08-22 the video path REPLACED the whole variant
668
+ * registry instead of merging, so a partial regenerate silently
669
+ * deregistered `poster`, `video` and — irrecoverably — `hls`, which
670
+ * is not a {@link RequestablePreset} and therefore cannot be asked
671
+ * for again. The objects kept serving from the CDN; only the
672
+ * registry died. Fixed server-side; a client on an older server
673
+ * still loses them.
653
674
  */
654
675
  async regenerate(
655
676
  assetId: string,
@@ -998,6 +1019,22 @@ const DEFAULT_UPLOAD_PRESETS: RequestablePreset[] = ["original"];
998
1019
  export type UploadResult = {
999
1020
  assetId: string;
1000
1021
  sha256: string;
1022
+ /**
1023
+ * The SAME 16-hex prefix an `AssetDTO` carries, so the result of an upload can
1024
+ * be handed straight to any URL builder.
1025
+ *
1026
+ * ⭐ It exists because it did not, and that cost a real 404. A Haiku agent
1027
+ * evaluating the SDK on 2026-08-23 did the most natural thing there is —
1028
+ * `transform(await upload(file), { width: 1280 })` — and got
1029
+ * `https://8ok.uk/t/width=1280/undefined.webp`. The builders read `sha`; this
1030
+ * type only had `sha256`. TypeScript caught it; running through `bun`, or in
1031
+ * plain JS, nothing did.
1032
+ *
1033
+ * The guard (`assertSha`) is the backstop. This field is the actual fix: the
1034
+ * obvious call is now the correct one, which is worth more than a good error
1035
+ * message about the wrong one.
1036
+ */
1037
+ sha: string;
1001
1038
  cdnUrl: string;
1002
1039
  };
1003
1040
 
@@ -1158,6 +1195,8 @@ class UsageApi {
1158
1195
  }
1159
1196
  }
1160
1197
 
1198
+ export { isUniversallyPlayableAudio } from "./audio-compat";
1199
+
1161
1200
  export class NitidaClient {
1162
1201
  readonly slots: SlotsApi;
1163
1202
  readonly assets: AssetsApi;
@@ -1257,7 +1296,7 @@ export class NitidaClient {
1257
1296
  if (!this.opts.signingKey) {
1258
1297
  throw new Error(
1259
1298
  "aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. " +
1260
- "Pull the tenant's signing key from /admin/tenants/:id and pass it to the SDK constructor on a SERVER-side instance only.",
1299
+ "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 — 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.",
1261
1300
  );
1262
1301
  }
1263
1302
  // Signed path — custom (off-ladder) widths allowed. Empty opts → no
@@ -1541,6 +1580,7 @@ export class NitidaClient {
1541
1580
  return {
1542
1581
  assetId: existing.id,
1543
1582
  sha256: sha,
1583
+ sha: sha.slice(0, 16),
1544
1584
  cdnUrl: this.urlFor(existing, this.bestPresetForAsset(existing, mime)),
1545
1585
  };
1546
1586
  }
@@ -1566,6 +1606,7 @@ export class NitidaClient {
1566
1606
  return {
1567
1607
  assetId: presign.asset.id,
1568
1608
  sha256: sha,
1609
+ sha: sha.slice(0, 16),
1569
1610
  cdnUrl: this.urlFor(presign.asset, this.defaultPresetForMime(mime)),
1570
1611
  };
1571
1612
  }
@@ -1616,6 +1657,7 @@ export class NitidaClient {
1616
1657
  return {
1617
1658
  assetId,
1618
1659
  sha256: sha,
1660
+ sha: sha.slice(0, 16),
1619
1661
  cdnUrl: this.urlFor(final, this.bestPresetForAsset(final, mime)),
1620
1662
  };
1621
1663
  }
@@ -1633,15 +1675,35 @@ export class NitidaClient {
1633
1675
  * `presets` string. Falls back through the preference order
1634
1676
  * lg → md → sm → thumb → original (for images)
1635
1677
  * video → poster (for videos)
1636
- * mp3original (for audio)
1678
+ * originalmp3 (for audio ALREADY playable everywhere)
1679
+ * mp3 → original (for any other audio)
1637
1680
  * so an upload that was processed with e.g. `["original"]` still
1638
1681
  * returns a non-404 URL in `aq.upload`'s result.
1682
+ *
1683
+ * ⭐ Why audio branches on the source mime (changed 2026-08-22).
1684
+ *
1685
+ * It used to be `mp3 → original` unconditionally, so `upload().cdnUrl`
1686
+ * handed back the server's auto-generated mp3 — libmp3lame, mono ~96 kbps —
1687
+ * even when the caller had uploaded an MP3 or an AAC that already plays in
1688
+ * every target browser. A consumer asking for "my file" silently received a
1689
+ * re-encoded, lower-quality one, with no error to notice.
1690
+ *
1691
+ * Measured across the platform: of 126 audio assets carrying an mp3 variant,
1692
+ * **105 had an `audio/mpeg` source** — an MP3 re-encoded into an MP3, for
1693
+ * zero compatibility gain.
1694
+ *
1695
+ * The mp3 still wins for `audio/webm`/Opus and anything exotic, which is the
1696
+ * case it was built for: Chrome records webm/Opus, which iOS Safari cannot
1697
+ * decode. That guarantee is preserved exactly; only the needless downgrade
1698
+ * is gone.
1639
1699
  */
1640
1700
  private bestPresetForAsset(asset: AssetDTO, mime: string): VariantPreset {
1641
1701
  const order: VariantPreset[] = mime.startsWith("video/")
1642
1702
  ? ["video", "poster"]
1643
1703
  : mime.startsWith("audio/")
1644
- ? ["mp3", "original"] // prefer the cross-browser mp3, else the playable original; never image presets
1704
+ ? isUniversallyPlayableAudio(mime)
1705
+ ? ["original", "mp3"] // the upload already plays everywhere — don't hand back a re-encode
1706
+ : ["mp3", "original"] // exotic codec: the cross-browser mp3 earns its place
1645
1707
  : ["lg", "md", "sm", "thumb", "xl", "original"];
1646
1708
  return (
1647
1709
  order.find((p) => hasPreset(asset, p)) ?? this.defaultPresetForMime(mime)
@@ -94,6 +94,9 @@ export {
94
94
  type AssetDTO,
95
95
  type AssetPalette,
96
96
  type AssetVariant,
97
+ accessMessage,
98
+ assertPublic,
99
+ assertSha,
97
100
  bestTextContrast,
98
101
  type ComposeMarketingComposition,
99
102
  type ComposeMarketingOptions,
@@ -103,6 +106,7 @@ export {
103
106
  computeVariantDimensions,
104
107
  configureSlotResolver,
105
108
  contrastRatio,
109
+ deriveAccessKey,
106
110
  extractAssetSha,
107
111
  getAmbientGradient,
108
112
  getAssetDimensions,
@@ -113,6 +117,8 @@ export {
113
117
  getHlsStreamingUrl,
114
118
  getPaletteBlurBackground,
115
119
  getPaletteCssVars,
120
+ getPrivateAssetUrl,
121
+ getPrivateTransformUrl,
116
122
  getSignedTransformUrl,
117
123
  getTenantId,
118
124
  getTextColorForBackground,
@@ -123,6 +129,7 @@ export {
123
129
  hasPreset,
124
130
  hlsLadderAlignment,
125
131
  invalidateSlotCache,
132
+ isUniversallyPlayableAudio,
126
133
  iteratePaletteSwatches,
127
134
  mimeFromFileName,
128
135
  type NitidaClientOptions,
@@ -139,6 +146,7 @@ export {
139
146
  relativeLuminance,
140
147
  resolveSlot,
141
148
  resolveSlots,
149
+ type SignAccessOptions,
142
150
  type SignedTransformOptions,
143
151
  type SlotDTO,
144
152
  type SlotHistoryEntry,
@@ -146,6 +154,7 @@ export {
146
154
  serializeTransform,
147
155
  setCdnBase,
148
156
  setTenantId,
157
+ signAccessUrl,
149
158
  signTransformUrl,
150
159
  TRANSFORM_WIDTHS,
151
160
  type TransformEffect,
@@ -164,4 +173,5 @@ export {
164
173
  type UsageWindow,
165
174
  type VariantEntryPreset,
166
175
  type VariantPreset,
176
+ type VisibilityHint,
167
177
  } from "..";
package/src/web/index.ts CHANGED
@@ -111,6 +111,9 @@ export {
111
111
  type AssetDTO,
112
112
  type AssetPalette,
113
113
  type AssetVariant,
114
+ accessMessage,
115
+ assertPublic,
116
+ assertSha,
114
117
  bestTextContrast,
115
118
  type ComposeMarketingComposition,
116
119
  type ComposeMarketingOptions,
@@ -120,6 +123,7 @@ export {
120
123
  computeVariantDimensions,
121
124
  configureSlotResolver,
122
125
  contrastRatio,
126
+ deriveAccessKey,
123
127
  extractAssetSha,
124
128
  getAmbientGradient,
125
129
  getAssetDimensions,
@@ -130,6 +134,8 @@ export {
130
134
  getHlsStreamingUrl,
131
135
  getPaletteBlurBackground,
132
136
  getPaletteCssVars,
137
+ getPrivateAssetUrl,
138
+ getPrivateTransformUrl,
133
139
  getSignedTransformUrl,
134
140
  getTenantId,
135
141
  getTextColorForBackground,
@@ -140,6 +146,7 @@ export {
140
146
  hasPreset,
141
147
  hlsLadderAlignment,
142
148
  invalidateSlotCache,
149
+ isUniversallyPlayableAudio,
143
150
  iteratePaletteSwatches,
144
151
  mimeFromFileName,
145
152
  type PaletteSwatch,
@@ -155,6 +162,7 @@ export {
155
162
  relativeLuminance,
156
163
  resolveSlot,
157
164
  resolveSlots,
165
+ type SignAccessOptions,
158
166
  type SignedTransformOptions,
159
167
  type SlotDTO,
160
168
  type SlotHistoryEntry,
@@ -162,6 +170,7 @@ export {
162
170
  serializeTransform,
163
171
  setCdnBase,
164
172
  setTenantId,
173
+ signAccessUrl,
165
174
  signTransformUrl,
166
175
  TRANSFORM_WIDTHS,
167
176
  type TransformEffect,
@@ -180,6 +189,7 @@ export {
180
189
  type UsageWindow,
181
190
  type VariantEntryPreset,
182
191
  type VariantPreset,
192
+ type VisibilityHint,
183
193
  } from "..";
184
194
 
185
195
  // Multipart uploader helpers are NOT re-exported from this subpath.