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

Configure Feed

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

doocumentation (a small start)

+532 -91
+2
bytes/util.ts
··· 48 48 return buf; 49 49 }); 50 50 51 + /** Supported base encodings */ 51 52 export type SupportedEncodings = 52 53 | "utf8" 53 54 | "utf-8" ··· 68 69 ...bases, 69 70 }; 70 71 72 + /** Supported base encoding multibase codecs */ 71 73 export default BASES; 72 74 73 75 /**
-2
common/deno.json
··· 8 8 "@logtape/file": "jsr:@logtape/file@^1.2.0-dev.344+834f24a9", 9 9 "@logtape/logtape": "jsr:@logtape/logtape@^1.2.0-dev.344+834f24a9", 10 10 "@std/cbor": "jsr:@std/cbor@^0.1.8", 11 - "@std/crypto": "jsr:@std/crypto@^1.0.5", 12 11 "@std/encoding": "jsr:@std/encoding@^1.0.10", 13 12 "@std/fs": "jsr:@std/fs@^1.0.19", 14 - "@std/streams": "jsr:@std/streams@^1.0.12", 15 13 "multiformats": "npm:multiformats@^13.4.1", 16 14 "zod": "jsr:@zod/zod@^4.1.11" 17 15 },
-1
common/ipld.ts
··· 6 6 import { sha256 } from "multiformats/hashes/sha2"; 7 7 import { schema } from "./types.ts"; 8 8 import * as check from "./check.ts"; 9 - import { crypto } from "@std/crypto"; 10 9 import { concat, equals, fromString, toString } from "@atp/bytes"; 11 10 12 11 export const cborEncode = cborCodec.encode;
+7 -1
crypto/plugins.ts
··· 1 + import type { DidKeyPlugin } from "@atp/crypto"; 1 2 import { p256Plugin } from "./p256/plugin.ts"; 2 3 import { secp256k1Plugin } from "./secp256k1/plugin.ts"; 3 4 4 - export const plugins = [p256Plugin, secp256k1Plugin]; 5 + /** 6 + * Plugins for different elliptic curves. 7 + * 8 + * Currently supports P-256 and secp256k1. 9 + */ 10 + export const plugins: DidKeyPlugin[] = [p256Plugin, secp256k1Plugin];
+2
crypto/random.ts
··· 2 2 import { type SupportedEncodings, toString } from "@atp/bytes"; 3 3 import { sha256 } from "./sha.ts"; 4 4 5 + /** Generate random bytes using noble hashes' randomBytes function. */ 5 6 export const randomBytes = noble.randomBytes; 6 7 8 + /** Generate random string from {@linkcode randomBytes}. */ 7 9 export const randomStr = ( 8 10 byteLength: number, 9 11 encoding: SupportedEncodings,
+9 -3
crypto/sha.ts
··· 1 1 import * as noble from "@noble/hashes/sha2.js"; 2 2 import { fromString, toString } from "@atp/bytes"; 3 3 4 - // takes either bytes of utf8 input 5 - // @TODO this can be sync 4 + /** 5 + * Creates a SHA-256 hash of the input. 6 + * Takes either bytes of utf8 input 7 + * @param input - Bytes to hash. 8 + */ 6 9 export const sha256 = ( 7 10 input: Uint8Array | string, 8 11 ): Uint8Array => { ··· 10 13 return noble.sha256(bytes); 11 14 }; 12 15 13 - // @TODO this can be sync 16 + /** 17 + * Hashes the input using SHA-256 and returns the result as a hexadecimal string. 18 + * @param input - Bytes to hash. 19 + */ 14 20 export const sha256Hex = ( 15 21 input: Uint8Array | string, 16 22 ): string => {
+31
crypto/types.ts
··· 1 + /** 2 + * Creates signatures for messages. 3 + * 4 + * @prop jwtAlg - The JWT algorithm used for signing. 5 + * @prop sign - Returns a signature for the given message bytes. 6 + */ 1 7 export interface Signer { 2 8 jwtAlg: string; 3 9 sign(msg: Uint8Array): Uint8Array; 4 10 } 5 11 12 + /** 13 + * Can create DID keys. 14 + * @prop did - Returns a DID key. 15 + */ 6 16 export interface Didable { 7 17 did(): string; 8 18 } 9 19 20 + /** 21 + * Combines a {@linkcode Signer} and {@linkcode Didable} into a Keypair. 22 + */ 10 23 export interface Keypair extends Signer, Didable {} 11 24 25 + /** 26 + * Keypair with an export method. 27 + * @prop export - Exports the keypair as a Uint8Array. 28 + */ 12 29 export interface ExportableKeypair extends Keypair { 13 30 export(): Promise<Uint8Array>; 14 31 } 15 32 33 + /** 34 + * DID key plugin with key compression and signature verification utilities. 35 + * @prop prefix - The DID key prefix. 36 + * @prop jwtAlg - The JWT algorithm used for signing. 37 + * @prop verifySignature - Verifies a signature for the given message bytes. 38 + * @prop compressPubkey - Compresses a public key. 39 + * @prop decompressPubkey - Decompresses a compressed public key. 40 + */ 16 41 export type DidKeyPlugin = { 17 42 prefix: Uint8Array; 18 43 jwtAlg: string; ··· 27 52 decompressPubkey: (compressed: Uint8Array) => Uint8Array; 28 53 }; 29 54 55 + /** 56 + * Options for less strict signature verification. 57 + * These options are only recommended for testing purposes. 58 + * @prop allowMalleableSig - Don't enforce low-S signatures. Explicitly against specification. Only recommended for testing purposes. 59 + * @prop allowDerSig - Allow DER-encoded signatures. Only recommended for testing purposes. 60 + */ 30 61 export type VerifyOptions = { 31 62 allowMalleableSig?: boolean; 32 63 allowDerSig?: boolean;
+21
crypto/utils.ts
··· 1 1 import { equals, fromString } from "@atp/bytes"; 2 2 import { BASE58_MULTIBASE_PREFIX, DID_KEY_PREFIX } from "./const.ts"; 3 3 4 + /** 5 + * Extracts the multikey from a `did:key` string. 6 + * @param did - The `did:key` string to extract the multikey from. 7 + * @throws Error if the input doesn't start with `did:key:`. 8 + */ 4 9 export const extractMultikey = (did: string): string => { 5 10 if (!did.startsWith(DID_KEY_PREFIX)) { 6 11 throw new Error(`Incorrect prefix for did:key: ${did}`); ··· 8 13 return did.slice(DID_KEY_PREFIX.length); 9 14 }; 10 15 16 + /** 17 + * Extracts the bytes from a multikey string using base58btc encoding. 18 + * @param multikey - The multikey string to extract the bytes from. 19 + * @throws Error if the input doesn't start with `z`. 20 + */ 11 21 export const extractPrefixedBytes = (multikey: string): Uint8Array => { 12 22 if (!multikey.startsWith(BASE58_MULTIBASE_PREFIX)) { 13 23 throw new Error(`Incorrect prefix for multikey: ${multikey}`); ··· 18 28 ); 19 29 }; 20 30 31 + /** 32 + * Checks if the given bytes have the specified prefix. 33 + * @param bytes - The bytes to check. 34 + * @param prefix - The prefix to check for. 35 + * @returns True if the bytes have the specified prefix, false otherwise. 36 + */ 21 37 export const hasPrefix = (bytes: Uint8Array, prefix: Uint8Array): boolean => { 22 38 return equals(prefix, bytes.subarray(0, prefix.byteLength)); 23 39 }; 24 40 41 + /** 42 + * Detects the signature format of the given bytes. 43 + * @param sig - The signature bytes to detect the format of. 44 + * @returns The signature format, either "compact" or "der". 45 + */ 25 46 export function detectSigFormat(sig: Uint8Array): "compact" | "der" { 26 47 if (sig.length === 65) { 27 48 throw new Error(
+16
crypto/verify.ts
··· 3 3 import { plugins } from "./plugins.ts"; 4 4 import type { VerifyOptions } from "./types.ts"; 5 5 6 + /** 7 + * Verifies a given signature is valid for the given data using the specified DID key and algorithm. 8 + * @param didKey - The DID key to verify the signature with 9 + * @param data - The data to verify the signature against 10 + * @param sig - The signature to verify 11 + * @param opts - Options for loosening verification and jwt algorithm 12 + * @returns True if the signature is valid, false otherwise 13 + */ 6 14 export const verifySignature = ( 7 15 didKey: string, 8 16 data: Uint8Array, ··· 22 30 return plugin.verifySignature(didKey, data, sig, opts); 23 31 }; 24 32 33 + /** 34 + * {@linkcode verifySignature} with string inputs converted to bytes using UTF-8 encoding 35 + * @param didKey - The DID key string to verify the signature with 36 + * @param data - The data string to verify the signature against 37 + * @param sig - The signature string to verify 38 + * @param opts - Options for loosening verification 39 + * @returns True if the signature is valid, false otherwise 40 + */ 25 41 export const verifySignatureUtf8 = ( 26 42 didKey: string, 27 43 data: string,
+14 -48
deno.lock
··· 13 13 "jsr:@noble/curves@^2.0.1": "2.0.1", 14 14 "jsr:@noble/hashes@2": "2.0.1", 15 15 "jsr:@noble/hashes@^2.0.1": "2.0.1", 16 - "jsr:@std/assert@*": "1.0.14", 17 - "jsr:@std/assert@^1.0.13": "1.0.14", 18 - "jsr:@std/assert@^1.0.14": "1.0.14", 19 - "jsr:@std/async@^1.0.13": "1.0.13", 16 + "jsr:@std/assert@^1.0.14": "1.0.15", 20 17 "jsr:@std/bytes@^1.0.5": "1.0.6", 21 - "jsr:@std/bytes@^1.0.6": "1.0.6", 22 18 "jsr:@std/cbor@~0.1.8": "0.1.8", 23 - "jsr:@std/crypto@*": "1.0.5", 24 - "jsr:@std/crypto@^1.0.5": "1.0.5", 25 - "jsr:@std/data-structures@^1.0.9": "1.0.9", 26 19 "jsr:@std/encoding@^1.0.10": "1.0.10", 27 20 "jsr:@std/encoding@~1.0.5": "1.0.10", 28 21 "jsr:@std/fmt@~1.0.2": "1.0.8", 29 22 "jsr:@std/fs@1": "1.0.19", 30 23 "jsr:@std/fs@^1.0.19": "1.0.19", 31 - "jsr:@std/internal@^1.0.10": "1.0.10", 32 - "jsr:@std/internal@^1.0.9": "1.0.10", 24 + "jsr:@std/internal@^1.0.10": "1.0.12", 25 + "jsr:@std/internal@^1.0.12": "1.0.12", 26 + "jsr:@std/internal@^1.0.9": "1.0.12", 33 27 "jsr:@std/io@~0.224.9": "0.224.9", 34 28 "jsr:@std/path@1": "1.1.2", 35 29 "jsr:@std/path@^1.1.1": "1.1.2", 36 30 "jsr:@std/path@^1.1.2": "1.1.2", 37 - "jsr:@std/streams@^1.0.12": "1.0.12", 38 - "jsr:@std/streams@^1.0.9": "1.0.12", 39 - "jsr:@std/testing@^1.0.15": "1.0.15", 31 + "jsr:@std/streams@^1.0.9": "1.0.13", 40 32 "jsr:@std/text@~1.0.7": "1.0.16", 41 33 "jsr:@ts-morph/common@0.27": "0.27.0", 42 34 "jsr:@ts-morph/ts-morph@26": "26.0.0", ··· 115 107 "@noble/hashes@2.0.1": { 116 108 "integrity": "e0e908292a0bf91099cf8ba0720a1647cef82ab38b588815b5e9535b4ff4d7bb" 117 109 }, 118 - "@std/assert@1.0.14": { 119 - "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", 110 + "@std/assert@1.0.15": { 111 + "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", 120 112 "dependencies": [ 121 - "jsr:@std/internal@^1.0.10" 113 + "jsr:@std/internal@^1.0.12" 122 114 ] 123 115 }, 124 - "@std/async@1.0.13": { 125 - "integrity": "1d76ca5d324aef249908f7f7fe0d39aaf53198e5420604a59ab5c035adc97c96" 126 - }, 127 116 "@std/bytes@1.0.6": { 128 117 "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" 129 118 }, 130 119 "@std/cbor@0.1.8": { 131 120 "integrity": "a0d1c520f8963358cc96defd8cbd1f9e81e40adc2bbfb301f122150f2024d93e", 132 121 "dependencies": [ 133 - "jsr:@std/bytes@^1.0.5", 134 - "jsr:@std/streams@^1.0.9" 122 + "jsr:@std/bytes", 123 + "jsr:@std/streams" 135 124 ] 136 - }, 137 - "@std/crypto@1.0.5": { 138 - "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" 139 - }, 140 - "@std/data-structures@1.0.9": { 141 - "integrity": "033d6e17e64bf1f84a614e647c1b015fa2576ae3312305821e1a4cb20674bb4d" 142 125 }, 143 126 "@std/encoding@1.0.10": { 144 127 "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" ··· 153 136 "jsr:@std/path@^1.1.1" 154 137 ] 155 138 }, 156 - "@std/internal@1.0.10": { 157 - "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" 139 + "@std/internal@1.0.12": { 140 + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 158 141 }, 159 142 "@std/io@0.224.9": { 160 143 "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3" ··· 168 151 "@std/streams@1.0.10": { 169 152 "integrity": "75c0b1431873cd0d8b3d679015220204d36d3c7420d93b60acfc379eb0dc30af" 170 153 }, 171 - "@std/streams@1.0.12": { 172 - "integrity": "ae925fa1dc459b1abf5cbaa28cc5c7b0485853af3b2a384b0dc22d86e59dfbf4", 173 - "dependencies": [ 174 - "jsr:@std/bytes@^1.0.6" 175 - ] 176 - }, 177 - "@std/testing@1.0.15": { 178 - "integrity": "a490169f5ccb0f3ae9c94fbc69d2cd43603f2cffb41713a85f99bbb0e3087cbc", 179 - "dependencies": [ 180 - "jsr:@std/assert@^1.0.13", 181 - "jsr:@std/async", 182 - "jsr:@std/data-structures", 183 - "jsr:@std/fs@^1.0.19", 184 - "jsr:@std/internal@^1.0.10", 185 - "jsr:@std/path@^1.1.1" 186 - ] 154 + "@std/streams@1.0.13": { 155 + "integrity": "772d208cd0d3e5dac7c1d9e6cdb25842846d136eea4a41a62e44ed4ab0c8dd9e" 187 156 }, 188 157 "@std/text@1.0.16": { 189 158 "integrity": "ddb9853b75119a2473857d691cf1ec02ad90793a2e8b4a4ac49d7354281a0cf8" ··· 1077 1046 "jsr:@logtape/file@^1.2.0-dev.344+834f24a9", 1078 1047 "jsr:@logtape/logtape@^1.2.0-dev.344+834f24a9", 1079 1048 "jsr:@std/cbor@~0.1.8", 1080 - "jsr:@std/crypto@^1.0.5", 1081 1049 "jsr:@std/encoding@^1.0.10", 1082 1050 "jsr:@std/fs@^1.0.19", 1083 - "jsr:@std/streams@^1.0.12", 1084 1051 "jsr:@zod/zod@^4.1.11", 1085 1052 "npm:@ipld/dag-cbor@^9.2.5", 1086 1053 "npm:multiformats@^13.4.1" ··· 1095 1062 }, 1096 1063 "identity": { 1097 1064 "dependencies": [ 1098 - "jsr:@std/testing@^1.0.15", 1099 1065 "npm:@did-plc/lib@^0.0.4", 1100 1066 "npm:@did-plc/server@^0.0.1", 1101 1067 "npm:get-port@^7.1.0"
-1
identity/deno.json
··· 6 6 "imports": { 7 7 "@did-plc/lib": "npm:@did-plc/lib@^0.0.4", 8 8 "@did-plc/server": "npm:@did-plc/server@^0.0.1", 9 - "@std/testing": "jsr:@std/testing@^1.0.15", 10 9 "get-port": "npm:get-port@^7.1.0" 11 10 }, 12 11 "test": {
+1
identity/did/web-resolver.ts
··· 6 6 import { BaseResolver } from "./base-resolver.ts"; 7 7 import { timed } from "./util.ts"; 8 8 9 + /** Path to the DID document on a `did:web` DID. */ 9 10 export const DOC_PATH = "/.well-known/did.json"; 10 11 11 12 export class DidWebResolver extends BaseResolver {
+20
identity/errors.ts
··· 1 + /** Error thrown when a DID cannot be found. 2 + * Could be due to a non-existent DID or network issues. 3 + */ 1 4 export class DidNotFoundError extends Error { 2 5 constructor(public did: string) { 3 6 super(`Could not resolve DID: ${did}`); 4 7 } 5 8 } 6 9 10 + /** 11 + * Error thrown when a DID is not formatted correctly. 12 + * Most commonly, a DID missing `did:` at the beginning 13 + * or a DID missing a method. 14 + */ 7 15 export class PoorlyFormattedDidError extends Error { 8 16 constructor(public did: string) { 9 17 super(`Poorly formatted DID: ${did}`); 10 18 } 11 19 } 12 20 21 + /** 22 + * Error thrown for unsupported methods. 23 + * The methods supported are `did:plc` and `did:web`. 24 + */ 13 25 export class UnsupportedDidMethodError extends Error { 14 26 constructor(public did: string) { 15 27 super(`Unsupported DID method: ${did}`); 16 28 } 17 29 } 18 30 31 + /** 32 + * Error thrown for DIDs where DID formatting could not be 33 + * validated or parsed. 34 + */ 19 35 export class PoorlyFormattedDidDocumentError extends Error { 20 36 constructor( 21 37 public did: string, ··· 25 41 } 26 42 } 27 43 44 + /** 45 + * Error thrown for `did:web` DIDs where the path is not supported. 46 + * Caused by more than one path segment. 47 + */ 28 48 export class UnsupportedDidWebPathError extends Error { 29 49 constructor(public did: string) { 30 50 super(`Unsupported did:web paths: ${did}`);
+3
identity/id-resolver.ts
··· 2 2 import { HandleResolver } from "./handle/index.ts"; 3 3 import type { IdentityResolverOpts } from "./types.ts"; 4 4 5 + /** 6 + * Combines Handle and DID resolvers into a single identity resolver class. 7 + */ 5 8 export class IdResolver { 6 9 public handle: HandleResolver; 7 10 public did: DidResolver;
+40
identity/mod.ts
··· 1 + /** 2 + * # Identity Resolution in AT Protocol 3 + * 4 + * Library for decentralized identities in AT Protocol 5 + * using DIDs and handles. 6 + * 7 + * Handles are resolved to DIDs and DIDs can be resolved to DID 8 + * documents, which can be used to get information about the user 9 + * such as their verification method, service endpoints and handle. 10 + * 11 + * @example Resolving a Handle and verifying against DID document 12 + * ```typescript 13 + * const didres = new DidResolver({}) 14 + * const hdlres = new HandleResolver({}) 15 + * 16 + * const handle = 'atproto.com' 17 + * const did = await hdlres.resolve(handle) 18 + * 19 + * if (did == undefined) { 20 + * throw new Error('expected handle to resolve') 21 + * } 22 + * console.log(did) // did:plc:ewvi7nxzyoun6zhxrhs64oiz 23 + * 24 + * const doc = await didres.resolve(did) 25 + * console.log(doc) 26 + * 27 + * // additional resolutions of same DID will be cached for some time, 28 + * // unless forceRefresh flag is used 29 + * const doc2 = await didres.resolve(did, true) 30 + * 31 + * // helper methods use the same cache 32 + * const data = await didres.resolveAtprotoData(did) 33 + * 34 + * if (data.handle != handle) { 35 + * throw new Error('invalid handle (did not match DID document)') 36 + * } 37 + * ``` 38 + * 39 + * @module 40 + */ 1 41 export * from "./did/index.ts"; 2 42 export * from "./handle/index.ts"; 3 43 export * from "./id-resolver.ts";
+33
identity/types.ts
··· 3 3 export { didDocument } from "@atp/common"; 4 4 export type { DidDocument } from "@atp/common"; 5 5 6 + /** 7 + * Options for a combined handle and did resolver. 8 + * @property timeout - Timeout in milliseconds for resolving handles. 9 + * @property plcUrl - URL of the PLC registry or mirror used for the `did:plc` method. 10 + * @property didCache - Cache for storing recently resolved DID documents. 11 + * @property backupNameservers - List of backup nameservers to use for handle resolution. 12 + */ 6 13 export type IdentityResolverOpts = { 7 14 timeout?: number; 8 15 plcUrl?: string; ··· 10 17 backupNameservers?: string[]; 11 18 }; 12 19 20 + /** 21 + * Options for a handle resolver. 22 + * @property timeout - Timeout in milliseconds for resolving handles. 23 + * @property backupNameservers - List of backup nameservers to use if the primary DNS nameservers fails. 24 + */ 13 25 export type HandleResolverOpts = { 14 26 timeout?: number; 15 27 backupNameservers?: string[]; 16 28 }; 17 29 30 + /** 31 + * Options for a DID resolver. 32 + * @property timeout - Timeout in milliseconds for resolving DIDs. 33 + * @property plcUrl - URL of the PLC registry or mirror used for the `did:plc` method. 34 + * @property didCache - Cache for storing recently resolved DID documents. 35 + */ 18 36 export type DidResolverOpts = { 19 37 timeout?: number; 20 38 plcUrl?: string; 21 39 didCache?: DidCache; 22 40 }; 23 41 42 + /** 43 + * Data associated with an AT Protocol repository. 44 + * @property did - The decentralized identifier of the repository. Never changes. 45 + * @property signingKey - The public key used for signing records and operations. 46 + * @property handle - The domain used for representing the repository to users, can change over time. 47 + * @property pds - The URL of the repository's personal data server, where the repository's data is stored. 48 + */ 24 49 export type AtprotoData = { 25 50 did: string; 26 51 signingKey: string; ··· 28 53 pds: string; 29 54 }; 30 55 56 + /** 57 + * Stored when caching resolved DID documents. 58 + * @property did - Decentralized identifier of the repository 59 + * @property doc - The resolved DID document 60 + * @property updatedAt - Timestamp of when the cache entry was last updated 61 + * @property stale - Whether the cache entry is too old and needs to be refreshed 62 + * @property expired - Whether the cache entry has expired and should be removed 63 + */ 31 64 export type CacheResult = { 32 65 did: string; 33 66 doc: DidDocument;
+12
repo/mod.ts
··· 1 + /** 2 + * Utilities for working with atproto repositories, and in particular the 3 + * Merkle Search Tree (MST) data structure. 4 + * 5 + * Repositories in atproto are signed key/value stores containing CBOR-encoded 6 + * data records. The structure and implementation details are described in 7 + * {@link https://atproto.com/specs/repository | the specification.} 8 + * This includes MST node format, serialization, structural 9 + * constraints, and more. 10 + * 11 + * @module 12 + */ 1 13 export * from "./block-map.ts"; 2 14 export * from "./cid-set.ts"; 3 15 export * from "./repo.ts";
+135
sync/firehose/index.ts
··· 42 42 type Sync, 43 43 } from "./lexicons.ts"; 44 44 45 + /** 46 + * The options for the firehose. 47 + * @property idResolver - used to resolve dids. 48 + * @property handleEvent - Handles indexing logic for each event after it is parsed and authenticated. 49 + * @property onError - Handles logic for non-fatal errors that are encountered. In most cases, these can just be logged. 50 + * @property getCursor - Logic for retrieving the start cursor. Not allowed if runner is provided. 51 + * @property runner - In-memory partitioned queue for processing events from different repos concurrently. 52 + * @property service - Relay service URL. Defaults to Bluesky's `wss://bsky.network` 53 + * @property subscriptionReconnectDelay - Delay in milliseconds before reconnecting to the firehose after a disconnection. Defaults to 3000ms. 54 + * @property unauthenticatedCommits - Whether to allow unauthenticated commits. Defaults to false, only recommended for testing. 55 + * @property unauthenticatedHandles - Whether to allow unauthenticated handles. Defaults to false, only recommended for testing. 56 + * @property filterCollections - Client-side filtering of lexicon record collections to include in event handling. Filtering happens client-side. Defaults to an empty array. 57 + * @property excludeIdentity - Excludes identity events from handling. Defaults to false. 58 + * @property excludeAccount - Excludes account events from handling. Defaults to false. 59 + * @property excludeCommit - Excludes commit events from handling. Defaults to false. 60 + * @property excludeSync - Excludes repo sync events from handling. Defaults to false. 61 + */ 45 62 export type FirehoseOptions = WebSocketOptions & { 46 63 idResolver: IdResolver; 47 64 ··· 64 81 excludeSync?: boolean; 65 82 }; 66 83 84 + /** 85 + * The firehose class will spin up a websocket connection to 86 + * com.atproto.sync.subscribeRepos on a given repo host 87 + * (by default the Relay run by Bluesky). 88 + * Each event will be parsed, authenticated, and then passed on to the 89 + * supplied handleEvent which can handle indexing logic. 90 + * On Commit events, the firehose will verify signatures and repo proofs 91 + * to ensure that the event is authentic. This can be disabled with the 92 + * unauthenticatedCommits flag. Similarly on Identity events, the firehose 93 + * will fetch the latest DID document for the repo and do bidirectional 94 + * verification on the associated handle. This can be disabled with the 95 + * unauthenticatedHandles flag. 96 + * 97 + * Events of a certain type can be excluded using the 98 + * excludeIdentity/excludeAccount/excludeCommit flags. 99 + * 100 + * And repo writes can be filtered down to specific collections using 101 + * filterCollections. By default, all events are parsed and passed 102 + * through to the handler. Note that this filtered currently happens 103 + * client-side, though it is likely we will introduce server-side 104 + * methods for doing so in the future. 105 + * 106 + * When using the firehose class, events are processed serially. 107 + * Each event must be finished being handled before the next one is parsed 108 + * and authenticated. 109 + * 110 + * @example Simple indexing service 111 + * ```typescript 112 + * import { Firehose } from '@atproto/sync' 113 + * import { IdResolver } from '@atproto/identity' 114 + * 115 + * const idResolver = new IdResolver() 116 + * const firehose = new Firehose({ 117 + * idResolver, 118 + * service: 'wss://bsky.network', 119 + * handleEvt: async (evt) => { 120 + * if (evt.event === 'identity') { 121 + * // ... 122 + * } else if (evt.event === 'account') { 123 + * // ... 124 + * } else if (evt.event === 'create') { 125 + * // ... 126 + * } else if (evt.event === 'update') { 127 + * // ... 128 + * } else if (evt.event === 'delete') { 129 + * // ... 130 + * } 131 + * }, 132 + * onError: (err) => { 133 + * console.error(err) 134 + * }, 135 + * filterCollections: ['com.myexample.app'], 136 + * }) 137 + * firehose.start() 138 + * 139 + * // on service shutdown 140 + * await firehose.destroy() 141 + * ``` 142 + * 143 + * For more robust indexing pipelines, it's recommended to use the 144 + * supplied MemoryRunner class. This provides an in-memory partitioned 145 + * queue. As events from a given repo must be processed in order, this 146 + * allows events to be processed concurrently while still processing 147 + * events from any given repo serially. 148 + * 149 + * The MemoryRunner also tracks an internal cursor based on the last 150 + * finished consecutive work. This ensures that no events are dropped, 151 + * although it does mean that some events may occassionally be replayed 152 + * (if the websocket drops and reconnects) and therefore it's recommended 153 + * that any indexing logic is idempotent. An optional setCursor parameter 154 + * may be supplied to the MemoryRunner which can be used to persistently 155 + * store the most recently processed cursor. 156 + * 157 + * @example Indexing with MemoryRunner 158 + * ```typescript 159 + * import { Firehose, MemoryRunner } from '@atproto/sync' 160 + * import { IdResolver } from '@atproto/identity' 161 + * 162 + * const idResolver = new IdResolver() 163 + * const runner = new MemoryRunner({ 164 + * setCursor: (cursor) => { 165 + * // persist cursor 166 + * }, 167 + * }) 168 + * const firehose = new Firehose({ 169 + * idResolver, 170 + * runner, 171 + * service: 'wss://bsky.network', 172 + * handleEvt: async (evt) => { 173 + * // ... 174 + * }, 175 + * onError: (err) => { 176 + * console.error(err) 177 + * }, 178 + * }) 179 + * firehose.start() 180 + * 181 + * // on service shutdown 182 + * await firehose.destroy() 183 + * await runner.destroy() 184 + * ``` 185 + * @property service - The service URL for the firehose. 186 + * @property runner - The runner for the firehose. 187 + * @property idResolver - The ID resolver for the firehose. 188 + * @property opts - The options for the firehose. 189 + */ 67 190 export class Firehose { 68 191 private sub: Subscription<RepoEvent>; 69 192 private abortController: AbortController; ··· 379 502 return ["takendown", "suspended", "deleted", "deactivated"].includes(str); 380 503 }; 381 504 505 + /** 506 + * An error in validating/authenticating an event from the firehose. 507 + */ 382 508 export class FirehoseValidationError extends Error { 383 509 constructor( 384 510 err: unknown, ··· 388 514 } 389 515 } 390 516 517 + /** 518 + * An error in parsing an event from the firehose. 519 + */ 391 520 export class FirehoseParseError extends Error { 392 521 constructor( 393 522 err: unknown, ··· 397 526 } 398 527 } 399 528 529 + /** 530 + * An error in the subscription to the firehose. 531 + */ 400 532 export class FirehoseSubscriptionError extends Error { 401 533 constructor(err: unknown) { 402 534 super("error on firehose subscription", { cause: err }); 403 535 } 404 536 } 405 537 538 + /** 539 + * An error in your firehose event handler logic. 540 + */ 406 541 export class FirehoseHandlerError extends Error { 407 542 constructor( 408 543 err: unknown,
+3
sync/firehose/lexicons.ts
··· 159 159 [k: string]: unknown; 160 160 } 161 161 162 + /** Determines if an event is a RepoOp */ 162 163 export function isRepoOp(v: unknown): v is RepoOp { 163 164 return ( 164 165 isObj(v) && ··· 167 168 ); 168 169 } 169 170 171 + /** Lexicon type for com.atproto.sync.subscribeRepos */ 170 172 export const ComAtprotoSyncSubscribeRepos: LexiconDoc = { 171 173 lexicon: 1, 172 174 id: "com.atproto.sync.subscribeRepos", ··· 438 440 439 441 const lexicons = new Lexicons([ComAtprotoSyncSubscribeRepos]); 440 442 443 + /** Validates a repo event */ 441 444 export const isValidRepoEvent = (evt: unknown) => { 442 445 return lexicons.assertValidXrpcMessage<RepoEvent>( 443 446 "com.atproto.sync.subscribeRepos",
+69
sync/mod.ts
··· 1 + /** 2 + * # AT Protocol Sync Tool 3 + * 4 + * This module provides tools for syncing data from AT Protocol. 5 + * Currently, it supports firehose (relay) subscriptions. 6 + * 7 + * The firehose class will spin up a websocket connection to 8 + * com.atproto.sync.subscribeRepos on a given repo host 9 + * (by default the Relay run by Bluesky). 10 + * Each event will be parsed, authenticated, and then passed on to the 11 + * supplied handleEvt which can handle indexing. 12 + * On Commit events, the firehose will verify signatures and repo proofs 13 + * to ensure that the event is authentic. This can be disabled with the 14 + * unauthenticatedCommits flag. Similarly on Identity events, the firehose 15 + * will fetch the latest DID document for the repo and do bidirectional 16 + * verification on the associated handle. This can be disabled with the 17 + * unauthenticatedHandles flag. 18 + * 19 + * Events of a certain type can be excluded using the 20 + * excludeIdentity/excludeAccount/excludeCommit flags. 21 + * 22 + * And repo writes can be filtered down to specific collections using 23 + * filterCollections. By default, all events are parsed and passed 24 + * through to the handler. Note that this filtered currently happens 25 + * client-side, though it is likely we will introduce server-side 26 + * methods for doing so in the future. 27 + * 28 + * Non-fatal errors that are encountered will be passed to the required 29 + * onError handler. In most cases, these can just be logged. 30 + * 31 + * When using the firehose class, events are processed serially. 32 + * Each event must be finished being handled before the next one is parsed 33 + * and authenticated. 34 + * 35 + * @example Simple indexing service 36 + * ```typescript 37 + * import { Firehose } from '@atproto/sync' 38 + * import { IdResolver } from '@atproto/identity' 39 + * 40 + * const idResolver = new IdResolver() 41 + * const firehose = new Firehose({ 42 + * idResolver, 43 + * service: 'wss://bsky.network', 44 + * handleEvt: async (evt) => { 45 + * if (evt.event === 'identity') { 46 + * // ... 47 + * } else if (evt.event === 'account') { 48 + * // ... 49 + * } else if (evt.event === 'create') { 50 + * // ... 51 + * } else if (evt.event === 'update') { 52 + * // ... 53 + * } else if (evt.event === 'delete') { 54 + * // ... 55 + * } 56 + * }, 57 + * onError: (err) => { 58 + * console.error(err) 59 + * }, 60 + * filterCollections: ['com.myexample.app'], 61 + * }) 62 + * firehose.start() 63 + * 64 + * // on service shutdown 65 + * await firehose.destroy() 66 + * ``` 67 + * 68 + * @module 69 + */ 1 70 export * from "./runner/index.ts"; 2 71 export * from "./firehose/index.ts"; 3 72 export * from "./events.ts";
+3 -2
sync/runner/memory-runner.ts
··· 9 9 setCursorInterval?: number; // milliseconds between persisted cursor saves (throttling) 10 10 }; 11 11 12 - // A queue with arbitrarily many partitions, each processing work sequentially. 13 - // Partitions are created lazily and taken out of memory when they go idle. 12 + /** A queue with arbitrarily many partitions, each processing work sequentially. 13 + * Partitions are created lazily and taken out of memory when they go idle. 14 + */ 14 15 export class MemoryRunner implements EventRunner { 15 16 consecutive: ConsecutiveList<number> = new ConsecutiveList<number>(); 16 17 mainQueue: PQueue;
+5 -5
sync/tests/runner_test.ts
··· 29 29 const complete: number[] = []; 30 30 // partition 1 items start slow but get faster: slow should still complete first. 31 31 runner.addTask("1", async () => { 32 - await wait(8); 32 + await wait(30); 33 33 complete.push(11); 34 34 }); 35 35 runner.addTask("1", async () => { 36 - await wait(4); 36 + await wait(20); 37 37 complete.push(12); 38 38 }); 39 39 runner.addTask("1", async () => { 40 - await wait(0); 40 + await wait(1); 41 41 complete.push(13); 42 42 }); 43 43 assertEquals(runner.partitions.size, 1); 44 44 // partition 2 items complete quickly except the last, which is slowest of all events. 45 45 runner.addTask("2", async () => { 46 - await wait(0); 46 + await wait(1); 47 47 complete.push(21); 48 48 }); 49 49 runner.addTask("2", async () => { ··· 55 55 complete.push(23); 56 56 }); 57 57 runner.addTask("2", async () => { 58 - await wait(10); 58 + await wait(60); 59 59 complete.push(24); 60 60 }); 61 61 assertEquals(runner.partitions.size, 2);
+1 -1
syntax/deno.json
··· 1 1 { 2 2 "name": "@atp/syntax", 3 - "version": "0.1.0-alpha.1", 3 + "version": "0.1.0-alpha.2", 4 4 "exports": "./mod.ts", 5 5 "license": "MIT", 6 6 "test": {
+21 -15
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 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 + * @param did - DID to validate 15 + * @throws {InvalidDidError} if the DID is invalid 16 + */ 14 17 export const ensureValidDid = (did: string): void => { 15 18 if (!did.startsWith("did:")) { 16 19 throw new InvalidDidError("DID requires 'did:' prefix"); ··· 43 46 } 44 47 }; 45 48 49 + /** 50 + * Simple regex version of {@linkcode ensureValidDid} constraints 51 + * @param did - DID to validate 52 + * @throws {InvalidDidError} - if the DID is invalid 53 + */ 46 54 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 55 if (!/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/.test(did)) { 50 56 throw new InvalidDidError("DID didn't validate via regex"); 51 57 }
+10 -9
syntax/handle.ts
··· 1 1 export const INVALID_HANDLE = "handle.invalid"; 2 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 3 + /** Registration-time restrictions, not protocol-level restrictions. 4 + * `.test` is allowed but only should be used in testing and development. 5 + * @see {https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains} 6 + */ 7 7 export const DISALLOWED_TLDS = [ 8 8 ".local", 9 9 ".arpa", ··· 14 14 ".alt", 15 15 // policy could concievably change on ".onion" some day 16 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 17 ]; 20 18 21 19 /** ··· 143 141 144 142 /** 145 143 * Thrown when a handle is invalid. 144 + * Caused by invalid characters (only ASCII letters, digits, dashes, periods are allowed), 145 + * length longer than 253 characters, or one of the {@linkcode DISALLOWED_TLDS} used. 146 146 */ 147 147 export class InvalidHandleError extends Error {} 148 - /** @deprecated Never used */ 148 + 149 + /** @deprecated Use {@linkcode InvalidHandleError} */ 149 150 export class ReservedHandleError extends Error {} 150 - /** @deprecated Never used */ 151 + /** @deprecated Use {@linkcode InvalidHandleError} */ 151 152 export class UnsupportedDomainError extends Error {} 152 - /** @deprecated Never used */ 153 + /** @deprecated Use {@linkcode InvalidHandleError} */ 153 154 export class DisallowedDomainError extends Error {}
+10
syntax/recordkey.ts
··· 1 + /** 2 + * Validates a record key (rkey) 3 + * @param rkey - Record key to validate 4 + * @throws {InvalidRecordKeyError} if the record key is invalid 5 + */ 1 6 export const ensureValidRecordKey = (rkey: string): void => { 2 7 if (rkey.length > 512 || rkey.length < 1) { 3 8 throw new InvalidRecordKeyError("record key must be 1 to 512 characters"); ··· 11 16 } 12 17 }; 13 18 19 + /** 20 + * Validates a record key (rkey) to a boolean 21 + * @param rkey - Record key to validate 22 + * @returns true if the record key is valid, false otherwise 23 + */ 14 24 export const isValidRecordKey = (rkey: string): boolean => { 15 25 try { 16 26 ensureValidRecordKey(rkey);
+46
xrpc/client.ts
··· 25 25 isErrorResponseBody, 26 26 } from "./util.ts"; 27 27 28 + /** 29 + * HTTP Client for AT Protocol XRPC APIs. 30 + * 31 + * Provides methods for making HTTP requests to AT Protocol XRPC APIs 32 + * with lexicon validation and response parsing. 33 + * 34 + * @example Fetching an XRPC endpoint 35 + * ```typescript 36 + * import { LexiconDoc } from '@atp/lexicon' 37 + * import { XrpcClient } from '@atp/xrpc' 38 + * 39 + * const pingLexicon = { 40 + * lexicon: 1, 41 + * id: 'io.example.ping', 42 + * defs: { 43 + * main: { 44 + * type: 'query', 45 + * description: 'Ping the server', 46 + * parameters: { 47 + * type: 'params', 48 + * properties: { message: { type: 'string' } }, 49 + * }, 50 + * output: { 51 + * encoding: 'application/json', 52 + * schema: { 53 + * type: 'object', 54 + * required: ['message'], 55 + * properties: { message: { type: 'string' } }, 56 + * }, 57 + * }, 58 + * }, 59 + * }, 60 + * } satisfies LexiconDoc 61 + * 62 + * const xrpc = new XrpcClient('https://ping.example.com', [ 63 + * // Any number of lexicon here 64 + * pingLexicon, 65 + * ]) 66 + * 67 + * const res1 = await xrpc.call('io.example.ping', { 68 + * message: 'hello world', 69 + * }) 70 + * res1.encoding // => 'application/json' 71 + * res1.body // => {message: 'hello world'} 72 + * ``` 73 + */ 28 74 export class XrpcClient { 29 75 readonly fetchHandler: FetchHandler; 30 76 readonly headers: Map<string, Gettable<null | string>> = new Map<
-2
xrpc/mod.ts
··· 33 33 * } satisfies LexiconDoc 34 34 * 35 35 * const xrpc = new XrpcClient('https://ping.example.com', [ 36 - * // Any number of lexicon here 37 36 * pingLexicon, 38 37 * ]) 39 38 * 40 39 * const res1 = await xrpc.call('io.example.ping', { 41 40 * message: 'hello world', 42 41 * }) 43 - * res1.encoding // => 'application/json' 44 42 * res1.body // => {message: 'hello world'} 45 43 * ``` 46 44 * @module
+18 -1
xrpc/types.ts
··· 5 5 export type HeadersMap = Record<string, string | undefined>; 6 6 7 7 export type { 8 - /** @deprecated not to be confused with the WHATWG Headers constructor */ 8 + /** 9 + * @deprecated not to be confused with the WHATWG Headers constructor. 10 + * Use {@linkcode HeadersMap} instead. 11 + */ 9 12 HeadersMap as Headers, 10 13 }; 11 14 ··· 71 74 return ResponseType[httpResponseCodeToEnum(status)]; 72 75 } 73 76 77 + /** 78 + * Error messages corresponding to XRPC error codes. 79 + */ 74 80 export const ResponseTypeStrings: Record<ResponseType, string> = { 75 81 [ResponseType.Unknown]: "Unknown", 76 82 [ResponseType.InvalidResponse]: "Invalid Response", ··· 94 100 return ResponseTypeStrings[httpResponseCodeToEnum(status)]; 95 101 } 96 102 103 + /** 104 + * Response type of a successful XRPC request. 105 + */ 97 106 export class XRPCResponse { 98 107 success = true; 99 108 ··· 103 112 ) {} 104 113 } 105 114 115 + /** 116 + * Response type of a failed XRPC request with details of the error. 117 + */ 106 118 export class XRPCError extends Error { 107 119 success = false; 108 120 ··· 166 178 } 167 179 } 168 180 181 + /** 182 + * Error for an invalid response from an XRPC request. 183 + * Caused by a validation error with the lexicon schema 184 + * matching the NSID of the endpoint. 185 + */ 169 186 export class XRPCInvalidResponseError extends XRPCError { 170 187 constructor( 171 188 public lexiconNsid: string,