fix(verifayda): format nested Fayda address objects into strings

This commit is contained in:
ghost2023
2026-08-02 00:11:39 +03:00
parent 6af63717ea
commit aaf0aa3c92
6 changed files with 173 additions and 8 deletions

View File

@@ -7,12 +7,15 @@ export interface GenerateClientAssertionInput {
expiresIn?: string;
}
// Mirrors the National ID Program's own reference implementation
// (fayda-auth-python): plain {alg: RS256} header, no kid, no jti — eSignet
// resolves the verification key from client_id alone.
export async function generateClientAssertion(
input: GenerateClientAssertionInput,
): Promise<string> {
const privateKey = await importJWK(input.privateJwk, 'RS256');
return new SignJWT({})
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
.setProtectedHeader({ alg: 'RS256' })
.setIssuer(input.clientId)
.setSubject(input.clientId)
.setAudience(input.audience)

View File

@@ -396,13 +396,63 @@ export class VerifaydaService {
);
}
// Like name/gender, address is flattened by eSignet into top-level
// `address#en` / `address#am` keys — but since it's a structured claim,
// each of those is itself an object whose *leaf* fields carry the same
// locale suffix again, e.g.
// `address#en: { "zone#en": "...", "region#en": "...", "woreda#en": "..." }`.
private static readonly ADDRESS_FIELD_ORDER = [
'houseNumber',
'kebele',
'woreda',
'city',
'subCity',
'zone',
'region',
'postalCode',
'country',
];
private formatFaydaAddress(
address: Record<string, unknown> | undefined,
locale: 'en' | 'am',
): string | undefined {
if (!address) return undefined;
const formatted = address[`formatted#${locale}`];
if (typeof formatted === 'string' && formatted.trim()) return formatted;
const suffix = `#${locale}`;
const byField = new Map<string, string>();
for (const [key, value] of Object.entries(address)) {
if (!key.endsWith(suffix) || typeof value !== 'string' || !value.trim()) continue;
byField.set(key.slice(0, -suffix.length), value);
}
const ordered = VerifaydaService.ADDRESS_FIELD_ORDER.filter((f) =>
byField.has(f),
).map((f) => byField.get(f)!);
const rest = [...byField.entries()]
.filter(([f]) => !VerifaydaService.ADDRESS_FIELD_ORDER.includes(f))
.map(([, v]) => v);
const parts = [...ordered, ...rest];
return parts.length > 0 ? parts.join(', ') : undefined;
}
private normalizeUserInfo(raw: FaydaUserInfo): NormalizedFaydaUserInfo {
const nameEn = raw['name#en'] as string | undefined;
const nameAm = raw['name#am'] as string | undefined;
const genderEn = raw['gender#en'] as string | undefined;
const genderAm = raw['gender#am'] as string | undefined;
const addressEn = raw['address#en'] as string | undefined;
const addressAm = raw['address#am'] as string | undefined;
const addressEn = this.formatFaydaAddress(
raw['address#en'] as Record<string, unknown> | undefined,
'en',
);
const addressAm = this.formatFaydaAddress(
raw['address#am'] as Record<string, unknown> | undefined,
'am',
);
const rawPhone = (raw.phone_number ?? raw['phone_number#en'] ?? raw['phone_number#am'] ?? raw.phone) as string | undefined;
return {

View File

@@ -21,7 +21,8 @@ export interface FaydaUserInfo {
gender?: string;
birthdate?: string;
picture?: string;
address?: Record<string, unknown>;
'address#en'?: Record<string, unknown>;
'address#am'?: Record<string, unknown>;
[key: string]: unknown;
}