refactor: migrate to zod v4

This commit is contained in:
Shibo Lyu 2025-05-22 02:11:42 +08:00 committed by Shibo Lyu
parent fdcb07be23
commit 45d74bc024
16 changed files with 37 additions and 39 deletions

View file

@ -1,4 +1,4 @@
import type z from "zod";
import type { z } from "zod/v4";
import {
type BlahSignedPayload,
blahSignedPayloadSchemaOf,
@ -22,9 +22,7 @@ export class BlahPublicKey {
this.name = id.slice(0, 4) + "..." + id.slice(-4);
}
static async fromPublicKey(
publicKey: CryptoKey,
): Promise<BlahPublicKey> {
static async fromPublicKey(publicKey: CryptoKey): Promise<BlahPublicKey> {
const rawKey = await crypto.subtle.exportKey("raw", publicKey);
const id = bufToHex(rawKey);
return new BlahPublicKey(publicKey, id);
@ -67,7 +65,9 @@ export class BlahPublicKey {
options: SignOrVerifyOptions = {},
): Promise<{ payload: z.infer<P>; key: BlahPublicKey }> {
const signedPayloadSchema = blahSignedPayloadSchemaOf(schema);
const parsed = signedPayloadSchema.parse(signedPayload) as z.infer<P>;
const parsed = signedPayloadSchema.parse(
signedPayload,
) as BlahSignedPayload<z.infer<P>>;
return await BlahPublicKey.verifyPayload(parsed, options);
}

View file

@ -1,7 +1,7 @@
import { expect, test } from "vitest";
import { BlahKeyPair } from "./keypair.ts";
import { z } from "zod";
import { z } from "zod/v4";
import { BlahPublicKey } from "./publicKey.ts";
import type { SignOrVerifyOptions } from "./signAndVerify.ts";

View file

@ -1,6 +1,6 @@
import { expectTypeOf, test } from "vitest";
import z from "zod";
import { z } from "zod/v4";
import type { BlahSignedPayload } from "./mod.ts";
import {
type BlahPayloadSignee,

View file

@ -1,10 +1,10 @@
import z from "zod";
import { z } from "zod/v4";
export function blahPayloadSigneeSchemaOf<P extends z.ZodTypeAny>(schema: P) {
return z.object({
nonce: z.number().int(),
nonce: z.int(),
payload: schema,
timestamp: z.number().int(),
timestamp: z.int(),
id_key: z.string(),
act_key: z.string(),
});

View file

@ -1,7 +1,7 @@
import { expectTypeOf, test } from "vitest";
import { type BlahActKeyRecord, blahActKeyRecordSchema } from "./actKey.ts";
import z from "zod";
import { z } from "zod/v4";
test("BlahActKeyRecord typed correctly", () => {
expectTypeOf<

View file

@ -1,4 +1,4 @@
import z from "zod";
import { z } from "zod/v4";
import { BlahPublicKey } from "../crypto/publicKey.ts";
import { BlahKeyPair } from "../crypto/keypair.ts";
import type { BlahSignedPayload } from "../crypto/signedPayload.ts";
@ -7,7 +7,7 @@ import type { SignOrVerifyOptions } from "../crypto/signAndVerify.ts";
export const blahActKeyRecordSchema = z.object({
typ: z.literal("user_act_key"),
act_key: z.string(),
expire_time: z.number().int(),
expire_time: z.int(),
comment: z.string(),
});
@ -61,8 +61,8 @@ export class BlahActKey {
sigValid = false;
}
const key: BlahPublicKey | BlahKeyPair = keypair ??
await BlahPublicKey.fromID(record.act_key);
const key: BlahPublicKey | BlahKeyPair =
keypair ?? (await BlahPublicKey.fromID(record.act_key));
const fullConfig: Required<ActKeyUpdate> = {
expiresAt: new Date(record.expire_time * 1000),
comment: record.comment,
@ -142,10 +142,10 @@ export class BlahActKey {
): Promise<BlahSignedPayload<P>> {
if (!this.canSign) throw new Error("Cannot sign without a private key");
return await (this.internalKey as BlahKeyPair).signPayload(
payload,
{ ...options, identityKeyId: this.internalIdKeyPublic.id },
);
return await (this.internalKey as BlahKeyPair).signPayload(payload, {
...options,
identityKeyId: this.internalIdKeyPublic.id,
});
}
/**
@ -156,9 +156,7 @@ export class BlahActKey {
*
* @param payload The signed payload to verify.
*/
async verifyPayload<P>(
payload: BlahSignedPayload<P>,
): Promise<P> {
async verifyPayload<P>(payload: BlahSignedPayload<P>): Promise<P> {
if (new Date(payload.signee.timestamp * 1000) > this.internalExpiresAt) {
throw new Error("Key was expired at the time of signing");
}

View file

@ -6,7 +6,7 @@ import {
getIdentityDescriptionFileURL,
identityDescriptionFilePath,
} from "./identityDescription.ts";
import { z } from "zod";
import { z } from "zod/v4";
test("BlahIdentityDescription typed correctly", () => {
expectTypeOf<

View file

@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { blahSignedPayloadSchemaOf } from "../crypto/signedPayload.ts";
import { type BlahActKeyRecord, blahActKeyRecordSchema } from "./actKey.ts";
import {
@ -32,9 +32,7 @@ export const identityDescriptionFilePath = "/.well-known/blah/identity.json";
* @returns The full URL to the identity description file.
* @throws Error if the ID URL format is invalid.
*/
export function getIdentityDescriptionFileURL(
idURL: string,
): string {
export function getIdentityDescriptionFileURL(idURL: string): string {
if (!validateIDURLFormat(idURL)) throw new Error("Invalid ID URL format");
const url = new URL(idURL);
url.pathname = identityDescriptionFilePath;

View file

@ -1,4 +1,4 @@
import type z from "zod";
import type { z } from "zod/v4";
export * from "./identity.ts";

View file

@ -1,6 +1,6 @@
import { expect, test, expectTypeOf } from "vitest";
import z from "zod";
import { z } from "zod/v4";
import {
type BlahProfile,
blahProfileSchema,

View file

@ -1,9 +1,9 @@
import z from "zod";
import { z } from "zod/v4";
/** Schema for Blah user profile. */
export const blahProfileSchema = z.object({
typ: z.literal("profile"),
preferred_chat_server_urls: z.array(z.string().url()),
preferred_chat_server_urls: z.array(z.url()),
id_urls: z.array(z.string().refine(validateIDURLFormat)).min(1),
name: z.string(),
bio: z.string().optional(),
@ -22,14 +22,16 @@ export type BlahProfile = {
export function validateIDURLFormat(url: string): boolean {
try {
const idURL = new URL(url);
return !!idURL &&
return (
!!idURL &&
idURL.protocol === "https:" &&
idURL.pathname === "/" &&
!url.endsWith("/") &&
!idURL.search &&
!idURL.hash &&
!idURL.username &&
!idURL.password;
!idURL.password
);
} catch {
return false;
}

View file

@ -6,7 +6,7 @@
* @module
*/
import type z from "zod";
import type z from "zod/v4";
import {
type BlahRichTextSpan,

View file

@ -1,7 +1,7 @@
import { expectTypeOf, test } from "vitest";
import { type BlahRichText, blahRichTextSchema } from "./richText.ts";
import z from "zod";
import { z } from "zod/v4";
test("BlahRichText typed correctly", () => {
expectTypeOf<

View file

@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { type BlahRichTextSpan, blahRichTextSpanSchema } from "./span.ts";
export const blahRichTextSchema = z.array(blahRichTextSpanSchema);

View file

@ -1,7 +1,7 @@
import { expectTypeOf, test } from "vitest";
import { type BlahRichTextSpan, blahRichTextSpanSchema } from "./span.ts";
import z from "zod";
import { z } from "zod/v4";
test("BlahRichTextSpan typed correctly", () => {
expectTypeOf<BlahRichTextSpan>().toEqualTypeOf<

View file

@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
export const blahRichTextSpanAttributesSchema = z.object({
b: z.boolean().default(false),
@ -8,7 +8,7 @@ export const blahRichTextSpanAttributesSchema = z.object({
m: z.boolean().default(false),
tag: z.boolean().default(false),
spoiler: z.boolean().default(false),
link: z.string().url().optional(),
link: z.url().optional(),
});
export type BlahRichTextSpanAttributes = {