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/messages/records-write.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';
import { DwnInterfaceName, DwnMethodName, Message } from './message.js';
import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
const methodToAllowedActionMap = {
    [DwnMethodName.Write]: ProtocolAction.Write,
    [DwnMethodName.Read]: ProtocolAction.Read,
};
export class ProtocolAuthorization {
    /**
     * Performs protocol-based authorization against the given message.
     * @throws {Error} if authorization fails.
     */
    static authorize(tenant, incomingMessage, author, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            // fetch ancestor message chain
            const ancestorMessageChain = yield ProtocolAuthorization.constructAncestorMessageChain(tenant, incomingMessage, messageStore);
            // fetch the protocol definition
            const protocolDefinition = yield ProtocolAuthorization.fetchProtocolDefinition(tenant, incomingMessage, ancestorMessageChain, messageStore);
            // verify declared protocol type exists in protocol and that it conforms to type specification
            ProtocolAuthorization.verifyType(incomingMessage.message, protocolDefinition.types);
            // validate `protocolPath`
            ProtocolAuthorization.verifyProtocolPath(incomingMessage, ancestorMessageChain);
            // get the rule set for the inbound message
            const inboundMessageRuleSet = ProtocolAuthorization.getRuleSet(incomingMessage.message, protocolDefinition, ancestorMessageChain);
            // verify method invoked against the allowed actions
            ProtocolAuthorization.verifyAllowedActions(tenant, author, incomingMessage.message.descriptor.method, inboundMessageRuleSet, ancestorMessageChain);
            // verify allowed condition of incoming message
            yield ProtocolAuthorization.verifyActionCondition(tenant, incomingMessage, messageStore);
        });
    }
    /**
     * Fetches the protocol definition based on the protocol specified in the given message.
     */
    static fetchProtocolDefinition(tenant, incomingMessage, ancestorMessageChain, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            // get the protocol URI
            let protocolUri;
            if (incomingMessage.message.descriptor.method === DwnMethodName.Write) {
                protocolUri = incomingMessage.message.descriptor.protocol;
            }
            else {
                protocolUri = ancestorMessageChain[ancestorMessageChain.length - 1].descriptor.protocol;
            }
            // fetch the corresponding protocol definition
            const query = {
                interface: DwnInterfaceName.Protocols,
                method: DwnMethodName.Configure,
                protocol: protocolUri
            };
            const protocols = yield messageStore.query(tenant, query);
            if (protocols.length === 0) {
                throw new Error(`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, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            const ancestorMessageChain = [];
            // Get first RecordsWrite in ancestor chain, or use incoming write message
            let recordsWrite;
            if (incomingMessage.message.descriptor.method === DwnMethodName.Write) {
                recordsWrite = incomingMessage;
            }
            else {
                const recordsRead = incomingMessage;
                const query = {
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    recordId: recordsRead.message.descriptor.recordId,
                };
                const existingMessages = yield messageStore.query(tenant, query);
                const recordsWriteMessage = yield RecordsWrite.getNewestMessage(existingMessages);
                recordsWrite = yield RecordsWrite.parse(recordsWriteMessage);
                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 parentMessages = yield messageStore.query(tenant, query);
                if (parentMessages.length === 0) {
                    throw new Error(`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(inboundMessage, protocolDefinition, ancestorMessageChain) {
        let protocolPath;
        if (inboundMessage.descriptor.method === DwnMethodName.Write) {
            protocolPath = inboundMessage.descriptor.protocolPath;
        }
        else {
            protocolPath = ancestorMessageChain[ancestorMessageChain.length - 1].descriptor.protocolPath;
        }
        const protocolPathArray = protocolPath.split('/');
        // traverse rule sets using protocolPath
        let currentRuleSet = protocolDefinition.structure;
        let i = 0;
        while (i < protocolPathArray.length) {
            const currentTypeName = protocolPathArray[i];
            const nextRuleSet = currentRuleSet[currentTypeName];
            if (nextRuleSet === undefined) {
                const partialProtocolPath = protocolPathArray.slice(0, i + 1).join('/');
                throw new DwnError(DwnErrorCode.ProtocolAuthorizationMissingRuleSet, `No rule set defined for protocolPath ${partialProtocolPath}`);
            }
            currentRuleSet = nextRuleSet;
            i++;
        }
        return currentRuleSet;
    }
    /**
     * 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(inboundMessage, ancestorMessageChain) {
        // skip verification if this is not a RecordsWrite
        if (inboundMessage.message.descriptor.method !== DwnMethodName.Write) {
            return;
        }
        const declaredProtocolPath = inboundMessage.message.descriptor.protocolPath;
        const declaredTypeName = ProtocolAuthorization.getTypeName(declaredProtocolPath);
        let ancestorProtocolPath = '';
        for (const ancestor of ancestorMessageChain) {
            const protocolPath = ancestor.descriptor.protocolPath;
            const ancestorTypeName = ProtocolAuthorization.getTypeName(protocolPath);
            ancestorProtocolPath += `${ancestorTypeName}/`; // e.g. `foo/bar/`, notice the trailing slash
        }
        const actualProtocolPath = ancestorProtocolPath + declaredTypeName; // e.g. `foo/bar/baz`
        if (declaredProtocolPath !== actualProtocolPath) {
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationIncorrectProtocolPath, `Declared protocol path '${declaredProtocolPath}' is not the same as actual protocol path '${actualProtocolPath}'.`);
        }
    }
    /**
     * 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) {
        // skip verification if this is not a RecordsWrite
        if (inboundMessage.descriptor.method !== DwnMethodName.Write) {
            return;
        }
        const recordsWriteMessage = inboundMessage;
        const typeNames = Object.keys(protocolTypes);
        const declaredProtocolPath = recordsWriteMessage.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 = recordsWriteMessage.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 } = recordsWriteMessage.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 } = recordsWriteMessage.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}'`);
        }
    }
    /**
     * Verifies the actions specified in the given message matches the allowed actions in the rule set.
     * @throws {Error} if action not allowed.
     */
    static verifyAllowedActions(tenant, author, incomingMessageMethod, inboundMessageRuleSet, ancestorMessageChain) {
        const inboundMessageAction = methodToAllowedActionMap[incomingMessageMethod];
        const actionRules = inboundMessageRuleSet.$actions;
        if (actionRules === undefined) {
            // if no action rule is defined, owner of DWN can do everything
            if (author === tenant) {
                return;
            }
            else {
                throw new Error(`no action rule defined for ${incomingMessageMethod}, ${author} is unauthorized`);
            }
        }
        for (const actionRule of actionRules) {
            if (actionRule.can !== inboundMessageAction) {
                continue;
            }
            switch (actionRule.who) {
                case ProtocolActor.Anyone:
                    return;
                case ProtocolActor.Author:
                    const messageForAuthorCheck = ProtocolAuthorization.getMessage(ancestorMessageChain, actionRule.of);
                    if (messageForAuthorCheck !== undefined) {
                        const expectedAuthor = Message.getAuthor(messageForAuthorCheck);
                        if (author === expectedAuthor) {
                            return;
                        }
                    }
                    break;
                case ProtocolActor.Recipient:
                    const messageForRecipientCheck = ProtocolAuthorization.getMessage(ancestorMessageChain, actionRule.of);
                    if (messageForRecipientCheck !== undefined) {
                        const expectedAuthor = messageForRecipientCheck.descriptor.recipient;
                        if (author === expectedAuthor) {
                            return;
                        }
                    }
                    break;
                // default:
                //    JSON schema validations ensure that there are no other cases
            }
        }
        // No action rules were satisfied, author is not authorized
        throw new DwnError(DwnErrorCode.ProtocolAuthorizationActionNotAllowed, `inbound message action ${inboundMessageAction} not allowed for author`);
    }
    /**
     * Verifies if the desired action can be taken.
     * Currently the only check is: if the write is not the initial write, the author must be the same as the initial write
     * @throws {Error} if fails verification
     */
    static verifyActionCondition(tenant, incomingMessage, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            if (incomingMessage.message.descriptor.method === DwnMethodName.Read) {
                // Currently no conditions for reads
            }
            else if (incomingMessage.message.descriptor.method === DwnMethodName.Write) {
                const recordsWrite = incomingMessage;
                const isInitialWrite = yield recordsWrite.isInitialWrite();
                if (!isInitialWrite) {
                    // fetch the initialWrite
                    const query = {
                        entryId: recordsWrite.message.recordId
                    };
                    const result = yield messageStore.query(tenant, query);
                    // check the author of the initial write matches the author of the incoming message
                    const initialWrite = result[0];
                    const authorOfInitialWrite = Message.getAuthor(initialWrite);
                    if (recordsWrite.author !== authorOfInitialWrite) {
                        throw new Error(`author of incoming message '${recordsWrite.author}' must match to author of initial write '${authorOfInitialWrite}'`);
                    }
                }
            }
        });
    }
    /**
     * Gets the message from the message chain based on the path specified.
     * Returns undefined if matching message does not existing in ancestor chain
     * @param protocolPath `/` delimited path starting from the root ancestor.
     *                    Each path segment denotes the expected record type declared in protocol definition.
     *                    e.g. `A/B/C` means that the root ancestor must be of type A, its child must be of type B, followed by a child of type C.
     *                    NOTE: the path scheme use here may be temporary dependent on final protocol spec.
     */
    static getMessage(ancestorMessageChain, protocolPath) {
        const expectedAncestors = protocolPath.split('/');
        // consider moving this check to ProtocolsConfigure message ingestion
        if (expectedAncestors.length > ancestorMessageChain.length) {
            return undefined;
        }
        let i = 0;
        while (true) {
            const expectedDefinitionId = expectedAncestors[i];
            const ancestorMessage = ancestorMessageChain[i];
            const actualDefinitionId = ProtocolAuthorization.getTypeName(ancestorMessage.descriptor.protocolPath);
            if (actualDefinitionId !== expectedDefinitionId) {
                throw new Error(`mismatching record schema: expecting ${expectedDefinitionId} but actual ${actualDefinitionId}`);
            }
            // we have found the message if we are looking at the last message specified by the path
            if (i + 1 === expectedAncestors.length) {
                return ancestorMessage;
            }
            i++;
        }
    }
    static getTypeName(protocolPath) {
        return protocolPath.split('/').slice(-1)[0];
    }
}
//# sourceMappingURL=protocol-authorization.js.map