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);
}