From fa5fe6af546e8f32cab76dab21efbf410b8fee46 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Wed, 24 Sep 2025 21:57:33 -0400 Subject: [PATCH] THE (documented) RAPTURE --- bytes/mod.ts | 3 +-- common/mod.ts | 6 +++++ crypto/mod.ts | 43 ++++++++++++++++++++++++++++++ lex-cli/mod.ts | 33 +++++++++++++++++++++-- lexicon/mod.ts | 31 ++++++++++++++++++++++ syntax/aturi.ts | 15 +++++++++++ syntax/handle.ts | 69 ++++++++++++++++++++++++++++++++++-------------- syntax/mod.ts | 58 ++++++++++++++++++++++++++++++++++++++++ syntax/nsid.ts | 39 ++++++++++++++++++--------- xrpc/mod.ts | 46 ++++++++++++++++++++++++++++++++ 10 files changed, 306 insertions(+), 37 deletions(-) diff --git a/bytes/mod.ts b/bytes/mod.ts index 161e051..a0666a9 100644 --- a/bytes/mod.ts +++ b/bytes/mod.ts @@ -1,8 +1,7 @@ /** * @module * - * `Uint8Array`s bring memory-efficient(ish) byte handling to browsers - they are similar to Node.js `Buffer`s but lack a lot of the utility methods present on that class. - * This module exports a number of function that let you do common operations - joining Uint8Arrays together, seeing if they have the same contents etc. + * Simple `Uint8Array` utilities for AT Protocol. * * ## alloc(size) * diff --git a/common/mod.ts b/common/mod.ts index a953bde..c81cf5e 100644 --- a/common/mod.ts +++ b/common/mod.ts @@ -1,3 +1,9 @@ +/** + * # AT Protocol Common Utilities + * + * Shared TypeScript code for other @atproto/* packages. + * This package is oriented towards writing servers. + */ export * as check from "./check.ts"; export * as util from "./util.ts"; diff --git a/crypto/mod.ts b/crypto/mod.ts index 580aa58..40a2d78 100644 --- a/crypto/mod.ts +++ b/crypto/mod.ts @@ -1,3 +1,46 @@ +/** + * # AT Protocol Cryptographic Utilities + * + * This module provides cryptographic helpers for AT Protocol. + * + * 2 cryptographic systems are currently supported: + * - P-256 elliptic curve: aka "NIST P-256", aka secp256r1 (note the r), aka prime256v1 + * - K-256 elliptic curve: aka "NIST K-256", aka secp256k1 (note the k) + * + * The details of cryptography in atproto are described in {@link https://atproto.com/specs/cryptography | the specification.} + * This includes string encodings, validity of "low-S" signatures, byte representation + * "compression", hashing, and more. + * + * @example + * ```typescript + * import { verifySignature, Secp256k1Keypair, P256Keypair } from '@atp/crypto' + * + * // generate a new random K-256 private key + * const keypair = await Secp256k1Keypair.create({ exportable: true }) + * + * // sign binary data, resulting signature bytes. + * // SHA-256 hash of data is what actually gets signed. + * // signature output is often base64-encoded. + * const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]) + * const sig = await keypair.sign(data) + * + * // serialize the public key as a did:key string, which includes key type metadata + * const pubDidKey = keypair.did() + * console.log(pubDidKey) + * + * // output would look something like: 'did:key:zQ3shVRtgqTRHC7Lj4DYScoDgReNpsDp3HBnuKBKt1FSXKQ38' + * + * // verify signature using public key + * const ok = verifySignature(pubDidKey, data, sig) + * if (!ok) { + * throw new Error('Uh oh, something is fishy') + * } else { + * console.log('Success') + * } + * ``` + * + * @module + */ export * from "./const.ts"; export * from "./did.ts"; export * from "./multibase.ts"; diff --git a/lex-cli/mod.ts b/lex-cli/mod.ts index de41995..c7b6253 100644 --- a/lex-cli/mod.ts +++ b/lex-cli/mod.ts @@ -1,5 +1,34 @@ -#!/usr/bin/env node - +/** + * # AT Protocol Lexicon CLI + * + * A command-line interface for generating docs, servers, and clients + * from AT Protocol lexicon files. + * + * Turn lexicon files into: + * - Markdown documentation + * - Server implementation + * - TypeScript objects + * - Client implementation + * + * ## Installation + * ```bash + * deno install -g jsr:@atp/lex-cli@latest --name lex-cli + * ``` + * Alternatively, you can use it without installation by referring to + * it as `jsr:@atp/lex-cli` instead of `lex-cli`. + * + * @example Generate Server + * ```bash + * lex-cli gen-server -i -o + * ``` + * + * @example Generate Client + * ```bash + * lex-cli gen-api -i -o + * ``` + * + * @module + */ import { Command } from "@cliffy/command"; import { genApi, genMd, genServer, genTsObj } from "./cmd/index.ts"; import process from "node:process"; diff --git a/lexicon/mod.ts b/lexicon/mod.ts index 42d3087..17a0d66 100644 --- a/lexicon/mod.ts +++ b/lexicon/mod.ts @@ -1,3 +1,34 @@ +/** + * # AT Protocol Lexicon Validation Utility + * + * This module provides utilities for validating and working with AT Protocol lexicons + * in TypeScript. + * + * @example Validate a lexicon + * ```typescript + * import { Lexicon } from "@atp/lexicon"; + * + * // create your lexicons collection + * const lex = new Lexicons() + * + * // add your lexicons + * lex.add({ + * lex: 1, + * id: 'com.example.post', + * defs: { + * // ... + * } + * }) + * + * // validate + * lex.assertValidRecord('com.example.record', {$type: 'com.example.record', ...}) + * lex.assertValidXrpcParams('com.example.query', {...}) + * lex.assertValidXrpcInput('com.example.procedure', {...}) + * lex.assertValidXrpcOutput('com.example.query', {...}) + * ``` + * + * @module + */ export * from "./types.ts"; export * from "./lexicons.ts"; export * from "./blob-refs.ts"; diff --git a/syntax/aturi.ts b/syntax/aturi.ts index 3a72998..8b62822 100644 --- a/syntax/aturi.ts +++ b/syntax/aturi.ts @@ -6,6 +6,21 @@ export const ATP_URI_REGEX = // --path----- --query-- --hash-- const RELATIVE_REGEX = /^(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i; +/** + * AT URI Validation and parsing class + * + * @example AT URIs + * ```typescript + * import { AtUri } from '@atp/syntax' + * + * const uri = new AtUri('at://bob.com/com.example.post/1234') + * uri.protocol // => 'at:' + * uri.origin // => 'at://bob.com' + * uri.hostname // => 'bob.com' + * uri.collection // => 'com.example.post' + * uri.rkey // => '1234' + * ``` + */ export class AtUri { hash: string; host: string; diff --git a/syntax/handle.ts b/syntax/handle.ts index 9fcd707..290c992 100644 --- a/syntax/handle.ts +++ b/syntax/handle.ts @@ -18,25 +18,30 @@ export const DISALLOWED_TLDS = [ // "should" "never" actually resolve and get registered in production ]; -// Handle constraints, in English: -// - must be a possible domain name -// - RFC-1035 is commonly referenced, but has been updated. eg, RFC-3696, -// section 2. and RFC-3986, section 3. can now have leading numbers (eg, -// 4chan.org) -// - "labels" (sub-names) are made of ASCII letters, digits, hyphens -// - can not start or end with a hyphen -// - TLD (last component) should not start with a digit -// - can't end with a hyphen (can end with digit) -// - each segment must be between 1 and 63 characters (not including any periods) -// - overall length can't be more than 253 characters -// - separated by (ASCII) periods; does not start or end with period -// - case insensitive -// - domains (handles) are equal if they are the same lower-case -// - punycode allowed for internationalization -// - no whitespace, null bytes, joining chars, etc -// - does not validate whether domain or TLD exists, or is a reserved or -// special TLD (eg, .onion or .local) -// - does not validate punycode +/** + * Ensure a handle is valid + * @throws If handle is invalid + * + * Handle constraints, in English: + * - must be a possible domain name + * - RFC-1035 is commonly referenced, but has been updated. eg, RFC-3696, + * section 2. and RFC-3986, section 3. can now have leading numbers (eg, + * 4chan.org) + * - "labels" (sub-names) are made of ASCII letters, digits, hyphens + * - can not start or end with a hyphen + * - TLD (last component) should not start with a digit + * - can't end with a hyphen (can end with digit) + * - each segment must be between 1 and 63 characters (not including any periods) + * - overall length can't be more than 253 characters + * - separated by (ASCII) periods; does not start or end with period + * - case insensitive + * - domains (handles) are equal if they are the same lower-case + * - punycode allowed for internationalization + * - no whitespace, null bytes, joining chars, etc + * - does not validate whether domain or TLD exists, or is a reserved or + * special TLD (eg, .onion or .local) + * - does not validate punycode + */ export const ensureValidHandle = (handle: string): void => { // check that all chars are boring ASCII if (!/^[a-zA-Z0-9.-]*$/.test(handle)) { @@ -73,7 +78,10 @@ export const ensureValidHandle = (handle: string): void => { } }; -// simple regex translation of above constraints +/** + * Ensure a handle is valid using a regex pattern. + * @throws If handle is invalid + */ export const ensureValidHandleRegex = (handle: string): void => { if ( !/^([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])?$/ @@ -88,16 +96,29 @@ export const ensureValidHandleRegex = (handle: string): void => { } }; +/** + * Converts a handle to lowercase. + */ export const normalizeHandle = (handle: string): string => { return handle.toLowerCase(); }; +/** + * Converts a handle to lowercase and ensures it is valid. + * @returns The normalized handle if it is valid + * @throws If handle is invalid + */ export const normalizeAndEnsureValidHandle = (handle: string): string => { const normalized = normalizeHandle(handle); ensureValidHandle(normalized); return normalized; }; +/** + * Checks if a handle is valid and returns a boolean. + * + * @returns True if handle is valid + */ export const isValidHandle = (handle: string): boolean => { try { ensureValidHandle(handle); @@ -111,10 +132,18 @@ export const isValidHandle = (handle: string): boolean => { return true; }; +/** + * Check if a TLD is valid. + * + * Disallowed TLDs: {@linkcode DISALLOWED_TLDS} + */ export const isValidTld = (handle: string): boolean => { return !DISALLOWED_TLDS.some((domain) => handle.endsWith(domain)); }; +/** + * Thrown when a handle is invalid. + */ export class InvalidHandleError extends Error {} /** @deprecated Never used */ export class ReservedHandleError extends Error {} diff --git a/syntax/mod.ts b/syntax/mod.ts index 05d2bb1..9a02443 100644 --- a/syntax/mod.ts +++ b/syntax/mod.ts @@ -1,3 +1,61 @@ +/** + * # AT Protocol Syntax Validation + * + * Validation utilities for AT Protocol strings: + * - DIDs + * - Handles + * - NSIDs + * - AT URIs + * - TIDs + * - Record Keys + * - Datetimes + * + * @example Handles + * ```typescript + * import { isValidHandle, ensureValidHandle, isValidDid } from '@atp/syntax' + * + * isValidHandle('alice.test') // returns true + * ensureValidHandle('alice.test') // returns void + * + * isValidHandle('al!ce.test') // returns false + * ensureValidHandle('al!ce.test') // throws an error + * ``` + * + * @example NSIDs + * ```typescript + * import { NSID } from '@atp/syntax' + * + * const id1 = NSID.parse('com.example.foo') + * id1.authority // => 'example.com' + * id1.name // => 'foo' + * id1.toString() // => 'com.example.foo' + * + * const id2 = NSID.create('example.com', 'foo') + * id2.authority // => 'example.com' + * id2.name // => 'foo' + * id2.toString() // => 'com.example.foo' + * + * NSID.isValid('com.example.foo') // => true + * NSID.isValid('com.example.someThing') // => true + * NSID.isValid('example.com/foo') // => false + * NSID.isValid('foo') // => false + * ``` + * + * @example AT URIs + * ```typescript + * import { AtUri } from '@atp/syntax' + * + * const uri = new AtUri('at://bob.com/com.example.post/1234') + * uri.protocol // => 'at:' + * uri.origin // => 'at://bob.com' + * uri.hostname // => 'bob.com' + * uri.collection // => 'com.example.post' + * uri.rkey // => '1234' + * ``` + * + * @module + */ + export * from "./handle.ts"; export * from "./did.ts"; export * from "./nsid.ts"; diff --git a/syntax/nsid.ts b/syntax/nsid.ts index f45e5c4..bd37439 100644 --- a/syntax/nsid.ts +++ b/syntax/nsid.ts @@ -1,16 +1,29 @@ -/* -Grammar: - -alpha = "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h" / "i" / "j" / "k" / "l" / "m" / "n" / "o" / "p" / "q" / "r" / "s" / "t" / "u" / "v" / "w" / "x" / "y" / "z" / "A" / "B" / "C" / "D" / "E" / "F" / "G" / "H" / "I" / "J" / "K" / "L" / "M" / "N" / "O" / "P" / "Q" / "R" / "S" / "T" / "U" / "V" / "W" / "X" / "Y" / "Z" -number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0" -delim = "." -segment = alpha *( alpha / number / "-" ) -authority = segment *( delim segment ) -name = alpha *( alpha / number ) -nsid = authority delim name - -*/ - +/** + * NameSpaced Identifier class + * + * Validation and parsing based on the NSID specification: + * https://atproto.com/specs/nsid + * + * @example NSIDs + * ```typescript + * import { NSID } from '@atp/syntax' + * + * const id1 = NSID.parse('com.example.foo') + * id1.authority // => 'example.com' + * id1.name // => 'foo' + * id1.toString() // => 'com.example.foo' + * + * const id2 = NSID.create('example.com', 'foo') + * id2.authority // => 'example.com' + * id2.name // => 'foo' + * id2.toString() // => 'com.example.foo' + * + * NSID.isValid('com.example.foo') // => true + * NSID.isValid('com.example.someThing') // => true + * NSID.isValid('example.com/foo') // => false + * NSID.isValid('foo') // => false + * ``` + */ export class NSID { readonly segments: readonly string[]; diff --git a/xrpc/mod.ts b/xrpc/mod.ts index a163973..d79659f 100644 --- a/xrpc/mod.ts +++ b/xrpc/mod.ts @@ -1,3 +1,49 @@ +/** + * # XRPC Client + * + * TypeScript client library for talking to AT Protocol services, + * with Lexicon schema validation. + * + * @example Fetching an XRPC endpoint + * ```typescript + * import { LexiconDoc } from '@atproto/lexicon' + * import { XrpcClient } from '@atproto/xrpc' + * + * const pingLexicon = { + * lexicon: 1, + * id: 'io.example.ping', + * defs: { + * main: { + * type: 'query', + * description: 'Ping the server', + * parameters: { + * type: 'params', + * properties: { message: { type: 'string' } }, + * }, + * output: { + * encoding: 'application/json', + * schema: { + * type: 'object', + * required: ['message'], + * properties: { message: { type: 'string' } }, + * }, + * }, + * }, + * }, + * } satisfies LexiconDoc + * + * const xrpc = new XrpcClient('https://ping.example.com', [ + * // Any number of lexicon here + * pingLexicon, + * ]) + * + * const res1 = await xrpc.call('io.example.ping', { + * message: 'hello world', + * }) + * res1.encoding // => 'application/json' + * res1.body // => {message: 'hello world'} + * ``` + */ export * from "./client.ts"; export * from "./fetch-handler.ts"; export * from "./types.ts"; -- 2.51.2