index.ts282 lines · main
1/**
2 * @briven/auth/engine — Briven Auth client for **briven-engine** (Path A).
3 *
4 * First-party proxy rule (required):
5 * Browser → https://YOUR_APP/api/auth/* → https://api.briven.tech/v1/auth-core/fdi/*
6 * so session cookies sit on the app domain.
7 *
8 * import { createBrivenEngineClient } from '@briven/auth/engine';
9 *
10 * const auth = createBrivenEngineClient({
11 * projectId: 'p_abc',
12 * // Prefer same-origin proxy path in the browser:
13 * apiBasePath: '/api/auth',
14 * });
15 *
16 * Product brand: briven-engine only.
17 */
18
19export const BRIVEN_ENGINE_ID = 'briven-engine' as const;
20
21export type BrivenEngineClientOptions = {
22 readonly projectId: string;
23 /**
24 * Publishable Auth key `pk_briven_auth_…` (required for FDI after Batch A lock).
25 * Prefer injecting this on the **server proxy** so the browser never needs it;
26 * when calling the API directly (server-side or local), pass it here.
27 */
28 readonly publicKey?: string;
29 /**
30 * Base path for FDI calls. Default `/api/auth` (first-party proxy on app).
31 * For direct API (server-side only / local): full origin + `/v1/auth-core/fdi`.
32 */
33 readonly apiBasePath?: string;
34 /** Absolute API origin when not using same-origin proxy. */
35 readonly apiOrigin?: string;
36 readonly fetch?: typeof globalThis.fetch;
37};
38
39export type BrivenEngineSession = {
40 readonly userId: string;
41 readonly sessionHandle?: string;
42 readonly accessTokenPayload?: Record<string, unknown>;
43};
44
45export type BrivenEngineResult<T> =
46 | { ok: true; data: T }
47 | { ok: false; status: number; message: string };
48
49export type BrivenEngineClient = {
50 readonly engine: typeof BRIVEN_ENGINE_ID;
51 readonly projectId: string;
52 /** Session recipe: refresh */
53 refreshSession: () => Promise<BrivenEngineResult<unknown>>;
54 /** Session recipe: sign out */
55 signOut: () => Promise<BrivenEngineResult<unknown>>;
56 /** EmailPassword sign up */
57 signUpEmailPassword: (input: {
58 email: string;
59 password: string;
60 turnstileToken?: string;
61 }) => Promise<BrivenEngineResult<unknown>>;
62 /** EmailPassword sign in */
63 signInEmailPassword: (input: {
64 email: string;
65 password: string;
66 turnstileToken?: string;
67 }) => Promise<BrivenEngineResult<unknown>>;
68 /** Passwordless: create code (email or phone — SMS included) */
69 createPasswordlessCode: (input: {
70 email?: string;
71 phoneNumber?: string;
72 /** Cloudflare Turnstile token when platform captcha is required */
73 turnstileToken?: string;
74 }) => Promise<BrivenEngineResult<unknown>>;
75 /** Passwordless: consume user input code */
76 consumePasswordlessCode: (input: {
77 preAuthSessionId: string;
78 userInputCode: string;
79 deviceId: string;
80 }) => Promise<BrivenEngineResult<unknown>>;
81 /** Passkeys: start sign-in (browser then finishes with navigator.credentials.get) */
82 passkeySignInOptions: (input?: {
83 rpId?: string;
84 expectedOrigin?: string;
85 }) => Promise<BrivenEngineResult<unknown>>;
86 /** Passkeys: finish sign-in */
87 passkeySignInFinish: (input: {
88 challengeId: string;
89 credential: unknown;
90 rpId?: string;
91 expectedOrigin?: string;
92 }) => Promise<BrivenEngineResult<unknown>>;
93 /** Passkeys: start register (needs existing session cookie) */
94 passkeyRegisterOptions: (input?: {
95 rpId?: string;
96 expectedOrigin?: string;
97 }) => Promise<BrivenEngineResult<unknown>>;
98 /** Passkeys: finish register */
99 passkeyRegisterFinish: (input: {
100 challengeId: string;
101 credential: unknown;
102 rpId?: string;
103 expectedOrigin?: string;
104 }) => Promise<BrivenEngineResult<unknown>>;
105 /** Raw FDI helper */
106 fdi: (path: string, init?: RequestInit) => Promise<Response>;
107};
108
109function joinBase(apiOrigin: string | undefined, apiBasePath: string): string {
110 const path = apiBasePath.replace(/\/$/, '') || '/api/auth';
111 if (!apiOrigin) return path;
112 return `${apiOrigin.replace(/\/$/, '')}${path.startsWith('/') ? path : `/${path}`}`;
113}
114
115export function createBrivenEngineClient(
116 opts: BrivenEngineClientOptions,
117): BrivenEngineClient {
118 const fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
119 const base = joinBase(
120 opts.apiOrigin,
121 opts.apiBasePath ?? '/api/auth',
122 );
123
124 async function fdi(path: string, init?: RequestInit): Promise<Response> {
125 const p = path.startsWith('/') ? path : `/${path}`;
126 const headers = new Headers(init?.headers);
127 headers.set('x-briven-project-id', opts.projectId);
128 headers.set('x-briven-engine', BRIVEN_ENGINE_ID);
129 // FDI lock: Bearer pk required (proxy may also inject; client sends when known).
130 const pk = opts.publicKey?.trim();
131 if (pk && pk.startsWith('pk_briven_auth_') && !headers.has('authorization')) {
132 headers.set('authorization', `Bearer ${pk}`);
133 }
134 if (!headers.has('content-type') && init?.body) {
135 headers.set('content-type', 'application/json');
136 }
137 return fetchFn(`${base}${p}`, {
138 ...init,
139 headers,
140 credentials: 'include',
141 });
142 }
143
144 async function jsonResult(res: Response): Promise<BrivenEngineResult<unknown>> {
145 let data: unknown = null;
146 try {
147 data = await res.json();
148 } catch {
149 data = null;
150 }
151 if (!res.ok) {
152 const msg =
153 data && typeof data === 'object' && data !== null && 'message' in data
154 ? String((data as { message: unknown }).message)
155 : res.statusText;
156 return { ok: false, status: res.status, message: msg };
157 }
158 return { ok: true, data };
159 }
160
161 return {
162 engine: BRIVEN_ENGINE_ID,
163 projectId: opts.projectId,
164 fdi,
165 refreshSession: async () => {
166 const res = await fdi('/session/refresh', { method: 'POST', body: '{}' });
167 return jsonResult(res);
168 },
169 signOut: async () => {
170 const res = await fdi('/signout', { method: 'POST', body: '{}' });
171 return jsonResult(res);
172 },
173 signUpEmailPassword: async ({ email, password, turnstileToken }) => {
174 const res = await fdi('/signup', {
175 method: 'POST',
176 body: JSON.stringify({
177 formFields: [
178 { id: 'email', value: email },
179 { id: 'password', value: password },
180 ],
181 ...(turnstileToken ? { turnstileToken } : {}),
182 }),
183 headers: { rid: 'emailpassword' },
184 });
185 return jsonResult(res);
186 },
187 signInEmailPassword: async ({ email, password, turnstileToken }) => {
188 const res = await fdi('/signin', {
189 method: 'POST',
190 body: JSON.stringify({
191 formFields: [
192 { id: 'email', value: email },
193 { id: 'password', value: password },
194 ],
195 ...(turnstileToken ? { turnstileToken } : {}),
196 }),
197 headers: { rid: 'emailpassword' },
198 });
199 return jsonResult(res);
200 },
201 createPasswordlessCode: async (input) => {
202 const body: Record<string, string> = {};
203 if (input.email) body.email = input.email;
204 if (input.phoneNumber) body.phoneNumber = input.phoneNumber;
205 if (input.turnstileToken) body.turnstileToken = input.turnstileToken;
206 const res = await fdi('/signinup/code', {
207 method: 'POST',
208 body: JSON.stringify(body),
209 headers: { rid: 'passwordless' },
210 });
211 return jsonResult(res);
212 },
213 consumePasswordlessCode: async (input) => {
214 const res = await fdi('/signinup/code/consume', {
215 method: 'POST',
216 body: JSON.stringify(input),
217 headers: { rid: 'passwordless' },
218 });
219 return jsonResult(res);
220 },
221 passkeySignInOptions: async (input = {}) => {
222 const res = await fdi('/webauthn/signin/options', {
223 method: 'POST',
224 body: JSON.stringify(input),
225 headers: { rid: 'webauthn' },
226 });
227 return jsonResult(res);
228 },
229 passkeySignInFinish: async (input) => {
230 const res = await fdi('/webauthn/signin/finish', {
231 method: 'POST',
232 body: JSON.stringify({
233 challengeId: input.challengeId,
234 credential: input.credential,
235 response: input.credential,
236 rpId: input.rpId,
237 expectedOrigin: input.expectedOrigin,
238 }),
239 headers: { rid: 'webauthn' },
240 });
241 return jsonResult(res);
242 },
243 passkeyRegisterOptions: async (input = {}) => {
244 const res = await fdi('/webauthn/register/options', {
245 method: 'POST',
246 body: JSON.stringify(input),
247 headers: { rid: 'webauthn' },
248 });
249 return jsonResult(res);
250 },
251 passkeyRegisterFinish: async (input) => {
252 const res = await fdi('/webauthn/register/finish', {
253 method: 'POST',
254 body: JSON.stringify({
255 challengeId: input.challengeId,
256 credential: input.credential,
257 response: input.credential,
258 rpId: input.rpId,
259 expectedOrigin: input.expectedOrigin,
260 }),
261 headers: { rid: 'webauthn' },
262 });
263 return jsonResult(res);
264 },
265 };
266}
267
268/** Next.js (or any) first-party proxy target for briven-engine FDI. */
269export function brivenEngineProxyTarget(apiOrigin?: string): string {
270 const origin = (apiOrigin ?? 'https://api.briven.tech').replace(/\/$/, '');
271 return `${origin}/v1/auth-core/fdi`;
272}
273
274export { BRIVEN_ENGINE_SCAFFOLDS, listBrivenEngineScaffolds } from './scaffolds';
275export {
276 proxyBrivenEngineAuth,
277 brivenEngineNextHandler,
278 appAuthPathToFdiSuffix,
279 resolveFdiTarget,
280} from './proxy';
281export type { BrivenEngineProxyOptions } from './proxy';
282// Server session helper: import from '@briven/auth/engine/server'