chore: sync in-progress work across marketing, admin, API and tests

Snapshot of uncommitted work that had accumulated in the tree alongside
the Turnstile changes:

- marketing pages, SEO helpers (lib/seo.ts, lib/marketing/) and
  structured data
- admin billing actions and a per-user portfolio view, plus an admin
  error boundary
- rate limiting (lib/rate-limit.ts) applied across the /api/v1 surface
- CSP and proxy adjustments, accounting/webhook lib updates
- Playwright config and an e2e/unit test suite
- next bumped to ^16.3.4 with the lockfile regenerated
- generated AGENTS.md / CLAUDE.md

Authored by other sessions working in this tree; committed here so the
Turnstile work could be pushed without leaving the tree dirty.
Typecheck passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-09-05 16:27:16 -04:00
co-authored by Claude Opus 5
parent 8f90347659
commit 1d02598786
71 changed files with 3641 additions and 819 deletions
+161
View File
@@ -0,0 +1,161 @@
import { test, expect } from "@playwright/test"
import {
ALLOWED_UPLOAD_EXTENSIONS,
isAllowedUploadExt,
extOf,
contentTypeForKey,
contentMatchesExtension,
keyBelongsToOwner,
} from "@/lib/storage"
/**
* Upload validation is the boundary between "a landlord attached a lease PDF"
* and "a tenant stored an HTML file that executes on our origin". Two
* independent gates matter: the extension allowlist and the magic-byte check.
* Neither is sufficient alone.
*/
const sig = (...bytes: number[]) => Buffer.from(bytes)
const PDF = sig(0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x37)
const PNG = sig(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)
const JPG = sig(0xff, 0xd8, 0xff, 0xe0)
const GIF = sig(0x47, 0x49, 0x46, 0x38, 0x39, 0x61)
const ZIP = sig(0x50, 0x4b, 0x03, 0x04)
const OLE = sig(0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1)
const WEBP = Buffer.concat([sig(0x52, 0x49, 0x46, 0x46), sig(0, 0, 0, 0), sig(0x57, 0x45, 0x42, 0x50)])
const HTML = Buffer.from("<html><script>alert(1)</script>", "utf8")
const SVG = Buffer.from('<svg xmlns="http://www.w3.org/2000/svg">', "utf8")
test.describe("extension allowlist", () => {
test("accepts every documented extension", () => {
for (const ext of ALLOWED_UPLOAD_EXTENSIONS) {
expect(isAllowedUploadExt(`file.${ext}`), `${ext} should be allowed`).toBe(true)
}
})
test("rejects executable and markup types", () => {
// svg and html can execute JavaScript when served inline from our origin.
for (const name of [
"x.svg",
"x.html",
"x.htm",
"x.js",
"x.mjs",
"x.exe",
"x.sh",
"x.php",
"x.xml",
"x.json",
]) {
expect(isAllowedUploadExt(name), `${name} should be rejected`).toBe(false)
}
})
test("is case-insensitive", () => {
expect(isAllowedUploadExt("SCAN.PDF")).toBe(true)
expect(isAllowedUploadExt("Photo.JPeG")).toBe(true)
// …and stays case-insensitive for the denied set.
expect(isAllowedUploadExt("payload.SVG")).toBe(false)
})
test("uses the LAST extension in a multi-dot name", () => {
// "invoice.pdf.html" is html, not pdf — the classic double-extension trick.
expect(isAllowedUploadExt("invoice.pdf.html")).toBe(false)
expect(isAllowedUploadExt("archive.tar.pdf")).toBe(true)
expect(extOf("invoice.pdf.html")).toBe("html")
})
test("rejects a name with no extension", () => {
expect(isAllowedUploadExt("noextension")).toBe(false)
})
})
test.describe("magic-byte verification", () => {
test("accepts content matching its claimed extension", () => {
expect(contentMatchesExtension(PDF, "pdf")).toBe(true)
expect(contentMatchesExtension(PNG, "png")).toBe(true)
expect(contentMatchesExtension(JPG, "jpg")).toBe(true)
expect(contentMatchesExtension(JPG, "jpeg")).toBe(true)
expect(contentMatchesExtension(GIF, "gif")).toBe(true)
expect(contentMatchesExtension(WEBP, "webp")).toBe(true)
expect(contentMatchesExtension(ZIP, "docx")).toBe(true)
expect(contentMatchesExtension(ZIP, "xlsx")).toBe(true)
expect(contentMatchesExtension(OLE, "doc")).toBe(true)
expect(contentMatchesExtension(OLE, "xls")).toBe(true)
})
test("rejects HTML disguised with an allowed extension", () => {
// The whole point: an allowlisted extension over script content.
expect(contentMatchesExtension(HTML, "pdf")).toBe(false)
expect(contentMatchesExtension(HTML, "png")).toBe(false)
expect(contentMatchesExtension(HTML, "jpg")).toBe(false)
expect(contentMatchesExtension(HTML, "docx")).toBe(false)
expect(contentMatchesExtension(SVG, "png")).toBe(false)
})
test("rejects one image type renamed as another", () => {
expect(contentMatchesExtension(PNG, "pdf")).toBe(false)
expect(contentMatchesExtension(PDF, "png")).toBe(false)
expect(contentMatchesExtension(GIF, "webp")).toBe(false)
})
test("rejects a truncated header that cannot be verified", () => {
expect(contentMatchesExtension(sig(0x25, 0x50), "pdf")).toBe(false)
expect(contentMatchesExtension(Buffer.alloc(0), "png")).toBe(false)
})
test("allows csv and txt, which have no reliable signature", () => {
// Documented behaviour — these are served as attachments, not inline.
expect(contentMatchesExtension(HTML, "csv")).toBe(true)
expect(contentMatchesExtension(HTML, "txt")).toBe(true)
})
})
test.describe("contentTypeForKey", () => {
test("maps known extensions and defaults to octet-stream", () => {
expect(contentTypeForKey("a/b/c.pdf")).toBe("application/pdf")
expect(contentTypeForKey("a/b/c.PNG")).toBe("image/png")
expect(contentTypeForKey("a/b/c.jpeg")).toBe("image/jpeg")
expect(contentTypeForKey("a/b/c.unknown")).toBe("application/octet-stream")
expect(contentTypeForKey("noext")).toBe("application/octet-stream")
})
test("never returns an inline-executable content type", () => {
for (const name of ["x.svg", "x.html", "x.js"]) {
expect(contentTypeForKey(name)).toBe("application/octet-stream")
}
})
})
test.describe("keyBelongsToOwner — cross-tenant isolation", () => {
test("accepts a key in the owner's own namespace", () => {
expect(keyBelongsToOwner("user123/documents/a.pdf", "user123")).toBe(true)
expect(keyBelongsToOwner("/user123/documents/a.pdf", "user123")).toBe(true)
})
test("rejects another tenant's namespace", () => {
expect(keyBelongsToOwner("user999/documents/a.pdf", "user123")).toBe(false)
})
test("rejects a prefix that merely starts with the owner id", () => {
// "user1234" must not satisfy owner "user123".
expect(keyBelongsToOwner("user1234/documents/a.pdf", "user123")).toBe(false)
})
test("rejects traversal attempts", () => {
expect(keyBelongsToOwner("../user999/a.pdf", "user123")).toBe(false)
expect(keyBelongsToOwner("..\\\\user999\\\\a.pdf", "user123")).toBe(false)
})
test("rejects empty inputs rather than defaulting open", () => {
expect(keyBelongsToOwner("", "user123")).toBe(false)
expect(keyBelongsToOwner(null, "user123")).toBe(false)
expect(keyBelongsToOwner(undefined, "user123")).toBe(false)
expect(keyBelongsToOwner("user123/a.pdf", "")).toBe(false)
})
test("handles backslash separators the same as forward slashes", () => {
expect(keyBelongsToOwner("user123\\\\documents\\\\a.pdf", "user123")).toBe(true)
expect(keyBelongsToOwner("user999\\\\documents\\\\a.pdf", "user123")).toBe(false)
})
})