@bluesky-social/syntax
Version:
Validation for atproto identifiers and formats: DID, handle, NSID, AT URI, etc
107 lines (105 loc) • 3.99 kB
JavaScript
;
/*
Grammar:
alpha = "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h" / "i" / "j" / "k" / "l" / "m" / "n" / "o" / "p" / "q" / "r" / "s" / "t" / "u" / "v" / "w" / "x" / "y" / "z" / "A" / "B" / "C" / "D" / "E" / "F" / "G" / "H" / "I" / "J" / "K" / "L" / "M" / "N" / "O" / "P" / "Q" / "R" / "S" / "T" / "U" / "V" / "W" / "X" / "Y" / "Z"
number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0"
delim = "."
segment = alpha *( alpha / number / "-" )
authority = segment *( delim segment )
name = alpha *( alpha )
nsid = authority delim name
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InvalidNsidError = exports.ensureValidNsidRegex = exports.ensureValidNsid = exports.NSID = void 0;
class NSID {
static parse(nsid) {
return new NSID(nsid);
}
static create(authority, name) {
const segments = [...authority.split('.').reverse(), name].join('.');
return new NSID(segments);
}
static isValid(nsid) {
try {
NSID.parse(nsid);
return true;
}
catch (e) {
return false;
}
}
constructor(nsid) {
Object.defineProperty(this, "segments", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
(0, exports.ensureValidNsid)(nsid);
this.segments = nsid.split('.');
}
get authority() {
return this.segments
.slice(0, this.segments.length - 1)
.reverse()
.join('.');
}
get name() {
return this.segments.at(this.segments.length - 1);
}
toString() {
return this.segments.join('.');
}
}
exports.NSID = NSID;
// Human readable constraints on NSID:
// - a valid domain in reversed notation
// - followed by an additional period-separated name, which is camel-case letters
const ensureValidNsid = (nsid) => {
const toCheck = nsid;
// check that all chars are boring ASCII
if (!/^[a-zA-Z0-9.-]*$/.test(toCheck)) {
throw new InvalidNsidError('Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)');
}
if (toCheck.length > 253 + 1 + 63) {
throw new InvalidNsidError('NSID is too long (317 chars max)');
}
const labels = toCheck.split('.');
if (labels.length < 3) {
throw new InvalidNsidError('NSID needs at least three parts');
}
for (let i = 0; i < labels.length; i++) {
const l = labels[i];
if (l.length < 1) {
throw new InvalidNsidError('NSID parts can not be empty');
}
if (l.length > 63) {
throw new InvalidNsidError('NSID part too long (max 63 chars)');
}
if (l.endsWith('-') || l.startsWith('-')) {
throw new InvalidNsidError('NSID parts can not start or end with hyphen');
}
if (/^[0-9]/.test(l) && i === 0) {
throw new InvalidNsidError('NSID first part may not start with a digit');
}
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(l) && i + 1 === labels.length) {
throw new InvalidNsidError('NSID name part must be only letters and digits (and no leading digit)');
}
}
};
exports.ensureValidNsid = ensureValidNsid;
const ensureValidNsidRegex = (nsid) => {
// simple regex to enforce most constraints via just regex and length.
// hand wrote this regex based on above constraints
if (!/^[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})?)$/.test(nsid)) {
throw new InvalidNsidError("NSID didn't validate via regex");
}
if (nsid.length > 253 + 1 + 63) {
throw new InvalidNsidError('NSID is too long (317 chars max)');
}
};
exports.ensureValidNsidRegex = ensureValidNsidRegex;
class InvalidNsidError extends Error {
}
exports.InvalidNsidError = InvalidNsidError;
//# sourceMappingURL=nsid.js.map