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.
@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:
deleteManyObjectsauto-chunks to the 1000-keyDeleteObjectslimit and resolves to one result per chunk (DeleteObjectsCommandOutput[], not a single output). Per-key partial failures (a 200 with anErrorsarray) are not aggregated — iterate the chunk results to inspect them.client.parseKey(url)only understands this client's own endpoint host. It's bound withs3Hostonly, 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 standaloneparseKey/parseB2Url/isB2Urlfrom./urlwith explicits3Host/b2Hostfor 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 defaultus-east-1will sign wrong) andforcePathStyle: true. The main content bucket (R2) needs neither.