Skip to content

Commit b6a10f6

Browse files
ABCrimsonclaude
andcommitted
v1.1.5: fix OAuth, navigation scaling, and comprehensive codebase audit
Critical fixes: - Fix OAuth broken in production: add .trim() to all env var reads in auth.config.ts to handle trailing newlines from Vercel encrypted vars - Fix navigation bar text overlap at intermediate widths by raising desktop breakpoint from md (768px) to lg (1024px) Codebase audit (40+ files across all packages): - CI: fix actions/checkout@v6 → @v4 and actions/setup-node@v6 → @v4 - Web: parallel data fetching with Promise.all, extract shared latex.ts, module-scope Framer Motion variants, focus-visible patterns, a11y attrs - API: remove double error logging, Redis pipeline for atomic ops, optional chaining in profile resolver, typed logger levels - Math engine: Set-based isMathFunction with missing Si/Ci/erf/li, astEquals over JSON.stringify, factorial overflow guard, NodeType enums - Plot engine: fix renderCurve scene leak, remove dead WebGL code, typed gridColor - Database: fix singleton always-store pattern, remove redundant index - Workers: extract arcLengthSchema to validators module - Docs: version refs, DataLoader counts, path corrections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0329137 commit b6a10f6

43 files changed

Lines changed: 214 additions & 243 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ jobs:
2424
runs-on: ubuntu-latest
2525
timeout-minutes: 10
2626
steps:
27-
- uses: actions/checkout@v6
27+
- uses: actions/checkout@v4
2828

2929
- uses: pnpm/action-setup@v4
3030
with:
3131
version: 11.0.0-alpha.11
3232

33-
- uses: actions/setup-node@v6
33+
- uses: actions/setup-node@v4
3434
with:
3535
node-version: ${{ env.NODE_VERSION }}
3636
cache: 'pnpm'
@@ -63,13 +63,13 @@ jobs:
6363
runs-on: ubuntu-latest
6464
timeout-minutes: 10
6565
steps:
66-
- uses: actions/checkout@v6
66+
- uses: actions/checkout@v4
6767

6868
- uses: pnpm/action-setup@v4
6969
with:
7070
version: 11.0.0-alpha.11
7171

72-
- uses: actions/setup-node@v6
72+
- uses: actions/setup-node@v4
7373
with:
7474
node-version: ${{ env.NODE_VERSION }}
7575

@@ -97,13 +97,13 @@ jobs:
9797
runs-on: ubuntu-latest
9898
timeout-minutes: 15
9999
steps:
100-
- uses: actions/checkout@v6
100+
- uses: actions/checkout@v4
101101

102102
- uses: pnpm/action-setup@v4
103103
with:
104104
version: 11.0.0-alpha.11
105105

106-
- uses: actions/setup-node@v6
106+
- uses: actions/setup-node@v4
107107
with:
108108
node-version: ${{ env.NODE_VERSION }}
109109

@@ -139,13 +139,13 @@ jobs:
139139
runs-on: ubuntu-latest
140140
timeout-minutes: 10
141141
steps:
142-
- uses: actions/checkout@v6
142+
- uses: actions/checkout@v4
143143

144144
- uses: pnpm/action-setup@v4
145145
with:
146146
version: 11.0.0-alpha.11
147147

148-
- uses: actions/setup-node@v6
148+
- uses: actions/setup-node@v4
149149
with:
150150
node-version: ${{ env.NODE_VERSION }}
151151

@@ -199,13 +199,13 @@ jobs:
199199
runs-on: ubuntu-latest
200200
timeout-minutes: 20
201201
steps:
202-
- uses: actions/checkout@v6
202+
- uses: actions/checkout@v4
203203

204204
- uses: pnpm/action-setup@v4
205205
with:
206206
version: 11.0.0-alpha.11
207207

208-
- uses: actions/setup-node@v6
208+
- uses: actions/setup-node@v4
209209
with:
210210
node-version: ${{ env.NODE_VERSION }}
211211

.github/workflows/deploy-workers.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ jobs:
1414
matrix:
1515
worker: [cas-service, export-service, rate-limiter]
1616
steps:
17-
- uses: actions/checkout@v6
17+
- uses: actions/checkout@v4
1818
- uses: pnpm/action-setup@v4
19-
- uses: actions/setup-node@v6
19+
- uses: actions/setup-node@v4
2020
with:
2121
node-version: '24'
2222
cache: 'pnpm'

ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ GraphQL API integrated into the Next.js app via route handler.
130130
- Upstash Redis caching in `src/lib/cache.ts`
131131
- API package exports source `.ts` files (not dist/) for monorepo dev
132132

133-
**Tech:** Apollo Server 5.4.0, GraphQL 16.12.0, DataLoader 2.2.3, Zod
133+
**Tech:** Apollo Server 5.4.0, GraphQL 16.13.0, DataLoader 2.2.3, Zod
134134

135135
### @nextcalc/types
136136

apps/api/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Backend GraphQL API for NextCalc Pro, built with Apollo Server, Prisma, and Next
44

55
## Tech Stack
66

7-
- **Database:** Neon PostgreSQL with Prisma 7.5.0-dev.32
7+
- **Database:** Neon PostgreSQL with Prisma 7.5.0-dev.33
88
- **API:** GraphQL with Apollo Server 5.4.0
99
- **Authentication:** Auth.js v5 (NextAuth 5.0-beta.30) with OAuth + jose 6.1.3 JWT verification
1010
- **Caching:** Upstash Redis
@@ -364,7 +364,7 @@ vercel --prod
364364

365365
# Run database migrations
366366
vercel env pull .env.production
367-
pnpm prisma:deploy
367+
pnpm --filter @nextcalc/database db:migrate:deploy
368368
```
369369

370370
### Environment Variables

apps/api/src/graphql/resolvers/profile.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export const profileResolvers = {
1515
args: { input: { name?: string; bio?: string } },
1616
context: GraphQLContext,
1717
) => {
18-
requireAuth(context);
18+
const user = requireAuth(context);
1919
const { name, bio } = args.input;
2020
const updated = await context.prisma.user.update({
21-
where: { id: context.user?.id },
21+
where: { id: user.id },
2222
data: {
2323
...(name !== undefined ? { name } : {}),
2424
...(bio !== undefined ? { bio } : {}),
@@ -30,9 +30,9 @@ export const profileResolvers = {
3030

3131
Query: {
3232
userProfile: async (_parent: unknown, args: { userId: string }, context: GraphQLContext) => {
33-
requireAuth(context);
33+
const user = requireAuth(context);
3434

35-
if (args.userId !== context.user?.id) {
35+
if (args.userId !== user.id) {
3636
throw new ForbiddenError('You can only view your own profile data');
3737
}
3838

@@ -91,9 +91,9 @@ export const profileResolvers = {
9191
args: { userId: string; days: number },
9292
context: GraphQLContext,
9393
) => {
94-
requireAuth(context);
94+
const user = requireAuth(context);
9595

96-
if (args.userId !== context.user?.id) {
96+
if (args.userId !== user.id) {
9797
throw new ForbiddenError('You can only view your own profile data');
9898
}
9999

@@ -133,9 +133,9 @@ export const profileResolvers = {
133133
},
134134

135135
userAnalytics: async (_parent: unknown, args: { userId: string }, context: GraphQLContext) => {
136-
requireAuth(context);
136+
const user = requireAuth(context);
137137

138-
if (args.userId !== context.user?.id) {
138+
if (args.userId !== user.id) {
139139
throw new ForbiddenError('You can only view your own profile data');
140140
}
141141

apps/api/src/lib/cache.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,13 @@ export async function getCachedUpvoteCount(targetId: string): Promise<number | n
260260
export async function incrementUpvoteCount(targetId: string): Promise<void> {
261261
if (!redis) return;
262262
const key = cacheKey(CACHE_KEYS.UPVOTE_COUNT, targetId);
263-
await redis.incr(key);
264-
await redis.expire(key, CACHE_TTL.UPVOTE_COUNT);
263+
await redis.pipeline().incr(key).expire(key, CACHE_TTL.UPVOTE_COUNT).exec();
265264
}
266265

267266
export async function decrementUpvoteCount(targetId: string): Promise<void> {
268267
if (!redis) return;
269268
const key = cacheKey(CACHE_KEYS.UPVOTE_COUNT, targetId);
270-
await redis.decr(key);
271-
await redis.expire(key, CACHE_TTL.UPVOTE_COUNT);
269+
await redis.pipeline().decr(key).expire(key, CACHE_TTL.UPVOTE_COUNT).exec();
272270
}
273271

274272
export async function cacheCommentCount(postId: string, count: number): Promise<void> {

apps/api/src/plugins/performance-monitoring.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export const performanceMonitoringPlugin = (): ApolloServerPlugin<GraphQLContext
7979
timestamp: new Date().toISOString(),
8080
};
8181

82-
const logLevel = errors > 0 ? 'error' : duration > 1000 ? 'warn' : 'info';
82+
const logLevel: 'error' | 'warn' | 'info' =
83+
errors > 0 ? 'error' : duration > 1000 ? 'warn' : 'info';
8384

8485
logger[logLevel]('GraphQL request completed', {
8586
operationName: metrics.operationName ?? 'anonymous',

apps/api/src/server.ts

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
*/
1212

1313
import { ApolloServer } from '@apollo/server';
14-
import { unwrapResolverError } from '@apollo/server/errors';
1514
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
1615
import {
1716
ApolloServerPluginLandingPageLocalDefault,
@@ -23,7 +22,6 @@ import { resolvers } from './graphql/resolvers';
2322
import { typeDefs } from './graphql/schema';
2423
import type { GraphQLContext } from './lib/context';
2524
import { createDataLoaders } from './lib/dataloaders';
26-
import { logger } from './lib/logger';
2725
import {
2826
errorTrackingPlugin,
2927
performanceMonitoringPlugin,
@@ -44,23 +42,14 @@ const isDevelopment = process.env.NODE_ENV !== 'production';
4442
* AS5 formatError using unwrapResolverError
4543
*
4644
* unwrapResolverError extracts the original error thrown by the resolver,
47-
* stripping away Apollo's wrapper. This allows logging the real error
48-
* while masking internal details in production responses.
45+
* stripping away Apollo's wrapper. Error logging is handled by
46+
* errorTrackingPlugin (didEncounterErrors hook) — formatError only
47+
* masks internal details in production responses.
4948
*/
5049
const formatError = (
5150
formattedError: GraphQLFormattedError,
52-
error: unknown,
51+
_error: unknown,
5352
): GraphQLFormattedError => {
54-
const originalError = unwrapResolverError(error);
55-
56-
// Always log errors structurally — in production the level filter handles visibility
57-
logger.error('GraphQL error in formatError', {
58-
message: formattedError.message,
59-
path: formattedError.path?.join('.'),
60-
code: formattedError.extensions?.code as string | undefined,
61-
original: originalError instanceof Error ? originalError.message : undefined,
62-
});
63-
6453
// In production, mask internal server errors to avoid leaking implementation details
6554
if (
6655
!isDevelopment &&
@@ -98,23 +87,8 @@ export function createApolloServer(httpServer?: import('node:http').Server) {
9887
footer: false,
9988
}),
10089

101-
// AS5 error lifecycle hooks
102-
{
103-
async contextCreationDidFail({ error }) {
104-
logger.error('Context creation failed', {
105-
error: error instanceof Error ? error.message : String(error),
106-
stack: error instanceof Error ? error.stack : undefined,
107-
});
108-
},
109-
async unexpectedErrorProcessingRequest({ error }) {
110-
logger.error('Unexpected Apollo Server error', {
111-
error: error instanceof Error ? error.message : String(error),
112-
stack: error instanceof Error ? error.stack : undefined,
113-
});
114-
},
115-
},
116-
117-
// Custom plugins
90+
// Custom plugins (errorTrackingPlugin handles contextCreationDidFail
91+
// and unexpectedErrorProcessingRequest — no inline duplicates needed)
11892
performanceMonitoringPlugin(),
11993
responseCachingPlugin(),
12094
queryComplexityPlugin(1000),

apps/web/app/[locale]/learn/[topic]/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ export default async function TopicPage({ params }: { params: Promise<{ topic: s
6767
notFound();
6868
}
6969

70-
const definitions = await getDefinitionsByTopic(topic);
71-
const problems = await getProblemsByTopic(topic);
70+
const [definitions, problems] = await Promise.all([
71+
getDefinitionsByTopic(topic),
72+
getProblemsByTopic(topic),
73+
]);
7274

7375
// Fetch user's bookmarked definition IDs
7476
let bookmarkedIds: string[] = [];

apps/web/app/[locale]/worksheet/client-wrapper.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ const WorksheetEditor = dynamic(
1717
{
1818
ssr: false,
1919
loading: () => (
20-
<div className="flex items-center justify-center min-h-[60vh]">
20+
<div className="flex items-center justify-center min-h-[60vh]" role="status" aria-live="polite">
2121
<div className="flex flex-col items-center gap-4 text-muted-foreground rounded-2xl border border-border/50 bg-card/50 backdrop-blur-md px-10 py-8 shadow-sm">
22-
<Loader2 className="h-8 w-8 animate-spin" aria-label="Loading worksheet editor" />
22+
<Loader2 className="h-8 w-8 animate-spin" aria-hidden="true" />
2323
<p className="text-sm">Loading worksheet editor...</p>
2424
</div>
2525
</div>

0 commit comments

Comments
 (0)