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 { RecordsWrite } from '../interfaces/records-write.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from './message.js';
import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
export class ProtocolAuthorization {
    /**
     * Performs validation on the structure of RecordsWrite messages that use a protocol.
     * @throws {Error} if validation fails.
     */
    static validateReferentialIntegrity(tenant, incomingMessage, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            // fetch the protocol definition
            const protocolDefinition = yield ProtocolAuthorization.fetchProtocolDefinition(tenant, incomingMessage.message.descriptor.protocol, messageStore);
            // verify declared protocol type exists in protocol and that it conforms to type specification
            ProtocolAuthorization.verifyType(incomingMessage.message, protocolDefinition.types);
            // validate `protocolPath`
            yield ProtocolAuthorization.verifyProtocolPath(tenant, incomingMessage, messageStore);
            // get the rule set for the inbound message
            const inboundMessageRuleSet = ProtocolAuthorization.getRuleSet(incomingMessage.message.descriptor.protocolPath, protocolDefinition);
            // If the incoming message is writing a $globalRole record, validate that the recipient is unique
            yield ProtocolAuthorization.verifyUniqueRoleRecipient(tenant, incomingMessage, inboundMessageRuleSet, messageStore);
        });
    }
    /**
     * Performs protocol-based authorization against the incoming RecordsWrite message.
     * @throws {Error} if authorization fails.
     */
    static authorizeWrite(tenant, incomingMessage, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            // fetch ancestor message chain
            const ancestorMessageChain = yield ProtocolAuthorization.constructAncestorMessageChain(tenant, incomingMessage, incomingMessage, messageStore);
            // fetch the protocol definition
            const protocolDefinition = yield ProtocolAuthorization.fetchProtocolDefinition(tenant, incomingMessage.message.descriptor.protocol, messageStore);
            // get the rule set for the inbound message
            const inboundMessageRuleSet = ProtocolAuthorization.getRuleSet(incomingMessage.message.descriptor.protocolPath, protocolDefinition);
            // If the incoming message has `protocolRole` in the descriptor, validate the invoked role
            yield ProtocolAuthorization.verifyInvokedRole(tenant, incomingMessage, incomingMessage.message.descriptor.protocol, incomingMessage.message.contextId, protocolDefinition, messageStore);
            // verify method invoked against the allowed actions
            yield ProtocolAuthorization.verifyAllowedActions(tenant, incomingMessage, inboundMessageRuleSet, ancestorMessageChain, messageStore);
        });
    }
    /**
     * Performs protocol-based authorization against the incoming RecordsRead  message.
     * @param newestRecordsWrite Either the incomingMessage itself if the incoming is a RecordsWrite,
     *                     or the latest RecordsWrite associated with the recordId being read.
     * @throws {Error} if authorization fails.
     */
    static authorizeRead(tenant, incomingMessage, newestRecordsWrite, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            // fetch ancestor message chain
            const ancestorMessageChain = yield ProtocolAuthorization.constructAncestorMessageChain(tenant, incomingMessage, newestRecordsWrite, messageStore);
            // fetch the protocol definition
            const protocolDefinition = yield ProtocolAuthorization.fetchProtocolDefinition(tenant, newestRecordsWrite.message.descriptor.protocol, messageStore);
            // get the rule set for the inbound message
            const inboundMessageRuleSet = ProtocolAuthorization.getRuleSet(newestRecordsWrite.message.descriptor.protocolPath, protocolDefinition);
            // If the incoming message has `protocolRole` in the descriptor, validate the invoked role
            yield ProtocolAuthorization.verifyInvokedRole(tenant, incomingMessage, newestRecordsWrite.message.descriptor.protocol, newestRecordsWrite.message.contextId, protocolDefinition, messageStore);
            // verify method invoked against the allowed actions
            yield ProtocolAuthorization.verifyAllowedActions(tenant, incomingMessage, inboundMessageRuleSet, ancestorMessageChain, messageStore);
        });
    }
    /**
     * Performs protocol-based authorization against the incoming RecordsQuery message.
     * @throws {Error} if authorization fails.
     */
    static authorizeQuery(tenant, incomingMessage, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            // validate that required properties exist in query filter
            const { protocol, protocolPath, contextId } = incomingMessage.message.descriptor.filter;
            // fetch the protocol definition
            const protocolDefinition = yield ProtocolAuthorization.fetchProtocolDefinition(tenant, protocol, // authorizeQuery` is only called if `protocol` is present
            messageStore);
            // get the rule set for the inbound message
            const inboundMessageRuleSet = ProtocolAuthorization.getRuleSet(protocolPath, // presence of `protocolPath` is verified in `parse()`
            protocolDefinition);
            // If the incoming message has `protocolRole` in the descriptor, validate the invoked role
            yield ProtocolAuthorization.verifyInvokedRole(tenant, incomingMessage, protocol, contextId, protocolDefinition, messageStore);
            // verify method invoked against the allowed actions
            yield ProtocolAuthorization.verifyAllowedActions(tenant, incomingMessage, inboundMessageRuleSet, [], // ancestor chain is not relevant to queries
            messageStore);
        });
    }
    static authorizeDelete(tenant, incomingMessage, newestRecordsWrite, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            // fetch ancestor message chain
            const ancestorMessageChain = yield ProtocolAuthorization.constructAncestorMessageChain(tenant, incomingMessage, newestRecordsWrite, messageStore);
            // fetch the protocol definition
            const protocolDefinition = yield ProtocolAuthorization.fetchProtocolDefinition(tenant, newestRecordsWrite.message.descriptor.protocol, messageStore);
            // get the rule set for the inbound message
            const inboundMessageRuleSet = ProtocolAuthorization.getRuleSet(newestRecordsWrite.message.descriptor.protocolPath, protocolDefinition);
            // If the incoming message has `protocolRole` in the descriptor, validate the invoked role
            yield ProtocolAuthorization.verifyInvokedRole(tenant, incomingMessage, newestRecordsWrite.message.descriptor.protocol, newestRecordsWrite.message.contextId, protocolDefinition, messageStore);
            // verify method invoked against the allowed actions
            yield ProtocolAuthorization.verifyAllowedActions(tenant, incomingMessage, inboundMessageRuleSet, ancestorMessageChain, messageStore);
        });
    }
    /**
     * Fetches the protocol definition based on the protocol specified in the given message.
     */
    static fetchProtocolDefinition(tenant, protocolUri, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            // fetch the corresponding protocol definition
            const query = {
                interface: DwnInterfaceName.Protocols,
                method: DwnMethodName.Configure,
                protocol: protocolUri
            };
            const { messages: protocols } = yield messageStore.query(tenant, [query]);
            if (protocols.length === 0) {
                throw new DwnError(DwnErrorCode.ProtocolAuthorizationProtocolNotFound, `unable to find protocol definition for ${protocolUri}`);
            }
            const protocolMessage = protocols[0];
            return protocolMessage.descriptor.definition;
        });
    }
    /**
     * Constructs a chain of ancestor messages
     * @returns the ancestor chain of messages where the first element is the root of the chain; returns empty array if no parent is specified.
     */
    static constructAncestorMessageChain(tenant, incomingMessage, recordsWrite, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            const ancestorMessageChain = [];
            if (incomingMessage.message.descriptor.method !== DwnMethodName.Write) {
                // Unless inboundMessage is a Write, recordsWrite is also an ancestor message
                ancestorMessageChain.push(recordsWrite.message);
            }
            const protocol = recordsWrite.message.descriptor.protocol;
            const contextId = recordsWrite.message.contextId;
            // keep walking up the chain from the inbound message's parent, until there is no more parent
            let currentParentId = recordsWrite.message.descriptor.parentId;
            while (currentParentId !== undefined) {
                // fetch parent
                const query = {
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    protocol,
                    contextId,
                    recordId: currentParentId
                };
                const { messages: parentMessages } = yield messageStore.query(tenant, [query]);
                // We already check the immediate parent in `verifyProtocolPath`, so if it triggers,
                // it means a bug that caused an invalid message to be saved to the DWN.
                if (parentMessages.length === 0) {
                    throw new DwnError(DwnErrorCode.ProtocolAuthorizationParentNotFound, `no parent found with ID ${currentParentId}`);
                }
                const parent = parentMessages[0];
                ancestorMessageChain.push(parent);
                currentParentId = parent.descriptor.parentId;
            }
            return ancestorMessageChain.reverse(); // root ancestor first
        });
    }
    /**
     * Gets the rule set corresponding to the given message chain.
     */
    static getRuleSet(protocolPath, protocolDefinition) {
        const ruleSet = ProtocolAuthorization.getRuleSetAtProtocolPath(protocolPath, protocolDefinition);
        if (ruleSet === undefined) {
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationMissingRuleSet, `No rule set defined for protocolPath ${protocolPath}`);
        }
        return ruleSet;
    }
    /**
     * Verifies the `protocolPath` declared in the given message (if it is a RecordsWrite) matches the path of actual ancestor chain.
     * @throws {DwnError} if fails verification.
     */
    static verifyProtocolPath(tenant, inboundMessage, messageStore) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const declaredProtocolPath = inboundMessage.message.descriptor.protocolPath;
            const declaredTypeName = ProtocolAuthorization.getTypeName(declaredProtocolPath);
            const parentId = inboundMessage.message.descriptor.parentId;
            if (parentId === undefined) {
                if (declaredProtocolPath !== declaredTypeName) {
                    throw new DwnError(DwnErrorCode.ProtocolAuthorizationParentlessIncorrectProtocolPath, `Declared protocol path '${declaredProtocolPath}' is not valid for records with no parentId'.`);
                }
            }
            else {
                const protocol = inboundMessage.message.descriptor.protocol;
                const contextId = inboundMessage.message.contextId;
                const query = {
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    protocol,
                    contextId,
                    recordId: parentId
                };
                const { messages: parentMessages } = yield messageStore.query(tenant, [query]);
                const parentProtocolPath = (_b = (_a = parentMessages[0]) === null || _a === void 0 ? void 0 : _a.descriptor) === null || _b === void 0 ? void 0 : _b.protocolPath;
                const actualProtocolPath = `${parentProtocolPath}/${declaredTypeName}`;
                if (parentProtocolPath === undefined || actualProtocolPath !== declaredProtocolPath) {
                    throw new DwnError(DwnErrorCode.ProtocolAuthorizationIncorrectProtocolPath, `Could not find matching parent record to verify declared protocol path '${declaredProtocolPath}'.`);
                }
            }
        });
    }
    /**
     * Verifies the `dataFormat` and `schema` declared in the given message (if it is a RecordsWrite) matches dataFormat
     * and schema of the type in the given protocol.
     * @throws {DwnError} if fails verification.
     */
    static verifyType(inboundMessage, protocolTypes) {
        const typeNames = Object.keys(protocolTypes);
        const declaredProtocolPath = inboundMessage.descriptor.protocolPath;
        const declaredTypeName = ProtocolAuthorization.getTypeName(declaredProtocolPath);
        if (!typeNames.includes(declaredTypeName)) {
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationInvalidType, `record with type ${declaredTypeName} not allowed in protocol`);
        }
        const protocolPath = inboundMessage.descriptor.protocolPath;
        // existence of `protocolType` has already been verified
        const typeName = ProtocolAuthorization.getTypeName(protocolPath);
        const protocolType = protocolTypes[typeName];
        // no `schema` specified in protocol definition means that any schema is allowed
        const { schema } = inboundMessage.descriptor;
        if (protocolType.schema !== undefined && protocolType.schema !== schema) {
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationInvalidSchema, `type '${typeName}' must have schema '${protocolType.schema}', \
        instead has '${schema}'`);
        }
        // no `dataFormats` specified in protocol definition means that all dataFormats are allowed
        const { dataFormat } = inboundMessage.descriptor;
        if (protocolType.dataFormats !== undefined && !protocolType.dataFormats.includes(dataFormat)) {
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationIncorrectDataFormat, `type '${typeName}' must have data format in (${protocolType.dataFormats}), \
        instead has '${dataFormat}'`);
        }
    }
    /**
     * Check if the incoming message is invoking a role. If so, validate the invoked role.
     */
    static verifyInvokedRole(tenant, incomingMessage, protocolUri, contextId, protocolDefinition, messageStore) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            const protocolRole = (_a = incomingMessage.signaturePayload) === null || _a === void 0 ? void 0 : _a.protocolRole;
            // Only verify role if there is a role being invoked
            if (protocolRole === undefined) {
                return;
            }
            const roleRuleSet = ProtocolAuthorization.getRuleSetAtProtocolPath(protocolRole, protocolDefinition);
            if (roleRuleSet === undefined || (!roleRuleSet.$globalRole && !roleRuleSet.$contextRole)) {
                throw new DwnError(DwnErrorCode.ProtocolAuthorizationNotARole, `Protocol path ${protocolRole} is not a valid protocolRole`);
            }
            const roleRecordFilter = {
                interface: DwnInterfaceName.Records,
                method: DwnMethodName.Write,
                protocol: protocolUri,
                protocolPath: protocolRole,
                recipient: incomingMessage.author,
                isLatestBaseState: true,
            };
            if (roleRuleSet.$contextRole) {
                if (contextId === undefined) {
                    throw new DwnError(DwnErrorCode.ProtocolAuthorizationMissingContextId, 'Could not verify $contextRole because contextId is missing');
                }
                roleRecordFilter.contextId = contextId;
            }
            const { messages: matchingMessages } = yield messageStore.query(tenant, [roleRecordFilter]);
            if (matchingMessages.length === 0) {
                throw new DwnError(DwnErrorCode.ProtocolAuthorizationMissingRole, `No role record found for protocol path ${protocolRole}`);
            }
        });
    }
    /**
     * Returns a list of ProtocolAction(s) based on the incoming message, one of which must be allowed for the message to be authorized.
     */
    static getActionsSeekingARuleMatch(tenant, incomingMessage, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            switch (incomingMessage.message.descriptor.method) {
                case DwnMethodName.Delete:
                    return [ProtocolAction.Delete];
                case DwnMethodName.Query:
                    return [ProtocolAction.Query];
                case DwnMethodName.Read:
                    return [ProtocolAction.Read];
                case DwnMethodName.Write:
                    const incomingRecordsWrite = incomingMessage;
                    if (yield incomingRecordsWrite.isInitialWrite()) {
                        // only 'write' allows initial RecordsWrites; 'update' only applies to subsequent RecordsWrites
                        return [ProtocolAction.Write];
                    }
                    else if (yield incomingRecordsWrite.isAuthoredByInitialRecordAuthor(tenant, messageStore)) {
                        // Both 'update' and 'write' authorize the incoming message
                        return [ProtocolAction.Write, ProtocolAction.Update];
                    }
                    else {
                        // Actors other than the initial record author must be authorized to 'update' the message
                        return [ProtocolAction.Update];
                    }
                // default:
                // not reachable in typescript
            }
        });
    }
    /**
     * Verifies the action (e.g. read/write) specified in the given message matches the allowed actions in the rule set.
     * @throws {Error} if action not allowed.
     */
    static verifyAllowedActions(tenant, incomingMessage, inboundMessageRuleSet, ancestorMessageChain, messageStore) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            const incomingMessageMethod = incomingMessage.message.descriptor.method;
            const inboundMessageActions = yield ProtocolAuthorization.getActionsSeekingARuleMatch(tenant, incomingMessage, messageStore);
            const author = incomingMessage.author;
            const actionRules = inboundMessageRuleSet.$actions;
            // We have already checked that the message is not from tenant, owner, or permissionsGrant
            if (actionRules === undefined) {
                throw new DwnError(DwnErrorCode.ProtocolAuthorizationActionNotAllowed, `no action rule defined for ${incomingMessageMethod}, ${author} is unauthorized`);
            }
            const invokedRole = (_a = incomingMessage.signaturePayload) === null || _a === void 0 ? void 0 : _a.protocolRole;
            for (const actionRule of actionRules) {
                if (!inboundMessageActions.includes(actionRule.can)) {
                    continue;
                }
                if (invokedRole !== undefined) {
                    // When a protocol role is being invoked, we require that there is a matching `role` rule.
                    if (actionRule.role === invokedRole) {
                        // role is successfully invoked
                        return;
                    }
                    else {
                        continue;
                    }
                }
                else if (actionRule.who === ProtocolActor.Anyone) {
                    return;
                }
                else if (author === undefined) {
                    continue;
                }
                const ancestorRuleSuccess = yield ProtocolAuthorization.checkActor(author, actionRule, ancestorMessageChain);
                if (ancestorRuleSuccess) {
                    return;
                }
            }
            // No action rules were satisfied, author is not authorized
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationActionNotAllowed, `inbound message action not allowed for author`);
        });
    }
    /**
     * Verifies that writes to a $globalRole or $contextRole record do not have the same recipient as an existing RecordsWrite
     * to the same $globalRole or the same $contextRole in the same context.
     */
    static verifyUniqueRoleRecipient(tenant, incomingMessage, inboundMessageRuleSet, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            const incomingRecordsWrite = incomingMessage;
            if (!inboundMessageRuleSet.$globalRole && !inboundMessageRuleSet.$contextRole) {
                return;
            }
            const recipient = incomingRecordsWrite.message.descriptor.recipient;
            if (recipient === undefined) {
                throw new DwnError(DwnErrorCode.ProtocolAuthorizationRoleMissingRecipient, 'Role records must have a recipient');
            }
            const protocolPath = incomingRecordsWrite.message.descriptor.protocolPath;
            const filter = {
                interface: DwnInterfaceName.Records,
                method: DwnMethodName.Write,
                isLatestBaseState: true,
                protocol: incomingRecordsWrite.message.descriptor.protocol,
                protocolPath,
                recipient,
            };
            if (inboundMessageRuleSet.$contextRole) {
                filter.contextId = incomingRecordsWrite.message.contextId;
            }
            const { messages: matchingMessages } = yield messageStore.query(tenant, [filter]);
            const matchingRecords = matchingMessages;
            const matchingRecordsExceptIncomingRecordId = matchingRecords.filter((recordsWriteMessage) => recordsWriteMessage.recordId !== incomingRecordsWrite.message.recordId);
            if (matchingRecordsExceptIncomingRecordId.length > 0) {
                if (inboundMessageRuleSet.$globalRole) {
                    throw new DwnError(DwnErrorCode.ProtocolAuthorizationDuplicateGlobalRoleRecipient, `DID '${recipient}' is already recipient of a $globalRole record at protocol path '${protocolPath}`);
                }
                else {
                    // $contextRole
                    throw new DwnError(DwnErrorCode.ProtocolAuthorizationDuplicateContextRoleRecipient, `DID '${recipient}' is already recipient of a $contextRole record at protocol path '${protocolPath} in the same context`);
                }
            }
        });
    }
    static getRuleSetAtProtocolPath(protocolPath, protocolDefinition) {
        const protocolPathArray = protocolPath.split('/');
        let currentRuleSet = protocolDefinition.structure;
        let i = 0;
        while (i < protocolPathArray.length) {
            const currentTypeName = protocolPathArray[i];
            const nextRuleSet = currentRuleSet[currentTypeName];
            if (nextRuleSet === undefined) {
                return undefined;
            }
            currentRuleSet = nextRuleSet;
            i++;
        }
        return currentRuleSet;
    }
    /**
     * Checks if there is a record in the ancestor chain matching the `who: 'author' | 'recipient'` action rule.
     * @returns true if the action rule is satisfied. false otherwise
     */
    static checkActor(author, actionRule, ancestorMessageChain) {
        return __awaiter(this, void 0, void 0, function* () {
            // Iterate up the ancestor chain to find a message with matching protocolPath
            const ancestorRecordsWrite = ancestorMessageChain.find((recordsWriteMessage) => recordsWriteMessage.descriptor.protocolPath === actionRule.of);
            // If this is reached, there is likely an issue with the protocol definition.
            // The protocolPath to the actionRule should start with actionRule.of
            // consider moving this check to ProtocolsConfigure message ingestion
            if (ancestorRecordsWrite === undefined) {
                return false;
            }
            if (actionRule.who === ProtocolActor.Recipient) {
                // Recipient of ancestor message must be the author of the incoming message
                return author === ancestorRecordsWrite.descriptor.recipient;
            }
            else { // actionRule.who === ProtocolActor.Author
                // Author of ancestor message must be the author of the incoming message
                const ancestorAuthor = (yield RecordsWrite.parse(ancestorRecordsWrite)).author;
                return author === ancestorAuthor;
            }
        });
    }
    static getTypeName(protocolPath) {
        return protocolPath.split('/').slice(-1)[0];
    }
}
//# sourceMappingURL=protocol-authorization.js.map