proxy.ts172 lines · main
1/**
2 * First-party proxy for briven-engine FDI.
3 *
4 * Browser talks to YOUR app:
5 * https://your-app.com/api/auth/signup
6 * This proxy forwards to Briven:
7 * https://api.briven.tech/v1/auth-core/fdi/signup
8 * and returns Set-Cookie so cookies land on **your-app.com**.
9 *
10 * Product: briven-engine · storage stays on Briven Doltgres.
11 */
12
13const BRIVEN_ENGINE_ID = 'briven-engine' as const;
14
15const HOP_BY_HOP = new Set([
16 'connection',
17 'keep-alive',
18 'proxy-authenticate',
19 'proxy-authorization',
20 'te',
21 'trailers',
22 'transfer-encoding',
23 'upgrade',
24 'host',
25 'content-length',
26]);
27
28export type BrivenEngineProxyOptions = {
29 /** Briven API origin, e.g. https://api.briven.tech */
30 apiOrigin?: string;
31 /** Default: /v1/auth-core/fdi */
32 fdiBasePath?: string;
33 /** Optional fixed project id stamped on every hop */
34 projectId?: string;
35 fetch?: typeof globalThis.fetch;
36};
37
38/**
39 * Absolute FDI base, e.g. https://api.briven.tech/v1/auth-core/fdi
40 */
41export function resolveFdiTarget(opts?: BrivenEngineProxyOptions): string {
42 const origin = (opts?.apiOrigin ?? 'https://api.briven.tech').replace(
43 /\/$/,
44 '',
45 );
46 const base = (opts?.fdiBasePath ?? '/v1/auth-core/fdi').replace(/\/$/, '');
47 const path = base.startsWith('/') ? base : `/${base}`;
48 return `${origin}${path}`;
49}
50
51/**
52 * Map app path under /api/auth → FDI path suffix.
53 * /api/auth/signup → /signup
54 * /api/auth/signinup/code → /signinup/code
55 */
56export function appAuthPathToFdiSuffix(
57 pathname: string,
58 proxyMount = '/api/auth',
59): string {
60 const mount = proxyMount.replace(/\/$/, '') || '/api/auth';
61 let rest = pathname;
62 if (rest.startsWith(mount)) {
63 rest = rest.slice(mount.length);
64 }
65 if (!rest.startsWith('/')) rest = `/${rest}`;
66 if (rest === '/') rest = '';
67 return rest || '';
68}
69
70/**
71 * Proxy one Request to briven-engine FDI. Use from Next.js route handlers
72 * or any fetch-compatible server.
73 */
74export async function proxyBrivenEngineAuth(
75 request: Request,
76 opts: BrivenEngineProxyOptions & {
77 /** Override path suffix (without FDI base). Default: derived from request URL. */
78 pathSuffix?: string;
79 proxyMount?: string;
80 } = {},
81): Promise<Response> {
82 const fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
83 const targetBase = resolveFdiTarget(opts);
84 const url = new URL(request.url);
85 const suffix =
86 opts.pathSuffix ??
87 appAuthPathToFdiSuffix(url.pathname, opts.proxyMount ?? '/api/auth');
88 const dest = `${targetBase}${suffix}${url.search}`;
89
90 const headers = new Headers();
91 request.headers.forEach((value, key) => {
92 if (HOP_BY_HOP.has(key.toLowerCase())) return;
93 headers.set(key, value);
94 });
95 headers.set('x-briven-engine', BRIVEN_ENGINE_ID);
96 if (opts.projectId) {
97 headers.set('x-briven-project-id', opts.projectId);
98 }
99
100 const method = request.method.toUpperCase();
101 const hasBody = method !== 'GET' && method !== 'HEAD';
102
103 const upstream = await fetchFn(dest, {
104 method,
105 headers,
106 body: hasBody ? await request.arrayBuffer() : undefined,
107 redirect: 'manual',
108 });
109
110 // Rebuild response so Set-Cookie reaches the browser on the **app** host.
111 const outHeaders = new Headers();
112 upstream.headers.forEach((value, key) => {
113 const k = key.toLowerCase();
114 if (k === 'transfer-encoding') return;
115 // Multiple Set-Cookie: append each
116 if (k === 'set-cookie') {
117 // Headers.forEach may combine; use getSetCookie when available
118 return;
119 }
120 outHeaders.set(key, value);
121 });
122
123 const anyHeaders = upstream.headers as Headers & {
124 getSetCookie?: () => string[];
125 };
126 if (typeof anyHeaders.getSetCookie === 'function') {
127 for (const c of anyHeaders.getSetCookie()) {
128 outHeaders.append('set-cookie', c);
129 }
130 } else {
131 const single = upstream.headers.get('set-cookie');
132 if (single) outHeaders.append('set-cookie', single);
133 }
134
135 outHeaders.set('x-briven-engine', BRIVEN_ENGINE_ID);
136 outHeaders.set('x-briven-proxy', 'first-party');
137
138 return new Response(upstream.body, {
139 status: upstream.status,
140 statusText: upstream.statusText,
141 headers: outHeaders,
142 });
143}
144
145/**
146 * Next.js App Router helper — drop into app/api/auth/[...path]/route.ts
147 *
148 * export const GET = brivenEngineNextHandler({ apiOrigin: process.env.BRIVEN_API_ORIGIN })
149 * export const POST = GET
150 */
151export function brivenEngineNextHandler(opts: BrivenEngineProxyOptions = {}) {
152 return async (
153 request: Request,
154 context?: { params?: Promise<{ path?: string[] }> | { path?: string[] } },
155 ): Promise<Response> => {
156 let pathSuffix: string | undefined;
157 if (context?.params) {
158 const params = await Promise.resolve(context.params);
159 if (params?.path?.length) {
160 pathSuffix = `/${params.path.join('/')}`;
161 }
162 }
163 return proxyBrivenEngineAuth(request, {
164 ...opts,
165 pathSuffix,
166 projectId:
167 opts.projectId ??
168 request.headers.get('x-briven-project-id') ??
169 undefined,
170 });
171 };
172}