- Cleans expired verifications and orphaned usage rows
- Previously only ran once on startup (13d+ uptime = accumulation)
- Interval uses .unref() to not block graceful shutdown
- Stopped during shutdown before pool.end()
- Idempotent start (safe to call multiple times)
- 6 TDD tests added (periodic-cleanup.test.ts)
- 663 tests total, all passing
- Extract shared PDF options construction into buildPdfOptions()
- Both renderPdf and renderUrlPdf now use the shared builder
- 5 TDD tests added (pdf-options-builder.test.ts)
- 633 tests passing, 0 tsc errors
- New src/utils/pdf-handler.ts with handlePdfRoute() helper
- Handles: content-type validation, PDF option validation, slot acquire/release, error mapping, response headers
- Refactored convert.ts from 388 to 233 lines (40% reduction)
- 10 TDD tests for the new helper (RED→GREEN verified)
- All 618 tests passing, zero tsc --noEmit errors
- All remaining catch(err) and catch(error) blocks now use : unknown
across keys.ts, email.ts, usage.ts, index.ts (shutdown handlers)
- Extract admin/usage routes from index.ts (459→391 lines) into
new src/routes/admin.ts with authMiddleware + adminAuth per-route
- Remove unused imports from index.ts (getConcurrencyStats, isProKey,
getUsageForKey, getUsageStats, NextFunction)
- 10 new TDD tests (7 error helper, 3 admin router)
- 608 total tests, all passing
- Replace all catch(err: any) with catch(err: unknown) across 8 source files
- Add errorMessage() and errorCode() helpers for safe error property access
- Type nodemailer transport config as SMTPTransport.Options (was any)
- Type health endpoint databaseStatus (was any)
- Type convert route margin param (was any)
- Change queryWithRetry params from any[] to unknown[]
- Update isTransientError to require Error instances (was accepting plain objects)
- 19 new TDD tests (error-type-safety.test.ts)
- Updated existing tests to use proper Error instances
- 598 tests total, all passing, zero type errors
- CORS middleware now allows both docfast.dev and staging.docfast.dev origins
for auth/billing routes, with Vary: Origin header for proper caching
- Unknown origins fall back to production origin (not reflected)
- 13 TDD tests added for CORS behavior
Type safety improvements:
- Augment Express.Request with requestId, acquirePdfSlot, releasePdfSlot
- Use Puppeteer's PaperFormat and PuppeteerLifeCycleEvent types in browser.ts
- Use 'as const' for format literals in convert/demo/templates routes
- Replace Stripe apiVersion 'as any' with @ts-expect-error
- Zero 'as any' casts remaining in production code
579 tests passing (13 new), 51 test files
- Added multi-stage build to reduce final image size
- Stage 1 (builder): Installs all deps, compiles TS, generates OpenAPI, builds HTML
- Stage 2 (production): Fresh base image with only production deps and compiled artifacts
- Final image no longer contains src/, tsconfig.json, or dev dependencies
- Added TDD test (dockerfile-build.test.ts) to verify build artifacts exist
- All 561 tests pass
Reduces image size by excluding TypeScript source, build tools, and dev dependencies.
- Remove verificationsCache array and loadVerifications() function from verification.ts
- Remove verifyToken() and verifyTokenSync() functions (multi-replica unsafe, never used)
- Remove createVerification() function (stores unused data)
- Remove GET /verify route and verifyPage() helper function
- Remove loadVerifications() call from startup
- Remove createVerification() usage from signup route
- Update imports and test mocks to match removed functions
- Keep active 6-digit code system intact (createPendingVerification, verifyCode, etc.)
All 559 tests passing. The active verification system using pending_verifications
table and 6-digit codes continues to work normally.
- updateEmailByCustomer: DB fallback when stripe_customer_id not in cache
- updateKeyEmail: DB fallback when key not in cache
- POST /v1/recover: DB fallback when email not in cache (was only on verify)
- 6 TDD tests added (keys-email-update.test.ts, recover-initial-db-fallback.test.ts)
- 547 tests total, all passing
/v1/email-change was missing from the restricted CORS list, getting
wildcard Access-Control-Allow-Origin: * instead of being restricted to
https://docfast.dev like other account management routes (signup,
recover, billing, demo). TDD: test added to app-routes.test.ts.
Remove fire-and-forget SIGTERM/SIGINT handlers from usage.ts (race condition
with pool.end() in index.ts). Instead, await flushDirtyEntries() in the
index.ts shutdown orchestrator between stopping the server and closing the
DB pool.
- downgradeByCustomer now queries DB when key not in memory cache,
preventing cancelled customers from keeping Pro access in multi-pod setups
- recover/verify endpoint falls back to DB lookup when cache miss on email
- Added TDD tests for both fallback paths (4 new tests)
- renderPdf() and renderUrlPdf() now return { pdf, durationMs }
- Timing wraps the actual render with Date.now()
- Log render duration via logger.info
- Add X-Render-Time response header in convert and demo routes
- Update all callers in convert, demo, templates routes
- Add TDD tests in render-timing.test.ts
- Update existing test mocks for new return shape
- Add 'URL to PDF' nav link and example section
- Show basic and advanced cURL examples for /v1/convert/url
- Include security notes (JS disabled, private URLs blocked)
- Add test coverage for the new section
- Section 2.1: Replace Free Tier with Demo (Free) - no account required,
5 req/hour, testing and evaluation only, no SLA/support
- Section 5.1: Change 'no SLA for free tier' to 'no SLA for demo usage'
- Add terms-content test to verify no Free Tier references remain
- Rebuild public/terms.html via build-html.cjs
Remove global express.json({ limit: '2mb' }) that preempted route-specific
parsers. Each route group now has its own express.json() with correct limit:
- Demo: 50KB, Convert: 500KB, Others: 2MB, Stripe webhook: unchanged
- Replace unbounded Set with Map<sessionId, timestamp> tracking insertion time
- Add periodic cleanup every hour to remove entries older than 24h
- Add on-demand cleanup before duplicate checks for timely cleanup
- Add comprehensive TDD tests verifying TTL behavior:
* Fresh entries work correctly
* Stale entries (>24h) get cleaned up
* Fresh entries survive cleanup
* Bounded size with many entries
- All 447 tests pass including 4 new TTL tests
- Memory leak fixed while preserving DB-level deduplication
When renderUrlPdf() sets up request interception for SSRF DNS pinning,
the interceptor and event listener were never cleaned up in recyclePage().
This could cause subsequent HTML-to-PDF conversions on the same pooled
page to have external resources blocked by the stale interceptor.
- Export recyclePage for testability
- Add removeAllListeners('request') + setRequestInterception(false)
- Add browser-recycle.test.ts with TDD (red→green verified)
Tests: 443 passing (was 442)