This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathproxy.ts
More file actions
134 lines (121 loc) · 5.31 KB
/
Copy pathproxy.ts
File metadata and controls
134 lines (121 loc) · 5.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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)$).*)',
],
}