Found while building MOTIR-2690, which removed the acceptance lane's continue-on-error and so had to read the vendored uploader's exit paths end to end. Out of that card's scope (it changes the lane's failure semantics; this changes how the upload is performed), filed rather than absorbed.
scripts/upload-acceptance-video.mjs in the starter is a VENDORED COPY of motir-core's, and its own header says so: "It is not a fork: keep it in sync." It has drifted by three upstream cards.
The starter still uploads through the Vercel Blob client SDK:
import { put as putBlob } from '@vercel/blob/client';
// …
await putBlob(targets.video.pathname, fs.readFileSync(artifacts.video), {
access: 'private',
token: targets.video.token, // ← expects a `vercel_blob_client_…` token
contentType: targets.video.contentType,
});
targets.*.token has not been a Vercel client upload token since MOTIR-2389 (done) moved the blob store to S3/Tigris. Verified on motir-core origin/main, lib/blob/uploader.ts:
export async function mintPrivateUploadToken(pathname, opts) {
return getSignedUrl(s3Client(), new PutObjectCommand({ … }), {
expiresIn: opts.ttlSeconds ?? 300,
signableHeaders: new Set(['content-type']),
});
}
So /upload-token now returns a presigned PUT URL, and the consumer is expected to fetch(url, { method: 'PUT', headers: { 'content-type': target.contentType }, body }). Handing that URL to @vercel/blob's put as a token cannot work.
This is LATENT in this repo, not live. The starter owns no tests/e2e/acceptance*.spec.ts, and the lane's paths: filter means it never triggers — so nothing is red today. It goes live for the first PR (ours or an adopter's) that writes an acceptance spec, and the failure mode upstream was Failed to parse URL from <the whole credential>, which names neither side.
Re-syncing should bring all of it, not just the PUT:
putSignedArtifact, and dropping the @vercel/blob dependency from package.json (^2.6.1, referenced nowhere else once this goes).assertPresignedTarget (turns exactly this failure into a sentence naming which deployment is stale) and describeToken (the previous diagnosis printed ~700 characters of a live upload grant into a public job log, twice per recording).assessArtifactSizes / resolveMaxArtifactBytes / assertWithinMintedCap, and the policy that an over-cap trace is dropped while an over-cap video rejects the recording. Absent here, an over-cap artifact fails inside the upload instead, after the mint.scripts/upload-acceptance-video.mjs PUTs each artifact to the presigned URL the mint returns, sending content-type exactly as target.contentType — the header is inside the signature, so a guess from the filename is a signature failure.@vercel/blob is gone from the starter's package.json and from every import, or a comment records what still needs it.Failed to parse URL from ….tests/acceptance-video-uploader.test.ts is re-synced alongside it — it currently mocks @vercel/blob/client and asserts putBlobMock call counts, which the port invalidates. The two files are vendored together precisely so a bad sync fails locally.nextjs-prisma-vercel-starter/scripts/upload-acceptance-video.mjs — the vendored copy, and its own keep-in-sync header.nextjs-prisma-vercel-starter/tests/acceptance-video-uploader.test.ts — vendored with it; the vi.mock('@vercel/blob/client') at the top is the thing to replace.motir-core/scripts/upload-acceptance-video.mjs — the source of truth to re-copy from.motir-core/lib/blob/uploader.ts — mintPrivateUploadToken, the presigned-PUT contract and why signableHeaders is not optional.nextjs-prisma-vercel-starter/docs/acceptance-video.md § Why the uploader is VENDORED here — the MOTIR-1941 decision this card is the maintenance cost of.