Documentation Index
Fetch the complete documentation index at: https://mintlify.com/bluesky-social/atproto/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The @atproto/syntax package provides validation and parsing utilities for AT Protocol identifier formats including DIDs, handles, NSIDs, AT URIs, TIDs, and record keys.
Installation
npm install @atproto/syntax
DID (Decentralized Identifier)
Validation for W3C DID identifiers.
Validation Functions
import { isValidDid, ensureValidDid } from '@atproto/syntax'
// Type guard - returns boolean
if (isValidDid('did:plc:abc123xyz')) {
console.log('Valid DID')
}
// Assertion - throws on invalid
try {
ensureValidDid('did:plc:abc123xyz')
console.log('DID is valid')
} catch (err) {
console.error('Invalid DID:', err.message)
}
function isValidDid<I extends string>(input: I): input is I & DidString
Type guard that returns true if the input is a valid DID.
function ensureValidDid<I extends string>(input: I): asserts input is I & DidString
Asserts that input is a valid DID. Throws InvalidDidError if invalid.
type DidString<M extends string = string> = `did:${M}:${string}`
Constraints:
- Must start with
did:
- Method name must be lowercase letters
- Method-specific identifier can contain:
a-zA-Z0-9._:%-
- Cannot end with
: or %
- Maximum length: 2048 characters
Examples:
'did:plc:ewvi7nxzyoun6zhxrhs64oiz' // ✓ valid
'did:web:example.com' // ✓ valid
'DID:plc:abc' // ✗ invalid (uppercase)
'did:plc:' // ✗ invalid (ends with :)
Handle
Validation for domain-based handles.
Validation Functions
import {
isValidHandle,
ensureValidHandle,
normalizeHandle,
isValidTld
} from '@atproto/syntax'
// Validate handle
if (isValidHandle('alice.bsky.social')) {
console.log('Valid handle')
}
// Normalize (lowercase)
const normalized = normalizeHandle('Alice.BSKY.Social')
console.log(normalized) // 'alice.bsky.social'
// Validate TLD
if (!isValidTld('alice.local')) {
console.log('Disallowed TLD')
}
function isValidHandle<I extends string>(input: I): input is I & HandleString
Type guard that returns true if the input is a valid handle.
function ensureValidHandle<I extends string>(input: I): asserts input is I & HandleString
Asserts that input is a valid handle. Throws InvalidHandleError if invalid.
function normalizeHandle(handle: string): string
Converts handle to lowercase (handles are case-insensitive).
function isValidTld(handle: string): boolean
Checks if handle’s TLD is allowed (not .local, .internal, etc.).
type HandleString = `${string}.${string}`
Constraints:
- Valid domain name format (RFC-1035, RFC-3696)
- Segments contain only:
a-zA-Z0-9-
- Segments cannot start or end with hyphen
- Each segment: 1-63 characters
- Total length: max 253 characters
- At least 2 segments (e.g.,
user.domain)
- TLD must start with letter
- Case-insensitive
Disallowed TLDs:
.local, .arpa, .invalid, .localhost, .internal, .example, .alt, .onion
Examples:
'alice.bsky.social' // ✓ valid
'bob.com' // ✓ valid
'charlie' // ✗ invalid (no TLD)
'alice.local' // ✗ invalid (disallowed TLD)
'-invalid.com' // ✗ invalid (starts with hyphen)
NSID (Namespaced Identifier)
Validation for Lexicon schema identifiers.
NSID Class
import { NSID } from '@atproto/syntax'
// Parse NSID
const nsid = NSID.parse('com.example.song')
console.log(nsid.authority) // 'example.com'
console.log(nsid.name) // 'song'
console.log(nsid.toString()) // 'com.example.song'
// Create from parts
const nsid2 = NSID.create('example.com', 'post')
console.log(nsid2.toString()) // 'com.example.post'
// Validate
if (NSID.isValid('com.atproto.repo.createRecord')) {
console.log('Valid NSID')
}
static parse(input: string): NSID
Parses an NSID string. Throws InvalidNsidError if invalid.
static create(authority: string, name: string): NSID
Creates an NSID from authority and name.Domain authority (e.g., ‘example.com’)
Method name (e.g., ‘post’)
static isValid(nsid: string): boolean
Returns true if the string is a valid NSID.
Properties:
authority: string - Reversed domain (e.g., ‘example.com’)
name: string - Method name
segments: readonly string[] - All segments
Validation Functions
import { isValidNsid, ensureValidNsid } from '@atproto/syntax'
if (isValidNsid('com.example.song')) {
console.log('Valid NSID')
}
ensureValidNsid('com.example.song') // throws if invalid
type NsidString = `${string}.${string}.${string}`
Constraints:
- Reversed domain notation + name
- Segments contain:
a-zA-Z0-9-
- Cannot start/end with hyphen
- First segment cannot start with digit
- Last segment (name) must be letters/digits only (no hyphens)
- Each segment: 1-63 characters
- Total length: max 317 characters (253 + 1 + 63)
- At least 3 segments
Examples:
'com.atproto.repo.createRecord' // ✓ valid
'com.example.song' // ✓ valid
'example.song' // ✗ invalid (only 2 segments)
'com.example.my-song' // ✗ invalid (hyphen in name)
AT URI
Validation and parsing for AT Protocol URIs.
AtUri Class
import { AtUri } from '@atproto/syntax'
// Parse AT URI
const uri = new AtUri('at://bob.com/app.bsky.feed.post/3jzfcijpj2z2a')
console.log(uri.protocol) // 'at:'
console.log(uri.host) // 'bob.com'
console.log(uri.hostname) // 'bob.com'
console.log(uri.collection) // 'app.bsky.feed.post'
console.log(uri.rkey) // '3jzfcijpj2z2a'
console.log(uri.pathname) // '/app.bsky.feed.post/3jzfcijpj2z2a'
// Get DID (if host is DID)
const uri2 = new AtUri('at://did:plc:abc/app.bsky.feed.post/123')
console.log(uri2.did) // 'did:plc:abc'
// Construct from parts
const uri3 = AtUri.make('alice.bsky.social', 'app.bsky.feed.post', '3k2akjd')
console.log(uri3.toString())
// 'at://alice.bsky.social/app.bsky.feed.post/3k2akjd'
// With query params
const uri4 = new AtUri('at://bob.com/collection/rkey?param=value')
console.log(uri4.searchParams.get('param')) // 'value'
constructor(uri: string, base?: string | AtUri)
Parses an AT URI. Optionally resolves relative URIs against a base.Base URI for resolving relative references
static make(handleOrDid: string, collection?: string, rkey?: string): AtUri
Constructs an AT URI from parts.
Properties:
protocol: string - Always 'at:'
origin: string - at://{host}
host: string - DID or handle
hostname: string - Alias for host
pathname: string - Path portion
collection: string - Collection NSID
rkey: string - Record key
hash: string - Fragment
searchParams: URLSearchParams - Query parameters
did: DidString - DID (throws if host is not DID)
Methods:
toString(): AtUriString - Converts to string
type AtUriString = `at://${string}`
Format:
at://authority/collection/rkey?query#fragment
Examples:
'at://bob.com/app.bsky.feed.post/123' // ✓ valid
'at://did:plc:abc/app.bsky.actor.profile/self' // ✓ valid
'at://alice.com' // ✓ valid (repo only)
'at://bob.com/app.bsky.feed.post/123?cursor=xyz' // ✓ valid (with query)
TID (Timestamp Identifier)
Validation for timestamp-based identifiers.
import { isValidTid, ensureValidTid } from '@atproto/syntax'
if (isValidTid('3jzfcijpj2z2a')) {
console.log('Valid TID')
}
ensureValidTid('3jzfcijpj2z2a') // throws if invalid
function isValidTid<I extends string>(input: I): input is I & TidString
Type guard for TID validation.
function ensureValidTid<I extends string>(input: I): asserts input is I & TidString
Asserts input is a valid TID. Throws InvalidTidError if invalid.
Constraints:
- Exactly 13 characters
- Base32-sortable encoding
- First character:
[234567abcdefghij]
- Remaining characters:
[234567abcdefghijklmnopqrstuvwxyz]
Examples:
'3jzfcijpj2z2a' // ✓ valid
'3k2akjd8fj3la' // ✓ valid
'1234567890123' // ✗ invalid (starts with 1)
'3jzfcijpj2z' // ✗ invalid (too short)
Record Key
Validation for repository record keys.
import { isValidRecordKey, ensureValidRecordKey } from '@atproto/syntax'
if (isValidRecordKey('3jzfcijpj2z2a')) {
console.log('Valid record key')
}
if (isValidRecordKey('self')) {
console.log('Valid literal key')
}
ensureValidRecordKey('my-record-123') // throws if invalid
function isValidRecordKey<I extends string>(input: I): input is I & RecordKeyString
Type guard for record key validation.
function ensureValidRecordKey<I extends string>(input: I): asserts input is I & RecordKeyString
Asserts input is a valid record key. Throws InvalidRecordKeyError if invalid.
type RecordKeyString = string
Constraints:
- Allowed characters:
a-zA-Z0-9_~.:-
- Length: 1-512 characters
- Cannot be
. or ..
Examples:
'3jzfcijpj2z2a' // ✓ valid (TID)
'self' // ✓ valid (literal)
'my-record' // ✓ valid
'2023-01-01' // ✓ valid
'.' // ✗ invalid
'..' // ✗ invalid
'invalid/key' // ✗ invalid (slash not allowed)
Datetime
Validation for ISO 8601 datetime strings.
import { isValidDatetime, ensureValidDatetime } from '@atproto/syntax'
const dt = new Date().toISOString()
if (isValidDatetime(dt)) {
console.log('Valid datetime')
}
ensureValidDatetime('2024-01-15T12:30:00.000Z') // throws if invalid
function isValidDatetime(input: string): boolean
Returns true if input is a valid ISO 8601 datetime.
function ensureValidDatetime(input: string): void
Asserts input is a valid ISO 8601 datetime. Throws if invalid.
Complete Example
import {
isValidDid,
isValidHandle,
NSID,
AtUri,
isValidTid,
isValidRecordKey
} from '@atproto/syntax'
// Validate identifiers
const did = 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'
const handle = 'alice.bsky.social'
if (isValidDid(did) && isValidHandle(handle)) {
console.log('Valid identifiers')
}
// Parse NSID
const nsid = NSID.parse('app.bsky.feed.post')
console.log(`Authority: ${nsid.authority}, Name: ${nsid.name}`)
// Parse AT URI
const uri = new AtUri(`at://${did}/${nsid.toString()}/3jzfcijpj2z2a`)
console.log('URI components:', {
host: uri.host,
collection: uri.collection,
rkey: uri.rkey
})
// Validate record key
const rkey = uri.rkey
if (isValidTid(rkey)) {
console.log('Record key is a TID')
} else if (isValidRecordKey(rkey)) {
console.log('Record key is valid')
}
// Construct new URI
const newUri = AtUri.make(
handle,
'app.bsky.actor.profile',
'self'
)
console.log('Profile URI:', newUri.toString())
// at://alice.bsky.social/app.bsky.actor.profile/self
// Work with query parameters
const queryUri = new AtUri(
`at://${handle}/app.bsky.feed.post?cursor=abc&limit=50`
)
console.log('Cursor:', queryUri.searchParams.get('cursor'))
console.log('Limit:', queryUri.searchParams.get('limit'))
Error Classes
All validation functions throw specific error types:
InvalidDidError
InvalidHandleError
InvalidNsidError
InvalidTidError
InvalidRecordKeyError
InvalidDatetimeError
import { InvalidDidError, ensureValidDid } from '@atproto/syntax'
try {
ensureValidDid('not-a-did')
} catch (err) {
if (err instanceof InvalidDidError) {
console.error('DID validation failed:', err.message)
}
}
Additional Resources