File size: 3,535 Bytes
6db5454 7d2647c 6db5454 7d2647c 6db5454 7d2647c 6db5454 7d2647c 6db5454 | 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 | /**
* Server-side proxy to https://etiya-d2l-api.hf.space.
*
* Browser → /api/proxy/<path> (NO auth header in browser request)
* ↓
* Next.js server adds Authorization: Bearer ${HF_TOKEN}
* ↓
* Forwards to D2L_API_URL/<path>
*
* Why a proxy and not NEXT_PUBLIC_HF_TOKEN?
* - NEXT_PUBLIC_* values are inlined into the client bundle.
* Anyone viewing the page source can read them.
* - Server-only env vars (no NEXT_PUBLIC_) stay on the server.
* The browser cannot read them. This is the only safe pattern
* for Bearer tokens.
*/
import { NextRequest, NextResponse } from "next/server";
const API_URL = process.env.D2L_API_URL || "https://etiya-d2l-api.hf.space";
const TOKEN = process.env.HF_TOKEN;
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
async function handle(req: NextRequest, params: { path: string[] }) {
if (!TOKEN) {
return NextResponse.json(
{
error:
"HF_TOKEN env var not set on server. Copy .env.example to .env.local and add your token.",
},
{ status: 500 }
);
}
const path = (params.path ?? []).join("/");
const search = req.nextUrl.search; // includes leading "?"
const upstreamUrl = `${API_URL}/${path}${search}`;
const upstreamHeaders: Record<string, string> = {
Authorization: `Bearer ${TOKEN}`,
// Forward client's Accept so SSE requests propagate; default to JSON.
Accept: req.headers.get("accept") || "application/json",
};
let body: BodyInit | null = null;
if (req.method !== "GET" && req.method !== "HEAD") {
const contentType = req.headers.get("content-type") || "application/json";
upstreamHeaders["Content-Type"] = contentType;
body = await req.text();
}
let resp: Response;
try {
resp = await fetch(upstreamUrl, {
method: req.method,
headers: upstreamHeaders,
body,
cache: "no-store",
});
} catch (e: unknown) {
return NextResponse.json(
{
error: "upstream_unreachable",
message: e instanceof Error ? e.message : String(e),
upstream: upstreamUrl,
},
{ status: 502 }
);
}
// Pass the upstream body through as a stream so SSE (text/event-stream)
// reaches the browser unbuffered. Buffering with `await resp.text()` would
// collapse streaming into a single chunk and defeat token-by-token render.
const contentType = resp.headers.get("content-type") || "application/json";
const headers: Record<string, string> = {
"content-type": contentType,
"cache-control": resp.headers.get("cache-control") ?? "no-cache, no-transform",
};
if (contentType.includes("text/event-stream")) {
headers["x-accel-buffering"] = "no";
headers["connection"] = "keep-alive";
}
return new NextResponse(resp.body, {
status: resp.status,
headers,
});
}
// Next.js 15+ — `params` is a Promise that must be awaited inside the route.
type RouteCtx = { params: Promise<{ path: string[] }> };
export async function GET(req: NextRequest, ctx: RouteCtx) {
return handle(req, await ctx.params);
}
export async function POST(req: NextRequest, ctx: RouteCtx) {
return handle(req, await ctx.params);
}
export async function PUT(req: NextRequest, ctx: RouteCtx) {
return handle(req, await ctx.params);
}
export async function DELETE(req: NextRequest, ctx: RouteCtx) {
return handle(req, await ctx.params);
}
export async function PATCH(req: NextRequest, ctx: RouteCtx) {
return handle(req, await ctx.params);
}
|