File size: 3,074 Bytes
45a105b
de6cac5
45a105b
 
 
 
 
 
de6cac5
 
45a105b
 
 
de6cac5
45a105b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de6cac5
 
 
 
45a105b
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { apiRequest } from './client'
import { setActiveUser, setToken } from '../auth/session'
import {
  serializeAssertion, serializeRegistration, toCreationOptions, toRequestOptions,
  isWebAuthnSupported, PasskeyCancelled, PasskeyUnsupported,
} from '../auth/passkey'
import type { DemoSeedResponse, EnrollResponse } from '../types'

/** "Load demo identity": seed a tri-modal identity, receive a session token, and
 *  mark this user as the active session user (used by the payment flow). */
export async function demoSeed(userId: string): Promise<DemoSeedResponse> {
  const r = await apiRequest<DemoSeedResponse>('/demo/seed', { body: { user_id: userId } })
  if (r.token) setToken(r.token)
  setActiveUser({ userId: r.user_id, via: 'demo' })
  return r
}

export async function enrollUnified(
  userId: string,
  faceImage: string,
  fingerprintImage?: string,
): Promise<EnrollResponse> {
  const body: Record<string, string> = { user_id: userId, face_image: faceImage }
  if (fingerprintImage) body.fingerprint_image = fingerprintImage
  const r = await apiRequest<EnrollResponse>('/unified/enroll', { body })
  if (r.token) setToken(r.token)
  return r
}

// ---- Passkey / WebAuthn (challenges always from the backend) ----
export async function registerPasskey(userId: string): Promise<boolean> {
  if (!isWebAuthnSupported()) throw new PasskeyUnsupported('WebAuthn not supported')
  const options = await apiRequest<Record<string, unknown>>('/webauthn/register/begin', {
    body: { user_id: userId },
  })
  let cred: PublicKeyCredential | null
  try {
    cred = (await navigator.credentials.create(toCreationOptions(options))) as PublicKeyCredential | null
  } catch (e) {
    throw new PasskeyCancelled((e as Error)?.name ?? 'cancelled')
  }
  if (!cred) throw new PasskeyCancelled('no credential')
  const r = await apiRequest<{ success: boolean }>('/webauthn/register/complete', {
    body: { user_id: userId, credential: serializeRegistration(cred) },
  })
  return !!r.success
}

export async function authenticatePasskey(userId: string): Promise<boolean> {
  if (!isWebAuthnSupported()) throw new PasskeyUnsupported('WebAuthn not supported')
  const options = await apiRequest<Record<string, unknown>>('/webauthn/authenticate/begin', {
    body: { user_id: userId },
  })
  let cred: PublicKeyCredential | null
  try {
    cred = (await navigator.credentials.get(toRequestOptions(options))) as PublicKeyCredential | null
  } catch (e) {
    throw new PasskeyCancelled((e as Error)?.name ?? 'cancelled')
  }
  if (!cred) throw new PasskeyCancelled('no credential')
  const r = await apiRequest<{ success: boolean }>('/webauthn/authenticate/complete', {
    body: { user_id: userId, credential: serializeAssertion(cred) },
  })
  // Passkey login establishes the active user for the payment flow. The demo backend
  // does not require AMANPAY_REQUIRE_AUTH in this trusted environment, so a bearer
  // token is optional here; the active user is what the Pay page uses.
  if (r.success) setActiveUser({ userId, via: 'passkey' })
  return !!r.success
}