···66import { sha256 } from "multiformats/hashes/sha2";
77import { schema } from "./types.ts";
88import * as check from "./check.ts";
99-import { crypto } from "@std/crypto";
109import { concat, equals, fromString, toString } from "@atp/bytes";
11101211export const cborEncode = cborCodec.encode;
+7-1
crypto/plugins.ts
···11+import type { DidKeyPlugin } from "@atp/crypto";
12import { p256Plugin } from "./p256/plugin.ts";
23import { secp256k1Plugin } from "./secp256k1/plugin.ts";
3444-export const plugins = [p256Plugin, secp256k1Plugin];
55+/**
66+ * Plugins for different elliptic curves.
77+ *
88+ * Currently supports P-256 and secp256k1.
99+ */
1010+export const plugins: DidKeyPlugin[] = [p256Plugin, secp256k1Plugin];
+2
crypto/random.ts
···22import { type SupportedEncodings, toString } from "@atp/bytes";
33import { sha256 } from "./sha.ts";
4455+/** Generate random bytes using noble hashes' randomBytes function. */
56export const randomBytes = noble.randomBytes;
6788+/** Generate random string from {@linkcode randomBytes}. */
79export const randomStr = (
810 byteLength: number,
911 encoding: SupportedEncodings,
+9-3
crypto/sha.ts
···11import * as noble from "@noble/hashes/sha2.js";
22import { fromString, toString } from "@atp/bytes";
3344-// takes either bytes of utf8 input
55-// @TODO this can be sync
44+/**
55+ * Creates a SHA-256 hash of the input.
66+ * Takes either bytes of utf8 input
77+ * @param input - Bytes to hash.
88+ */
69export const sha256 = (
710 input: Uint8Array | string,
811): Uint8Array => {
···1013 return noble.sha256(bytes);
1114};
12151313-// @TODO this can be sync
1616+/**
1717+ * Hashes the input using SHA-256 and returns the result as a hexadecimal string.
1818+ * @param input - Bytes to hash.
1919+ */
1420export const sha256Hex = (
1521 input: Uint8Array | string,
1622): string => {
+31
crypto/types.ts
···11+/**
22+ * Creates signatures for messages.
33+ *
44+ * @prop jwtAlg - The JWT algorithm used for signing.
55+ * @prop sign - Returns a signature for the given message bytes.
66+ */
17export interface Signer {
28 jwtAlg: string;
39 sign(msg: Uint8Array): Uint8Array;
410}
5111212+/**
1313+ * Can create DID keys.
1414+ * @prop did - Returns a DID key.
1515+ */
616export interface Didable {
717 did(): string;
818}
9192020+/**
2121+ * Combines a {@linkcode Signer} and {@linkcode Didable} into a Keypair.
2222+ */
1023export interface Keypair extends Signer, Didable {}
11242525+/**
2626+ * Keypair with an export method.
2727+ * @prop export - Exports the keypair as a Uint8Array.
2828+ */
1229export interface ExportableKeypair extends Keypair {
1330 export(): Promise<Uint8Array>;
1431}
15323333+/**
3434+ * DID key plugin with key compression and signature verification utilities.
3535+ * @prop prefix - The DID key prefix.
3636+ * @prop jwtAlg - The JWT algorithm used for signing.
3737+ * @prop verifySignature - Verifies a signature for the given message bytes.
3838+ * @prop compressPubkey - Compresses a public key.
3939+ * @prop decompressPubkey - Decompresses a compressed public key.
4040+ */
1641export type DidKeyPlugin = {
1742 prefix: Uint8Array;
1843 jwtAlg: string;
···2752 decompressPubkey: (compressed: Uint8Array) => Uint8Array;
2853};
29545555+/**
5656+ * Options for less strict signature verification.
5757+ * These options are only recommended for testing purposes.
5858+ * @prop allowMalleableSig - Don't enforce low-S signatures. Explicitly against specification. Only recommended for testing purposes.
5959+ * @prop allowDerSig - Allow DER-encoded signatures. Only recommended for testing purposes.
6060+ */
3061export type VerifyOptions = {
3162 allowMalleableSig?: boolean;
3263 allowDerSig?: boolean;
+21
crypto/utils.ts
···11import { equals, fromString } from "@atp/bytes";
22import { BASE58_MULTIBASE_PREFIX, DID_KEY_PREFIX } from "./const.ts";
3344+/**
55+ * Extracts the multikey from a `did:key` string.
66+ * @param did - The `did:key` string to extract the multikey from.
77+ * @throws Error if the input doesn't start with `did:key:`.
88+ */
49export const extractMultikey = (did: string): string => {
510 if (!did.startsWith(DID_KEY_PREFIX)) {
611 throw new Error(`Incorrect prefix for did:key: ${did}`);
···813 return did.slice(DID_KEY_PREFIX.length);
914};
10151616+/**
1717+ * Extracts the bytes from a multikey string using base58btc encoding.
1818+ * @param multikey - The multikey string to extract the bytes from.
1919+ * @throws Error if the input doesn't start with `z`.
2020+ */
1121export const extractPrefixedBytes = (multikey: string): Uint8Array => {
1222 if (!multikey.startsWith(BASE58_MULTIBASE_PREFIX)) {
1323 throw new Error(`Incorrect prefix for multikey: ${multikey}`);
···1828 );
1929};
20303131+/**
3232+ * Checks if the given bytes have the specified prefix.
3333+ * @param bytes - The bytes to check.
3434+ * @param prefix - The prefix to check for.
3535+ * @returns True if the bytes have the specified prefix, false otherwise.
3636+ */
2137export const hasPrefix = (bytes: Uint8Array, prefix: Uint8Array): boolean => {
2238 return equals(prefix, bytes.subarray(0, prefix.byteLength));
2339};
24404141+/**
4242+ * Detects the signature format of the given bytes.
4343+ * @param sig - The signature bytes to detect the format of.
4444+ * @returns The signature format, either "compact" or "der".
4545+ */
2546export function detectSigFormat(sig: Uint8Array): "compact" | "der" {
2647 if (sig.length === 65) {
2748 throw new Error(
+16
crypto/verify.ts
···33import { plugins } from "./plugins.ts";
44import type { VerifyOptions } from "./types.ts";
5566+/**
77+ * Verifies a given signature is valid for the given data using the specified DID key and algorithm.
88+ * @param didKey - The DID key to verify the signature with
99+ * @param data - The data to verify the signature against
1010+ * @param sig - The signature to verify
1111+ * @param opts - Options for loosening verification and jwt algorithm
1212+ * @returns True if the signature is valid, false otherwise
1313+ */
614export const verifySignature = (
715 didKey: string,
816 data: Uint8Array,
···2230 return plugin.verifySignature(didKey, data, sig, opts);
2331};
24323333+/**
3434+ * {@linkcode verifySignature} with string inputs converted to bytes using UTF-8 encoding
3535+ * @param didKey - The DID key string to verify the signature with
3636+ * @param data - The data string to verify the signature against
3737+ * @param sig - The signature string to verify
3838+ * @param opts - Options for loosening verification
3939+ * @returns True if the signature is valid, false otherwise
4040+ */
2541export const verifySignatureUtf8 = (
2642 didKey: string,
2743 data: string,
···66import { BaseResolver } from "./base-resolver.ts";
77import { timed } from "./util.ts";
8899+/** Path to the DID document on a `did:web` DID. */
910export const DOC_PATH = "/.well-known/did.json";
10111112export class DidWebResolver extends BaseResolver {
+20
identity/errors.ts
···11+/** Error thrown when a DID cannot be found.
22+ * Could be due to a non-existent DID or network issues.
33+ */
14export class DidNotFoundError extends Error {
25 constructor(public did: string) {
36 super(`Could not resolve DID: ${did}`);
47 }
58}
691010+/**
1111+ * Error thrown when a DID is not formatted correctly.
1212+ * Most commonly, a DID missing `did:` at the beginning
1313+ * or a DID missing a method.
1414+ */
715export class PoorlyFormattedDidError extends Error {
816 constructor(public did: string) {
917 super(`Poorly formatted DID: ${did}`);
1018 }
1119}
12202121+/**
2222+ * Error thrown for unsupported methods.
2323+ * The methods supported are `did:plc` and `did:web`.
2424+ */
1325export class UnsupportedDidMethodError extends Error {
1426 constructor(public did: string) {
1527 super(`Unsupported DID method: ${did}`);
1628 }
1729}
18303131+/**
3232+ * Error thrown for DIDs where DID formatting could not be
3333+ * validated or parsed.
3434+ */
1935export class PoorlyFormattedDidDocumentError extends Error {
2036 constructor(
2137 public did: string,
···2541 }
2642}
27434444+/**
4545+ * Error thrown for `did:web` DIDs where the path is not supported.
4646+ * Caused by more than one path segment.
4747+ */
2848export class UnsupportedDidWebPathError extends Error {
2949 constructor(public did: string) {
3050 super(`Unsupported did:web paths: ${did}`);
+3
identity/id-resolver.ts
···22import { HandleResolver } from "./handle/index.ts";
33import type { IdentityResolverOpts } from "./types.ts";
4455+/**
66+ * Combines Handle and DID resolvers into a single identity resolver class.
77+ */
58export class IdResolver {
69 public handle: HandleResolver;
710 public did: DidResolver;
+40
identity/mod.ts
···11+/**
22+ * # Identity Resolution in AT Protocol
33+ *
44+ * Library for decentralized identities in AT Protocol
55+ * using DIDs and handles.
66+ *
77+ * Handles are resolved to DIDs and DIDs can be resolved to DID
88+ * documents, which can be used to get information about the user
99+ * such as their verification method, service endpoints and handle.
1010+ *
1111+ * @example Resolving a Handle and verifying against DID document
1212+ * ```typescript
1313+ * const didres = new DidResolver({})
1414+ * const hdlres = new HandleResolver({})
1515+ *
1616+ * const handle = 'atproto.com'
1717+ * const did = await hdlres.resolve(handle)
1818+ *
1919+ * if (did == undefined) {
2020+ * throw new Error('expected handle to resolve')
2121+ * }
2222+ * console.log(did) // did:plc:ewvi7nxzyoun6zhxrhs64oiz
2323+ *
2424+ * const doc = await didres.resolve(did)
2525+ * console.log(doc)
2626+ *
2727+ * // additional resolutions of same DID will be cached for some time,
2828+ * // unless forceRefresh flag is used
2929+ * const doc2 = await didres.resolve(did, true)
3030+ *
3131+ * // helper methods use the same cache
3232+ * const data = await didres.resolveAtprotoData(did)
3333+ *
3434+ * if (data.handle != handle) {
3535+ * throw new Error('invalid handle (did not match DID document)')
3636+ * }
3737+ * ```
3838+ *
3939+ * @module
4040+ */
141export * from "./did/index.ts";
242export * from "./handle/index.ts";
343export * from "./id-resolver.ts";
+33
identity/types.ts
···33export { didDocument } from "@atp/common";
44export type { DidDocument } from "@atp/common";
5566+/**
77+ * Options for a combined handle and did resolver.
88+ * @property timeout - Timeout in milliseconds for resolving handles.
99+ * @property plcUrl - URL of the PLC registry or mirror used for the `did:plc` method.
1010+ * @property didCache - Cache for storing recently resolved DID documents.
1111+ * @property backupNameservers - List of backup nameservers to use for handle resolution.
1212+ */
613export type IdentityResolverOpts = {
714 timeout?: number;
815 plcUrl?: string;
···1017 backupNameservers?: string[];
1118};
12192020+/**
2121+ * Options for a handle resolver.
2222+ * @property timeout - Timeout in milliseconds for resolving handles.
2323+ * @property backupNameservers - List of backup nameservers to use if the primary DNS nameservers fails.
2424+ */
1325export type HandleResolverOpts = {
1426 timeout?: number;
1527 backupNameservers?: string[];
1628};
17293030+/**
3131+ * Options for a DID resolver.
3232+ * @property timeout - Timeout in milliseconds for resolving DIDs.
3333+ * @property plcUrl - URL of the PLC registry or mirror used for the `did:plc` method.
3434+ * @property didCache - Cache for storing recently resolved DID documents.
3535+ */
1836export type DidResolverOpts = {
1937 timeout?: number;
2038 plcUrl?: string;
2139 didCache?: DidCache;
2240};
23414242+/**
4343+ * Data associated with an AT Protocol repository.
4444+ * @property did - The decentralized identifier of the repository. Never changes.
4545+ * @property signingKey - The public key used for signing records and operations.
4646+ * @property handle - The domain used for representing the repository to users, can change over time.
4747+ * @property pds - The URL of the repository's personal data server, where the repository's data is stored.
4848+ */
2449export type AtprotoData = {
2550 did: string;
2651 signingKey: string;
···2853 pds: string;
2954};
30555656+/**
5757+ * Stored when caching resolved DID documents.
5858+ * @property did - Decentralized identifier of the repository
5959+ * @property doc - The resolved DID document
6060+ * @property updatedAt - Timestamp of when the cache entry was last updated
6161+ * @property stale - Whether the cache entry is too old and needs to be refreshed
6262+ * @property expired - Whether the cache entry has expired and should be removed
6363+ */
3164export type CacheResult = {
3265 did: string;
3366 doc: DidDocument;
+12
repo/mod.ts
···11+/**
22+ * Utilities for working with atproto repositories, and in particular the
33+ * Merkle Search Tree (MST) data structure.
44+ *
55+ * Repositories in atproto are signed key/value stores containing CBOR-encoded
66+ * data records. The structure and implementation details are described in
77+ * {@link https://atproto.com/specs/repository | the specification.}
88+ * This includes MST node format, serialization, structural
99+ * constraints, and more.
1010+ *
1111+ * @module
1212+ */
113export * from "./block-map.ts";
214export * from "./cid-set.ts";
315export * from "./repo.ts";
+135
sync/firehose/index.ts
···4242 type Sync,
4343} from "./lexicons.ts";
44444545+/**
4646+ * The options for the firehose.
4747+ * @property idResolver - used to resolve dids.
4848+ * @property handleEvent - Handles indexing logic for each event after it is parsed and authenticated.
4949+ * @property onError - Handles logic for non-fatal errors that are encountered. In most cases, these can just be logged.
5050+ * @property getCursor - Logic for retrieving the start cursor. Not allowed if runner is provided.
5151+ * @property runner - In-memory partitioned queue for processing events from different repos concurrently.
5252+ * @property service - Relay service URL. Defaults to Bluesky's `wss://bsky.network`
5353+ * @property subscriptionReconnectDelay - Delay in milliseconds before reconnecting to the firehose after a disconnection. Defaults to 3000ms.
5454+ * @property unauthenticatedCommits - Whether to allow unauthenticated commits. Defaults to false, only recommended for testing.
5555+ * @property unauthenticatedHandles - Whether to allow unauthenticated handles. Defaults to false, only recommended for testing.
5656+ * @property filterCollections - Client-side filtering of lexicon record collections to include in event handling. Filtering happens client-side. Defaults to an empty array.
5757+ * @property excludeIdentity - Excludes identity events from handling. Defaults to false.
5858+ * @property excludeAccount - Excludes account events from handling. Defaults to false.
5959+ * @property excludeCommit - Excludes commit events from handling. Defaults to false.
6060+ * @property excludeSync - Excludes repo sync events from handling. Defaults to false.
6161+ */
4562export type FirehoseOptions = WebSocketOptions & {
4663 idResolver: IdResolver;
4764···6481 excludeSync?: boolean;
6582};
66838484+/**
8585+ * The firehose class will spin up a websocket connection to
8686+ * com.atproto.sync.subscribeRepos on a given repo host
8787+ * (by default the Relay run by Bluesky).
8888+ * Each event will be parsed, authenticated, and then passed on to the
8989+ * supplied handleEvent which can handle indexing logic.
9090+ * On Commit events, the firehose will verify signatures and repo proofs
9191+ * to ensure that the event is authentic. This can be disabled with the
9292+ * unauthenticatedCommits flag. Similarly on Identity events, the firehose
9393+ * will fetch the latest DID document for the repo and do bidirectional
9494+ * verification on the associated handle. This can be disabled with the
9595+ * unauthenticatedHandles flag.
9696+ *
9797+ * Events of a certain type can be excluded using the
9898+ * excludeIdentity/excludeAccount/excludeCommit flags.
9999+ *
100100+ * And repo writes can be filtered down to specific collections using
101101+ * filterCollections. By default, all events are parsed and passed
102102+ * through to the handler. Note that this filtered currently happens
103103+ * client-side, though it is likely we will introduce server-side
104104+ * methods for doing so in the future.
105105+ *
106106+ * When using the firehose class, events are processed serially.
107107+ * Each event must be finished being handled before the next one is parsed
108108+ * and authenticated.
109109+ *
110110+ * @example Simple indexing service
111111+ * ```typescript
112112+ * import { Firehose } from '@atproto/sync'
113113+ * import { IdResolver } from '@atproto/identity'
114114+ *
115115+ * const idResolver = new IdResolver()
116116+ * const firehose = new Firehose({
117117+ * idResolver,
118118+ * service: 'wss://bsky.network',
119119+ * handleEvt: async (evt) => {
120120+ * if (evt.event === 'identity') {
121121+ * // ...
122122+ * } else if (evt.event === 'account') {
123123+ * // ...
124124+ * } else if (evt.event === 'create') {
125125+ * // ...
126126+ * } else if (evt.event === 'update') {
127127+ * // ...
128128+ * } else if (evt.event === 'delete') {
129129+ * // ...
130130+ * }
131131+ * },
132132+ * onError: (err) => {
133133+ * console.error(err)
134134+ * },
135135+ * filterCollections: ['com.myexample.app'],
136136+ * })
137137+ * firehose.start()
138138+ *
139139+ * // on service shutdown
140140+ * await firehose.destroy()
141141+ * ```
142142+ *
143143+ * For more robust indexing pipelines, it's recommended to use the
144144+ * supplied MemoryRunner class. This provides an in-memory partitioned
145145+ * queue. As events from a given repo must be processed in order, this
146146+ * allows events to be processed concurrently while still processing
147147+ * events from any given repo serially.
148148+ *
149149+ * The MemoryRunner also tracks an internal cursor based on the last
150150+ * finished consecutive work. This ensures that no events are dropped,
151151+ * although it does mean that some events may occassionally be replayed
152152+ * (if the websocket drops and reconnects) and therefore it's recommended
153153+ * that any indexing logic is idempotent. An optional setCursor parameter
154154+ * may be supplied to the MemoryRunner which can be used to persistently
155155+ * store the most recently processed cursor.
156156+ *
157157+ * @example Indexing with MemoryRunner
158158+ * ```typescript
159159+ * import { Firehose, MemoryRunner } from '@atproto/sync'
160160+ * import { IdResolver } from '@atproto/identity'
161161+ *
162162+ * const idResolver = new IdResolver()
163163+ * const runner = new MemoryRunner({
164164+ * setCursor: (cursor) => {
165165+ * // persist cursor
166166+ * },
167167+ * })
168168+ * const firehose = new Firehose({
169169+ * idResolver,
170170+ * runner,
171171+ * service: 'wss://bsky.network',
172172+ * handleEvt: async (evt) => {
173173+ * // ...
174174+ * },
175175+ * onError: (err) => {
176176+ * console.error(err)
177177+ * },
178178+ * })
179179+ * firehose.start()
180180+ *
181181+ * // on service shutdown
182182+ * await firehose.destroy()
183183+ * await runner.destroy()
184184+ * ```
185185+ * @property service - The service URL for the firehose.
186186+ * @property runner - The runner for the firehose.
187187+ * @property idResolver - The ID resolver for the firehose.
188188+ * @property opts - The options for the firehose.
189189+ */
67190export class Firehose {
68191 private sub: Subscription<RepoEvent>;
69192 private abortController: AbortController;
···379502 return ["takendown", "suspended", "deleted", "deactivated"].includes(str);
380503};
381504505505+/**
506506+ * An error in validating/authenticating an event from the firehose.
507507+ */
382508export class FirehoseValidationError extends Error {
383509 constructor(
384510 err: unknown,
···388514 }
389515}
390516517517+/**
518518+ * An error in parsing an event from the firehose.
519519+ */
391520export class FirehoseParseError extends Error {
392521 constructor(
393522 err: unknown,
···397526 }
398527}
399528529529+/**
530530+ * An error in the subscription to the firehose.
531531+ */
400532export class FirehoseSubscriptionError extends Error {
401533 constructor(err: unknown) {
402534 super("error on firehose subscription", { cause: err });
403535 }
404536}
405537538538+/**
539539+ * An error in your firehose event handler logic.
540540+ */
406541export class FirehoseHandlerError extends Error {
407542 constructor(
408543 err: unknown,
+3
sync/firehose/lexicons.ts
···159159 [k: string]: unknown;
160160}
161161162162+/** Determines if an event is a RepoOp */
162163export function isRepoOp(v: unknown): v is RepoOp {
163164 return (
164165 isObj(v) &&
···167168 );
168169}
169170171171+/** Lexicon type for com.atproto.sync.subscribeRepos */
170172export const ComAtprotoSyncSubscribeRepos: LexiconDoc = {
171173 lexicon: 1,
172174 id: "com.atproto.sync.subscribeRepos",
···438440439441const lexicons = new Lexicons([ComAtprotoSyncSubscribeRepos]);
440442443443+/** Validates a repo event */
441444export const isValidRepoEvent = (evt: unknown) => {
442445 return lexicons.assertValidXrpcMessage<RepoEvent>(
443446 "com.atproto.sync.subscribeRepos",
+69
sync/mod.ts
···11+/**
22+ * # AT Protocol Sync Tool
33+ *
44+ * This module provides tools for syncing data from AT Protocol.
55+ * Currently, it supports firehose (relay) subscriptions.
66+ *
77+ * The firehose class will spin up a websocket connection to
88+ * com.atproto.sync.subscribeRepos on a given repo host
99+ * (by default the Relay run by Bluesky).
1010+ * Each event will be parsed, authenticated, and then passed on to the
1111+ * supplied handleEvt which can handle indexing.
1212+ * On Commit events, the firehose will verify signatures and repo proofs
1313+ * to ensure that the event is authentic. This can be disabled with the
1414+ * unauthenticatedCommits flag. Similarly on Identity events, the firehose
1515+ * will fetch the latest DID document for the repo and do bidirectional
1616+ * verification on the associated handle. This can be disabled with the
1717+ * unauthenticatedHandles flag.
1818+ *
1919+ * Events of a certain type can be excluded using the
2020+ * excludeIdentity/excludeAccount/excludeCommit flags.
2121+ *
2222+ * And repo writes can be filtered down to specific collections using
2323+ * filterCollections. By default, all events are parsed and passed
2424+ * through to the handler. Note that this filtered currently happens
2525+ * client-side, though it is likely we will introduce server-side
2626+ * methods for doing so in the future.
2727+ *
2828+ * Non-fatal errors that are encountered will be passed to the required
2929+ * onError handler. In most cases, these can just be logged.
3030+ *
3131+ * When using the firehose class, events are processed serially.
3232+ * Each event must be finished being handled before the next one is parsed
3333+ * and authenticated.
3434+ *
3535+ * @example Simple indexing service
3636+ * ```typescript
3737+ * import { Firehose } from '@atproto/sync'
3838+ * import { IdResolver } from '@atproto/identity'
3939+ *
4040+ * const idResolver = new IdResolver()
4141+ * const firehose = new Firehose({
4242+ * idResolver,
4343+ * service: 'wss://bsky.network',
4444+ * handleEvt: async (evt) => {
4545+ * if (evt.event === 'identity') {
4646+ * // ...
4747+ * } else if (evt.event === 'account') {
4848+ * // ...
4949+ * } else if (evt.event === 'create') {
5050+ * // ...
5151+ * } else if (evt.event === 'update') {
5252+ * // ...
5353+ * } else if (evt.event === 'delete') {
5454+ * // ...
5555+ * }
5656+ * },
5757+ * onError: (err) => {
5858+ * console.error(err)
5959+ * },
6060+ * filterCollections: ['com.myexample.app'],
6161+ * })
6262+ * firehose.start()
6363+ *
6464+ * // on service shutdown
6565+ * await firehose.destroy()
6666+ * ```
6767+ *
6868+ * @module
6969+ */
170export * from "./runner/index.ts";
271export * from "./firehose/index.ts";
372export * from "./events.ts";
+3-2
sync/runner/memory-runner.ts
···99 setCursorInterval?: number; // milliseconds between persisted cursor saves (throttling)
1010};
11111212-// A queue with arbitrarily many partitions, each processing work sequentially.
1313-// Partitions are created lazily and taken out of memory when they go idle.
1212+/** A queue with arbitrarily many partitions, each processing work sequentially.
1313+ * Partitions are created lazily and taken out of memory when they go idle.
1414+ */
1415export class MemoryRunner implements EventRunner {
1516 consecutive: ConsecutiveList<number> = new ConsecutiveList<number>();
1617 mainQueue: PQueue;
···11-// Human-readable constraints:
22-// - valid W3C DID (https://www.w3.org/TR/did-core/#did-syntax)
33-// - entire URI is ASCII: [a-zA-Z0-9._:%-]
44-// - always starts "did:" (lower-case)
55-// - method name is one or more lower-case letters, followed by ":"
66-// - remaining identifier can have any of the above chars, but can not end in ":"
77-// - it seems that a bunch of ":" can be included, and don't need spaces between
88-// - "%" is used only for "percent encoding" and must be followed by two hex characters (and thus can't end in "%")
99-// - query ("?") and fragment ("#") stuff is defined for "DID URIs", but not as part of identifier itself
1010-// - "The current specification does not take a position on the maximum length of a DID"
1111-// - in current atproto, only allowing did:plc and did:web. But not *forcing* this at lexicon layer
1212-// - hard length limit of 8KBytes
1313-// - not going to validate "percent encoding" here
11+/** Human-readable constraints:
22+ * - valid W3C DID (https://www.w3.org/TR/did-core/#did-syntax)
33+ * - entire URI is ASCII: [a-zA-Z0-9._:%-]
44+ * - always starts "did:" (lower-case)
55+ * - method name is one or more lower-case letters, followed by ":"
66+ * - remaining identifier can have any of the above chars, but can not end in ":"
77+ * - it seems that a bunch of ":" can be included, and don't need spaces between
88+ * - "%" is used only for "percent encoding" and must be followed by two hex characters (and thus can't end in "%")
99+ * - query ("?") and fragment ("#") stuff is defined for "DID URIs", but not as part of identifier itself
1010+ * - "The current specification does not take a position on the maximum length of a DID"
1111+ * - in current atproto, only allowing did:plc and did:web. But not *forcing* this at lexicon layer
1212+ * - hard length limit of 8KBytes
1313+ * - not going to validate "percent encoding" here
1414+ * @param did - DID to validate
1515+ * @throws {InvalidDidError} if the DID is invalid
1616+ */
1417export const ensureValidDid = (did: string): void => {
1518 if (!did.startsWith("did:")) {
1619 throw new InvalidDidError("DID requires 'did:' prefix");
···4346 }
4447};
45484949+/**
5050+ * Simple regex version of {@linkcode ensureValidDid} constraints
5151+ * @param did - DID to validate
5252+ * @throws {InvalidDidError} - if the DID is invalid
5353+ */
4654export const ensureValidDidRegex = (did: string): void => {
4747- // simple regex to enforce most constraints via just regex and length.
4848- // hand wrote this regex based on above constraints
4955 if (!/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/.test(did)) {
5056 throw new InvalidDidError("DID didn't validate via regex");
5157 }
+10-9
syntax/handle.ts
···11export const INVALID_HANDLE = "handle.invalid";
2233-// Currently these are registration-time restrictions, not protocol-level
44-// restrictions. We have a couple accounts in the wild that we need to clean up
55-// before hard-disallow.
66-// See also: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains
33+/** Registration-time restrictions, not protocol-level restrictions.
44+ * `.test` is allowed but only should be used in testing and development.
55+ * @see {https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains}
66+ */
77export const DISALLOWED_TLDS = [
88 ".local",
99 ".arpa",
···1414 ".alt",
1515 // policy could concievably change on ".onion" some day
1616 ".onion",
1717- // NOTE: .test is allowed in testing and devopment. In practical terms
1818- // "should" "never" actually resolve and get registered in production
1917];
20182119/**
···143141144142/**
145143 * Thrown when a handle is invalid.
144144+ * Caused by invalid characters (only ASCII letters, digits, dashes, periods are allowed),
145145+ * length longer than 253 characters, or one of the {@linkcode DISALLOWED_TLDS} used.
146146 */
147147export class InvalidHandleError extends Error {}
148148-/** @deprecated Never used */
148148+149149+/** @deprecated Use {@linkcode InvalidHandleError} */
149150export class ReservedHandleError extends Error {}
150150-/** @deprecated Never used */
151151+/** @deprecated Use {@linkcode InvalidHandleError} */
151152export class UnsupportedDomainError extends Error {}
152152-/** @deprecated Never used */
153153+/** @deprecated Use {@linkcode InvalidHandleError} */
153154export class DisallowedDomainError extends Error {}
+10
syntax/recordkey.ts
···11+/**
22+ * Validates a record key (rkey)
33+ * @param rkey - Record key to validate
44+ * @throws {InvalidRecordKeyError} if the record key is invalid
55+ */
16export const ensureValidRecordKey = (rkey: string): void => {
27 if (rkey.length > 512 || rkey.length < 1) {
38 throw new InvalidRecordKeyError("record key must be 1 to 512 characters");
···1116 }
1217};
13181919+/**
2020+ * Validates a record key (rkey) to a boolean
2121+ * @param rkey - Record key to validate
2222+ * @returns true if the record key is valid, false otherwise
2323+ */
1424export const isValidRecordKey = (rkey: string): boolean => {
1525 try {
1626 ensureValidRecordKey(rkey);
···55export type HeadersMap = Record<string, string | undefined>;
6677export type {
88- /** @deprecated not to be confused with the WHATWG Headers constructor */
88+ /**
99+ * @deprecated not to be confused with the WHATWG Headers constructor.
1010+ * Use {@linkcode HeadersMap} instead.
1111+ */
912 HeadersMap as Headers,
1013};
1114···7174 return ResponseType[httpResponseCodeToEnum(status)];
7275}
73767777+/**
7878+ * Error messages corresponding to XRPC error codes.
7979+ */
7480export const ResponseTypeStrings: Record<ResponseType, string> = {
7581 [ResponseType.Unknown]: "Unknown",
7682 [ResponseType.InvalidResponse]: "Invalid Response",
···94100 return ResponseTypeStrings[httpResponseCodeToEnum(status)];
95101}
96102103103+/**
104104+ * Response type of a successful XRPC request.
105105+ */
97106export class XRPCResponse {
98107 success = true;
99108···103112 ) {}
104113}
105114115115+/**
116116+ * Response type of a failed XRPC request with details of the error.
117117+ */
106118export class XRPCError extends Error {
107119 success = false;
108120···166178 }
167179}
168180181181+/**
182182+ * Error for an invalid response from an XRPC request.
183183+ * Caused by a validation error with the lexicon schema
184184+ * matching the NSID of the endpoint.
185185+ */
169186export class XRPCInvalidResponseError extends XRPCError {
170187 constructor(
171188 public lexiconNsid: string,