Skip to content

Commit 481e88d

Browse files
committed
checkpoint
1 parent 3e18703 commit 481e88d

File tree

17 files changed

+133
-91
lines changed

17 files changed

+133
-91
lines changed

src/auth/context.server.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,15 @@ function getSessionSecret(): string {
2828
'SESSION_SECRET environment variable is required in production',
2929
)
3030
}
31+
// In development, require explicit opt-in to use insecure default
32+
if (process.env.ALLOW_INSECURE_SESSION_SECRET !== 'true') {
33+
throw new Error(
34+
'SESSION_SECRET environment variable is required. ' +
35+
'Set ALLOW_INSECURE_SESSION_SECRET=true to use insecure default in development.',
36+
)
37+
}
3138
console.warn(
32-
'[Auth] SESSION_SECRET not set, using insecure default for development only',
39+
'[Auth] WARNING: Using insecure session secret for development. Do NOT use in production.',
3340
)
3441
return 'dev-secret-key-change-in-production'
3542
}

src/auth/session.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,12 @@ export function createOAuthStateCookie(
244244
state: string,
245245
isProduction: boolean,
246246
): string {
247-
return `oauth_state=${encodeURIComponent(state)}; HttpOnly; Path=/; Max-Age=${10 * 60}; SameSite=Lax${isProduction ? '; Secure' : ''}`
247+
// Use SameSite=Strict for OAuth state to prevent CSRF during OAuth flow
248+
return `oauth_state=${encodeURIComponent(state)}; HttpOnly; Path=/; Max-Age=${10 * 60}; SameSite=Strict${isProduction ? '; Secure' : ''}`
248249
}
249250

250251
export function clearOAuthStateCookie(isProduction: boolean): string {
251-
return `oauth_state=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax${isProduction ? '; Secure' : ''}`
252+
return `oauth_state=; HttpOnly; Path=/; Max-Age=0; SameSite=Strict${isProduction ? '; Secure' : ''}`
252253
}
253254

254255
export function getOAuthStateCookie(request: Request): string | null {

src/components/game/ui/GameHUD.tsx

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from 'react'
22
import { useGameStore } from '../hooks/useGameStore'
3-
import { Compass, Map, Coins, ShoppingBag } from 'lucide-react'
3+
import { Map, Coins, ShoppingBag, Zap } from 'lucide-react'
44

55
export function GameHUD() {
66
const {
@@ -13,7 +13,7 @@ export function GameHUD() {
1313
cornerIslands,
1414
showcaseUnlocked,
1515
cornersUnlocked,
16-
boatRotation,
16+
1717
coinsCollected,
1818
boatHealth,
1919
shipStats,
@@ -22,6 +22,7 @@ export function GameHUD() {
2222
clearLastUnlockedUpgrade,
2323
openShop,
2424
setPhase,
25+
fireCannon,
2526
} = useGameStore()
2627

2728
// Cooldown progress (0-1, 1 = ready)
@@ -116,22 +117,6 @@ export function GameHUD() {
116117

117118
if (phase !== 'playing') return null
118119

119-
// Convert rotation to compass direction
120-
const getCompassDirection = (rotation: number) => {
121-
// Normalize to 0-360
122-
const degrees = ((rotation * 180) / Math.PI + 360) % 360
123-
124-
if (degrees >= 337.5 || degrees < 22.5) return 'N'
125-
if (degrees >= 22.5 && degrees < 67.5) return 'NW'
126-
if (degrees >= 67.5 && degrees < 112.5) return 'W'
127-
if (degrees >= 112.5 && degrees < 157.5) return 'SW'
128-
if (degrees >= 157.5 && degrees < 202.5) return 'S'
129-
if (degrees >= 202.5 && degrees < 247.5) return 'SE'
130-
if (degrees >= 247.5 && degrees < 292.5) return 'E'
131-
if (degrees >= 292.5 && degrees < 337.5) return 'NE'
132-
return 'N'
133-
}
134-
135120
// lastUnlockedUpgrade is now the full Upgrade object
136121
const upgradeInfo = lastUnlockedUpgrade
137122

@@ -140,7 +125,16 @@ export function GameHUD() {
140125
{/* Coin hint */}
141126
{showCoinHint && (
142127
<div className="absolute top-16 left-1/2 -translate-x-1/2 animate-in fade-in slide-in-from-top-2 duration-300">
143-
<div className="bg-black/60 backdrop-blur-sm rounded-xl px-4 py-2 text-center text-white text-sm max-w-xs">
128+
<div className="bg-black/60 backdrop-blur-sm rounded-xl px-4 py-2 text-center text-white text-sm max-w-xs relative">
129+
<button
130+
onClick={() => {
131+
setShowCoinHint(false)
132+
setCoinHintDismissed(true)
133+
}}
134+
className="absolute -top-1 -right-1 w-5 h-5 bg-white/20 hover:bg-white/30 rounded-full flex items-center justify-center text-white/60 hover:text-white transition-colors pointer-events-auto"
135+
>
136+
×
137+
</button>
144138
<span className="text-yellow-400 font-medium">Coins</span> make you
145139
faster and can be spent in the{' '}
146140
<span className="text-yellow-400 font-medium">shop</span>!
@@ -149,15 +143,7 @@ export function GameHUD() {
149143
)}
150144

151145
{/* Top bar */}
152-
<div className="flex justify-between items-start p-4">
153-
{/* Compass */}
154-
<div className="pointer-events-auto bg-black/30 backdrop-blur-sm rounded-xl px-4 py-2 flex items-center gap-2 text-white">
155-
<Compass className="w-5 h-5" />
156-
<span className="font-mono font-bold text-lg w-8">
157-
{getCompassDirection(boatRotation)}
158-
</span>
159-
</div>
160-
146+
<div className="flex justify-end items-start p-4">
161147
{/* Coin counter + Shop button */}
162148
<button
163149
onClick={openShop}
@@ -169,7 +155,7 @@ export function GameHUD() {
169155
</button>
170156

171157
{/* Island counter */}
172-
<div className="pointer-events-auto bg-black/30 backdrop-blur-sm rounded-xl px-4 py-2 flex items-center gap-2 text-white">
158+
<div className="pointer-events-auto bg-black/30 backdrop-blur-sm rounded-xl px-4 py-2 flex items-center gap-2 text-white ml-2">
173159
<Map className="w-5 h-5" />
174160
<span className="font-bold">
175161
{discoveredIslands.size} / {totalIslands}
@@ -182,31 +168,45 @@ export function GameHUD() {
182168

183169
{/* Bottom bar - Battle stage only */}
184170
{stage === 'battle' && (
185-
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2">
171+
<div
172+
className={`absolute bottom-4 flex flex-col items-center gap-2 ${'max-md:right-4 max-md:left-auto max-md:translate-x-0 left-1/2 -translate-x-1/2'}`}
173+
>
174+
{/* Touch fire button - only visible on touch devices */}
175+
<button
176+
onClick={fireCannon}
177+
className="md:hidden pointer-events-auto bg-red-500/80 backdrop-blur-sm rounded-full p-4 flex items-center justify-center text-white hover:bg-red-600/80 active:scale-95 transition-all shadow-lg"
178+
aria-label="Fire cannons"
179+
>
180+
<Zap className="w-6 h-6" />
181+
</button>
186182
{/* Health bar */}
187183
<div
188184
className={`backdrop-blur-sm rounded-full px-4 py-2 flex items-center gap-3 transition-colors duration-75 ${
189185
damageFlash ? 'bg-red-500/70' : 'bg-black/40'
190-
}`}
186+
} max-md:px-3 max-md:py-1.5`}
191187
>
192-
<span className="text-red-400 text-sm font-medium">HP</span>
193-
<div className="w-32 h-3 bg-black/50 rounded-full overflow-hidden">
188+
<span className="text-red-400 text-sm font-medium max-md:text-xs">
189+
HP
190+
</span>
191+
<div className="w-32 h-3 bg-black/50 rounded-full overflow-hidden max-md:w-20 max-md:h-2">
194192
<div
195193
className="h-full bg-gradient-to-r from-red-600 to-red-400"
196194
style={{
197195
width: `${(boatHealth / shipStats.maxHealth) * 100}%`,
198196
}}
199197
/>
200198
</div>
201-
<span className="text-white text-sm font-mono w-12">
199+
<span className="text-white text-sm font-mono w-12 max-md:text-xs max-md:w-10">
202200
{Math.round(boatHealth)}/{shipStats.maxHealth}
203201
</span>
204202
</div>
205203

206204
{/* Cooldown gauge */}
207-
<div className="bg-black/40 backdrop-blur-sm rounded-full px-4 py-2 flex items-center gap-3">
208-
<span className="text-cyan-400 text-sm font-medium">CANNON</span>
209-
<div className="w-24 h-2 bg-black/50 rounded-full overflow-hidden">
205+
<div className="bg-black/40 backdrop-blur-sm rounded-full px-4 py-2 flex items-center gap-3 max-md:px-3 max-md:py-1.5">
206+
<span className="text-cyan-400 text-sm font-medium max-md:text-xs">
207+
CANNON
208+
</span>
209+
<div className="w-24 h-2 bg-black/50 rounded-full overflow-hidden max-md:w-16 max-md:h-1.5">
210210
<div
211211
className={`h-full transition-all duration-75 ${
212212
cooldownProgress >= 1
@@ -216,7 +216,7 @@ export function GameHUD() {
216216
style={{ width: `${cooldownProgress * 100}%` }}
217217
/>
218218
</div>
219-
<span className="text-white/60 text-xs w-12">
219+
<span className="text-white/60 text-xs w-12 max-md:text-[10px] max-md:w-10">
220220
{cooldownProgress >= 1 ? 'READY' : 'LOADING'}
221221
</span>
222222
</div>

src/components/game/ui/StatsHUD.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function StatsHUD() {
9191
: '-'
9292

9393
return (
94-
<div className="absolute top-16 right-4 z-10 pointer-events-auto">
94+
<div className="absolute top-16 right-4 z-10 pointer-events-auto hidden md:block">
9595
<div className="bg-black/40 backdrop-blur-sm rounded-lg px-4 py-3 text-xs text-white min-w-[150px] border border-white/5">
9696
{/* Speed */}
9797
<div className="space-y-1 mb-2">

src/components/game/ui/TouchControls.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export function TouchControls() {
9696
}
9797

9898
const onTouchMove = (e: TouchEvent) => {
99+
e.preventDefault()
99100
const touch = e.touches[0]
100101
handleMove(touch.clientX, touch.clientY)
101102
}
@@ -105,7 +106,7 @@ export function TouchControls() {
105106
}
106107

107108
window.addEventListener('touchstart', onTouchStart, { passive: true })
108-
window.addEventListener('touchmove', onTouchMove, { passive: true })
109+
window.addEventListener('touchmove', onTouchMove, { passive: false })
109110
window.addEventListener('touchend', onTouchEnd)
110111
window.addEventListener('touchcancel', onTouchEnd)
111112

src/mcp/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { doc, docSchema } from './tools/doc'
66
import { searchDocs, searchDocsSchema } from './tools/search-docs'
77
import { npmStats, npmStatsSchema } from './tools/npm-stats'
88
import { ecosystem, ecosystemSchema } from './tools/ecosystem'
9+
import { env } from '~/utils/env'
910

1011
export type McpAuthContext = {
1112
userId: string
@@ -81,7 +82,7 @@ export const ALL_TOOL_NAMES = [
8182
export type ToolName = (typeof ALL_TOOL_NAMES)[number]
8283

8384
function getEnabledTools(): Set<ToolName> | undefined {
84-
const envVar = process.env.TANSTACK_MCP_ENABLED_TOOLS
85+
const envVar = env.TANSTACK_MCP_ENABLED_TOOLS
8586
if (!envVar) return undefined
8687

8788
const validTools = new Set<ToolName>()

src/routes/oauth/register.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ export const Route = createFileRoute('/oauth/register')({
1212
server: {
1313
handlers: {
1414
POST: async ({ request }: { request: Request }) => {
15+
// CORS: Allow any origin for OAuth client registration
16+
// This is secure because:
17+
// 1. Registration only generates deterministic client IDs
18+
// 2. No secrets are issued (PKCE public clients)
19+
// 3. Redirect URIs are validated for localhost or HTTPS
20+
const origin = request.headers.get('Origin')
1521
setResponseHeader('Content-Type', 'application/json')
16-
setResponseHeader('Access-Control-Allow-Origin', '*')
22+
setResponseHeader('Access-Control-Allow-Origin', origin || '*')
1723
setResponseHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
1824
setResponseHeader(
1925
'Access-Control-Allow-Headers',
@@ -85,11 +91,12 @@ export const Route = createFileRoute('/oauth/register')({
8591
)
8692
}
8793
},
88-
OPTIONS: async () => {
94+
OPTIONS: async ({ request }: { request: Request }) => {
95+
const origin = request.headers.get('Origin')
8996
return new Response(null, {
9097
status: 204,
9198
headers: {
92-
'Access-Control-Allow-Origin': '*',
99+
'Access-Control-Allow-Origin': origin || '*',
93100
'Access-Control-Allow-Methods': 'POST, OPTIONS',
94101
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
95102
},

src/routes/oauth/token.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ export const Route = createFileRoute('/oauth/token')({
1010
server: {
1111
handlers: {
1212
POST: async ({ request }: { request: Request }) => {
13-
// Set CORS headers for all responses
14-
setResponseHeader('Access-Control-Allow-Origin', '*')
13+
// CORS: Allow any origin for OAuth token endpoint
14+
// This is secure because:
15+
// 1. PKCE (code_verifier) is required - attacker cannot forge this
16+
// 2. Authorization code is one-time use and short-lived
17+
// 3. No cookies are used - tokens are returned in response body
18+
const origin = request.headers.get('Origin')
19+
setResponseHeader('Access-Control-Allow-Origin', origin || '*')
1520
setResponseHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
1621
setResponseHeader(
1722
'Access-Control-Allow-Headers',
@@ -135,11 +140,12 @@ export const Route = createFileRoute('/oauth/token')({
135140
{ status: 400 },
136141
)
137142
},
138-
OPTIONS: async () => {
143+
OPTIONS: async ({ request }: { request: Request }) => {
144+
const origin = request.headers.get('Origin')
139145
return new Response(null, {
140146
status: 204,
141147
headers: {
142-
'Access-Control-Allow-Origin': '*',
148+
'Access-Control-Allow-Origin': origin || '*',
143149
'Access-Control-Allow-Methods': 'POST, OPTIONS',
144150
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
145151
},

src/server/github.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { graphql } from '@octokit/graphql'
2+
import { env } from '~/utils/env'
23

34
export const graphqlWithAuth = graphql.defaults({
45
headers: {
5-
authorization: `token ${process.env.GITHUB_AUTH_TOKEN}`,
6+
authorization: `token ${env.GITHUB_AUTH_TOKEN}`,
67
},
78
})

src/utils/discord.server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL
1+
import { env } from '~/utils/env'
2+
3+
const DISCORD_WEBHOOK_URL = env.DISCORD_WEBHOOK_URL
24

35
type EmbedField = {
46
name: string

0 commit comments

Comments
 (0)