@ultimat3/storage 15.0.0 → 17.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
@@ -14,6 +14,33 @@ Tier 1. Object storage: named disks, safe keys, signed URLs, sniffed uploads.
14
14
  fits the table is `@ultimat3/auth`'s: ship the server function (`grantUpload`), let the app
15
15
  wrap it in its own `action()` with its own policy. See `docs/architecture/17-uploads.md`.
16
16
 
17
+ - **Every byte ceiling and every TTL is screened where it is DECLARED, through core's
18
+ `finiteCount` and nothing else, `As of 2026-08-26`** — `uploadPolicy({ maxBytes })`, both
19
+ drivers' `maxPutBytes`, `createUploadGrant({ expiresInMs })`, `buildSignedUrl`'s two and the s3
20
+ driver's presign TTL. There was briefly a private `assertFiniteSignedUrlBound` here saying the
21
+ same thing in its own words; a second finite-bound path is one that drifts from the shared
22
+ contract, which is exactly what `jobs`, `realtime` and `query` each proved with a copy of their
23
+ own. Both presigners now call the one function, which is what `driver-parity.test.ts` needs to
24
+ stay true.
25
+ Measured: `uploadPolicy({ maxBytes: Number.NaN })` accepted a 5,000,016-byte PNG through
26
+ `validateUpload`, because `size > NaN` is false — the one number deciding how much a caller may
27
+ store stopped deciding anything. Variant `quality` is core's `assertFiniteImageQuality` and NOT a
28
+ second screen: `variantKey` never reaches the encoder, so a copy that disagreed would mint
29
+ `q150` keys for bytes `transformImageBytes` then refuses.
30
+ - **The default is taken on `undefined` and on nothing else, `As of 2026-08-26`** —
31
+ `options.x === undefined ? D : options.x`, never `options.x ?? D`. `??` coalesces on `null` too,
32
+ so an explicitly blanked key in a decoded JSON config took the default BEFORE the screen above
33
+ could refuse it: the mirror of the `NaN` half, where one value slips past the guard and the other
34
+ past the default, and both end in a bound nobody chose. Every site above, `quality` included.
35
+
36
+ - **`MAX_KEY_LENGTH` is BYTES, and is measured in bytes, `As of 2026-08-26`.** S3's limit is "a
37
+ sequence of Unicode characters whose UTF-8 encoding is at most 1,024 bytes long", and `path.ts`
38
+ measured `key.length` — UTF-16 code units — while its message said "chars". A code-unit count is
39
+ never MORE than the UTF-8 byte count, so a non-ASCII key over the real limit passed this guard
40
+ and was refused by the store instead: 400 CJK characters is 400 units and 1,200 bytes. Keeping
41
+ local and remote disks interchangeable is the whole reason the ceiling exists, so it has to be
42
+ the store's ceiling. Same defect and same fix as `@ultimat3/cache`'s surrogate-key guard.
43
+
17
44
  | Rule | |
18
45
  |---|---|
19
46
  | Errors | `StorageError` + one factory per code in `errors.ts`; never `throw new Error` |
@@ -162,8 +189,11 @@ Gotchas:
162
189
  a test fixture) got the verdict from one table and the behaviour from another, in the dangerous
163
190
  direction: a production boot with no secret, launched from a development shell that has one,
164
191
  signing every grant with the published literal. All three reads now come off `options.env ??
165
- process.env`, so a bare `localDriver({ root })` is unchanged and additive. `driver-local.test.ts`
166
- pins it by mutation — reverting any one read to `process.env` fails.
192
+ process.env`, so a bare `localDriver({ root })` is unchanged and additive.
193
+ `driver-local-boot.test.ts` pins it by mutation — reverting any one read to `process.env` fails.
194
+ That file is the CONSTRUCTION half, split off `driver-local.test.ts` at the line ceiling along
195
+ the seam the guard already draws: nothing in it writes a byte, so it needs no temporary
196
+ directory, and `driver-local.test.ts` keeps everything the disk actually does with an object.
167
197
  - **The mounted read half is `@ultimat3/cli`'s `dev-storage.ts`, not this package.** `GET
168
198
  /_storage/:disk/*key` gates on `@ultimat3/policy`'s `evaluate()` (`storage:read`), which is tier
169
199
  2 and unreachable from here — so a "serve this object" helper in this package could only ever be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/storage",
3
- "version": "15.0.0",
3
+ "version": "17.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": "15.0.0"
34
+ "@ultimat3/core": "17.0.0"
35
35
  }
36
36
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import {
7
7
  type Clock,
8
+ finiteCount,
8
9
  isLocal,
9
10
  type ResolveEnvironmentOptions,
10
11
  resolveEnvironment,
@@ -159,7 +160,14 @@ const isMissingFile = (error: unknown): boolean => stringField(error, 'code') ==
159
160
 
160
161
  export function localDriver(options: LocalDriverOptions): StorageDriver {
161
162
  const root = options.root.replace(/\/+$/, '');
162
- const maxPutBytes = options.maxPutBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
163
+ // `=== undefined`, never `??`: `??` coalesces on `null` too, so an explicitly blanked key in a
164
+ // decoded JSON config took the default instead of the refusal `finiteCount` is here to raise.
165
+ const maxPutBytes = finiteCount(
166
+ 'the local disk driver',
167
+ 'maxPutBytes',
168
+ options.maxPutBytes === undefined ? DEFAULT_MAX_UPLOAD_BYTES : options.maxPutBytes,
169
+ 1,
170
+ );
163
171
  const clock = options.clock ?? systemClock;
164
172
  // The segment is the disk's REGISTERED name, learned from `defineStorage` at boot — the driver
165
173
  // kind is not a mount point, and minting under it made every disk not literally named `local`
package/src/driver-s3.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // The client is built lazily on first use so importing this module never opens a socket, and
4
4
  // credentials arrive as env var NAMES: a literal key in app.config.ts is a key in git.
5
5
 
6
- import { ConfigInvalidError, EnvMissingError, stringField } from '@ultimat3/core';
6
+ import { ConfigInvalidError, EnvMissingError, finiteCount, stringField } from '@ultimat3/core';
7
7
  import {
8
8
  DEFAULT_CONTENT_TYPE,
9
9
  type ListOptions,
@@ -27,6 +27,7 @@ import {
27
27
  storageNotImplemented,
28
28
  } from './errors';
29
29
  import { assertSafeKey } from './path';
30
+ import { DEFAULT_SIGNED_URL_TTL_MS } from './signed-url';
30
31
  import { DEFAULT_MAX_UPLOAD_BYTES } from './upload';
31
32
 
32
33
  const DRIVER_NAME = 's3';
@@ -209,7 +210,14 @@ function refuseUnsupportedPut(bucket: string, key: string, putOptions?: PutOptio
209
210
  }
210
211
 
211
212
  export function s3Driver(options: S3DriverOptions): StorageDriver {
212
- const maxPutBytes = options.maxPutBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
213
+ // `=== undefined`, never `??`: `??` coalesces on `null` too, so an explicitly blanked key in a
214
+ // decoded JSON config took the default instead of the refusal `finiteCount` is here to raise.
215
+ const maxPutBytes = finiteCount(
216
+ 'the s3 driver',
217
+ 'maxPutBytes',
218
+ options.maxPutBytes === undefined ? DEFAULT_MAX_UPLOAD_BYTES : options.maxPutBytes,
219
+ 1,
220
+ );
213
221
  let client: S3ClientLike | undefined;
214
222
  const conn = (): S3ClientLike => {
215
223
  client ??= buildClient(options);
@@ -356,7 +364,17 @@ export function s3Driver(options: S3DriverOptions): StorageDriver {
356
364
  * `maxBytes` — S3 has no header for it, so size stays a server-side policy check.
357
365
  */
358
366
  async signedUrl(key: string, urlOptions?: SignedUrlOptions): Promise<string> {
359
- const expiresInMs = urlOptions?.expiresInMs ?? 900_000;
367
+ // Screened with the SAME function `buildSignedUrl` applies on the local disk — one
368
+ // finite-bound path for both presigners, never a private copy per package: `Math.ceil(NaN /
369
+ // 1000)` is `NaN`, so `X-Amz-Expires=NaN` went to AWS and the app got a 403 on a link it
370
+ // believed it had just minted. `=== undefined` rather than `??`, so an explicit `null` is
371
+ // refused instead of silently taking the default.
372
+ const expiresInMs = finiteCount(
373
+ 'the s3 driver',
374
+ 'expiresInMs',
375
+ urlOptions?.expiresInMs === undefined ? DEFAULT_SIGNED_URL_TTL_MS : urlOptions.expiresInMs,
376
+ 1,
377
+ );
360
378
  return conn()
361
379
  .file(assertSafeKey(key))
362
380
  .presign({
package/src/grant.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // only client input that survives is the filename's extension, and only if it survives a regex.
6
6
 
7
7
  import type { Clock } from '@ultimat3/core';
8
- import { systemClock } from '@ultimat3/core';
8
+ import { finiteCount, systemClock } from '@ultimat3/core';
9
9
  import type { AttachmentTarget } from './attachment';
10
10
  import { attachmentKey, pendingKey, quarantineKey, uploadName } from './attachment';
11
11
  import type { StorageDriver } from './driver';
@@ -82,7 +82,17 @@ export async function grantUpload(input: GrantUploadInput): Promise<UploadGrant>
82
82
  throw tooLarge(key, size, policy.maxBytes);
83
83
  }
84
84
 
85
- const expiresInMs = input.expiresInMs ?? DEFAULT_SIGNED_URL_TTL_MS;
85
+ // Refused here as well as in the presigner one call down: `expiresAt: now + NaN` is `NaN` in the
86
+ // grant this function RETURNS, and the message a caller can act on names `createUploadGrant`'s
87
+ // own option rather than `buildSignedUrl`'s.
88
+ // `=== undefined`, never `??`, for the reason `buildSignedUrl` states: `??` coalesces on `null`
89
+ // too, so an explicit one took the default instead of this refusal.
90
+ const expiresInMs = finiteCount(
91
+ 'createUploadGrant',
92
+ 'expiresInMs',
93
+ input.expiresInMs === undefined ? DEFAULT_SIGNED_URL_TTL_MS : input.expiresInMs,
94
+ 1,
95
+ );
86
96
  const url = await input.disk.signedUrl(key, {
87
97
  method: 'PUT',
88
98
  maxBytes: policy.maxBytes,
package/src/image.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  // bytes, `transformImage()` returns exactly the size `fitDimensions()` already promised.
5
5
 
6
6
  import {
7
+ assertFiniteImageQuality,
7
8
  blurDataUrl,
8
9
  type ImageFormat,
9
10
  imageUnsupported,
@@ -90,7 +91,14 @@ export function variantKey(sourceKey: string, transform: ImageTransform): string
90
91
  if (transform.width !== undefined) parts.push(`w${transform.width}`);
91
92
  if (transform.height !== undefined) parts.push(`h${transform.height}`);
92
93
  if (transform.fit !== undefined) parts.push(transform.fit);
93
- const quality = transform.quality ?? DEFAULT_QUALITY;
94
+ // Core's screen, not a second one: this function never reaches the encoder, so without it a
95
+ // `q150` or a `qNaN` variant key is minted for bytes `transformImageBytes` then refuses — and
96
+ // an unbounded quality is an unbounded number of distinct keys in the bucket.
97
+ // `=== undefined`, never `??`: `??` coalesces on `null` too, so an explicit one was spelled
98
+ // into a key as the default rather than refused.
99
+ const quality = assertFiniteImageQuality(
100
+ transform.quality === undefined ? DEFAULT_QUALITY : transform.quality,
101
+ );
94
102
  if (quality !== DEFAULT_QUALITY) parts.push(`q${quality}`);
95
103
  if (parts.length === 0) parts.push('full');
96
104
  return assertSafeKey(`${stem}@${parts.join('-')}.${FORMAT_EXTENSIONS[format]}`);
@@ -173,7 +181,9 @@ export async function transformImage(
173
181
  // what stops a rounded edge leaving a transparent (in JPEG: black) sliver inside it.
174
182
  fit: 'cover',
175
183
  format: transform.format ?? 'webp',
176
- quality: transform.quality ?? DEFAULT_QUALITY,
184
+ quality: assertFiniteImageQuality(
185
+ transform.quality === undefined ? DEFAULT_QUALITY : transform.quality,
186
+ ),
177
187
  });
178
188
  }
179
189
 
package/src/path.ts CHANGED
@@ -5,8 +5,19 @@
5
5
 
6
6
  import { pathUnsafe } from './errors';
7
7
 
8
- /** S3's own limit; keeping local and remote disks interchangeable requires the same ceiling. */
8
+ /**
9
+ * S3's own limit; keeping local and remote disks interchangeable requires the same ceiling.
10
+ *
11
+ * BYTES, and S3 says so — "a sequence of Unicode characters whose UTF-8 encoding is at most 1,024
12
+ * bytes long". It was measured with `String.length`, which is UTF-16 code units, and a code-unit
13
+ * count is never MORE than the UTF-8 byte count — so every non-ASCII key over the real limit
14
+ * passed this guard and was refused by the store instead, which is the one thing a shared ceiling
15
+ * exists to prevent. `MAX_KEY_LENGTH` keeps its name: it is exported, and the number is unchanged.
16
+ */
9
17
  export const MAX_KEY_LENGTH = 1024;
18
+
19
+ /** What the wire counts. The key is at most ~1 KB, so this is cheaper than deciding not to. */
20
+ const utf8Bytes = (value: string): number => new TextEncoder().encode(value).byteLength;
10
21
  export const ORG_PREFIX = 'org';
11
22
 
12
23
  /**
@@ -32,8 +43,9 @@ function hasControlByte(key: string): boolean {
32
43
 
33
44
  function unsafeReason(key: string): string | undefined {
34
45
  if (key.length === 0) return 'is empty';
35
- if (key.length > MAX_KEY_LENGTH) {
36
- return `is ${key.length} chars, over the ${MAX_KEY_LENGTH} limit`;
46
+ const bytes = utf8Bytes(key);
47
+ if (bytes > MAX_KEY_LENGTH) {
48
+ return `is ${bytes} bytes, over the ${MAX_KEY_LENGTH}-byte limit`;
37
49
  }
38
50
  if (hasControlByte(key)) return 'contains a NUL or control byte';
39
51
  if (key.includes('\\')) return 'contains a backslash';
package/src/signed-url.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // a 2GB executable. Verification never throws and never short-circuits on expiry before the
5
5
  // signature, so a forged URL can never learn "the signature was fine, just late".
6
6
 
7
- import { type Clock, systemClock, timingSafeEqual } from '@ultimat3/core';
7
+ import { type Clock, finiteCount, systemClock, timingSafeEqual } from '@ultimat3/core';
8
8
  import type { SignedUrlMethod } from './driver';
9
9
  import { assertSafeKey, isSafeKey } from './path';
10
10
 
@@ -102,13 +102,36 @@ export async function signConstraints(
102
102
  const trimBase = (base: string): string => base.replace(/\/+$/, '');
103
103
  const encodeKey = (key: string): string => key.split('/').map(encodeURIComponent).join('/');
104
104
 
105
+ /**
106
+ * The mint's side of the screen `parseConstraints` already applies to the read. `finiteCount` from
107
+ * `@ultimat3/core` is that screen — a private copy of it lived here, and a second finite-bound path
108
+ * is one that drifts from the shared contract, which is what `jobs`, `realtime` and `query` each
109
+ * proved with a byte-identical copy of their own. The S3 driver calls the same function directly,
110
+ * because it presigns through the provider rather than through this one, and two presigners that
111
+ * refuse different inputs is the disagreement `driver-parity.test.ts` exists to catch.
112
+ *
113
+ * A URL is minted here and verified elsewhere, so a number this function writes and that one
114
+ * refuses is a link that is dead on arrival — the app's own call succeeded, and the only report is
115
+ * a 400 at a recipient who cannot fix it. `expiresInMs: NaN` is exactly that: `now + NaN` is
116
+ * `NaN`, `String` writes the literal `NaN` into `x-exp`, and `Number.isSafeInteger` on the other
117
+ * side says no.
118
+ */
105
119
  export async function buildSignedUrl(input: SignedUrlInput): Promise<string> {
106
120
  const key = assertSafeKey(input.key);
107
121
  const clock = input.clock ?? systemClock;
122
+ // `=== undefined`, never `??`: `??` coalesces on `null` too, so an explicit `null` would take
123
+ // the default instead of the refusal `finiteCount` is here to raise.
124
+ const expiresInMs = finiteCount(
125
+ 'buildSignedUrl',
126
+ 'expiresInMs',
127
+ input.expiresInMs === undefined ? DEFAULT_SIGNED_URL_TTL_MS : input.expiresInMs,
128
+ 1,
129
+ );
130
+ if (input.maxBytes !== undefined) finiteCount('buildSignedUrl', 'maxBytes', input.maxBytes, 0);
108
131
  const constraints: SignedUrlConstraints = {
109
132
  key,
110
133
  method: input.method ?? 'GET',
111
- expiresAt: clock.now().getTime() + (input.expiresInMs ?? DEFAULT_SIGNED_URL_TTL_MS),
134
+ expiresAt: clock.now().getTime() + expiresInMs,
112
135
  maxBytes: input.maxBytes,
113
136
  // An empty string is not a content type, and `canonicalRequest` renders it and `undefined`
114
137
  // identically — so minting one would produce a URL indistinguishable from an unconstrained
package/src/upload.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  // is a stored-XSS delivery vehicle the moment any surface serves it back with the declared
5
5
  // type, so the magic bytes decide and a contradiction is rejected outright.
6
6
 
7
+ import { finiteCount } from '@ultimat3/core';
7
8
  import { sha256Base64 } from './driver';
8
9
  import { checksumMismatch, contentTypeMismatch, contentTypeNotAllowed, tooLarge } from './errors';
9
10
  import { assertSafeKey } from './path';
@@ -43,7 +44,18 @@ export interface UploadPolicyInit {
43
44
 
44
45
  export function uploadPolicy(init: UploadPolicyInit = {}): UploadPolicy {
45
46
  return {
46
- maxBytes: init.maxBytes ?? DEFAULT_MAX_UPLOAD_BYTES,
47
+ // Screened where it is DECLARED, because every reader of it is a comparison: `size >
48
+ // policy.maxBytes` is false for a `NaN` ceiling, so the cap that decides how much a caller may
49
+ // store stops deciding anything. Measured: a 5,000,016-byte PNG passed `validateUpload` under
50
+ // `uploadPolicy({ maxBytes: Number.NaN })`.
51
+ // `=== undefined`, never `??`: `??` coalesces on `null` too, so an explicitly blanked key in
52
+ // a decoded JSON config took the default instead of the refusal beside it.
53
+ maxBytes: finiteCount(
54
+ 'uploadPolicy',
55
+ 'maxBytes',
56
+ init.maxBytes === undefined ? DEFAULT_MAX_UPLOAD_BYTES : init.maxBytes,
57
+ 1,
58
+ ),
47
59
  allowedContentTypes: init.allowedContentTypes ?? IMAGE_CONTENT_TYPES,
48
60
  requireChecksum: init.requireChecksum ?? false,
49
61
  };