@enbox/api
Version:
SDK for accessing the features and capabilities of Web5
573 lines • 32.2 kB
JavaScript
/**
* NOTE: Added reference types here to avoid a `pnpm` bug during build.
* https://github.com/TBD54566975/web5-js/pull/507
*/
/// <reference types="@enbox/dwn-sdk-js" />
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());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { AgentPermissionsApi, } from '@enbox/agent';
import { isEmptyObject } from '@enbox/common';
import { DwnInterface, getRecordAuthor } from '@enbox/agent';
import { Record } from './record.js';
import { dataToBlob } from './utils.js';
import { Protocol } from './protocol.js';
import { PermissionGrant } from './permission-grant.js';
import { PermissionRequest } from './permission-request.js';
import { SubscriptionUtil } from './subscription-util.js';
/**
* Interface to interact with DWN Records and Protocols
*/
export class DwnApi {
constructor(options) {
this.agent = options.agent;
this.connectedDid = options.connectedDid;
this.delegateDid = options.delegateDid;
this.permissionsApi = new AgentPermissionsApi({ agent: this.agent });
}
/**
* API to interact with Grants
*
* NOTE: This is an EXPERIMENTAL API that will change behavior.
*
* Currently only supports issuing requests, grants, revokes and queries on behalf without permissions or impersonation.
* If the agent is connected to a delegateDid, the delegateDid will be used to sign/author the underlying records.
* If the agent is not connected to a delegateDid, the connectedDid will be used to sign/author the underlying records.
*
* @beta
*/
get permissions() {
return {
/**
* Request permission for a specific scope.
*/
request: (request) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const { message } = yield this.permissionsApi.createRequest(Object.assign(Object.assign({}, request), { author: (_a = this.delegateDid) !== null && _a !== void 0 ? _a : this.connectedDid }));
const requestParams = {
connectedDid: (_b = this.delegateDid) !== null && _b !== void 0 ? _b : this.connectedDid,
agent: this.agent,
message,
};
return yield PermissionRequest.parse(requestParams);
}),
/**
* Grant permission for a specific scope to a grantee DID.
*/
grant: (request) => __awaiter(this, void 0, void 0, function* () {
var _c, _d;
const { message } = yield this.permissionsApi.createGrant(Object.assign(Object.assign({}, request), { author: (_c = this.delegateDid) !== null && _c !== void 0 ? _c : this.connectedDid }));
const grantParams = {
connectedDid: (_d = this.delegateDid) !== null && _d !== void 0 ? _d : this.connectedDid,
agent: this.agent,
message,
};
return yield PermissionGrant.parse(grantParams);
}),
/**
* Query permission requests. You can filter by protocol and specify if you want to query a remote DWN.
*/
queryRequests: (request = {}) => __awaiter(this, void 0, void 0, function* () {
var _e, _f, _g;
const { from } = request, params = __rest(request, ["from"]);
const fetchResponse = yield this.permissionsApi.fetchRequests(Object.assign(Object.assign({}, params), { author: (_e = this.delegateDid) !== null && _e !== void 0 ? _e : this.connectedDid, target: (_f = from !== null && from !== void 0 ? from : this.delegateDid) !== null && _f !== void 0 ? _f : this.connectedDid, remote: from !== undefined }));
const requests = [];
for (const permission of fetchResponse) {
const requestParams = {
connectedDid: (_g = this.delegateDid) !== null && _g !== void 0 ? _g : this.connectedDid,
agent: this.agent,
message: permission.message,
};
requests.push(yield PermissionRequest.parse(requestParams));
}
return requests;
}),
/**
* Query permission grants. You can filter by grantee, grantor, protocol and specify if you want to query a remote DWN.
*/
queryGrants: (request = {}) => __awaiter(this, void 0, void 0, function* () {
var _h, _j, _k;
const { checkRevoked, from } = request, params = __rest(request, ["checkRevoked", "from"]);
const remote = from !== undefined;
const author = (_h = this.delegateDid) !== null && _h !== void 0 ? _h : this.connectedDid;
const target = (_j = from !== null && from !== void 0 ? from : this.delegateDid) !== null && _j !== void 0 ? _j : this.connectedDid;
const fetchResponse = yield this.permissionsApi.fetchGrants(Object.assign(Object.assign({}, params), { author,
target,
remote }));
const grants = [];
for (const permission of fetchResponse) {
const grantParams = {
connectedDid: (_k = this.delegateDid) !== null && _k !== void 0 ? _k : this.connectedDid,
agent: this.agent,
message: permission.message,
};
if (checkRevoked) {
const grantRecordId = permission.grant.id;
if (yield this.permissionsApi.isGrantRevoked({ author, target, grantRecordId, remote })) {
continue;
}
}
grants.push(yield PermissionGrant.parse(grantParams));
}
return grants;
})
};
}
/**
* API to interact with DWN protocols (e.g., `dwn.protocols.configure()`).
*/
get protocols() {
return {
/**
* Configure method, used to setup a new protocol (or update) with the passed definitions
*/
configure: (request) => __awaiter(this, void 0, void 0, function* () {
const agentRequest = {
author: this.connectedDid,
messageParams: request.message,
messageType: DwnInterface.ProtocolsConfigure,
target: this.connectedDid
};
if (this.delegateDid) {
const { message: delegatedGrant } = yield this.permissionsApi.getPermissionForRequest({
connectedDid: this.connectedDid,
delegateDid: this.delegateDid,
protocol: request.message.definition.protocol,
delegate: true,
cached: true,
messageType: agentRequest.messageType
});
agentRequest.messageParams = Object.assign(Object.assign({}, agentRequest.messageParams), { delegatedGrant });
agentRequest.granteeDid = this.delegateDid;
}
const agentResponse = yield this.agent.processDwnRequest(agentRequest);
const { message, messageCid, reply: { status } } = agentResponse;
const response = { status };
if (status.code < 300) {
const metadata = { author: this.connectedDid, messageCid };
response.protocol = new Protocol(this.agent, message, metadata);
}
return response;
}),
/**
* Query the available protocols
*/
query: (request) => __awaiter(this, void 0, void 0, function* () {
const agentRequest = {
author: this.connectedDid,
messageParams: request.message,
messageType: DwnInterface.ProtocolsQuery,
target: request.from || this.connectedDid
};
if (this.delegateDid) {
// We attempt to get a grant within a try catch, if there is no grant we will still sign the query with the delegate DID's key
// If the protocol is public, the query should be successful. This allows the app to query for public protocols without having a grant.
try {
const { grant: { id: permissionGrantId } } = yield this.permissionsApi.getPermissionForRequest({
connectedDid: this.connectedDid,
delegateDid: this.delegateDid,
protocol: request.message.filter.protocol,
cached: true,
messageType: agentRequest.messageType
});
agentRequest.messageParams = Object.assign(Object.assign({}, agentRequest.messageParams), { permissionGrantId });
agentRequest.granteeDid = this.delegateDid;
}
catch (_error) {
// if a grant is not found, we should author the request as the delegated DID to get public protocols
agentRequest.author = this.delegateDid;
}
}
let agentResponse;
if (request.from) {
agentResponse = yield this.agent.sendDwnRequest(agentRequest);
}
else {
agentResponse = yield this.agent.processDwnRequest(agentRequest);
}
const reply = agentResponse.reply;
const { entries = [], status } = reply;
const protocols = entries.map((entry) => {
const metadata = { author: this.connectedDid };
return new Protocol(this.agent, entry, metadata);
});
return { protocols, status };
})
};
}
/**
* API to interact with DWN records (e.g., `dwn.records.create()`).
*/
get records() {
return {
/**
* Alias for the `write` method
*/
create: (request) => __awaiter(this, void 0, void 0, function* () {
return this.records.write(request);
}),
/**
* Write a record based on an existing one (useful for updating an existing record)
*/
createFrom: (request) => __awaiter(this, void 0, void 0, function* () {
var _a;
const _b = request.record.toJSON(), { author: inheritedAuthor } = _b, inheritedProperties = __rest(_b, ["author"]);
// If `data` is being updated then `dataCid` and `dataSize` must not be present.
if (request.data !== undefined) {
delete inheritedProperties.dataCid;
delete inheritedProperties.dataSize;
}
// If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation
// will throw an error if `published` is false but `datePublished` is set.
if (((_a = request.message) === null || _a === void 0 ? void 0 : _a.published) === false && inheritedProperties.datePublished !== undefined) {
delete inheritedProperties.datePublished;
delete inheritedProperties.published;
}
// If the request changes the `author` or message `descriptor` then the deterministic `recordId` will change.
// As a result, we will discard the `recordId` if either of these changes occur.
if (!isEmptyObject(request.message) || (request.author && request.author !== inheritedAuthor)) {
delete inheritedProperties.recordId;
}
return this.records.write({
data: request.data,
message: Object.assign(Object.assign({}, inheritedProperties), request.message),
});
}),
/**
* Delete a record
*/
delete: (request) => __awaiter(this, void 0, void 0, function* () {
const agentRequest = {
/**
* The `author` is the DID that will sign the message and must be the DID the Web5 app is
* connected with and is authorized to access the signing private key of.
*/
author: this.connectedDid,
messageParams: request.message,
messageType: DwnInterface.RecordsDelete,
/**
* The `target` is the DID of the DWN tenant under which the delete will be executed.
* If `from` is provided, the delete operation will be executed on a remote DWN.
* Otherwise, the record will be deleted on the local DWN.
*/
target: request.from || this.connectedDid
};
if (this.delegateDid) {
const { message: delegatedGrant } = yield this.permissionsApi.getPermissionForRequest({
connectedDid: this.connectedDid,
delegateDid: this.delegateDid,
protocol: request.protocol,
delegate: true,
cached: true,
messageType: agentRequest.messageType
});
agentRequest.messageParams = Object.assign(Object.assign({}, agentRequest.messageParams), { delegatedGrant });
agentRequest.granteeDid = this.delegateDid;
}
let agentResponse;
if (request.from) {
agentResponse = yield this.agent.sendDwnRequest(agentRequest);
}
else {
agentResponse = yield this.agent.processDwnRequest(agentRequest);
}
const { reply: { status } } = agentResponse;
return { status };
}),
/**
* Query a single or multiple records based on the given filter
*/
query: (request) => __awaiter(this, void 0, void 0, function* () {
const agentRequest = {
/**
* The `author` is the DID that will sign the message and must be the DID the Web5 app is
* connected with and is authorized to access the signing private key of.
*/
author: this.connectedDid,
messageParams: request.message,
messageType: DwnInterface.RecordsQuery,
/**
* The `target` is the DID of the DWN tenant under which the query will be executed.
* If `from` is provided, the query operation will be executed on a remote DWN.
* Otherwise, the local DWN will be queried.
*/
target: request.from || this.connectedDid
};
if (this.delegateDid) {
// if we don't find a delegated grant, we will attempt to query signing as the delegated DID
// This is to allow the API caller to query public records without needing to impersonate the delegate.
//
// NOTE: When a read-only DwnApi is implemented, callers should use that instead when they don't have an explicit permission.
// This should fail if a permission is not found.
// TODO: https://github.com/TBD54566975/web5-js/issues/898
try {
const { message: delegatedGrant } = yield this.permissionsApi.getPermissionForRequest({
connectedDid: this.connectedDid,
delegateDid: this.delegateDid,
protocol: request.protocol,
delegate: true,
cached: true,
messageType: agentRequest.messageType
});
agentRequest.messageParams = Object.assign(Object.assign({}, agentRequest.messageParams), { delegatedGrant });
agentRequest.granteeDid = this.delegateDid;
}
catch (_error) {
// if a grant is not found, we should author the request as the delegated DID to get public records
agentRequest.author = this.delegateDid;
}
}
let agentResponse;
if (request.from) {
agentResponse = yield this.agent.sendDwnRequest(agentRequest);
}
else {
agentResponse = yield this.agent.processDwnRequest(agentRequest);
}
const reply = agentResponse.reply;
const { entries = [], status, cursor } = reply;
const records = entries.map((entry) => {
const recordOptions = Object.assign({
/**
* Extract the `author` DID from the record entry since records may be signed by the
* tenant owner or any other entity.
*/
author: getRecordAuthor(entry),
/**
* Set the `connectedDid` to currently connected DID so that subsequent calls to
* {@link Record} instance methods, such as `record.update()` are executed on the
* local DWN even if the record was returned by a query of a remote DWN.
*/
connectedDid: this.connectedDid,
/**
* If the record was returned by a query of a remote DWN, set the `remoteOrigin` to
* the DID of the DWN that returned the record. The `remoteOrigin` property will be used
* to determine which DWN to send subsequent read requests to in the event the data
* payload exceeds the threshold for being returned with queries.
*/
remoteOrigin: request.from, delegateDid: this.delegateDid, protocolRole: agentRequest.messageParams.protocolRole }, entry);
const record = new Record(this.agent, recordOptions, this.permissionsApi);
return record;
});
return { records, status, cursor };
}),
/**
* Read a single record based on the given filter
*/
read: (request) => __awaiter(this, void 0, void 0, function* () {
const agentRequest = {
/**
* The `author` is the DID that will sign the message and must be the DID the Web5 app is
* connected with and is authorized to access the signing private key of.
*/
author: this.connectedDid,
messageParams: request.message,
messageType: DwnInterface.RecordsRead,
/**
* The `target` is the DID of the DWN tenant under which the read will be executed.
* If `from` is provided, the read operation will be executed on a remote DWN.
* Otherwise, the read will occur on the local DWN.
*/
target: request.from || this.connectedDid
};
if (this.delegateDid) {
// if we don't find a delegated grant, we will attempt to read signing as the delegated DID
// This is to allow the API caller to read public records without needing to impersonate the delegate.
//
// NOTE: When a read-only DwnApi is implemented, callers should use that instead when they don't have an explicit permission.
// This should fail if a permission is not found.
// TODO: https://github.com/TBD54566975/web5-js/issues/898
try {
const { message: delegatedGrant } = yield this.permissionsApi.getPermissionForRequest({
connectedDid: this.connectedDid,
delegateDid: this.delegateDid,
protocol: request.protocol,
delegate: true,
cached: true,
messageType: agentRequest.messageType
});
agentRequest.messageParams = Object.assign(Object.assign({}, agentRequest.messageParams), { delegatedGrant });
agentRequest.granteeDid = this.delegateDid;
}
catch (_error) {
// if a grant is not found, we should author the request as the delegated DID to get public records
agentRequest.author = this.delegateDid;
}
}
let agentResponse;
if (request.from) {
agentResponse = yield this.agent.sendDwnRequest(agentRequest);
}
else {
agentResponse = yield this.agent.processDwnRequest(agentRequest);
}
const { reply: { entry, status } } = agentResponse;
let record;
if (200 <= status.code && status.code <= 299) {
const recordOptions = Object.assign({
/**
* Extract the `author` DID from the record since records may be signed by the
* tenant owner or any other entity.
*/
author: getRecordAuthor(entry.recordsWrite),
/**
* Set the `connectedDid` to currently connected DID so that subsequent calls to
* {@link Record} instance methods, such as `record.update()` are executed on the
* local DWN even if the record was read from a remote DWN.
*/
connectedDid: this.connectedDid,
/**
* If the record was returned by reading from a remote DWN, set the `remoteOrigin` to
* the DID of the DWN that returned the record. The `remoteOrigin` property will be used
* to determine which DWN to send subsequent read requests to in the event the data
* payload must be read again (e.g., if the data stream is consumed).
*/
remoteOrigin: request.from, delegateDid: this.delegateDid, data: entry.data, initialWrite: entry.initialWrite }, entry.recordsWrite);
record = new Record(this.agent, recordOptions, this.permissionsApi);
}
return { record, status };
}),
/**
* Subscribes to records based on the given filter and emits events to the `subscriptionHandler`.
*
* @param request must include the `message` with the subscription filter and the `subscriptionHandler` to process the events.
* @returns the subscription status and the subscription object used to close the subscription.
*/
subscribe: (request) => __awaiter(this, void 0, void 0, function* () {
const agentRequest = {
/**
* The `author` is the DID that will sign the message and must be the DID the Web5 app is
* connected with and is authorized to access the signing private key of.
*/
author: this.connectedDid,
messageParams: request.message,
messageType: DwnInterface.RecordsSubscribe,
/**
* The `target` is the DID of the DWN tenant under which the subscribe operation will be executed.
* If `from` is provided, the subscribe operation will be executed on a remote DWN.
* Otherwise, the local DWN will execute the subscribe operation.
*/
target: request.from || this.connectedDid,
/**
* The handler to process the subscription events.
*/
subscriptionHandler: SubscriptionUtil.recordSubscriptionHandler({
agent: this.agent,
connectedDid: this.connectedDid,
delegateDid: this.delegateDid,
permissionsApi: this.permissionsApi,
protocolRole: request.message.protocolRole,
request
})
};
if (this.delegateDid) {
// if we don't find a delegated grant, we will attempt to subscribe signing as the delegated DID
// This is to allow the API caller to subscribe to public records without needing to impersonate the delegate.
//
// NOTE: When a read-only DwnApi is implemented, callers should use that instead when they don't have an explicit permission.
// This should fail if a permission is not found.
// TODO: https://github.com/TBD54566975/web5-js/issues/898
try {
const { message: delegatedGrant } = yield this.permissionsApi.getPermissionForRequest({
connectedDid: this.connectedDid,
delegateDid: this.delegateDid,
protocol: request.protocol,
delegate: true,
cached: true,
messageType: agentRequest.messageType
});
agentRequest.messageParams = Object.assign(Object.assign({}, agentRequest.messageParams), { delegatedGrant });
agentRequest.granteeDid = this.delegateDid;
}
catch (_error) {
// if a grant is not found, we should author the request as the delegated DID to get public records
agentRequest.author = this.delegateDid;
}
}
;
let agentResponse;
if (request.from) {
agentResponse = yield this.agent.sendDwnRequest(agentRequest);
}
else {
agentResponse = yield this.agent.processDwnRequest(agentRequest);
}
const reply = agentResponse.reply;
const { status, subscription } = reply;
return { status, subscription };
}),
/**
* Writes a record to the DWN
*
* As a convenience, the Record instance returned will cache a copy of the data. This is done
* to maintain consistency with other DWN methods, like RecordsQuery, that include relatively
* small data payloads when returning RecordsWrite message properties. Regardless of data
* size, methods such as `record.data.stream()` will return the data when called even if it
* requires fetching from the DWN datastore.
*/
write: (request) => __awaiter(this, void 0, void 0, function* () {
var _c;
const { dataBlob, dataFormat } = dataToBlob(request.data, (_c = request.message) === null || _c === void 0 ? void 0 : _c.dataFormat);
const dwnRequestParams = {
store: request.store,
messageType: DwnInterface.RecordsWrite,
messageParams: Object.assign(Object.assign({}, request.message), { dataFormat }),
author: this.connectedDid,
target: this.connectedDid,
dataStream: dataBlob
};
// if impersonation is enabled, fetch the delegated grant to use with the write operation
if (this.delegateDid) {
const { message: delegatedGrant } = yield this.permissionsApi.getPermissionForRequest({
connectedDid: this.connectedDid,
delegateDid: this.delegateDid,
protocol: request.message.protocol,
delegate: true,
cached: true,
messageType: dwnRequestParams.messageType
});
dwnRequestParams.messageParams = Object.assign(Object.assign({}, dwnRequestParams.messageParams), { delegatedGrant });
dwnRequestParams.granteeDid = this.delegateDid;
}
;
const agentResponse = yield this.agent.processDwnRequest(dwnRequestParams);
const { message: responseMessage, reply: { status } } = agentResponse;
let record;
if (200 <= status.code && status.code <= 299) {
const recordOptions = Object.assign({
/**
* Assume the author is the connected DID since the record was just written to the
* local DWN.
*/
author: this.connectedDid,
/**
* Set the `connectedDid` to currently connected DID so that subsequent calls to
* {@link Record} instance methods, such as `record.update()` are executed on the
* local DWN.
*/
connectedDid: this.connectedDid, encodedData: dataBlob, delegateDid: this.delegateDid }, responseMessage);
record = new Record(this.agent, recordOptions, this.permissionsApi);
}
return { record, status };
}),
};
}
}
//# sourceMappingURL=dwn-api.js.map