~james/core

core/web/src/lib/api/syntax.ts -rw-r--r-- 3.5 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import {
	isCid,
	isDatetime,
	isDid,
	isNsid,
	isRecordKey,
	isTid,
	parseCanonicalResourceUri
} from "@atcute/lexicons/syntax";
import type {
	CanonicalResourceUri,
	Cid as RawCid,
	Datetime as RawDatetime,
	Did as RawDid,
	Nsid as RawNsid,
	RecordKey as RawRecordKey,
	ResourceUri as RawResourceUri,
	Tid as RawTid
} from "@atcute/lexicons/syntax";
export type { RawCid, RawDatetime, RawResourceUri };

// asserting wrappers over atcute's syntax guards for xrpc input boundaries.
// untrusted strings (user input, route params, storage) get rejected here
// instead of traveling to the server and failing as a 400.
declare const tag: unique symbol;

type Lewitype<T, N extends string> = T & { readonly [tag]: N };

export type Did = Lewitype<RawDid, "Did">;
export type Nsid = Lewitype<RawNsid, "Nsid">;
export type Rkey = Lewitype<RawRecordKey, "Rkey">;
export type Tid = Lewitype<RawTid, "Tid">;
export type Cid = Lewitype<RawCid, "Cid">;
export type AtUri = Lewitype<CanonicalResourceUri, "AtUri">;
export type Datetime = Lewitype<RawDatetime, "Datetime">;
export type KnotOrigin = Lewitype<string, "KnotOrigin">;
export type GitOid = Lewitype<string, "GitOid">;
export type LabelKey = Lewitype<string, "LabelKey">;
export type Capability = Lewitype<string, "Capability">;
export type ProxyPath = Lewitype<string, "ProxyPath">;

const invalid = (kind: string, value: string): Error =>
	new Error(`"${value}" is not a valid ${kind}.`);

export const didOf = (value: string): Did => {
	if (!isDid(value)) throw invalid("DID", value);
	return value as Did;
};

export const nsidOf = (value: string): Nsid => {
	if (!isNsid(value)) throw invalid("NSID", value);
	return value as Nsid;
};

export const recordKeyOf = (value: string): Rkey => {
	if (!isRecordKey(value)) throw invalid("rkey", value);
	return value as Rkey;
};

export const tidOf = (value: string): Tid => {
	if (!isTid(value)) throw invalid("TID", value);
	return value as Tid;
};

export const cidOf = (value: string): Cid => {
	if (!isCid(value)) throw invalid("CID", value);
	return value as Cid;
};

export const atUriOf = (value: string): AtUri => {
	parseCanonicalResourceUri(value);
	return value as AtUri;
};

export const datetimeOf = (value: string): Datetime => {
	if (!isDatetime(value)) throw invalid("datetime", value);
	return value as Datetime;
};

export const knotOf = (value: string): KnotOrigin => {
	const parsed = URL.parse(`https://${value}`);
	if (parsed === null || parsed.host !== value || parsed.pathname !== "/")
		throw invalid("knot origin", value);
	return value as KnotOrigin;
};

const GIT_OID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;

export const gitOidOf = (value: string): GitOid => {
	if (!GIT_OID.test(value)) throw invalid("git OID", value);
	return value as GitOid;
};

const LABEL_KEY = /^(?!did:)[A-Za-z0-9._~:-]{1,512}$/;

export const labelKeyOf = (value: string): LabelKey => {
	if (!LABEL_KEY.test(value)) throw invalid("label key", value);
	return value as LabelKey;
};

const CAPABILITY = /^\S+$/;

export const capabilityOf = (value: string): Capability => {
	if (value.length === 0 || value.length > 512 || !CAPABILITY.test(value))
		throw invalid("cap", value);
	return value as Capability;
};

const PROXY_PATH = /^_[A-Za-z0-9.-]+$/;

export const proxyPathOf = (value: string): ProxyPath => {
	if (!PROXY_PATH.test(value)) throw invalid("proxy path", value);
	return value as ProxyPath;
};

export type XrpcPath = Nsid | ProxyPath;

export const xrpcPathOf = (value: string): XrpcPath =>
	value.startsWith("_") ? proxyPathOf(value) : nsidOf(value);