1type Did<TMethod extends string = string> = `did:${TMethod}:${string}`;
2
3type Nsid = `${string}.${string}.${string}`;
4
5type RecordKey = string;
6
7const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$/;
8
9const NSID_RE =
10 /^[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?:\.[a-zA-Z](?:[a-zA-Z0-9]{0,62})?)$/;
11
12const RECORD_KEY_RE = /^(?!\.{1,2}$)[a-zA-Z0-9_~.:-]{1,512}$/;
13
14export const ATURI_RE =
15 /^at:\/\/([a-zA-Z0-9._:%-]+)(?:\/([a-zA-Z0-9-.]+)(?:\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/;
16
17const isDid = (input: unknown): input is Did => {
18 return (
19 typeof input === "string" && input.length >= 7 && input.length <= 2048 && DID_RE.test(input)
20 );
21};
22
23const isNsid = (input: unknown): input is Nsid => {
24 return (
25 typeof input === "string" && input.length >= 5 && input.length <= 317 && NSID_RE.test(input)
26 );
27};
28
29const isRecordKey = (input: unknown): input is RecordKey => {
30 return (
31 typeof input === "string" &&
32 input.length >= 1 &&
33 input.length <= 512 &&
34 RECORD_KEY_RE.test(input)
35 );
36};
37
38export interface AddressedAtUri {
39 repo: Did;
40 collection: Nsid;
41 rkey: string;
42 fragment: string | undefined;
43}
44
45export const parseAddressedAtUri = (str: string): AddressedAtUri => {
46 const match = ATURI_RE.exec(str);
47 assert(match !== null, `invalid addressed-at-uri: ${str}`);
48
49 const [, r, c, k, f] = match;
50 assert(isDid(r), `invalid repo in addressed-at-uri: ${r}`);
51 assert(isNsid(c), `invalid collection in addressed-at-uri: ${c}`);
52 assert(isRecordKey(k), `invalid rkey in addressed-at-uri: ${k}`);
53
54 return {
55 repo: r,
56 collection: c,
57 rkey: k,
58 fragment: f,
59 };
60};
61
62function assert(condition: boolean, message: string): asserts condition {
63 if (!condition) {
64 throw new Error(message);
65 }
66}