import * as l from "@cerulea/lexicon"; export const Star = l.record("site.hackery.stars.star", { key: "tid" }, { url: l.stringWith({ format: "uri", maxLength: 2048 }), title: l.optional(l.stringWith({ maxLength: 2048 })), comment: l.optional(l.stringWith({ maxGraphemes: 32768 })), createdAt: l.datetime, }); export type Star = l.Infer; export const Follow = l.record("site.hackery.stars.follow", { key: "tid" }, { subject: l.did, createdAt: l.datetime, }); export type Follow = l.Infer; export const Profile = l.object({ avatar: l.optional( l.blob({ accept: ["image/png", "image/jpeg", "image/webp", "image/gif"] }), ), }); // A star's link as it will be stored: a bare http(s) URL short enough for the record. export function starUrl(raw: string | undefined): string | undefined { const url = URL.parse(raw ?? ""); const usable = url && ["http:", "https:"].includes(url.protocol) && url.hostname && !url.username && !url.password && url.href.length <= 2048; return usable ? url.href : undefined; } export type Did = `did:${string}:${string}`; // Only PLC and web DIDs can be resolved here. export const isDid = (value: string | undefined): value is Did => /^did:(plc|web):[a-zA-Z0-9.:%_-]+$/.test(value ?? ""); export type AtUri = { did: string; collection: string; rkey: string }; export const atUri = ({ did, collection, rkey }: AtUri) => `at://${did}/${collection}/${rkey}`; // Our collections all use TID keys, so anything else is rejected. export function parseAtUri(uri: string | undefined): AtUri | undefined { const [, did, collection, rkey] = /^at:\/\/(did:[^/]+)\/([^/]+)\/([234567a-z]{13})$/.exec(uri ?? "") ?? []; if (did && l.atUri.validate(uri).success) return { did, collection, rkey }; }