@ultimat3/storage 7.0.0 → 9.0.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/CLAUDE.md CHANGED
@@ -32,6 +32,7 @@ Tier 1. Object storage: named disks, safe keys, signed URLs, sniffed uploads.
32
32
  | `signed-url.ts` | HMAC over the constraint tuple, constant-time verify |
33
33
  | `upload.ts` | magic-byte sniff + size/allowlist/checksum policy |
34
34
  | `image.ts` | deterministic variant keys; byte path over core's pipeline (png/jpeg encode only) |
35
+ | | `VARIANT_FORMATS` — what a variant KEY can carry — and NOT `IMAGE_FORMATS`, which is core's and means what core can PROBE. See below |
35
36
  | | `variantKey` is the cache identity `@ultimat3/cli`'s `/media/*` route looks a variant up by — derived, never stored, so a request that misses transforms once and every later one is a disk read |
36
37
  | `storage.ts` | `defineStorage` + module-level `storage()` / `disk()` |
37
38
  | `grant.ts` | `grantUpload` — the ONE way a presigned PUT is minted; the client never names a key |
@@ -169,7 +170,21 @@ Gotchas:
169
170
  in `beforeEach`.
170
171
  - `image.ts` owns no pixels: core's `transformImageBytes`/`blurDataUrl` are the only scaler.
171
172
  Its image failures (`X_IMAGE_UNSUPPORTED`, `X_IMAGE_DECODE_FAILED`) surface unwrapped —
172
- wrapping them in a `StorageError` would give one failure two codes.
173
+ wrapping them in a `StorageError` would give one failure two codes. `variantKey` now RAISES
174
+ core's `imageUnsupported()` as well as passing them through, for that same reason.
175
+ - **`VARIANT_FORMATS` is this package's format vocabulary, and `IMAGE_FORMATS` is core's.** Until
176
+ 9.0.0 both packages exported `IMAGE_FORMATS` **and** `ImageFormat` from their own barrels over
177
+ different sets (core: `png|jpeg|webp|avif|gif|svg`, what it can PROBE; storage: `avif|webp|jpeg|png`),
178
+ so a caller narrowing on storage's held a type saying `gif` and `svg` could not occur and a
179
+ `probeImage()` value that was one — and `variantKey('photos/hero.gif', { format })` minted
180
+ `photos/hero@full.undefined`, a well-formed writable key naming a file nothing can serve. The set
181
+ is now a strict subset **by the compiler**: `as const satisfies readonly ImageFormat[]`, so a
182
+ member core cannot name is a build error here. `isVariantFormat` takes `string` on purpose, so
183
+ `probeImage(bytes).format` narrows through it with no cast. `avif` is in the set and `gif`/`svg`
184
+ are not because the question is what a variant KEY can carry, never what core can transform —
185
+ core decodes `gif` perfectly well, and naming this set `TRANSFORMABLE_FORMATS` would have been
186
+ the same lie one rename later. `scripts/render-modes.ts` holds the `IMAGE_FORMATS` row (`by:
187
+ 'name'`) and `image.test.ts` fails the day either core name reappears in `src/index.ts`.
173
188
  - `transformImage()` must encode at exactly `fitDimensions()`'s size, and passes `format`
174
189
  explicitly (`?? 'webp'`, which then rejects): bytes that disagree with the `variantKey`
175
190
  extension, or with the `width`/`height` `@ultimat3/seo` inlined, are the layout shift the
package/README.md CHANGED
@@ -235,7 +235,7 @@ Inside `pending/` deliberately: an upload nobody ever scanned is still an orphan
235
235
  | `X_STORAGE_QUARANTINED` | `promoteAttachment` on a key nothing has released from `pending/quarantine/` |
236
236
  | `X_NOT_IMPLEMENTED` | S3 user metadata / cache-control; `serverSideEncryption` on either driver |
237
237
  | `X_ENV_MISSING` | core's: S3 credential env vars, or a `localDriver` built outside development where neither `signingSecret` nor `STORAGE_SIGNING_SECRET` holds a secret other than the published `DEV_SIGNING_SECRET` |
238
- | `X_IMAGE_UNSUPPORTED` | core's: an `avif` encode, or a source no built-in decoder reads |
238
+ | `X_IMAGE_UNSUPPORTED` | core's: an `avif` encode, a source no built-in decoder reads, or a `variantKey` format no variant can carry |
239
239
  | `X_IMAGE_DECODE_FAILED` | core's: truncated or corrupt image bytes |
240
240
 
241
241
  ## Images
@@ -243,6 +243,15 @@ Inside `pending/` deliberately: an upload nobody ever scanned is still an orphan
243
243
  `variantKey()`, `srcsetDescriptors()`, `fitDimensions()` are pure — `@ultimat3/seo` builds
244
244
  `srcset` from them without decoding a byte.
245
245
 
246
+ **`VARIANT_FORMATS` / `VariantFormat` are what a variant KEY can carry, `As of 2026-08`** —
247
+ `avif`, `webp`, `jpeg`, `png` — and this package exports no `IMAGE_FORMATS` and no `ImageFormat`. Those two names are
248
+ `@ultimat3/core`'s, over the six formats it can PROBE (`png`, `jpeg`, `webp`, `avif`, `gif`,
249
+ `svg`). Until 9.0.0 both packages exported both names over different sets, so a caller narrowing on
250
+ storage's held a type saying `gif` and `svg` could not occur and a `probeImage()` value that was
251
+ one. `isVariantFormat(format)` takes a `string`, so a probed format narrows through it with no
252
+ cast, and `variantKey()` refuses anything else with core's `X_IMAGE_UNSUPPORTED` instead of minting
253
+ `photos/hero@full.undefined`.
254
+
246
255
  `transformImage()` and `blurPlaceholder()` are real, over `@ultimat3/core`'s pipeline, which is
247
256
  `Bun.Image` — no `sharp`, no dependency. **It encodes `png`, `jpeg` and `webp`** — so the default
248
257
  format and the default `.webp` key extension finally agree, and a `srcset` entry can be served
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/storage",
3
- "version": "7.0.0",
3
+ "version": "9.0.0",
4
4
  "description": "Named disks over Bun.file and Bun.s3: safe keys, signed URLs, sniffed uploads",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,6 +31,6 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/core": "7.0.0"
34
+ "@ultimat3/core": "9.0.0"
35
35
  }
36
36
  }
package/src/image.ts CHANGED
@@ -3,11 +3,41 @@
3
3
  // `<img srcset>` from `srcsetDescriptors()` without decoding a byte, and when it does need the
4
4
  // bytes, `transformImage()` returns exactly the size `fitDimensions()` already promised.
5
5
 
6
- import { blurDataUrl, probeImage, transformImageBytes } from '@ultimat3/core';
6
+ import {
7
+ blurDataUrl,
8
+ type ImageFormat,
9
+ imageUnsupported,
10
+ probeImage,
11
+ transformImageBytes,
12
+ } from '@ultimat3/core';
7
13
  import { assertSafeKey, keyExtname } from './path';
8
14
 
9
- export const IMAGE_FORMATS = ['avif', 'webp', 'jpeg', 'png'] as const;
10
- export type ImageFormat = (typeof IMAGE_FORMATS)[number];
15
+ /**
16
+ * The formats a stored VARIANT can be minted in — a strict subset of the ones `@ultimat3/core`
17
+ * can PROBE, and a different question from them. `satisfies readonly ImageFormat[]` is the whole
18
+ * derivation: a member core cannot name is a compile error here, so this can never become a
19
+ * second vocabulary the way it was one until 9.0.0 (both packages exported `IMAGE_FORMATS` and
20
+ * `ImageFormat`, over different sets, from their own barrels — so a caller narrowing on storage's
21
+ * held a type saying `gif` and `svg` could not occur and a `probeImage()` value that was one).
22
+ *
23
+ * NOT "the formats storage can transform": core decodes `gif` perfectly well. This set is what a
24
+ * variant KEY can carry, which is why `avif` is in it (key and `srcset` math only — asking for
25
+ * its bytes rejects with core's `X_IMAGE_UNSUPPORTED`) and `gif` and `svg` are not.
26
+ */
27
+ export const VARIANT_FORMATS = [
28
+ 'avif',
29
+ 'webp',
30
+ 'jpeg',
31
+ 'png',
32
+ ] as const satisfies readonly ImageFormat[];
33
+ export type VariantFormat = (typeof VARIANT_FORMATS)[number];
34
+
35
+ /**
36
+ * Takes `string`, so `probeImage(bytes).format` narrows through it without a cast. That is the
37
+ * whole point: the answer for `gif` is `false`, and it used to be unaskable.
38
+ */
39
+ export const isVariantFormat = (format: string): format is VariantFormat =>
40
+ (VARIANT_FORMATS as readonly string[]).includes(format);
11
41
 
12
42
  /** `cover` fills the box and crops the overflow; `contain` fits inside it, no crop. */
13
43
  export type ImageFit = 'cover' | 'contain';
@@ -15,7 +45,7 @@ export type ImageFit = 'cover' | 'contain';
15
45
  export interface ImageTransform {
16
46
  readonly width?: number | undefined;
17
47
  readonly height?: number | undefined;
18
- readonly format?: ImageFormat | undefined;
48
+ readonly format?: VariantFormat | undefined;
19
49
  /** 1-100. Omitted means the format default (`DEFAULT_QUALITY`). */
20
50
  readonly quality?: number | undefined;
21
51
  readonly fit?: ImageFit | undefined;
@@ -29,7 +59,7 @@ export interface ImageSize {
29
59
  export const DEFAULT_QUALITY = 80;
30
60
  export const DEFAULT_SRCSET_WIDTHS = [320, 640, 960, 1280, 1920] as const;
31
61
 
32
- const FORMAT_EXTENSIONS: Readonly<Record<ImageFormat, string>> = {
62
+ const FORMAT_EXTENSIONS: Readonly<Record<VariantFormat, string>> = {
33
63
  avif: 'avif',
34
64
  webp: 'webp',
35
65
  jpeg: 'jpg',
@@ -43,7 +73,19 @@ const FORMAT_EXTENSIONS: Readonly<Record<ImageFormat, string>> = {
43
73
  export function variantKey(sourceKey: string, transform: ImageTransform): string {
44
74
  const safe = assertSafeKey(sourceKey);
45
75
  const stem = safe.slice(0, safe.length - keyExtname(safe).length);
46
- const format = transform.format ?? 'webp';
76
+ // `string`, not `VariantFormat`: a format off a query string or through an `as` reaches here as
77
+ // any string core can probe, and indexing FORMAT_EXTENSIONS with `gif` minted
78
+ // `photos/hero@full.undefined` — a well-formed, writable key naming a file nothing can serve.
79
+ // Core's code, not a storage one: one bad format is one failure, and seo and the dev asset
80
+ // route already report it as X_IMAGE_UNSUPPORTED.
81
+ const format: string = transform.format ?? 'webp';
82
+ if (!isVariantFormat(format)) {
83
+ throw imageUnsupported(
84
+ `"${format}" is not a format a stored variant can be minted in`,
85
+ `variantKey('${safe}', { format: 'webp' }) — one of ${VARIANT_FORMATS.join(', ')}; a ${format} source is served as-is, it has no variant`,
86
+ { format, key: safe },
87
+ );
88
+ }
47
89
  const parts: string[] = [];
48
90
  if (transform.width !== undefined) parts.push(`w${transform.width}`);
49
91
  if (transform.height !== undefined) parts.push(`h${transform.height}`);
@@ -63,7 +105,7 @@ export interface SrcsetDescriptor {
63
105
 
64
106
  export interface SrcsetOptions {
65
107
  readonly widths?: readonly number[] | undefined;
66
- readonly format?: ImageFormat | undefined;
108
+ readonly format?: VariantFormat | undefined;
67
109
  readonly quality?: number | undefined;
68
110
  /** Intrinsic size of the source. Widths above it are dropped — upscaling is never useful. */
69
111
  readonly intrinsic?: ImageSize | undefined;
package/src/index.ts CHANGED
@@ -97,20 +97,24 @@ export type { GrantUploadInput, UploadGrant, UploadRequest } from './grant';
97
97
  export { grantUpload } from './grant';
98
98
  export type {
99
99
  ImageFit,
100
- ImageFormat,
101
100
  ImageSize,
102
101
  ImageTransform,
103
102
  SrcsetDescriptor,
104
103
  SrcsetOptions,
104
+ VariantFormat,
105
105
  } from './image';
106
+ // No `ImageFormat` and no `IMAGE_FORMATS` here, deliberately: `@ultimat3/core` owns that name and
107
+ // that set (what it can PROBE), this package owns `VARIANT_FORMATS` (what a variant can be minted
108
+ // in), and `image.test.ts` fails the day either core name reappears in this file.
106
109
  export {
107
110
  blurPlaceholder,
108
111
  DEFAULT_QUALITY,
109
112
  DEFAULT_SRCSET_WIDTHS,
110
113
  fitDimensions,
111
- IMAGE_FORMATS,
114
+ isVariantFormat,
112
115
  srcsetDescriptors,
113
116
  transformImage,
117
+ VARIANT_FORMATS,
114
118
  variantKey,
115
119
  } from './image';
116
120