Fixes every CRITICAL/HIGH finding from the 2026-06-09 full-codebase audit
(REVIEW_FINDINGS.md); each fix went through independent spec + code-quality
review. Plan and per-task log: docs/superpowers/plans/2026-06-10-audit-high-fix-pass.md
- attendance: schemas accept the combined local datetimes the forms/service
use (new dateTimeString helpers in schemas/common.ts), breaks persist on
create, AttendanceCreate submit rebuilt — every submit 400'd since 519edce
- 2fa: backup codes wired to /totp/backup-verify (+ remember-me parity),
enrollment QR generated locally via qrcode (CSP-blocked external service
also leaked the secret), dashboard shows per-user enrollment, not policy
- invoices/orders: per-line VAT survives re-saves (numberOr 0-respecting
coercion in formatters.ts), billing_text persists on update, issued-order
status transitions update UI gates
- trips: real pagination on all 3 pages, GET /trips/stats server aggregate
(shared buildTripsWhere + legacy distance coalesce), vehicle_id applies on
PUT with both-vehicle odometer recompute, print rebuilt (sync window.open,
escaped template, server totals)
- orders api: attachment_data PDF blob excluded from all non-binary reads
- warehouse: unit field is a Select over UnitEnum, receipt attachments
downloadable via new authenticated GET route
- downloads: shared RFC 5987 contentDisposition helper — Czech filenames no
longer 500 (warehouse, received-invoices, orders endpoints)
- misc: block-env hook actually blocks (exit 2 + stderr), project create
works with empty dates, NaN filter guards on trips endpoints
- deps: remove unused concurrently (clears both critical advisories), pin
@hono/node-server >=1.19.13 via overrides (clears the 3 moderates without
the Prisma 6 downgrade), drop deprecated @types stubs
Gates: tsc -b clean - vitest 30 files / 342 tests (31 new) - eslint 0 errors
- build OK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
29 lines
1.3 KiB
TypeScript
29 lines
1.3 KiB
TypeScript
/**
|
|
* Build a Content-Disposition header value that is safe for non-ASCII
|
|
* filenames (Czech diacritics are the norm here: "příloha č. 1.pdf").
|
|
*
|
|
* Node's HTTP layer THROWS on header values containing characters above
|
|
* U+00FF, so the raw filename must never be interpolated into the header
|
|
* directly — that turns a download of a diacritic-named file into a 500.
|
|
* Per RFC 5987 / RFC 6266 the real UTF-8 name is carried in the `filename*`
|
|
* parameter (percent-encoded), while `filename` holds an ASCII-only fallback
|
|
* for legacy clients. Modern browsers prefer `filename*`.
|
|
*/
|
|
export function contentDisposition(
|
|
type: "inline" | "attachment",
|
|
fileName: string,
|
|
): string {
|
|
const asciiFallback = fileName
|
|
.replace(/[^\x20-\x7e]/g, "_")
|
|
.replace(/["\\]/g, "");
|
|
// encodeURIComponent leaves ' ( ) * raw, but they are not RFC 5987
|
|
// attr-chars (the apostrophe is even the ext-value delimiter) — strict
|
|
// parsers would reject the filename* and fall back to the mangled ASCII
|
|
// name. Percent-encode them like the `content-disposition` npm package.
|
|
const encoded = encodeURIComponent(fileName).replace(
|
|
/['()*]/g,
|
|
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
);
|
|
return `${type}; filename="${asciiFallback}"; filename*=UTF-8''${encoded}`;
|
|
}
|