import fetchHelper from './fetchHelper' import type { EncryptedBlob } from './passwordCipher' // Shape stored in ad_user.WebAuthnCredentials (Text/CLOB column). // Read returns the canonical PascalCase column name; writes use lowercase-first // (`webAuthnCredentials`) per iDempiere REST casing rules. export interface WebAuthnKeyEntry { credentialId: string // base64url credential id publicKey: string // base64 of Uint8Array (Buffer.toString('base64')) counter: number transports?: string[] nickname: string createdAt: number // ms epoch deviceType?: string // 'singleDevice' | 'multiDevice' backedUp?: boolean } export interface WebAuthnCredentialsPayload { keys: WebAuthnKeyEntry[] password: EncryptedBlob | null } const EMPTY: WebAuthnCredentialsPayload = { keys: [], password: null } function getServiceToken(): string { const config = useRuntimeConfig() const token: string | undefined = (config.api as any)?.idempieretoken if (!token) { throw new Error('IDEMPIERETOKEN env var is not set — cannot read/write ad_user without a user session') } return token } function parseColumn(rec: any): WebAuthnCredentialsPayload { if (!rec) return { ...EMPTY } const raw = rec.WebAuthnCredentials ?? rec.webAuthnCredentials ?? null if (!raw) return { ...EMPTY } try { const obj = typeof raw === 'string' ? JSON.parse(raw) : raw return { keys: Array.isArray(obj?.keys) ? obj.keys : [], password: obj?.password ?? null, } } catch { return { ...EMPTY } } } export async function readCredentials(event: any, userId: number, token?: string): Promise { const tk = token || getServiceToken() const rec: any = await fetchHelper(event, 'models/ad_user/' + userId, 'GET', tk, null) return parseColumn(rec) } export async function writeCredentials(event: any, userId: number, payload: WebAuthnCredentialsPayload, token?: string): Promise { const tk = token || getServiceToken() const serialized = JSON.stringify(payload) await fetchHelper(event, 'models/ad_user/' + userId, 'PUT', tk, { webAuthnCredentials: serialized, tableName: 'AD_User', }) } export async function fetchUserName(event: any, userId: number, token?: string): Promise { const tk = token || getServiceToken() const rec: any = await fetchHelper(event, 'models/ad_user/' + userId, 'GET', tk, null).catch(() => null) if (!rec) return null // iDempiere ad_user has Name (display) and Value (login id). Use Name as the // login identifier — that's what auth/tokens accepts (see login.post.ts and // auto-defaults.post.ts which both query by `name eq '...'`). return rec.Name ?? rec.name ?? rec.Value ?? rec.value ?? null }