@nitida/asset-client 0.16.4 → 0.18.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 +54 -0
- package/README.md +31 -3
- package/dist/index.cjs +247 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +305 -87
- package/dist/index.d.ts +305 -87
- package/dist/index.js +241 -99
- package/dist/index.js.map +1 -1
- package/package.json +2 -6
- package/src/access.ts +200 -0
- package/src/index.ts +278 -3
- package/src/transform.ts +39 -5
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed URLs for PRIVATE assets — the `/a/{tenant}/…?exp&sig` tree.
|
|
3
|
+
*
|
|
4
|
+
* ## Who calls this, and who must not
|
|
5
|
+
*
|
|
6
|
+
* The tenant's BACKEND, which knows who the viewer is and holds the signing
|
|
7
|
+
* key. Never a browser: shipping the signing key to the client would let any
|
|
8
|
+
* visitor mint URLs for any private asset of that tenant, which is the whole
|
|
9
|
+
* property the tree exists to provide. This module is deliberately importable
|
|
10
|
+
* from anywhere — it runs on WebCrypto, so browsers, Node, Bun and Workers all
|
|
11
|
+
* work — and that convenience is exactly why the warning is here rather than
|
|
12
|
+
* in a doc nobody reads at the call site.
|
|
13
|
+
*
|
|
14
|
+
* ## Not the same signature as `signTransformUrl`
|
|
15
|
+
*
|
|
16
|
+
* `signTransformUrl` vouches for a WIDTH; this vouches for a VIEWER, until
|
|
17
|
+
* `exp`. They come from the same `signing_key` but not the same key material:
|
|
18
|
+
* the access key is derived (`HMAC(signing_key, "nitida/access/v1")`) so that
|
|
19
|
+
* no crafted transform path can be replayed as an access signature. The full
|
|
20
|
+
* argument lives beside the server implementation in `access-signing.ts`; the
|
|
21
|
+
* short version is that a shared payload with a prefix separator IS
|
|
22
|
+
* collidable, because both fields of the transform message are
|
|
23
|
+
* attacker-influenced path segments.
|
|
24
|
+
*
|
|
25
|
+
* ## `exp` is mandatory
|
|
26
|
+
*
|
|
27
|
+
* A signed URL that never expires is a public URL as soon as someone forwards
|
|
28
|
+
* it. There is no "no expiry" option here, and there will not be one.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The per-tenant access key, derived from `signing_key`. Same value the origin
|
|
32
|
+
* computes and hands the edge — nothing needs to be stored or synchronised.
|
|
33
|
+
*/
|
|
34
|
+
declare function deriveAccessKey(signingKey: string): Promise<Uint8Array>;
|
|
35
|
+
/**
|
|
36
|
+
* The signed message. Byte-identical to the server's `accessMessage` and the
|
|
37
|
+
* worker's — three implementations of one string, which is why all three pin
|
|
38
|
+
* the exact bytes in a test.
|
|
39
|
+
*
|
|
40
|
+
* `tenantPrefix` is the base36 segment as it appears in the URL, not the
|
|
41
|
+
* decimal id: tenant 10 lives at `/a/a/`, and the thing being vouched for is a
|
|
42
|
+
* path.
|
|
43
|
+
*/
|
|
44
|
+
declare function accessMessage(tenantPrefix: string, exp: number, resourcePath: string): string;
|
|
45
|
+
type SignAccessOptions = {
|
|
46
|
+
/** Lifetime in seconds. Required — see the header. */
|
|
47
|
+
expiresInSeconds: number;
|
|
48
|
+
/** Injectable clock, for tests that need a URL already dead on arrival. */
|
|
49
|
+
nowSeconds?: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Turn a PUBLIC-tree URL into a signed PRIVATE-tree URL.
|
|
53
|
+
*
|
|
54
|
+
* https://8ok.uk/5/v/<sha16>-lg.webp
|
|
55
|
+
* → https://8ok.uk/a/5/v/<sha16>-lg.webp?exp=…&sig=…
|
|
56
|
+
*
|
|
57
|
+
* Accepts a URL that is already under `/a/` and re-signs it, so calling twice
|
|
58
|
+
* is not an error and does not produce `/a/a/`.
|
|
59
|
+
*/
|
|
60
|
+
declare function signAccessUrl(publicUrl: string, signingKey: string, opts: SignAccessOptions): Promise<string>;
|
|
61
|
+
/** What every URL builder accepts so it can refuse a doomed URL. */
|
|
62
|
+
type VisibilityHint = {
|
|
63
|
+
visibility?: "public" | "private";
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Refuse to build a public URL for a private asset.
|
|
67
|
+
*
|
|
68
|
+
* Doctrine of the house: **a silence reads as "you can't"**. Returning
|
|
69
|
+
* `https://8ok.uk/5/v/<sha>-lg.webp` for a private asset is not a smaller
|
|
70
|
+
* failure than throwing — it is a URL that answers 404, in a platform where a
|
|
71
|
+
* 404 has always meant "that file does not exist". The caller then debugs the
|
|
72
|
+
* wrong thing.
|
|
73
|
+
*
|
|
74
|
+
* Only refuses when it was actually TOLD. A caller passing `{ sha }` carries no
|
|
75
|
+
* visibility, and guessing would break every existing call site to protect
|
|
76
|
+
* assets that are not there.
|
|
77
|
+
*/
|
|
78
|
+
declare function assertPublic(asset: VisibilityHint, fn: string,
|
|
79
|
+
/**
|
|
80
|
+
* The call to make instead — declared per call site, not guessed.
|
|
81
|
+
*
|
|
82
|
+
* It matters which one: `getPrivateAssetUrl` signs a STORED preset, and
|
|
83
|
+
* pointing a transform caller at it sends them to a function that cannot do
|
|
84
|
+
* what they asked for. The first version of this message named
|
|
85
|
+
* `getPrivateAssetUrl` for all seven builders; a test caught it.
|
|
86
|
+
*/
|
|
87
|
+
escape: string): void;
|
|
88
|
+
|
|
1
89
|
/**
|
|
2
90
|
* Color palette helpers — render harmonious ambient backgrounds behind
|
|
3
91
|
* product images, inspired by Spotify Now Playing / Apple Music / Pico.
|
|
@@ -92,83 +180,6 @@ declare function iteratePaletteSwatches(palette: AssetPalette | null | undefined
|
|
|
92
180
|
*/
|
|
93
181
|
declare function getPaletteBlurBackground(palette: AssetPalette | null | undefined): string | undefined;
|
|
94
182
|
|
|
95
|
-
/**
|
|
96
|
-
* @nitida/asset-client/slots — slot resolver for tenant-named assets.
|
|
97
|
-
*
|
|
98
|
-
* Slots give tenants a way to attach stable, human-readable names
|
|
99
|
-
* ("webapp.wizard.pool-type.icon-1", "storefront.cr.hero-video.landscape_hd_16x9.mp4")
|
|
100
|
-
* to assets they uploaded. Consumers resolve names → AssetDTOs at
|
|
101
|
-
* build / runtime so their source never hardcodes a CDN URL; an
|
|
102
|
-
* admin rebinds a slot in the platform console and every consumer
|
|
103
|
-
* picks up the swap on cache refresh.
|
|
104
|
-
*
|
|
105
|
-
* Two layers in this package:
|
|
106
|
-
* - `resolveSlot` / `resolveSlots` — universal (server, edge,
|
|
107
|
-
* workers) fetch helpers. Cache 60s by default.
|
|
108
|
-
* - React hooks live in `@nitida/asset-client/react/use-slot`
|
|
109
|
-
* (kept out of this module so the SSR-safe core stays
|
|
110
|
-
* dependency-free of react).
|
|
111
|
-
*/
|
|
112
|
-
|
|
113
|
-
type SlotDTO = {
|
|
114
|
-
slotKey: string;
|
|
115
|
-
/** Preset hint set when the slot was bound (e.g. `thumb` for icon slots). */
|
|
116
|
-
preset: VariantPreset | null;
|
|
117
|
-
description: string | null;
|
|
118
|
-
updatedAt: string;
|
|
119
|
-
asset: AssetDTO;
|
|
120
|
-
};
|
|
121
|
-
type SlotResolution = {
|
|
122
|
-
/** The resolved DTO (`null` when the slot is unbound or asset missing). */
|
|
123
|
-
slot: SlotDTO | null;
|
|
124
|
-
/**
|
|
125
|
-
* Effective preset — what `url` below was built with. Resolution order:
|
|
126
|
-
* 1. caller's `preset` override
|
|
127
|
-
* 2. slot's `preset` hint
|
|
128
|
-
* 3. `lg` for images, `video` for video kind
|
|
129
|
-
*/
|
|
130
|
-
preset: VariantPreset;
|
|
131
|
-
/** The CDN URL the consumer should use. */
|
|
132
|
-
url: string | null;
|
|
133
|
-
};
|
|
134
|
-
/**
|
|
135
|
-
* Configure the resolver process-wide. Call once at boot from your
|
|
136
|
-
* storefront layout / server entry / worker init.
|
|
137
|
-
*
|
|
138
|
-
* configureSlotResolver({
|
|
139
|
-
* endpoint: process.env.AQUIENPZ_URL,
|
|
140
|
-
* apiKey: process.env.AQUIENPZ_API_KEY, // amk_rt_* — server-only
|
|
141
|
-
* tenantCode: "acme-co",
|
|
142
|
-
* });
|
|
143
|
-
*/
|
|
144
|
-
declare function configureSlotResolver(opts: {
|
|
145
|
-
endpoint?: string;
|
|
146
|
-
apiKey?: string;
|
|
147
|
-
tenantCode?: string;
|
|
148
|
-
}): void;
|
|
149
|
-
/** Wipe the in-process cache (test helper or forced refresh). */
|
|
150
|
-
declare function invalidateSlotCache(slotKey?: string): void;
|
|
151
|
-
type ResolveSlotOptions = {
|
|
152
|
-
/** Override preset (caller knows the use case better than the slot binding). */
|
|
153
|
-
preset?: VariantPreset;
|
|
154
|
-
/** TTL for the in-process cache. Default 60s. Set 0 to bypass. */
|
|
155
|
-
ttlMs?: number;
|
|
156
|
-
};
|
|
157
|
-
/**
|
|
158
|
-
* Resolve a single slot to a CDN URL. Returns `{slot: null, url: null}`
|
|
159
|
-
* when the slot is unbound — callers fall back to a placeholder.
|
|
160
|
-
*
|
|
161
|
-
* Cached for `ttlMs` (default 60s). Slot rebindings propagate within the
|
|
162
|
-
* TTL window without an app restart.
|
|
163
|
-
*/
|
|
164
|
-
declare function resolveSlot(slotKey: string, opts?: ResolveSlotOptions): Promise<SlotResolution>;
|
|
165
|
-
/**
|
|
166
|
-
* Bulk-resolve N slot keys in one round-trip. The SDK's `useSlots`
|
|
167
|
-
* React hook calls this so every storefront header (logo + tagline +
|
|
168
|
-
* nav cover + …) loads as one request.
|
|
169
|
-
*/
|
|
170
|
-
declare function resolveSlots(slotKeys: string[], opts?: ResolveSlotOptions): Promise<Record<string, SlotResolution>>;
|
|
171
|
-
|
|
172
183
|
/**
|
|
173
184
|
* On-the-fly transform URL builder.
|
|
174
185
|
*
|
|
@@ -309,7 +320,7 @@ declare function serializeTransform(opts: SignedTransformOptions): string;
|
|
|
309
320
|
* <video src={aq.transformVideo(asset, { width: 1080, height: 1920 })}
|
|
310
321
|
* autoPlay muted loop playsInline />
|
|
311
322
|
*/
|
|
312
|
-
declare function getVideoTransformUrl(asset: Pick<AssetDTO, "sha"
|
|
323
|
+
declare function getVideoTransformUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHint, opts: TransformOptions): string | null;
|
|
313
324
|
/**
|
|
314
325
|
* Build an HLS streaming URL for a VIDEO asset (Phase 5). Returns the
|
|
315
326
|
* master.m3u8 entry point — HLS-aware players (Video.js's
|
|
@@ -368,8 +379,9 @@ declare function getVideoTransformUrl(asset: Pick<AssetDTO, "sha">, opts: Transf
|
|
|
368
379
|
* on demand after the raw is unavailable it reads the `-v.mp4`, which is
|
|
369
380
|
* capped at 1920 wide. `getAssetUrl(sha, "video")` is always <= 1080p.
|
|
370
381
|
*/
|
|
371
|
-
declare function getHlsStreamingUrl(asset: Pick<AssetDTO, "sha"
|
|
372
|
-
|
|
382
|
+
declare function getHlsStreamingUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHint, opts?: Omit<TransformOptions, "format">): string;
|
|
383
|
+
|
|
384
|
+
declare function getTransformUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHint, opts: TransformOptions): string | null;
|
|
373
385
|
/**
|
|
374
386
|
* Build AND sign a transform URL, allowing an off-ladder custom `width`.
|
|
375
387
|
*
|
|
@@ -381,7 +393,7 @@ declare function getTransformUrl(asset: Pick<AssetDTO, "sha">, opts: TransformOp
|
|
|
381
393
|
* Returns `null` only when `opts` serialize to an empty DSL (no transform
|
|
382
394
|
* requested) — same contract as {@link getTransformUrl}.
|
|
383
395
|
*/
|
|
384
|
-
declare function getSignedTransformUrl(asset: Pick<AssetDTO, "sha"
|
|
396
|
+
declare function getSignedTransformUrl(asset: Pick<AssetDTO, "sha"> & VisibilityHint, opts: SignedTransformOptions, signingKey: string): Promise<string> | null;
|
|
385
397
|
/**
|
|
386
398
|
* Sign a transform URL with the tenant's HMAC signing key. Appends
|
|
387
399
|
* `?sig=<hex>` where hex = HMAC-SHA256(signingKey, `<canonical-DSL>/<filename>`).
|
|
@@ -404,7 +416,84 @@ declare function signTransformUrl(unsignedUrl: string, signingKey: string): Prom
|
|
|
404
416
|
* sizes="(max-width: 768px) 100vw, 50vw"
|
|
405
417
|
* />
|
|
406
418
|
*/
|
|
407
|
-
declare function getTransformSrcSet(asset: Pick<AssetDTO, "sha"
|
|
419
|
+
declare function getTransformSrcSet(asset: Pick<AssetDTO, "sha"> & VisibilityHint, widths: number[], extraOpts?: Omit<TransformOptions, "width">): string;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* @nitida/asset-client/slots — slot resolver for tenant-named assets.
|
|
423
|
+
*
|
|
424
|
+
* Slots give tenants a way to attach stable, human-readable names
|
|
425
|
+
* ("webapp.wizard.pool-type.icon-1", "storefront.cr.hero-video.landscape_hd_16x9.mp4")
|
|
426
|
+
* to assets they uploaded. Consumers resolve names → AssetDTOs at
|
|
427
|
+
* build / runtime so their source never hardcodes a CDN URL; an
|
|
428
|
+
* admin rebinds a slot in the platform console and every consumer
|
|
429
|
+
* picks up the swap on cache refresh.
|
|
430
|
+
*
|
|
431
|
+
* Two layers in this package:
|
|
432
|
+
* - `resolveSlot` / `resolveSlots` — universal (server, edge,
|
|
433
|
+
* workers) fetch helpers. Cache 60s by default.
|
|
434
|
+
* - React hooks live in `@nitida/asset-client/react/use-slot`
|
|
435
|
+
* (kept out of this module so the SSR-safe core stays
|
|
436
|
+
* dependency-free of react).
|
|
437
|
+
*/
|
|
438
|
+
|
|
439
|
+
type SlotDTO = {
|
|
440
|
+
slotKey: string;
|
|
441
|
+
/** Preset hint set when the slot was bound (e.g. `thumb` for icon slots). */
|
|
442
|
+
preset: VariantPreset | null;
|
|
443
|
+
description: string | null;
|
|
444
|
+
updatedAt: string;
|
|
445
|
+
asset: AssetDTO;
|
|
446
|
+
};
|
|
447
|
+
type SlotResolution = {
|
|
448
|
+
/** The resolved DTO (`null` when the slot is unbound or asset missing). */
|
|
449
|
+
slot: SlotDTO | null;
|
|
450
|
+
/**
|
|
451
|
+
* Effective preset — what `url` below was built with. Resolution order:
|
|
452
|
+
* 1. caller's `preset` override
|
|
453
|
+
* 2. slot's `preset` hint
|
|
454
|
+
* 3. `lg` for images, `video` for video kind
|
|
455
|
+
*/
|
|
456
|
+
preset: VariantPreset;
|
|
457
|
+
/** The CDN URL the consumer should use. */
|
|
458
|
+
url: string | null;
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Configure the resolver process-wide. Call once at boot from your
|
|
462
|
+
* storefront layout / server entry / worker init.
|
|
463
|
+
*
|
|
464
|
+
* configureSlotResolver({
|
|
465
|
+
* endpoint: process.env.AQUIENPZ_URL,
|
|
466
|
+
* apiKey: process.env.AQUIENPZ_API_KEY, // amk_rt_* — server-only
|
|
467
|
+
* tenantCode: "acme-co",
|
|
468
|
+
* });
|
|
469
|
+
*/
|
|
470
|
+
declare function configureSlotResolver(opts: {
|
|
471
|
+
endpoint?: string;
|
|
472
|
+
apiKey?: string;
|
|
473
|
+
tenantCode?: string;
|
|
474
|
+
}): void;
|
|
475
|
+
/** Wipe the in-process cache (test helper or forced refresh). */
|
|
476
|
+
declare function invalidateSlotCache(slotKey?: string): void;
|
|
477
|
+
type ResolveSlotOptions = {
|
|
478
|
+
/** Override preset (caller knows the use case better than the slot binding). */
|
|
479
|
+
preset?: VariantPreset;
|
|
480
|
+
/** TTL for the in-process cache. Default 60s. Set 0 to bypass. */
|
|
481
|
+
ttlMs?: number;
|
|
482
|
+
};
|
|
483
|
+
/**
|
|
484
|
+
* Resolve a single slot to a CDN URL. Returns `{slot: null, url: null}`
|
|
485
|
+
* when the slot is unbound — callers fall back to a placeholder.
|
|
486
|
+
*
|
|
487
|
+
* Cached for `ttlMs` (default 60s). Slot rebindings propagate within the
|
|
488
|
+
* TTL window without an app restart.
|
|
489
|
+
*/
|
|
490
|
+
declare function resolveSlot(slotKey: string, opts?: ResolveSlotOptions): Promise<SlotResolution>;
|
|
491
|
+
/**
|
|
492
|
+
* Bulk-resolve N slot keys in one round-trip. The SDK's `useSlots`
|
|
493
|
+
* React hook calls this so every storefront header (logo + tagline +
|
|
494
|
+
* nav cover + …) loads as one request.
|
|
495
|
+
*/
|
|
496
|
+
declare function resolveSlots(slotKeys: string[], opts?: ResolveSlotOptions): Promise<Record<string, SlotResolution>>;
|
|
408
497
|
|
|
409
498
|
/**
|
|
410
499
|
* @nitida/asset-client — read helpers for asset URLs.
|
|
@@ -432,6 +521,31 @@ declare function getTransformSrcSet(asset: Pick<AssetDTO, "sha">, widths: number
|
|
|
432
521
|
* with vertical product photos.
|
|
433
522
|
*/
|
|
434
523
|
type VariantPreset = "thumb" | "sm" | "md" | "lg" | "xl" | "original" | "poster" | "video" | "aiproxy" | "hls" | "mp3";
|
|
524
|
+
/**
|
|
525
|
+
* What you may ASK the server to produce.
|
|
526
|
+
*
|
|
527
|
+
* NOT the same set as {@link VariantPreset}, and conflating the two is the
|
|
528
|
+
* single most expensive type error this package has shipped. Three unknown
|
|
529
|
+
* agents evaluating the SDK all hit it, independently, in the same afternoon:
|
|
530
|
+
*
|
|
531
|
+
* regenerate(id, { presets: ["hls"] }) // compiled → HTTP 400
|
|
532
|
+
* upload(file, { presets: ["mp3"] }) // compiled → HTTP 400
|
|
533
|
+
*
|
|
534
|
+
* Both symbols are perfectly real — they are things a variant CAN BE. Neither
|
|
535
|
+
* is something you can ASK FOR. `hls` is built by the video pipeline when a
|
|
536
|
+
* video is transcoded; `mp3` is emitted automatically alongside any audio
|
|
537
|
+
* original so iOS Safari can play it. You do not order either one.
|
|
538
|
+
*
|
|
539
|
+
* And it was wrong in the other direction too, which nobody had noticed:
|
|
540
|
+
* **`probe` is requestable and was not on `VariantPreset` at all**, so the type
|
|
541
|
+
* forbade a request the server has always accepted.
|
|
542
|
+
*
|
|
543
|
+
* Verified 2026-08-21 against the Elysia schemas of all four write routes —
|
|
544
|
+
* `/assets/process`, the presign route, `/assets/:id/regenerate` and both
|
|
545
|
+
* multipart routes. All four accept exactly this list and nothing else, with
|
|
546
|
+
* no drift between them.
|
|
547
|
+
*/
|
|
548
|
+
type RequestablePreset = Exclude<VariantPreset, "hls" | "mp3"> | "probe";
|
|
435
549
|
/** 1-char alias used in storage keys / wire `presets` string. */
|
|
436
550
|
declare const PRESET_SHORT: Record<VariantPreset, string>;
|
|
437
551
|
declare const PRESET_LONG: Record<string, VariantPreset>;
|
|
@@ -443,9 +557,44 @@ declare const PRESET_MAX_DIM: Record<VariantPreset, number | null>;
|
|
|
443
557
|
* One generated variant of an asset. Returned by the admin endpoints
|
|
444
558
|
* (`GET /assets/:id`, `POST /assets/:id/regenerate`).
|
|
445
559
|
*/
|
|
560
|
+
/**
|
|
561
|
+
* What `AssetVariant.preset` can actually hold.
|
|
562
|
+
*
|
|
563
|
+
* ⚠️ NOT `VariantPreset`, and the difference is a real bug the type used to
|
|
564
|
+
* hide. Measured against the live API on 2026-08-21, one asset came back with
|
|
565
|
+
* **29 variants, 25 of them `transform-<hash>`** — 86 % of the array — while the
|
|
566
|
+
* type said every entry was one of eleven known presets. So this compiles:
|
|
567
|
+
*
|
|
568
|
+
* ```ts
|
|
569
|
+
* for (const v of asset.variants ?? []) getAssetUrl(asset, v.preset);
|
|
570
|
+
* ```
|
|
571
|
+
*
|
|
572
|
+
* `tsc` exits 0, and at runtime 25 of those 29 URLs come out as
|
|
573
|
+
* `<sha>-undefined.undefined` and answer 404, because `PRESET_SHORT[preset]`
|
|
574
|
+
* and `PRESET_EXT[preset]` are `undefined` for a hash that is not a preset.
|
|
575
|
+
*
|
|
576
|
+
* The `transform-*` entries are NOT junk and are not being removed: they are
|
|
577
|
+
* the materialised cache of past on-demand requests — still ready, still free
|
|
578
|
+
* to fetch — and the inventory model treats them as a first-class
|
|
579
|
+
* `transform-cache` family, which is exactly the question a composer asks
|
|
580
|
+
* ("what can I fetch cheaply right now?"). Deleting them would destroy that.
|
|
581
|
+
*
|
|
582
|
+
* So the type tells the truth instead. `(string & {})` keeps autocomplete on
|
|
583
|
+
* the known presets while admitting the rest, and a caller that wants to build
|
|
584
|
+
* a URL now has to narrow first — which is the whole point.
|
|
585
|
+
*/
|
|
586
|
+
type VariantEntryPreset = VariantPreset
|
|
587
|
+
/** Indexed stills (`-pr0.jpg`, …). Requestable, never on the compact string. */
|
|
588
|
+
| "probe"
|
|
589
|
+
/** `transform-<dslHash>` and `upscale_*` — materialised cache, not a rung. */
|
|
590
|
+
| (string & {});
|
|
446
591
|
type AssetVariant = {
|
|
447
|
-
/**
|
|
448
|
-
|
|
592
|
+
/**
|
|
593
|
+
* What this entry IS. Usually a named preset; can also be a
|
|
594
|
+
* `transform-<hash>` cache artifact — see {@link VariantEntryPreset} before
|
|
595
|
+
* passing it to {@link getAssetUrl}.
|
|
596
|
+
*/
|
|
597
|
+
preset: VariantEntryPreset;
|
|
449
598
|
/** Public CDN URL of this variant. */
|
|
450
599
|
url: string;
|
|
451
600
|
/** Pixel width. Absent for `original`-only assets where image processing was skipped, or for video presets. */
|
|
@@ -566,6 +715,23 @@ type AssetDTO = {
|
|
|
566
715
|
*/
|
|
567
716
|
presets: string;
|
|
568
717
|
status: "processing" | "ready" | "failed";
|
|
718
|
+
/**
|
|
719
|
+
* Who may fetch the bytes.
|
|
720
|
+
*
|
|
721
|
+
* - `"public"` — the CDN serves it to anyone with the URL. The default,
|
|
722
|
+
* and what all 27 484 assets were until this field existed.
|
|
723
|
+
* - `"private"` — every public door answers **404**: the stored variants,
|
|
724
|
+
* the raw original, the HLS ladder and `/t/`. The bytes are reachable
|
|
725
|
+
* only through a signed URL under `/a/{tenant}/…?exp&sig`, which your
|
|
726
|
+
* BACKEND mints with {@link getPrivateAssetUrl}.
|
|
727
|
+
*
|
|
728
|
+
* Optional so an older server that does not send it is read as `"public"` —
|
|
729
|
+
* which is what such a server means.
|
|
730
|
+
*
|
|
731
|
+
* ⚠️ A 404 on a private asset is not a missing file. It is the feature
|
|
732
|
+
* working. See {@link getPrivateAssetUrl}.
|
|
733
|
+
*/
|
|
734
|
+
visibility?: "public" | "private";
|
|
569
735
|
/** Soft-delete timestamp (ISO). Hidden from catalog when set. */
|
|
570
736
|
deletedAt?: string | null;
|
|
571
737
|
/**
|
|
@@ -635,7 +801,59 @@ type OriginalHints = {
|
|
|
635
801
|
* const url = hasPreset(asset, "thumb") ? getAssetUrl(asset, "thumb") : null;
|
|
636
802
|
* ```
|
|
637
803
|
*/
|
|
638
|
-
declare function getAssetUrl(asset: Pick<AssetDTO, "sha"> & OriginalHints, preset: VariantPreset): string;
|
|
804
|
+
declare function getAssetUrl(asset: Pick<AssetDTO, "sha"> & OriginalHints & VisibilityHint, preset: VariantPreset): string;
|
|
805
|
+
/**
|
|
806
|
+
* The signed URL for one preset of a PRIVATE asset — what every refusal above
|
|
807
|
+
* points at.
|
|
808
|
+
*
|
|
809
|
+
* ```ts
|
|
810
|
+
* // On your BACKEND, once you have decided this viewer may see it:
|
|
811
|
+
* const url = await getPrivateAssetUrl(asset, "lg", tenantSigningKey, {
|
|
812
|
+
* expiresInSeconds: 300,
|
|
813
|
+
* });
|
|
814
|
+
* ```
|
|
815
|
+
*
|
|
816
|
+
* It works on a public asset too — `/a/` is a different door onto the same
|
|
817
|
+
* object — but there is no reason to pay for it: a public URL is cacheable at
|
|
818
|
+
* the edge and costs nothing, a signed one is neither.
|
|
819
|
+
*
|
|
820
|
+
* ⚠️ **Backend only.** Handing the signing key to a browser lets any visitor
|
|
821
|
+
* mint URLs for every private asset the tenant owns, which is the whole
|
|
822
|
+
* property the private tree exists to provide.
|
|
823
|
+
*
|
|
824
|
+
* ⚠️ Needs {@link setTenantId} (or a `NitidaClient` with `tenantId`), like
|
|
825
|
+
* every variant URL builder: the tenant segment is base36 and part of what the
|
|
826
|
+
* signature covers, so a missing tenant does not produce a wrong URL — it
|
|
827
|
+
* produces an unsignable one.
|
|
828
|
+
*/
|
|
829
|
+
declare function getPrivateAssetUrl(asset: Pick<AssetDTO, "sha"> & OriginalHints, preset: VariantPreset, signingKey: string, opts: SignAccessOptions): Promise<string>;
|
|
830
|
+
/**
|
|
831
|
+
* The signed URL for a TRANSFORM of a private asset — an arbitrary width, crop
|
|
832
|
+
* or format, not just the sizes that happen to be materialised.
|
|
833
|
+
*
|
|
834
|
+
* ```ts
|
|
835
|
+
* const url = await getPrivateTransformUrl(
|
|
836
|
+
* asset,
|
|
837
|
+
* { width: 1280, format: "webp" },
|
|
838
|
+
* tenantSigningKey,
|
|
839
|
+
* { expiresInSeconds: 300 },
|
|
840
|
+
* );
|
|
841
|
+
* // → https://8ok.uk/a/5/t/format=webp,width=1280/<sha>.webp?exp=…&sig=…
|
|
842
|
+
* ```
|
|
843
|
+
*
|
|
844
|
+
* Why this exists at all: a private asset that can only be served at the sizes
|
|
845
|
+
* someone already generated is barely a product. The signed tree mirrors the
|
|
846
|
+
* public one, transforms included.
|
|
847
|
+
*
|
|
848
|
+
* Returns `null` when `opts` serialize to an empty DSL — same contract as
|
|
849
|
+
* {@link getTransformUrl}, because "no transform requested" is not an error,
|
|
850
|
+
* it just means you wanted {@link getPrivateAssetUrl}.
|
|
851
|
+
*
|
|
852
|
+
* ⚠️ **Backend only**, like every signer here. And note the width is a plain
|
|
853
|
+
* `number`: a signed URL is a trusted caller, so the edge ladder does not
|
|
854
|
+
* apply — the same rule `getSignedTransformUrl` already follows.
|
|
855
|
+
*/
|
|
856
|
+
declare function getPrivateTransformUrl(asset: Pick<AssetDTO, "sha">, opts: SignedTransformOptions, signingKey: string, signOpts: SignAccessOptions): Promise<string | null>;
|
|
639
857
|
/**
|
|
640
858
|
* Did the processor actually generate this preset?
|
|
641
859
|
*
|
|
@@ -654,7 +872,7 @@ declare function getAssetUrl(asset: Pick<AssetDTO, "sha"> & OriginalHints, prese
|
|
|
654
872
|
* ```
|
|
655
873
|
*/
|
|
656
874
|
declare function hasPreset(asset: Pick<AssetDTO, "presets">, preset: VariantPreset): boolean;
|
|
657
|
-
declare function getAssetSrcSet(asset: Pick<AssetDTO, "sha" | "presets">): string;
|
|
875
|
+
declare function getAssetSrcSet(asset: Pick<AssetDTO, "sha" | "presets"> & VisibilityHint): string;
|
|
658
876
|
/**
|
|
659
877
|
* Compute the dimensions a variant would have given the source asset's
|
|
660
878
|
* width/height and the variant's bounding box. For thumbnails (square
|
|
@@ -671,4 +889,4 @@ declare function getAssetDimensions(asset: Pick<AssetDTO, "w" | "h">): {
|
|
|
671
889
|
height: number;
|
|
672
890
|
} | null;
|
|
673
891
|
|
|
674
|
-
export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type ResolveSlotOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type 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 };
|
|
892
|
+
export { type AssetDTO, type AssetPalette, type AssetVariant, type HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, type PaletteSwatch, type RequestablePreset, type ResolveSlotOptions, type SignAccessOptions, type SignedTransformOptions, type SlotDTO, type SlotResolution, TRANSFORM_WIDTHS, type TransformEffect, type TransformFit, type TransformFormat, type TransformGravity, type TransformOptions, type TransformWidth, type VariantEntryPreset, type VariantPreset, type 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 };
|