Files
Zachary Lowden bf7331e553 chore(deps): vitest 4.0.18 -> 4.1.11 — clears 4 critical dev-only alerts (#4235)
Dev-tooling only. No runtime dependency moves, no source change.

Clears 4 Dependabot alerts, all CRITICAL, all `scope=development`:

  #331 CVE-2026-73653  @vitest/browser  provider commands bypass file-access
  #272 CVE-2026-53633  @vitest/browser  Browser Mode API can proxy CDP
  #269 CVE-2026-47428  @vitest/browser  unsanitized otelCarrier query param
  #264 CVE-2026-47429  vitest           UI server arbitrary file read/exec

Confirmed against the regenerated lockfile, not estimated: every open alert
was re-evaluated by resolving each advisory's vulnerable ranges against the
`packages:` set of the lockfile before and after. 4 cleared, 0 introduced,
0 other alerts moved.

WHY THE WHOLE FAMILY MOVES, not just the two named packages. The vitest
packages peer-depend on each other by EXACT version (`vitest: 4.0.18`, not a
range). Bumping only `vitest` and `@vitest/browser` leaves
`@vitest/browser-playwright@4.0.18` — which is pinned exactly in the manifest
— pulling a second, still-vulnerable `@vitest/browser@4.0.18` into the tree
alongside the new one, so the alerts do NOT clear. Measured: that resolution
keeps `@vitest/browser@4.0.18` and reports `unmet peer vitest@4.0.18`. Moving
`vitest`, `@vitest/browser`, `@vitest/browser-playwright` and
`@vitest/coverage-v8` together is what leaves a single 4.1.11 of each.

The 20 changed manifest lines are version strings only. `pnpm up` was not used
to produce them: it also re-sorts dependency keys alphabetically, which added
unrelated churn to two files. The floors move `^4.0.18` -> `^4.1.11` so a fresh
resolve cannot land back on a vulnerable version.

Test matrix — `origin/main` @ df7733b99c vs this branch, same toolchain:

  suite       baseline                          after
  typecheck   0 errors                          0 errors
  unit        20407 passed / 25 skipped (1301)  20407 passed / 25 skipped (1301)
  packages    1079 passed / 8 skipped (78)      1079 passed / 8 skipped (78)
  apps        691 passed / 35 skipped (68)      691 passed / 35 skipped (68)

Identical counts on every suite. The baseline was recorded first, on an
unmodified checkout, with a green `pnpm install --frozen-lockfile` as the
control.

NOT TOUCHED, deliberately:

  fast-xml-parser — alert #137 (critical) is left open ON PURPOSE. The
  `"@aws-sdk/core>fast-xml-parser": "5.2.5"` override pins the vulnerable
  version because a blanket CVE bump of this package broke S3 error parsing in
  production once already (#3267). Both entries are byte-identical to `main`
  here. Clearing #137 needs the parsing regression fixed first, not a bump.

  nodemailer — declared `^6.8.0` against a top fix of 9.0.1. A three-major
  bump of the email path is not a lockfile refresh; out of scope.

The only unstaged file after this change is `packages/civitai-db-schema/src/enums.ts`,
which the `postinstall` generator rewrites on an unmodified `main` too. It is
pre-existing drift, so it is left out of this commit rather than smuggled in.
2026-08-21 16:33:30 -05:00
..

@civitai/storage

Server-side low-level S3 / R2 / B2 object client, shared by the main app and the SvelteKit spokes so nobody hand-rolls the AWS SDK wiring. Reads config from env (S3_UPLOAD_*) with per-call overrides.

The client is server-only (holds bucket credentials — never import it into browser code). The URL helpers (./url: parseKey, parseB2Url, isB2Url) are pure and browser-safe.

Ships raw like the other @civitai/* packages; consumers transpile it (Next transpilePackages, Vite ssr.noExternal).

Scope

The client owns the S3Client construction and the primitive object ops: deleteObject, deleteManyObjects, putObject, presigned getPutUrl / getGetUrl / getGetUrlByKey, the multipart trio (getMultipartPutUrl / completeMultipartUpload / abortMultipartUpload), and checkFileExists / getFileMetadata.

Two behaviors to know:

  • deleteManyObjects auto-chunks to the 1000-key DeleteObjects limit and resolves to one result per chunk (DeleteObjectsCommandOutput[], not a single output). Per-key partial failures (a 200 with an Errors array) are not aggregated — iterate the chunk results to inspect them.
  • client.parseKey(url) only understands this client's own endpoint host. It's bound with s3Host only, so a URL on a different backend (e.g. a path-style B2 URL when this is the R2 client) will mis-parse. Cross-backend routing stays in the app: use the standalone parseKey / parseB2Url / isB2Url from ./url with explicit s3Host / b2Host for that.

Domain logic stays in the consuming app, on top of this client:

  • ModelFile refcount guards + bucket allowlists (urlsSafeToDelete, deleteModelFileObject)
  • B2 PUT metrics + client middleware (instrumentB2Client, recordB2PresignIssued)
  • media-location registration for the scanner (uploadImageBufferToStore)
  • cross-backend URL routing (deciding R2-vs-B2 per URL) and classifyS3MultipartError

Use

import { createStorageClient } from '@civitai/storage';

// Defaults to the main content bucket (S3_UPLOAD_ENDPOINT / _KEY / _SECRET / _BUCKET / _REGION).
const storage = createStorageClient();

// A different backend/bucket — pass explicit config (e.g. the CSAM bucket, or B2):
const csam = createStorageClient({
  endpoint: process.env.CSAM_UPLOAD_ENDPOINT,
  accessKey: process.env.CSAM_UPLOAD_KEY,
  secretKey: process.env.CSAM_UPLOAD_SECRET,
  bucket: process.env.CSAM_BUCKET,
});

await storage.deleteObject(key);
const { bucket, key } = storage.parseKey(imageUrl);
const { url } = await storage.getGetUrl(imageUrl, { fileName: 'download.zip' });

Config

Field (option) Env default Notes
endpoint S3_UPLOAD_ENDPOINT required (per client)
accessKey S3_UPLOAD_KEY required
secretKey S3_UPLOAD_SECRET required
bucket S3_UPLOAD_BUCKET optional default bucket; per-call bucket? overrides
region S3_UPLOAD_REGION optional; defaults to us-east-1. R2/custom endpoints ignore it, but real S3/B2 sign requests with it — set it explicitly for B2 (e.g. us-west-004)
forcePathStyle set true for B2/path-style endpoints

Config resolves lazily on first use and throws only if a required field is missing then — a bare import is side-effect-free.

B2-backed clients need both region (SigV4 signs with it — the default us-east-1 will sign wrong) and forcePathStyle: true. The main content bucket (R2) needs neither.