// Handle -- the human-readable name for an account, and a real domain name.
//
// https://atproto.com/specs/handle
// Ported from @atproto/syntax packages/syntax/src/handle.ts.
//
// Two things about handles that the type cannot express and callers keep
// getting wrong:
//
// - A handle is not an identity. It is a domain name pointing at a DID, and
// it can be reassigned. Store the DID; display the handle. This is why
// `AtIdentifier` exists and why nothing in this library keys anything by
// handle.
// - Validity is not resolvability, and neither is `is_valid_tld`. A handle
// can be perfectly well-formed, have an allowed TLD, and belong to nobody.
// Only resolution answers that, and resolution needs the network.
///|
/// The DNS cap, and the reason handles are bounded at all.
const HANDLE_MAX_LENGTH : Int = 253
///|
const HANDLE_MAX_LABEL_LENGTH : Int = 63
///|
/// What a PDS returns in place of a handle when it has one on file but could
/// not bidirectionally verify it against the DID document. It is deliberately
/// itself a syntactically valid handle, so it flows through code that does not
/// check -- which is precisely why code that displays a handle should.
pub const INVALID_HANDLE : String = "handle.invalid"
///|
/// TLDs a handle may not use. Policy, not syntax: these strings parse fine and
/// are refused for what they mean, so `parse` does not consult this list and
/// `is_valid_tld` is a separate question.
///
/// `.test` is deliberately absent -- it is allowed, for development.
pub let disallowed_tlds : Array[String] = [
".local", ".arpa", ".invalid", ".localhost", ".internal", ".example", ".alt",
// Policy could conceivably change on .onion some day.
".onion",
]
///|
/// A syntactically valid handle. Always lower-case: `parse` normalizes, because
/// handles are domain names and DNS is case-insensitive, so treating
/// `Alice.BSky.social` and `alice.bsky.social` as different values would be a
/// bug waiting to be written.
pub struct Handle(String) derive(Eq, Debug)
///|
pub impl Show for Handle with fn output(self, logger) {
logger.write_string(self.0)
}
///|
pub fn Handle::to_string(self : Self) -> String {
self.0
}
///|
/// Trusts the caller, and does NOT normalize -- `unchecked` means unchecked.
pub fn Handle::unchecked(handle : String) -> Handle {
Handle(handle)
}
///|
pub fn Handle::is_valid(handle : String) -> Bool {
try {
Handle::parse(handle) |> ignore
true
} catch {
_ => false
}
}
///|
/// Lower-casing is safe to do before validating rather than after because the
/// character-set gate rejects everything outside ASCII, so no locale-dependent
/// case mapping can apply.
pub fn Handle::parse(handle : String) -> Handle raise SyntaxError {
fn bad(reason : String) -> SyntaxError {
SyntaxError(kind=Handle, input=handle, reason~)
}
guard all_chars(handle, is_dns_char) else {
raise bad(
"Disallowed characters in handle (ASCII letters, digits, dashes, periods only)",
)
}
guard handle.length() <= HANDLE_MAX_LENGTH else {
raise bad("Handle is too long (\{HANDLE_MAX_LENGTH} chars max)")
}
let labels = handle.split(".").collect()
guard labels.length() >= 2 else {
raise bad("Handle domain needs at least two parts")
}
for label in labels {
guard label.length() > 0 else { raise bad("Handle parts can not be empty") }
guard label.length() <= HANDLE_MAX_LABEL_LENGTH else {
raise bad("Handle part too long (max \{HANDLE_MAX_LABEL_LENGTH} chars)")
}
guard !code_unit_is(label[0], '-') &&
!code_unit_is(label[label.length() - 1], '-') else {
raise bad("Handle parts can not start or end with hyphens")
}
}
let tld = labels[labels.length() - 1]
guard code_unit_is_ascii_alpha(tld[0]) else {
raise bad("Handle final component (TLD) must start with ASCII letter")
}
Handle(handle.to_lower())
}
///|
/// Whether the handle's TLD is one atproto permits. Separate from `parse`
/// because it is policy: the list has changed and will change again, and a
/// handle that stops being allowed does not retroactively stop being
/// well-formed.
pub fn Handle::is_valid_tld(self : Self) -> Bool {
for tld in disallowed_tlds {
if self.0.has_suffix(tld) {
return false
}
}
true
}
///|
/// True for the `handle.invalid` sentinel a PDS returns when it could not
/// verify a handle against its DID document. Such a value is well-formed, so
/// nothing else in this package will flag it.
pub fn Handle::is_invalid_sentinel(self : Self) -> Bool {
self.0 == INVALID_HANDLE
}