* fix(pdf): align textkit line-box and font metrics to browser behaviour
CJK characters in resumes with a tightened typography line-height
(< ~1.4) had their descenders clipped by the next line. Latin glyphs
in the same resume rendered fine. Fixes the visual regression vs the
v5.0.x Puppeteer-based renderer reported in issue #2986 and follow-ups.
The clipping is caused by two independent gaps in @react-pdf/textkit
relative to standard CSS line-box rules:
1. `height(run)` short-circuits to the user-supplied lineHeight and
ignores the run's intrinsic ascent + descent. CSS line-boxes are
spec'd as `max(line-height, content-area)` — when CJK glyphs are
present the content-area is taller than a tightened lineHeight, so
the box must grow. textkit didn't, so the baseline (computed from
the real, larger CJK ascent) sat below the box and the descender
bled into the next line.
2. `ascent / descent / lineGap` are read directly from fontkit's hhea
defaults. For Source Han Sans/Serif (the CJK fallbacks registered
in #3013) hhea is intentionally inflated for legacy Windows GDI
compatibility (1.45 em vs 1.0 em), so even a fixed line-box would
have been excessively tall. Browsers (and the v5.0.x Puppeteer
renderer) read OS/2 sTypoAscender/Descender/LineGap instead, which
are the values the type designers intend for modern shaping.
Both are upstream behaviours of `@react-pdf/textkit`, but waiting for
an upstream release would leave existing users with broken CJK output.
The fix is shipped as a pnpm patch (~30 LOC):
- `resolveTypoMetrics(font)`: prefer OS/2 typo metrics, fall back to
hhea when an OS/2 table is absent (e.g. the StandardFont stand-ins
for Helvetica/Courier/Times). Used by ascent/descent/lineGap so all
height-related calculations stay consistent.
- `height(run)`: `Math.max(lineHeight || 0, intrinsic)` instead of
the original short-circuit, matching CSS line-box rules.
The patch is self-contained: existing Latin-only resumes are
unaffected (IBM Plex Serif's typo metrics equal hhea; Roboto's typo
is slightly smaller, but only changes the rendered line-box for users
who set lineHeight below ~1.17, which already used to clip ascenders
under v5.1.x and now lays out as it would in a browser).
Tooling notes:
- `Dockerfile.dev` copies `patches/` before `pnpm install` so the
dev image build no longer fails on `--frozen-lockfile`. The
production `Dockerfile` already gets it for free via
`turbo prune --docker` (the patch reference in package.json marks
the directory as part of the pruned slice).
- The patch will become a no-op once an equivalent fix lands upstream
in @react-pdf/textkit; the entry can then be removed from
`pnpm.patchedDependencies` and the file deleted.
* fix(deps): regenerate lockfile and move patchedDependencies for pnpm 11
The previous commit's lockfile was authored by pnpm 8 (lockfileVersion 6.0)
and kept patchedDependencies under package.json#pnpm. The repository now
declares packageManager: pnpm@11.1.2, which:
- writes lockfileVersion 9.0 and rejects v6 with ERR_PNPM_LOCKFILE_BREAKING_CHANGE
on --frozen-lockfile (CI failure observed in autofix.ci);
- reads pnpm settings from pnpm-workspace.yaml, silently ignoring the
package.json#pnpm field — so the textkit patch was no longer applied.
Regenerate pnpm-lock.yaml with pnpm 11.1.2 and move patchedDependencies
to pnpm-workspace.yaml so the patch is applied and CI passes.
* chore: update dependencies
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat: add text color support to the rich text editor
* improve design of text color picker
* Update translations for color picker features in multiple languages
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Add stricter URL and redirect validation, endpoint rate limiting, safer defaults for printer and compose config, and CSP protections across server and API surfaces.
Made-with: Cursor
* feat(mcp): add OAuth 2.1 authentication for claude.ai MCP connector
Enable OAuth 2.1 (RFC 8414 + RFC 7591) for the MCP endpoint using
better-auth's MCP plugin. This allows claude.ai and other MCP clients
to authenticate via Dynamic Client Registration and Authorization Code
flow with PKCE, using the existing login page.
- Add `mcp()` plugin to better-auth config with login page redirect
- Add `.well-known/oauth-authorization-server` discovery endpoint
- Add `.well-known/oauth-protected-resource` metadata endpoint
- Update MCP handler to accept Bearer tokens via `getMcpSession`
- Retain `x-api-key` fallback for backward compatibility
- Return proper HTTP 401 + WWW-Authenticate header for unauthed requests
- Add `oauthApplication`, `oauthAccessToken`, `oauthConsent` tables
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): use typed AuthError and suppress noisy verifyApiKey throws
- Replace string-matching error detection with instanceof AuthError
- Wrap verifyApiKey in try-catch to avoid logging malformed key errors
- Move console.error below auth check so 401s don't pollute logs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mcp): add database migration for OAuth tables
Creates oauth_application, oauth_access_token, and oauth_consent tables
required for MCP OAuth 2.1 Dynamic Client Registration flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): resolve OAuth Bearer token auth for oRPC tool calls
The oRPC context only checked session cookies and API keys, causing
MCP tool calls from OAuth clients (claude.ai) to fail with Unauthorized
even though the MCP endpoint itself authenticated successfully.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): look up user by userId from OAuth access token
getMcpSession returns OAuthAccessToken (with userId), not a session
object with a user property. Must query the user table by userId.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(mcp): migrate from deprecated mcp() plugin to @better-auth/oauth-provider
The better-auth MCP plugin is marked for deprecation in favor of the
OAuth Provider plugin. This refactors the entire OAuth 2.1 flow to use
@better-auth/oauth-provider with JWT-based token verification, replacing
the opaque token lookup via getMcpSession().
Key changes:
- Replace mcp() with jwt() + oauthProvider() in auth config
- Replace getMcpSession() with verifyAccessToken() (JWT/JWKS)
- Replace oauthApplication table with oauthClient (RFC 7591 compliant)
- Add oauthRefreshToken table and jwks table for JWT signing keys
- Extract shared authBaseUrl and verifyOAuthToken helper
- Hoist McpServer to module scope (avoid per-request reconstruction)
- Update .well-known discovery endpoints for OAuth Provider
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): resolve OAuth 2.1 flow for claude.ai MCP connector
Multiple fixes required to make the full MCP OAuth flow work with
claude.ai's implementation:
- Add RFC 8414 discovery route at /.well-known/oauth-authorization-server/api/auth
(claude.ai appends the issuer path per spec)
- Add /auth/oauth server route to handle login/consent flow
(generates auth codes directly, bypassing h3 cookie issues)
- Default token_endpoint_auth_method to "none" via onRequest plugin hook
(claude.ai omits this field, causing confidential client rejection)
- Strip prompt=consent from authorize requests via onRequest hook
(better-auth checks prompt before skipConsent, causing redirect loops)
- Add validAudiences for MCP resource URL
(JWT aud claim contains the MCP URL, not the base URL)
- Disable CSRF check for cross-origin OAuth flows
- Log token endpoint errors for debugging
- Set skipConsent on OAuth clients via /auth/oauth route
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mcp): harden OAuth security and enforce lock on delete
- Scope CSRF bypass to OAuth2 paths only instead of disabling globally
- Validate redirect_uri against registered client URIs (prevents code interception)
- Use pathname matching instead of fragile url.includes() for route guards
- Replace biased modulo code generation with crypto.randomBytes
- Enforce resume lock check on delete (previously silently ignored)
- Remove debug console.error logging of OAuth token response bodies
- Use Response.json() consistently for MCP 401 response
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update dependencies, refine ignore patterns, and enhance documentation
- Updated various dependencies in package.json and pnpm-lock.yaml for improved stability and features.
- Adjusted ignore patterns in knip.json to include specific component directories.
- Enhanced documentation for the MCP server, clarifying authentication methods and configuration options.
- Made minor adjustments to VSCode settings for better code organization.
* fix(mcp): resolve OAuth client registration and stale token handling
Claude.ai sends token_endpoint_auth_method: "client_secret_post" without
a client_secret during Dynamic Client Registration, causing Better Auth to
reject it as an unauthenticated confidential client. Force to "none" for
unauthenticated registrations.
Also catch JWKS verification errors (e.g. key rotation after redeployment)
so stale Bearer tokens return 401 instead of 200 with an error body,
allowing clients to re-initiate the OAuth flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* reiterate on tests
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>