Skip to content

fix(web): Infra repeat abort, REST credential errors and SDK preflight cache - #4179

Merged
richiemcilroy merged 10 commits into
mainfrom
fix/infra-rest-polish-2
Nov 13, 2026
Merged

richiemcilroy merged 10 commits into
mainfrom
fix/infra-rest-polish-2

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

Eight follow-ups on the Cap Infra developer API: five from the app-builder persona and three from the speech persona. This is a sibling of #4176 and touches none of its changed lines.

Repeated abort answers 200

POST /api/developer/sdk/v1/upload/multipart/abort already answered 200 { "success": true } when the same upload was aborted twice, because storage accepts the repeat. If storage instead answers NoSuchUpload, abort now also returns 200, so a repeat behaves the same on every storage backend. The docs and OpenAPI said this answered 404. They now say abort is safe to repeat.

Presign keeps signing without a storage check

The open uploadId is not stored on the video row. Initiate finds it by listing open uploads in storage. The only JSON column on the row that is free during an upload is processing, and that holds the encode state. Status resolution, stored-bytes accounting and the claim and release paths all read it, and they reset it to null. A storage ListParts call on every presign is ruled out, because presign is on the hot path.

Presign is left as it was. The docs and OpenAPI now say that signing does not contact storage. A wrong or aborted uploadId fails at the part PUT, and complete answers 404 upload_not_found. Storing the id would also miss uploads already open when this deploys.

Distinct REST credential errors

withDeveloperSecretAuth now separates the cases the SDK routes already separate:

Request Before After
No Authorization header 401 invalid_secret_key, "Invalid secret key" 401 invalid_secret_key, "Missing secret key", with a hint that no header was sent
A cpk_ public key 401 invalid_secret_key, "Invalid secret key" 403 public_key_not_allowed, "Public keys cannot call the REST API"
Any other non-csk_ value 401 invalid_secret_key unchanged
A wrong or revoked csk_ key 401 invalid_secret_key, "Invalid or revoked secret key" unchanged

Code change: a public key on the REST API now gets 403 public_key_not_allowed instead of 401 invalid_secret_key. The SDK routes already use that code. Every other code is unchanged. OpenAPI lists the 403 on every REST operation through restErrors, and the shared Forbidden description names the code.

Preflight cache on the SDK upload routes

developerSdkCors sets maxAge: 7200, so OPTIONS answers on the upload routes carry Access-Control-Max-Age: 7200. Browsers stop preflighting every part. Hono only adds the header to preflight answers.

Usage after deletes: not a bug, nothing changed

The report saw /usage 12 s above the listed videos shortly after some deletes. The code has no window in which a deleted video counts:

  • Delete writes deletedAt to the primary database before it answers 200.
  • /usage reads that same database through getAppStorage, filtered on deletedAt IS NULL, status ready and a duration set.
  • There is no cache and no read replica. The usage answer showed x-vercel-cache: BYPASS.

The likely cause is in the test app. Its library lists videos with ?userId=, while /usage covers the whole app. The persona also ran the Node upload example on the 12 s demo clip with and without a userId. A clip with no userId counts in usage but never appears in a user-scoped list. That also fits the reading being correct after the final cleanup.

Ledger order across page edges

GET /transactions ordered rows by createdAt desc, id desc in SQL, then orderLedgerRows put each second's rows in balance-chain order inside the fetched page. Ids are random and createdAt has one-second resolution. So when a same-second group straddled a page edge, the two pages split it by id, and the balance chain broke at the edge. The CSV export had the same break at its 1,000-row batch edges.

No SQL key gives the chain order without a migration. Instead:

  • listAppLedger (the REST API) keeps the same limit/offset query, which now locates the page. When the page is not the whole ledger, a second query reads every row in the seconds the page spans. They are ordered the same way a single page would order them, and the page is sliced out from the position of its first row. That position is the first row's index in the second query's SQL order, so there is still no count. A first page that holds the whole ledger stays one query.
  • readAppLedger (the CSV export) finishes the last second of each full batch before it yields. The next batch starts strictly before that second.
  • The console's cursor pages (listAppLedgerPage) are unchanged. They are a view, and nothing reconciles them page by page.

The docs now say the order holds across page edges and does not depend on limit.

Speech seconds and the charge

Get Usage gains one sentence. speech.seconds is measured audio, while each transcript is charged on its length rounded to a whole second and then down to a whole micro-credit, so seconds times the rate can differ slightly from microCredits. For the exact charge, use microCredits or each transcript's costMicroCredits. The API is unchanged.

A web page at a transcript URL

When the first bytes of a transcript URL are HTML (<!doctype html or <html, any case, after an optional BOM and whitespace), the transcript fails with the message "The URL returned a web page, not a media file". The code stays unsupported_format. looksLikeHtml shares the BOM and whitespace skip with looksLikePlaylist. XML, playlists and other bytes keep the old message.

Tests

  • developer-sdk-upload.test.ts: a first abort, a repeat abort and an abort that storage answers with NoSuchUpload all return 200 { success: true }. This replaces the test that expected 404.

  • developer-api-contract.test.ts:

    • The upload preflight carries access-control-max-age: 7200 on initiate, presign-part, complete and abort.
    • A REST call with no header gets the missing-key message.
    • A cpk_ key gets 403 public_key_not_allowed and reads no billing.
    • Another value and a wrong csk_ key keep their answers.
  • developer-transcripts-api, developer-transactions-api and developer-upload-token-flow now expect 403 public_key_not_allowed for a public key.

  • infra-ledger-paging.test.ts: nine rows across three seconds, five of them in one second, with ids out of chain order. Walking with limit 1, 2, 3, 4, 5 and 8 gives the same ids as one page of 100, and the chain has no break. Exporting in batches of 2, 3 and 4 gives the same order. Both tests fail against main's ledger store.

  • developer-transcribe-download.test.ts: an HTML page, one after a BOM and blank lines in upper case, and <HTML with no doctype get the web page message. XML, a playlist and random bytes keep the old one. Every case keeps unsupported_format, stores nothing and charges nothing.

Against main's utils.ts and infra-video-upload.ts, 12 of the first five items' tests fail. Across the web unit suite (8,540 passed), the only failure is share-stage-identity.test.ts, which fails the same way when run on its own and touches nothing changed here.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new actionable issues were found.

Summary

This PR makes repeated upload aborts succeed, distinguishes missing and public REST credentials, and caches SDK preflight answers for two hours.

  • Transaction pages and exports keep same-second rows in balance order.
  • REST requests get distinct answers for missing keys and public keys.
  • Multipart aborts succeed even when an upload is already gone.
  • Browsers can reuse SDK upload preflight answers for two hours.
  • Transcript URLs that return HTML get a web-page error message.
  • Usage docs explain why speech seconds and charges can differ.

Tests were not run during this review.

Reviews (8) · Last reviewed commit: "Merge branch 'main' into fix/infra-rest-..." · Reviewed by Greptile

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

Comment thread apps/web/app/api/utils.ts Resolved
@sandchest-com

sandchest-com Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Sandchest

Sandchest reviewed 3366963: nothing exploitable found in this change.

Sandchest reviews every change for vulnerabilities an attacker could use · 6 credits · Review details

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 06:23 Inactive
@vercel

vercel Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
cap-status Ignored Ignored Preview Nov 13, 2026 6:57am UTC

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 1 other finding in Capy.

Open in Capy

# Conflicts:
#	apps/web/__tests__/unit/developer-api-contract.test.ts
@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 06:29 Inactive
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@vercel
vercel Bot temporarily deployed to Preview – cap-status November 13, 2026 06:51 Inactive

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@richiemcilroy
richiemcilroy merged commit b9029b9 into main Nov 13, 2026
39 checks passed

This branch was successfully deployed

1 active and 1 inactive (outdated) deployments
Preview – cap-web — 3366963a Deployed Nov 13, 2026 by vercel[bot]
Preview – cap-status — fd9c835b Deployed Nov 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant