Files
accounted/proxy.ts
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

135 lines
5.3 KiB
TypeScript

import { NextResponse, type NextRequest } from 'next/server'
import { usesForbiddenWhiteLabelBackend } from '@/lib/domains/production-white-label-backend'
import { createLogger } from '@/lib/logger'
import { updateSession } from '@/lib/supabase/middleware'
const log = createLogger('proxy')
// Both guards below flag their record `alert: true`, and from here that flag
// pages nobody: the observability sink is only ever registered by lib/init.ts,
// which middleware must not call (it would drag the extension registry and the
// analytics client into the middleware bundle). The flag stays as the record's
// intent; the rule that turns one of these lines into an alert is configured on
// the hosting side, matching the stable `operation` field of the emitted JSON.
/**
* Empty, non-cacheable 503. The body stays empty on purpose: the deployment is
* already known to be wired wrong, so the response must not echo a hostname, a
* backend URL or a credential back to whoever asked.
*/
function serviceUnavailable(): NextResponse {
return new NextResponse(null, {
status: 503,
headers: {
'Cache-Control': 'no-store',
},
})
}
/**
* Whether a build-time public value never made it into the bundle: absent,
* empty, or still the `__NEXT_PUBLIC_*__` sentinel the Dockerfile bakes in and
* docker-entrypoint.sh substitutes at container start. Mirrors the check in
* lib/supabase/client.ts.
*
* Takes the value as an argument rather than reading process.env itself: an
* in-place comparison of a NEXT_PUBLIC_* var is constant-folded at build time
* and erases the sentinel the entrypoint needs to find. See
* lib/env/public-flags.ts.
*/
function isMissingPublicValue(value: string | undefined): boolean {
return !value || value.startsWith('__')
}
/**
* The Supabase browser env vars that updateSession needs, by name, or an empty
* list when the deployment is configured.
*
* Static property reads, never a `process.env[name]` loop: Next.js substitutes
* only literal references at build time, so a dynamic lookup would report a
* healthy deployment as unconfigured and 503 every request on every path.
*/
function missingSupabaseEnv(): string[] {
const missing: string[] = []
if (isMissingPublicValue(process.env.NEXT_PUBLIC_SUPABASE_URL)) {
missing.push('NEXT_PUBLIC_SUPABASE_URL')
}
if (isMissingPublicValue(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)) {
missing.push('NEXT_PUBLIC_SUPABASE_ANON_KEY')
}
return missing
}
export async function proxy(request: NextRequest) {
// Ahead of everything, because everything below needs a Supabase client.
// updateSession builds one with non-null assertions and @supabase/ssr throws
// synchronously when either value is falsy: that throw escapes the Web
// Handler and 500s every path the matcher covers, /login and /robots.txt
// included, with no log line to find it by. Preview deployments built
// without the vars stayed bricked that way for weeks.
const missingEnv = missingSupabaseEnv()
if (missingEnv.length > 0) {
log.error('Refused request without Supabase configuration', {
alert: true,
operation: 'supabase_env_missing',
requestHostname: request.nextUrl.hostname,
missing: missingEnv,
})
return serviceUnavailable()
}
if (
usesForbiddenWhiteLabelBackend(
request.nextUrl.hostname,
process.env.NEXT_PUBLIC_SUPABASE_URL,
)
) {
// Renamed from 'Blocked production white-label host from staging backend'.
// The guard asserts the production project rather than naming one
// forbidden project, so the staging project, a third project and a backend
// URL the build cannot parse all land here. `operation` is unchanged: it is
// what a hosting-side alert rule matches on.
log.error(
'Blocked production white-label host from a non-production backend',
{
alert: true,
operation: 'white_label_backend_guard',
requestHostname: request.nextUrl.hostname,
backendClassification: 'non_production',
},
)
return serviceUnavailable()
}
return await updateSession(request)
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - Static assets (images, scripts, manifest, icons, etc.)
*
* NOTE: `/api` is intentionally INCLUDED so the proxy can enforce the MFA
* (AAL2) gate on cookie-authenticated API calls (updateSession short-
* circuits API routes after that check: see lib/supabase/middleware.ts).
*
* `/rl` is the PostHog reverse-proxy prefix (rewrites in next.config.ts).
* It MUST be excluded: middleware runs BEFORE next.config rewrites, so
* without this updateSession() treats an ingestion POST as an unknown
* protected path and 307s it to /login. That silently kills analytics on
* every logged-out page and, because flags and asset loads still succeed
* through the rewrite, the integration looks healthy while no events
* arrive. Keep in sync with `api_host` in instrumentation-client.ts.
*/
'/((?!_next/static|_next/image|favicon.ico|\\.well-known|rl/|sw\\.js|sw-register\\.js|manifest\\.json|manifest\\.webmanifest|icons/|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|js|json)$).*)',
],
}