feat: Add identity description file URL utilities

Add utilities to work with identity description file URLs,
including validation of ID URL format and path construction.
Also export these functions in the public API.
This commit is contained in:
Shibo Lyu 2025-04-30 22:33:34 +08:00
parent 2c84f4dee7
commit 6e8d77b11a
5 changed files with 99 additions and 5 deletions

View file

@ -1,11 +1,27 @@
import {
type BlahIdentityDescription,
blahIdentityDescriptionSchema,
getIdentityDescriptionFileURL,
identityDescriptionFilePath,
} from "./identityDescription.ts";
import { assertTypeMatchesZodSchema } from "../test/utils.ts";
import { expect } from "@std/expect";
Deno.test("type BlahIdentityDescription is accurate", () => {
assertTypeMatchesZodSchema<BlahIdentityDescription>(
blahIdentityDescriptionSchema,
);
});
Deno.test("getIdentityDescriptionFileURL", () => {
expect(getIdentityDescriptionFileURL("https://lao.sb")).toBe(
"https://lao.sb" + identityDescriptionFilePath,
);
expect(getIdentityDescriptionFileURL("https://test.lao.sb")).toBe(
"https://test.lao.sb" + identityDescriptionFilePath,
);
expect(() => getIdentityDescriptionFileURL("https://trailing-slash.lao.sb/"))
.toThrow();
});

View file

@ -1,17 +1,42 @@
import { z } from "zod";
import { blahSignedPayloadSchemaOf } from "../crypto/signedPayload.ts";
import { type BlahActKeyRecord, blahActKeyRecordSchema } from "./actKey.ts";
import { type BlahProfile, blahProfileSchema } from "./profile.ts";
import {
type BlahProfile,
blahProfileSchema,
validateIDURLFormat,
} from "./profile.ts";
import type { BlahSignedPayload } from "../crypto/mod.ts";
/** Schema for Blah identity description. */
export const blahIdentityDescriptionSchema = z.object({
id_key: z.string(),
act_keys: z.array(blahSignedPayloadSchemaOf(blahActKeyRecordSchema)).min(1),
profile: blahSignedPayloadSchemaOf(blahProfileSchema),
});
/** Type for Blah identity description. */
export type BlahIdentityDescription = {
id_key: string;
act_keys: Array<BlahSignedPayload<BlahActKeyRecord>>;
profile: BlahSignedPayload<BlahProfile>;
};
/** Path to the identity description file under a given ID URL. */
export const identityDescriptionFilePath = "/.well-known/blah/identity.json";
/**
* Get the full URL to the identity description file for a given ID URL.
*
* @param idURL - The ID URL to get the identity description file URL for.
* @returns The full URL to the identity description file.
* @throws Error if the ID URL format is invalid.
*/
export function getIdentityDescriptionFileURL(
idURL: string,
): string {
if (!validateIDURLFormat(idURL)) throw new Error("Invalid ID URL format");
const url = new URL(idURL);
url.pathname = identityDescriptionFilePath;
return url.toString();
}

View file

@ -5,17 +5,25 @@ export * from "./identity.ts";
import {
type BlahProfile,
blahProfileSchema as internalBlahProfileSchema,
validateIDURLFormat,
} from "./profile.ts";
const blahProfileSchema: z.ZodType<BlahProfile> = internalBlahProfileSchema;
export { type BlahProfile, blahProfileSchema };
export { type BlahProfile, blahProfileSchema, validateIDURLFormat };
import {
type BlahIdentityDescription,
blahIdentityDescriptionSchema as internalBlahIdentityDescriptionSchema,
getIdentityDescriptionFileURL,
identityDescriptionFilePath,
} from "./identityDescription.ts";
const blahIdentityDescriptionSchema: z.ZodType<BlahIdentityDescription> =
internalBlahIdentityDescriptionSchema;
export { type BlahIdentityDescription, blahIdentityDescriptionSchema };
export {
type BlahIdentityDescription,
blahIdentityDescriptionSchema,
getIdentityDescriptionFileURL,
identityDescriptionFilePath,
};
import {
type BlahActKeyRecord,

View file

@ -1,6 +1,36 @@
import { type BlahProfile, blahProfileSchema } from "./profile.ts";
import {
type BlahProfile,
blahProfileSchema,
validateIDURLFormat,
} from "./profile.ts";
import { assertTypeMatchesZodSchema } from "../test/utils.ts";
import { expect } from "@std/expect";
Deno.test("type BlahProfile is accurate", () => {
assertTypeMatchesZodSchema<BlahProfile>(blahProfileSchema);
});
Deno.test("ID URL format - valid", () => {
expect(validateIDURLFormat("https://lao.sb")).toBe(true);
expect(validateIDURLFormat("https://test.lao.sb")).toBe(true);
expect(validateIDURLFormat("https://🧧.lao.sb")).toBe(true);
});
Deno.test("ID URL format - invalid", () => {
// Must be valid URL
expect(validateIDURLFormat("lao.sb")).toBe(false);
// No trailing slash
expect(validateIDURLFormat("https://lao.sb/")).toBe(false);
// No search params
expect(validateIDURLFormat("https://lao.sb?query=1")).toBe(false);
// No fragment
expect(validateIDURLFormat("https://lao.sb#fragment")).toBe(false);
// No path
expect(validateIDURLFormat("https://lao.sb/path")).toBe(false);
// No username
expect(validateIDURLFormat("https://user@lao.sb")).toBe(false);
// No password
expect(validateIDURLFormat("https://user:123@lao.sb")).toBe(false);
// No non-HTTPS protocol
expect(validateIDURLFormat("http://lao.sb")).toBe(false);
});

View file

@ -1,13 +1,15 @@
import z from "zod";
/** Schema for Blah user profile. */
export const blahProfileSchema = z.object({
typ: z.literal("profile"),
preferred_chat_server_urls: z.array(z.string().url()),
id_urls: z.array(z.string().url()).min(1),
id_urls: z.array(z.string().refine(validateIDURLFormat)).min(1),
name: z.string(),
bio: z.string().optional(),
});
/** Type for Blah user profile. */
export type BlahProfile = {
typ: "profile";
preferred_chat_server_urls: string[];
@ -15,3 +17,16 @@ export type BlahProfile = {
name: string;
bio?: string;
};
/** Validate the format of an ID URL. */
export function validateIDURLFormat(url: string): boolean {
const idURL = URL.parse(url);
return !!idURL &&
idURL.protocol === "https:" &&
idURL.pathname === "/" &&
!url.endsWith("/") &&
!idURL.search &&
!idURL.hash &&
!idURL.username &&
!idURL.password;
}