@atproto/did
Version:
DID resolution and verification library
236 lines (203 loc) • 6.52 kB
text/typescript
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('#')))
}