UNPKG

@enbox/api

Version:

SDK for accessing the features and capabilities of Web5

573 lines 32.2 kB
/** * 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