@ceramicnetwork/3id-did-resolver
Version:
DID Resolver for the 3ID method
175 lines • 6.55 kB
JavaScript
import { TileDocument } from '@ceramicnetwork/stream-tile';
import { LegacyResolver } from './legacyResolver.js';
import * as u8a from 'uint8arrays';
import { StreamID, CommitID } from '@ceramicnetwork/streamid';
import { CID } from 'multiformats/cid';
import { errorRepresentation, withResolutionError } from './error-representation.js';
import { EventType } from '@ceramicnetwork/common';
const DID_LD_JSON = 'application/did+ld+json';
const DID_JSON = 'application/did+json';
function isLegacyDid(didId) {
try {
CID.parse(didId);
return true;
}
catch (e) {
return false;
}
}
const formatTime = (timestamp) => {
return new Date(timestamp * 1000).toISOString().split('.')[0] + 'Z';
};
export function wrapDocument(content, did) {
if (!(content && content.publicKeys))
return null;
const startDoc = {
id: did,
verificationMethod: [],
authentication: [],
keyAgreement: [],
};
return Object.entries(content.publicKeys).reduce((diddoc, [keyName, keyValue]) => {
const keyBuf = u8a.fromString(keyValue.slice(1), 'base58btc');
const entry = {
id: `${did}#${keyName}`,
type: '',
controller: did,
publicKeyBase58: u8a.toString(keyBuf.slice(2), 'base58btc'),
};
if (keyBuf[0] === 0xe7) {
entry.type = 'EcdsaSecp256k1Signature2019';
diddoc.verificationMethod.push(entry);
diddoc.authentication.push(entry);
}
else if (keyBuf[0] === 0xec) {
entry.type = 'X25519KeyAgreementKey2019';
diddoc.verificationMethod.push(entry);
diddoc.keyAgreement.push(entry);
}
return diddoc;
}, startDoc);
}
function lastAnchorOrGenesisEntry(log) {
for (let index = log.length - 1; index >= 0; index--) {
const entry = log[index];
if (entry.type === EventType.TIME) {
return entry;
}
}
return log[0];
}
function extractMetadata(requestedVersionState, latestVersionState) {
const metadata = {};
const { timestamp: updated, cid: versionId } = lastAnchorOrGenesisEntry(requestedVersionState.log);
const { timestamp: nextUpdate, cid: nextVersionId } = latestVersionState.log.find(({ timestamp }) => {
return timestamp && ((updated && timestamp > updated) || !updated);
}) || {};
if (updated) {
metadata.updated = formatTime(updated);
}
if (nextUpdate) {
metadata.nextUpdate = formatTime(nextUpdate);
}
if (versionId) {
metadata.versionId = requestedVersionState.log.length === 1 ? '0' : versionId?.toString();
}
if (nextVersionId) {
metadata.nextVersionId = nextVersionId.toString();
}
return metadata;
}
function getVersionInfo(query = '') {
const asSearchParams = new URLSearchParams(query);
const versionId = asSearchParams.get('versionId') || asSearchParams.get('version-id') || undefined;
const versionTime = asSearchParams.get('versionTime');
return {
commit: versionId,
timestamp: versionTime ? Math.floor(new Date(versionTime).getTime() / 1000) : undefined,
};
}
async function legacyResolve(ceramic, didId, verNfo) {
const legacyPublicKeys = await LegacyResolver(didId);
const metadata = { controllers: [legacyPublicKeys.keyDid], family: '3id', deterministic: true };
const streamid = (await TileDocument.create(ceramic, null, metadata, { anchor: false, publish: false })).id;
const didResult = await resolve(ceramic, streamid.toString(), verNfo, didId);
if (didResult.didDocument === null) {
didResult.didDocument = wrapDocument(legacyPublicKeys, `did:3:${didId}`);
}
return didResult;
}
async function resolve(ceramic, didId, verNfo, v03ID) {
const streamId = StreamID.fromString(didId);
let commitId;
const query = [{ streamId }];
if (verNfo.commit) {
commitId = CommitID.make(streamId, verNfo.commit);
query.push({ streamId: commitId });
}
else if (verNfo.timestamp) {
query.push({
streamId,
opts: { atTime: verNfo.timestamp },
});
}
const resp = await ceramic.multiQuery(query);
const latest = resp[didId];
if (!latest) {
throw new Error(`Failed to properly resolve 3ID, stream ${didId} not found in response.`);
}
const latestVersionState = latest.state;
const commitIdStr = commitId?.toString() || Object.keys(resp).find((k) => k !== didId);
const requestedVersionState = (commitIdStr && resp[commitIdStr]?.state) || latestVersionState;
const metadata = extractMetadata(requestedVersionState, latestVersionState);
let tile;
if (commitIdStr) {
const found = resp[commitIdStr];
if (found) {
tile = found;
}
else {
throw new Error(`No resolution for commit ${commitIdStr}`);
}
}
else {
tile = latest;
}
const content = tile.state.content;
const document = wrapDocument(content, `did:3:${v03ID || didId}`);
return {
didResolutionMetadata: { contentType: DID_JSON },
didDocument: document,
didDocumentMetadata: metadata,
};
}
export function getResolver(ceramic) {
return {
'3': (_did, parsed, _resolver, options) => {
return withResolutionError(async () => {
const contentType = options.accept || DID_JSON;
const verNfo = getVersionInfo(parsed.query);
const id = parsed.id;
let resolution;
if (isLegacyDid(id)) {
resolution = await legacyResolve(ceramic, id, verNfo);
}
else {
resolution = await resolve(ceramic, id, verNfo);
}
switch (contentType) {
case DID_JSON:
return resolution;
case DID_LD_JSON: {
if (resolution.didDocument) {
resolution.didDocument['@context'] = 'https://www.w3.org/ns/did/v1';
}
resolution.didResolutionMetadata.contentType = DID_LD_JSON;
return resolution;
}
default:
return errorRepresentation({ error: 'representationNotSupported' });
}
});
},
};
}
//# sourceMappingURL=index.js.map