ditto/src/schema.ts

125 lines
3.3 KiB
TypeScript
Raw Normal View History

import { verifySignature, z } from '@/deps.ts';
2023-03-05 04:10:56 +00:00
2023-04-29 20:49:22 +00:00
import type { Event } from './event.ts';
const optionalString = z.string().optional().catch(undefined);
2023-05-12 04:49:32 +00:00
/** Validates individual items in an array, dropping any that aren't valid. */
function filteredArray<T extends z.ZodTypeAny>(schema: T) {
return z.any().array().catch([])
.transform((arr) => (
arr.map((item) => {
const parsed = schema.safeParse(item);
return parsed.success ? parsed.data : undefined;
}).filter((item): item is z.infer<T> => Boolean(item))
));
}
const jsonSchema = z.string().transform((value, ctx) => {
try {
2023-07-08 23:41:11 +00:00
return JSON.parse(value) as unknown;
} catch (_e) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid JSON' });
return z.NEVER;
}
});
2023-04-29 20:49:22 +00:00
const metaContentSchema = z.object({
name: optionalString,
about: optionalString,
picture: optionalString,
banner: optionalString,
nip05: optionalString,
lud16: optionalString,
});
/** Author metadata from Event<0>. */
type MetaContent = z.infer<typeof metaContentSchema>;
/**
* Get (and validate) data from a kind 0 event.
* https://github.com/nostr-protocol/nips/blob/master/01.md
*/
function parseMetaContent(event: Event<0>): MetaContent {
2023-04-29 20:49:22 +00:00
try {
const json = JSON.parse(event.content);
2023-06-11 19:41:16 +00:00
return metaContentSchema.passthrough().parse(json);
2023-04-29 20:49:22 +00:00
} catch (_e) {
return {};
}
}
2023-04-11 00:34:00 +00:00
/** Alias for `safeParse`, but instead of returning a success object it returns the value (or undefined on fail). */
function parseValue<T>(schema: z.ZodType<T>, value: unknown): T | undefined {
2023-04-08 02:37:16 +00:00
const result = schema.safeParse(value);
return result.success ? result.data : undefined;
}
2023-04-11 00:34:00 +00:00
const parseRelay = (relay: string | URL) => parseValue(relaySchema, relay);
2023-04-08 02:37:16 +00:00
const relaySchema = z.custom<URL>((relay) => {
if (typeof relay !== 'string') return false;
try {
const { protocol } = new URL(relay);
return protocol === 'wss:' || protocol === 'ws:';
} catch (_e) {
return false;
}
});
const nostrIdSchema = z.string().regex(/^[0-9a-f]{64}$/);
const eventSchema = z.object({
id: nostrIdSchema,
kind: z.number(),
tags: z.array(z.array(z.string())),
content: z.string(),
created_at: z.number(),
pubkey: nostrIdSchema,
sig: z.string(),
2023-07-08 23:41:11 +00:00
});
const signedEventSchema = eventSchema.refine(verifySignature);
2023-05-12 04:49:32 +00:00
const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()]);
2023-07-08 23:41:11 +00:00
/** https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */
const decode64Schema = z.string().transform((value, ctx) => {
try {
const binString = atob(value);
const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)!);
return new TextDecoder().decode(bytes);
} catch (_e) {
2023-07-09 02:01:49 +00:00
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid base64', fatal: true });
2023-07-08 23:41:11 +00:00
return z.NEVER;
}
});
2023-07-09 21:08:49 +00:00
/** Transforms a string into a `URL` object. */
const urlTransformSchema = z.string().transform((val, ctx) => {
try {
return new URL(val);
} catch (_e) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid URI',
fatal: true,
});
return z.NEVER;
}
});
2023-05-12 04:49:32 +00:00
export {
2023-07-08 23:41:11 +00:00
decode64Schema,
2023-05-12 04:49:32 +00:00
emojiTagSchema,
filteredArray,
jsonSchema,
type MetaContent,
metaContentSchema,
parseMetaContent,
parseRelay,
relaySchema,
2023-07-08 23:41:11 +00:00
signedEventSchema,
2023-07-09 21:08:49 +00:00
urlTransformSchema,
2023-05-12 04:49:32 +00:00
};