Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9b8cfb7
feat: prewarm tinfoil attestation when a confidential model is selected
ital0 Jul 30, 2026
a590cf2
perf: keep the prompt prefix stable so the tinfoil router cache can hit
ital0 Jul 31, 2026
9c75bd1
perf: warm the enclave connection and cache session cookies in the ti…
ital0 Jul 31, 2026
26e1d0f
fix: keep backend keep-alive above the alb idle timeout to stop stray…
ital0 Jul 30, 2026
bdc7c86
fix: keep the user turn last by placing volatile notes before it
ital0 Jul 31, 2026
b63bb1d
fix: log client aborts as 499 and allow keep-warm restarts
ital0 Jul 31, 2026
de1842f
revert: drop the session cookie cache to keep device revocation instant
ital0 Jul 31, 2026
b9bb619
fix: route tinfoil requests to the atc-assigned enclave to stop key-c…
ital0 Jul 31, 2026
9df0157
fix: preserve the enclave api path prefix when routing to the assigne…
ital0 Jul 31, 2026
54b427b
chore: measure pre-handler latency on the tinfoil proxy path
ital0 Jul 31, 2026
62a12e1
chore: expose cross-origin timing so browsers can decompose request l…
ital0 Jul 31, 2026
b4b5cc8
chore: expose the server-timing header to cross-origin clients
ital0 Jul 31, 2026
082acd0
fix: keep system content contiguous so anthropic requests stop failing
ital0 Jul 31, 2026
85eff11
perf: cache cors preflights instead of re-checking every five seconds
ital0 Jul 31, 2026
950743f
chore: measure inference latency and surface upstream retry attempts
ital0 Jul 31, 2026
f2c56f5
fix: keep the enclave connection warm for the host traffic actually uses
ital0 Jul 31, 2026
ef89e1a
fix: carry volatile system notes into continuation steps
ital0 Jul 31, 2026
1a853b8
chore: instrument upstream attempts for every inference provider
ital0 Jul 31, 2026
a9c1645
docs: correct where per-send system notes are placed
ital0 Jul 31, 2026
a7acaf3
fix: echo the request origin when credentials and wildcard cors combine
ital0 Jul 31, 2026
ff96cb0
docs: restore the prompt-injection rationale for note ordering
ital0 Jul 31, 2026
2981c27
fix: shorten the cors preflight cache to bound stale policy windows
ital0 Jul 31, 2026
295d0ff
test: align the preflight cache expectation with the shortened max-age
ital0 Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions backend/src/auth/session-cookie-httponly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ const buildEmailDeps = (): AuthEmailDeps => ({
const parseSetCookie = (raw: string) => {
const [nameValue, ...attrParts] = raw.split(';').map((p) => p.trim())
const name = nameValue.split('=')[0]
const flags = new Set(attrParts.map((p) => p.split('=')[0].toLowerCase()))
return { name, flags }
const attributes = new Map(
attrParts.map((part) => {
const [key, value = ''] = part.split('=', 2)
return [key.toLowerCase(), value] as const
}),
)
return { name, attributes, flags: new Set(attributes.keys()) }
}

/** Find the session-token cookie among all Set-Cookie lines (tolerates the `__Secure-` prefix). */
Expand Down Expand Up @@ -86,7 +91,7 @@ describe('session cookie HttpOnly (CWE-1004 regression)', () => {
}
})

it('email-OTP sign-in issues an HttpOnly, SameSite session cookie (and no JS-readable __session cookie)', async () => {
it('email-OTP sign-in issues an HttpOnly session cookie', async () => {
const email = `httponly-otp-${crypto.randomUUID()}@example.com`
await db.insert(waitlist).values({ id: crypto.randomUUID(), email, status: 'approved' })

Expand Down
61 changes: 54 additions & 7 deletions backend/src/config/cors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { describe, expect, it } from 'bun:test'
import { createCorsMiddleware } from '@/config/cors'
import { getCorsOriginsList } from '@/config/settings'
import cors from '@elysiajs/cors'
import { Elysia } from 'elysia'

/**
* Integration tests for CORS middleware behavior.
* Verifies that the actual HTTP headers are set correctly for various origins.
*/
describe('CORS integration', () => {
const createTestApp = (corsOrigins: string[]) =>
const createTestApp = (corsOrigins: string[], corsAllowCredentials = true) =>
new Elysia()
.use(
cors({
origin: corsOrigins,
credentials: true,
methods: 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
allowedHeaders: 'Content-Type,Authorization',
createCorsMiddleware({
corsOrigins: corsOrigins.join(','),
corsAllowCredentials,
corsAllowMethods: 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
corsExposeHeaders: '',
}),
)
.get('/test', () => ({ ok: true }))
Expand All @@ -40,6 +40,7 @@ describe('CORS integration', () => {

expect(res.headers.get('access-control-allow-origin')).toBe('https://app.example.com')
expect(res.headers.get('access-control-allow-credentials')).toBe('true')
expect(res.headers.get('timing-allow-origin')).toBe('https://app.example.com')
})

it('should allow tauri://localhost', async () => {
Expand Down Expand Up @@ -75,6 +76,7 @@ describe('CORS integration', () => {
)

expect(res.headers.get('access-control-allow-origin')).toBeNull()
expect(res.headers.get('timing-allow-origin')).toBeNull()
})

it('should reject unknown origins', async () => {
Expand All @@ -101,6 +103,24 @@ describe('CORS integration', () => {
)

expect(res.headers.get('access-control-allow-origin')).toBeNull()
expect(res.headers.get('timing-allow-origin')).toBeNull()
})

it('should expose timing for preflight from an allowed origin', async () => {
const app = createTestApp(origins)
const res = await app.handle(
new Request('http://localhost/test', {
method: 'OPTIONS',
headers: {
Origin: 'https://app.example.com',
'Access-Control-Request-Method': 'DELETE',
},
}),
)

expect(res.headers.get('access-control-allow-origin')).toBe('https://app.example.com')
expect(res.headers.get('access-control-max-age')).toBe('600')
expect(res.headers.get('timing-allow-origin')).toBe('https://app.example.com')
})
})

Expand Down Expand Up @@ -131,4 +151,31 @@ describe('CORS integration', () => {
expect(res.headers.get('access-control-allow-origin')).toBeNull()
})
})

it('echoes the request origin for credentialed wildcard access', async () => {
const app = createTestApp(['*'])
const res = await app.handle(
new Request('http://localhost/test', {
headers: { Origin: 'https://app.example.com' },
}),
)

expect(res.headers.get('access-control-allow-origin')).toBe('https://app.example.com')
expect(res.headers.get('access-control-allow-credentials')).toBe('true')
expect(res.headers.get('timing-allow-origin')).toBe('https://app.example.com')
expect(res.headers.get('vary')).toContain('Origin')
})

it('uses wildcard access when credentials are disabled', async () => {
const app = createTestApp(['*'], false)
const res = await app.handle(
new Request('http://localhost/test', {
headers: { Origin: 'https://app.example.com' },
}),
)

expect(res.headers.get('access-control-allow-origin')).toBe('*')
expect(res.headers.get('access-control-allow-credentials')).toBeNull()
expect(res.headers.get('timing-allow-origin')).toBe('*')
})
})
65 changes: 65 additions & 0 deletions backend/src/config/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { getCorsOriginsList, isOriginAllowed, type Settings } from '@/config/settings'
import cors from '@elysiajs/cors'
import { Elysia } from 'elysia'

type CorsSettings = Pick<Settings, 'corsOrigins' | 'corsAllowCredentials' | 'corsAllowMethods' | 'corsExposeHeaders'>

/** Resolve a request origin using the configured exact-origin CORS policy. */
const resolveCorsOrigin = (
request: Request,
settings: Pick<Settings, 'corsOrigins'>,
allowsAnyOrigin: boolean,
allowCredentials: boolean,
): string | null => {
if (allowsAnyOrigin) {
// Browsers reject wildcard ACAO with credentials, so credentialed wildcard policies must echo Origin.
return allowCredentials ? request.headers.get('Origin') : '*'
}

const origin = request.headers.get('Origin')
if (origin === null || !isOriginAllowed(origin, settings)) {
return null
}
return origin
}

/** Configure CORS and grant Resource Timing access to the origin CORS resolved for the request. */
export const createCorsMiddleware = (settings: CorsSettings) => {
const corsOrigins = getCorsOriginsList(settings)
const allowsAnyOrigin = corsOrigins.includes('*')
Comment thread
ital0 marked this conversation as resolved.
const resolveRequestOrigin = (request: Request) =>
resolveCorsOrigin(request, settings, allowsAnyOrigin, settings.corsAllowCredentials)
const corsOrigin =
allowsAnyOrigin && !settings.corsAllowCredentials
? '*'
: (request: Request) => resolveRequestOrigin(request) !== null

return new Elysia({ name: 'cors-with-resource-timing' })
.onRequest(({ request, set }) => {
const allowedOrigin = resolveRequestOrigin(request)
if (allowedOrigin === null) {
return
}

// Resource Timing consumes TAO without CORS exposure; timing is revealed only to the CORS-allowed origin.
set.headers['Timing-Allow-Origin'] = allowedOrigin
})
.use(
cors({
origin: corsOrigin,
credentials: settings.corsAllowCredentials,
methods: settings.corsAllowMethods,
// Echo back the client's Access-Control-Request-Headers. The universal
// proxy forwards arbitrary upstream headers as X-Proxy-Passthrough-*.
allowedHeaders: true,
exposeHeaders: settings.corsExposeHeaders,
// Preflights cost ~195ms measured. 10 minutes covers back-to-back chat sends while keeping a
// CORS policy change from lingering in browser caches (Safari caps around this value anyway).
maxAge: 600,
}),
)
}
11 changes: 10 additions & 1 deletion backend/src/config/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('Config Settings', () => {
})

describe('CORS default security', () => {
const corsEnvKeys = ['CORS_ORIGINS'] as const
const corsEnvKeys = ['CORS_ORIGINS', 'CORS_EXPOSE_HEADERS'] as const

let savedEnv: Partial<Record<string, string | undefined>>

Expand Down Expand Up @@ -99,6 +99,15 @@ describe('Config Settings', () => {
expect(isOriginAllowed('http://localhost:1420', settings)).toBe(true)
})

it('should expose proxy and server timing to cross-origin clients by default', () => {
delete process.env.CORS_EXPOSE_HEADERS
const settings = getSettings()

expect(settings.corsExposeHeaders.split(',')).toContain('X-Proxy-Timing')
expect(settings.corsExposeHeaders.split(',')).toContain('Server-Timing')
expect(settings.corsExposeHeaders.split(',')).not.toContain('Timing-Allow-Origin')
})

it('should not match non-Tauri origins by default', () => {
delete process.env.CORS_ORIGINS
const settings = getSettings()
Expand Down
12 changes: 4 additions & 8 deletions backend/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { z } from 'zod'
const betterAuthTimeString = z.string().regex(/^\d+[smhd]$/, {
message: 'must be a Better Auth time string (digits followed by s, m, h, or d)',
})
const defaultCorsExposeHeaders =
'set-auth-token,X-Proxy-Final-Url,X-Proxy-Passthrough-Content-Type,X-Proxy-Passthrough-Mcp-Session-Id,X-Proxy-Passthrough-Mcp-Protocol-Version,X-Proxy-Passthrough-Location,X-Proxy-Passthrough-Anthropic-Version,WWW-Authenticate,Ehbp-Response-Nonce,X-Proxy-Timing,Server-Timing'

/**
* Settings schema for environment variables validation
Expand Down Expand Up @@ -102,11 +104,7 @@ const settingsSchema = z
corsAllowMethods: z.string().default('GET,POST,PUT,DELETE,PATCH,OPTIONS'),
corsAllowHeaders: z.string().default(''),
// Protocol-required: frontend proxy-fetch.ts unwrap needs these visible cross-origin (cors does not echo expose-headers).
corsExposeHeaders: z
.string()
.default(
'set-auth-token,X-Proxy-Final-Url,X-Proxy-Passthrough-Content-Type,X-Proxy-Passthrough-Mcp-Session-Id,X-Proxy-Passthrough-Mcp-Protocol-Version,X-Proxy-Passthrough-Location,X-Proxy-Passthrough-Anthropic-Version,WWW-Authenticate,Ehbp-Response-Nonce',
),
corsExposeHeaders: z.string().default(defaultCorsExposeHeaders),

// E2E encryption — when true, devices must complete the trust flow before syncing
e2eeEnabled: z.boolean().default(false),
Expand Down Expand Up @@ -218,9 +216,7 @@ const parseSettings = (): Settings => {
corsAllowCredentials: process.env.CORS_ALLOW_CREDENTIALS !== 'false',
corsAllowMethods: process.env.CORS_ALLOW_METHODS || 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
corsAllowHeaders: process.env.CORS_ALLOW_HEADERS || '',
corsExposeHeaders:
process.env.CORS_EXPOSE_HEADERS ||
'set-auth-token,X-Proxy-Final-Url,X-Proxy-Passthrough-Content-Type,X-Proxy-Passthrough-Mcp-Session-Id,X-Proxy-Passthrough-Mcp-Protocol-Version,X-Proxy-Passthrough-Location,X-Proxy-Passthrough-Anthropic-Version,WWW-Authenticate,Ehbp-Response-Nonce',
corsExposeHeaders: process.env.CORS_EXPOSE_HEADERS || defaultCorsExposeHeaders,
e2eeEnabled: process.env.E2EE_ENABLED === 'true',
minAppVersion: process.env.MIN_APP_VERSION || '',
swaggerEnabled: process.env.SWAGGER_ENABLED === 'true',
Expand Down
40 changes: 22 additions & 18 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createMicrosoftAuthRoutes } from '@/auth/microsoft'
import { createOidcConfigRoutes } from '@/auth/oidc'
import { createSsoDesktopCallbackRoutes } from '@/auth/sso-desktop-callback'
import { createLoggerMiddleware, createStandaloneLogger } from '@/config/logger'
import { createCorsMiddleware } from '@/config/cors'
import { getCorsOriginsList, getSettings } from '@/config/settings'
import { runMigrations } from '@/db/client'
import { createInferenceRoutes } from '@/inference/routes'
Expand All @@ -22,6 +23,7 @@ import { createSearchRoutes } from '@/api/search'
import { createPreviewRoutes } from '@/api/preview'
import { createPostHogRoutes } from '@/posthog/routes'
import { createProToolsRoutes } from '@/pro/routes'
import { createTinfoilKeepWarm } from '@/tinfoil/keep-warm'
import { createTinfoilRoutes } from '@/tinfoil/routes'
import { createWaitlistRoutes } from '@/waitlist/routes'
import { createAccountRoutes } from '@/api/account'
Expand All @@ -31,7 +33,6 @@ import { createConfigRoutes } from '@/api/config'
import { createEncryptionRoutes } from '@/api/encryption'
import { createPowerSyncRoutes } from '@/api/powersync'
import type { AppDeps } from '@/types'
import { cors } from '@elysiajs/cors'
import { Elysia } from 'elysia'

/**
Expand Down Expand Up @@ -83,31 +84,20 @@ export const createApp = async (deps?: AppDeps) => {
)
const auth = deps?.auth ?? createdAuth

const appLogger = createStandaloneLogger(settings)

// Build the production observability recorder unless tests injected their own.
// Proxy events go to Pino + OTel only — not PostHog (proxy traffic is infra
// plumbing, not product analytics).
const proxyObservability =
deps?.proxyObservability ??
createObservabilityRecorder({
logger: createStandaloneLogger(settings),
logger: appLogger,
})

return (
configuredApp
.use(
cors({
origin: getCorsOriginsList(settings),
credentials: settings.corsAllowCredentials,
methods: settings.corsAllowMethods,
// Echo back the client's Access-Control-Request-Headers. The universal
// proxy at /v1/proxy forwards arbitrary upstream headers as
// X-Proxy-Passthrough-* (provider SDKs add x-api-key, x-stainless-*,
// openai-organization, anthropic-beta, …). A static allowlist can't
// enumerate every upstream's header set without breaking preflight.
allowedHeaders: true,
exposeHeaders: settings.corsExposeHeaders,
}),
)
.use(createCorsMiddleware(settings))
.use(createLoggerMiddleware(settings))
.use(createHttpLoggingMiddleware(settings.trustedProxy))
.use(createErrorHandlingMiddleware())
Expand All @@ -129,7 +119,7 @@ export const createApp = async (deps?: AppDeps) => {
dnsLookup: deps?.dnsLookup,
}),
)
.use(createTinfoilRoutes({ auth, fetchFn, rateLimit: proRateLimit }))
.use(createTinfoilRoutes({ auth, fetchFn, logger: appLogger, rateLimit: proRateLimit }))
.use(
createUniversalProxyWsRoutes({
auth,
Expand All @@ -140,7 +130,14 @@ export const createApp = async (deps?: AppDeps) => {
)
.use(createSearchRoutes(auth, proRateLimit, { exaClient: deps?.searchExaClient }))
.use(createPreviewRoutes({ auth, fetchFn, rateLimit: proRateLimit, dnsLookup: deps?.dnsLookup }))
.use(createInferenceRoutes(auth, createInferenceRateLimit(database, rateLimitSettings)))
.use(
createInferenceRoutes({
auth,
fetchFn: deps?.fetchFn,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Convention — Inference gets raw deps?.fetchFn while every sibling gets the resolved fetchFn — load-bearing but undocumented

Every other route is handed the resolved fetchFn (deps?.fetchFn ?? globalThis.fetch), but inference is deliberately passed the raw deps?.fetchFn. That's intentional — the client-cache gate does if (client && !fetchFn) return cached, so passing a truthy globalThis.fetch in prod would silently disable client caching and rebuild the client every request. The problem is nothing says so, so a well-meaning refactor "normalizing" this to match the siblings would quietly regress performance. A one-line comment explaining why inference must receive undefined in prod would protect it.

logger: appLogger,
rateLimit: createInferenceRateLimit(database, rateLimitSettings),
}),
)
.use(createConfigRoutes(settings))
.use(createPostHogRoutes(fetchFn))
.use(
Expand All @@ -166,6 +163,7 @@ export const createApp = async (deps?: AppDeps) => {
const startServer = async () => {
const settings = getSettings()
const log = createStandaloneLogger(settings)
const tinfoilKeepWarm = createTinfoilKeepWarm(settings, { logger: log })

// Set up logging
log.info('Starting Thunderbolt Server...')
Expand Down Expand Up @@ -196,6 +194,9 @@ const startServer = async () => {
hostname,
port: settings.port,
reusePort: process.env.NODE_ENV === 'production',
// Must stay strictly above the ALB idle timeout (60s) — see deploy/pulumi; Bun cap is 255.
// If the server closes first, the ALB reuses a dead connection and returns 502.
idleTimeout: 120,
},
() => {
log.info(
Expand All @@ -206,6 +207,7 @@ const startServer = async () => {
},
'🦊 Elysia server started',
)
tinfoilKeepWarm.start()

if (settings.swaggerEnabled) {
log.info(
Expand All @@ -220,11 +222,13 @@ const startServer = async () => {

// Graceful shutdown
process.on('SIGINT', async () => {
tinfoilKeepWarm.stop()
log.info('Received SIGINT, shutting down gracefully...')
process.exit(0)
})

process.on('SIGTERM', async () => {
tinfoilKeepWarm.stop()
log.info('Received SIGTERM, shutting down gracefully...')
process.exit(0)
})
Expand Down
Loading
Loading