proxy.ts214 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 /**
36 * Server-side publishable key `pk_briven_auth_…` injected as Authorization
37 * when the browser request has none. Prefer env BRIVEN_AUTH_PUBLIC_KEY —
38 * keeps the key off the client when using first-party proxy only.
39 */
40 publicKey?: string;
41 fetch?: typeof globalThis.fetch;
42};
43
44/**
45 * Absolute FDI base, e.g. https://api.briven.tech/v1/auth-core/fdi
46 */
47export function resolveFdiTarget(opts?: BrivenEngineProxyOptions): string {
48 const origin = (opts?.apiOrigin ?? 'https://api.briven.tech').replace(
49 /\/$/,
50 '',
51 );
52 const base = (opts?.fdiBasePath ?? '/v1/auth-core/fdi').replace(/\/$/, '');
53 const path = base.startsWith('/') ? base : `/${base}`;
54 return `${origin}${path}`;
55}
56
57/**
58 * Map app path under /api/auth → FDI path suffix.
59 * /api/auth/signup → /signup
60 * /api/auth/signinup/code → /signinup/code
61 */
62export function appAuthPathToFdiSuffix(
63 pathname: string,
64 proxyMount = '/api/auth',
65): string {
66 const mount = proxyMount.replace(/\/$/, '') || '/api/auth';
67 let rest = pathname;
68 if (rest.startsWith(mount)) {
69 rest = rest.slice(mount.length);
70 }
71 if (!rest.startsWith('/')) rest = `/${rest}`;
72 if (rest === '/') rest = '';
73 return rest || '';
74}
75
76/**
77 * Proxy one Request to briven-engine FDI. Use from Next.js route handlers
78 * or any fetch-compatible server.
79 */
80export async function proxyBrivenEngineAuth(
81 request: Request,
82 opts: BrivenEngineProxyOptions & {
83 /** Override path suffix (without FDI base). Default: derived from request URL. */
84 pathSuffix?: string;
85 proxyMount?: string;
86 } = {},
87): Promise<Response> {
88 const fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
89 const targetBase = resolveFdiTarget(opts);
90 const url = new URL(request.url);
91 const suffix =
92 opts.pathSuffix ??
93 appAuthPathToFdiSuffix(url.pathname, opts.proxyMount ?? '/api/auth');
94 const dest = `${targetBase}${suffix}${url.search}`;
95
96 const headers = new Headers();
97 request.headers.forEach((value, key) => {
98 if (HOP_BY_HOP.has(key.toLowerCase())) return;
99 headers.set(key, value);
100 });
101 headers.set('x-briven-engine', BRIVEN_ENGINE_ID);
102 if (opts.projectId) {
103 headers.set('x-briven-project-id', opts.projectId);
104 }
105 // FDI lock: inject server publishable key when browser omitted Authorization.
106 const pk = opts.publicKey?.trim();
107 if (
108 pk &&
109 pk.startsWith('pk_briven_auth_') &&
110 !headers.has('authorization')
111 ) {
112 headers.set('authorization', `Bearer ${pk}`);
113 }
114
115 // Forward real visitor IP so Auth emails can show Location + geo.
116 // Without this, the API only sees the app host and geo shows "Unknown".
117 const incomingFwd = request.headers.get('x-forwarded-for');
118 const incomingReal =
119 request.headers.get('x-real-ip') ||
120 request.headers.get('cf-connecting-ip') ||
121 request.headers.get('x-vercel-forwarded-for')?.split(',')[0]?.trim();
122 // Best-effort client IP from the incoming request (Next may expose via headers).
123 let clientIp =
124 request.headers.get('x-briven-client-ip')?.trim() ||
125 incomingReal ||
126 incomingFwd?.split(',')[0]?.trim() ||
127 null;
128 // Node/Next: some runtimes put peer address in x-forwarded-for already.
129 if (clientIp) {
130 headers.set('x-briven-client-ip', clientIp);
131 // Append so the API's left-most public hop is the browser when possible.
132 if (incomingFwd) {
133 if (!incomingFwd.includes(clientIp)) {
134 headers.set('x-forwarded-for', `${clientIp}, ${incomingFwd}`);
135 }
136 } else {
137 headers.set('x-forwarded-for', clientIp);
138 }
139 if (!headers.has('x-real-ip')) headers.set('x-real-ip', clientIp);
140 }
141
142 const method = request.method.toUpperCase();
143 const hasBody = method !== 'GET' && method !== 'HEAD';
144
145 const upstream = await fetchFn(dest, {
146 method,
147 headers,
148 body: hasBody ? await request.arrayBuffer() : undefined,
149 redirect: 'manual',
150 });
151
152 // Rebuild response so Set-Cookie reaches the browser on the **app** host.
153 const outHeaders = new Headers();
154 upstream.headers.forEach((value, key) => {
155 const k = key.toLowerCase();
156 if (k === 'transfer-encoding') return;
157 // Multiple Set-Cookie: append each
158 if (k === 'set-cookie') {
159 // Headers.forEach may combine; use getSetCookie when available
160 return;
161 }
162 outHeaders.set(key, value);
163 });
164
165 const anyHeaders = upstream.headers as Headers & {
166 getSetCookie?: () => string[];
167 };
168 if (typeof anyHeaders.getSetCookie === 'function') {
169 for (const c of anyHeaders.getSetCookie()) {
170 outHeaders.append('set-cookie', c);
171 }
172 } else {
173 const single = upstream.headers.get('set-cookie');
174 if (single) outHeaders.append('set-cookie', single);
175 }
176
177 outHeaders.set('x-briven-engine', BRIVEN_ENGINE_ID);
178 outHeaders.set('x-briven-proxy', 'first-party');
179
180 return new Response(upstream.body, {
181 status: upstream.status,
182 statusText: upstream.statusText,
183 headers: outHeaders,
184 });
185}
186
187/**
188 * Next.js App Router helper — drop into app/api/auth/[...path]/route.ts
189 *
190 * export const GET = brivenEngineNextHandler({ apiOrigin: process.env.BRIVEN_API_ORIGIN })
191 * export const POST = GET
192 */
193export function brivenEngineNextHandler(opts: BrivenEngineProxyOptions = {}) {
194 return async (
195 request: Request,
196 context?: { params?: Promise<{ path?: string[] }> | { path?: string[] } },
197 ): Promise<Response> => {
198 let pathSuffix: string | undefined;
199 if (context?.params) {
200 const params = await Promise.resolve(context.params);
201 if (params?.path?.length) {
202 pathSuffix = `/${params.path.join('/')}`;
203 }
204 }
205 return proxyBrivenEngineAuth(request, {
206 ...opts,
207 pathSuffix,
208 projectId:
209 opts.projectId ??
210 request.headers.get('x-briven-project-id') ??
211 undefined,
212 });
213 };
214}