Suite of AT Protocol TypeScript libraries built on web standards
21
fork

Configure Feed

Select the types of activity you want to include in your feed.

syntax

+2880 -56
+1 -1
deno.json
··· 1 1 { 2 - "workspace": ["xrpc-server", "lex-cli", "common"] 2 + "workspace": ["xrpc-server", "lex-cli", "common", "syntax"] 3 3 }
+3 -5
deno.lock
··· 12 12 "jsr:@logtape/file@^1.0.4": "1.0.4", 13 13 "jsr:@logtape/logtape@*": "1.0.4", 14 14 "jsr:@logtape/logtape@^1.0.4": "1.0.4", 15 + "jsr:@std/assert@*": "1.0.14", 15 16 "jsr:@std/assert@^1.0.14": "1.0.14", 16 17 "jsr:@std/bytes@*": "1.0.6", 17 18 "jsr:@std/bytes@^1.0.2": "1.0.6", ··· 41 42 "npm:@atproto/crypto@~0.4.4": "0.4.4", 42 43 "npm:@atproto/lexicon@~0.4.11": "0.4.14", 43 44 "npm:@atproto/lexicon@~0.4.14": "0.4.14", 44 - "npm:@atproto/syntax@~0.4.1": "0.4.1", 45 45 "npm:@atproto/xrpc@0.7": "0.7.4", 46 46 "npm:@ipld/dag-cbor@^9.2.5": "9.2.5", 47 47 "npm:@types/node@*": "24.2.0", 48 48 "npm:cbor-x@*": "1.6.0", 49 49 "npm:jose@*": "6.1.0", 50 - "npm:multiformats@^13.3.6": "13.4.0", 50 + "npm:multiformats@*": "13.4.0", 51 51 "npm:multiformats@^13.4.0": "13.4.0", 52 52 "npm:rate-limiter-flexible@^2.4.1": "2.4.2", 53 53 "npm:uint8arrays@3.0.0": "3.0.0", ··· 373 373 "jsr:@std/path@^1.1.2", 374 374 "jsr:@ts-morph/ts-morph@26", 375 375 "jsr:@zod/zod@^4.1.5", 376 - "npm:@atproto/lexicon@~0.4.14", 377 - "npm:@atproto/syntax@~0.4.1" 376 + "npm:@atproto/lexicon@~0.4.14" 378 377 ] 379 378 }, 380 379 "xrpc-server": { ··· 386 385 "npm:@atproto/crypto@~0.4.4", 387 386 "npm:@atproto/lexicon@~0.4.11", 388 387 "npm:@atproto/xrpc@0.7", 389 - "npm:multiformats@^13.3.6", 390 388 "npm:rate-limiter-flexible@^2.4.1", 391 389 "npm:uint8arrays@3.0.0", 392 390 "npm:ws@^8.12.0"
+1 -1
lex-cli/codegen/client.ts
··· 5 5 VariableDeclarationKind, 6 6 } from "ts-morph"; 7 7 import { type LexiconDoc, Lexicons, type LexRecord } from "@atproto/lexicon"; 8 - import { NSID } from "@atproto/syntax"; 8 + import { NSID } from "@atp/syntax"; 9 9 import type { GeneratedAPI } from "../types.ts"; 10 10 import { gen, lexiconsTs, utilTs } from "./common.ts"; 11 11 import {
+1 -1
lex-cli/codegen/server.ts
··· 5 5 VariableDeclarationKind, 6 6 } from "ts-morph"; 7 7 import { type LexiconDoc, Lexicons } from "@atproto/lexicon"; 8 - import { NSID } from "@atproto/syntax"; 8 + import { NSID } from "@atp/syntax"; 9 9 import type { GeneratedAPI } from "../types.ts"; 10 10 import { gen, lexiconsTs, utilTs } from "./common.ts"; 11 11 import {
+1 -1
lex-cli/codegen/util.ts
··· 1 1 import type { LexiconDoc, LexUserType } from "@atproto/lexicon"; 2 - import { NSID } from "@atproto/syntax"; 2 + import { NSID } from "@atp/syntax"; 3 3 4 4 export interface CodeGenOptions { 5 5 useJsExtension?: boolean;
-1
lex-cli/deno.json
··· 9 9 "@std/fs": "jsr:@std/fs@^1.0.19", 10 10 "@std/path": "jsr:@std/path@^1.1.2", 11 11 "@atproto/lexicon": "npm:@atproto/lexicon@^0.4.14", 12 - "@atproto/syntax": "npm:@atproto/syntax@^0.4.1", 13 12 "ts-morph": "jsr:@ts-morph/ts-morph@^26.0.0", 14 13 "zod": "jsr:@zod/zod@^4.1.5" 15 14 }
syntax/.DS_Store

This is a binary file and will not be displayed.

+138
syntax/aturi-val.ts
··· 1 + import { ensureValidDid, ensureValidDidRegex } from "./did.ts"; 2 + import { ensureValidHandle, ensureValidHandleRegex } from "./handle.ts"; 3 + import { isValidNsid } from "./nsid.ts"; 4 + import { ensureValidRecordKey } from "./recordkey.ts"; 5 + 6 + // Human-readable constraints on ATURI: 7 + // - following regular URLs, a 8KByte hard total length limit 8 + // - follows ATURI docs on website 9 + // - all ASCII characters, no whitespace. non-ASCII could be URL-encoded 10 + // - starts "at://" 11 + // - "authority" is a valid DID or a valid handle 12 + // - optionally, follow "authority" with "/" and valid NSID as start of path 13 + // - optionally, if NSID given, follow that with "/" and rkey 14 + // - rkey path component can include URL-encoded ("percent encoded"), or: 15 + // ALPHA / DIGIT / "-" / "." / "_" / "~" / ":" / "@" / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" 16 + // [a-zA-Z0-9._~:@!$&'\(\)*+,;=-] 17 + // - rkey must have at least one char 18 + // - regardless of path component, a fragment can follow as "#" and then a JSON pointer (RFC-6901) 19 + export const ensureValidAtUri = (uri: string) => { 20 + // JSON pointer is pretty different from rest of URI, so split that out first 21 + const uriParts = uri.split("#"); 22 + if (uriParts.length > 2) { 23 + throw new Error('ATURI can have at most one "#", separating fragment out'); 24 + } 25 + const fragmentPart = uriParts[1] || null; 26 + uri = uriParts[0]; 27 + 28 + // check that all chars are boring ASCII 29 + if (!/^[a-zA-Z0-9._~:@!$&')(*+,;=%/-]*$/.test(uri)) { 30 + throw new Error("Disallowed characters in ATURI (ASCII)"); 31 + } 32 + 33 + const parts = uri.split("/"); 34 + if (parts.length >= 3 && (parts[0] !== "at:" || parts[1].length !== 0)) { 35 + throw new Error('ATURI must start with "at://"'); 36 + } 37 + if (parts.length < 3) { 38 + throw new Error("ATURI requires at least method and authority sections"); 39 + } 40 + 41 + try { 42 + if (parts[2].startsWith("did:")) { 43 + ensureValidDid(parts[2]); 44 + } else { 45 + ensureValidHandle(parts[2]); 46 + } 47 + } catch { 48 + throw new Error("ATURI authority must be a valid handle or DID"); 49 + } 50 + 51 + if (parts.length >= 4) { 52 + if (parts[3].length === 0) { 53 + throw new Error( 54 + "ATURI can not have a slash after authority without a path segment", 55 + ); 56 + } 57 + if (!isValidNsid(parts[3])) { 58 + throw new Error( 59 + "ATURI requires first path segment (if supplied) to be valid NSID", 60 + ); 61 + } 62 + } 63 + 64 + if (parts.length >= 5) { 65 + if (parts[4].length === 0) { 66 + throw new Error( 67 + "ATURI can not have a slash after collection, unless record key is provided", 68 + ); 69 + } 70 + try { 71 + ensureValidRecordKey(parts[4]); 72 + } catch { 73 + throw new Error("ATURI record key must be a valid record key"); 74 + } 75 + } 76 + 77 + if (parts.length >= 6) { 78 + throw new Error( 79 + "ATURI path can have at most two parts, and no trailing slash", 80 + ); 81 + } 82 + 83 + if (uriParts.length >= 2 && fragmentPart == null) { 84 + throw new Error("ATURI fragment must be non-empty and start with slash"); 85 + } 86 + 87 + if (fragmentPart != null) { 88 + if (fragmentPart.length === 0 || fragmentPart[0] !== "/") { 89 + throw new Error("ATURI fragment must be non-empty and start with slash"); 90 + } 91 + // NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace 92 + if (!/^\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\]/-]*$/.test(fragmentPart)) { 93 + throw new Error("Disallowed characters in ATURI fragment (ASCII)"); 94 + } 95 + } 96 + 97 + if (uri.length > 8 * 1024) { 98 + throw new Error("ATURI is far too long"); 99 + } 100 + }; 101 + 102 + export const ensureValidAtUriRegex = (uri: string): void => { 103 + // simple regex to enforce most constraints via just regex and length. 104 + // hand wrote this regex based on above constraints. whew! 105 + const aturiRegex = 106 + /^at:\/\/(?<authority>[a-zA-Z0-9._:%-]+)(\/(?<collection>[a-zA-Z0-9-.]+)(\/(?<rkey>[a-zA-Z0-9._~:-]+))?)?(#(?<fragment>\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 107 + const rm = uri.match(aturiRegex); 108 + if (!rm || !rm.groups) { 109 + throw new Error("ATURI didn't validate via regex"); 110 + } 111 + const groups = rm.groups; 112 + 113 + try { 114 + ensureValidHandleRegex(groups.authority); 115 + } catch { 116 + try { 117 + ensureValidDidRegex(groups.authority); 118 + } catch { 119 + throw new Error("ATURI authority must be a valid handle or DID"); 120 + } 121 + } 122 + 123 + if (groups.collection && !isValidNsid(groups.collection)) { 124 + throw new Error("ATURI collection path segment must be a valid NSID"); 125 + } 126 + 127 + if (groups.rkey) { 128 + try { 129 + ensureValidRecordKey(groups.rkey); 130 + } catch { 131 + throw new Error("ATURI record key must be a valid record key"); 132 + } 133 + } 134 + 135 + if (uri.length > 8 * 1024) { 136 + throw new Error("ATURI is far too long"); 137 + } 138 + };
+136
syntax/aturi.ts
··· 1 + export * from "./aturi-val.ts"; 2 + 3 + export const ATP_URI_REGEX = 4 + // proto- --did-------------- --name---------------- --path---- --query-- --hash-- 5 + /^(at:\/\/)?((?:did:[a-z0-9:%-]+)|(?:[a-z0-9][a-z0-9.:-]*))(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i; 6 + // --path----- --query-- --hash-- 7 + const RELATIVE_REGEX = /^(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i; 8 + 9 + export class AtUri { 10 + hash: string; 11 + host: string; 12 + pathname: string; 13 + searchParams: URLSearchParams; 14 + 15 + constructor(uri: string, base?: string) { 16 + let parsed; 17 + if (base) { 18 + parsed = parse(base); 19 + if (!parsed) { 20 + throw new Error(`Invalid at uri: ${base}`); 21 + } 22 + const relativep = parseRelative(uri); 23 + if (!relativep) { 24 + throw new Error(`Invalid path: ${uri}`); 25 + } 26 + Object.assign(parsed, relativep); 27 + } else { 28 + parsed = parse(uri); 29 + if (!parsed) { 30 + throw new Error(`Invalid at uri: ${uri}`); 31 + } 32 + } 33 + 34 + this.hash = parsed.hash; 35 + this.host = parsed.host; 36 + this.pathname = parsed.pathname; 37 + this.searchParams = parsed.searchParams; 38 + } 39 + 40 + static make(handleOrDid: string, collection?: string, rkey?: string) { 41 + let str = handleOrDid; 42 + if (collection) str += "/" + collection; 43 + if (rkey) str += "/" + rkey; 44 + return new AtUri(str); 45 + } 46 + 47 + get protocol() { 48 + return "at:"; 49 + } 50 + 51 + get origin() { 52 + return `at://${this.host}`; 53 + } 54 + 55 + get hostname() { 56 + return this.host; 57 + } 58 + 59 + set hostname(v: string) { 60 + this.host = v; 61 + } 62 + 63 + get search() { 64 + return this.searchParams.toString(); 65 + } 66 + 67 + set search(v: string) { 68 + this.searchParams = new URLSearchParams(v); 69 + } 70 + 71 + get collection() { 72 + return this.pathname.split("/").filter(Boolean)[0] || ""; 73 + } 74 + 75 + set collection(v: string) { 76 + const parts = this.pathname.split("/").filter(Boolean); 77 + parts[0] = v; 78 + this.pathname = parts.join("/"); 79 + } 80 + 81 + get rkey() { 82 + return this.pathname.split("/").filter(Boolean)[1] || ""; 83 + } 84 + 85 + set rkey(v: string) { 86 + const parts = this.pathname.split("/").filter(Boolean); 87 + if (!parts[0]) parts[0] = "undefined"; 88 + parts[1] = v; 89 + this.pathname = parts.join("/"); 90 + } 91 + 92 + get href() { 93 + return this.toString(); 94 + } 95 + 96 + toString() { 97 + let path = this.pathname || "/"; 98 + if (!path.startsWith("/")) { 99 + path = `/${path}`; 100 + } 101 + let qs = this.searchParams.toString(); 102 + if (qs && !qs.startsWith("?")) { 103 + qs = `?${qs}`; 104 + } 105 + let hash = this.hash; 106 + if (hash && !hash.startsWith("#")) { 107 + hash = `#${hash}`; 108 + } 109 + return `at://${this.host}${path}${qs}${hash}`; 110 + } 111 + } 112 + 113 + function parse(str: string) { 114 + const match = ATP_URI_REGEX.exec(str); 115 + if (match) { 116 + return { 117 + hash: match[5] || "", 118 + host: match[2] || "", 119 + pathname: match[3] || "", 120 + searchParams: new URLSearchParams(match[4] || ""), 121 + }; 122 + } 123 + return undefined; 124 + } 125 + 126 + function parseRelative(str: string) { 127 + const match = RELATIVE_REGEX.exec(str); 128 + if (match) { 129 + return { 130 + hash: match[3] || "", 131 + pathname: match[1] || "", 132 + searchParams: new URLSearchParams(match[2] || ""), 133 + }; 134 + } 135 + return undefined; 136 + }
+115
syntax/datetime.ts
··· 1 + /* Validates datetime string against atproto Lexicon 'datetime' format. 2 + * Syntax is described at: https://atproto.com/specs/lexicon#datetime 3 + */ 4 + export const ensureValidDatetime = (dtStr: string): void => { 5 + const date = new Date(dtStr); 6 + // must parse as ISO 8601; this also verifies semantics like month is not 13 or 00 7 + if (isNaN(date.getTime())) { 8 + throw new InvalidDatetimeError("datetime did not parse as ISO 8601"); 9 + } 10 + if (date.toISOString().startsWith("-")) { 11 + throw new InvalidDatetimeError("datetime normalized to a negative time"); 12 + } 13 + // regex and other checks for RFC-3339 14 + if ( 15 + !/^[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-6][0-9]:[0-6][0-9](.[0-9]{1,20})?(Z|([+-][0-2][0-9]:[0-5][0-9]))$/ 16 + .test( 17 + dtStr, 18 + ) 19 + ) { 20 + throw new InvalidDatetimeError("datetime didn't validate via regex"); 21 + } 22 + if (dtStr.length > 64) { 23 + throw new InvalidDatetimeError("datetime is too long (64 chars max)"); 24 + } 25 + if (dtStr.endsWith("-00:00")) { 26 + throw new InvalidDatetimeError( 27 + 'datetime can not use "-00:00" for UTC timezone', 28 + ); 29 + } 30 + if (dtStr.startsWith("000")) { 31 + throw new InvalidDatetimeError( 32 + "datetime so close to year zero not allowed", 33 + ); 34 + } 35 + }; 36 + 37 + /* Same logic as ensureValidDatetime(), but returns a boolean instead of throwing an exception. 38 + */ 39 + export const isValidDatetime = (dtStr: string): boolean => { 40 + try { 41 + ensureValidDatetime(dtStr); 42 + } catch (err) { 43 + if (err instanceof InvalidDatetimeError) { 44 + return false; 45 + } 46 + throw err; 47 + } 48 + 49 + return true; 50 + }; 51 + 52 + /* Takes a flexible datetime string and normalizes representation. 53 + * 54 + * This function will work with any valid atproto datetime (eg, anything which isValidDatetime() is true for). It *additionally* is more flexible about accepting datetimes that don't comply to RFC 3339, or are missing timezone information, and normalizing them to a valid datetime. 55 + * 56 + * One use-case is a consistent, sortable string. Another is to work with older invalid createdAt datetimes. 57 + * 58 + * Successful output will be a valid atproto datetime with millisecond precision (3 sub-second digits) and UTC timezone with trailing 'Z' syntax. Throws `InvalidDatetimeError` if the input string could not be parsed as a datetime, even with permissive parsing. 59 + * 60 + * Expected output format: YYYY-MM-DDTHH:mm:ss.sssZ 61 + */ 62 + export const normalizeDatetime = (dtStr: string): string => { 63 + if (isValidDatetime(dtStr)) { 64 + const outStr = new Date(dtStr).toISOString(); 65 + if (isValidDatetime(outStr)) { 66 + return outStr; 67 + } 68 + } 69 + 70 + // check if this permissive datetime is missing a timezone 71 + if (!/.*(([+-]\d\d:?\d\d)|[a-zA-Z])$/.test(dtStr)) { 72 + const date = new Date(dtStr + "Z"); 73 + if (!isNaN(date.getTime())) { 74 + const tzStr = date.toISOString(); 75 + if (isValidDatetime(tzStr)) { 76 + return tzStr; 77 + } 78 + } 79 + } 80 + 81 + // finally try parsing as simple datetime 82 + const date = new Date(dtStr); 83 + if (isNaN(date.getTime())) { 84 + throw new InvalidDatetimeError( 85 + "datetime did not parse as any timestamp format", 86 + ); 87 + } 88 + const isoStr = date.toISOString(); 89 + if (isValidDatetime(isoStr)) { 90 + return isoStr; 91 + } else { 92 + throw new InvalidDatetimeError( 93 + "datetime normalized to invalid timestamp string", 94 + ); 95 + } 96 + }; 97 + 98 + /* Variant of normalizeDatetime() which always returns a valid datetime strings. 99 + * 100 + * If a InvalidDatetimeError is encountered, returns the UNIX epoch time as a UTC datetime (1970-01-01T00:00:00.000Z). 101 + */ 102 + export const normalizeDatetimeAlways = (dtStr: string): string => { 103 + try { 104 + return normalizeDatetime(dtStr); 105 + } catch (err) { 106 + if (err instanceof InvalidDatetimeError) { 107 + return new Date(0).toISOString(); 108 + } 109 + throw err; 110 + } 111 + }; 112 + 113 + /* Indicates a datetime string did not pass full atproto Lexicon datetime string format checks. 114 + */ 115 + export class InvalidDatetimeError extends Error {}
+6
syntax/deno.json
··· 1 + { 2 + "name": "@atp/syntax", 3 + "version": "0.1.0-alpha.1", 4 + "exports": "./mod.ts", 5 + "license": "MIT" 6 + }
+58
syntax/did.ts
··· 1 + // Human-readable constraints: 2 + // - valid W3C DID (https://www.w3.org/TR/did-core/#did-syntax) 3 + // - entire URI is ASCII: [a-zA-Z0-9._:%-] 4 + // - always starts "did:" (lower-case) 5 + // - method name is one or more lower-case letters, followed by ":" 6 + // - remaining identifier can have any of the above chars, but can not end in ":" 7 + // - it seems that a bunch of ":" can be included, and don't need spaces between 8 + // - "%" is used only for "percent encoding" and must be followed by two hex characters (and thus can't end in "%") 9 + // - query ("?") and fragment ("#") stuff is defined for "DID URIs", but not as part of identifier itself 10 + // - "The current specification does not take a position on the maximum length of a DID" 11 + // - in current atproto, only allowing did:plc and did:web. But not *forcing* this at lexicon layer 12 + // - hard length limit of 8KBytes 13 + // - not going to validate "percent encoding" here 14 + export const ensureValidDid = (did: string): void => { 15 + if (!did.startsWith("did:")) { 16 + throw new InvalidDidError("DID requires 'did:' prefix"); 17 + } 18 + 19 + // check that all chars are boring ASCII 20 + if (!/^[a-zA-Z0-9._:%-]*$/.test(did)) { 21 + throw new InvalidDidError( 22 + "Disallowed characters in DID (ASCII letters, digits, and a couple other characters only)", 23 + ); 24 + } 25 + 26 + const { length, 1: method } = did.split(":"); 27 + if (length < 3) { 28 + throw new InvalidDidError( 29 + "DID requires prefix, method, and method-specific content", 30 + ); 31 + } 32 + 33 + if (!/^[a-z]+$/.test(method)) { 34 + throw new InvalidDidError("DID method must be lower-case letters"); 35 + } 36 + 37 + if (did.endsWith(":") || did.endsWith("%")) { 38 + throw new InvalidDidError("DID can not end with ':' or '%'"); 39 + } 40 + 41 + if (did.length > 2 * 1024) { 42 + throw new InvalidDidError("DID is too long (2048 chars max)"); 43 + } 44 + }; 45 + 46 + export const ensureValidDidRegex = (did: string): void => { 47 + // simple regex to enforce most constraints via just regex and length. 48 + // hand wrote this regex based on above constraints 49 + if (!/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/.test(did)) { 50 + throw new InvalidDidError("DID didn't validate via regex"); 51 + } 52 + 53 + if (did.length > 2 * 1024) { 54 + throw new InvalidDidError("DID is too long (2048 chars max)"); 55 + } 56 + }; 57 + 58 + export class InvalidDidError extends Error {}
+124
syntax/handle.ts
··· 1 + export const INVALID_HANDLE = "handle.invalid"; 2 + 3 + // Currently these are registration-time restrictions, not protocol-level 4 + // restrictions. We have a couple accounts in the wild that we need to clean up 5 + // before hard-disallow. 6 + // See also: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains 7 + export const DISALLOWED_TLDS = [ 8 + ".local", 9 + ".arpa", 10 + ".invalid", 11 + ".localhost", 12 + ".internal", 13 + ".example", 14 + ".alt", 15 + // policy could concievably change on ".onion" some day 16 + ".onion", 17 + // NOTE: .test is allowed in testing and devopment. In practical terms 18 + // "should" "never" actually resolve and get registered in production 19 + ]; 20 + 21 + // Handle constraints, in English: 22 + // - must be a possible domain name 23 + // - RFC-1035 is commonly referenced, but has been updated. eg, RFC-3696, 24 + // section 2. and RFC-3986, section 3. can now have leading numbers (eg, 25 + // 4chan.org) 26 + // - "labels" (sub-names) are made of ASCII letters, digits, hyphens 27 + // - can not start or end with a hyphen 28 + // - TLD (last component) should not start with a digit 29 + // - can't end with a hyphen (can end with digit) 30 + // - each segment must be between 1 and 63 characters (not including any periods) 31 + // - overall length can't be more than 253 characters 32 + // - separated by (ASCII) periods; does not start or end with period 33 + // - case insensitive 34 + // - domains (handles) are equal if they are the same lower-case 35 + // - punycode allowed for internationalization 36 + // - no whitespace, null bytes, joining chars, etc 37 + // - does not validate whether domain or TLD exists, or is a reserved or 38 + // special TLD (eg, .onion or .local) 39 + // - does not validate punycode 40 + export const ensureValidHandle = (handle: string): void => { 41 + // check that all chars are boring ASCII 42 + if (!/^[a-zA-Z0-9.-]*$/.test(handle)) { 43 + throw new InvalidHandleError( 44 + "Disallowed characters in handle (ASCII letters, digits, dashes, periods only)", 45 + ); 46 + } 47 + 48 + if (handle.length > 253) { 49 + throw new InvalidHandleError("Handle is too long (253 chars max)"); 50 + } 51 + const labels = handle.split("."); 52 + if (labels.length < 2) { 53 + throw new InvalidHandleError("Handle domain needs at least two parts"); 54 + } 55 + for (let i = 0; i < labels.length; i++) { 56 + const l = labels[i]; 57 + if (l.length < 1) { 58 + throw new InvalidHandleError("Handle parts can not be empty"); 59 + } 60 + if (l.length > 63) { 61 + throw new InvalidHandleError("Handle part too long (max 63 chars)"); 62 + } 63 + if (l.endsWith("-") || l.startsWith("-")) { 64 + throw new InvalidHandleError( 65 + "Handle parts can not start or end with hyphens", 66 + ); 67 + } 68 + if (i + 1 === labels.length && !/^[a-zA-Z]/.test(l)) { 69 + throw new InvalidHandleError( 70 + "Handle final component (TLD) must start with ASCII letter", 71 + ); 72 + } 73 + } 74 + }; 75 + 76 + // simple regex translation of above constraints 77 + export const ensureValidHandleRegex = (handle: string): void => { 78 + if ( 79 + !/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/ 80 + .test( 81 + handle, 82 + ) 83 + ) { 84 + throw new InvalidHandleError("Handle didn't validate via regex"); 85 + } 86 + if (handle.length > 253) { 87 + throw new InvalidHandleError("Handle is too long (253 chars max)"); 88 + } 89 + }; 90 + 91 + export const normalizeHandle = (handle: string): string => { 92 + return handle.toLowerCase(); 93 + }; 94 + 95 + export const normalizeAndEnsureValidHandle = (handle: string): string => { 96 + const normalized = normalizeHandle(handle); 97 + ensureValidHandle(normalized); 98 + return normalized; 99 + }; 100 + 101 + export const isValidHandle = (handle: string): boolean => { 102 + try { 103 + ensureValidHandle(handle); 104 + } catch (err) { 105 + if (err instanceof InvalidHandleError) { 106 + return false; 107 + } 108 + throw err; 109 + } 110 + 111 + return true; 112 + }; 113 + 114 + export const isValidTld = (handle: string): boolean => { 115 + return !DISALLOWED_TLDS.some((domain) => handle.endsWith(domain)); 116 + }; 117 + 118 + export class InvalidHandleError extends Error {} 119 + /** @deprecated Never used */ 120 + export class ReservedHandleError extends Error {} 121 + /** @deprecated Never used */ 122 + export class UnsupportedDomainError extends Error {} 123 + /** @deprecated Never used */ 124 + export class DisallowedDomainError extends Error {}
+7
syntax/mod.ts
··· 1 + export * from "./handle.ts"; 2 + export * from "./did.ts"; 3 + export * from "./nsid.ts"; 4 + export * from "./aturi.ts"; 5 + export * from "./tid.ts"; 6 + export * from "./recordkey.ts"; 7 + export * from "./datetime.ts";
+211
syntax/nsid.ts
··· 1 + /* 2 + Grammar: 3 + 4 + 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" 5 + number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0" 6 + delim = "." 7 + segment = alpha *( alpha / number / "-" ) 8 + authority = segment *( delim segment ) 9 + name = alpha *( alpha / number ) 10 + nsid = authority delim name 11 + 12 + */ 13 + 14 + export class NSID { 15 + readonly segments: readonly string[]; 16 + 17 + static parse(input: string): NSID { 18 + return new NSID(input); 19 + } 20 + 21 + static create(authority: string, name: string): NSID { 22 + const input = [...authority.split(".").reverse(), name].join("."); 23 + return new NSID(input); 24 + } 25 + 26 + static isValid(nsid: string) { 27 + return isValidNsid(nsid); 28 + } 29 + 30 + static from(input: { toString: () => string }): NSID { 31 + if (input instanceof NSID) { 32 + // No need to clone, NSID is immutable 33 + return input; 34 + } 35 + if (Array.isArray(input)) { 36 + return new NSID((input as string[]).join(".")); 37 + } 38 + return new NSID(String(input)); 39 + } 40 + 41 + constructor(nsid: string) { 42 + this.segments = parseNsid(nsid); 43 + } 44 + 45 + get authority() { 46 + return this.segments 47 + .slice(0, this.segments.length - 1) 48 + .reverse() 49 + .join("."); 50 + } 51 + 52 + get name() { 53 + return this.segments.at(this.segments.length - 1); 54 + } 55 + 56 + toString() { 57 + return this.segments.join("."); 58 + } 59 + } 60 + 61 + export function ensureValidNsid(nsid: string): void { 62 + const result = validateNsid(nsid); 63 + if (!result.success) throw new InvalidNsidError(result.message); 64 + } 65 + 66 + export function parseNsid(nsid: string): string[] { 67 + const result = validateNsid(nsid); 68 + if (!result.success) throw new InvalidNsidError(result.message); 69 + return result.value; 70 + } 71 + 72 + export function isValidNsid(nsid: string): boolean { 73 + // Since the regex version is more performant for valid NSIDs, we use it when 74 + // we don't care about error details. 75 + return validateNsidRegex(nsid).success; 76 + } 77 + 78 + type ValidateResult<T> = 79 + | { success: true; value: T } 80 + | { success: false; message: string }; 81 + 82 + // Human readable constraints on NSID: 83 + // - a valid domain in reversed notation 84 + // - followed by an additional period-separated name, which is camel-case letters 85 + export function validateNsid(input: string): ValidateResult<string[]> { 86 + if (input.length > 253 + 1 + 63) { 87 + return { 88 + success: false, 89 + message: "NSID is too long (317 chars max)", 90 + }; 91 + } 92 + if (hasDisallowedCharacters(input)) { 93 + return { 94 + success: false, 95 + message: 96 + "Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)", 97 + }; 98 + } 99 + const segments = input.split("."); 100 + if (segments.length < 3) { 101 + return { 102 + success: false, 103 + message: "NSID needs at least three parts", 104 + }; 105 + } 106 + for (const l of segments) { 107 + if (l.length < 1) { 108 + return { 109 + success: false, 110 + message: "NSID parts can not be empty", 111 + }; 112 + } 113 + if (l.length > 63) { 114 + return { 115 + success: false, 116 + message: "NSID part too long (max 63 chars)", 117 + }; 118 + } 119 + if (startsWithHyphen(l) || endsWithHyphen(l)) { 120 + return { 121 + success: false, 122 + message: "NSID parts can not start or end with hyphen", 123 + }; 124 + } 125 + } 126 + if (startsWithNumber(segments[0])) { 127 + return { 128 + success: false, 129 + message: "NSID first part may not start with a digit", 130 + }; 131 + } 132 + if (!isValidIdentifier(segments[segments.length - 1])) { 133 + return { 134 + success: false, 135 + message: 136 + "NSID name part must be only letters and digits (and no leading digit)", 137 + }; 138 + } 139 + return { 140 + success: true, 141 + value: segments, 142 + }; 143 + } 144 + 145 + function hasDisallowedCharacters(v: string) { 146 + return !/^[a-zA-Z0-9.-]*$/.test(v); 147 + } 148 + 149 + function startsWithNumber(v: string) { 150 + const charCode = v.charCodeAt(0); 151 + return charCode >= 48 && charCode <= 57; 152 + } 153 + 154 + function startsWithHyphen(v: string) { 155 + return v.charCodeAt(0) === 45; /* - */ 156 + } 157 + 158 + function endsWithHyphen(v: string) { 159 + return v.charCodeAt(v.length - 1) === 45; /* - */ 160 + } 161 + 162 + function isValidIdentifier(v: string) { 163 + // Note, since we already know that "v" only contains [a-zA-Z0-9-], we can 164 + // simplify the following regex by checking only the first char and presence 165 + // of "-". 166 + 167 + // return /^[a-zA-Z][a-zA-Z0-9]*$/.test(v) 168 + return !startsWithNumber(v) && !v.includes("-"); 169 + } 170 + 171 + /** 172 + * @deprecated Use {@link ensureValidNsid} if you care about error details, 173 + * {@link parseNsid}/{@link NSID.parse} if you need the parsed segments, or 174 + * {@link isValidNsid} if you just want a boolean. 175 + */ 176 + export function ensureValidNsidRegex(nsid: string): void { 177 + const result = validateNsidRegex(nsid); 178 + if (!result.success) throw new InvalidNsidError(result.message); 179 + } 180 + 181 + /** 182 + * Regexp based validation that behaves identically to the previous code but 183 + * provides less detailed error messages (while being 20% to 50% faster). 184 + */ 185 + export function validateNsidRegex(value: string): ValidateResult<string> { 186 + if (value.length > 253 + 1 + 63) { 187 + return { 188 + success: false, 189 + message: "NSID is too long (317 chars max)", 190 + }; 191 + } 192 + 193 + if ( 194 + !/^[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})?)$/ 195 + .test( 196 + value, 197 + ) 198 + ) { 199 + return { 200 + success: false, 201 + message: "NSID didn't validate via regex", 202 + }; 203 + } 204 + 205 + return { 206 + success: true, 207 + value, 208 + }; 209 + } 210 + 211 + export class InvalidNsidError extends Error {}
+27
syntax/recordkey.ts
··· 1 + export const ensureValidRecordKey = (rkey: string): void => { 2 + if (rkey.length > 512 || rkey.length < 1) { 3 + throw new InvalidRecordKeyError("record key must be 1 to 512 characters"); 4 + } 5 + // simple regex to enforce most constraints via just regex and length. 6 + if (!/^[a-zA-Z0-9_~.:-]{1,512}$/.test(rkey)) { 7 + throw new InvalidRecordKeyError("record key syntax not valid (regex)"); 8 + } 9 + if (rkey === "." || rkey === "..") { 10 + throw new InvalidRecordKeyError("record key can not be '.' or '..'"); 11 + } 12 + }; 13 + 14 + export const isValidRecordKey = (rkey: string): boolean => { 15 + try { 16 + ensureValidRecordKey(rkey); 17 + } catch (err) { 18 + if (err instanceof InvalidRecordKeyError) { 19 + return false; 20 + } 21 + throw err; 22 + } 23 + 24 + return true; 25 + }; 26 + 27 + export class InvalidRecordKeyError extends Error {}
+602
syntax/tests/aturi_test.ts
··· 1 + import { assertEquals, assertThrows } from "jsr:@std/assert"; 2 + import { AtUri, ensureValidAtUri, ensureValidAtUriRegex } from "../mod.ts"; 3 + 4 + Deno.test("At Uris - parses valid at uris", () => { 5 + // input host path query hash 6 + type AtUriTest = [string, string, string, string, string]; 7 + const TESTS: AtUriTest[] = [ 8 + ["foo.com", "foo.com", "", "", ""], 9 + ["at://foo.com", "foo.com", "", "", ""], 10 + ["at://foo.com/", "foo.com", "/", "", ""], 11 + ["at://foo.com/foo", "foo.com", "/foo", "", ""], 12 + ["at://foo.com/foo/", "foo.com", "/foo/", "", ""], 13 + ["at://foo.com/foo/bar", "foo.com", "/foo/bar", "", ""], 14 + ["at://foo.com?foo=bar", "foo.com", "", "foo=bar", ""], 15 + ["at://foo.com?foo=bar&baz=buux", "foo.com", "", "foo=bar&baz=buux", ""], 16 + ["at://foo.com/?foo=bar", "foo.com", "/", "foo=bar", ""], 17 + ["at://foo.com/foo?foo=bar", "foo.com", "/foo", "foo=bar", ""], 18 + ["at://foo.com/foo/?foo=bar", "foo.com", "/foo/", "foo=bar", ""], 19 + ["at://foo.com#hash", "foo.com", "", "", "#hash"], 20 + ["at://foo.com/#hash", "foo.com", "/", "", "#hash"], 21 + ["at://foo.com/foo#hash", "foo.com", "/foo", "", "#hash"], 22 + ["at://foo.com/foo/#hash", "foo.com", "/foo/", "", "#hash"], 23 + ["at://foo.com?foo=bar#hash", "foo.com", "", "foo=bar", "#hash"], 24 + 25 + [ 26 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 27 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 28 + "", 29 + "", 30 + "", 31 + ], 32 + [ 33 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 34 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 35 + "", 36 + "", 37 + "", 38 + ], 39 + [ 40 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/", 41 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 42 + "/", 43 + "", 44 + "", 45 + ], 46 + [ 47 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo", 48 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 49 + "/foo", 50 + "", 51 + "", 52 + ], 53 + [ 54 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/", 55 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 56 + "/foo/", 57 + "", 58 + "", 59 + ], 60 + [ 61 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/bar", 62 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 63 + "/foo/bar", 64 + "", 65 + "", 66 + ], 67 + [ 68 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar", 69 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 70 + "", 71 + "foo=bar", 72 + "", 73 + ], 74 + [ 75 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar&baz=buux", 76 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 77 + "", 78 + "foo=bar&baz=buux", 79 + "", 80 + ], 81 + [ 82 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/?foo=bar", 83 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 84 + "/", 85 + "foo=bar", 86 + "", 87 + ], 88 + [ 89 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo?foo=bar", 90 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 91 + "/foo", 92 + "foo=bar", 93 + "", 94 + ], 95 + [ 96 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/?foo=bar", 97 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 98 + "/foo/", 99 + "foo=bar", 100 + "", 101 + ], 102 + [ 103 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw#hash", 104 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 105 + "", 106 + "", 107 + "#hash", 108 + ], 109 + [ 110 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/#hash", 111 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 112 + "/", 113 + "", 114 + "#hash", 115 + ], 116 + [ 117 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo#hash", 118 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 119 + "/foo", 120 + "", 121 + "#hash", 122 + ], 123 + [ 124 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw/foo/#hash", 125 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 126 + "/foo/", 127 + "", 128 + "#hash", 129 + ], 130 + [ 131 + "at://did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw?foo=bar#hash", 132 + "did:example:EiAnKD8-jfdd0MDcZUjAbRgaThBrMxPTFOxcnfJhI7Ukaw", 133 + "", 134 + "foo=bar", 135 + "#hash", 136 + ], 137 + 138 + ["did:web:localhost%3A1234", "did:web:localhost%3A1234", "", "", ""], 139 + ["at://did:web:localhost%3A1234", "did:web:localhost%3A1234", "", "", ""], 140 + [ 141 + "at://did:web:localhost%3A1234/", 142 + "did:web:localhost%3A1234", 143 + "/", 144 + "", 145 + "", 146 + ], 147 + [ 148 + "at://did:web:localhost%3A1234/foo", 149 + "did:web:localhost%3A1234", 150 + "/foo", 151 + "", 152 + "", 153 + ], 154 + [ 155 + "at://did:web:localhost%3A1234/foo/", 156 + "did:web:localhost%3A1234", 157 + "/foo/", 158 + "", 159 + "", 160 + ], 161 + [ 162 + "at://did:web:localhost%3A1234/foo/bar", 163 + "did:web:localhost%3A1234", 164 + "/foo/bar", 165 + "", 166 + "", 167 + ], 168 + [ 169 + "at://did:web:localhost%3A1234?foo=bar", 170 + "did:web:localhost%3A1234", 171 + "", 172 + "foo=bar", 173 + "", 174 + ], 175 + [ 176 + "at://did:web:localhost%3A1234?foo=bar&baz=buux", 177 + "did:web:localhost%3A1234", 178 + "", 179 + "foo=bar&baz=buux", 180 + "", 181 + ], 182 + [ 183 + "at://did:web:localhost%3A1234/?foo=bar", 184 + "did:web:localhost%3A1234", 185 + "/", 186 + "foo=bar", 187 + "", 188 + ], 189 + [ 190 + "at://did:web:localhost%3A1234/foo?foo=bar", 191 + "did:web:localhost%3A1234", 192 + "/foo", 193 + "foo=bar", 194 + "", 195 + ], 196 + [ 197 + "at://did:web:localhost%3A1234/foo/?foo=bar", 198 + "did:web:localhost%3A1234", 199 + "/foo/", 200 + "foo=bar", 201 + "", 202 + ], 203 + [ 204 + "at://did:web:localhost%3A1234#hash", 205 + "did:web:localhost%3A1234", 206 + "", 207 + "", 208 + "#hash", 209 + ], 210 + [ 211 + "at://did:web:localhost%3A1234/#hash", 212 + "did:web:localhost%3A1234", 213 + "/", 214 + "", 215 + "#hash", 216 + ], 217 + [ 218 + "at://did:web:localhost%3A1234/foo#hash", 219 + "did:web:localhost%3A1234", 220 + "/foo", 221 + "", 222 + "#hash", 223 + ], 224 + [ 225 + "at://did:web:localhost%3A1234/foo/#hash", 226 + "did:web:localhost%3A1234", 227 + "/foo/", 228 + "", 229 + "#hash", 230 + ], 231 + [ 232 + "at://did:web:localhost%3A1234?foo=bar#hash", 233 + "did:web:localhost%3A1234", 234 + "", 235 + "foo=bar", 236 + "#hash", 237 + ], 238 + [ 239 + "at://4513echo.bsky.social/app.bsky.feed.post/3jsrpdyf6ss23", 240 + "4513echo.bsky.social", 241 + "/app.bsky.feed.post/3jsrpdyf6ss23", 242 + "", 243 + "", 244 + ], 245 + ]; 246 + for (const [uri, hostname, pathname, search, hash] of TESTS) { 247 + const urip = new AtUri(uri); 248 + assertEquals(urip.protocol, "at:"); 249 + assertEquals(urip.host, hostname); 250 + assertEquals(urip.hostname, hostname); 251 + assertEquals(urip.origin, `at://${hostname}`); 252 + assertEquals(urip.pathname, pathname); 253 + assertEquals(urip.search, search); 254 + assertEquals(urip.hash, hash); 255 + } 256 + }); 257 + 258 + Deno.test("At Uris - handles ATP-specific parsing", () => { 259 + { 260 + const urip = new AtUri("at://foo.com"); 261 + assertEquals(urip.collection, ""); 262 + assertEquals(urip.rkey, ""); 263 + } 264 + { 265 + const urip = new AtUri("at://foo.com/com.example.foo"); 266 + assertEquals(urip.collection, "com.example.foo"); 267 + assertEquals(urip.rkey, ""); 268 + } 269 + { 270 + const urip = new AtUri("at://foo.com/com.example.foo/123"); 271 + assertEquals(urip.collection, "com.example.foo"); 272 + assertEquals(urip.rkey, "123"); 273 + } 274 + }); 275 + 276 + Deno.test("At Uris - supports modifications", () => { 277 + const urip = new AtUri("at://foo.com"); 278 + assertEquals(urip.toString(), "at://foo.com/"); 279 + 280 + urip.host = "bar.com"; 281 + assertEquals(urip.toString(), "at://bar.com/"); 282 + urip.host = "did:web:localhost%3A1234"; 283 + assertEquals(urip.toString(), "at://did:web:localhost%3A1234/"); 284 + urip.host = "foo.com"; 285 + 286 + urip.pathname = "/"; 287 + assertEquals(urip.toString(), "at://foo.com/"); 288 + urip.pathname = "/foo"; 289 + assertEquals(urip.toString(), "at://foo.com/foo"); 290 + urip.pathname = "foo"; 291 + assertEquals(urip.toString(), "at://foo.com/foo"); 292 + 293 + urip.collection = "com.example.foo"; 294 + urip.rkey = "123"; 295 + assertEquals(urip.toString(), "at://foo.com/com.example.foo/123"); 296 + urip.rkey = "124"; 297 + assertEquals(urip.toString(), "at://foo.com/com.example.foo/124"); 298 + urip.collection = "com.other.foo"; 299 + assertEquals(urip.toString(), "at://foo.com/com.other.foo/124"); 300 + urip.pathname = ""; 301 + urip.rkey = "123"; 302 + assertEquals(urip.toString(), "at://foo.com/undefined/123"); 303 + urip.pathname = "foo"; 304 + 305 + urip.search = "?foo=bar"; 306 + assertEquals(urip.toString(), "at://foo.com/foo?foo=bar"); 307 + urip.searchParams.set("baz", "buux"); 308 + assertEquals(urip.toString(), "at://foo.com/foo?foo=bar&baz=buux"); 309 + 310 + urip.hash = "#hash"; 311 + assertEquals(urip.toString(), "at://foo.com/foo?foo=bar&baz=buux#hash"); 312 + urip.hash = "hash"; 313 + assertEquals(urip.toString(), "at://foo.com/foo?foo=bar&baz=buux#hash"); 314 + }); 315 + 316 + Deno.test("At Uris - supports relative URIs", () => { 317 + // input path query hash 318 + type AtUriTest = [string, string, string, string]; 319 + const TESTS: AtUriTest[] = [ 320 + // input hostname pathname query hash 321 + ["", "", "", ""], 322 + ["/", "/", "", ""], 323 + ["/foo", "/foo", "", ""], 324 + ["/foo/", "/foo/", "", ""], 325 + ["/foo/bar", "/foo/bar", "", ""], 326 + ["?foo=bar", "", "foo=bar", ""], 327 + ["?foo=bar&baz=buux", "", "foo=bar&baz=buux", ""], 328 + ["/?foo=bar", "/", "foo=bar", ""], 329 + ["/foo?foo=bar", "/foo", "foo=bar", ""], 330 + ["/foo/?foo=bar", "/foo/", "foo=bar", ""], 331 + ["#hash", "", "", "#hash"], 332 + ["/#hash", "/", "", "#hash"], 333 + ["/foo#hash", "/foo", "", "#hash"], 334 + ["/foo/#hash", "/foo/", "", "#hash"], 335 + ["?foo=bar#hash", "", "foo=bar", "#hash"], 336 + ]; 337 + const BASES: string[] = [ 338 + "did:web:localhost%3A1234", 339 + "at://did:web:localhost%3A1234", 340 + "at://did:web:localhost%3A1234/foo/bar?foo=bar&baz=buux#hash", 341 + "did:web:localhost%3A1234", 342 + "at://did:web:localhost%3A1234", 343 + "at://did:web:localhost%3A1234/foo/bar?foo=bar&baz=buux#hash", 344 + ]; 345 + 346 + for (const base of BASES) { 347 + const basep = new AtUri(base); 348 + for (const [relative, pathname, search, hash] of TESTS) { 349 + const urip = new AtUri(relative, base); 350 + assertEquals(urip.protocol, "at:"); 351 + assertEquals(urip.host, basep.host); 352 + assertEquals(urip.hostname, basep.hostname); 353 + assertEquals(urip.origin, basep.origin); 354 + assertEquals(urip.pathname, pathname); 355 + assertEquals(urip.search, search); 356 + assertEquals(urip.hash, hash); 357 + } 358 + } 359 + }); 360 + 361 + Deno.test("AtUri validation - enforces spec basics", () => { 362 + const expectValid = (h: string) => { 363 + ensureValidAtUri(h); 364 + ensureValidAtUriRegex(h); 365 + }; 366 + const expectInvalid = (h: string) => { 367 + assertThrows(() => ensureValidAtUri(h)); 368 + assertThrows(() => ensureValidAtUriRegex(h)); 369 + }; 370 + 371 + expectValid("at://did:plc:asdf123"); 372 + expectValid("at://user.bsky.social"); 373 + expectValid("at://did:plc:asdf123/com.atproto.feed.post"); 374 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/record"); 375 + 376 + expectValid("at://did:plc:asdf123#/frag"); 377 + expectValid("at://user.bsky.social#/frag"); 378 + expectValid("at://did:plc:asdf123/com.atproto.feed.post#/frag"); 379 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/record#/frag"); 380 + 381 + expectInvalid("a://did:plc:asdf123"); 382 + expectInvalid("at//did:plc:asdf123"); 383 + expectInvalid("at:/a/did:plc:asdf123"); 384 + expectInvalid("at:/did:plc:asdf123"); 385 + expectInvalid("AT://did:plc:asdf123"); 386 + expectInvalid("http://did:plc:asdf123"); 387 + expectInvalid("://did:plc:asdf123"); 388 + expectInvalid("at:did:plc:asdf123"); 389 + expectInvalid("at:/did:plc:asdf123"); 390 + expectInvalid("at:///did:plc:asdf123"); 391 + expectInvalid("at://:/did:plc:asdf123"); 392 + expectInvalid("at:/ /did:plc:asdf123"); 393 + expectInvalid("at://did:plc:asdf123 "); 394 + expectInvalid("at://did:plc:asdf123/ "); 395 + expectInvalid(" at://did:plc:asdf123"); 396 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post "); 397 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post# "); 398 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post#/ "); 399 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post#/frag "); 400 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post#fr ag"); 401 + expectInvalid("//did:plc:asdf123"); 402 + expectInvalid("at://name"); 403 + expectInvalid("at://name.0"); 404 + expectInvalid("at://diD:plc:asdf123"); 405 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.p@st"); 406 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.p$st"); 407 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.p%st"); 408 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.p&st"); 409 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.p()t"); 410 + expectInvalid("at://did:plc:asdf123/com.atproto.feed_post"); 411 + expectInvalid("at://did:plc:asdf123/-com.atproto.feed.post"); 412 + expectInvalid("at://did:plc:asdf@123/com.atproto.feed.post"); 413 + 414 + expectInvalid("at://DID:plc:asdf123"); 415 + expectInvalid("at://user.bsky.123"); 416 + expectInvalid("at://bsky"); 417 + expectInvalid("at://did:plc:"); 418 + expectInvalid("at://did:plc:"); 419 + expectInvalid("at://frag"); 420 + 421 + expectValid( 422 + "at://did:plc:asdf123/com.atproto.feed.post/" + "o".repeat(512), 423 + ); 424 + expectInvalid( 425 + "at://did:plc:asdf123/com.atproto.feed.post/" + "o".repeat(8200), 426 + ); 427 + }); 428 + 429 + Deno.test("AtUri validation - has specified behavior on edge cases", () => { 430 + const expectInvalid = (h: string) => { 431 + assertThrows(() => ensureValidAtUri(h)); 432 + assertThrows(() => ensureValidAtUriRegex(h)); 433 + }; 434 + 435 + expectInvalid("at://user.bsky.social//"); 436 + expectInvalid("at://user.bsky.social//com.atproto.feed.post"); 437 + expectInvalid("at://user.bsky.social/com.atproto.feed.post//"); 438 + expectInvalid( 439 + "at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more", 440 + ); 441 + expectInvalid("at://did:plc:asdf123/short/stuff"); 442 + expectInvalid("at://did:plc:asdf123/12345"); 443 + }); 444 + 445 + Deno.test("AtUri validation - enforces no trailing slashes", () => { 446 + const expectValid = (h: string) => { 447 + ensureValidAtUri(h); 448 + ensureValidAtUriRegex(h); 449 + }; 450 + const expectInvalid = (h: string) => { 451 + assertThrows(() => ensureValidAtUri(h)); 452 + assertThrows(() => ensureValidAtUriRegex(h)); 453 + }; 454 + 455 + expectValid("at://did:plc:asdf123"); 456 + expectInvalid("at://did:plc:asdf123/"); 457 + 458 + expectValid("at://user.bsky.social"); 459 + expectInvalid("at://user.bsky.social/"); 460 + 461 + expectValid("at://did:plc:asdf123/com.atproto.feed.post"); 462 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/"); 463 + 464 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/record"); 465 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/record/"); 466 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/record/#/frag"); 467 + }); 468 + 469 + Deno.test("AtUri validation - enforces strict paths", () => { 470 + const expectValid = (h: string) => { 471 + ensureValidAtUri(h); 472 + ensureValidAtUriRegex(h); 473 + }; 474 + const expectInvalid = (h: string) => { 475 + assertThrows(() => ensureValidAtUri(h)); 476 + assertThrows(() => ensureValidAtUriRegex(h)); 477 + }; 478 + 479 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/asdf123"); 480 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf"); 481 + }); 482 + 483 + Deno.test("AtUri validation - is restrictive about record keys", () => { 484 + const expectValid = (h: string) => { 485 + ensureValidAtUri(h); 486 + ensureValidAtUriRegex(h); 487 + }; 488 + const expectInvalid = (h: string) => { 489 + assertThrows(() => ensureValidAtUri(h)); 490 + assertThrows(() => ensureValidAtUriRegex(h)); 491 + }; 492 + 493 + // Valid record keys 494 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/asdf123"); 495 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/a"); 496 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/asdf-123"); 497 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/self."); 498 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/lang:"); 499 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/:"); 500 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/-"); 501 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/_"); 502 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/~"); 503 + expectValid("at://did:plc:asdf123/com.atproto.feed.post/..."); 504 + 505 + // Invalid record keys (now properly rejected) 506 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/%23"); 507 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123"); 508 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/~'sdf123"); 509 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/$"); 510 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/@"); 511 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/!"); 512 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/*"); 513 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/("); 514 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/,"); 515 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/;"); 516 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/."); 517 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/.."); 518 + }); 519 + 520 + Deno.test("AtUri validation - properly validates URL encoding in record keys", () => { 521 + const expectInvalid = (h: string) => { 522 + assertThrows(() => ensureValidAtUri(h)); 523 + assertThrows(() => ensureValidAtUriRegex(h)); 524 + }; 525 + 526 + // These are now properly rejected as invalid record keys 527 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/%30"); 528 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/%3"); 529 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/%"); 530 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/%zz"); 531 + expectInvalid("at://did:plc:asdf123/com.atproto.feed.post/%%%"); 532 + }); 533 + 534 + Deno.test("AtUri validation - is very permissive about fragments", () => { 535 + const expectValid = (h: string) => { 536 + ensureValidAtUri(h); 537 + ensureValidAtUriRegex(h); 538 + }; 539 + const expectInvalid = (h: string) => { 540 + assertThrows(() => ensureValidAtUri(h)); 541 + assertThrows(() => ensureValidAtUriRegex(h)); 542 + }; 543 + 544 + expectValid("at://did:plc:asdf123#/frac"); 545 + 546 + expectInvalid("at://did:plc:asdf123#"); 547 + expectInvalid("at://did:plc:asdf123##"); 548 + expectInvalid("#at://did:plc:asdf123"); 549 + expectInvalid("at://did:plc:asdf123#/asdf#/asdf"); 550 + 551 + expectValid("at://did:plc:asdf123#/com.atproto.feed.post"); 552 + expectValid("at://did:plc:asdf123#/com.atproto.feed.post/"); 553 + expectValid("at://did:plc:asdf123#/asdf/"); 554 + 555 + expectValid( 556 + "at://did:plc:asdf123/com.atproto.feed.post#/$@!*():,;~.sdf123", 557 + ); 558 + expectValid("at://did:plc:asdf123#/[asfd]"); 559 + 560 + expectValid("at://did:plc:asdf123#/$"); 561 + expectValid("at://did:plc:asdf123#/*"); 562 + expectValid("at://did:plc:asdf123#/;"); 563 + expectValid("at://did:plc:asdf123#/,"); 564 + }); 565 + 566 + Deno.test("AtUri validation - conforms to interop valid ATURIs", async () => { 567 + const expectValid = (h: string) => { 568 + ensureValidAtUri(h); 569 + ensureValidAtUriRegex(h); 570 + }; 571 + 572 + const filePath = 573 + new URL("./interop/aturi_syntax_valid.txt", import.meta.url).pathname; 574 + const fileContent = await Deno.readTextFile(filePath); 575 + const lines = fileContent.split("\n"); 576 + 577 + for (const line of lines) { 578 + if (line.startsWith("#") || line.length === 0) { 579 + continue; 580 + } 581 + expectValid(line); 582 + } 583 + }); 584 + 585 + Deno.test("AtUri validation - conforms to interop invalid ATURIs", async () => { 586 + const expectInvalid = (h: string) => { 587 + assertThrows(() => ensureValidAtUri(h)); 588 + assertThrows(() => ensureValidAtUriRegex(h)); 589 + }; 590 + 591 + const filePath = 592 + new URL("./interop/aturi_syntax_invalid.txt", import.meta.url).pathname; 593 + const fileContent = await Deno.readTextFile(filePath); 594 + const lines = fileContent.split("\n"); 595 + 596 + for (const line of lines) { 597 + if (line.startsWith("#") || line.length === 0) { 598 + continue; 599 + } 600 + expectInvalid(line); 601 + } 602 + });
+130
syntax/tests/datetime_test.ts
··· 1 + import { assertEquals, assertThrows } from "jsr:@std/assert"; 2 + import { 3 + ensureValidDatetime, 4 + InvalidDatetimeError, 5 + isValidDatetime, 6 + normalizeDatetime, 7 + normalizeDatetimeAlways, 8 + } from "../mod.ts"; 9 + 10 + Deno.test("datetime validation - conforms to interop valid datetimes", async () => { 11 + const expectValid = (h: string) => { 12 + ensureValidDatetime(h); 13 + normalizeDatetime(h); 14 + normalizeDatetimeAlways(h); 15 + }; 16 + 17 + const filePath = 18 + new URL("./interop/datetime_syntax_valid.txt", import.meta.url).pathname; 19 + const fileContent = await Deno.readTextFile(filePath); 20 + const lines = fileContent.split("\n"); 21 + 22 + for (const line of lines) { 23 + if (line.startsWith("#") || line.length === 0) { 24 + continue; 25 + } 26 + if (!isValidDatetime(line)) { 27 + console.log(line); 28 + } 29 + expectValid(line); 30 + } 31 + }); 32 + 33 + Deno.test("datetime validation - conforms to interop invalid datetimes", async () => { 34 + const expectInvalid = (h: string) => { 35 + assertThrows(() => ensureValidDatetime(h), InvalidDatetimeError); 36 + }; 37 + 38 + const filePath = 39 + new URL("./interop/datetime_syntax_invalid.txt", import.meta.url).pathname; 40 + const fileContent = await Deno.readTextFile(filePath); 41 + const lines = fileContent.split("\n"); 42 + 43 + for (const line of lines) { 44 + if (line.startsWith("#") || line.length === 0) { 45 + continue; 46 + } 47 + expectInvalid(line); 48 + } 49 + }); 50 + 51 + Deno.test("datetime validation - conforms to interop invalid parse (semantics) datetimes", async () => { 52 + const expectInvalid = (h: string) => { 53 + assertThrows(() => ensureValidDatetime(h), InvalidDatetimeError); 54 + }; 55 + 56 + const filePath = 57 + new URL("./interop/datetime_parse_invalid.txt", import.meta.url).pathname; 58 + const fileContent = await Deno.readTextFile(filePath); 59 + const lines = fileContent.split("\n"); 60 + 61 + for (const line of lines) { 62 + if (line.startsWith("#") || line.length === 0) { 63 + continue; 64 + } 65 + expectInvalid(line); 66 + } 67 + }); 68 + 69 + Deno.test("normalization - normalizes datetimes", () => { 70 + assertEquals( 71 + normalizeDatetime("1234-04-12T23:20:50Z"), 72 + "1234-04-12T23:20:50.000Z", 73 + ); 74 + assertEquals( 75 + normalizeDatetime("1985-04-12T23:20:50Z"), 76 + "1985-04-12T23:20:50.000Z", 77 + ); 78 + assertEquals( 79 + normalizeDatetime("1985-04-12T23:20:50.123"), 80 + "1985-04-12T23:20:50.123Z", 81 + ); 82 + assertEquals( 83 + normalizeDatetime("1985-04-12 23:20:50.123"), 84 + "1985-04-12T23:20:50.123Z", 85 + ); 86 + assertEquals( 87 + normalizeDatetime("1985-04-12T10:20:50.1+01:00"), 88 + "1985-04-12T09:20:50.100Z", 89 + ); 90 + assertEquals( 91 + normalizeDatetime("Fri, 02 Jan 1999 12:34:56 GMT"), 92 + "1999-01-02T12:34:56.000Z", 93 + ); 94 + }); 95 + 96 + Deno.test("normalization - throws on invalid normalized datetimes", () => { 97 + assertThrows(() => normalizeDatetime(""), InvalidDatetimeError); 98 + assertThrows(() => normalizeDatetime("blah"), InvalidDatetimeError); 99 + assertThrows( 100 + () => normalizeDatetime("1999-19-39T23:20:50.123Z"), 101 + InvalidDatetimeError, 102 + ); 103 + assertThrows( 104 + () => normalizeDatetime("-000001-12-31T23:00:00.000Z"), 105 + InvalidDatetimeError, 106 + ); 107 + assertThrows( 108 + () => normalizeDatetime("0000-01-01T00:00:00+01:00"), 109 + InvalidDatetimeError, 110 + ); 111 + assertThrows( 112 + () => normalizeDatetime("0001-01-01T00:00:00+01:00"), 113 + InvalidDatetimeError, 114 + ); 115 + }); 116 + 117 + Deno.test("normalization - normalizes datetimes always", () => { 118 + assertEquals( 119 + normalizeDatetimeAlways("1985-04-12T23:20:50Z"), 120 + "1985-04-12T23:20:50.000Z", 121 + ); 122 + assertEquals( 123 + normalizeDatetimeAlways("blah"), 124 + "1970-01-01T00:00:00.000Z", 125 + ); 126 + assertEquals( 127 + normalizeDatetimeAlways("0000-01-01T00:00:00+01:00"), 128 + "1970-01-01T00:00:00.000Z", 129 + ); 130 + });
+113
syntax/tests/did_test.ts
··· 1 + import { assertThrows } from "jsr:@std/assert"; 2 + import { 3 + ensureValidDid, 4 + ensureValidDidRegex, 5 + InvalidDidError, 6 + } from "../mod.ts"; 7 + 8 + Deno.test("DID permissive validation - enforces spec details", () => { 9 + const expectValid = (h: string) => { 10 + ensureValidDid(h); 11 + ensureValidDidRegex(h); 12 + }; 13 + const expectInvalid = (h: string) => { 14 + assertThrows(() => ensureValidDid(h), InvalidDidError); 15 + assertThrows(() => ensureValidDidRegex(h), InvalidDidError); 16 + }; 17 + 18 + expectValid("did:method:val"); 19 + expectValid("did:method:VAL"); 20 + expectValid("did:method:val123"); 21 + expectValid("did:method:123"); 22 + expectValid("did:method:val-two"); 23 + expectValid("did:method:val_two"); 24 + expectValid("did:method:val.two"); 25 + expectValid("did:method:val:two"); 26 + expectValid("did:method:val%BB"); 27 + 28 + expectInvalid("did"); 29 + expectInvalid("didmethodval"); 30 + expectInvalid("method:did:val"); 31 + expectInvalid("did:method:"); 32 + expectInvalid("didmethod:val"); 33 + expectInvalid("did:methodval"); 34 + expectInvalid(":did:method:val"); 35 + expectInvalid("did.method.val"); 36 + expectInvalid("did:method:val:"); 37 + expectInvalid("did:method:val%"); 38 + expectInvalid("DID:method:val"); 39 + expectInvalid("did:METHOD:val"); 40 + expectInvalid("did:m123:val"); 41 + 42 + expectValid("did:method:" + "v".repeat(240)); 43 + expectInvalid("did:method:" + "v".repeat(8500)); 44 + 45 + expectValid("did:m:v"); 46 + expectValid("did:method::::val"); 47 + expectValid("did:method:-"); 48 + expectValid("did:method:-:_:.:%ab"); 49 + expectValid("did:method:."); 50 + expectValid("did:method:_"); 51 + expectValid("did:method::."); 52 + 53 + expectInvalid("did:method:val/two"); 54 + expectInvalid("did:method:val?two"); 55 + expectInvalid("did:method:val#two"); 56 + expectInvalid("did:method:val%"); 57 + 58 + expectValid( 59 + "did:onion:2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid", 60 + ); 61 + }); 62 + 63 + Deno.test("DID permissive validation - allows some real DID values", () => { 64 + const expectValid = (h: string) => { 65 + ensureValidDid(h); 66 + ensureValidDidRegex(h); 67 + }; 68 + 69 + expectValid("did:example:123456789abcdefghi"); 70 + expectValid("did:plc:7iza6de2dwap2sbkpav7c6c6"); 71 + expectValid("did:web:example.com"); 72 + expectValid("did:web:localhost%3A1234"); 73 + expectValid("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"); 74 + expectValid("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a"); 75 + }); 76 + 77 + Deno.test("DID permissive validation - conforms to interop valid DIDs", async () => { 78 + const expectValid = (h: string) => { 79 + ensureValidDid(h); 80 + ensureValidDidRegex(h); 81 + }; 82 + 83 + const filePath = 84 + new URL("./interop/did_syntax_valid.txt", import.meta.url).pathname; 85 + const fileContent = await Deno.readTextFile(filePath); 86 + const lines = fileContent.split("\n"); 87 + 88 + for (const line of lines) { 89 + if (line.startsWith("#") || line.length === 0) { 90 + continue; 91 + } 92 + expectValid(line); 93 + } 94 + }); 95 + 96 + Deno.test("DID permissive validation - conforms to interop invalid DIDs", async () => { 97 + const expectInvalid = (h: string) => { 98 + assertThrows(() => ensureValidDid(h), InvalidDidError); 99 + assertThrows(() => ensureValidDidRegex(h), InvalidDidError); 100 + }; 101 + 102 + const filePath = 103 + new URL("./interop/did_syntax_invalid.txt", import.meta.url).pathname; 104 + const fileContent = await Deno.readTextFile(filePath); 105 + const lines = fileContent.split("\n"); 106 + 107 + for (const line of lines) { 108 + if (line.startsWith("#") || line.length === 0) { 109 + continue; 110 + } 111 + expectInvalid(line); 112 + } 113 + });
+286
syntax/tests/handle_test.ts
··· 1 + import { assertEquals, assertThrows } from "jsr:@std/assert"; 2 + import { 3 + ensureValidHandle, 4 + ensureValidHandleRegex, 5 + InvalidHandleError, 6 + normalizeAndEnsureValidHandle, 7 + } from "../mod.ts"; 8 + 9 + Deno.test("handle validation - allows valid handles", () => { 10 + const expectValid = (h: string) => { 11 + ensureValidHandle(h); 12 + ensureValidHandleRegex(h); 13 + }; 14 + 15 + expectValid("A.ISI.EDU"); 16 + expectValid("XX.LCS.MIT.EDU"); 17 + expectValid("SRI-NIC.ARPA"); 18 + expectValid("john.test"); 19 + expectValid("jan.test"); 20 + expectValid("a234567890123456789.test"); 21 + expectValid("john2.test"); 22 + expectValid("john-john.test"); 23 + expectValid("john.bsky.app"); 24 + expectValid("jo.hn"); 25 + expectValid("a.co"); 26 + expectValid("a.org"); 27 + expectValid("joh.n"); 28 + expectValid("j0.h0"); 29 + const longHandle = "shoooort" + ".loooooooooooooooooooooooooong".repeat(8) + 30 + ".test"; 31 + assertEquals(longHandle.length, 253); 32 + expectValid(longHandle); 33 + expectValid("short." + "o".repeat(63) + ".test"); 34 + expectValid("jaymome-johnber123456.test"); 35 + expectValid("jay.mome-johnber123456.test"); 36 + expectValid("john.test.bsky.app"); 37 + 38 + // NOTE: this probably isn't ever going to be a real domain, but my read of 39 + // the RFC is that it would be possible 40 + expectValid("john.t"); 41 + }); 42 + 43 + // NOTE: we may change this at the proto level; currently only disallowed at 44 + // the registration level 45 + Deno.test("handle validation - allows .local and .arpa handles (proto-level)", () => { 46 + const expectValid = (h: string) => { 47 + ensureValidHandle(h); 48 + ensureValidHandleRegex(h); 49 + }; 50 + 51 + expectValid("laptop.local"); 52 + expectValid("laptop.arpa"); 53 + }); 54 + 55 + Deno.test("handle validation - allows punycode handles", () => { 56 + const expectValid = (h: string) => { 57 + ensureValidHandle(h); 58 + ensureValidHandleRegex(h); 59 + }; 60 + 61 + expectValid("xn--ls8h.test"); // 💩.test 62 + expectValid("xn--bcher-kva.tld"); // bücher.tld 63 + expectValid("xn--3jk.com"); 64 + expectValid("xn--w3d.com"); 65 + expectValid("xn--vqb.com"); 66 + expectValid("xn--ppd.com"); 67 + expectValid("xn--cs9a.com"); 68 + expectValid("xn--8r9a.com"); 69 + expectValid("xn--cfd.com"); 70 + expectValid("xn--5jk.com"); 71 + expectValid("xn--2lb.com"); 72 + }); 73 + 74 + Deno.test("handle validation - allows onion (Tor) handles", () => { 75 + const expectValid = (h: string) => { 76 + ensureValidHandle(h); 77 + ensureValidHandleRegex(h); 78 + }; 79 + 80 + expectValid("expyuzz4wqqyqhjn.onion"); 81 + expectValid("friend.expyuzz4wqqyqhjn.onion"); 82 + expectValid( 83 + "g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", 84 + ); 85 + expectValid( 86 + "friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", 87 + ); 88 + expectValid( 89 + "friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", 90 + ); 91 + expectValid( 92 + "2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", 93 + ); 94 + expectValid( 95 + "friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", 96 + ); 97 + }); 98 + 99 + Deno.test("handle validation - throws on invalid handles", () => { 100 + const expectInvalid = (h: string) => { 101 + assertThrows(() => ensureValidHandle(h), InvalidHandleError); 102 + assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError); 103 + }; 104 + 105 + expectInvalid("did:thing.test"); 106 + expectInvalid("did:thing"); 107 + expectInvalid("john-.test"); 108 + expectInvalid("john.0"); 109 + expectInvalid("john.-"); 110 + expectInvalid("short." + "o".repeat(64) + ".test"); 111 + expectInvalid("short" + ".loooooooooooooooooooooooong".repeat(10) + ".test"); 112 + const longHandle = "shooooort" + ".loooooooooooooooooooooooooong".repeat(8) + 113 + ".test"; 114 + assertEquals(longHandle.length, 254); 115 + expectInvalid(longHandle); 116 + expectInvalid("xn--bcher-.tld"); 117 + expectInvalid("john..test"); 118 + expectInvalid("jo_hn.test"); 119 + expectInvalid("-john.test"); 120 + expectInvalid(".john.test"); 121 + expectInvalid("jo!hn.test"); 122 + expectInvalid("jo%hn.test"); 123 + expectInvalid("jo&hn.test"); 124 + expectInvalid("jo@hn.test"); 125 + expectInvalid("jo*hn.test"); 126 + expectInvalid("jo|hn.test"); 127 + expectInvalid("jo:hn.test"); 128 + expectInvalid("jo/hn.test"); 129 + expectInvalid("john💩.test"); 130 + expectInvalid("bücher.test"); 131 + expectInvalid("john .test"); 132 + expectInvalid("john.test."); 133 + expectInvalid("john"); 134 + expectInvalid("john."); 135 + expectInvalid(".john"); 136 + expectInvalid("john.test."); 137 + expectInvalid(".john.test"); 138 + expectInvalid(" john.test"); 139 + expectInvalid("john.test "); 140 + expectInvalid("joh-.test"); 141 + expectInvalid("john.-est"); 142 + expectInvalid("john.tes-"); 143 + }); 144 + 145 + Deno.test('handle validation - throws on "dotless" TLD handles', () => { 146 + const expectInvalid = (h: string) => { 147 + assertThrows(() => ensureValidHandle(h), InvalidHandleError); 148 + assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError); 149 + }; 150 + 151 + expectInvalid("org"); 152 + expectInvalid("ai"); 153 + expectInvalid("gg"); 154 + expectInvalid("io"); 155 + }); 156 + 157 + Deno.test("handle validation - correctly validates corner cases (modern vs. old RFCs)", () => { 158 + const expectValid = (h: string) => { 159 + ensureValidHandle(h); 160 + ensureValidHandleRegex(h); 161 + }; 162 + const expectInvalid = (h: string) => { 163 + assertThrows(() => ensureValidHandle(h), InvalidHandleError); 164 + assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError); 165 + }; 166 + 167 + expectValid("12345.test"); 168 + expectValid("8.cn"); 169 + expectValid("4chan.org"); 170 + expectValid("4chan.o-g"); 171 + expectValid("blah.4chan.org"); 172 + expectValid("thing.a01"); 173 + expectValid("120.0.0.1.com"); 174 + expectValid("0john.test"); 175 + expectValid("9sta--ck.com"); 176 + expectValid("99stack.com"); 177 + expectValid("0ohn.test"); 178 + expectValid("john.t--t"); 179 + expectValid("thing.0aa.thing"); 180 + 181 + expectInvalid("cn.8"); 182 + expectInvalid("thing.0aa"); 183 + expectInvalid("thing.0aa"); 184 + }); 185 + 186 + Deno.test("handle validation - does not allow IP addresses as handles", () => { 187 + const expectInvalid = (h: string) => { 188 + assertThrows(() => ensureValidHandle(h), InvalidHandleError); 189 + assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError); 190 + }; 191 + 192 + expectInvalid("127.0.0.1"); 193 + expectInvalid("192.168.0.142"); 194 + expectInvalid("fe80::7325:8a97:c100:94b"); 195 + expectInvalid("2600:3c03::f03c:9100:feb0:af1f"); 196 + }); 197 + 198 + Deno.test("handle validation - is consistent with examples from stackoverflow", () => { 199 + const expectValid = (h: string) => { 200 + ensureValidHandle(h); 201 + ensureValidHandleRegex(h); 202 + }; 203 + const expectInvalid = (h: string) => { 204 + assertThrows(() => ensureValidHandle(h), InvalidHandleError); 205 + assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError); 206 + }; 207 + 208 + const okStackoverflow = [ 209 + "stack.com", 210 + "sta-ck.com", 211 + "sta---ck.com", 212 + "sta--ck9.com", 213 + "stack99.com", 214 + "sta99ck.com", 215 + "google.com.uk", 216 + "google.co.in", 217 + "google.com", 218 + "maselkowski.pl", 219 + "m.maselkowski.pl", 220 + "xn--masekowski-d0b.pl", 221 + "xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s", 222 + "xn--stackoverflow.com", 223 + "stackoverflow.xn--com", 224 + "stackoverflow.co.uk", 225 + "xn--masekowski-d0b.pl", 226 + "xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s", 227 + ]; 228 + okStackoverflow.forEach(expectValid); 229 + 230 + const badStackoverflow = [ 231 + "-notvalid.at-all", 232 + "-thing.com", 233 + "www.masełkowski.pl.com", 234 + ]; 235 + badStackoverflow.forEach(expectInvalid); 236 + }); 237 + 238 + Deno.test("handle validation - conforms to interop valid handles", async () => { 239 + const expectValid = (h: string) => { 240 + ensureValidHandle(h); 241 + ensureValidHandleRegex(h); 242 + }; 243 + 244 + const filePath = 245 + new URL("./interop/handle_syntax_valid.txt", import.meta.url).pathname; 246 + const fileContent = await Deno.readTextFile(filePath); 247 + const lines = fileContent.split("\n"); 248 + 249 + for (const line of lines) { 250 + if (line.startsWith("#") || line.length === 0) { 251 + continue; 252 + } 253 + expectValid(line); 254 + } 255 + }); 256 + 257 + Deno.test("handle validation - conforms to interop invalid handles", async () => { 258 + const expectInvalid = (h: string) => { 259 + assertThrows(() => ensureValidHandle(h), InvalidHandleError); 260 + assertThrows(() => ensureValidHandleRegex(h), InvalidHandleError); 261 + }; 262 + 263 + const filePath = 264 + new URL("./interop/handle_syntax_invalid.txt", import.meta.url).pathname; 265 + const fileContent = await Deno.readTextFile(filePath); 266 + const lines = fileContent.split("\n"); 267 + 268 + for (const line of lines) { 269 + if (line.startsWith("#") || line.length === 0) { 270 + continue; 271 + } 272 + expectInvalid(line); 273 + } 274 + }); 275 + 276 + Deno.test("normalization - normalizes handles", () => { 277 + const normalized = normalizeAndEnsureValidHandle("JoHn.TeST"); 278 + assertEquals(normalized, "john.test"); 279 + }); 280 + 281 + Deno.test("normalization - throws on invalid normalized handles", () => { 282 + assertThrows( 283 + () => normalizeAndEnsureValidHandle("JoH!n.TeST"), 284 + InvalidHandleError, 285 + ); 286 + });
+28
syntax/tests/interop/atidentifier_syntax_invalid.txt
··· 1 + 2 + # invalid handles 3 + did:thing.test 4 + did:thing 5 + john-.test 6 + john.0 7 + john.- 8 + xn--bcher-.tld 9 + john..test 10 + jo_hn.test 11 + 12 + # invalid DIDs 13 + did 14 + didmethodval 15 + method:did:val 16 + did:method: 17 + didmethod:val 18 + did:methodval) 19 + :did:method:val 20 + did:method:val: 21 + did:method:val% 22 + DID:method:val 23 + 24 + # other invalid stuff 25 + email@example.com 26 + @handle@example.com 27 + @handle 28 + blah
+15
syntax/tests/interop/atidentifier_syntax_valid.txt
··· 1 + 2 + # allows valid handles 3 + XX.LCS.MIT.EDU 4 + john.test 5 + jan.test 6 + a234567890123456789.test 7 + john2.test 8 + john-john.test 9 + 10 + # allows valid DIDs 11 + did:method:val 12 + did:method:VAL 13 + did:method:val123 14 + did:method:123 15 + did:method:val-two
+89
syntax/tests/interop/aturi_syntax_invalid.txt
··· 1 + 2 + # enforces spec basics 3 + a://did:plc:asdf123 4 + at//did:plc:asdf123 5 + at:/a/did:plc:asdf123 6 + at:/did:plc:asdf123 7 + AT://did:plc:asdf123 8 + http://did:plc:asdf123 9 + ://did:plc:asdf123 10 + at:did:plc:asdf123 11 + at:/did:plc:asdf123 12 + at:///did:plc:asdf123 13 + at://:/did:plc:asdf123 14 + at:/ /did:plc:asdf123 15 + at://did:plc:asdf123 16 + at://did:plc:asdf123/ 17 + at://did:plc:asdf123 18 + at://did:plc:asdf123/com.atproto.feed.post 19 + at://did:plc:asdf123/com.atproto.feed.post# 20 + at://did:plc:asdf123/com.atproto.feed.post#/ 21 + at://did:plc:asdf123/com.atproto.feed.post#/frag 22 + at://did:plc:asdf123/com.atproto.feed.post#fr ag 23 + //did:plc:asdf123 24 + at://name 25 + at://name.0 26 + at://diD:plc:asdf123 27 + at://did:plc:asdf123/com.atproto.feed.p@st 28 + at://did:plc:asdf123/com.atproto.feed.p$st 29 + at://did:plc:asdf123/com.atproto.feed.p%st 30 + at://did:plc:asdf123/com.atproto.feed.p&st 31 + at://did:plc:asdf123/com.atproto.feed.p()t 32 + at://did:plc:asdf123/com.atproto.feed_post 33 + at://did:plc:asdf123/-com.atproto.feed.post 34 + at://did:plc:asdf@123/com.atproto.feed.post 35 + at://DID:plc:asdf123 36 + at://user.bsky.123 37 + at://bsky 38 + at://did:plc: 39 + at://did:plc: 40 + at://frag 41 + 42 + # too long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(8200) 43 + at://did:plc:asdf123/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 44 + 45 + # has specified behavior on edge cases 46 + at://user.bsky.social// 47 + at://user.bsky.social//com.atproto.feed.post 48 + at://user.bsky.social/com.atproto.feed.post// 49 + at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more', 50 + at://did:plc:asdf123/short/stuff 51 + at://did:plc:asdf123/12345 52 + 53 + # enforces no trailing slashes 54 + at://did:plc:asdf123/ 55 + at://user.bsky.social/ 56 + at://did:plc:asdf123/com.atproto.feed.post/ 57 + at://did:plc:asdf123/com.atproto.feed.post/record/ 58 + at://did:plc:asdf123/com.atproto.feed.post/record/#/frag 59 + 60 + # enforces strict paths 61 + at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf 62 + 63 + # is very permissive about fragments 64 + at://did:plc:asdf123# 65 + at://did:plc:asdf123## 66 + #at://did:plc:asdf123 67 + at://did:plc:asdf123#/asdf#/asdf 68 + 69 + # new less permissive about record keys for Lexicon use (with recordkey more specified) 70 + at://did:plc:asdf123/com.atproto.feed.post/%23 71 + at://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123 72 + at://did:plc:asdf123/com.atproto.feed.post/~'sdf123") 73 + at://did:plc:asdf123/com.atproto.feed.post/$ 74 + at://did:plc:asdf123/com.atproto.feed.post/@ 75 + at://did:plc:asdf123/com.atproto.feed.post/! 76 + at://did:plc:asdf123/com.atproto.feed.post/* 77 + at://did:plc:asdf123/com.atproto.feed.post/( 78 + at://did:plc:asdf123/com.atproto.feed.post/, 79 + at://did:plc:asdf123/com.atproto.feed.post/; 80 + at://did:plc:asdf123/com.atproto.feed.post/abc%30123 81 + at://did:plc:asdf123/com.atproto.feed.post/%30 82 + at://did:plc:asdf123/com.atproto.feed.post/%3 83 + at://did:plc:asdf123/com.atproto.feed.post/% 84 + at://did:plc:asdf123/com.atproto.feed.post/%zz 85 + at://did:plc:asdf123/com.atproto.feed.post/%%% 86 + 87 + # disallow dot / double-dot 88 + at://did:plc:asdf123/com.atproto.feed.post/. 89 + at://did:plc:asdf123/com.atproto.feed.post/..
+35
syntax/tests/interop/aturi_syntax_valid.txt
··· 1 + 2 + # enforces spec basics 3 + at://did:plc:asdf123 4 + at://user.bsky.social 5 + at://did:plc:asdf123/com.atproto.feed.post 6 + at://did:plc:asdf123/com.atproto.feed.post/record 7 + 8 + # very long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(512) 9 + at://did:plc:asdf123/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 10 + 11 + # enforces no trailing slashes 12 + at://did:plc:asdf123 13 + at://user.bsky.social 14 + at://did:plc:asdf123/com.atproto.feed.post 15 + at://did:plc:asdf123/com.atproto.feed.post/record 16 + 17 + # enforces strict paths 18 + at://did:plc:asdf123/com.atproto.feed.post/asdf123 19 + 20 + # is very permissive about record keys 21 + at://did:plc:asdf123/com.atproto.feed.post/asdf123 22 + at://did:plc:asdf123/com.atproto.feed.post/a 23 + 24 + at://did:plc:asdf123/com.atproto.feed.post/asdf-123 25 + at://did:abc:123 26 + at://did:abc:123/io.nsid.someFunc/record-key 27 + 28 + at://did:abc:123/io.nsid.someFunc/self. 29 + at://did:abc:123/io.nsid.someFunc/lang: 30 + at://did:abc:123/io.nsid.someFunc/: 31 + at://did:abc:123/io.nsid.someFunc/- 32 + at://did:abc:123/io.nsid.someFunc/_ 33 + at://did:abc:123/io.nsid.someFunc/~ 34 + at://did:abc:123/io.nsid.someFunc/... 35 + at://did:plc:asdf123/com.atproto.feed.postV2
+7
syntax/tests/interop/datetime_parse_invalid.txt
··· 1 + # superficial syntax parses ok, but are not valid datetimes for semantic reasons (eg, "month zero") 2 + 1985-00-12T23:20:50.123Z 3 + 1985-04-00T23:20:50.123Z 4 + 1985-13-12T23:20:50.123Z 5 + 1985-04-12T25:20:50.123Z 6 + 1985-04-12T23:99:50.123Z 7 + 1985-04-12T23:20:61.123Z
+68
syntax/tests/interop/datetime_syntax_invalid.txt
··· 1 + 2 + # subtle changes to: 1985-04-12T23:20:50.123Z 3 + 1985-04-12T23:20:50.123z 4 + 01985-04-12T23:20:50.123Z 5 + 985-04-12T23:20:50.123Z 6 + 1985-04-12T23:20:50.Z 7 + 1985-04-32T23;20:50.123Z 8 + 1985-04-32T23;20:50.123Z 9 + 10 + # en-dash and em-dash 11 + 1985—04-32T23;20:50.123Z 12 + 1985–04-32T23;20:50.123Z 13 + 14 + # whitespace 15 + 1985-04-12T23:20:50.123Z 16 + 1985-04-12T23:20:50.123Z 17 + 1985-04-12T 23:20:50.123Z 18 + 19 + # not enough zero padding 20 + 1985-4-12T23:20:50.123Z 21 + 1985-04-2T23:20:50.123Z 22 + 1985-04-12T3:20:50.123Z 23 + 1985-04-12T23:0:50.123Z 24 + 1985-04-12T23:20:5.123Z 25 + 26 + # too much zero padding 27 + 01985-04-12T23:20:50.123Z 28 + 1985-004-12T23:20:50.123Z 29 + 1985-04-012T23:20:50.123Z 30 + 1985-04-12T023:20:50.123Z 31 + 1985-04-12T23:020:50.123Z 32 + 1985-04-12T23:20:050.123Z 33 + 34 + # strict capitalization (ISO-8601) 35 + 1985-04-12t23:20:50.123Z 36 + 1985-04-12T23:20:50.123z 37 + 38 + # RFC-3339, but not ISO-8601 39 + 1985-04-12T23:20:50.123-00:00 40 + 1985-04-12_23:20:50.123Z 41 + 1985-04-12 23:20:50.123Z 42 + 43 + # ISO-8601, but weird 44 + 1985-04-274T23:20:50.123Z 45 + 46 + # timezone is required 47 + 1985-04-12T23:20:50.123 48 + 1985-04-12T23:20:50 49 + 50 + 1985-04-12 51 + 1985-04-12T23:20Z 52 + 1985-04-12T23:20:5Z 53 + 1985-04-12T23:20:50.123 54 + +001985-04-12T23:20:50.123Z 55 + 23:20:50.123Z 56 + 57 + 1985-04-12T23:20:50.123+00 58 + 1985-04-12T23:20:50.123+00:0 59 + 1985-04-12T23:20:50.123+0:00 60 + 1985-04-12T23:20:50.123 61 + 1985-04-12T23:20:50.123+0000 62 + 1985-04-12T23:20:50.123+00 63 + 1985-04-12T23:20:50.123+ 64 + 1985-04-12T23:20:50.123- 65 + 66 + # ISO-8601, but normalizes to a negative time 67 + 0000-01-01T00:00:00+01:00 68 + -000001-12-31T23:00:00.000Z
+40
syntax/tests/interop/datetime_syntax_valid.txt
··· 1 + # "preferred" 2 + 1985-04-12T23:20:50.123Z 3 + 1985-04-12T23:20:50.000Z 4 + 2000-01-01T00:00:00.000Z 5 + 1985-04-12T23:20:50.123456Z 6 + 1985-04-12T23:20:50.120Z 7 + 1985-04-12T23:20:50.120000Z 8 + 9 + # "supported" 10 + 1985-04-12T23:20:50.1235678912345Z 11 + 1985-04-12T23:20:50.100Z 12 + 1985-04-12T23:20:50Z 13 + 1985-04-12T23:20:50.0Z 14 + 1985-04-12T23:20:50.123+00:00 15 + 1985-04-12T23:20:50.123-07:00 16 + 1985-04-12T23:20:50.123+07:00 17 + 1985-04-12T23:20:50.123+01:45 18 + 0985-04-12T23:20:50.123-07:00 19 + 1985-04-12T23:20:50.123-07:00 20 + 0123-01-01T00:00:00.000Z 21 + 22 + # various precisions, up through at least 12 digits 23 + 1985-04-12T23:20:50.1Z 24 + 1985-04-12T23:20:50.12Z 25 + 1985-04-12T23:20:50.123Z 26 + 1985-04-12T23:20:50.1234Z 27 + 1985-04-12T23:20:50.12345Z 28 + 1985-04-12T23:20:50.123456Z 29 + 1985-04-12T23:20:50.1234567Z 30 + 1985-04-12T23:20:50.12345678Z 31 + 1985-04-12T23:20:50.123456789Z 32 + 1985-04-12T23:20:50.1234567890Z 33 + 1985-04-12T23:20:50.12345678901Z 34 + 1985-04-12T23:20:50.123456789012Z 35 + 36 + # extreme but currently allowed 37 + 0010-12-31T23:00:00.000Z 38 + 1000-12-31T23:00:00.000Z 39 + 1900-12-31T23:00:00.000Z 40 + 3001-12-31T23:00:00.000Z
+19
syntax/tests/interop/did_syntax_invalid.txt
··· 1 + did 2 + didmethodval 3 + method:did:val 4 + did:method: 5 + didmethod:val 6 + did:methodval) 7 + :did:method:val 8 + did.method.val 9 + did:method:val: 10 + did:method:val% 11 + DID:method:val 12 + did:METHOD:val 13 + did:m123:val 14 + did:method:val/two 15 + did:method:val?two 16 + did:method:val#two 17 + did:method:val% 18 + did:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 19 +
+26
syntax/tests/interop/did_syntax_valid.txt
··· 1 + did:method:val 2 + did:method:VAL 3 + did:method:val123 4 + did:method:123 5 + did:method:val-two 6 + did:method:val_two 7 + did:method:val.two 8 + did:method:val:two 9 + did:method:val%BB 10 + did:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 11 + did:m:v 12 + did:method::::val 13 + did:method:- 14 + did:method:-:_:.:%ab 15 + did:method:. 16 + did:method:_ 17 + did:method::. 18 + 19 + # allows some real DID values 20 + did:onion:2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid 21 + did:example:123456789abcdefghi 22 + did:plc:7iza6de2dwap2sbkpav7c6c6 23 + did:web:example.com 24 + did:web:localhost%3A1234 25 + did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N 26 + did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a
+61
syntax/tests/interop/handle_syntax_invalid.txt
··· 1 + # throws on invalid handles 2 + did:thing.test 3 + did:thing 4 + john-.test 5 + john.0 6 + john.- 7 + xn--bcher-.tld 8 + john..test 9 + jo_hn.test 10 + -john.test 11 + .john.test 12 + jo!hn.test 13 + jo%hn.test 14 + jo&hn.test 15 + jo@hn.test 16 + jo*hn.test 17 + jo|hn.test 18 + jo:hn.test 19 + jo/hn.test 20 + john💩.test 21 + bücher.test 22 + john .test 23 + john.test. 24 + john 25 + john. 26 + .john 27 + john.test. 28 + .john.test 29 + john.test 30 + john.test 31 + joh-.test 32 + john.-est 33 + john.tes- 34 + 35 + # max over all handle: 'shoooort' + '.loooooooooooooooooooooooooong'.repeat(9) + '.test' 36 + shoooort.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.test 37 + 38 + # max segment: 'short.' + 'o'.repeat(64) + '.test' 39 + short.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.test 40 + 41 + # throws on "dotless" TLD handles 42 + org 43 + ai 44 + gg 45 + io 46 + 47 + # correctly validates corner cases (modern vs. old RFCs) 48 + cn.8 49 + thing.0aa 50 + thing.0aa 51 + 52 + # does not allow IP addresses as handles 53 + 127.0.0.1 54 + 192.168.0.142 55 + fe80::7325:8a97:c100:94b 56 + 2600:3c03::f03c:9100:feb0:af1f 57 + 58 + # examples from stackoverflow 59 + -notvalid.at-all 60 + -thing.com 61 + www.masełkowski.pl.com
+90
syntax/tests/interop/handle_syntax_valid.txt
··· 1 + # allows valid handles 2 + A.ISI.EDU 3 + XX.LCS.MIT.EDU 4 + SRI-NIC.ARPA 5 + john.test 6 + jan.test 7 + a234567890123456789.test 8 + john2.test 9 + john-john.test 10 + john.bsky.app 11 + jo.hn 12 + a.co 13 + a.org 14 + joh.n 15 + j0.h0 16 + jaymome-johnber123456.test 17 + jay.mome-johnber123456.test 18 + john.test.bsky.app 19 + 20 + # max over all handle: 'shoooort' + '.loooooooooooooooooooooooooong'.repeat(8) + '.test' 21 + shoooort.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.test 22 + 23 + # max segment: 'short.' + 'o'.repeat(63) + '.test' 24 + short.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.test 25 + 26 + # NOTE: this probably isn't ever going to be a real domain, but my read of the RFC is that it would be possible 27 + john.t 28 + 29 + # allows .local and .arpa handles (proto-level) 30 + laptop.local 31 + laptop.arpa 32 + 33 + # allows punycode handles 34 + # 💩.test 35 + xn--ls8h.test 36 + # bücher.tld 37 + xn--bcher-kva.tld 38 + xn--3jk.com 39 + xn--w3d.com 40 + xn--vqb.com 41 + xn--ppd.com 42 + xn--cs9a.com 43 + xn--8r9a.com 44 + xn--cfd.com 45 + xn--5jk.com 46 + xn--2lb.com 47 + 48 + # allows onion (Tor) handles 49 + expyuzz4wqqyqhjn.onion 50 + friend.expyuzz4wqqyqhjn.onion 51 + g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 52 + friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 53 + friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 54 + 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 55 + friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 56 + 57 + # correctly validates corner cases (modern vs. old RFCs) 58 + 12345.test 59 + 8.cn 60 + 4chan.org 61 + 4chan.o-g 62 + blah.4chan.org 63 + thing.a01 64 + 120.0.0.1.com 65 + 0john.test 66 + 9sta--ck.com 67 + 99stack.com 68 + 0ohn.test 69 + john.t--t 70 + thing.0aa.thing 71 + 72 + # examples from stackoverflow 73 + stack.com 74 + sta-ck.com 75 + sta---ck.com 76 + sta--ck9.com 77 + stack99.com 78 + sta99ck.com 79 + google.com.uk 80 + google.co.in 81 + google.com 82 + maselkowski.pl 83 + m.maselkowski.pl 84 + xn--masekowski-d0b.pl 85 + xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s 86 + xn--stackoverflow.com 87 + stackoverflow.xn--com 88 + stackoverflow.co.uk 89 + xn--masekowski-d0b.pl 90 + xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s
+30
syntax/tests/interop/nsid_syntax_invalid.txt
··· 1 + # length checks 2 + com.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.foo 3 + com.example.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 4 + com.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.foo 5 + 6 + # invalid examples 7 + com.example.foo.* 8 + com.example.foo.blah* 9 + com.example.foo.*blah 10 + com.exa💩ple.thing 11 + a-0.b-1.c-3 12 + a-0.b-1.c-o 13 + 1.0.0.127.record 14 + 0two.example.foo 15 + example.com 16 + com.example 17 + a. 18 + .one.two.three 19 + one.two.three 20 + one.two..three 21 + one .two.three 22 + one.two.three 23 + com.exa💩ple.thing 24 + com.atproto.feed.p@st 25 + com.atproto.feed.p_st 26 + com.atproto.feed.p*st 27 + com.atproto.feed.po#t 28 + com.atproto.feed.p!ot 29 + com.example-.foo 30 + com.example.fooBar.2
+32
syntax/tests/interop/nsid_syntax_valid.txt
··· 1 + # length checks 2 + com.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.foo 3 + com.example.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 4 + com.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.foo 5 + 6 + # valid examples 7 + com.example.fooBar 8 + com.example.fooBarV2 9 + net.users.bob.ping 10 + a.b.c 11 + m.xn--masekowski-d0b.pl 12 + one.two.three 13 + one.two.three.four-and.FiVe 14 + one.2.three 15 + a-0.b-1.c 16 + a0.b1.cc 17 + cn.8.lex.stuff 18 + test.12345.record 19 + a01.thing.record 20 + a.0.c 21 + xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two 22 + a0.b1.c3 23 + com.example.f00 24 + 25 + # allows onion (Tor) NSIDs 26 + onion.expyuzz4wqqyqhjn.spec.getThing 27 + onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing 28 + 29 + # allows starting-with-numeric segments (same as domains) 30 + org.4chan.lex.getThing 31 + cn.8.lex.stuff 32 + onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing
+15
syntax/tests/interop/recordkey_syntax_invalid.txt
··· 1 + # specs 2 + alpha/beta 3 + . 4 + .. 5 + #extra 6 + @handle 7 + any space 8 + any+space 9 + number[3] 10 + number(3) 11 + "quote" 12 + dHJ1ZQ== 13 + 14 + # too long: 'o'.repeat(513) 15 + ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
+21
syntax/tests/interop/recordkey_syntax_valid.txt
··· 1 + # specs 2 + self 3 + example.com 4 + ~1.2-3_ 5 + dHJ1ZQ 6 + _ 7 + literal:self 8 + pre:fix 9 + 10 + # more corner-cases 11 + : 12 + - 13 + _ 14 + ~ 15 + ... 16 + self. 17 + lang: 18 + :lang 19 + 20 + # very long: 'o'.repeat(512) 21 + oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
+15
syntax/tests/interop/tid_syntax_invalid.txt
··· 1 + 2 + # not base32 3 + 3jzfcijpj2z21 4 + 0000000000000 5 + 6 + # too long/short 7 + 3jzfcijpj2z2aa 8 + 3jzfcijpj2z2 9 + 10 + # old dashes syntax not actually supported (TTTT-TTT-TTTT-CC) 11 + 3jzf-cij-pj2z-2a 12 + 13 + # high bit can't be high 14 + zzzzzzzzzzzzz 15 + kjzfcijpj2z2a
+6
syntax/tests/interop/tid_syntax_valid.txt
··· 1 + # 13 digits 2 + # 234567abcdefghijklmnopqrstuvwxyz 3 + 4 + 3jzfcijpj2z2a 5 + 7777777777777 6 + 3zzzzzzzzzzzz
+222
syntax/tests/nsid_test.ts
··· 1 + import { assertEquals, assertThrows } from "jsr:@std/assert"; 2 + import { 3 + ensureValidNsid, 4 + InvalidNsidError, 5 + isValidNsid, 6 + NSID, 7 + parseNsid, 8 + validateNsid, 9 + validateNsidRegex, 10 + } from "../mod.ts"; 11 + 12 + Deno.test("NSID parsing & creation - parses valid NSIDs", () => { 13 + assertEquals(NSID.parse("com.example.foo").authority, "example.com"); 14 + assertEquals(NSID.parse("com.example.foo").name, "foo"); 15 + assertEquals(NSID.parse("com.example.foo").toString(), "com.example.foo"); 16 + assertEquals( 17 + NSID.parse("com.long-thing1.cool.fooBarBaz").authority, 18 + "cool.long-thing1.com", 19 + ); 20 + assertEquals( 21 + NSID.parse("com.long-thing1.cool.fooBarBaz").name, 22 + "fooBarBaz", 23 + ); 24 + assertEquals( 25 + NSID.parse("com.long-thing1.cool.fooBarBaz").toString(), 26 + "com.long-thing1.cool.fooBarBaz", 27 + ); 28 + }); 29 + 30 + Deno.test("NSID parsing & creation - creates valid NSIDs", () => { 31 + assertEquals(NSID.create("example.com", "foo").authority, "example.com"); 32 + assertEquals(NSID.create("example.com", "foo").name, "foo"); 33 + assertEquals( 34 + NSID.create("example.com", "foo").toString(), 35 + "com.example.foo", 36 + ); 37 + assertEquals( 38 + NSID.create("cool.long-thing1.com", "fooBarBaz").authority, 39 + "cool.long-thing1.com", 40 + ); 41 + assertEquals( 42 + NSID.create("cool.long-thing1.com", "fooBarBaz").name, 43 + "fooBarBaz", 44 + ); 45 + assertEquals( 46 + NSID.create("cool.long-thing1.com", "fooBarBaz").toString(), 47 + "com.long-thing1.cool.fooBarBaz", 48 + ); 49 + }); 50 + 51 + Deno.test("NSID validation - enforces spec details", () => { 52 + const expectValid = (h: string) => { 53 + assertEquals(isValidNsid(h), true); 54 + ensureValidNsid(h); 55 + assertEquals(parseNsid(h), h.split(".")); 56 + const regexResult = validateNsidRegex(h); 57 + assertEquals(regexResult.success, true); 58 + const validationResult = validateNsid(h); 59 + assertEquals(validationResult.success, true); 60 + }; 61 + const expectInvalid = (h: string) => { 62 + assertEquals(isValidNsid(h), false); 63 + assertThrows(() => parseNsid(h), InvalidNsidError); 64 + assertThrows(() => ensureValidNsid(h), InvalidNsidError); 65 + const regexResult = validateNsidRegex(h); 66 + assertEquals(regexResult.success, false); 67 + const validationResult = validateNsid(h); 68 + assertEquals(validationResult.success, false); 69 + }; 70 + 71 + expectValid("com.example.foo"); 72 + const longNsid = "com." + "o".repeat(63) + ".foo"; 73 + expectValid(longNsid); 74 + 75 + const tooLongNsid = "com." + "o".repeat(64) + ".foo"; 76 + expectInvalid(tooLongNsid); 77 + 78 + const longEnd = "com.example." + "o".repeat(63); 79 + expectValid(longEnd); 80 + 81 + const tooLongEnd = "com.example." + "o".repeat(64); 82 + expectInvalid(tooLongEnd); 83 + 84 + const longOverall = "com." + "middle.".repeat(40) + "foo"; 85 + assertEquals(longOverall.length, 287); 86 + expectValid(longOverall); 87 + 88 + const tooLongOverall = "com." + "middle.".repeat(50) + "foo"; 89 + assertEquals(tooLongOverall.length, 357); 90 + expectInvalid(tooLongOverall); 91 + }); 92 + 93 + Deno.test("NSID validation - valid NSIDs", () => { 94 + const expectValid = (h: string) => { 95 + assertEquals(isValidNsid(h), true); 96 + ensureValidNsid(h); 97 + assertEquals(parseNsid(h), h.split(".")); 98 + const regexResult = validateNsidRegex(h); 99 + assertEquals(regexResult.success, true); 100 + const validationResult = validateNsid(h); 101 + assertEquals(validationResult.success, true); 102 + }; 103 + 104 + const validNsids = [ 105 + "com.example.foo", 106 + "o".repeat(63) + ".foo.bar", 107 + "com." + "o".repeat(63) + ".foo", 108 + "com.example." + "o".repeat(63), 109 + "com." + "middle.".repeat(40) + "foo", 110 + "a-0.b-1.c", 111 + "a.0.c", 112 + "a.b.c", 113 + "a0.b1.c3", 114 + "a0.b1.cc", 115 + "a01.thing.record", 116 + "cn.8.lex.stuff", 117 + "com.example.f00", 118 + "com.example.fooBar", 119 + "m.xn--masekowski-d0b.pl", 120 + "net.users.bob.ping", 121 + "one.2.three", 122 + "one.two.three", 123 + "one.two.three.four-and.FiVe", 124 + "onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing", 125 + "onion.expyuzz4wqqyqhjn.spec.getThing", 126 + "onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing", 127 + "org.4chan.lex.getThing", 128 + "test.12345.record", 129 + "xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two", 130 + ]; 131 + 132 + for (const validNsid of validNsids) { 133 + expectValid(validNsid); 134 + } 135 + }); 136 + 137 + Deno.test("NSID validation - invalid NSIDs", () => { 138 + const invalidNsids = [ 139 + "o".repeat(64) + ".foo.bar", 140 + "com." + "o".repeat(64) + ".foo", 141 + "com.example." + "o".repeat(64), 142 + "com." + "middle.".repeat(50) + "foo", 143 + "com.example.foo.*", 144 + "com.example.foo.blah*", 145 + "com.example.foo.*blah", 146 + "com.exa💩ple.thing", 147 + "a-0.b-1.c-3", 148 + "a-0.b-1.c-o", 149 + "1.0.0.127.record", 150 + "0two.example.foo", 151 + "example.com", 152 + "com.example", 153 + "a.", 154 + ".one.two.three", 155 + "one.two.three ", 156 + "one.two..three", 157 + "one .two.three", 158 + " one.two.three", 159 + "com.atproto.feed.p@st", 160 + "com.atproto.feed.p_st", 161 + "com.atproto.feed.p*st", 162 + "com.atproto.feed.po#t", 163 + "com.atproto.feed.p!ot", 164 + "com.example-.foo", 165 + "com.-example.foo", 166 + "com.example.0foo", 167 + "com.example.f-o", 168 + ]; 169 + 170 + for (const invalidNsid of invalidNsids) { 171 + const result = validateNsid(invalidNsid); 172 + assertEquals(result.success, false); 173 + } 174 + }); 175 + 176 + Deno.test("NSID validation - conforms to interop valid NSIDs", async () => { 177 + const expectValid = (h: string) => { 178 + assertEquals(isValidNsid(h), true); 179 + ensureValidNsid(h); 180 + assertEquals(parseNsid(h), h.split(".")); 181 + const regexResult = validateNsidRegex(h); 182 + assertEquals(regexResult.success, true); 183 + const validationResult = validateNsid(h); 184 + assertEquals(validationResult.success, true); 185 + }; 186 + 187 + const filePath = 188 + new URL("./interop/nsid_syntax_valid.txt", import.meta.url).pathname; 189 + const fileContent = await Deno.readTextFile(filePath); 190 + const lines = fileContent.split("\n"); 191 + 192 + for (const line of lines) { 193 + if (line.startsWith("#") || line.length === 0) { 194 + continue; 195 + } 196 + expectValid(line); 197 + } 198 + }); 199 + 200 + Deno.test("NSID validation - conforms to interop invalid NSIDs", async () => { 201 + const expectInvalid = (h: string) => { 202 + assertEquals(isValidNsid(h), false); 203 + assertThrows(() => parseNsid(h), InvalidNsidError); 204 + assertThrows(() => ensureValidNsid(h), InvalidNsidError); 205 + const regexResult = validateNsidRegex(h); 206 + assertEquals(regexResult.success, false); 207 + const validationResult = validateNsid(h); 208 + assertEquals(validationResult.success, false); 209 + }; 210 + 211 + const filePath = 212 + new URL("./interop/nsid_syntax_invalid.txt", import.meta.url).pathname; 213 + const fileContent = await Deno.readTextFile(filePath); 214 + const lines = fileContent.split("\n"); 215 + 216 + for (const line of lines) { 217 + if (line.startsWith("#") || line.length === 0) { 218 + continue; 219 + } 220 + expectInvalid(line); 221 + } 222 + });
+38
syntax/tests/recordkey_test.ts
··· 1 + import { assertThrows } from "jsr:@std/assert"; 2 + import { ensureValidRecordKey, InvalidRecordKeyError } from "../mod.ts"; 3 + 4 + Deno.test("recordkey validation - conforms to interop valid recordkey", async () => { 5 + const expectValid = (r: string) => { 6 + ensureValidRecordKey(r); 7 + }; 8 + 9 + const filePath = 10 + new URL("./interop/recordkey_syntax_valid.txt", import.meta.url).pathname; 11 + const fileContent = await Deno.readTextFile(filePath); 12 + const lines = fileContent.split("\n"); 13 + 14 + for (const line of lines) { 15 + if (line.startsWith("#") || line.length === 0) { 16 + continue; 17 + } 18 + expectValid(line); 19 + } 20 + }); 21 + 22 + Deno.test("recordkey validation - conforms to interop invalid recordkeys", async () => { 23 + const expectInvalid = (r: string) => { 24 + assertThrows(() => ensureValidRecordKey(r), InvalidRecordKeyError); 25 + }; 26 + 27 + const filePath = 28 + new URL("./interop/recordkey_syntax_invalid.txt", import.meta.url).pathname; 29 + const fileContent = await Deno.readTextFile(filePath); 30 + const lines = fileContent.split("\n"); 31 + 32 + for (const line of lines) { 33 + if (line.startsWith("#") || line.length === 0) { 34 + continue; 35 + } 36 + expectInvalid(line); 37 + } 38 + });
+38
syntax/tests/tid_test.ts
··· 1 + import { assertThrows } from "jsr:@std/assert"; 2 + import { ensureValidTid, InvalidTidError } from "../mod.ts"; 3 + 4 + Deno.test("tid validation - conforms to interop valid tid", async () => { 5 + const expectValid = (t: string) => { 6 + ensureValidTid(t); 7 + }; 8 + 9 + const filePath = 10 + new URL("./interop/tid_syntax_valid.txt", import.meta.url).pathname; 11 + const fileContent = await Deno.readTextFile(filePath); 12 + const lines = fileContent.split("\n"); 13 + 14 + for (const line of lines) { 15 + if (line.startsWith("#") || line.length === 0) { 16 + continue; 17 + } 18 + expectValid(line); 19 + } 20 + }); 21 + 22 + Deno.test("tid validation - conforms to interop invalid tids", async () => { 23 + const expectInvalid = (t: string) => { 24 + assertThrows(() => ensureValidTid(t), InvalidTidError); 25 + }; 26 + 27 + const filePath = 28 + new URL("./interop/tid_syntax_invalid.txt", import.meta.url).pathname; 29 + const fileContent = await Deno.readTextFile(filePath); 30 + const lines = fileContent.split("\n"); 31 + 32 + for (const line of lines) { 33 + if (line.startsWith("#") || line.length === 0) { 34 + continue; 35 + } 36 + expectInvalid(line); 37 + } 38 + });
+18
syntax/tid.ts
··· 1 + const TID_LENGTH = 13; 2 + const TID_REGEX = /^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/; 3 + 4 + export const ensureValidTid = (tid: string): void => { 5 + if (tid.length !== TID_LENGTH) { 6 + throw new InvalidTidError(`TID must be ${TID_LENGTH} characters`); 7 + } 8 + // simple regex to enforce most constraints via just regex and length. 9 + if (!TID_REGEX.test(tid)) { 10 + throw new InvalidTidError("TID syntax not valid (regex)"); 11 + } 12 + }; 13 + 14 + export const isValidTid = (tid: string): boolean => { 15 + return tid.length === TID_LENGTH && TID_REGEX.test(tid); 16 + }; 17 + 18 + export class InvalidTidError extends Error {}
-1
xrpc-server/deno.json
··· 11 11 "@std/encoding": "jsr:@std/encoding@^1.0.10", 12 12 "zod": "jsr:@zod/zod@^4.0.17", 13 13 "hono": "jsr:@hono/hono@^4.7.10", 14 - "multiformats": "npm:multiformats@^13.3.6", 15 14 "rate-limiter-flexible": "npm:rate-limiter-flexible@^2.4.1", 16 15 "uint8arrays": "npm:uint8arrays@3.0.0", 17 16 "ws": "npm:ws@^8.12.0"
-42
xrpc-server/example.ts
··· 1 - import { createServer } from "./server.ts"; 2 - import type { HandlerContext, HandlerSuccess } from "./types.ts"; 3 - 4 - // Create a new XRPC server instance 5 - const server = createServer(); 6 - 7 - // Add a simple query method 8 - server.method("com.example.getStatus", { 9 - handler: async (_ctx: HandlerContext): Promise<HandlerSuccess> => { 10 - return { 11 - encoding: "application/json", 12 - body: { 13 - status: "ok", 14 - timestamp: new Date().toISOString(), 15 - }, 16 - }; 17 - }, 18 - }); 19 - 20 - // Add a simple procedure method 21 - server.method("com.example.createPost", { 22 - handler: (ctx: HandlerContext): HandlerSuccess => { 23 - const { input } = ctx; 24 - 25 - return { 26 - encoding: "application/json", 27 - body: { 28 - success: true, 29 - message: "Post created successfully", 30 - data: input, 31 - }, 32 - }; 33 - }, 34 - }); 35 - 36 - // Start the Deno server 37 - console.log("Starting XRPC server on port 8000..."); 38 - 39 - Deno.serve({ 40 - port: 8000, 41 - handler: server.handler, 42 - });
+6 -2
xrpc-server/stream/frames.ts
··· 1 - import * as uint8arrays from "uint8arrays"; 2 1 import { cborDecodeMulti, cborEncode } from "@atp/common"; 3 2 import type { 4 3 ErrorFrameBody, ··· 35 34 * @returns {Uint8Array} The serialized frame as bytes 36 35 */ 37 36 toBytes(): Uint8Array { 38 - return uint8arrays.concat([cborEncode(this.header), cborEncode(this.body)]); 37 + const headerBytes = cborEncode(this.header); 38 + const bodyBytes = cborEncode(this.body); 39 + const result = new Uint8Array(headerBytes.length + bodyBytes.length); 40 + result.set(headerBytes, 0); 41 + result.set(bodyBytes, headerBytes.length); 42 + return result; 39 43 } 40 44 41 45 /**
+1 -1
xrpc-server/tests/ipld_test.ts
··· 1 - import { CID } from "multiformats/cid"; 1 + import { CID } from "npm:multiformats/cid"; 2 2 import type { LexiconDoc } from "@atproto/lexicon"; 3 3 import { XrpcClient } from "@atproto/xrpc"; 4 4 import * as xrpcServer from "../mod.ts";