@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.
- package/LICENSE +1 -1
- package/NOTICE +2 -2
- package/bin/paint.js +32 -0
- package/next.config.mjs +8 -0
- package/package.json +1 -1
- package/public/dev-editor-inspector.js +14 -0
- package/public/sw-proxy/sw.js +886 -0
- package/src/app/api/sw-fetch/[[...path]]/route.ts +149 -0
- package/src/app/docs/DocsClient.tsx +1 -1
- package/src/app/docs/page.tsx +112 -405
- package/src/app/page.tsx +2 -0
- package/src/components/ConnectModal.tsx +98 -181
- package/src/components/PreviewFrame.tsx +91 -0
- package/src/components/TargetSelector.tsx +49 -15
- package/src/components/left-panel/LayerNode.tsx +5 -2
- package/src/components/left-panel/LayersPanel.tsx +10 -1
- package/src/hooks/usePostMessage.ts +7 -1
- package/src/lib/serviceWorkerRegistration.ts +163 -0
- package/src/store/treeSlice.ts +29 -17
- package/src/store/uiSlice.ts +6 -0
|
@@ -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: '
|
|
9
|
+
{ id: 'framework-guides', label: 'Compatibility' },
|
|
10
10
|
{ id: 'troubleshooting', label: 'Troubleshooting' },
|
|
11
11
|
{ id: 'faq', label: 'FAQ' },
|
|
12
12
|
] as const
|