auth-core-fdi.ts771 lines · main
1/**
2 * briven-engine FDI-compatible routes — implemented on Doltgres only.
3 * No SuperTokens Core process.
4 *
5 * Public end-user endpoints (apps proxy here first-party).
6 */
7
8import { Hono } from 'hono';
9
10import {
11 signInEmailPassword,
12 signUpEmailPassword,
13} from '../services/auth-core/emailpassword.js';
14import {
15 createEngineSession,
16 revokeEngineSession,
17} from '../services/auth-core/native-session.js';
18import {
19 consumePasswordlessCode,
20 createPasswordlessCode,
21} from '../services/auth-core/passwordless.js';
22import {
23 getAuthorisationUrl,
24 signInUpWithCode,
25 signInUpWithThirdPartyProfile,
26 type SupportedSocial,
27} from '../services/auth-core/thirdparty.js';
28import {
29 createTotpDevice,
30 listTotpDevices,
31 removeTotpDevice,
32 userHasVerifiedTotp,
33 verifyAndEnableTotpDevice,
34 verifyUserTotp,
35} from '../services/auth-core/mfa.js';
36import {
37 createAuthenticationOptions,
38 createRegistrationOptions,
39 deletePasskey,
40 finishAuthentication,
41 finishRegistration,
42 listPasskeys,
43} from '../services/auth-core/webauthn.js';
44import { env } from '../env.js';
45import { requireTurnstileIfConfigured } from '../services/auth-core/abuse.js';
46import { isAuthCoreInitialized } from '../services/auth-core/engine.js';
47import { resolveAuthTenantFromHeaders } from '../services/auth-core/request-tenant.js';
48import { verifyAuthCoreSession } from '../services/auth-core/session.js';
49import type { AppEnv } from '../types/app-env.js';
50
51export const authCoreFdiRouter = new Hono<AppEnv>();
52
53const FDI = '/v1/auth-core/fdi';
54
55/** Cookie holds session handle (Doltgres lookup key). Secure in production. */
56function setSessionCookies(
57 c: { header: (n: string, v: string, o?: { append?: boolean }) => void },
58 session: { sessionHandle: string; refreshToken: string; expiresAt: Date },
59) {
60 const secure = env.BRIVEN_ENV === 'production' ? '; Secure' : '';
61 const maxAge = Math.max(
62 60,
63 Math.floor((session.expiresAt.getTime() - Date.now()) / 1000),
64 );
65 c.header(
66 'Set-Cookie',
67 `sAccessToken=${encodeURIComponent(session.sessionHandle)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${secure}`,
68 { append: true },
69 );
70 c.header(
71 'Set-Cookie',
72 `sRefreshToken=${encodeURIComponent(session.refreshToken)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${secure}`,
73 { append: true },
74 );
75}
76
77async function captchaGate(
78 body: Record<string, unknown>,
79): Promise<Response | null> {
80 const cap = await requireTurnstileIfConfigured(body);
81 if (!cap.ok) {
82 return new Response(
83 JSON.stringify({
84 status: 'CAPTCHA_ERROR',
85 engine: 'briven-engine',
86 message: cap.message,
87 }),
88 { status: 400, headers: { 'content-type': 'application/json' } },
89 );
90 }
91 return null;
92}
93
94function notReady() {
95 return {
96 code: 'auth_core_sdk_not_ready',
97 engine: 'briven-engine',
98 storage: 'doltgres',
99 message: 'briven-engine not ready on Doltgres',
100 };
101}
102
103authCoreFdiRouter.post(`${FDI}/signup`, async (c) => {
104 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
105 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
106 let body: {
107 formFields?: Array<{ id: string; value: string }>;
108 email?: string;
109 password?: string;
110 turnstileToken?: string;
111 } = {};
112 try {
113 body = await c.req.json();
114 } catch {
115 body = {};
116 }
117 const cap = await captchaGate(body as Record<string, unknown>);
118 if (cap) return cap;
119 const fields = Object.fromEntries(
120 (body.formFields ?? []).map((f) => [f.id, f.value]),
121 );
122 const email = body.email ?? fields.email;
123 const password = body.password ?? fields.password;
124 if (!email || !password) {
125 return c.json({ status: 'FIELD_ERROR', formFields: [] }, 400);
126 }
127 const result = await signUpEmailPassword({
128 email,
129 password,
130 tenantId: tenant?.tenantId,
131 projectId: tenant?.projectId,
132 });
133 if (result.status !== 'OK') {
134 return c.json({ status: result.status });
135 }
136 const session = await createEngineSession({
137 userId: result.user.id,
138 tenantId: result.user.tenantId,
139 });
140 // Phase 2: cookie value = session handle (lookup key on Doltgres).
141 setSessionCookies(c, session);
142 c.header('x-briven-engine', 'briven-engine');
143 c.header('x-briven-session-handle', session.sessionHandle);
144 if (tenant) c.header('x-briven-tenant-id', tenant.tenantId);
145 return c.json({
146 status: 'OK',
147 user: { id: result.user.id, emails: [result.user.email] },
148 session: {
149 handle: session.sessionHandle,
150 userId: session.userId,
151 },
152 engine: 'briven-engine',
153 storage: 'doltgres',
154 });
155});
156
157authCoreFdiRouter.post(`${FDI}/signin`, async (c) => {
158 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
159 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
160 let body: {
161 formFields?: Array<{ id: string; value: string }>;
162 email?: string;
163 password?: string;
164 turnstileToken?: string;
165 } = {};
166 try {
167 body = await c.req.json();
168 } catch {
169 body = {};
170 }
171 const cap = await captchaGate(body as Record<string, unknown>);
172 if (cap) return cap;
173 const fields = Object.fromEntries(
174 (body.formFields ?? []).map((f) => [f.id, f.value]),
175 );
176 const email = body.email ?? fields.email;
177 const password = body.password ?? fields.password;
178 if (!email || !password) {
179 return c.json({ status: 'FIELD_ERROR' }, 400);
180 }
181 const result = await signInEmailPassword({
182 email,
183 password,
184 tenantId: tenant?.tenantId,
185 projectId: tenant?.projectId,
186 });
187 if (result.status !== 'OK') {
188 return c.json({ status: result.status });
189 }
190 // Phase 5: if TOTP enrolled, require second factor before issuing session.
191 if (await userHasVerifiedTotp(result.user.id)) {
192 return c.json({
193 status: 'MFA_REQUIRED',
194 factor: 'totp',
195 userId: result.user.id,
196 tenantId: result.user.tenantId,
197 engine: 'briven-engine',
198 storage: 'doltgres',
199 message: 'password ok — send TOTP code to /v1/auth-core/fdi/totp/verify',
200 });
201 }
202 const session = await createEngineSession({
203 userId: result.user.id,
204 tenantId: result.user.tenantId,
205 });
206 setSessionCookies(c, session);
207 c.header('x-briven-engine', 'briven-engine');
208 c.header('x-briven-session-handle', session.sessionHandle);
209 return c.json({
210 status: 'OK',
211 user: { id: result.user.id, emails: [result.user.email] },
212 session: { handle: session.sessionHandle, userId: session.userId },
213 engine: 'briven-engine',
214 storage: 'doltgres',
215 });
216});
217
218authCoreFdiRouter.post(`${FDI}/signout`, async (c) => {
219 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
220 const handle =
221 c.req.header('x-briven-session-handle') ??
222 (() => {
223 const cookie = c.req.header('cookie') ?? '';
224 const m = /(?:^|;\s*)sAccessToken=([^;]+)/.exec(cookie);
225 return m?.[1] ? decodeURIComponent(m[1]) : undefined;
226 })();
227 if (handle) await revokeEngineSession(handle);
228 c.header(
229 'Set-Cookie',
230 'sAccessToken=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0',
231 { append: true },
232 );
233 c.header(
234 'Set-Cookie',
235 'sRefreshToken=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0',
236 { append: true },
237 );
238 return c.json({ status: 'OK', engine: 'briven-engine' });
239});
240
241/** Passwordless: create email/SMS code (magic link + OTP). Phase 3. */
242authCoreFdiRouter.post(`${FDI}/signinup/code`, async (c) => {
243 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
244 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
245 let body: {
246 email?: string;
247 phoneNumber?: string;
248 flowType?: 'USER_INPUT_CODE' | 'MAGIC_LINK' | 'USER_INPUT_CODE_AND_MAGIC_LINK';
249 magicLinkBaseUrl?: string;
250 } = {};
251 try {
252 body = await c.req.json();
253 } catch {
254 body = {};
255 }
256 const result = await createPasswordlessCode({
257 email: body.email,
258 phoneNumber: body.phoneNumber,
259 projectId: tenant?.projectId,
260 tenantId: tenant?.tenantId,
261 flowType: body.flowType,
262 magicLinkBaseUrl: body.magicLinkBaseUrl,
263 });
264 if (result.status !== 'OK') {
265 return c.json({ ...result, engine: 'briven-engine' }, 400);
266 }
267 return c.json({
268 status: 'OK',
269 engine: 'briven-engine',
270 storage: 'doltgres',
271 preAuthSessionId: result.preAuthSessionId,
272 deviceId: result.deviceId,
273 flowType: result.flowType,
274 channel: result.channel,
275 // Dev-only fields stripped in production inside service
276 userInputCode: result.userInputCode,
277 linkCode: result.linkCode,
278 delivery: result.delivery,
279 });
280});
281
282/** Passwordless: consume OTP or magic link. Phase 3. */
283authCoreFdiRouter.post(`${FDI}/signinup/code/consume`, async (c) => {
284 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
285 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
286 let body: {
287 preAuthSessionId?: string;
288 deviceId?: string;
289 userInputCode?: string;
290 linkCode?: string;
291 } = {};
292 try {
293 body = await c.req.json();
294 } catch {
295 body = {};
296 }
297 if (!body.preAuthSessionId || !body.deviceId) {
298 return c.json(
299 {
300 status: 'BAD_REQUEST',
301 message: 'preAuthSessionId and deviceId required',
302 engine: 'briven-engine',
303 },
304 400,
305 );
306 }
307 const result = await consumePasswordlessCode({
308 preAuthSessionId: body.preAuthSessionId,
309 deviceId: body.deviceId,
310 userInputCode: body.userInputCode,
311 linkCode: body.linkCode,
312 projectId: tenant?.projectId,
313 tenantId: tenant?.tenantId,
314 });
315 if (result.status !== 'OK') {
316 return c.json(
317 { ...result, engine: 'briven-engine' },
318 result.status === 'EXPIRED' ? 401 : 400,
319 );
320 }
321 // accessToken === session handle (Phase 2 cookie contract)
322 setSessionCookies(c, {
323 sessionHandle: result.session.handle,
324 refreshToken: result.session.refreshToken,
325 expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
326 });
327 c.header('x-briven-engine', 'briven-engine');
328 c.header('x-briven-session-handle', result.session.handle);
329 return c.json({
330 status: 'OK',
331 engine: 'briven-engine',
332 storage: 'doltgres',
333 createdNewUser: result.createdNewUser,
334 user: result.user,
335 session: { handle: result.session.handle, userId: result.session.userId },
336 });
337});
338
339/** Social: get Google/GitHub authorisation URL (Phase 4). */
340authCoreFdiRouter.get(`${FDI}/authorisationurl`, async (c) => {
341 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
342 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
343 const thirdPartyId = (c.req.query('thirdPartyId') ?? '') as SupportedSocial;
344 const redirectURI = c.req.query('redirectURI') ?? '';
345 const result = await getAuthorisationUrl({
346 thirdPartyId,
347 redirectURI,
348 projectId: tenant?.projectId ?? c.req.query('projectId') ?? undefined,
349 });
350 if (result.status !== 'OK') {
351 return c.json({ ...result, engine: 'briven-engine' }, 400);
352 }
353 return c.json({ ...result, engine: 'briven-engine', storage: 'doltgres' });
354});
355
356/** Social: complete sign-in with OAuth authorization code */
357authCoreFdiRouter.post(`${FDI}/signinup`, async (c) => {
358 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
359 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
360 let body: {
361 thirdPartyId?: SupportedSocial;
362 redirectURI?: string;
363 code?: string;
364 state?: string;
365 /** Dev-only synthetic profile (step 3 local proof) */
366 testProfile?: {
367 thirdPartyUserId: string;
368 email?: string;
369 emailVerified?: boolean;
370 name?: string;
371 };
372 } = {};
373 try {
374 body = await c.req.json();
375 } catch {
376 body = {};
377 }
378
379 // Development-only: skip real provider when testProfile supplied
380 if (
381 body.testProfile &&
382 body.thirdPartyId &&
383 env.BRIVEN_ENV !== 'production'
384 ) {
385 const result = await signInUpWithThirdPartyProfile({
386 profile: {
387 thirdPartyId: body.thirdPartyId,
388 thirdPartyUserId: body.testProfile.thirdPartyUserId,
389 email: body.testProfile.email ?? null,
390 emailVerified: body.testProfile.emailVerified ?? true,
391 name: body.testProfile.name ?? null,
392 },
393 projectId: tenant?.projectId,
394 tenantId: tenant?.tenantId,
395 });
396 if (result.status !== 'OK') {
397 return c.json({ ...result, engine: 'briven-engine' }, 400);
398 }
399 setSessionCookies(c, {
400 sessionHandle: result.session.handle,
401 refreshToken: result.session.refreshToken,
402 expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
403 });
404 c.header('x-briven-engine', 'briven-engine');
405 c.header('x-briven-session-handle', result.session.handle);
406 return c.json({
407 status: 'OK',
408 engine: 'briven-engine',
409 storage: 'doltgres',
410 createdNewUser: result.createdNewUser,
411 user: result.user,
412 session: { handle: result.session.handle, userId: result.session.userId },
413 mode: 'test_profile',
414 });
415 }
416
417 if (!body.thirdPartyId || !body.code || !body.redirectURI) {
418 return c.json(
419 {
420 status: 'BAD_REQUEST',
421 engine: 'briven-engine',
422 message: 'thirdPartyId, code, redirectURI required (or testProfile in dev)',
423 },
424 400,
425 );
426 }
427
428 const result = await signInUpWithCode({
429 thirdPartyId: body.thirdPartyId,
430 code: body.code,
431 redirectURI: body.redirectURI,
432 projectId: tenant?.projectId,
433 state: body.state,
434 });
435 if (result.status !== 'OK') {
436 return c.json({ ...result, engine: 'briven-engine' }, 400);
437 }
438 setSessionCookies(c, {
439 sessionHandle: result.session.handle,
440 refreshToken: result.session.refreshToken,
441 expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
442 });
443 c.header('x-briven-engine', 'briven-engine');
444 c.header('x-briven-session-handle', result.session.handle);
445 return c.json({
446 status: 'OK',
447 engine: 'briven-engine',
448 storage: 'doltgres',
449 createdNewUser: result.createdNewUser,
450 user: result.user,
451 session: { handle: result.session.handle, userId: result.session.userId },
452 });
453});
454
455authCoreFdiRouter.post(`${FDI}/session/refresh`, async (c) => {
456 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
457 // Minimal: client should re-signin if session expired; full refresh later.
458 return c.json({ status: 'UNAUTHORISED' }, 401);
459});
460
461// ─── Phase 5: TOTP MFA ───────────────────────────────────────────────
462
463async function sessionUserId(c: {
464 req: { header: (n: string) => string | undefined; raw: { headers: Headers } };
465}): Promise<string | null> {
466 const result = await verifyAuthCoreSession({
467 url: 'http://local/session',
468 method: 'GET',
469 headers: c.req.raw.headers,
470 cookieHeader: c.req.header('cookie'),
471 });
472 if (!result.ok) return null;
473 return result.session.getUserId();
474}
475
476/** Enroll TOTP (needs existing session). */
477authCoreFdiRouter.post(`${FDI}/totp/setup`, async (c) => {
478 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
479 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
480 const userId = await sessionUserId(c);
481 if (!userId) {
482 return c.json(
483 { status: 'UNAUTHORISED', engine: 'briven-engine', message: 'session required' },
484 401,
485 );
486 }
487 let body: { deviceName?: string } = {};
488 try {
489 body = await c.req.json();
490 } catch {
491 body = {};
492 }
493 const created = await createTotpDevice(userId, body.deviceName ?? 'authenticator', {
494 projectId: tenant?.projectId,
495 tenantId: tenant?.tenantId,
496 });
497 if (!created.ok) {
498 return c.json({ status: 'ERROR', ...created }, 400);
499 }
500 return c.json({
501 status: 'OK',
502 engine: 'briven-engine',
503 storage: 'doltgres',
504 deviceId: created.deviceId,
505 deviceName: created.deviceName,
506 secret: created.secret,
507 otpauthUrl: created.otpauthUrl,
508 });
509});
510
511/** Confirm enroll with first code from authenticator app. */
512authCoreFdiRouter.post(`${FDI}/totp/setup/verify`, async (c) => {
513 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
514 const userId = await sessionUserId(c);
515 if (!userId) {
516 return c.json(
517 { status: 'UNAUTHORISED', engine: 'briven-engine', message: 'session required' },
518 401,
519 );
520 }
521 let body: { deviceId?: string; code?: string } = {};
522 try {
523 body = await c.req.json();
524 } catch {
525 body = {};
526 }
527 if (!body.code) {
528 return c.json({ status: 'BAD_REQUEST', message: 'code required' }, 400);
529 }
530 const v = await verifyAndEnableTotpDevice({
531 userId,
532 deviceId: body.deviceId,
533 code: body.code,
534 });
535 if (!v.ok) {
536 return c.json({ status: 'ERROR', engine: 'briven-engine', message: v.message }, 400);
537 }
538 return c.json({ status: 'OK', engine: 'briven-engine', storage: 'doltgres' });
539});
540
541/**
542 * Second factor after password when MFA_REQUIRED.
543 * Body: { userId, code, tenantId? }
544 */
545authCoreFdiRouter.post(`${FDI}/totp/verify`, async (c) => {
546 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
547 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
548 let body: { userId?: string; code?: string; tenantId?: string } = {};
549 try {
550 body = await c.req.json();
551 } catch {
552 body = {};
553 }
554 if (!body.userId || !body.code) {
555 return c.json(
556 { status: 'BAD_REQUEST', message: 'userId and code required' },
557 400,
558 );
559 }
560 const ok = await verifyUserTotp(body.userId, body.code);
561 if (!ok.ok) {
562 return c.json({ status: 'WRONG_CREDENTIALS_ERROR', engine: 'briven-engine' }, 401);
563 }
564 const tenantId = body.tenantId ?? tenant?.tenantId ?? 'public';
565 const session = await createEngineSession({
566 userId: body.userId,
567 tenantId,
568 });
569 setSessionCookies(c, session);
570 c.header('x-briven-engine', 'briven-engine');
571 c.header('x-briven-session-handle', session.sessionHandle);
572 return c.json({
573 status: 'OK',
574 engine: 'briven-engine',
575 storage: 'doltgres',
576 session: { handle: session.sessionHandle, userId: session.userId },
577 });
578});
579
580authCoreFdiRouter.get(`${FDI}/totp/devices`, async (c) => {
581 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
582 const userId = await sessionUserId(c);
583 if (!userId) {
584 return c.json({ status: 'UNAUTHORISED', engine: 'briven-engine' }, 401);
585 }
586 const list = await listTotpDevices(userId);
587 return c.json({ status: 'OK', ...list });
588});
589
590authCoreFdiRouter.delete(`${FDI}/totp/devices/:deviceId`, async (c) => {
591 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
592 const userId = await sessionUserId(c);
593 if (!userId) {
594 return c.json({ status: 'UNAUTHORISED', engine: 'briven-engine' }, 401);
595 }
596 const r = await removeTotpDevice(userId, c.req.param('deviceId'));
597 return c.json({ status: r.ok ? 'OK' : 'ERROR', engine: 'briven-engine' }, r.ok ? 200 : 404);
598});
599
600// ─── Phase 5: Passkeys (WebAuthn) ────────────────────────────────────
601
602authCoreFdiRouter.post(`${FDI}/webauthn/register/options`, async (c) => {
603 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
604 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
605 const userId = await sessionUserId(c);
606 if (!userId) {
607 return c.json({ status: 'UNAUTHORISED', engine: 'briven-engine' }, 401);
608 }
609 let body: { userName?: string; rpId?: string } = {};
610 try {
611 body = await c.req.json();
612 } catch {
613 body = {};
614 }
615 const result = await createRegistrationOptions({
616 userId,
617 userName: body.userName ?? userId,
618 projectId: tenant?.projectId,
619 tenantId: tenant?.tenantId,
620 rpId: body.rpId,
621 });
622 if (result.status !== 'OK') {
623 return c.json({ ...result, engine: 'briven-engine' }, 400);
624 }
625 return c.json({ ...result, engine: 'briven-engine', storage: 'doltgres' });
626});
627
628authCoreFdiRouter.post(`${FDI}/webauthn/register/finish`, async (c) => {
629 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
630 const userId = await sessionUserId(c);
631 if (!userId) {
632 return c.json({ status: 'UNAUTHORISED', engine: 'briven-engine' }, 401);
633 }
634 let body: {
635 challengeId?: string;
636 credentialId?: string;
637 publicKey?: string;
638 transports?: string[];
639 response?: unknown;
640 rpId?: string;
641 expectedOrigin?: string;
642 } = {};
643 try {
644 body = await c.req.json();
645 } catch {
646 body = {};
647 }
648 if (!body.challengeId) {
649 return c.json({ status: 'BAD_REQUEST', message: 'challengeId required' }, 400);
650 }
651 const result = await finishRegistration({
652 userId,
653 challengeId: body.challengeId,
654 credentialId: body.credentialId,
655 publicKey: body.publicKey,
656 transports: body.transports,
657 // eslint-disable-next-line @typescript-eslint/no-explicit-any
658 response: body.response as any,
659 rpId: body.rpId,
660 expectedOrigin: body.expectedOrigin,
661 });
662 if (result.status !== 'OK') {
663 return c.json({ ...result, engine: 'briven-engine' }, 400);
664 }
665 return c.json({ ...result, engine: 'briven-engine', storage: 'doltgres' });
666});
667
668authCoreFdiRouter.post(`${FDI}/webauthn/signin/options`, async (c) => {
669 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
670 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
671 let body: { userId?: string; rpId?: string } = {};
672 try {
673 body = await c.req.json();
674 } catch {
675 body = {};
676 }
677 const result = await createAuthenticationOptions({
678 userId: body.userId,
679 projectId: tenant?.projectId,
680 tenantId: tenant?.tenantId,
681 rpId: body.rpId,
682 });
683 if (result.status !== 'OK') {
684 return c.json({ ...result, engine: 'briven-engine' }, 400);
685 }
686 return c.json({ ...result, engine: 'briven-engine', storage: 'doltgres' });
687});
688
689authCoreFdiRouter.post(`${FDI}/webauthn/signin/finish`, async (c) => {
690 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
691 const tenant = resolveAuthTenantFromHeaders((n) => c.req.header(n));
692 let body: {
693 challengeId?: string;
694 credentialId?: string;
695 response?: unknown;
696 rpId?: string;
697 expectedOrigin?: string;
698 } = {};
699 try {
700 body = await c.req.json();
701 } catch {
702 body = {};
703 }
704 if (!body.challengeId) {
705 return c.json({ status: 'BAD_REQUEST', message: 'challengeId required' }, 400);
706 }
707 const result = await finishAuthentication({
708 challengeId: body.challengeId,
709 credentialId: body.credentialId,
710 // eslint-disable-next-line @typescript-eslint/no-explicit-any
711 response: body.response as any,
712 projectId: tenant?.projectId,
713 rpId: body.rpId,
714 expectedOrigin: body.expectedOrigin,
715 });
716 if (result.status !== 'OK') {
717 return c.json({ ...result, engine: 'briven-engine' }, 400);
718 }
719 setSessionCookies(c, {
720 sessionHandle: result.session.handle,
721 refreshToken: result.session.refreshToken,
722 expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
723 });
724 c.header('x-briven-engine', 'briven-engine');
725 c.header('x-briven-session-handle', result.session.handle);
726 return c.json({
727 status: 'OK',
728 engine: 'briven-engine',
729 storage: 'doltgres',
730 verified: result.verified,
731 userId: result.userId,
732 session: { handle: result.session.handle, userId: result.session.userId },
733 });
734});
735
736authCoreFdiRouter.get(`${FDI}/webauthn/credentials`, async (c) => {
737 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
738 const userId = await sessionUserId(c);
739 if (!userId) {
740 return c.json({ status: 'UNAUTHORISED', engine: 'briven-engine' }, 401);
741 }
742 const list = await listPasskeys(userId);
743 return c.json({ status: 'OK', ...list });
744});
745
746authCoreFdiRouter.delete(`${FDI}/webauthn/credentials/:id`, async (c) => {
747 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
748 const userId = await sessionUserId(c);
749 if (!userId) {
750 return c.json({ status: 'UNAUTHORISED', engine: 'briven-engine' }, 401);
751 }
752 const r = await deletePasskey(userId, c.req.param('id'));
753 return c.json({ status: r.ok ? 'OK' : 'ERROR', engine: 'briven-engine' }, r.ok ? 200 : 404);
754});
755
756authCoreFdiRouter.all(`${FDI}/*`, async (c) => {
757 if (!isAuthCoreInitialized()) return c.json(notReady(), 503);
758 return c.json(
759 {
760 code: 'auth_core_fdi_partial',
761 engine: 'briven-engine',
762 storage: 'doltgres',
763 message:
764 'Path not implemented. Working: EP, passwordless, social, TOTP MFA, passkeys.',
765 path: c.req.path,
766 },
767 404,
768 );
769});
770
771