@antigenic-oss/paint 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
1
+ import { type NextRequest, NextResponse } from 'next/server'
2
+
3
+ /**
4
+ * Lightweight server-side proxy for the SW proxy.
5
+ * Fetches from the target localhost server and returns the raw response
6
+ * (no script stripping, no inspector injection — the SW handles all that).
7
+ * This exists because the SW runs in the browser and can't fetch cross-origin
8
+ * (localhost:3000) without CORS headers from the target.
9
+ */
10
+
11
+ const STRIP_HEADERS = new Set([
12
+ 'content-encoding',
13
+ 'transfer-encoding',
14
+ 'cross-origin-embedder-policy',
15
+ 'cross-origin-opener-policy',
16
+ 'cross-origin-resource-policy',
17
+ 'content-security-policy',
18
+ 'content-security-policy-report-only',
19
+ 'x-frame-options',
20
+ ])
21
+
22
+ async function handler(
23
+ request: NextRequest,
24
+ { params }: { params: Promise<{ path?: string[] }> },
25
+ ) {
26
+ const targetUrl = request.headers.get('x-sw-target')
27
+ if (!targetUrl) {
28
+ return new NextResponse('Missing x-sw-target header', { status: 400 })
29
+ }
30
+
31
+ // Validate localhost only
32
+ try {
33
+ const parsed = new URL(targetUrl)
34
+ if (
35
+ parsed.hostname !== 'localhost' &&
36
+ parsed.hostname !== '127.0.0.1'
37
+ ) {
38
+ return new NextResponse('Only localhost targets allowed', { status: 403 })
39
+ }
40
+ } catch {
41
+ return new NextResponse('Invalid target URL', { status: 400 })
42
+ }
43
+
44
+ const { path } = await params
45
+ const targetPath = path ? `/${path.join('/')}` : '/'
46
+ const targetOrigin = new URL(targetUrl).origin
47
+ const search = request.nextUrl.search
48
+ const fetchUrl = targetOrigin + targetPath + search
49
+
50
+ try {
51
+ // Forward all request headers to the target server. This ensures RSC
52
+ // headers (RSC, Next-Router-State-Tree, Next-Router-Prefetch), auth
53
+ // headers, Supabase headers, and any custom API headers reach the target.
54
+ // Only skip headers that must reflect the actual target or are internal.
55
+ const SKIP_REQUEST_HEADERS = new Set([
56
+ 'host',
57
+ 'origin',
58
+ 'referer',
59
+ 'connection',
60
+ 'x-sw-target',
61
+ 'x-forwarded-for',
62
+ 'x-forwarded-host',
63
+ 'x-forwarded-proto',
64
+ 'x-forwarded-port',
65
+ 'x-invoke-path',
66
+ 'x-invoke-query',
67
+ 'x-middleware-invoke',
68
+ 'x-middleware-prefetch',
69
+ ])
70
+ const forwardHeaders: Record<string, string> = {}
71
+ request.headers.forEach((value, key) => {
72
+ if (!SKIP_REQUEST_HEADERS.has(key.toLowerCase())) {
73
+ forwardHeaders[key] = value
74
+ }
75
+ })
76
+
77
+ const init: RequestInit = {
78
+ method: request.method,
79
+ headers: forwardHeaders,
80
+ redirect: 'manual',
81
+ }
82
+ // Forward body for non-GET/HEAD requests
83
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
84
+ init.body = request.body
85
+ // @ts-expect-error -- Node fetch supports duplex for streaming body
86
+ init.duplex = 'half'
87
+ }
88
+
89
+ // Follow redirects manually so we can track the final URL
90
+ let response = await fetch(fetchUrl, init)
91
+ let finalUrl: string | null = null
92
+ let redirectCount = 0
93
+ while (
94
+ redirectCount < 10 &&
95
+ response.status >= 300 &&
96
+ response.status < 400 &&
97
+ response.headers.get('location')
98
+ ) {
99
+ const location = response.headers.get('location')!
100
+ const redirectTo = new URL(location, fetchUrl).href
101
+ finalUrl = redirectTo
102
+ response = await fetch(redirectTo, {
103
+ headers: forwardHeaders,
104
+ redirect: 'manual',
105
+ })
106
+ redirectCount++
107
+ }
108
+
109
+ // Copy headers, stripping security headers
110
+ const responseHeaders = new Headers()
111
+ for (const [key, value] of response.headers.entries()) {
112
+ if (!STRIP_HEADERS.has(key.toLowerCase())) {
113
+ responseHeaders.set(key, value)
114
+ }
115
+ }
116
+ responseHeaders.delete('content-length')
117
+
118
+ // Forward set-cookie headers from the target (auth tokens, sessions)
119
+ const setCookies = response.headers.getSetCookie?.()
120
+ if (setCookies?.length) {
121
+ for (const sc of setCookies) {
122
+ responseHeaders.append('set-cookie', sc)
123
+ }
124
+ }
125
+
126
+ // If the target redirected, tell the SW the final URL so the navigation
127
+ // blocker can set the correct path (prevents hydration mismatch).
128
+ if (finalUrl) {
129
+ responseHeaders.set('x-sw-final-url', finalUrl)
130
+ }
131
+
132
+ return new NextResponse(response.body, {
133
+ status: response.status,
134
+ headers: responseHeaders,
135
+ })
136
+ } catch (err) {
137
+ return new NextResponse(
138
+ `Failed to fetch from ${fetchUrl}: ${err instanceof Error ? err.message : String(err)}`,
139
+ { status: 502 },
140
+ )
141
+ }
142
+ }
143
+
144
+ export const GET = handler
145
+ export const POST = handler
146
+ export const PUT = handler
147
+ export const PATCH = handler
148
+ export const DELETE = handler
149
+ export const OPTIONS = handler
@@ -6,7 +6,7 @@ const NAV_ITEMS = [
6
6
  { id: 'how-it-works', label: 'How It Works' },
7
7
  { id: 'use-cases', label: 'Use Cases' },
8
8
  { id: 'quick-start', label: 'Quick Start' },
9
- { id: 'framework-guides', label: 'Framework Guides' },
9
+ { id: 'framework-guides', label: 'Compatibility' },
10
10
  { id: 'troubleshooting', label: 'Troubleshooting' },
11
11
  { id: 'faq', label: 'FAQ' },
12
12
  ] as const