did-resolver
Version:
Resolve DID documents
153 lines • 5.69 kB
JavaScript
// Copyright 2018 Consensys AG
/**
* Represents the Verification Relationship between a DID subject and a Verification Method.
*
* @see {@link https://www.w3.org/TR/did-core/#verification-relationships}
*/
export const VerificationRelationship = {
authentication: 'authentication',
assertionMethod: 'assertionMethod',
keyAgreement: 'keyAgreement',
capabilityInvocation: 'capabilityInvocation',
capabilityDelegation: 'capabilityDelegation',
};
export function inMemoryCache() {
const cache = new Map();
return async (parsed, resolve, options) => {
if (options?.cache === false)
return await resolve();
const cached = cache.get(parsed.didUrl);
if (cached !== undefined)
return cached;
const result = await resolve();
if (result.didResolutionMetadata?.error !== 'notFound') {
cache.set(parsed.didUrl, result);
}
return result;
};
}
export function noCache(parsed, resolve) {
return resolve();
}
// RFC 3986 ABNF components (referenced by DID Core v1.0 §3.2)
const HEXDIG = '[0-9a-fA-F]'; // allows both lowercase and uppercase
const PCT_ENCODED = `(?:%${HEXDIG}{2})`; // pct-encoded = "%" HEXDIG HEXDIG
// DID Core v1.0 §3.1
const ID_CHAR = `(?:[a-zA-Z0-9._-]|${PCT_ENCODED})`; // idchar
const METHOD = '[a-z0-9]+'; // method-name = 1*(%x61-7A / DIGIT)
const METHOD_ID = `(?:${ID_CHAR}*:)*${ID_CHAR}+`; // method-specific-id
// DID Core v1.0 §3.2 tail via RFC 3986 §3.3
const UNRESERVED = '[a-zA-Z0-9._~-]';
const SUB_DELIMS = "[!$&'()*+,;=]";
const PCHAR = `(?:${UNRESERVED}|${PCT_ENCODED}|${SUB_DELIMS}|[:@])`; // pchar
const PATH_ABEMPTY = `(?:/${PCHAR}*)*`; // path-abempty
const QUERY_CHARS = `(?:${PCHAR}|[/?])*`; // query
const FRAGMENT_CHARS = `(?:${PCHAR}|[/?])*`; // fragment
const DID_URL_MATCHER = new RegExp(`^did:(${METHOD}):(${METHOD_ID})(${PATH_ABEMPTY})(?:\\?(${QUERY_CHARS}))?(?:#(${FRAGMENT_CHARS}))?$`);
/**
* Parses a DID URL strictly according to the DID Core v1.0 specification ABNF
* (§3.1 DID Syntax, §3.2 DID URL Syntax). Unlike {@link parse}, this method
* does not accept legacy DID parameters (`;key=value`) and enforces the
* RFC 3986 character sets for path, query, and fragment components.
*
* @param didUrl - the DID URL string to be parsed
* @returns a ParsedDID object, or null if the input does not conform to the spec
*/
export function parse(didUrl) {
if (!didUrl)
return null;
const m = didUrl.match(DID_URL_MATCHER);
if (!m)
return null;
const parsed = {
did: `did:${m[1]}:${m[2]}`,
method: m[1],
id: m[2],
didUrl,
};
// path-abempty always matches (possibly empty); only attach a real path.
if (m[3])
parsed.path = m[3];
// query/fragment groups are `undefined` when absent, `''` when present but empty
// (`did:x:y?` / `did:x:y#`), preserve the distinction.
if (m[4] !== undefined)
parsed.query = m[4];
if (m[5] !== undefined)
parsed.fragment = m[5];
// params are intentionally omitted (matrix parameters not in DID Core v1.0 spec)
return parsed;
}
const EMPTY_RESULT = {
didResolutionMetadata: {},
didDocument: null,
didDocumentMetadata: {},
};
export function wrapLegacyResolver(resolve) {
return async (did, parsed, resolver) => {
try {
const doc = await resolve(did, parsed, resolver);
return {
...EMPTY_RESULT,
didResolutionMetadata: { contentType: 'application/did+ld+json' },
didDocument: doc,
};
}
catch (e) {
return {
...EMPTY_RESULT,
didResolutionMetadata: {
error: 'notFound',
message: String(e), // This is not in spec, but may be helpful
},
};
}
};
}
/**
* This implementation of {@link Resolvable} bundles together multiple implementations of {@link DIDResolver} and
* presents a single function call to users.
*/
export class Resolver {
registry;
cache;
constructor(registry = {}, options = {}) {
this.registry = registry;
this.cache = options.cache === true ? inMemoryCache() : options.cache === false ? noCache : options.cache || noCache;
if (options.legacyResolvers) {
Object.keys(options.legacyResolvers).map((methodName) => {
if (!this.registry[methodName]) {
this.registry[methodName] = wrapLegacyResolver(options.legacyResolvers[methodName]);
}
});
}
}
async resolve(didUrl, options = {}) {
let parsed = parse(didUrl);
if (parsed === null) {
return {
...EMPTY_RESULT,
didResolutionMetadata: { error: 'invalidDid' },
};
}
const resolver = this.registry[parsed.method];
if (!resolver) {
return {
...EMPTY_RESULT,
didResolutionMetadata: { error: 'unsupportedDidMethod' },
};
}
// Use resolver's method-specific parser if available for additional validation
if (resolver.parser) {
const refined = resolver.parser(parsed);
if (refined === null) {
return {
...EMPTY_RESULT,
didResolutionMetadata: { error: 'invalidDid' },
};
}
parsed = refined;
}
return this.cache(parsed, () => resolver(parsed.did, parsed, this, options), options);
}
}
//# sourceMappingURL=index.js.map