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 { isRecordsType } from './dwn-api.js';
import { mapConcurrent } from './utils.js';
import { Convert, TtlCache } from '@enbox/common';
import { DwnInterface, DwnPermissionGrant, DwnPermissionRequest } from './types/dwn.js';
import { DwnInterfaceName, DwnMethodName, PermissionScopeMatcher, PermissionsProtocol } from '@enbox/dwn-sdk-js';
const REVOCATION_CHECK_CONCURRENCY = 8;
export class AgentPermissionsApi {
    get agent() {
        if (!this._agent) {
            throw new Error('AgentPermissionsApi: Agent is not set');
        }
        return this._agent;
    }
    set agent(agent) {
        this._agent = agent;
    }
    constructor({ agent } = {}) {
        /** cache for fetching a permission {@link PermissionGrant}, keyed by a specific MessageType and protocol */
        this._cachedPermissions = new TtlCache({ ttl: 60 * 1000 });
        this._revokedGrantIds = new Set();
        this._agent = agent;
    }
    getPermissionForRequest(_a) {
        return __awaiter(this, arguments, void 0, function* ({ connectedDid, delegateDid, delegate, messageType, protocol, protocolPath, contextId, cached = false }) {
            const cacheKey = JSON.stringify([connectedDid, delegateDid, messageType, protocol, protocolPath, contextId]);
            const cachedGrant = cached ? this._cachedPermissions.get(cacheKey) : undefined;
            if (cachedGrant) {
                return cachedGrant;
            }
            const permissionGrants = yield this.fetchGrants({
                author: delegateDid,
                target: delegateDid,
                grantor: connectedDid,
                grantee: delegateDid,
                checkRevoked: true,
            });
            // get the delegate grants that match the messageParams and are associated with the connectedDid as the grantor
            const grant = yield AgentPermissionsApi.matchGrantFromArray(connectedDid, delegateDid, { messageType, protocol, protocolPath, contextId }, permissionGrants, delegate);
            if (!grant) {
                throw new Error(`CachedPermissions: No permissions found for ${messageType}: ${[protocol, protocolPath, contextId].filter(Boolean).join('/') || undefined}`);
            }
            this._cachedPermissions.set(cacheKey, grant);
            return grant;
        });
    }
    fetchGrants(_a) {
        return __awaiter(this, arguments, void 0, function* ({ author, target, grantee, grantor, protocol, remote = false, checkRevoked = false, }) {
            // filter by a protocol using tags if provided
            const tags = protocol ? { protocol } : undefined;
            const params = {
                author: author,
                target: target,
                messageType: DwnInterface.RecordsQuery,
                messageParams: {
                    filter: {
                        author: grantor, // the author of the grant would be the grantor
                        recipient: grantee, // the recipient of the grant would be the grantee
                        protocol: PermissionsProtocol.uri,
                        protocolPath: PermissionsProtocol.grantPath,
                        tags
                    }
                }
            };
            const { reply } = remote ? yield this.agent.sendDwnRequest(params) : yield this.agent.processDwnRequest(params);
            if (reply.status.code !== 200) {
                throw new Error(`PermissionsApi: Failed to fetch grants: ${reply.status.detail}`);
            }
            const grantMessages = reply.entries;
            // Revocation filtering is opt-in and intentionally checks one grant at a
            // time. If this becomes hot, add a dedicated revocation-read path instead
            // of broad nested queries or parentId set filters.
            const revokedGrantIds = checkRevoked
                ? yield this.fetchRevokedGrantIds({ author, target, grantMessages, remote })
                : new Set();
            const grants = [];
            for (const entry of grantMessages) {
                if (revokedGrantIds.has(entry.recordId)) {
                    continue;
                }
                const grant = DwnPermissionGrant.parse(entry);
                grants.push({ grant, message: entry });
            }
            return grants;
        });
    }
    /**
     * Fetches the grant record IDs that have a stored revocation.
     */
    fetchRevokedGrantIds(_a) {
        return __awaiter(this, arguments, void 0, function* ({ author, target, grantMessages, remote }) {
            if (grantMessages.length === 0) {
                return new Set();
            }
            const revokedGrantIds = new Set();
            yield mapConcurrent(grantMessages, REVOCATION_CHECK_CONCURRENCY, (grantMessage) => __awaiter(this, void 0, void 0, function* () {
                if (this._revokedGrantIds.has(grantMessage.recordId)) {
                    revokedGrantIds.add(grantMessage.recordId);
                    return;
                }
                const revoked = yield this.isGrantRevoked({
                    author,
                    target,
                    grantRecordId: grantMessage.recordId,
                    remote,
                });
                if (revoked) {
                    revokedGrantIds.add(grantMessage.recordId);
                }
            }));
            return revokedGrantIds;
        });
    }
    fetchRequests(_a) {
        return __awaiter(this, arguments, void 0, function* ({ author, target, protocol, remote = false }) {
            // filter by a protocol using tags if provided
            const tags = protocol ? { protocol } : undefined;
            const params = {
                author: author,
                target: target,
                messageType: DwnInterface.RecordsQuery,
                messageParams: {
                    filter: {
                        protocol: PermissionsProtocol.uri,
                        protocolPath: PermissionsProtocol.requestPath,
                        tags
                    }
                }
            };
            const { reply } = remote ? yield this.agent.sendDwnRequest(params) : yield this.agent.processDwnRequest(params);
            if (reply.status.code !== 200) {
                throw new Error(`PermissionsApi: Failed to fetch requests: ${reply.status.detail}`);
            }
            const requests = [];
            for (const entry of reply.entries) {
                const request = DwnPermissionRequest.parse(entry);
                requests.push({ request, message: entry });
            }
            return requests;
        });
    }
    isGrantRevoked(_a) {
        return __awaiter(this, arguments, void 0, function* ({ author, target, grantRecordId, remote = false }) {
            if (this._revokedGrantIds.has(grantRecordId)) {
                return true;
            }
            const params = {
                author,
                target,
                messageType: DwnInterface.RecordsRead,
                messageParams: {
                    filter: {
                        parentId: grantRecordId,
                        protocol: PermissionsProtocol.uri,
                        protocolPath: PermissionsProtocol.revocationPath,
                    }
                }
            };
            const { reply: revocationReply } = remote ? yield this.agent.sendDwnRequest(params) : yield this.agent.processDwnRequest(params);
            if (revocationReply.status.code === 404) {
                // no revocation found, the grant is not revoked
                return false;
            }
            else if (revocationReply.status.code === 200) {
                // a revocation was found, the grant is revoked
                this._revokedGrantIds.add(grantRecordId);
                return true;
            }
            throw new Error(`PermissionsApi: Failed to check if grant is revoked: ${revocationReply.status.detail}`);
        });
    }
    createGrant(params) {
        return __awaiter(this, void 0, void 0, function* () {
            const { author, store = false, delegated = false } = params, createGrantParams = __rest(params, ["author", "store", "delegated"]);
            let tags = undefined;
            if (PermissionsProtocol.hasProtocolScope(createGrantParams.scope)) {
                tags = { protocol: createGrantParams.scope.protocol };
            }
            const permissionGrantData = {
                dateExpires: createGrantParams.dateExpires,
                requestId: createGrantParams.requestId,
                description: createGrantParams.description,
                delegated,
                scope: createGrantParams.scope,
            };
            if (createGrantParams.connectSession) {
                permissionGrantData.connectSession = createGrantParams.connectSession;
            }
            const permissionsGrantBytes = Convert.object(permissionGrantData).toUint8Array();
            const messageParams = {
                recipient: createGrantParams.grantedTo,
                protocol: PermissionsProtocol.uri,
                protocolPath: PermissionsProtocol.grantPath,
                dataFormat: 'application/json',
                tags
            };
            const { reply, message } = yield this.agent.processDwnRequest({
                store,
                author,
                target: author,
                messageType: DwnInterface.RecordsWrite,
                messageParams,
                dataStream: new Blob([permissionsGrantBytes])
            });
            if (reply.status.code !== 202) {
                throw new Error(`PermissionsApi: Failed to create grant: ${reply.status.detail}`);
            }
            const dataEncodedMessage = Object.assign(Object.assign({}, message), { encodedData: Convert.uint8Array(permissionsGrantBytes).toBase64Url() });
            const grant = DwnPermissionGrant.parse(dataEncodedMessage);
            return { grant, message: dataEncodedMessage };
        });
    }
    createRequest(params) {
        return __awaiter(this, void 0, void 0, function* () {
            const { author, store = false, delegated = false } = params, createGrantParams = __rest(params, ["author", "store", "delegated"]);
            let tags = undefined;
            if (PermissionsProtocol.hasProtocolScope(createGrantParams.scope)) {
                tags = { protocol: createGrantParams.scope.protocol };
            }
            const permissionRequestData = {
                description: createGrantParams.description,
                delegated,
                scope: createGrantParams.scope
            };
            const permissionRequestBytes = Convert.object(permissionRequestData).toUint8Array();
            const messageParams = {
                protocol: PermissionsProtocol.uri,
                protocolPath: PermissionsProtocol.requestPath,
                dataFormat: 'application/json',
                tags
            };
            const { reply, message } = yield this.agent.processDwnRequest({
                store,
                author,
                target: author,
                messageType: DwnInterface.RecordsWrite,
                messageParams,
                dataStream: new Blob([permissionRequestBytes])
            });
            if (reply.status.code !== 202) {
                throw new Error(`PermissionsApi: Failed to create request: ${reply.status.detail}`);
            }
            const dataEncodedMessage = Object.assign(Object.assign({}, message), { encodedData: Convert.uint8Array(permissionRequestBytes).toBase64Url() });
            const request = DwnPermissionRequest.parse(dataEncodedMessage);
            return { request, message: dataEncodedMessage };
        });
    }
    createRevocation(params) {
        return __awaiter(this, void 0, void 0, function* () {
            const { author, store = false, grant, description } = params;
            const revokeData = { description };
            const permissionRevocationBytes = Convert.object(revokeData).toUint8Array();
            let tags = undefined;
            if (PermissionsProtocol.hasProtocolScope(grant.scope)) {
                tags = { protocol: grant.scope.protocol };
            }
            const messageParams = {
                parentContextId: grant.id,
                protocol: PermissionsProtocol.uri,
                protocolPath: PermissionsProtocol.revocationPath,
                dataFormat: 'application/json',
                tags
            };
            if (params.permissionGrantId) {
                messageParams.permissionGrantId = params.permissionGrantId;
            }
            const { reply, message } = yield this.agent.processDwnRequest({
                store,
                author,
                target: author,
                messageType: DwnInterface.RecordsWrite,
                messageParams,
                granteeDid: params.granteeDid,
                dataStream: new Blob([permissionRevocationBytes])
            });
            if (reply.status.code !== 202) {
                throw new Error(`PermissionsApi: Failed to create revocation: ${reply.status.detail}`);
            }
            if (store) {
                this._revokedGrantIds.add(grant.id);
            }
            const dataEncodedMessage = Object.assign(Object.assign({}, message), { encodedData: Convert.uint8Array(permissionRevocationBytes).toBase64Url() });
            return { message: dataEncodedMessage };
        });
    }
    clear() {
        return __awaiter(this, void 0, void 0, function* () {
            this._cachedPermissions.clear();
            this._revokedGrantIds.clear();
        });
    }
    /**
     * Matches the appropriate grant from an array of grants based on the provided parameters.
     *
     * @param delegated if true, only delegated grants are turned, if false all grants are returned including delegated ones.
     */
    static matchGrantFromArray(grantor_1, grantee_1, messageParams_1, grants_1) {
        return __awaiter(this, arguments, void 0, function* (grantor, grantee, messageParams, grants, delegated = false) {
            let bestEntry;
            let bestRank;
            for (const entry of grants) {
                const { grant, message } = entry;
                if (delegated === true && grant.delegated !== true) {
                    continue;
                }
                const { messageType, protocol, protocolPath, contextId } = messageParams;
                if (this.matchScopeFromGrant(grantor, grantee, messageType, grant, protocol, protocolPath, contextId)) {
                    const rank = AgentPermissionsApi.getGrantMatchRank(messageType, grant);
                    if (bestRank === undefined || AgentPermissionsApi.isGrantMatchRankBetter(rank, bestRank)) {
                        bestEntry = { grant, message };
                        bestRank = rank;
                    }
                }
            }
            return bestEntry;
        });
    }
    static getGrantMatchRank(messageType, grant) {
        const scopeMessageType = grant.scope.interface + grant.scope.method;
        return {
            exactMessageType: scopeMessageType === messageType,
            specificity: AgentPermissionsApi.getScopeSpecificity(grant),
        };
    }
    static getScopeSpecificity(grant) {
        const scope = grant.scope;
        if ('protocolPath' in scope && typeof scope.protocolPath === 'string') {
            return 2000 + scope.protocolPath.split('/').length;
        }
        if ('contextId' in scope && typeof scope.contextId === 'string') {
            return 1000 + scope.contextId.split('/').length;
        }
        if ('protocol' in scope && typeof scope.protocol === 'string') {
            return 1;
        }
        return 0;
    }
    static isGrantMatchRankBetter(candidate, current) {
        if (candidate.exactMessageType !== current.exactMessageType) {
            return candidate.exactMessageType;
        }
        return candidate.specificity > current.specificity;
    }
    static matchScopeFromGrant(grantor, grantee, messageType, grant, protocol, protocolPath, contextId) {
        // Check if the grant matches the provided parameters
        if (grant.grantee !== grantee || grant.grantor !== grantor) {
            return false;
        }
        const scope = grant.scope;
        const scopeMessageType = scope.interface + scope.method;
        // Messages.Read is the only valid Messages scope and covers Query, Read, and Subscribe operations.
        // Defensively require method === Read so malformed grants with method Query/Subscribe
        // are rejected rather than treated as valid scopes.
        const isMessagesScopeMatch = scope.interface === DwnInterfaceName.Messages
            ? scope.method === DwnMethodName.Read
                && (messageType === DwnInterface.MessagesQuery
                    || messageType === DwnInterface.MessagesRead
                    || messageType === DwnInterface.MessagesSubscribe)
            : false;
        // Records.Read is the canonical read-like Records scope and covers Query,
        // Read, Subscribe, and Count operations at the agent permission lookup layer.
        const isRecordsReadLikeMessage = messageType === DwnInterface.RecordsCount
            || messageType === DwnInterface.RecordsQuery
            || messageType === DwnInterface.RecordsRead
            || messageType === DwnInterface.RecordsSubscribe;
        let isRecordsScopeMatch = false;
        if (scope.interface === DwnInterfaceName.Records) {
            if (isRecordsReadLikeMessage) {
                isRecordsScopeMatch = scope.method === DwnMethodName.Read;
            }
            else {
                isRecordsScopeMatch = scopeMessageType === messageType;
            }
        }
        const isOtherScopeMatch = scope.interface !== DwnInterfaceName.Messages
            && scope.interface !== DwnInterfaceName.Records
            && scopeMessageType === messageType;
        if (isMessagesScopeMatch || isRecordsScopeMatch || isOtherScopeMatch) {
            if (isRecordsType(messageType)) {
                const recordScope = scope;
                return PermissionScopeMatcher.matches(recordScope, { protocol, protocolPath, contextId });
            }
            else {
                const messagesScope = scope;
                return PermissionScopeMatcher.matches(messagesScope, { protocol, protocolPath, contextId });
            }
        }
        return false;
    }
}
//# sourceMappingURL=permissions-api.js.map