UNPKG

@atproto/did

Version:

DID resolution and verification library

236 lines (203 loc) 6.52 kB
import { z } from 'zod' import { DidDocument, DidService } from './did-document.js' import { DidError, InvalidDidError } from './did-error.js' import { DidRefAbsolute, isDidRefAbsolute } from './did-ref.js' import { Did } from './did.js' import { canParse } from './lib/uri.js' import { DID_PLC_PREFIX, DID_WEB_PREFIX, assertDidPlc, assertDidWeb, isDidPlc, isDidWeb, } from './methods.js' import { Identifier, matchesIdentifier } from './utils.js' // This file contains atproto-specific DID validation utilities. export type AtprotoIdentityDidMethods = 'plc' | 'web' export type AtprotoDid = Did<AtprotoIdentityDidMethods> export type AtprotoDidDocument = DidDocument<AtprotoIdentityDidMethods> export const atprotoDidSchema = z .string() .refine(isAtprotoDid, `Atproto only allows "plc" and "web" DID methods`) export function isAtprotoDid(input: unknown): input is AtprotoDid { return isDidPlc(input) || isAtprotoDidWeb(input) } export function asAtprotoDid<T>(input: T) { assertAtprotoDid(input) return input } export function assertAtprotoDid(input: unknown): asserts input is AtprotoDid { if (typeof input !== 'string') { throw new InvalidDidError(typeof input, `DID must be a string`) } else if (input.startsWith(DID_PLC_PREFIX)) { assertDidPlc(input) } else if (input.startsWith(DID_WEB_PREFIX)) { assertAtprotoDidWeb(input) } else { throw new InvalidDidError( input, `Atproto only allows "plc" and "web" DID methods`, ) } } export function assertAtprotoDidWeb( input: unknown, ): asserts input is Did<'web'> { assertDidWeb(input) if (isDidWebWithPath(input)) { throw new InvalidDidError( input, `Atproto does not allow path components in Web DIDs`, ) } if (isDidWebWithHttpsPort(input)) { throw new InvalidDidError( input, `Atproto does not allow port numbers in Web DIDs, except for localhost`, ) } } /** * @see {@link https://atproto.com/specs/did#blessed-did-methods} */ export function isAtprotoDidWeb(input: unknown): input is Did<'web'> { if (!isDidWeb(input)) { return false } if (isDidWebWithPath(input)) { return false } if (isDidWebWithHttpsPort(input)) { return false } return true } function isDidWebWithPath(did: Did<'web'>): boolean { return did.includes(':', DID_WEB_PREFIX.length) } function isLocalhostDid(did: Did<'web'>): boolean { return ( did === 'did:web:localhost' || did.startsWith('did:web:localhost:') || did.startsWith('did:web:localhost%3A') ) } function isDidWebWithHttpsPort(did: Did<'web'>): boolean { if (isLocalhostDid(did)) return false const pathIdx = did.indexOf(':', DID_WEB_PREFIX.length) const hasPort = pathIdx === -1 ? // No path component, check if there's a port separator anywhere after // the "did:web:" prefix did.includes('%3A', DID_WEB_PREFIX.length) : // There is a path component; if there is an encoded colon *before* it, // then there is a port number did.lastIndexOf('%3A', pathIdx) !== -1 return hasPort } export type AtprotoData< M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods, > = { did: Did<M> aka?: string key?: AtprotoVerificationMethod<M> pds?: AtprotoPersonalDataServerService<M> } export function extractAtprotoData<M extends AtprotoIdentityDidMethods>( document: DidDocument<M>, ): AtprotoData<M> { return { did: document.id, aka: document.alsoKnownAs?.find(isAtprotoAka)?.slice(5), key: document.verificationMethod?.find( isAtprotoVerificationMethod<M>, document, ), pds: document.service?.find( isAtprotoPersonalDataServerService<M>, document, ), } } export function extractPdsUrl(document: AtprotoDidDocument): URL { const service = document.service?.find( isAtprotoPersonalDataServerService, document, ) if (!service) { throw new DidError( document.id, `Document ${document.id} does not contain a (valid) #atproto_pds service URL`, 'did-service-not-found', ) } return new URL(service.serviceEndpoint) } export type AtprotoAka = `at://${string}` export function isAtprotoAka(value: string): value is AtprotoAka { return value.startsWith('at://') } export type AtprotoPersonalDataServerService< M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods, > = DidService & { id: Identifier<Did<M>, 'atproto_pds'> type: 'AtprotoPersonalDataServer' serviceEndpoint: string } export function isAtprotoPersonalDataServerService< M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods, >( this: DidDocument<M>, service: null | undefined | DidService, ): service is AtprotoPersonalDataServerService<M> { return ( service?.type === 'AtprotoPersonalDataServer' && typeof service.serviceEndpoint === 'string' && canParse(service.serviceEndpoint) && matchesIdentifier(this.id, 'atproto_pds', service.id) ) } export const ATPROTO_VERIFICATION_METHOD_TYPES = Object.freeze([ 'EcdsaSecp256r1VerificationKey2019', 'EcdsaSecp256k1VerificationKey2019', 'Multikey', ] as const) export type SupportedAtprotoVerificationMethodType = (typeof ATPROTO_VERIFICATION_METHOD_TYPES)[number] type VerificationMethod = NonNullable<DidDocument['verificationMethod']>[number] export type AtprotoVerificationMethod< M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods, > = Extract<VerificationMethod, object> & { id: Identifier<Did<M>, 'atproto'> type: SupportedAtprotoVerificationMethodType publicKeyMultibase: string } export function isAtprotoVerificationMethod< M extends AtprotoIdentityDidMethods = AtprotoIdentityDidMethods, >( this: DidDocument<M>, method: | null | undefined | NonNullable<DidDocument<M>['verificationMethod']>[number], ): method is AtprotoVerificationMethod<M> { return ( typeof method === 'object' && typeof method?.publicKeyMultibase === 'string' && (ATPROTO_VERIFICATION_METHOD_TYPES as readonly unknown[]).includes( method.type, ) && matchesIdentifier(this.id, 'atproto', method.id) ) } /** * An atproto-constrained absolute DID reference: `${AtprotoDid}#${fragment}`. */ export type AtprotoDidRefAbsolute = DidRefAbsolute<AtprotoIdentityDidMethods> export function isAtprotoDidRefAbsolute( value: unknown, ): value is AtprotoDidRefAbsolute { if (!isDidRefAbsolute(value)) return false return isAtprotoDid(value.slice(0, value.indexOf('#'))) }