From fc47399e2ca2d28fec3cd44387e3ab83575bccb4 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Wed, 16 Apr 2025 02:10:49 +0800 Subject: [PATCH] feat: Add identity URL validation utilities --- src/lib/idURL.ts | 84 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/lib/idURL.ts diff --git a/src/lib/idURL.ts b/src/lib/idURL.ts new file mode 100644 index 0000000..fa0c503 --- /dev/null +++ b/src/lib/idURL.ts @@ -0,0 +1,84 @@ +import { + BlahIdentity, + blahIdentityDescriptionSchema, + type BlahIdentityDescription +} from '@blah-im/core/identity'; + +export function idURLToUsername(idURL: string): string { + const url = new URL(idURL); + return url.host; +} + +export type IDURLValidity = + | { valid: true } + | ({ valid: false } & ( + | { + reason: + | 'invalid-url' + | 'profile-invalid' + | 'identity-mismatch' + | 'identity-missing-idurl'; + } + | { + reason: 'identity-request-failed'; + status?: number; + errorText?: string; + } + )); +export async function validateIDURL(url: string, identity: BlahIdentity): Promise { + const idURL = URL.parse(url); + if ( + !idURL || + idURL.protocol !== 'https:' || + idURL.pathname !== '/' || + idURL.search || + idURL.username || + idURL.password + ) + return { valid: false, reason: 'invalid-url' }; + + const profileFileURL = (() => { + let url = idURL; + url.pathname = '/.well-known/blah/identity.json'; + return url.toString(); + })(); + + try { + const response = await fetch(profileFileURL, { + method: 'GET', + headers: { Accept: 'application/json' } + }); + if (!response.ok || response.status !== 200) { + return { + valid: false, + reason: 'identity-request-failed', + status: response.status, + errorText: await response.text() + }; + } + + const identityDescription = await response.json(); + const { data: parsedIdentityDescription } = + blahIdentityDescriptionSchema.safeParse(identityDescription); + if (!parsedIdentityDescription) return { valid: false, reason: 'profile-invalid' }; + + if (parsedIdentityDescription.id_key !== identity.idPublicKey.id) + return { valid: false, reason: 'identity-mismatch' }; + + if (parsedIdentityDescription.profile.signee.payload.id_urls.findIndex((x) => x === url) === -1) + return { valid: false, reason: 'identity-missing-idurl' }; + + const identityFromDescription = + await BlahIdentity.fromIdentityDescription(parsedIdentityDescription); + if (!identityFromDescription.profileSigValid) + return { valid: false, reason: 'profile-invalid' }; + + return { valid: true }; + } catch (e) { + return { + valid: false, + reason: 'identity-request-failed', + errorText: (e as Error).message + }; + } +}