commit
03caefa0fb
@ -0,0 +1 @@
|
|||||||
|
/node_modules
|
@ -0,0 +1,13 @@
|
|||||||
|
Copyright (c) 2021 Charles E. Lehner
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
@ -0,0 +1,16 @@
|
|||||||
|
# SSB DID Resolver
|
||||||
|
|
||||||
|
[DID Resolver][] implementation for [did:ssb][].
|
||||||
|
|
||||||
|
## Test vector generator
|
||||||
|
|
||||||
|
`gen-test.js` generates implementation files for [DID Test Suite][].
|
||||||
|
To use it, run it on the command-line, passing the path to the `implementations` directory of
|
||||||
|
`did-test-suite`. That will resolve some SSB DIDs and write some files to the directory.
|
||||||
|
```
|
||||||
|
$ node gen-test ../did-test-suite/packages/did-core-test-server/suites/implementations
|
||||||
|
```
|
||||||
|
|
||||||
|
[DID Resolver]: https://www.w3.org/TR/did-core/#dfn-did-resolvers
|
||||||
|
[did:ssb]: %dvDyHqLfb0SYOe8lxlUA0ZQquydaSiG4eA/HXrVbVYw=.sha256
|
||||||
|
[DID Test Suite]: https://github.com/w3c/did-test-suite
|
@ -0,0 +1,257 @@
|
|||||||
|
// did:ssb test vector generator for DID Test Suite
|
||||||
|
|
||||||
|
const DIDSSBResolver = require('./resolver');
|
||||||
|
|
||||||
|
const dest = process.argv[2];
|
||||||
|
if (!dest) throw 'Usage: node gen-test.js <destination>';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const didJsonFilename = path.join(dest, 'did-ssb.json');
|
||||||
|
const resolverJsonFilename = path.join(dest, 'resolver-ssb.json');
|
||||||
|
const dereferencerJsonFilename = path.join(dest, 'dereferencer-ssb.json');
|
||||||
|
|
||||||
|
const testCommon = {
|
||||||
|
didMethod: 'did:ssb',
|
||||||
|
implementation: 'ssb-did-resolver',
|
||||||
|
implementer: 'Secure Scuttlebutt Consortium',
|
||||||
|
};
|
||||||
|
|
||||||
|
const didMethodInputs = [
|
||||||
|
{
|
||||||
|
did: 'did:ssb:ed25519:f_6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU',
|
||||||
|
resolutionOptions: {},
|
||||||
|
representationResolutionOptions: {}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const resolverInputs = [
|
||||||
|
{
|
||||||
|
function: 'resolve',
|
||||||
|
did: 'did:ssb:ed25519:f_6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU',
|
||||||
|
resolutionOptions: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: 'resolveRepresentation',
|
||||||
|
did: 'did:ssb:ed25519:f_6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU',
|
||||||
|
resolutionOptions: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
function: 'resolve',
|
||||||
|
did: 'did:ssb:ed25519:6RpN4Ztw3jLwzQtHl8XpnnR58LWZTAjwq2vvfyx7zkc',
|
||||||
|
resolutionOptions: {}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const dereferencerInputs = [
|
||||||
|
{
|
||||||
|
didUrl: 'did:ssb:ed25519:f_6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU',
|
||||||
|
dereferenceOptions: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
didUrl: 'did:ssb:ed25519:f_6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU?versionId=%25Vlo6kAc%2BIbGGBhD2MUi2r3ULz%2FNAGBWwGb%2FEMa4w4FI%3D.sha256',
|
||||||
|
dereferenceOptions: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
didUrl: 'did:ssb:ed25519:f_6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU?versionTime=2021-07-24T03:38:46Z',
|
||||||
|
dereferenceOptions: {}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let didParameters = {
|
||||||
|
versionId: 'did:ssb:ed25519:f_6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU?versionId=%25Vlo6kAc%2BIbGGBhD2MUi2r3ULz%2FNAGBWwGb%2FEMa4w4FI%3D.sha256',
|
||||||
|
versionTime: 'did:ssb:ed25519:f_6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU?versionTime=2021-07-24T03:38:46Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
const ssbClient = require('ssb-client');
|
||||||
|
let closeSbot, resolver;
|
||||||
|
ssbClient(function (err, sbot, config) {
|
||||||
|
if (err) throw err;
|
||||||
|
closeSbot = sbot.close.bind(sbot);
|
||||||
|
resolver = new DIDSSBResolver(sbot, config);
|
||||||
|
generateReports(done)
|
||||||
|
});
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
closeSbot(function (err) {
|
||||||
|
if (err && err !== true) throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateReports(next) {
|
||||||
|
let reportWaiting = 3;
|
||||||
|
generateMethodReport(reportNext);
|
||||||
|
generateResolverReport(reportNext);
|
||||||
|
generateDereferencerReport(reportNext);
|
||||||
|
function reportNext() {
|
||||||
|
if (--reportWaiting) return;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateMethodReport(next) {
|
||||||
|
let contentTypes = {};
|
||||||
|
let supportedContentTypes = [];
|
||||||
|
let dids = [];
|
||||||
|
let didMethodReport = {
|
||||||
|
...testCommon,
|
||||||
|
supportedContentTypes,
|
||||||
|
dids,
|
||||||
|
didParameters,
|
||||||
|
};
|
||||||
|
let methodWaiting = 0;
|
||||||
|
didMethodInputs.forEach(function (input) {
|
||||||
|
methodWaiting++;
|
||||||
|
const did = input.did;
|
||||||
|
const didReport = didMethodReport[did] = {};
|
||||||
|
dids.push(did);
|
||||||
|
let didWaiting = 2;
|
||||||
|
let resResult, resReprResult;
|
||||||
|
resolver.resolve(did, input.resolutionOptions, function (didResolutionMetadata, didDocument, didDocumentMetadata) {
|
||||||
|
if (didResolutionMetadata.error) throw didResolutionMetadata.error;
|
||||||
|
resResult = {didResolutionMetadata, didDocument, didDocumentMetadata};
|
||||||
|
didNext();
|
||||||
|
});
|
||||||
|
resolver.resolveRepresentation(did, input.representationResolutionOptions, function (didResolutionMetadata, didDocumentStream, didDocumentMetadata) {
|
||||||
|
if (didResolutionMetadata.error) throw didResolutionMetadata.error;
|
||||||
|
resReprResult = {didResolutionMetadata, didDocumentStream, didDocumentMetadata};
|
||||||
|
didNext();
|
||||||
|
});
|
||||||
|
function didNext() {
|
||||||
|
if (--didWaiting) return;
|
||||||
|
const {didDocument} = resResult;
|
||||||
|
let representationSpecificEntries = {};
|
||||||
|
let properties = {};
|
||||||
|
for (const property in didDocument) switch (property) {
|
||||||
|
case '@context':
|
||||||
|
representationSpecificEntries[property] = didDocument[property];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
properties[property] = didDocument[property];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
didReport.didDocumentDataModel = {
|
||||||
|
properties
|
||||||
|
};
|
||||||
|
const contentType = resReprResult.didResolutionMetadata.contentType;
|
||||||
|
if (!contentType) throw new Error('Expected contentType');
|
||||||
|
if (!contentTypes[contentType]) {
|
||||||
|
contentTypes[contentType] = true
|
||||||
|
supportedContentTypes.push(contentType);
|
||||||
|
}
|
||||||
|
didReport[contentType] = {
|
||||||
|
didDocumentDataModel: {
|
||||||
|
representationSpecificEntries
|
||||||
|
},
|
||||||
|
representation: resReprResult.didDocumentStream,
|
||||||
|
didDocumentMetadata: resReprResult.didDocumentMetadata,
|
||||||
|
didResolutionMetadata: resReprResult.didResolutionMetadata
|
||||||
|
};
|
||||||
|
methodNext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function methodNext() {
|
||||||
|
if (--methodWaiting) return;
|
||||||
|
const report = JSON.stringify(didMethodReport, null, 2);
|
||||||
|
fs.writeFileSync(didJsonFilename, report);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOutcome(error, deactivated) {
|
||||||
|
if (error) switch (error) {
|
||||||
|
case 'invalidDid': return 'invalidDidErrorOutcome';
|
||||||
|
case 'invalidDidUrl': return 'invalidDidUrlErrorOutcome';
|
||||||
|
case 'notFound': return 'notFoundErrorOutcome';
|
||||||
|
case 'representationNotSupported': return 'representationNotSupportedErrorOutcome';
|
||||||
|
default: throw new Error('Unknown error: ' + didResolutionMetadata.error);
|
||||||
|
}
|
||||||
|
if (deactivated) return 'deactivatedOutcome';
|
||||||
|
return 'defaultOutcome';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateResolverReport(next) {
|
||||||
|
let expectedOutcomes = {};
|
||||||
|
let executions = [];
|
||||||
|
const didResolverReport = {
|
||||||
|
...testCommon,
|
||||||
|
expectedOutcomes,
|
||||||
|
executions
|
||||||
|
};
|
||||||
|
let executionWaiting = resolverInputs.length;
|
||||||
|
resolverInputs.forEach(function (input, i) {
|
||||||
|
switch (input.function) {
|
||||||
|
case 'resolve':
|
||||||
|
resolver.resolve(input.did, input.resolutionOptions, function (didResolutionMetadata, didDocument, didDocumentMetadata) {
|
||||||
|
executions[i] = {
|
||||||
|
function: input.function,
|
||||||
|
input: {did: input.did, resolutionOptions: input.resolutionOptions},
|
||||||
|
output: {didResolutionMetadata, didDocument, didDocumentMetadata}
|
||||||
|
};
|
||||||
|
const outcome = getOutcome(didResolutionMetadata.error, didDocumentMetadata.deactivated);
|
||||||
|
const idxs = expectedOutcomes[outcome] || (expectedOutcomes[outcome] = []);
|
||||||
|
idxs.push(i);
|
||||||
|
executionNext();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'resolveRepresentation':
|
||||||
|
resolver.resolveRepresentation(input.did, input.resolutionOptions, function (didResolutionMetadata, didDocumentStream, didDocumentMetadata) {
|
||||||
|
executions[i] = {
|
||||||
|
function: input.function,
|
||||||
|
input: {did: input.did, resolutionOptions: input.resolutionOptions},
|
||||||
|
output: {didResolutionMetadata, didDocumentStream, didDocumentMetadata}
|
||||||
|
};
|
||||||
|
const outcome = getOutcome(didResolutionMetadata.error, didDocumentMetadata.deactivated);
|
||||||
|
const idxs = expectedOutcomes[outcome] || (expectedOutcomes[outcome] = []);
|
||||||
|
idxs.push(i);
|
||||||
|
executionNext();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown function: ' + input.function);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function executionNext() {
|
||||||
|
if (--executionWaiting) return;
|
||||||
|
for (const outcome in expectedOutcomes) {
|
||||||
|
expectedOutcomes[outcome].sort();
|
||||||
|
}
|
||||||
|
const report = JSON.stringify(didResolverReport, null, 2);
|
||||||
|
fs.writeFileSync(resolverJsonFilename, report);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDereferencerReport(next) {
|
||||||
|
let expectedOutcomes = {};
|
||||||
|
let executions = [];
|
||||||
|
const didUrlDereferencerReport = {
|
||||||
|
...testCommon,
|
||||||
|
expectedOutcomes,
|
||||||
|
executions
|
||||||
|
};
|
||||||
|
let executionWaiting = dereferencerInputs.length;
|
||||||
|
dereferencerInputs.forEach(function (input, i) {
|
||||||
|
resolver.dereference(input.didUrl, input.dereferenceOptions, function (dereferencingMetadata, contentStream, contentMetadata) {
|
||||||
|
executions[i] = {
|
||||||
|
function: 'dereference',
|
||||||
|
input,
|
||||||
|
output: {dereferencingMetadata, contentStream, contentMetadata}
|
||||||
|
};
|
||||||
|
const outcome = getOutcome(dereferencingMetadata.error, contentMetadata.deactivated);
|
||||||
|
const idxs = expectedOutcomes[outcome] || (expectedOutcomes[outcome] = []);
|
||||||
|
idxs.push(i);
|
||||||
|
executionNext();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function executionNext() {
|
||||||
|
if (--executionWaiting) return;
|
||||||
|
for (const outcome in expectedOutcomes) {
|
||||||
|
expectedOutcomes[outcome].sort();
|
||||||
|
}
|
||||||
|
const report = JSON.stringify(didUrlDereferencerReport, null, 2);
|
||||||
|
fs.writeFileSync(dereferencerJsonFilename, report);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "ssb-did-resolver",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"base64-url": "*",
|
||||||
|
"pull-stream": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/base64-url": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "http://localhost:33853/-/blobs/get/&M4DZ2DC8x5WIjq/p7r9x3BCJUQfUHm5uVXHOs1XXnPE=.sha256",
|
||||||
|
"integrity": "sha256-M4DZ2DC8x5WIjq/p7r9x3BCJUQfUHm5uVXHOs1XXnPE=",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/pull-stream": {
|
||||||
|
"version": "3.6.14",
|
||||||
|
"resolved": "http://localhost:35233/-/blobs/get/&WUect/OXBboO6Xft04OCQ06Vf39ZSNag27SC1kxtrFw=.sha256",
|
||||||
|
"integrity": "sha256-WUect/OXBboO6Xft04OCQ06Vf39ZSNag27SC1kxtrFw=",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"base64-url": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "http://localhost:33853/-/blobs/get/&M4DZ2DC8x5WIjq/p7r9x3BCJUQfUHm5uVXHOs1XXnPE=.sha256",
|
||||||
|
"integrity": "sha256-M4DZ2DC8x5WIjq/p7r9x3BCJUQfUHm5uVXHOs1XXnPE="
|
||||||
|
},
|
||||||
|
"pull-stream": {
|
||||||
|
"version": "3.6.14",
|
||||||
|
"resolved": "http://localhost:35233/-/blobs/get/&WUect/OXBboO6Xft04OCQ06Vf39ZSNag27SC1kxtrFw=.sha256",
|
||||||
|
"integrity": "sha256-WUect/OXBboO6Xft04OCQ06Vf39ZSNag27SC1kxtrFw="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"license": "ISC",
|
||||||
|
"version": "v0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-url": "2.2",
|
||||||
|
"pull-stream": "3.6"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,310 @@
|
|||||||
|
// did:ssb resolver
|
||||||
|
|
||||||
|
// did:ssb specification: v0.1 2021-07-22
|
||||||
|
// &LXcZ4lpcpi3tEwqu+HqQmVUFVsqr0BMo1UNjy0bFafA=.sha256
|
||||||
|
|
||||||
|
const Base64Url = require('base64-url');
|
||||||
|
const pull = require('pull-stream');
|
||||||
|
const qs = require('querystring');
|
||||||
|
|
||||||
|
function Resolver(sbot, config) {
|
||||||
|
this.sbot = sbot;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const didRe = /^did:ssb:(ed25519):([0-9a-zA-Z._\-]+)$/;
|
||||||
|
const didUrlRe = /^(did:[^?#\/]*)(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;
|
||||||
|
const ssbRefRe = /^(@|%|&)([A-Za-z0-9\/+]{43}=)\.([\w\d]+)$/;
|
||||||
|
const ssbMsgIdRe = /^%[A-Za-z0-9\/+]{43}=\.[\w\d]+$/;
|
||||||
|
|
||||||
|
function parseDIDURL(uri) {
|
||||||
|
const m = didUrlRe.exec(uri);
|
||||||
|
if (!m) return null;
|
||||||
|
return {
|
||||||
|
did: m[1],
|
||||||
|
path: m[2],
|
||||||
|
query: m[3],
|
||||||
|
fragment: m[4]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSsbFeedIdFromDID(did) {
|
||||||
|
const m = didRe.exec(did);
|
||||||
|
if (!m) return null;
|
||||||
|
return '@' + Base64Url.unescape(m[2]) + '.' + m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSsbMsgId(id) {
|
||||||
|
if (ssbMsgIdRe.test(id)) return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timestampToDateTime(ts) {
|
||||||
|
const d = new Date(ts);
|
||||||
|
if (isNaN(d)) return;
|
||||||
|
return d.toISOString().replace(/\.\d\d\dZ$/, 'Z');
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeDIDDocument(didDocument, contentType) {
|
||||||
|
switch (contentType) {
|
||||||
|
case 'application/did+json':
|
||||||
|
case 'application/did+ld+json':
|
||||||
|
return JSON.stringify(didDocument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDIDDocumentStream(didDocumentStream, contentType) {
|
||||||
|
switch (contentType) {
|
||||||
|
case 'application/did+json':
|
||||||
|
case 'application/did+ld+json':
|
||||||
|
return JSON.parse(didDocumentStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://viewer.scuttlebot.io/web/%25B79jjfhYHrr%2BCPSDpr2S5vRGWUJ5vwyfzbYCp0OyV9k%3D.sha256#read-resolve
|
||||||
|
Resolver.prototype.resolve = function (did, resolutionOptions, cb) {
|
||||||
|
this.resolveOrResolveRepresentation(did, resolutionOptions, false, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
Resolver.prototype.resolveRepresentation = function (did, resolutionOptions, cb) {
|
||||||
|
this.resolveOrResolveRepresentation(did, resolutionOptions, true, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
Resolver.prototype.resolveOrResolveRepresentation = function (did, resolutionOptions, calledResolveRepresentation, cb) {
|
||||||
|
const self = this;
|
||||||
|
let msg, nextMsg, firstMsg, contentType, didDocument, didDocumentStream, accept, versionId, versionTime;
|
||||||
|
const Null = calledResolveRepresentation ? '' : null;
|
||||||
|
for (const option in resolutionOptions) switch (option) {
|
||||||
|
case 'versionId': versionId = resolutionOptions.versionId; break;
|
||||||
|
case 'versionTime': versionTime = resolutionOptions.versionTime; break;
|
||||||
|
case 'accept': accept = resolutionOptions.accept; break;
|
||||||
|
default: return cb({error: 'resolutionOptionNotSupported', option}, Null, {});
|
||||||
|
}
|
||||||
|
// Note: this functions accepts versionId and versionTime as resolution options (resolution input metadata),
|
||||||
|
// even though they are not specified as such.
|
||||||
|
// 1
|
||||||
|
let didDocumentMetadata = {};
|
||||||
|
// 2
|
||||||
|
let didResolutionMetadata = {};
|
||||||
|
// 3
|
||||||
|
const feedId = parseSsbFeedIdFromDID(did);
|
||||||
|
if (!feedId) return cb({error: 'invalidDid'}, Null, {});
|
||||||
|
// 4
|
||||||
|
if (versionId) {
|
||||||
|
const msgId = parseSsbMsgId(versionId);
|
||||||
|
if (!msgId) return cb({error: 'invalidVersionId'}, Null, {});
|
||||||
|
self.sbot.get(msgId, function (err, value) {
|
||||||
|
if (err) return cb({error: 'ssbMessageMissing'}, Null, {});
|
||||||
|
if (value.author !== feedId) return cb({error: 'ssbMessageInvalidAuthor'}, Null, {});
|
||||||
|
if (value.content.type !== 'did-document-update') return cb({error: 'notFound'}, Null, {});
|
||||||
|
msg = {key: msgId, value};
|
||||||
|
gotLatestMessage();
|
||||||
|
});
|
||||||
|
} else if (versionTime) {
|
||||||
|
const versionDate = new Date(versionTime);
|
||||||
|
// 5
|
||||||
|
pull(
|
||||||
|
self.sbot.query.read({
|
||||||
|
reverse: true,
|
||||||
|
limit: 1,
|
||||||
|
query: [
|
||||||
|
{$filter: {
|
||||||
|
value: {
|
||||||
|
author: feedId,
|
||||||
|
content: {type: 'did-document-update'},
|
||||||
|
timestamp: {$lte: versionDate.getTime()}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
pull.collect(function (err, msgs) {
|
||||||
|
if (err) return cb({error: 'queryFailed',
|
||||||
|
errorMessage: String(err.message || err)}, Null, {});
|
||||||
|
msg = msgs[0];
|
||||||
|
gotLatestMessage()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 6
|
||||||
|
pull(
|
||||||
|
self.sbot.query.read({
|
||||||
|
reverse: true,
|
||||||
|
limit: 1,
|
||||||
|
query: [
|
||||||
|
{$filter: {
|
||||||
|
value: {
|
||||||
|
author: feedId,
|
||||||
|
content: {type: 'did-document-update'}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
pull.collect(function (err, msgs) {
|
||||||
|
if (err) return cb({error: 'queryFailed',
|
||||||
|
errorMessage: String(err.message || err)}, Null, {});
|
||||||
|
msg = msgs[0];
|
||||||
|
gotLatestMessage()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function gotLatestMessage() {
|
||||||
|
// 7
|
||||||
|
if (!msg) return cb({error: 'notFound'}, Null, {});
|
||||||
|
// 8
|
||||||
|
didDocumentMetadata.versionId = msg.key;
|
||||||
|
// 9
|
||||||
|
if (versionId || versionTime) {
|
||||||
|
pull(
|
||||||
|
self.sbot.query.read({
|
||||||
|
reverse: false,
|
||||||
|
limit: 1,
|
||||||
|
query: [
|
||||||
|
{$filter: {
|
||||||
|
value: {
|
||||||
|
author: feedId,
|
||||||
|
timestamp: {$gt: msg.value.timestamp},
|
||||||
|
content: {type: 'did-document-update'}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
pull.collect(function (err, msgs) {
|
||||||
|
if (err) return cb({error: 'queryFailed',
|
||||||
|
errorMessage: String(err.message || err)}, Null, {});
|
||||||
|
nextMsg = msgs[0];
|
||||||
|
gotNextMessage();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
gotNextMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gotNextMessage() {
|
||||||
|
if (nextMsg) {
|
||||||
|
// 9.1
|
||||||
|
didDocumentMetadata.nextVersionId = nextMsg.key;
|
||||||
|
// 9.2
|
||||||
|
const nextUpdate = timestampToDateTime(nextMsg.value.timestamp);
|
||||||
|
if (nextUpdate) didDocumentMetadata.nextUpdate = nextUpdate;
|
||||||
|
}
|
||||||
|
// 10
|
||||||
|
// TODO: update spec: updated should not be set for initial update (Create) operation
|
||||||
|
// const updated = didDocumentMetadata.updated = timestampToDateTime(msg.value.timestamp);
|
||||||
|
// if (updated) didDocumentMetadata.updated = updated;
|
||||||
|
// 11
|
||||||
|
pull(
|
||||||
|
self.sbot.query.read({
|
||||||
|
reverse: false,
|
||||||
|
limit: 1,
|
||||||
|
query: [
|
||||||
|
{$filter: {
|
||||||
|
value: {
|
||||||
|
author: feedId,
|
||||||
|
content: {type: 'did-document-update'}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
pull.collect(function (err, msgs) {
|
||||||
|
if (err) return cb({error: 'queryFailed',
|
||||||
|
errorMessage: String(err.message || err)}, Null, {});
|
||||||
|
firstMsg = msgs[0];
|
||||||
|
gotFirstMessage();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function gotFirstMessage() {
|
||||||
|
if (!firstMsg) return cb({error: 'ssbMessageMissing'}, Null, {});
|
||||||
|
// 12
|
||||||
|
const created = didDocumentMetadata.created = timestampToDateTime(firstMsg.value.timestamp);
|
||||||
|
if (created) didDocumentMetadata.created = created;
|
||||||
|
if (firstMsg.key !== msg.key) {
|
||||||
|
// TODO: update spec
|
||||||
|
const updated = didDocumentMetadata.updated = timestampToDateTime(msg.value.timestamp);
|
||||||
|
if (updated) didDocumentMetadata.updated = updated;
|
||||||
|
}
|
||||||
|
// 13
|
||||||
|
contentType = msg.value.content.contentType || 'application/did+json';
|
||||||
|
// 14
|
||||||
|
const blobLink = msg.value.content.didDocumentBlob;
|
||||||
|
if (blobLink) {
|
||||||
|
let blobId;
|
||||||
|
if (typeof blobLink === 'string') {
|
||||||
|
// 14.1
|
||||||
|
blobId = blobLink;
|
||||||
|
} else if (typeof blobLink === 'object' && blobLink !== null) {
|
||||||
|
// 14.2.1
|
||||||
|
blobId = blobLink.link;
|
||||||
|
// 14.2.2
|
||||||
|
if (blobLink.type) contentType = blobLink.type;
|
||||||
|
} else {
|
||||||
|
// 14.3
|
||||||
|
return cb({error: 'ssbInvalidBlobLink'}, Null, {});
|
||||||
|
}
|
||||||
|
// 14.4
|
||||||
|
getBlob(blobId, function (err, data) {
|
||||||
|
didDocumentStream = data;
|
||||||
|
gotDocumentOrStream();
|
||||||
|
})
|
||||||
|
} else if (msg.value.content.didDocumentString) {
|
||||||
|
// 15
|
||||||
|
didDocumentStream = msg.value.content.didDocumentString;
|
||||||
|
gotDocumentOrStream();
|
||||||
|
} else if (msg.value.content.didDocument) {
|
||||||
|
// 16
|
||||||
|
didDocument = msg.value.content.didDocument;
|
||||||
|
gotDocumentOrStream();
|
||||||
|
} else {
|
||||||
|
// 17
|
||||||
|
return cb({error: 'ssbInvalidUpdate'}, Null, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function gotDocumentOrStream() {
|
||||||
|
// TODO: specify use of accept option
|
||||||
|
if (accept && accept !== contentType) {
|
||||||
|
// TODO: convert between representations?
|
||||||
|
return cb({error: 'representationNotSupported'}, Null, {});
|
||||||
|
}
|
||||||
|
if (calledResolveRepresentation) {
|
||||||
|
// 18.1
|
||||||
|
didResolutionMetadata.contentType = contentType;
|
||||||
|
if (!didDocumentStream) {
|
||||||
|
// 18.2
|
||||||
|
didDocumentStream = serializeDIDDocument(didDocument, contentType);
|
||||||
|
if (!didDocumentStream) return cb({error: 'representationNotSupported'}, Null, {});
|
||||||
|
}
|
||||||
|
// 18.3
|
||||||
|
cb(didResolutionMetadata, didDocumentStream, didDocumentMetadata);
|
||||||
|
} else {
|
||||||
|
// 19.1
|
||||||
|
if (!didDocument) {
|
||||||
|
try {
|
||||||
|
didDocument = parseDIDDocumentStream(didDocumentStream, contentType);
|
||||||
|
} catch(e) {
|
||||||
|
return cb({error: 'invalidRepresentation', errorMessage: e.message}, Null, {});
|
||||||
|
}
|
||||||
|
if (!didDocument) return cb({error: 'representationNotSupported'}, Null, {});
|
||||||
|
}
|
||||||
|
// 19.2
|
||||||
|
cb(didResolutionMetadata, didDocument, didDocumentMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Resolver.prototype.dereference = function (didUrl, dereferenceOptions, cb) {
|
||||||
|
let accept;
|
||||||
|
for (const option in dereferenceOptions) switch (option) {
|
||||||
|
case 'accept': accept = dereferenceOptions.accept; break;
|
||||||
|
default: return cb({error: 'dereferencingOptionNotSupported', option}, null, {});
|
||||||
|
}
|
||||||
|
const parts = parseDIDURL(didUrl);
|
||||||
|
if (!didUrl) return cb({error: 'invalidDidUrl'}, null, {});
|
||||||
|
if (parts.path) return cb({error: 'didPathNotSupported'}, null, {});
|
||||||
|
// The caller is expected to deal with the fragment
|
||||||
|
if (parts.fragment) return cb({error: 'didFragmentNotSupported'}, null, {});
|
||||||
|
const didParameters = parts.query ? qs.decode(parts.query) : {};
|
||||||
|
const resolutionOptions = {...didParameters};
|
||||||
|
if (accept) resolutionOptions.accept = accept;
|
||||||
|
this.resolveRepresentation(parts.did, resolutionOptions, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Resolver;
|
Loading…
Reference in new issue