- Add GETTING_STARTED.md with quick start guide and development modes - Add INSTALL.sh automated installation script - Add INSTALLATION_CHECKLIST.md, INSTALLATION_SUCCESS.md, and INSTALLATION_SUMMARY.md - Add QUICK_REFERENCE.md for common commands - Add SETUP_GUIDE.md with detailed setup instructions - Update README.md with improved project overview - Add did-wallet app dependencies and node_modules
337 lines
18 KiB
JavaScript
337 lines
18 KiB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
import { Convert, NodeStream } from '@web5/common';
|
|
import { utils as cryptoUtils } from '@web5/crypto';
|
|
import { DidDht, DidJwk, DidResolverCacheLevel, UniversalResolver } from '@web5/dids';
|
|
import { Cid, DataStoreLevel, Dwn, DwnMethodName, EventLogLevel, Message, MessageStoreLevel } from '@tbd54566975/dwn-sdk-js';
|
|
import { DwnInterface, dwnMessageConstructors } from './types/dwn.js';
|
|
import { blobToIsomorphicNodeReadable, getDwnServiceEndpointUrls, isRecordsWrite, webReadableToIsomorphicNodeReadable } from './utils.js';
|
|
export function isDwnRequest(dwnRequest, messageType) {
|
|
return dwnRequest.messageType === messageType;
|
|
}
|
|
export function isDwnMessage(messageType, message) {
|
|
const incomingMessageInterfaceName = message.descriptor.interface + message.descriptor.method;
|
|
return incomingMessageInterfaceName === messageType;
|
|
}
|
|
export class AgentDwnApi {
|
|
constructor({ agent, dwn }) {
|
|
// If an agent is provided, set it as the execution context for this API.
|
|
this._agent = agent;
|
|
// Set the DWN instance for this API.
|
|
this._dwn = dwn;
|
|
}
|
|
/**
|
|
* Retrieves the `Web5PlatformAgent` execution context.
|
|
*
|
|
* @returns The `Web5PlatformAgent` instance that represents the current execution context.
|
|
* @throws Will throw an error if the `agent` instance property is undefined.
|
|
*/
|
|
get agent() {
|
|
if (this._agent === undefined) {
|
|
throw new Error('AgentDwnApi: Unable to determine agent execution context.');
|
|
}
|
|
return this._agent;
|
|
}
|
|
set agent(agent) {
|
|
this._agent = agent;
|
|
}
|
|
/**
|
|
* Public getter for the DWN instance used by this API.
|
|
*
|
|
* Notes:
|
|
* - This getter is public to allow advanced developers to access the DWN instance directly.
|
|
* However, it is recommended to use the `processRequest` method to interact with the DWN
|
|
* instance to ensure that the DWN message is constructed correctly.
|
|
* - The getter is named `node` to avoid confusion with the `dwn` property of the
|
|
* `Web5PlatformAgent`. In other words, so that a developer can call `agent.dwn.node` to access
|
|
* the DWN instance and not `agent.dwn.dwn`.
|
|
*/
|
|
get node() {
|
|
return this._dwn;
|
|
}
|
|
static createDwn({ dataPath, dataStore, didResolver, eventLog, eventStream, messageStore, tenantGate }) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
dataStore !== null && dataStore !== void 0 ? dataStore : (dataStore = new DataStoreLevel({ blockstoreLocation: `${dataPath}/DWN_DATASTORE` }));
|
|
didResolver !== null && didResolver !== void 0 ? didResolver : (didResolver = new UniversalResolver({
|
|
didResolvers: [DidDht, DidJwk],
|
|
cache: new DidResolverCacheLevel({ location: `${dataPath}/DID_RESOLVERCACHE` }),
|
|
}));
|
|
eventLog !== null && eventLog !== void 0 ? eventLog : (eventLog = new EventLogLevel({ location: `${dataPath}/DWN_EVENTLOG` }));
|
|
messageStore !== null && messageStore !== void 0 ? messageStore : (messageStore = new MessageStoreLevel(({
|
|
blockstoreLocation: `${dataPath}/DWN_MESSAGESTORE`,
|
|
indexLocation: `${dataPath}/DWN_MESSAGEINDEX`
|
|
})));
|
|
return yield Dwn.create({ dataStore, didResolver, eventLog, eventStream, messageStore, tenantGate });
|
|
});
|
|
}
|
|
processRequest(request) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// Constructs a DWN message. and if there is a data payload, transforms the data to a Node
|
|
// Readable stream.
|
|
const { message, dataStream } = yield this.constructDwnMessage({ request });
|
|
// Extracts the optional subscription handler from the request to pass into `processMessage.
|
|
const { subscriptionHandler } = request;
|
|
// Conditionally processes the message with the DWN instance:
|
|
// - If `store` is not explicitly set to false, it sends the message to the DWN node for
|
|
// processing, passing along the target DID, the message, and any associated data stream.
|
|
// - If `store` is set to false, it immediately returns a simulated 'accepted' status without
|
|
// storing the message/data in the DWN node.
|
|
const reply = (request.store !== false)
|
|
? yield this._dwn.processMessage(request.target, message, { dataStream, subscriptionHandler })
|
|
: { status: { code: 202, detail: 'Accepted' } };
|
|
// Returns an object containing the reply from processing the message, the original message,
|
|
// and the content identifier (CID) of the message.
|
|
return {
|
|
reply,
|
|
message,
|
|
messageCid: yield Message.getCid(message),
|
|
};
|
|
});
|
|
}
|
|
sendRequest(request) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// First, confirm the target DID can be dereferenced and extract the DWN service endpoint URLs.
|
|
const dwnEndpointUrls = yield getDwnServiceEndpointUrls(request.target, this.agent.did);
|
|
if (dwnEndpointUrls.length === 0) {
|
|
throw new Error(`AgentDwnApi: DID Service is missing or malformed: ${request.target}#dwn`);
|
|
}
|
|
let messageCid;
|
|
let message;
|
|
let data;
|
|
let subscriptionHandler;
|
|
// If `messageCid` is given, retrieve message and data, if any.
|
|
if ('messageCid' in request) {
|
|
({ message, data } = yield this.getDwnMessage({
|
|
author: request.author,
|
|
messageCid: request.messageCid,
|
|
messageType: request.messageType
|
|
}));
|
|
messageCid = request.messageCid;
|
|
}
|
|
else {
|
|
// Otherwise, construct a new message.
|
|
({ message } = yield this.constructDwnMessage({ request }));
|
|
if (request.dataStream && !(request.dataStream instanceof Blob)) {
|
|
throw new Error('AgentDwnApi: DataStream must be provided as a Blob');
|
|
}
|
|
data = request.dataStream;
|
|
subscriptionHandler = request.subscriptionHandler;
|
|
}
|
|
// Send the RPC request to the target DID's DWN service endpoint using the Agent's RPC client.
|
|
const reply = yield this.sendDwnRpcRequest({
|
|
targetDid: request.target,
|
|
dwnEndpointUrls,
|
|
message,
|
|
data,
|
|
subscriptionHandler
|
|
});
|
|
// If the message CID was not given in the `request`, compute it.
|
|
messageCid !== null && messageCid !== void 0 ? messageCid : (messageCid = yield Message.getCid(message));
|
|
// Returns an object containing the reply from processing the message, the original message,
|
|
// and the content identifier (CID) of the message.
|
|
return { reply, message, messageCid };
|
|
});
|
|
}
|
|
sendDwnRpcRequest({ targetDid, dwnEndpointUrls, message, data, subscriptionHandler }) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const errorMessages = [];
|
|
if (message.descriptor.method === DwnMethodName.Subscribe && subscriptionHandler === undefined) {
|
|
throw new Error('AgentDwnApi: Subscription handler is required for subscription requests.');
|
|
}
|
|
// Try sending to author's publicly addressable DWNs until the first request succeeds.
|
|
for (let dwnUrl of dwnEndpointUrls) {
|
|
try {
|
|
if (subscriptionHandler !== undefined) {
|
|
// we get the server info to check if the server supports WebSocket for subscription requests
|
|
const serverInfo = yield this.agent.rpc.getServerInfo(dwnUrl);
|
|
if (!serverInfo.webSocketSupport) {
|
|
// If the server does not support WebSocket, add an error message and continue to the next URL.
|
|
errorMessages.push({
|
|
url: dwnUrl,
|
|
message: 'WebSocket support is not enabled on the server.'
|
|
});
|
|
continue;
|
|
}
|
|
// If the server supports WebSocket, replace the subscription URL with a socket transport.
|
|
// For `http` we use the unsecured `ws` protocol, and for `https` we use the secured `wss` protocol.
|
|
const parsedUrl = new URL(dwnUrl);
|
|
parsedUrl.protocol = parsedUrl.protocol === 'http:' ? 'ws:' : 'wss:';
|
|
dwnUrl = parsedUrl.toString();
|
|
}
|
|
const dwnReply = yield this.agent.rpc.sendDwnRequest({
|
|
dwnUrl,
|
|
targetDid,
|
|
message,
|
|
data,
|
|
subscriptionHandler
|
|
});
|
|
return dwnReply;
|
|
}
|
|
catch (error) {
|
|
errorMessages.push({
|
|
url: dwnUrl,
|
|
message: (error instanceof Error) ? error.message : 'Unknown error',
|
|
});
|
|
}
|
|
}
|
|
throw new Error(`Failed to send DWN RPC request: ${JSON.stringify(errorMessages)}`);
|
|
});
|
|
}
|
|
constructDwnMessage({ request }) {
|
|
var _a;
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const rawMessage = request.rawMessage;
|
|
let readableStream;
|
|
// TODO: Consider refactoring to move data transformations imposed by fetch() limitations to the HTTP transport-related methods.
|
|
if (isDwnRequest(request, DwnInterface.RecordsWrite)) {
|
|
const messageParams = request.messageParams;
|
|
if (request.dataStream && !(messageParams === null || messageParams === void 0 ? void 0 : messageParams.data)) {
|
|
const { dataStream } = request;
|
|
let isomorphicNodeReadable;
|
|
if (dataStream instanceof Blob) {
|
|
isomorphicNodeReadable = blobToIsomorphicNodeReadable(dataStream);
|
|
readableStream = blobToIsomorphicNodeReadable(dataStream);
|
|
}
|
|
else if (dataStream instanceof ReadableStream) {
|
|
const [forCid, forProcessMessage] = dataStream.tee();
|
|
isomorphicNodeReadable = webReadableToIsomorphicNodeReadable(forCid);
|
|
readableStream = webReadableToIsomorphicNodeReadable(forProcessMessage);
|
|
}
|
|
if (!rawMessage) {
|
|
// @ts-ignore
|
|
messageParams.dataCid = yield Cid.computeDagPbCidFromStream(isomorphicNodeReadable);
|
|
// @ts-ignore
|
|
(_a = messageParams.dataSize) !== null && _a !== void 0 ? _a : (messageParams.dataSize = isomorphicNodeReadable['bytesRead']);
|
|
}
|
|
}
|
|
}
|
|
// Determine the signer for the message.
|
|
const signer = yield this.getSigner(request.author);
|
|
const dwnMessageConstructor = dwnMessageConstructors[request.messageType];
|
|
const dwnMessage = rawMessage ? yield dwnMessageConstructor.parse(rawMessage) : yield dwnMessageConstructor.create(Object.assign(Object.assign({}, request.messageParams), { signer }));
|
|
if (isRecordsWrite(dwnMessage) && request.signAsOwner) {
|
|
yield dwnMessage.signAsOwner(signer);
|
|
}
|
|
return { message: dwnMessage.message, dataStream: readableStream };
|
|
});
|
|
}
|
|
getSigner(author) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// If the author is the Agent's DID, use the Agent's signer.
|
|
if (author === this.agent.agentDid.uri) {
|
|
const signer = yield this.agent.agentDid.getSigner();
|
|
return {
|
|
algorithm: signer.algorithm,
|
|
keyId: signer.keyId,
|
|
sign: (data) => __awaiter(this, void 0, void 0, function* () {
|
|
return yield signer.sign({ data });
|
|
})
|
|
};
|
|
}
|
|
else {
|
|
// Otherwise, use the author's DID to determine the signing method.
|
|
try {
|
|
const signingMethod = yield this.agent.did.getSigningMethod({ didUri: author });
|
|
if (!signingMethod.publicKeyJwk) {
|
|
throw new Error(`Verification method '${signingMethod.id}' does not contain a public key in JWK format`);
|
|
}
|
|
// Compute the key URI of the verification method's public key.
|
|
const keyUri = yield this.agent.keyManager.getKeyUri({ key: signingMethod.publicKeyJwk });
|
|
// Verify that the key is present in the key manager. If not, an error is thrown.
|
|
const publicKey = yield this.agent.keyManager.getPublicKey({ keyUri });
|
|
// Bind the Agent's Key Manager to the signer.
|
|
const keyManager = this.agent.keyManager;
|
|
return {
|
|
algorithm: cryptoUtils.getJoseSignatureAlgorithmFromPublicKey(publicKey),
|
|
keyId: signingMethod.id,
|
|
sign: (data) => __awaiter(this, void 0, void 0, function* () {
|
|
return yield keyManager.sign({ data, keyUri: keyUri });
|
|
})
|
|
};
|
|
}
|
|
catch (error) {
|
|
throw new Error(`AgentDwnApi: Unable to get signer for author '${author}': ${error.message}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* FURTHER REFACTORING NEEDED BELOW THIS LINE
|
|
*/
|
|
getDwnMessage({ author, messageCid }) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
const signer = yield this.getSigner(author);
|
|
// Construct a MessagesGet message to fetch the message.
|
|
const messagesGet = yield dwnMessageConstructors[DwnInterface.MessagesGet].create({
|
|
messageCids: [messageCid],
|
|
signer
|
|
});
|
|
const result = yield this._dwn.processMessage(author, messagesGet.message);
|
|
if (!(result.entries && result.entries.length === 1)) {
|
|
throw new Error('AgentDwnApi: Expected 1 message entry in the MessagesGet response but received none or more than one.');
|
|
}
|
|
const [messageEntry] = result.entries;
|
|
const message = messageEntry.message;
|
|
if (!message) {
|
|
throw new Error(`AgentDwnApi: Message not found with CID: ${messageCid}`);
|
|
}
|
|
let dwnMessageWithBlob = { message };
|
|
// isRecordsWrite(message) && (dwnMessage.data = await this.getDataForRecordsWrite({ author, message, messageEntry, messageType, signer }));
|
|
// If the message is a RecordsWrite, either data will be present,
|
|
// OR we have to fetch it using a RecordsRead.
|
|
if (isRecordsWrite(messageEntry)) {
|
|
if (messageEntry.encodedData) {
|
|
const dataBytes = Convert.base64Url(messageEntry.encodedData).toUint8Array();
|
|
// TODO: test adding the messageEntry.message.descriptor.dataFormat to the Blob constructor.
|
|
dwnMessageWithBlob.data = new Blob([dataBytes]);
|
|
}
|
|
else {
|
|
const recordsRead = yield dwnMessageConstructors[DwnInterface.RecordsRead].create({
|
|
filter: {
|
|
recordId: messageEntry.message.recordId
|
|
},
|
|
signer
|
|
});
|
|
const reply = yield this._dwn.processMessage(author, recordsRead.message);
|
|
if (reply.status.code >= 400) {
|
|
const { status: { code, detail } } = reply;
|
|
throw new Error(`AgentDwnApi: (${code}) Failed to read data associated with record ${messageEntry.message.recordId}. ${detail}}`);
|
|
}
|
|
else if (reply.record) {
|
|
const dataBytes = yield NodeStream.consumeToBytes({ readable: reply.record.data });
|
|
dwnMessageWithBlob.data = new Blob([dataBytes]);
|
|
}
|
|
}
|
|
}
|
|
return dwnMessageWithBlob;
|
|
});
|
|
}
|
|
/**
|
|
* TODO: Refactor this to consolidate logic in AgentDwnApi and SyncEngineLevel.
|
|
* ADDED TO GET SYNC WORKING
|
|
* - createMessage()
|
|
* - processMessage()
|
|
*/
|
|
createMessage({ author, messageParams, messageType }) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
// Determine the signer for the message.
|
|
const signer = yield this.getSigner(author);
|
|
const dwnMessageConstructor = dwnMessageConstructors[messageType];
|
|
const dwnMessage = yield dwnMessageConstructor.create(Object.assign(Object.assign({}, messageParams), { signer }));
|
|
return dwnMessage;
|
|
});
|
|
}
|
|
processMessage({ dataStream, message, targetDid }) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
return yield this._dwn.processMessage(targetDid, message, { dataStream });
|
|
});
|
|
}
|
|
}
|
|
//# sourceMappingURL=dwn-api.js.map
|