# v2 git bundle
b0dde3c88575899000eb0cf7ecd2a8b8dbf7c0c8 refs/heads/bundle
b0dde3c88575899000eb0cf7ecd2a8b8dbf7c0c8 HEAD

PACK      3x8tree e8b12528787c775ae8115766a40dd7953f7ecd5e
parent 4c30f2a6e19093cb5959ca2e903130b382eacc94
author MythEclipse <asepharyanalm001@gmail.com> 1778162363 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778162363 +0700

refactor: migrate to Next.js with edge functions and update relay utilities

- Removed old relay utilities and tests, replacing them with a new structure under `src/lib/relay-utils.ts`.
- Implemented a new edge function in `app/api/proxy/route.ts` to handle proxy requests.
- Created a new Next.js layout and page structure for the frontend, including a user interface for testing the proxy.
- Added Tailwind CSS for styling and configured PostCSS.
- Updated TypeScript configuration for better compatibility with Next.js.
- Removed unused files and configurations related to the previous setup.
4x$tree 824993a93716c538147fe6509a92887fceeadf6f
parent db175e6e95926095e3a884e3fd7ef03582a23407
author MythEclipse <asepharyanalm001@gmail.com> 1778161518 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778161518 +0700

chore: remove index.html and update vercel.json for API routing
u[x)tree 2240a2eeb9ef126a3177ee89e031aa12ef097f75
parent cc38b7c7c6f2ff06f960f3a3c3d1b04089c9a992
author MythEclipse <asepharyanalm001@gmail.com> 1778161389 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778161389 +0700

fix: update fetch options to include target URL in buildRelayRequest
;`^x!tree d3fe3ff0692f8bbe92488d02382f36464374f556
parent 9e2f4d39b1a8d3a73ab92dac2b0e243b49fbafc1
author MythEclipse <asepharyanalm001@gmail.com> 1778161030 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778161030 +0700

feat: add isAllowedTarget function to validate URL protocols
Z`[ˑxAtree dbe56af9bf4b4b47a7abfff8e4291030eacdf049
parent a6333750e4f0a7e37d876cdd0841d45d9d96bbe7
author MythEclipse <asepharyanalm001@gmail.com> 1778160918 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778160918 +0700

feat: implement API handler for edge runtime with method validation and target normalization
h'xtree 0dedabb72e23890df0dc0259e55409ba0858b817
parent 9c05003e7014ae4176f3161d336da42e48f44eb3
author MythEclipse <asepharyanalm001@gmail.com> 1778160411 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778160411 +0700

feat: update project structure and enhance UI for Edge Proxy Test

- Add .gitignore to exclude api directory
- Remove deprecated api/index.ts file
- Create new public/index.html with improved UI and functionality
- Update package.json build script to copy index.html to dist
- Enhance request handling in index.html with custom headers and presets
- Improve response display with status, time, and size metrics
~1xtree 800017aa5fc029cc11e0b5225f3c5b5dd3348c03
parent 7df78ad117b4bd2d910f55d936cde0fd69af8a1a
author MythEclipse <asepharyanalm001@gmail.com> 1778159727 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778159727 +0700

fix: remove .vercelignore file
@Pxtree 0dcc76fa1c63756dd35dab912c3b8800456a1c1e
parent c0bfce3d268036c71afbd608f3c2fa3611d68f8c
author MythEclipse <asepharyanalm001@gmail.com> 1778159683 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778159683 +0700

fix: remove rewrites configuration from vercel.json
zYxntree 3a0f90b39e148b9a943c44dfa6cfe223891176cc
parent 149a646feac130302b05ed8a603021155a53b6e6
author MythEclipse <asepharyanalm001@gmail.com> 1778159621 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778159621 +0700

Revert "feat: update build output directory to 'dist' in package.json and set outputDirectory in vercel.json"

This reverts commit cfbc4609b59325f7b13e91d62e0f8b94002cd4a4.
96xtree 344e642adea961c456748be7213af6ef6e204cc1
parent 9759d63b38f0d3fa886b383d0ea4475b3ab77429
author MythEclipse <asepharyanalm001@gmail.com> 1778159284 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778159284 +0700

fix: correct typo in comment for allowed methods
1Vx=tree f331a4037f67e251d73de011fbd71e15cddac4b2
parent 49f50b7aa89cb81c9c8f5aaab448c20e0d640a7a
author MythEclipse <asepharyanalm001@gmail.com> 1778159037 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778159037 +0700

feat: add index.html for Edge Proxy Relay interface and remove rewrites from vercel.json
\
fRxtree c31ce882efc5d91844656c0d89263c9522fa686f
parent e5de4bbbfbb4c69d6fd70df07fc7c8c047fb506c
author MythEclipse <asepharyanalm001@gmail.com> 1778158843 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778158843 +0700

fix: update fetch request URL to use '/api' endpoint
Xx&tree 89c66c04db9db94a94a43abd6a2034e1d2c43777
parent 6f28d18b955b3910603456a27f32e841125961d1
author MythEclipse <asepharyanalm001@gmail.com> 1778158774 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778158774 +0700

fix: update fetch API endpoint and adjust rewrites in vercel.json
[x*tree 9bdb89021677b80d33ff8ed704715a9e879c16a1
parent 1b86a33b4ebfe0c7975141209bf853a19caf453b
author MythEclipse <asepharyanalm001@gmail.com> 1778158539 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778158539 +0700

feat: refactor header handling and update module path in package.json
^Nxtree fa3ca0df2a6bc08c55865b1b04182f086bfd278e
parent 8e0cf9d0f366b6278f51f404c0e58d0aa7a6825e
author MythEclipse <asepharyanalm001@gmail.com> 1778158465 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778158465 +0700

fix: remove functions configuration from vercel.json
Yx?tree 132c208074e34e844dba3863c01b8ca912b6e4c0
parent 212ce324805a01280ca3b01a8462b7a9ca0538ad
author MythEclipse <asepharyanalm001@gmail.com> 1778158400 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778158400 +0700

fix: update import path for relay-utils and restore functions configuration in vercel.json
|^fNx0tree b260d37e9b556c24ebf22ccb576e25579d337ccf
parent d8e1e125b15af9903a433f22664717c411e9ec05
author MythEclipse <asepharyanalm001@gmail.com> 1778158192 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778158192 +0700

feat: implement Edge Proxy Relay with request handling and header filtering
W`xEtree aa4701c8ba846de84d57912fdfcc6dbea97368fa
parent 4cf3a520d50a941037e411582bd68ba5585880d9
author MythEclipse <asepharyanalm001@gmail.com> 1778157968 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778157968 +0700

feat: update vercel.json to restore build and install commands, and adjust routing configuration
4igx.tree 4e5fea38ffc53deb285a8d6a2c3cd0c3ec428a0d
parent cfbc4609b59325f7b13e91d62e0f8b94002cd4a4
author MythEclipse <asepharyanalm001@gmail.com> 1778157960 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778157960 +0700

feat: add index.html for Edge Proxy Relay interface with request handling
%aVx˹1@<D>di3@avgD/y}s`4#np72aiZ(dpP޺a(ΕD7ށ B_;h$0x:tree 42c57f376aacb019117a6896b6277660fe6ec45c
parent 098eabe5bf58ceded334c84aec21fad22bf01301
author MythEclipse <asepharyanalm001@gmail.com> 1778157571 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778157571 +0700

feat: simplify vercel.json by removing function configuration and updating bunVersion
$fVx4tree e53f17f47db7c4ed24b58311fd833b0fd145369f
parent c49c1d7441bd6114d8ac9b6524c68fae9ec2c4d5
author MythEclipse <asepharyanalm001@gmail.com> 1778157124 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778157124 +0700

feat: add 'api' directory to .gitignore and update build script in package.json
UVbx!tree 13bf95cf078fe41cbe5b712a0246749dfa3c0595
parent 42cf7a89c7dcdf6ba019fa3fc4761de52ffae2c9
author MythEclipse <asepharyanalm001@gmail.com> 1778156931 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778156931 +0700

feat: add schema and bunVersion to vercel.json configuration
\Ôx$tree 16c4490cdaad8731da37b00549f4ed0f837d8722
parent b4f5bad19d991fdf2a385f828552bbf0258539bc
author MythEclipse <asepharyanalm001@gmail.com> 1778156765 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778156765 +0700

feat: add function configuration for Bun runtime in vercel.json
CE\ʐx0tree d174693999c8b1caeac264668058f25af27f5bfd
parent 9448a9cc7adc4296dc00d77f324d062935b0ed89
author MythEclipse <asepharyanalm001@gmail.com> 1778156548 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778156548 +0700

feat: enhance request handling with method validation and target URL checks
`ߝx-tree c66bb43dfece634df9c2966d72f842f199e14fcc
parent 86ca59ad8ec27cdd1e3a830507fc57f77d444808
author MythEclipse <asepharyanalm001@gmail.com> 1778155980 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778155980 +0700

feat: add code-review-graph integration and update project configuration
Ya)xAtree 8706934b5a75bb33997b6198061e8d1bc871db58
parent a4fcbbcca92256fb8df5a160f30215a98ec82d81
author MythEclipse <asepharyanalm001@gmail.com> 1778155966 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778155966 +0700

refactor: restructure project by moving and renaming files, and implementing relay utilities
R(h!x .tree 2aa466ef7705afc32b53317c523af03857c85caf
author MythEclipse <asepharyanalm001@gmail.com> 1778154129 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1778154129 +0700

initial commit for proxy-bun
FB?#x31 ҔT@OW;&f&Icd2CDA/=$3=/(͆ M:q:8M.*c8зL}vh˚GVA980_||۾_wg{xCTQ/XQÃ%ʤfĂ;YQ\6iPjDRf~n*5b"~pyoxGC0Uyz9f)נv#<&3/%B$D!d?3cvZUZQR)GI}el~m˅]:ۡ*ӡK(i2r2-}4L]~qIrq1Ьb㵜j7Vޙ~bb팸C; Q\0kkŽW{.Vkn/<5$13$z}nJ7|T Q_Lb2׳F.VzJyT HxJ 100644 settings.json e\Glj'	f,b40000 skills @2{*PfHnb/x{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "code-review-graph update --skip-flows",
            "timeout": 30
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "code-review-graph status",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
evx K100644 debug-issue.md 8V6/DզK100644 explore-codebase.md zP6.$2!x100644 refactor-safely.md τBW់Է-NƩ100644 review-changes.md kUwpxxr+A.ICx?---
name: Debug Issue
description: Systematically debug issues using graph-powered code navigation
---

## Debug Issue

Use the knowledge graph to systematically trace and debug issues.

### Steps

1. Use `semantic_search_nodes` to find code related to the issue.
2. Use `query_graph` with `callers_of` and `callees_of` to trace call chains.
3. Use `get_flow` to see full execution paths through suspected areas.
4. Run `detect_changes` to check if recent changes caused the issue.
5. Use `get_impact_radius` on suspected files to see what else is affected.

### Tips

- Check both callers and callees to understand the full context.
- Look at affected flows to find the entry point that triggers the bug.
- Recent changes are the most common source of new issues.

## Token Efficiency Rules
- ALWAYS start with `get_minimal_context(task="<your task>")` before any other graph tool.
- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
0KxJ---
name: Explore Codebase
description: Navigate and understand codebase structure using the knowledge graph
---

## Explore Codebase

Use the code-review-graph MCP tools to explore and understand the codebase.

### Steps

1. Run `list_graph_stats` to see overall codebase metrics.
2. Run `get_architecture_overview` for high-level community structure.
3. Use `list_communities` to find major modules, then `get_community` for details.
4. Use `semantic_search_nodes` to find specific functions or classes.
5. Use `query_graph` with patterns like `callers_of`, `callees_of`, `imports_of` to trace relationships.
6. Use `list_flows` and `get_flow` to understand execution paths.

### Tips

- Start broad (stats, architecture) then narrow down to specific areas.
- Use `children_of` on a file to see all its functions and classes.
- Use `find_large_functions` to identify complex code.

## Token Efficiency Rules
- ALWAYS start with `get_minimal_context(task="<your task>")` before any other graph tool.
- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
JxR---
name: Refactor Safely
description: Plan and execute safe refactoring using dependency analysis
---

## Refactor Safely

Use the knowledge graph to plan and execute refactoring with confidence.

### Steps

1. Use `refactor_tool` with mode="suggest" for community-driven refactoring suggestions.
2. Use `refactor_tool` with mode="dead_code" to find unreferenced code.
3. For renames, use `refactor_tool` with mode="rename" to preview all affected locations.
4. Use `apply_refactor_tool` with the refactor_id to apply renames.
5. After changes, run `detect_changes` to verify the refactoring impact.

### Safety Checks

- Always preview before applying (rename mode gives you an edit list).
- Check `get_impact_radius` before major refactors.
- Use `get_affected_flows` to ensure no critical paths are broken.
- Run `find_large_functions` to identify decomposition targets.

## Token Efficiency Rules
- ALWAYS start with `get_minimal_context(task="<your task>")` before any other graph tool.
- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
RBx+---
name: Review Changes
description: Perform a structured code review using change detection and impact
---

## Review Changes

Perform a thorough, risk-aware code review using the knowledge graph.

### Steps

1. Run `detect_changes` to get risk-scored change analysis.
2. Run `get_affected_flows` to find impacted execution paths.
3. For each high-risk function, run `query_graph` with pattern="tests_for" to check test coverage.
4. Run `get_impact_radius` to understand the blast radius.
5. For any untested changes, suggest specific test cases.

### Output Format

Provide findings grouped by risk level (high/medium/low) with:
- What changed and why it matters
- Test coverage status
- Suggested improvements
- Overall merge recommendation

## Token Efficiency Rules
- ALWAYS start with `get_minimal_context(task="<your task>")` before any other graph tool.
- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
7zxmPMK1ϯzPA<(dt&$N *2o>ޛL'6ĥ7H"*W7Bb: j_owwop.M"Fer5f;؀3FIct+NYfnҶDD`[~iWL
s?V|$=n6g_3Hlh?v*x吻3n.t|jxɼyF"F I
x P{
  "mcpServers": {
    "code-review-graph": {
      "command": "uvx",
      "args": [
        "code-review-graph",
        "serve"
      ],
      "type": "stdio"
    }
  }
}
8I,xmVmo6_qS)l	žu]]q%:f"
I6=GJ$@"~]Ob6%1ME2V9-}UL3J6/dȗT`	JSbl6ر{aGCM#v,s'k73ĕiv	G
9- TWz3T9)&ӲiĘګ~\9ЂJoŒ*˴_{	ZicUZ#*EzQ'GT~pGFG1H4u5]lV*LzYM	IES3Zc!y(P鄶|Ga3cȥR8ʓ{n{e>mZ>,8ٙF{zVho|gϷ&97Q+F(|~L
38,y_ЍAp3hS^'mv&\"~CSГcBi-2KϴժSτCwDQ(+]&tZȪ3~ayxU_[6v7F(uxld+T6+ib튵I}ކŞ"_82Ϲ(ù1<&<~m=}A?WbM*#&W}('O$9:ii"5<{1JUa.?owۛo{|^s?jp6
ekӈ`YP%r(
UؕzNV7Պ,B<=}|8lj!D aJh4-K.5`p`ι/J?(YǨ7Y+Yi6vp߯ȳpDʏKb~9"Ƌ'@X˰TyS.mۓp-c9ܧv'0!ak?HV;~s]3fJ?|2XMxf*㬐< [%spwy?EK$WY8p 6<cgT7X?0pv	0ma	=Yt/މjt?`vlW @s2Wl+b@3_71&fz]T=zox!	
<!-- code-review-graph MCP tools -->
## MCP Tools: code-review-graph

**IMPORTANT: This project has a knowledge graph. ALWAYS use the
code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore
the codebase.** The graph is faster, cheaper (fewer tokens), and gives
you structural context (callers, dependents, test coverage) that file
scanning cannot.

### When to use graph tools FIRST

- **Exploring code**: `semantic_search_nodes` or `query_graph` instead of Grep
- **Understanding impact**: `get_impact_radius` instead of manually tracing imports
- **Code review**: `detect_changes` + `get_review_context` instead of reading entire files
- **Finding relationships**: `query_graph` with callers_of/callees_of/imports_of/tests_for
- **Architecture questions**: `get_architecture_overview` + `list_communities`

Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need.

### Key Tools

| Tool | Use when |
|------|----------|
| `detect_changes` | Reviewing code changes — gives risk-scored analysis |
| `get_review_context` | Need source snippets for review — token-efficient |
| `get_impact_radius` | Understanding blast radius of a change |
| `get_affected_flows` | Finding which execution paths are impacted |
| `query_graph` | Tracing callers, callees, imports, tests, dependencies |
| `semantic_search_nodes` | Finding functions/classes by name or keyword |
| `get_architecture_overview` | Understanding high-level codebase structure |
| `refactor_tool` | Planning renames, finding dead code |

### Workflow

1. The graph auto-updates on file changes (via hooks).
2. Use `detect_changes` for code review.
3. Usie `get_affected_flows` to understand impact.
4. Use `query_graph` pattern="tests_for" to check coverage.
ykxT͎6)A-;z-Y;qlkzAcil1H?u}=$4-o15xbn3^9_d
jzɖfBpa5wkQ`)wF%9q^*=œ쬄8etm!L:shr,3|$O%Iu
J]wW>c);1OWZ9]	m%ưٰrI50gQx@eՋhha$<jo2bWZcGfubTi؎3b'gmЬ~OfޒO6V ҳ#\K+5kZj=&mAvͧW9ӡv<ۨܥW۫u8,WVU/I]aH5a&Us3똉\ӎ^_єTƒF('*mY#}rvxh,2Fa>Y툂rѵp/z$F~x>9NX_;o.)qԳח?SYqU(0%N663'!8$A`,d]J5;%}feSk˶Pڴh3$4a.G;o]`A"a"OzJh藊,x*'%Jqx\WmxV`@\weSa2`CC?a^Ao.:'W(>k!ଂ*H%S{މh3nz:gqLĒI*EY\/"?Cx p40000 api  Ok
S	/ۖ<100644 globals.css g6ϻ"100644 layout.tsx H
;^C85nĵp~#;100644 page.tsx }'SMެ 	Lu8x  40000 proxy b+ɜkv[A8#÷x$ 100644 route.ts 4`Jr՝=v:1N)#x;export const config = { runtime: "edge" };

// Allowlist: only these headers are forwarded to prevent leaking
// Vercel internal metadata, credentials, and infrastructure info
const ALLOWED_HEADERS = new Set([
	"content-type",
	"accept",
	"accept-encoding",
	"accept-language",
	"user-agent",
	"referer",
	"origin",
	"authorization",
	"proxy-authorization",
	"cache-control",
]);

// Blocklist: sensitive headers that should NEVER be forwarded
const BLOCKED_HEADERS = new Set([
	"x-vercel-id",
	"x-vercel-deployment-url",
	"x-vercel-oidc-token",
	"x-vercel-oidc-token-ts",
	"x-vercel-signature",
	"x-vercel-edgified",
	"x-vercel-ip-city",
	"x-vercel-ip-country",
	"x-vercel-ip-country-region",
	"x-vercel-ip-latency",
	"x-vercel-deployment-config",
	"x-vercel-rewritten-query",
	"cf-ray",
	"cf-connecting-ip",
	"cf-ipcountry",
	"cf-ray-id",
	"x-forwarded-for",
	"x-forwarded-host",
	"x-forwarded-proto",
	"forwarded",
	"cookie",
	"set-cookie",
	"x-real-ip",
	"x-cluster-client-ip",
	"x-api-key",
	"x-cache",
]);

const INTERNAL_HEADERS = new Set(["x-relay-target", "x-relay-path", "host"]);

export function isAllowedTarget(url: string): boolean {
	try {
		const parsed = new URL(url);
		return ["http:", "https:"].includes(parsed.protocol);
	} catch {
		return false;
	}
}

export function normalizeTargetUrl(target: string | null, relayPath: string): string | null {
	if (!target) return null;
	return target.replace(/\/$/, "") + relayPath;
}

export function filterHeaders(headers: Headers): Headers {
	const filtered = new Headers();
	for (const [key, value] of headers.entries()) {
		const lowerKey = key.toLowerCase();
		if (INTERNAL_HEADERS.has(lowerKey)) continue;
		if (BLOCKED_HEADERS.has(lowerKey)) continue;
		if (lowerKey.startsWith("x-vercel-")) continue;
		if (lowerKey.startsWith("cf-")) continue;
		if (lowerKey.startsWith("x-forwarded-")) continue;
		if (ALLOWED_HEADERS.has(lowerKey)) {
			filtered.set(key, value);
			continue;
		}
		filtered.set(key, value);
	}
	return filtered;
}

export function shouldSendBody(method: string): boolean {
	return method !== "GET" && method !== "HEAD";
}

const ALLOWED_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);

export default async function handler(req: Request): Promise<Response> {
	if (!ALLOWED_METHODS.has(req.method)) {
		return new Response(JSON.stringify({ error: "Method not allowed" }), {
			status: 405,
			headers: { "content-type": "application/json" },
		});
	}

	const target = req.headers.get("x-relay-target");
	const relayPath = req.headers.get("x-relay-path") || "/";
	const targetUrl = normalizeTargetUrl(target, relayPath);

	if (!targetUrl) {
		return new Response(
			JSON.stringify({ error: "Missing x-relay-target header" }),
			{ status: 400, headers: { "content-type": "application/json" } },
		);
	}

	if (!isAllowedTarget(targetUrl)) {
		return new Response(
			JSON.stringify({ error: "Target domain not allowed" }),
			{ status: 403, headers: { "content-type": "application/json" } },
		);
	}

	const headers = filterHeaders(new Headers(req.headers));
	const fetchOptions: RequestInit = {
		method: req.method,
		headers,
		body: shouldSendBody(req.method) ? req.body : undefined,
		duplex: "half",
	};

	const response = await fetch(targetUrl, fetchOptions);
	return new Response(response.body, {
		status: response.status,
		headers: response.headers,
	});
}NIx->3 D343'%(5'2(4D3(5$*X\W
/MɬJI,JO-	-
eUz&2srSS 
ujҊs@Ju9KK2s&lv8YzrW'$[)&Og3'jCWd ,ep))*`xU(PG!psxx
д :"f&#gxoB~Nf
YҼT[ԔT+JK2C4RRKSK4RKJ3K2RrR@4jR+
Jqw%ΘzR9.>!XL'٨ gjE
x&.!5g3s$L>Ē XQx; @tailwind base;
@tailwind components;
@tailwind utilities;
xMj0~EJ|wzkJIwEZuX{%%,|4/8.\,a\M8Ճ4Ozk1Y0ͳ[l@FFL^il|
Mi~GGaB)V.oPdC&.5YN|'j98#E]|'mo"<:3y?}zxXms۸_r2Gj$IkřXOf3I KwB|^/N	҄yyrdet_,:rBjJLVLZy$Ko'E4D|`)D,G4{FOP~IPq$_;߶\,ZGI3wjZ뫷KV;OPF8k2A%IS]p	zv眪Y"ygQANｑ:LViCpLOD+tYv8iI5}d#ح)8xIe)1xAz6YXPЈpswWA}^_X\.ˋ_ջ_^;@wyp<6J!K+O(͑maej	zO",T9he#n}FR5i	j[MyEs_
\\iaq&
y5hpCܚ<h@řſ檐* vdd <FӮѦSJ ~<J˘pOIk7
0
YS+'-30Ę!.tv< -!_;60%jEJn[N+y` K<Brc~U	goէv9ˍhռ&@?$M@G[ル/ءZmmE$O: lnSs$@Q`;
4cQigLkv3G	}7:d䝴;H.E"5CAaL؆ǭt/*bs}*qm[ FDri|zsgddIo|1Br\<S)214ֿϷ)ʶ>.$^85>9wF-_Iy+%Mc#n矁l鏝y4<=Z5#3$ǘ?!pɚ;yƮj_0F<&*9 	1<mV
u#<Pa- 9>ۀ3g+1lJڮc*%[$h)mI-/4n`Ð:o'ɋR뽿sf1&]4ϵNͩNZ/UH9͉QDūپ.%q|S<]'{$%f^v P603\8mOocCXdn*RIX 0hLfaA32\x%K^'e);HپЫ[⺡]2lb?@'%<
]L >HѨҼ'A*pP*((h*ai=Ж2j4`r?ػ7WūϪaN)Fe]7o&g&&4YAOh6	&NXҟ&zK@0Q:$Rkl},ɁÑI+{lgIyAe`&&#aFp˛^|YGPd VCz(97!{vTd2שvꝯXDv7T dբ򶯯نG?~8;^ uJ"?z@R3)sAbP2v[Ѧ6[Q*^b))'BL"d:[cy7T#P8 A@͡tjtw=3*H5P,}0]
0$%4ˇn9Lk2ω64/Kh>CCG9@Z˗6U!.R}P\.:o|{NDy(wx_AdODHdP*[8J!:x8o[ӹJ=z^jT	(ڥuzy|W{c	dop/j驿x{
	"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
	"vcs": {
		"enabled": true,
		"clientKind": "git",
		"useIgnoreFile": true
	},
	"files": {
		"ignoreUnknown": false
	},
	"formatter": {
		"enabled": true,
		"indentStyle": "tab"
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	},
	"javascript": {
		"formatter": {
			"quoteStyle": "double"
		}
	},
	"assist": {
		"enabled": true,
		"actions": {
			"source": {
				"organizeImports": "on"
			}
		}
	}
}
`W	x|sȖWtٷ1/$$V!$t@;?.-ٲ}{+<JgI2?=Q~UGyͳ 
{:Յ=ONsGQ+O~e?̍w<OC?9UcĻ^~477_}Dy5Dvco_tߌ&iE#nގmD_=w["F;J(^(3_OqߪXgG^4gɵl&OϮΜy{#S$˩PL:;/?|3+ZemI__O$a>eAaêbWlʒCǱ4DWӧ6=Vbp5j/xXIu\Y|=F-ǎB:ta5ExiWco|uQ&b
d}9g0u`*䕢x.rĿ4|h>
L̒Gq6_m#`Yp&8.d}j[n/qS`M5TYYljpѷ 4s*nހDr=+;I4+,n?PRXխp٣#O8FC{jTIm|%w_FN LYjꞳ~ȅCi]9KmXo:%".f9-RS`k#ր|VA0_ ~}˵M\b&L6Ct3iGpϖL&{{G2:Čm	56shV<Jvw^F򩆆O?טOUznnڽ^8SqY'q\O sLЍ{̔67oY	Ƕбڹ7oydc\jb+ZSi !H$!Mv2҉buʷ7F<:Ì+<Zob֌!A
/Ye<] 		=ۉq/a6m6ecX?ZAE|NHpO3͚%!컇yux΋$r~'eULN1h&u.tVE"#:_`  k"zQgIau_^t$ogG_=('st/ܑ6؍É`;D1Ẹc`{-J?;LiNFs_}mUAlxCgwQQIQQClbk ~7\!˝=b8P:PXtIoQ&#d"+\,MH
zkv,ALTo@kl]sބr^T#-,F/Ul<)ȡBBn<*8-qW4ϸtp*8'vO*Af"6GOXqV(H|Dgoy8$X=t(=7]k0:dM'hu%zL}DPh9`" fInphA΋wue9++\[~|4E빞wa32@jy3ylz]Z\\ɁYTC%3kY޻<n|Aͻ8U5|lf@M}ީiFRHL.HB#mOɩj=]WОHyЖrۭXZ+"1 Ժ1J4ř
M󣩁$G!rzȮjDuV\ڦ֪v",tp-6is)[g,-+.4-Fvh@NJ׎\Z!;Fih=Z^M&5nJc@Zؗ'",7ot:mIY^еV~p'~kCIuʹ43΄bδe1+syVq'.)WjǚsigZW-~rm?<߮V&`3JtN8Ю4Ul+ٹ*I4\Ƒߞ*V/'^ѻV!~?@*ى0f&砕bw.]>Xb&Y"a9dk;-X^ہf@{	dS!IeMazS;=zwϬwˢܟ&5a ೸Vdvm=_#צd[tN0mA';XKf݇Fb;Ɵ2@AP`ۈEz!"Xe}n7kDE>Ӕk4jZ0l{w"ðCWu̵ yj${i}*\*!em0a	9h׺aTXe{;@}v߆ۿA,Z07B+mMZ1'AɽD!I]#^Q"!yܴ|دo;m<18?Q(Ѓivf~WS3e a*`f!ߧG[,||,^ [eWaFf'6MIuk+5-Z]!+<88^*EӮ^P}&҃˴/w?%VͶB\n5M½vj,,3[lgtf'][!yO-vj6DWF~eml9QYGc1Z''Ҫmñ|6)4y5"/DrʣnYMo)\ X'|G>]~6س~2ח'gSATu8{;ip'71,ie)G+dE=YrM:c{m(pֿMn(s9 J9!x'8֤CqtZ훴%hkNn^3>脻Y멁Mݓ@eGiˌNArt-xXy tR~pZkK<^KxK[HA(f0߅ρ|_]jЦ\&K#kϫm?}[{B-O敌X68":1YY0S~[ q5/Mg3/%fnwwPGe9Ѫ<[Zk,Ve.z;͎A,@H48T{`á+fΙPU%0rmY5%ňͰZllK%s֘aRU6)U^nAL|	-%`塲-Y]NݤcrҢ w:Ǆ^' S\ށB&0~
@_GSwImZ d>B3UQ{"BӍV}Z!6{KЩj:IῌI-նg:TrD=-1(Na8n시u`B'U`3\Bx@TNRD,#ɒr^oiG˕Ռ^bӾ>#u5[(XXqkᎲ%;V( /1|0:r*K,73{F)Ӑj4vg!ȨΜlJEH?b#[v(Ǡ	8(E7M֝{z#y rXf!0
:ϖ9*¢ 5堍Biz/yy t2=|h\k(k7䂫Sqסm5kb8gUsrmXOzK^N2Chee8?
3Ի݂[9p0pˌ0wj-	EG,71vPCvL)bfO+=%o9>iw~sD~?ㄛ<Ze(do.LA:Y09 ֌`(KIжgS~6KI&p{[ {#>?lf^F y )&m	Ff֛ɛz<6*4	Wn.-lMʥ8l )ܥu~uh'>\'g;{^vxKirN*!I|$*}烨YOEϵvU:xWS~T<>Fy}4Eߥ^Q--a5FHŖK'}Wp[cs`lAZ< \k3dD<X](%]Q>=x!_zN|?OܹgE\l54F,`r(T\Mal-	p_KdGz	*ZYG.8 Eؼ2Wki ID'Tٱ?cQyHVgK_o8B<g7xbC^-G?F*L3/c^Zu 4
f_ARh<!J0AkMxD^;xgX_O?u (Rh)4mN&of'P؊lmQ85.`9Ix^W=&? |"^_XΕF.:_K7uI֬e`ܲS0rÈ&t+5ҰH-cђN&6,`)IE|8+ϚyܽzhW\:P'¨L:C2!\dɎca.uܲi[@ eDV]Tܠx/˞Ӟ7GqfME5F}JkW'M)XjV}QE	٨pf%aZ)G`QSt5Q{piAqJ܉ol4aafcA-{E)~v2`M!V=lsk)0-abk'ҸG{~-FdFBbS.B:WTHԴO"d*5E7%hD3WpQ?:}^Ї+ M$jVR'?6&h/s͏b5{@+</4g2LX\3<z~<K刯oҧ Y3zˁ(]%	S^-Q82_+WgcUGt͡$:Uh$zxzxlDx%J&_D* ~p~=QwHfL|g\?}8:}{ֶ[4}H~KK2L6|1%'tÇTOO3Iy{JA~O^H챯؀O׻~\vgY<-97G7WըG:+'(	`0a>e0Cm/9M]Gz?\{J^\cݧO%bmr
WIb_|n5W[eX~kFzrHc
<}XT9S
 եӸJ.{C{Ltp8`T*MWFKzIAnv=}حLG,#/	ytHp?.7OsH$*u8(I{u^~n_:P";ʮAj^lmO"|
#g5b?֟9]0@ٱ͖%nipδpz`;])z1\58J7sX \f2zxqO9J4 u=v#yMmt?%,t),
ɐ<<,R^DC({5M#.l}]q$O/7[߱Rj]	ZGNf)M1HjGF!uA{g>Qqh^͘~ Z^
cz/ںb#ݣﵿ㟏%.^ZTT2k#ҐWLmք1:*9:}kna,1)GȐRwm+j:2ں{0*[8@;DKr|hxVQNv	)v `PIS`Dk?^yz'ȷ~ߔO)s}-9>$u͂ꧾ
ȓ(s+
ft֣]f;,ILʬf䀨2EXrM^Bw>_\yN}Y=m<BX|eRȕWn]1_S~p~yu3w>#:wt+O~#:'4	}B[n|n$g>rc~'4c|FrǥRZǗmݹs4ڕ̝R:co/tb*ڵTSbQݢ<Gꄱ8%ͥH.{._;x^xQ7!05iJvRsURݴ1w).5(] ~E^:зEÛs'sQE7Ȼnx.-zSgRq:9CZR@X7ۚy,9`p7`AF
\3,N\KJoie%2`Z1޶ﱷgpŢ
^yĞڙ1Vae{ QM DEG4sby44%W>=<??~bra:{NS%ϊ̂T̘#,s6!0p^\*lfwE=@䢨lpq}3Way4_!&^$*;Pr5V`SqAv	R[ ~SЭ37wn?obo_	u'!u_Jiw J%\9rŶ15aV$7{`;0`cңlno-,3stǓDa˦76Cl$|P+DNt 2:KLsI^1`)AW֬5I,M.kئ6`g1VRWǿ7=xEimport { describe, expect, test } from "bun:test";
import {
	buildRelayRequest,
	createRelayResponse,
	normalizeTargetUrl,
	shouldSendBody,
	stripRelayHeaders,
} from "~/relay-utils";

describe("normalizeTargetUrl", () => {
	test("returns null when target is null", () => {
		expect(normalizeTargetUrl(null, "/")).toBeNull();
	});

	test("returns null when target is empty string", () => {
		expect(normalizeTargetUrl("", "/")).toBeNull();
	});

	test("removes trailing slash from target", () => {
		expect(normalizeTargetUrl("https://httpbin.org/", "/get")).toBe(
			"https://httpbin.org/get",
		);
	});

	test("keeps target without trailing slash", () => {
		expect(normalizeTargetUrl("https://httpbin.org", "/get")).toBe(
			"https://httpbin.org/get",
		);
	});

	test("appends relay path", () => {
		expect(normalizeTargetUrl("https://example.com", "/api/users")).toBe(
			"https://example.com/api/users",
		);
	});

	test("handles nested paths", () => {
		expect(
			normalizeTargetUrl("https://api.example.com", "/v1/users/123/profile"),
		).toBe("https://api.example.com/v1/users/123/profile");
	});
});

describe("stripRelayHeaders", () => {
	test("removes x-relay-target header", () => {
		const headers = new Headers({ "x-relay-target": "https://test.com" });
		const stripped = stripRelayHeaders(headers);
		expect(stripped.get("x-relay-target")).toBeNull();
	});

	test("removes x-relay-path header", () => {
		const headers = new Headers({ "x-relay-path": "/test" });
		const stripped = stripRelayHeaders(headers);
		expect(stripped.get("x-relay-path")).toBeNull();
	});

	test("removes host header", () => {
		const headers = new Headers({ host: "localhost:3000" });
		const stripped = stripRelayHeaders(headers);
		expect(stripped.get("host")).toBeNull();
	});

	test("preserves other headers", () => {
		const headers = new Headers({
			"x-relay-target": "https://test.com",
			"x-custom-header": "value",
			authorization: "Bearer token",
		});
		const stripped = stripRelayHeaders(headers);
		expect(stripped.get("x-custom-header")).toBe("value");
		expect(stripped.get("authorization")).toBe("Bearer token");
	});

	test("handles multiple relay headers", () => {
		const headers = new Headers({
			"x-relay-target": "https://test.com",
			"x-relay-path": "/path",
			host: "test.com",
			"content-type": "application/json",
		});
		const stripped = stripRelayHeaders(headers);
		expect(stripped.get("content-type")).toBe("application/json");
		expect(stripped.get("x-relay-target")).toBeNull();
		expect(stripped.get("x-relay-path")).toBeNull();
		expect(stripped.get("host")).toBeNull();
	});
});

describe("shouldSendBody", () => {
	test("returns false for GET", () => {
		expect(shouldSendBody("GET")).toBe(false);
	});

	test("returns false for HEAD", () => {
		expect(shouldSendBody("HEAD")).toBe(false);
	});

	test("returns true for POST", () => {
		expect(shouldSendBody("POST")).toBe(true);
	});

	test("returns true for PUT", () => {
		expect(shouldSendBody("PUT")).toBe(true);
	});

	test("returns true for PATCH", () => {
		expect(shouldSendBody("PATCH")).toBe(true);
	});

	test("returns true for DELETE", () => {
		expect(shouldSendBody("DELETE")).toBe(true);
	});

	test("is case-sensitive", () => {
		expect(shouldSendBody("get")).toBe(true);
		expect(shouldSendBody("post")).toBe(true);
	});
});

describe("buildRelayRequest", () => {
	test("sets correct method", () => {
		const req = new Request("http://test.com", { method: "POST" });
		const headers = new Headers();
		const result = buildRelayRequest(req, headers);
		expect(result.method).toBe("POST");
	});

	test("sets headers", () => {
		const req = new Request("http://test.com");
		const headers = new Headers({ "x-custom": "value" });
		const result = buildRelayRequest(req, headers);
		expect(result.headers).toBe(headers);
	});

	test("omits body for GET", () => {
		const req = new Request("http://test.com", { method: "GET" });
		const headers = new Headers();
		const result = buildRelayRequest(req, headers);
		expect(result.body).toBeUndefined();
	});

	test("omits body for HEAD", () => {
		const req = new Request("http://test.com", { method: "HEAD" });
		const headers = new Headers();
		const result = buildRelayRequest(req, headers);
		expect(result.body).toBeUndefined();
	});

	test("includes body for POST", () => {
		const body = JSON.stringify({ test: true });
		const req = new Request("http://test.com", { method: "POST", body });
		const headers = new Headers();
		const result = buildRelayRequest(req, headers);
		expect(result.body).toBeDefined();
		expect(result.duplex).toBe("half");
	});

	test("includes body for PUT", () => {
		const body = JSON.stringify({ update: true });
		const req = new Request("http://test.com", { method: "PUT", body });
		const headers = new Headers();
		const result = buildRelayRequest(req, headers);
		expect(result.body).toBeDefined();
	});

	test("includes body for PATCH", () => {
		const body = JSON.stringify({ patch: true });
		const req = new Request("http://test.com", { method: "PATCH", body });
		const headers = new Headers();
		const result = buildRelayRequest(req, headers);
		expect(result.body).toBeDefined();
	});

	test("includes body for DELETE", () => {
		const body = JSON.stringify({ delete: true });
		const req = new Request("http://test.com", { method: "DELETE", body });
		const headers = new Headers();
		const result = buildRelayRequest(req, headers);
		expect(result.body).toBeDefined();
	});
});

describe("createRelayResponse", () => {
	test("preserves status", async () => {
		const mockResponse = new Response("body", { status: 418 });
		const result = createRelayResponse(mockResponse);
		expect(result.status).toBe(418);
	});

	test("preserves headers", async () => {
		const mockResponse = new Response("body", {
			headers: { "x-custom": "value" },
		});
		const result = createRelayResponse(mockResponse);
		expect(result.headers.get("x-custom")).toBe("value");
	});

	test("preserves body", async () => {
		const mockResponse = new Response("test body content");
		const result = createRelayResponse(mockResponse);
		expect(await result.text()).toBe("test body content");
	});

	test("handles different status codes", async () => {
		const mockResponse = new Response(null, { status: 404 });
		const result = createRelayResponse(mockResponse);
		expect(result.status).toBe(404);
	});

	test("handles 500 status", async () => {
		const mockResponse = new Response("error", { status: 500 });
		const result = createRelayResponse(mockResponse);
		expect(result.status).toBe(500);
	});
});

describe("integration: full relay flow", () => {
	test("complete flow with mocked fetch", async () => {
		const target = "https://httpbin.org";
		const relayPath = "/get";
		const targetUrl = normalizeTargetUrl(target, relayPath);

		expect(targetUrl).toBe("https://httpbin.org/get");

		const originalHeaders = new Headers({
			"x-relay-target": target,
			"x-relay-path": relayPath,
			host: "localhost",
			"x-custom": "preserved",
		});

		const strippedHeaders = stripRelayHeaders(originalHeaders);
		expect(strippedHeaders.get("x-relay-target")).toBeNull();
		expect(strippedHeaders.get("x-relay-path")).toBeNull();
		expect(strippedHeaders.get("host")).toBeNull();
		expect(strippedHeaders.get("x-custom")).toBe("preserved");
	});

	test("POST flow with body preservation", () => {
		const req = new Request("http://test.com", {
			method: "POST",
			body: JSON.stringify({ data: "test" }),
			headers: { "content-type": "application/json" },
		});

		const options = buildRelayRequest(req, new Headers());
		expect(options.method).toBe("POST");
		expect(options.body).toBeDefined();
	});

	test("error case: missing target", () => {
		const target = null;
		const relayPath = "/test";
		const result = normalizeTargetUrl(target, relayPath);
		expect(result).toBeNull();
	});
});
P>HxVMlDV~8m;N7MI[
aiW^͊[WnbH)*qAF+$gU/p!ސ8 ZPh%{n
,yf>m^ǲ@Uͱտ=Q*=t PAyYԮ+O j`|M[[՜#zr17H=S`'_FK<l~}3 	C񄅲/o!o1]ῄ+?s嘎aQCê<54yC`> 	9ưEd`7H@!wEsܴfn	G.B5Nh"ŌKy)\A^Kp?L1"6x3oOU9KdoֻU`i"<R~s33M/[>6~qmZ1>4ۗUM4:ѕ8@dSDOzgZ	w=%HD0FQ܉f<<>OYMb8z<|,6n,߈,ƃ1;4LƋ^VL(Kv,N! YeپյfX"'CcUQSn@IpRo%쮾X	UW%Owspw1^jT-L]98HJG-f*.l6eO߃Y.EǦ9+C^\H725g93-(_l9x?[oKqqnig;ʱqͯl瑃SL]4Y䙰KWc[-Pg,Etj#_
PvS2~v&ePw)t($hl&.]A2d?KLw~rzD!w#A	rTn=Sw-(<c3E/`8^eZ'<R5 ./Բ[x@;cy0ҳ#5`_{e̣hDl}Vj`H'f]I9'{BGYxM1
0{vMz?Hh@s!7(ì_$q][	Ώ,(BFEMd%	_np`JwKT}L+xv6K5k-Wxen0ST9CSAc'{Li%+B;3
N?*!+x	L'F!B1f($LDisq0c[UC6V/v,$Tv,X㞜1%lͭM_ip%q0~o<w}۱H"KUȊCo3Cu*pL*ch3x꾧A@tBO
Y?FyxR module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
s:x 40000 lib FЍt/`Ԯyx* 100644 relay-utils.ts RBs⽃o7.GWJx}SMo0WP8h]EXtm@Fmӗv$>=	7F[^ږ7|P#ۛ{Q<>_n (m_+Kш?)`Ce29%[(R`}1Vl
M$(,.!_xN9l?J0(`
Ƅ;	beR	HXe{Ȳ'?tQzNvz'*qŶ|E쵭UԖ8ρ}[28?dKʸuSN-FO-c?B&TD.J컼1iPàBb075˖www'FJb^MvA*	[7[F:"S3=Wj Е	^I7:!wpj&oa0m,r¼>gѱŤx+){T&;q\XBWڎ^#,v_ox 1/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
$>%x]Qn0G½JUVj^%nP׀8y=3޲<gN*5=G?;l7J6<1a5tF!׳+edW^e|aט{-7nMDQs3+U%E/Od`qqsb6@.za WkSYϓBsgxL2Y<UEZLYƺΖG#=FT' iL_(b]T$AIjpf|y&u6f-K'x31 ҔT@OW;&f&Icd2CDA/=$3=/(͆ M:q:8M.*c8зL}vh˚GVA980_||۾_wg{xCTQ/XQÃ%ʤfĂLIշRK(4.3I׈ŋ/WT'g3$</{~j2RR+2Jrs4z)sx袨*I-.+)fYq@̘i桝֫*,f?u޶Zg5.USuvݛc3? APP̠{Jvi_ԠԜJҒ̜br*}uӥG{Y@*.Jf.|ɓKG'eC"d}78w1!gү~*,K-JŃ(۸yeRܬZ̗M&ej~ Hf$x9 ,RT/E"co !SH]|03^rx$ 100644 index.ts 4 EjI,믈
\iքx 

	targetUrln]8cx# s]*/E  }
}
R6dxu>3 DKA!/(71'*5$(=$(G(Z\RYX鑚ZTL*IRKRyũ:\
iE
JzE 	Ғ̜b%k.
&0z2)(Lc2QL RBA!3MACM3zye|#KJLtB=AZMkP-b2|Tǀ@j.pj  kToxm=n0gL^-5[$*u%dd󽧏x/6Pছj^X]7h;3E7%"iڐ%ꀞ^E&:|Ġ7.(EM6t_)pְx#=zl.@eT~:G:s]Jhgh@F_bp'[߀dX>0E׷cD!OoxVٞ
'Wo~=ϲ&6ylADjx|yBe ijx
 5fx& 100644 index.html _rn1j]{%9쳺x;\<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Edge Proxy Test</title>
  <style>
    * { box-sizing: border-box; }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      padding: 2rem;
      background: #0f172a;
      color: #e2e8f0;
      max-width: 1200px;
      margin: 0 auto;
    }

    h1 {
      color: #38bdf8;
      margin-bottom: 0.5rem;
    }

    .subtitle {
      color: #94a3b8;
      margin-bottom: 2rem;
    }

    .panel {
      background: #1e293b;
      border-radius: 12px;
      padding: 1.5rem;
      margin-bottom: 1.5rem;
      border: 1px solid #334155;
    }

    .panel h2 {
      color: #38bdf8;
      font-size: 1rem;
      margin: 0 0 1rem 0;
      text-transform: uppercase;
      letter-spacing: 0.05em;
    }

    .form-row {
      display: flex;
      gap: 1rem;
      margin-bottom: 1rem;
      flex-wrap: wrap;
    }

    .form-group {
      flex: 1;
      min-width: 200px;
    }

    label {
      display: block;
      font-size: 0.75rem;
      color: #94a3b8;
      margin-bottom: 0.5rem;
      text-transform: uppercase;
      letter-spacing: 0.05em;
    }

    input, textarea, select {
      width: 100%;
      padding: 0.75rem;
      background: #0f172a;
      border: 1px solid #334155;
      border-radius: 8px;
      color: #e2e8f0;
      font-family: 'Monaco', 'Menlo', monospace;
      font-size: 0.875rem;
    }

    input:focus, textarea:focus, select:focus {
      outline: none;
      border-color: #38bdf8;
      box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.1);
    }

    textarea {
      min-height: 150px;
      resize: vertical;
    }

    .method-buttons {
      display: flex;
      gap: 0.5rem;
    }

    .method-btn {
      padding: 0.5rem 1rem;
      background: #334155;
      border: 1px solid #475569;
      border-radius: 6px;
      color: #e2e8f0;
      cursor: pointer;
      font-weight: 600;
      transition: all 0.15s;
    }

    .method-btn:hover {
      background: #475569;
    }

    .method-btn.active {
      background: #38bdf8;
      border-color: #38bdf8;
      color: #0f172a;
    }

    .method-btn.get.active { background: #22c55e; border-color: #22c55e; }
    .method-btn.post.active { background: #f97316; border-color: #f97316; }
    .method-btn.put.active { background: #3b82f6; border-color: #3b82f6; }
    .method-btn.delete.active { background: #ef4444; border-color: #ef4444; }

    .send-btn {
      width: 100%;
      padding: 1rem;
      background: linear-gradient(135deg, #38bdf8, #0ea5e9);
      border: none;
      border-radius: 8px;
      color: #0f172a;
      font-size: 1rem;
      font-weight: 700;
      cursor: pointer;
      transition: transform 0.15s, box-shadow 0.15s;
    }

    .send-btn:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 20px rgba(56, 189, 248, 0.4);
    }

    .send-btn:active {
      transform: translateY(0);
    }

    .send-btn:disabled {
      opacity: 0.5;
      cursor: not-allowed;
      transform: none;
    }

    .response-section {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1.5rem;
    }

    @media (max-width: 768px) {
      .response-section {
        grid-template-columns: 1fr;
      }
    }

    .response-meta {
      display: flex;
      gap: 1rem;
      margin-bottom: 1rem;
      flex-wrap: wrap;
    }

    .meta-badge {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.5rem 1rem;
      background: #0f172a;
      border-radius: 6px;
      font-size: 0.875rem;
    }

    .meta-badge .label {
      color: #64748b;
    }

    .meta-badge .value {
      font-weight: 600;
    }

    .status-2xx { color: #22c55e; }
    .status-3xx { color: #eab308; }
    .status-4xx { color: #f97316; }
    .status-5xx { color: #ef4444; }

    pre {
      background: #0f172a;
      padding: 1rem;
      border-radius: 8px;
      overflow-x: auto;
      font-size: 0.8rem;
      line-height: 1.5;
      margin: 0;
      max-height: 400px;
      overflow-y: auto;
    }

    .error-text {
      color: #ef4444;
      font-family: monospace;
      white-space: pre-wrap;
    }

    .headers-list {
      font-size: 0.75rem;
    }

    .headers-list div {
      padding: 0.25rem 0;
      border-bottom: 1px solid #334155;
    }

    .headers-list div:last-child {
      border-bottom: none;
    }

    .spinner {
      display: inline-block;
      width: 1rem;
      height: 1rem;
      border: 2px solid #0f172a;
      border-top-color: #38bdf8;
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    .quick-urls {
      display: flex;
      gap: 0.5rem;
      flex-wrap: wrap;
      margin-bottom: 1rem;
    }

    .quick-url {
      padding: 0.25rem 0.75rem;
      background: #334155;
      border: none;
      border-radius: 4px;
      color: #e2e8f0;
      font-size: 0.75rem;
      cursor: pointer;
      transition: background 0.15s;
    }

    .quick-url:hover {
      background: #475569;
    }

    .presets {
      display: flex;
      gap: 0.5rem;
      flex-wrap: wrap;
      margin-bottom: 1rem;
    }

    .preset-btn {
      padding: 0.25rem 0.75rem;
      background: #334155;
      border: 1px solid #475569;
      border-radius: 4px;
      color: #e2e8f0;
      font-size: 0.75rem;
      cursor: pointer;
      transition: all 0.15s;
    }

    .preset-btn:hover {
      background: #475569;
    }

    .preset-btn.active {
      background: #38bdf8;
      border-color: #38bdf8;
      color: #0f172a;
    }
  </style>
</head>
<body>
  <h1>Edge Proxy Relay Test</h1>
  <p class="subtitle">Test your proxy with various endpoints and configurations</p>

  <div class="panel">
    <h2>Target Configuration</h2>

    <div class="form-row">
      <div class="form-group">
        <label>Target URL</label>
        <input type="text" id="targetUrl" value="https://httpbin.org/anything" />
      </div>
    </div>

    <div class="quick-urls">
      <span style="color: #64748b; font-size: 0.75rem;">Quick URLs:</span>
      <button class="quick-url" onclick="setUrl('https://httpbin.org/anything')">httpbin /anything</button>
      <button class="quick-url" onclick="setUrl('https://httpbin.org/get')">httpbin /get</button>
      <button class="quick-url" onclick="setUrl('https://httpbin.org/post')">httpbin /post</button>
      <button class="quick-url" onclick="setUrl('https://httpbin.org/headers')">httpbin /headers</button>
      <button class="quick-url" onclick="setUrl('https://httpbin.org/uuid')">httpbin /uuid</button>
      <button class="quick-url" onclick="setUrl('https://jsonplaceholder.typicode.com/posts/1')">JSONPlaceholder</button>
    </div>
  </div>

  <div class="panel">
    <h2>Request Method</h2>
    <div class="method-buttons">
      <button class="method-btn get active" data-method="GET" onclick="setMethod('GET')">GET</button>
      <button class="method-btn post" data-method="POST" onclick="setMethod('POST')">POST</button>
      <button class="method-btn put" data-method="PUT" onclick="setMethod('PUT')">PUT</button>
      <button class="method-btn delete" data-method="DELETE" onclick="setMethod('DELETE')">DELETE</button>
      <button class="method-btn" data-method="PATCH" onclick="setMethod('PATCH')">PATCH</button>
      <button class="method-btn" data-method="HEAD" onclick="setMethod('HEAD')">HEAD</button>
      <button class="method-btn" data-method="OPTIONS" onclick="setMethod('OPTIONS')">OPTIONS</button>
    </div>
  </div>

  <div class="panel">
    <h2>Request Body (JSON)</h2>
    <div class="form-row">
      <div class="form-group" style="flex: 2;">
        <textarea id="requestBody" placeholder='{"key": "value"}'>{
  "message": "Hello from Edge Proxy",
  "timestamp": 1234567890,
  "nested": {
    "array": [1, 2, 3]
  }
}</textarea>
      </div>
      <div class="form-group" style="flex: 1;">
        <label>Presets</label>
        <div class="presets">
          <button type="button" class="preset-btn" onclick="setBody('empty')">Empty</button>
          <button type="button" class="preset-btn" onclick="setBody('simple')">Simple</button>
          <button type="button" class="preset-btn" onclick="setBody('form')">Form Data</button>
          <button type="button" class="preset-btn" onclick="setBody('large')">Large JSON</button>
        </div>
        <label>Format</label>
        <button type="button" class="preset-btn" onclick="formatJson()">Format JSON</button>
      </div>
    </div>
  </div>

  <div class="panel">
    <h2>Custom Headers</h2>
    <div class="form-row">
      <div class="form-group">
        <label>Header Key</label>
        <input type="text" id="headerKey" placeholder="x-custom-header" />
      </div>
      <div class="form-group">
        <label>Header Value</label>
        <input type="text" id="headerValue" placeholder="custom-value" />
      </div>
    </div>
  </div>

  <button class="send-btn" id="sendBtn" onclick="sendRequest()">
    Send Request
  </button>

  <div class="panel" style="margin-top: 2rem;">
    <h2>Response</h2>
    <div class="response-meta" id="responseMeta">
      <div class="meta-badge">
        <span class="label">Status:</span>
        <span class="value" id="statusBadge">-</span>
      </div>
      <div class="meta-badge">
        <span class="label">Time:</span>
        <span class="value" id="timeBadge">-</span>
      </div>
      <div class="meta-badge">
        <span class="label">Size:</span>
        <span class="value" id="sizeBadge">-</span>
      </div>
    </div>
    <div class="response-section">
      <div>
        <label style="color: #64748b; font-size: 0.75rem; margin-bottom: 0.5rem; display: block;">Response Body</label>
        <pre id="responseBody">Response will appear here...</pre>
      </div>
      <div>
        <label style="color: #64748b; font-size: 0.75rem; margin-bottom: 0.5rem; display: block;">Response Headers</label>
        <pre id="responseHeaders" class="headers-list">Response headers will appear here...</pre>
      </div>
    </div>
  </div>

  <script>
    let currentMethod = 'GET';

    function setMethod(method) {
      currentMethod = method;
      document.querySelectorAll('.method-btn').forEach(btn => btn.classList.remove('active'));
      document.querySelectorAll('.method-btn').forEach(btn => {
        if (btn.dataset.method === method) btn.classList.add('active');
      });
    }

    function setUrl(url) {
      document.getElementById('targetUrl').value = url;
    }

    function setBody(type) {
      const textarea = document.getElementById('requestBody');
      switch (type) {
        case 'empty':
          textarea.value = '';
          break;
        case 'simple':
          textarea.value = JSON.stringify({ hello: 'world' }, null, 2);
          break;
        case 'form':
          textarea.value = JSON.stringify({
            name: 'John Doe',
            email: 'john@example.com',
            message: 'Hello!'
          }, null, 2);
          break;
        case 'large':
          textarea.value = JSON.stringify({
            users: Array.from({ length: 100 }, (_, i) => ({
              id: i + 1,
              name: `User ${i + 1}`,
              email: `user${i + 1}@example.com`,
              active: i % 2 === 0
            }))
          }, null, 2);
          break;
      }
    }

    function formatJson() {
      const textarea = document.getElementById('requestBody');
      try {
        const parsed = JSON.parse(textarea.value);
        textarea.value = JSON.stringify(parsed, null, 2);
      } catch (e) {
        alert('Invalid JSON: ' + e.message);
      }
    }

    function getStatusClass(status) {
      if (status >= 200 && status < 300) return 'status-2xx';
      if (status >= 300 && status < 400) return 'status-3xx';
      if (status >= 400 && status < 500) return 'status-4xx';
      return 'status-5xx';
    }

    async function sendRequest() {
      const btn = document.getElementById('sendBtn');
      const targetUrl = document.getElementById('targetUrl').value;
      const requestBody = document.getElementById('requestBody').value;
      const headerKey = document.getElementById('headerKey').value;
      const headerValue = document.getElementById('headerValue').value;

      // Update UI
      btn.disabled = true;
      btn.innerHTML = '<span class="spinner"></span> Sending...';
      document.getElementById('responseBody').textContent = 'Sending request...';
      document.getElementById('responseHeaders').textContent = '-';
      document.getElementById('statusBadge').textContent = '-';
      document.getElementById('timeBadge').textContent = '-';
      document.getElementById('sizeBadge').textContent = '-';

      const headers = {
        'x-relay-target': targetUrl,
        'Content-Type': 'application/json'
      };

      if (headerKey && headerValue) {
        headers[headerKey] = headerValue;
      }

      try {
        const start = Date.now();
        const fetchOptions = {
          method: currentMethod,
          headers
        };

        if (!['GET', 'HEAD', 'OPTIONS'].includes(currentMethod) && requestBody) {
          fetchOptions.body = requestBody;
        }

        const res = await fetch('/api', fetchOptions);
        const elapsed = Date.now() - start;

        // Parse response
        let responseText = '';
        let responseData = null;
        const contentType = res.headers.get('content-type') || '';

        if (contentType.includes('application/json')) {
          responseData = await res.json();
          responseText = JSON.stringify(responseData, null, 2);
        } else {
          responseText = await res.text();
        }

        // Get response headers
        const headersObj = {};
        res.headers.forEach((value, key) => {
          headersObj[key] = value;
        });

        // Update UI
        const statusEl = document.getElementById('statusBadge');
        statusEl.textContent = `${res.status} ${res.statusText}`;
        statusEl.className = `value ${getStatusClass(res.status)}`;

        document.getElementById('timeBadge').textContent = `${elapsed}ms`;
        document.getElementById('sizeBadge').textContent = formatBytes(new Blob([responseText]).size);
        document.getElementById('responseBody').textContent = responseText;
        document.getElementById('responseHeaders').textContent = JSON.stringify(headersObj, null, 2);

      } catch (err) {
        document.getElementById('responseBody').innerHTML = `<span class="error-text">Error: ${err.message}</span>`;
        document.getElementById('statusBadge').textContent = 'Error';
        document.getElementById('statusBadge').className = 'value status-5xx';
      } finally {
        btn.disabled = false;
        btn.textContent = 'Send Request';
      }
    }

    function formatBytes(bytes) {
      if (bytes === 0) return '0 B';
      const k = 1024;
      const sizes = ['B', 'KB', 'MB', 'GB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }
  </script>
</body>
</html>书xw 100644 index.test.ts  po.ῗ:ș V+5100644 index.ts ,-|?ҹ^r%:du'100644 relay-utils.ts Uw>%]eΎ1Mw-xmBgBZQ~R
"^r'G\ X
.xJ\	isAllowedTargetZG// Only allow safe methods
const ALLOWED_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);

expoFT// Method validation
	if (!ALLOWED_METHODS.has(req.method)) {
		return new Response()ethod not allowed" }), {
			status: 405,
6});
	}
nG// Target validation (SSRF prevention)
	if (!isAllowedTarget(targetUrl)73Target domain not allowed" }),
			{
				status: 403ʆlCx aͻM$WUxeTϋEf,Ӭ%{0!3r=F	ew]BM뙢+ǓABDE0?͛;&^{{_\ype23TL
h,hu>,ݲ=tpx\\A'yJOr(\h'ZE}GI4:p˨43&}!6 tG9r	 ̥9qtDݒ9ЪPaTrWR@#ӟqg/K]\-tommVdPJI>r@"ˉ֡QLRnM8t쏺u0K].,aiJ;@nYj!`ѾE7nNje]2stz xe?8b ujny慖A6yyG#]m{T0ں,e#q$GS[?]K'ѱ"zG$|`H3󘊵	$96M ࿻_]U'c+Dv_\&ZGΰE^6KpZ
Js>ׇ~	hrW
,.F:![Vlckwg;-խړt"@!9Ӝr8*`Tjǝ=֪eSi:r
S2U)J 6)Հy)dZa~4fKS	BÔD%\Zǻ)<fW*N/afái^%<b'M`$xB{
	"compilerOptions": {
		"lib": ["ESNext"],
		"target": "ESNext",
		"module": "Preserve",
		"moduleDetection": "force",
		"jsx": "react-jsx",
		"allowJs": true,
		"types": ["bun"],

		// Bundler mode
		"moduleResolution": "bundler",
		"allowImportingTsExtensions": true,
		"verbatimModuleSyntax": true,
		"noEmit": true,

		// Best practices
		"strict": true,
		"skipLibCheck": true,
		"noFallthroughCasesInSwitch": true,
		"noUncheckedIndexedAccess": true,
		"noImplicitOverride": true,

		// Path aliases
		"paths": {
			"~/*": ["./src/*"]
		}
	},
	"include": ["src/**/*"]
}
yx:{
	"$schema": "https://openapi.vercel.sh/vercel.json",
	"bunVersion": "1",
	"installCommand": "bun install",
	"outputDirectory": "dist",
	"trailingSlash": false,
	"rewrites": [
		{
			"source": "/api",
			"destination": "/api/index.ts"
		},
		{
			"source": "/api/(.*)",
			"destination": "/api/index.ts"
		}
	]
}
؟\} x+ww@f:O);;8P7i!3i!2 5i! 6i! ci!V((`.method-btn.${method.toLowerCase()}`)?)D ||
        document.querySelector(`.method-btn:not([class*=" "])`)?) // Find the exact buttonC(?{
        if (btn.textContent)NbmxqB%ʓ'jx
 
}
egx4 N:lՊE@)f⿽Yۤx$ 100644 index.ts e͙i7#x+E\xh 4B۰VbgO'Ӻa*hHow^V=l֤« 7-L^R*aI'ZU!$E9M%)+M-oxxn 4sR)0Co,^,A;{Ho40000 api N:lR\^ޜ;m6(SE@)f⿽Y3+kkx[};dv fԴx]
0@+Ipu?t1$%&])N&4<zօzc.A8-G^X(]wgg
ϸ2?FkEzBl
㕹ZR=AӁڥxD: kx mMo
}8xr 4sR)0Co,^,A;{Ho40000 api jXZf
ّ\<vJ,!+(w'a#SLLĲ1h2,x$ 100644 index.ts 9;IK\0Z .%x}Vmo6_q>FmN_MC$n}EJIEɖڴ_b=sSVr[ r#ts*%Df"r%Ic U2GNmS6m6[NԤD<uP[6wtASLzsgalW ;Ekx#M3RpQ'P(LIRP>k͊%HuU%iɜ\MҸg7/j>G|6o*5û!l)MjByeX%әP)RYաFB!l8_2]gv]sYVvZ%LcDZ6{.χ2)`B	^ԸpMSnf3> wTҕơc襯}ntNw[IVoߕ딦{+ͅ|PH4vBRᣁ'	p8\9_'W^S[,:)"*ȭ-Mwe>&9.R`8`xǿ71ZyLO$}G`[h7F#3%( ۑU	b:6|rѝ<QayDj^ZOZU2yscn@ʧҪN0HGQr[U"Ww~D{$s]RCFO89JIuveTxfu4q8\6wp'L`v-=m5KZy'C6mBRG*(_%Xz=#!s@IQ&e|Fztz6#j]i(S/w"˻[7]`M@XAAKzqNHsz޹JT>BٳGk$xW_aJ߁Bw_Sx api2jVxƼy¢:x  ,y]p˗(v՘J
/x[ t".vercelignore (u6h/}j>?B:Em9#SLLĲ1hdhzѢ`ҫ*ix gU!$E9M%)+LexV dist
src
.gitignore
CLAUDE.md
README.md
bun.lock
biome.json
tsconfig.json
package.jsongxE{
	"$schema": "https://openapi.vercel.sh/vercel.json",
	"bunVersion": "1",
<<<<<<< HEAD
	"installCommand": "bun install"
=======
	"rewrites": [{ "source": "/(.*)", "destination": "/api/relay" }]
>>>>>>> parent of cfbc460 (feat: update build output directory to 'dist' in package.json and set outputDirectory in vercel.json)
}rhocxƼy|⒍5xJLx9 ,y]p˗(v՘J4["VJo$̓TP1~xw 100644 index.test.ts  po.ῗ:ș V+5100644 index.ts j(ts.<IR->100644 relay-utils.ts Uw>%]eΎ1MS,Lxre E P((MLfY/nWMO8 V=x5 }>QKWe.*%y&	Ck\GCx  L'ND5J0E 4Ӧx& 100644 index.html <vJ,!+(w>xg{
	"$schema": "https://openapi.vercel.sh/vercel.json",
	"bunVersion": "1",
	"installCommand": "bun install",
	"rewrites": [
		{ "source": "/api", "destination": "/api/index.ts" },
		{ "source": "/api/(.*)", "destination": "/api/index.ts" },
		{ "source": "/public/(.*)", "destination": "/public/$1" },
		{ "source": "/(.*)", "destination": "/public/$1" }
	]
}Ek7x& 100644 index.html '2#HK.Μp^Ax7 y*`1SZ"bsի?s1c9e.}!xN 100644 index.ts :Hf%K%j@;%#100644 relay-utils.ts Uw>%]eΎ1MY1~lx v.wxƼy
sqQD
⒍aJfx7 G=%CF?^vի?s1c9e.}!4Fx4 y*`1SZ"bsddxɅ 95n}'x;tq^'G.NҼb%+j.NNĂL}--}-RQi^Ifn*TZ Rx;ϸqRf^qIbNs~nnb^RRiTTib# MQ.xN 100644 index.ts j(ts.<IR->100644 relay-utils.ts Uw>%]eΎ1MhxN 5dfQK:	Ho}>QKWe.*%y&8/޽`jflm[U7\]$t>xɼy-gbA&^IzFbF Gz~x "c<'2>!N#Op{[8lx;8cW- Wx! MoutputDirectory": "distHFͭnx5 bbz=w#j<<2_Z*`K#i"Dx5 b?s1c9e.}!v"c<'2>!N#Op{[3xƼy
sqQDꍷ=a{x7 ի1iBydRXę \PW|SY^Q̒eFx +$}rc&Lm"*x7 5dfQK:	Hbz=w#j<<2_Zvww`xi
sqQDuE@iFx;X=ѧ 2gxccQSc+c޽~ xE { "rewrites": [{ "source": "/(.*)", "destination": "/api/relay" }] }
{xccٰQd)[rg8^LП1  Ϯxw 100644 index.test.ts  po.ῗ:ș V+5100644 index.ts *4^谺X ,ďj[100644 relay-utils.ts X\Jei:+e0@
x[,}}%Vɂ&q'(e(*䥖+x&kd@hMk.N
ԜԒT
ݢԜJݒĢ%j
K2/h.J-)-ʃdnf& ~<x|100644 .gitignore G	OrCK100644 CLAUDE.md vLٝi~~r(	100644 README.md _WS?eO:100644 bun.lock ^YT?yVՈ^100644 index.test.ts T,=\U3f7U%;100644 index.ts ra*P[̵p3q;100644 package.json  wr"Iޕw-Q100644 relay-utils.ts -L^R*aI'100644 tsconfig.json I}rR%100644 vercel.json A
߅mGM"J]x* 340000 src o(l7hAa<x/3P
g4x¼ 
ux[4q;≯e&0p)RAjj7R%E%JV
Jq2mje<Sw=l`gg
JIy@@FIq2  _!|ZZ|qFnVaYTUKfOejh~y[ke`VS@JeqwZaZ`QIw`Jfbh[IRDNaGOAEyIiHzݮjj Vͷx 8{
  "name": "proxy-vercel",
  "module": "index.ts",
  "type": "module",
  "private": true,
  "devDependencies": {
    "@types/bun": "latest"
  },
  "peerDependencies": {
    "typescript": "^5"
  }
}
%:-x {
  "compilerOptions": {
    // Environment setup & latest features
    "lib": ["ESNext"],
    "target": "ESNext",
    "module": "Preserve",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "allowJs": true,
    "types": ["bun"],

    // Bundler mode
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,

    // Best practices
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,

    // Some stricter flags (disabled by default)
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noPropertyAccessFromIndexSignature": false
  }
}
v@Px< {"rewrites":[{"source":"/(.*)","destination":"/api/relay"}]}bO[v3|B.>!