auth-core-project.ts688 lines · main
| 1 | /** |
| 2 | * briven-engine per-project config + multitenancy + MFA + passkeys. |
| 3 | * Project routes: dashboard session + project admin. |
| 4 | * MFA/passkey admin: dashboard session. |
| 5 | * DOLTGRES ONLY. |
| 6 | */ |
| 7 | |
| 8 | import { Hono } from 'hono'; |
| 9 | |
| 10 | import { |
| 11 | requireAuthCoreDashboard, |
| 12 | requireAuthCoreProject, |
| 13 | } from '../middleware/auth-core-guard.js'; |
| 14 | import { BRIVEN_ENGINE_ID, isAuthCoreInitialized } from '../services/auth-core/engine.js'; |
| 15 | import { |
| 16 | getBrivenEngineProjectConfig, |
| 17 | setBrivenEngineBranding, |
| 18 | setBrivenEngineMethodFlags, |
| 19 | setBrivenEngineProviderSecrets, |
| 20 | setBrivenEngineSmsSecrets, |
| 21 | type BrivenEngineBranding, |
| 22 | type BrivenEngineMethodFlags, |
| 23 | } from '../services/auth-core/project-config.js'; |
| 24 | import { sendBrivenEngineSmsTest } from '../services/auth-core/delivery.js'; |
| 25 | import { listBrivenEngineAudit } from '../services/auth-core/audit.js'; |
| 26 | import { recordBrivenEngineAudit } from '../services/auth-core/audit.js'; |
| 27 | import { env } from '../env.js'; |
| 28 | import { |
| 29 | ensureBrivenEngineTenant, |
| 30 | listBrivenEngineTenants, |
| 31 | } from '../services/auth-core/multitenancy.js'; |
| 32 | import { |
| 33 | assignBrivenEngineRole, |
| 34 | createBrivenEngineRole, |
| 35 | getBrivenEngineUserRoles, |
| 36 | listBrivenEngineRoles, |
| 37 | } from '../services/auth-core/roles.js'; |
| 38 | import { |
| 39 | createTotpDevice, |
| 40 | listTotpDevices, |
| 41 | removeTotpDevice, |
| 42 | verifyAndEnableTotpDevice, |
| 43 | verifyUserTotp, |
| 44 | } from '../services/auth-core/mfa.js'; |
| 45 | import { |
| 46 | createAuthenticationOptions, |
| 47 | createRegistrationOptions, |
| 48 | deletePasskey, |
| 49 | finishAuthentication, |
| 50 | finishRegistration, |
| 51 | listPasskeys, |
| 52 | } from '../services/auth-core/webauthn.js'; |
| 53 | import { |
| 54 | BRIVEN_ENGINE_SOCIAL_CATALOG, |
| 55 | type BrivenSocialProviderId, |
| 56 | } from '../services/auth-core/providers.js'; |
| 57 | import type { AppEnv } from '../types/app-env.js'; |
| 58 | |
| 59 | const SOCIAL_IDS = new Set( |
| 60 | BRIVEN_ENGINE_SOCIAL_CATALOG.map((p) => p.thirdPartyId), |
| 61 | ); |
| 62 | |
| 63 | export const authCoreProjectRouter = new Hono<AppEnv>(); |
| 64 | |
| 65 | authCoreProjectRouter.use( |
| 66 | '/v1/auth-core/projects/:projectId/*', |
| 67 | ...requireAuthCoreProject('admin'), |
| 68 | ); |
| 69 | authCoreProjectRouter.use('/v1/auth-core/tenants', requireAuthCoreDashboard()); |
| 70 | authCoreProjectRouter.use('/v1/auth-core/roles', requireAuthCoreDashboard()); |
| 71 | authCoreProjectRouter.use('/v1/auth-core/roles/*', requireAuthCoreDashboard()); |
| 72 | authCoreProjectRouter.use('/v1/auth-core/mfa/*', requireAuthCoreDashboard()); |
| 73 | authCoreProjectRouter.use('/v1/auth-core/passkeys/*', requireAuthCoreDashboard()); |
| 74 | authCoreProjectRouter.use('/v1/auth-core/users/*/roles', requireAuthCoreDashboard()); |
| 75 | |
| 76 | authCoreProjectRouter.get('/v1/auth-core/projects/:projectId/config', async (c) => { |
| 77 | const projectId = c.req.param('projectId'); |
| 78 | try { |
| 79 | const config = await getBrivenEngineProjectConfig(projectId); |
| 80 | return c.json(config); |
| 81 | } catch (err) { |
| 82 | return c.json( |
| 83 | { |
| 84 | engine: BRIVEN_ENGINE_ID, |
| 85 | code: 'config_error', |
| 86 | message: err instanceof Error ? err.message : String(err), |
| 87 | }, |
| 88 | 400, |
| 89 | ); |
| 90 | } |
| 91 | }); |
| 92 | |
| 93 | authCoreProjectRouter.put( |
| 94 | '/v1/auth-core/projects/:projectId/providers/:thirdPartyId', |
| 95 | async (c) => { |
| 96 | const projectId = c.req.param('projectId'); |
| 97 | const thirdPartyIdRaw = c.req.param('thirdPartyId'); |
| 98 | const thirdPartyId = thirdPartyIdRaw as BrivenSocialProviderId; |
| 99 | if (!SOCIAL_IDS.has(thirdPartyId)) { |
| 100 | return c.json( |
| 101 | { |
| 102 | engine: BRIVEN_ENGINE_ID, |
| 103 | code: 'bad_request', |
| 104 | message: `unknown OAuth provider: ${thirdPartyIdRaw}`, |
| 105 | }, |
| 106 | 400, |
| 107 | ); |
| 108 | } |
| 109 | let body: { |
| 110 | clientId?: string; |
| 111 | clientSecret?: string; |
| 112 | additionalConfig?: Record<string, string>; |
| 113 | } = {}; |
| 114 | try { |
| 115 | body = await c.req.json(); |
| 116 | } catch { |
| 117 | body = {}; |
| 118 | } |
| 119 | const clientId = body.clientId?.trim() ?? ''; |
| 120 | const clientSecret = body.clientSecret?.trim() ?? ''; |
| 121 | if (!clientId || !clientSecret) { |
| 122 | return c.json( |
| 123 | { |
| 124 | engine: BRIVEN_ENGINE_ID, |
| 125 | code: 'bad_request', |
| 126 | message: 'clientId and clientSecret required (both non-empty)', |
| 127 | }, |
| 128 | 400, |
| 129 | ); |
| 130 | } |
| 131 | try { |
| 132 | const result = await setBrivenEngineProviderSecrets(projectId, { |
| 133 | thirdPartyId, |
| 134 | clientId, |
| 135 | clientSecret, |
| 136 | additionalConfig: body.additionalConfig, |
| 137 | }); |
| 138 | void recordBrivenEngineAudit({ |
| 139 | action: 'config.oauth_secrets.saved', |
| 140 | projectId, |
| 141 | // Never log secret values — provider id only. |
| 142 | metadata: { thirdPartyId }, |
| 143 | }); |
| 144 | // Return public config so UI can show “configured” |
| 145 | const config = await getBrivenEngineProjectConfig(projectId); |
| 146 | const saved = config.providers.find((p) => p.thirdPartyId === thirdPartyId); |
| 147 | return c.json({ |
| 148 | ...result, |
| 149 | config, |
| 150 | savedProvider: saved ?? null, |
| 151 | apiOrigin: env.BRIVEN_API_ORIGIN, |
| 152 | }); |
| 153 | } catch (err) { |
| 154 | return c.json( |
| 155 | { |
| 156 | engine: BRIVEN_ENGINE_ID, |
| 157 | code: 'save_failed', |
| 158 | message: err instanceof Error ? err.message : String(err), |
| 159 | }, |
| 160 | 500, |
| 161 | ); |
| 162 | } |
| 163 | }, |
| 164 | ); |
| 165 | |
| 166 | /** Toggle which sign-in methods this project uses. */ |
| 167 | authCoreProjectRouter.put( |
| 168 | '/v1/auth-core/projects/:projectId/methods', |
| 169 | async (c) => { |
| 170 | const projectId = c.req.param('projectId'); |
| 171 | let body: Partial<BrivenEngineMethodFlags> = {}; |
| 172 | try { |
| 173 | body = await c.req.json(); |
| 174 | } catch { |
| 175 | body = {}; |
| 176 | } |
| 177 | try { |
| 178 | const result = await setBrivenEngineMethodFlags(projectId, body); |
| 179 | void recordBrivenEngineAudit({ |
| 180 | action: 'config.methods.updated', |
| 181 | projectId, |
| 182 | metadata: { methods: result.methods }, |
| 183 | }); |
| 184 | const config = await getBrivenEngineProjectConfig(projectId); |
| 185 | return c.json({ ...result, config }); |
| 186 | } catch (err) { |
| 187 | return c.json( |
| 188 | { |
| 189 | engine: BRIVEN_ENGINE_ID, |
| 190 | code: 'save_failed', |
| 191 | message: err instanceof Error ? err.message : String(err), |
| 192 | }, |
| 193 | 500, |
| 194 | ); |
| 195 | } |
| 196 | }, |
| 197 | ); |
| 198 | |
| 199 | authCoreProjectRouter.put( |
| 200 | '/v1/auth-core/projects/:projectId/delivery/sms', |
| 201 | async (c) => { |
| 202 | const projectId = c.req.param('projectId'); |
| 203 | let body: { |
| 204 | accountSid?: string; |
| 205 | authToken?: string; |
| 206 | fromNumber?: string; |
| 207 | } = {}; |
| 208 | try { |
| 209 | body = await c.req.json(); |
| 210 | } catch { |
| 211 | body = {}; |
| 212 | } |
| 213 | const accountSid = body.accountSid?.trim() ?? ''; |
| 214 | const authToken = body.authToken?.trim() ?? ''; |
| 215 | const fromNumber = body.fromNumber?.trim() ?? ''; |
| 216 | if (!accountSid || !authToken || !fromNumber) { |
| 217 | return c.json( |
| 218 | { |
| 219 | engine: BRIVEN_ENGINE_ID, |
| 220 | code: 'bad_request', |
| 221 | message: 'accountSid, authToken, fromNumber required', |
| 222 | }, |
| 223 | 400, |
| 224 | ); |
| 225 | } |
| 226 | if (!fromNumber.startsWith('+')) { |
| 227 | return c.json( |
| 228 | { |
| 229 | engine: BRIVEN_ENGINE_ID, |
| 230 | code: 'bad_request', |
| 231 | message: |
| 232 | 'fromNumber must be E.164 (start with + and country code), e.g. +15551234567', |
| 233 | }, |
| 234 | 400, |
| 235 | ); |
| 236 | } |
| 237 | try { |
| 238 | const result = await setBrivenEngineSmsSecrets(projectId, { |
| 239 | accountSid, |
| 240 | authToken, |
| 241 | fromNumber, |
| 242 | }); |
| 243 | void recordBrivenEngineAudit({ |
| 244 | action: 'config.sms_secrets.saved', |
| 245 | projectId, |
| 246 | metadata: { fromNumber }, |
| 247 | }); |
| 248 | const config = await getBrivenEngineProjectConfig(projectId); |
| 249 | return c.json({ ...result, config }); |
| 250 | } catch (err) { |
| 251 | return c.json( |
| 252 | { |
| 253 | engine: BRIVEN_ENGINE_ID, |
| 254 | code: 'save_failed', |
| 255 | message: err instanceof Error ? err.message : String(err), |
| 256 | }, |
| 257 | 500, |
| 258 | ); |
| 259 | } |
| 260 | }, |
| 261 | ); |
| 262 | |
| 263 | /** Save login email / hosted UI branding for this project. */ |
| 264 | authCoreProjectRouter.put( |
| 265 | '/v1/auth-core/projects/:projectId/branding', |
| 266 | async (c) => { |
| 267 | const projectId = c.req.param('projectId'); |
| 268 | let body: Partial<BrivenEngineBranding> = {}; |
| 269 | try { |
| 270 | body = await c.req.json(); |
| 271 | } catch { |
| 272 | body = {}; |
| 273 | } |
| 274 | try { |
| 275 | const result = await setBrivenEngineBranding(projectId, body); |
| 276 | void recordBrivenEngineAudit({ |
| 277 | action: 'config.branding.saved', |
| 278 | projectId, |
| 279 | metadata: { |
| 280 | hasLogo: Boolean(result.branding.logoUrl), |
| 281 | primaryColor: result.branding.primaryColor, |
| 282 | }, |
| 283 | }); |
| 284 | const config = await getBrivenEngineProjectConfig(projectId); |
| 285 | return c.json({ ...result, config }); |
| 286 | } catch (err) { |
| 287 | return c.json( |
| 288 | { |
| 289 | engine: BRIVEN_ENGINE_ID, |
| 290 | code: 'save_failed', |
| 291 | message: err instanceof Error ? err.message : String(err), |
| 292 | }, |
| 293 | 500, |
| 294 | ); |
| 295 | } |
| 296 | }, |
| 297 | ); |
| 298 | |
| 299 | /** Security audit trail for this project (newest first). */ |
| 300 | authCoreProjectRouter.get( |
| 301 | '/v1/auth-core/projects/:projectId/audit', |
| 302 | async (c) => { |
| 303 | const projectId = c.req.param('projectId'); |
| 304 | const limit = Number(c.req.query('limit') ?? '50'); |
| 305 | const action = c.req.query('action') ?? null; |
| 306 | const userId = c.req.query('userId') ?? null; |
| 307 | try { |
| 308 | const result = await listBrivenEngineAudit({ |
| 309 | projectId, |
| 310 | limit: Number.isFinite(limit) ? limit : 50, |
| 311 | action, |
| 312 | userId, |
| 313 | }); |
| 314 | return c.json(result); |
| 315 | } catch (err) { |
| 316 | return c.json( |
| 317 | { |
| 318 | engine: BRIVEN_ENGINE_ID, |
| 319 | code: 'audit_list_failed', |
| 320 | message: err instanceof Error ? err.message : String(err), |
| 321 | }, |
| 322 | 500, |
| 323 | ); |
| 324 | } |
| 325 | }, |
| 326 | ); |
| 327 | |
| 328 | /** Send a test SMS with saved project secrets (no login code). */ |
| 329 | authCoreProjectRouter.post( |
| 330 | '/v1/auth-core/projects/:projectId/delivery/sms/test', |
| 331 | async (c) => { |
| 332 | const projectId = c.req.param('projectId'); |
| 333 | let body: { phoneNumber?: string } = {}; |
| 334 | try { |
| 335 | body = await c.req.json(); |
| 336 | } catch { |
| 337 | body = {}; |
| 338 | } |
| 339 | const phoneNumber = body.phoneNumber?.trim() ?? ''; |
| 340 | if (!phoneNumber) { |
| 341 | return c.json( |
| 342 | { |
| 343 | engine: BRIVEN_ENGINE_ID, |
| 344 | code: 'bad_request', |
| 345 | message: 'phoneNumber required (E.164, e.g. +15551234567)', |
| 346 | }, |
| 347 | 400, |
| 348 | ); |
| 349 | } |
| 350 | try { |
| 351 | const config = await getBrivenEngineProjectConfig(projectId); |
| 352 | if (!config.delivery.sms.configured) { |
| 353 | return c.json( |
| 354 | { |
| 355 | engine: BRIVEN_ENGINE_ID, |
| 356 | code: 'sms_not_configured', |
| 357 | ok: false, |
| 358 | message: |
| 359 | 'SMS not set — save Account SID, Auth token, and From number first', |
| 360 | delivery: config.delivery.sms, |
| 361 | methods: config.methods, |
| 362 | }, |
| 363 | 400, |
| 364 | ); |
| 365 | } |
| 366 | const result = await sendBrivenEngineSmsTest({ projectId, phoneNumber }); |
| 367 | const status = result.ok ? 200 : result.mode === 'log' ? 400 : 502; |
| 368 | return c.json( |
| 369 | { |
| 370 | engine: BRIVEN_ENGINE_ID, |
| 371 | ok: result.ok, |
| 372 | delivery: result, |
| 373 | methods: config.methods, |
| 374 | passwordlessSmsEnabled: config.methods.passwordlessSms, |
| 375 | hint: result.ok |
| 376 | ? config.methods.passwordlessSms |
| 377 | ? 'Test sent. passwordless-sms is on for this project.' |
| 378 | : 'Test sent. Turn on passwordless-sms under Providers so apps can use phone login.' |
| 379 | : undefined, |
| 380 | }, |
| 381 | status, |
| 382 | ); |
| 383 | } catch (err) { |
| 384 | return c.json( |
| 385 | { |
| 386 | engine: BRIVEN_ENGINE_ID, |
| 387 | code: 'sms_test_failed', |
| 388 | ok: false, |
| 389 | message: err instanceof Error ? err.message : String(err), |
| 390 | }, |
| 391 | 500, |
| 392 | ); |
| 393 | } |
| 394 | }, |
| 395 | ); |
| 396 | |
| 397 | authCoreProjectRouter.post( |
| 398 | '/v1/auth-core/projects/:projectId/tenant', |
| 399 | async (c) => { |
| 400 | const projectId = c.req.param('projectId'); |
| 401 | const result = await ensureBrivenEngineTenant(projectId); |
| 402 | return c.json(result, result.ok ? 200 : 503); |
| 403 | }, |
| 404 | ); |
| 405 | |
| 406 | authCoreProjectRouter.get('/v1/auth-core/tenants', async (c) => { |
| 407 | const result = await listBrivenEngineTenants(); |
| 408 | return c.json(result, result.ok ? 200 : 503); |
| 409 | }); |
| 410 | |
| 411 | authCoreProjectRouter.get('/v1/auth-core/roles', async (c) => { |
| 412 | return c.json(await listBrivenEngineRoles()); |
| 413 | }); |
| 414 | |
| 415 | authCoreProjectRouter.post('/v1/auth-core/roles', async (c) => { |
| 416 | let body: { |
| 417 | role?: string; |
| 418 | permissions?: string[]; |
| 419 | projectId?: string; |
| 420 | tenantId?: string; |
| 421 | } = {}; |
| 422 | try { |
| 423 | body = await c.req.json(); |
| 424 | } catch { |
| 425 | body = {}; |
| 426 | } |
| 427 | if (!body.role) { |
| 428 | return c.json({ engine: BRIVEN_ENGINE_ID, code: 'role_required' }, 400); |
| 429 | } |
| 430 | return c.json( |
| 431 | await createBrivenEngineRole(body.role, body.permissions ?? [], { |
| 432 | projectId: body.projectId, |
| 433 | tenantId: body.tenantId, |
| 434 | }), |
| 435 | ); |
| 436 | }); |
| 437 | |
| 438 | authCoreProjectRouter.post('/v1/auth-core/roles/assign', async (c) => { |
| 439 | let body: { |
| 440 | userId?: string; |
| 441 | role?: string; |
| 442 | projectId?: string; |
| 443 | tenantId?: string; |
| 444 | } = {}; |
| 445 | try { |
| 446 | body = await c.req.json(); |
| 447 | } catch { |
| 448 | body = {}; |
| 449 | } |
| 450 | if (!body.userId || !body.role) { |
| 451 | return c.json( |
| 452 | { engine: BRIVEN_ENGINE_ID, code: 'userId_and_role_required' }, |
| 453 | 400, |
| 454 | ); |
| 455 | } |
| 456 | return c.json( |
| 457 | await assignBrivenEngineRole(body.userId, body.role, { |
| 458 | projectId: body.projectId, |
| 459 | tenantId: body.tenantId, |
| 460 | }), |
| 461 | ); |
| 462 | }); |
| 463 | |
| 464 | authCoreProjectRouter.get('/v1/auth-core/users/:userId/roles', async (c) => { |
| 465 | return c.json( |
| 466 | await getBrivenEngineUserRoles(c.req.param('userId'), { |
| 467 | projectId: c.req.query('projectId') ?? undefined, |
| 468 | tenantId: c.req.query('tenantId') ?? undefined, |
| 469 | }), |
| 470 | ); |
| 471 | }); |
| 472 | |
| 473 | authCoreProjectRouter.get('/v1/auth-core/roles/list', async (c) => { |
| 474 | return c.json( |
| 475 | await listBrivenEngineRoles({ |
| 476 | projectId: c.req.query('projectId') ?? undefined, |
| 477 | tenantId: c.req.query('tenantId') ?? undefined, |
| 478 | }), |
| 479 | ); |
| 480 | }); |
| 481 | // ─── MFA TOTP ─────────────────────────────────────────────────────── |
| 482 | authCoreProjectRouter.post('/v1/auth-core/mfa/totp', async (c) => { |
| 483 | if (!isAuthCoreInitialized()) { |
| 484 | return c.json({ engine: BRIVEN_ENGINE_ID, code: 'sdk_not_ready' }, 503); |
| 485 | } |
| 486 | let body: { |
| 487 | userId?: string; |
| 488 | deviceName?: string; |
| 489 | projectId?: string; |
| 490 | } = {}; |
| 491 | try { |
| 492 | body = await c.req.json(); |
| 493 | } catch { |
| 494 | body = {}; |
| 495 | } |
| 496 | if (!body.userId) { |
| 497 | return c.json({ engine: BRIVEN_ENGINE_ID, code: 'userId_required' }, 400); |
| 498 | } |
| 499 | const result = await createTotpDevice( |
| 500 | body.userId, |
| 501 | body.deviceName ?? 'default', |
| 502 | { projectId: body.projectId }, |
| 503 | ); |
| 504 | return c.json(result, result.ok ? 200 : 400); |
| 505 | }); |
| 506 | |
| 507 | authCoreProjectRouter.post('/v1/auth-core/mfa/totp/verify', async (c) => { |
| 508 | let body: { |
| 509 | userId?: string; |
| 510 | deviceId?: string; |
| 511 | deviceName?: string; |
| 512 | code?: string; |
| 513 | } = {}; |
| 514 | try { |
| 515 | body = await c.req.json(); |
| 516 | } catch { |
| 517 | body = {}; |
| 518 | } |
| 519 | if (!body.userId || !body.code) { |
| 520 | return c.json( |
| 521 | { engine: BRIVEN_ENGINE_ID, code: 'userId_and_code_required' }, |
| 522 | 400, |
| 523 | ); |
| 524 | } |
| 525 | const result = await verifyAndEnableTotpDevice({ |
| 526 | userId: body.userId, |
| 527 | deviceId: body.deviceId, |
| 528 | deviceName: body.deviceName, |
| 529 | code: body.code, |
| 530 | }); |
| 531 | return c.json(result, result.ok ? 200 : 400); |
| 532 | }); |
| 533 | |
| 534 | authCoreProjectRouter.post('/v1/auth-core/mfa/totp/check', async (c) => { |
| 535 | let body: { userId?: string; code?: string } = {}; |
| 536 | try { |
| 537 | body = await c.req.json(); |
| 538 | } catch { |
| 539 | body = {}; |
| 540 | } |
| 541 | if (!body.userId || !body.code) { |
| 542 | return c.json( |
| 543 | { engine: BRIVEN_ENGINE_ID, code: 'userId_and_code_required' }, |
| 544 | 400, |
| 545 | ); |
| 546 | } |
| 547 | return c.json(await verifyUserTotp(body.userId, body.code)); |
| 548 | }); |
| 549 | |
| 550 | authCoreProjectRouter.get('/v1/auth-core/mfa/totp/:userId', async (c) => { |
| 551 | return c.json(await listTotpDevices(c.req.param('userId'))); |
| 552 | }); |
| 553 | |
| 554 | authCoreProjectRouter.delete( |
| 555 | '/v1/auth-core/mfa/totp/:userId/:deviceName', |
| 556 | async (c) => { |
| 557 | return c.json( |
| 558 | await removeTotpDevice(c.req.param('userId'), c.req.param('deviceName')), |
| 559 | ); |
| 560 | }, |
| 561 | ); |
| 562 | |
| 563 | // ─── Passkeys ─────────────────────────────────────────────────────── |
| 564 | authCoreProjectRouter.post('/v1/auth-core/passkeys/register/options', async (c) => { |
| 565 | let body: { userId?: string; userName?: string; projectId?: string } = {}; |
| 566 | try { |
| 567 | body = await c.req.json(); |
| 568 | } catch { |
| 569 | body = {}; |
| 570 | } |
| 571 | if (!body.userId || !body.userName) { |
| 572 | return c.json( |
| 573 | { engine: BRIVEN_ENGINE_ID, code: 'userId_and_userName_required' }, |
| 574 | 400, |
| 575 | ); |
| 576 | } |
| 577 | const result = await createRegistrationOptions({ |
| 578 | userId: body.userId, |
| 579 | userName: body.userName, |
| 580 | projectId: body.projectId, |
| 581 | }); |
| 582 | return c.json(result, result.status === 'OK' ? 200 : 400); |
| 583 | }); |
| 584 | |
| 585 | authCoreProjectRouter.post('/v1/auth-core/passkeys/register/finish', async (c) => { |
| 586 | let body: { |
| 587 | userId?: string; |
| 588 | challengeId?: string; |
| 589 | credentialId?: string; |
| 590 | publicKey?: string; |
| 591 | transports?: string[]; |
| 592 | response?: unknown; |
| 593 | expectedOrigin?: string; |
| 594 | } = {}; |
| 595 | try { |
| 596 | body = await c.req.json(); |
| 597 | } catch { |
| 598 | body = {}; |
| 599 | } |
| 600 | if (!body.userId || !body.challengeId) { |
| 601 | return c.json({ engine: BRIVEN_ENGINE_ID, code: 'bad_request' }, 400); |
| 602 | } |
| 603 | if (!body.response && (!body.credentialId || !body.publicKey)) { |
| 604 | return c.json( |
| 605 | { |
| 606 | engine: BRIVEN_ENGINE_ID, |
| 607 | code: 'bad_request', |
| 608 | message: 'response (WebAuthn JSON) or credentialId+publicKey required', |
| 609 | }, |
| 610 | 400, |
| 611 | ); |
| 612 | } |
| 613 | const result = await finishRegistration({ |
| 614 | userId: body.userId, |
| 615 | challengeId: body.challengeId, |
| 616 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 617 | response: body.response as any, |
| 618 | credentialId: body.credentialId, |
| 619 | publicKey: body.publicKey, |
| 620 | transports: body.transports, |
| 621 | expectedOrigin: body.expectedOrigin, |
| 622 | }); |
| 623 | return c.json(result, result.status === 'OK' ? 200 : 400); |
| 624 | }); |
| 625 | |
| 626 | authCoreProjectRouter.post('/v1/auth-core/passkeys/authenticate/options', async (c) => { |
| 627 | let body: { userId?: string; projectId?: string } = {}; |
| 628 | try { |
| 629 | body = await c.req.json(); |
| 630 | } catch { |
| 631 | body = {}; |
| 632 | } |
| 633 | const result = await createAuthenticationOptions({ |
| 634 | userId: body.userId, |
| 635 | projectId: body.projectId, |
| 636 | }); |
| 637 | return c.json(result, result.status === 'OK' ? 200 : 400); |
| 638 | }); |
| 639 | |
| 640 | authCoreProjectRouter.post('/v1/auth-core/passkeys/authenticate/finish', async (c) => { |
| 641 | let body: { |
| 642 | challengeId?: string; |
| 643 | credentialId?: string; |
| 644 | projectId?: string; |
| 645 | response?: unknown; |
| 646 | expectedOrigin?: string; |
| 647 | } = {}; |
| 648 | try { |
| 649 | body = await c.req.json(); |
| 650 | } catch { |
| 651 | body = {}; |
| 652 | } |
| 653 | if (!body.challengeId) { |
| 654 | return c.json({ engine: BRIVEN_ENGINE_ID, code: 'bad_request' }, 400); |
| 655 | } |
| 656 | if (!body.response && !body.credentialId) { |
| 657 | return c.json( |
| 658 | { |
| 659 | engine: BRIVEN_ENGINE_ID, |
| 660 | code: 'bad_request', |
| 661 | message: 'response or credentialId required', |
| 662 | }, |
| 663 | 400, |
| 664 | ); |
| 665 | } |
| 666 | const result = await finishAuthentication({ |
| 667 | challengeId: body.challengeId, |
| 668 | credentialId: body.credentialId, |
| 669 | // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 670 | response: body.response as any, |
| 671 | projectId: body.projectId, |
| 672 | expectedOrigin: body.expectedOrigin, |
| 673 | }); |
| 674 | return c.json(result, result.status === 'OK' ? 200 : 400); |
| 675 | }); |
| 676 | |
| 677 | authCoreProjectRouter.get('/v1/auth-core/passkeys/:userId', async (c) => { |
| 678 | return c.json(await listPasskeys(c.req.param('userId'))); |
| 679 | }); |
| 680 | |
| 681 | authCoreProjectRouter.delete( |
| 682 | '/v1/auth-core/passkeys/:userId/:credentialId', |
| 683 | async (c) => { |
| 684 | return c.json( |
| 685 | await deletePasskey(c.req.param('userId'), c.req.param('credentialId')), |
| 686 | ); |
| 687 | }, |
| 688 | ); |