"use strict";
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 __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __values = (this && this.__values) || function(o) {
    var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
    if (m) return m.call(o);
    if (o && typeof o.length === "number") return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
    throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProtocolAuthorization = void 0;
var records_write_js_1 = require("../interfaces/records/messages/records-write.js");
var dwn_error_js_1 = require("./dwn-error.js");
var message_js_1 = require("./message.js");
var protocols_types_js_1 = require("../types/protocols-types.js");
var methodToAllowedActionMap = (_a = {},
    _a[message_js_1.DwnMethodName.Write] = protocols_types_js_1.ProtocolAction.Write,
    _a[message_js_1.DwnMethodName.Read] = protocols_types_js_1.ProtocolAction.Read,
    _a);
var ProtocolAuthorization = /** @class */ (function () {
    function ProtocolAuthorization() {
    }
    /**
     * Performs protocol-based authorization against the given message.
     * @throws {Error} if authorization fails.
     */
    ProtocolAuthorization.authorize = function (tenant, incomingMessage, author, messageStore) {
        return __awaiter(this, void 0, void 0, function () {
            var ancestorMessageChain, protocolDefinition, inboundMessageRuleSet;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, ProtocolAuthorization.constructAncestorMessageChain(tenant, incomingMessage, messageStore)];
                    case 1:
                        ancestorMessageChain = _a.sent();
                        return [4 /*yield*/, ProtocolAuthorization.fetchProtocolDefinition(tenant, incomingMessage, ancestorMessageChain, messageStore)];
                    case 2:
                        protocolDefinition = _a.sent();
                        // 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);
                        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
                        return [4 /*yield*/, ProtocolAuthorization.verifyActionCondition(tenant, incomingMessage, messageStore)];
                    case 3:
                        // verify allowed condition of incoming message
                        _a.sent();
                        return [2 /*return*/];
                }
            });
        });
    };
    /**
     * Fetches the protocol definition based on the protocol specified in the given message.
     */
    ProtocolAuthorization.fetchProtocolDefinition = function (tenant, incomingMessage, ancestorMessageChain, messageStore) {
        return __awaiter(this, void 0, void 0, function () {
            var protocolUri, query, protocols, protocolMessage;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (incomingMessage.message.descriptor.method === message_js_1.DwnMethodName.Write) {
                            protocolUri = incomingMessage.message.descriptor.protocol;
                        }
                        else {
                            protocolUri = ancestorMessageChain[ancestorMessageChain.length - 1].descriptor.protocol;
                        }
                        query = {
                            interface: message_js_1.DwnInterfaceName.Protocols,
                            method: message_js_1.DwnMethodName.Configure,
                            protocol: protocolUri
                        };
                        return [4 /*yield*/, messageStore.query(tenant, query)];
                    case 1:
                        protocols = _a.sent();
                        if (protocols.length === 0) {
                            throw new Error("unable to find protocol definition for ".concat(protocolUri));
                        }
                        protocolMessage = protocols[0];
                        return [2 /*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.
     */
    ProtocolAuthorization.constructAncestorMessageChain = function (tenant, incomingMessage, messageStore) {
        return __awaiter(this, void 0, void 0, function () {
            var ancestorMessageChain, recordsWrite, recordsRead, query, existingMessages, recordsWriteMessage, protocol, contextId, currentParentId, query, parentMessages, parent_1;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        ancestorMessageChain = [];
                        if (!(incomingMessage.message.descriptor.method === message_js_1.DwnMethodName.Write)) return [3 /*break*/, 1];
                        recordsWrite = incomingMessage;
                        return [3 /*break*/, 5];
                    case 1:
                        recordsRead = incomingMessage;
                        query = {
                            interface: message_js_1.DwnInterfaceName.Records,
                            method: message_js_1.DwnMethodName.Write,
                            recordId: recordsRead.message.descriptor.recordId,
                        };
                        return [4 /*yield*/, messageStore.query(tenant, query)];
                    case 2:
                        existingMessages = _a.sent();
                        return [4 /*yield*/, records_write_js_1.RecordsWrite.getNewestMessage(existingMessages)];
                    case 3:
                        recordsWriteMessage = _a.sent();
                        return [4 /*yield*/, records_write_js_1.RecordsWrite.parse(recordsWriteMessage)];
                    case 4:
                        recordsWrite = _a.sent();
                        ancestorMessageChain.push(recordsWrite.message);
                        _a.label = 5;
                    case 5:
                        protocol = recordsWrite.message.descriptor.protocol;
                        contextId = recordsWrite.message.contextId;
                        currentParentId = recordsWrite.message.descriptor.parentId;
                        _a.label = 6;
                    case 6:
                        if (!(currentParentId !== undefined)) return [3 /*break*/, 8];
                        query = {
                            interface: message_js_1.DwnInterfaceName.Records,
                            method: message_js_1.DwnMethodName.Write,
                            protocol: protocol,
                            contextId: contextId,
                            recordId: currentParentId
                        };
                        return [4 /*yield*/, messageStore.query(tenant, query)];
                    case 7:
                        parentMessages = _a.sent();
                        if (parentMessages.length === 0) {
                            throw new Error("no parent found with ID ".concat(currentParentId));
                        }
                        parent_1 = parentMessages[0];
                        ancestorMessageChain.push(parent_1);
                        currentParentId = parent_1.descriptor.parentId;
                        return [3 /*break*/, 6];
                    case 8: return [2 /*return*/, ancestorMessageChain.reverse()]; // root ancestor first
                }
            });
        });
    };
    /**
     * Gets the rule set corresponding to the given message chain.
     */
    ProtocolAuthorization.getRuleSet = function (inboundMessage, protocolDefinition, ancestorMessageChain) {
        var protocolPath;
        if (inboundMessage.descriptor.method === message_js_1.DwnMethodName.Write) {
            protocolPath = inboundMessage.descriptor.protocolPath;
        }
        else {
            protocolPath = ancestorMessageChain[ancestorMessageChain.length - 1].descriptor.protocolPath;
        }
        var protocolPathArray = protocolPath.split('/');
        // traverse rule sets using protocolPath
        var currentRuleSet = protocolDefinition.structure;
        var i = 0;
        while (i < protocolPathArray.length) {
            var currentTypeName = protocolPathArray[i];
            var nextRuleSet = currentRuleSet[currentTypeName];
            if (nextRuleSet === undefined) {
                var partialProtocolPath = protocolPathArray.slice(0, i + 1).join('/');
                throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.ProtocolAuthorizationMissingRuleSet, "No rule set defined for protocolPath ".concat(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.
     */
    ProtocolAuthorization.verifyProtocolPath = function (inboundMessage, ancestorMessageChain) {
        var e_1, _a;
        // skip verification if this is not a RecordsWrite
        if (inboundMessage.message.descriptor.method !== message_js_1.DwnMethodName.Write) {
            return;
        }
        var declaredProtocolPath = inboundMessage.message.descriptor.protocolPath;
        var declaredTypeName = ProtocolAuthorization.getTypeName(declaredProtocolPath);
        var ancestorProtocolPath = '';
        try {
            for (var ancestorMessageChain_1 = __values(ancestorMessageChain), ancestorMessageChain_1_1 = ancestorMessageChain_1.next(); !ancestorMessageChain_1_1.done; ancestorMessageChain_1_1 = ancestorMessageChain_1.next()) {
                var ancestor = ancestorMessageChain_1_1.value;
                var protocolPath = ancestor.descriptor.protocolPath;
                var ancestorTypeName = ProtocolAuthorization.getTypeName(protocolPath);
                ancestorProtocolPath += "".concat(ancestorTypeName, "/"); // e.g. `foo/bar/`, notice the trailing slash
            }
        }
        catch (e_1_1) { e_1 = { error: e_1_1 }; }
        finally {
            try {
                if (ancestorMessageChain_1_1 && !ancestorMessageChain_1_1.done && (_a = ancestorMessageChain_1.return)) _a.call(ancestorMessageChain_1);
            }
            finally { if (e_1) throw e_1.error; }
        }
        var actualProtocolPath = ancestorProtocolPath + declaredTypeName; // e.g. `foo/bar/baz`
        if (declaredProtocolPath !== actualProtocolPath) {
            throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.ProtocolAuthorizationIncorrectProtocolPath, "Declared protocol path '".concat(declaredProtocolPath, "' is not the same as actual protocol path '").concat(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.
     */
    ProtocolAuthorization.verifyType = function (inboundMessage, protocolTypes) {
        // skip verification if this is not a RecordsWrite
        if (inboundMessage.descriptor.method !== message_js_1.DwnMethodName.Write) {
            return;
        }
        var recordsWriteMessage = inboundMessage;
        var typeNames = Object.keys(protocolTypes);
        var declaredProtocolPath = recordsWriteMessage.descriptor.protocolPath;
        var declaredTypeName = ProtocolAuthorization.getTypeName(declaredProtocolPath);
        if (!typeNames.includes(declaredTypeName)) {
            throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.ProtocolAuthorizationInvalidType, "record with type ".concat(declaredTypeName, " not allowed in protocol"));
        }
        var protocolPath = recordsWriteMessage.descriptor.protocolPath;
        // existence of `protocolType` has already been verified
        var typeName = ProtocolAuthorization.getTypeName(protocolPath);
        var protocolType = protocolTypes[typeName];
        // no `schema` specified in protocol definition means that any schema is allowed
        var schema = recordsWriteMessage.descriptor.schema;
        if (protocolType.schema !== undefined && protocolType.schema !== schema) {
            throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.ProtocolAuthorizationInvalidSchema, "type '".concat(typeName, "' must have schema '").concat(protocolType.schema, "',         instead has '").concat(schema, "'"));
        }
        // no `dataFormats` specified in protocol definition means that all dataFormats are allowed
        var dataFormat = recordsWriteMessage.descriptor.dataFormat;
        if (protocolType.dataFormats !== undefined && !protocolType.dataFormats.includes(dataFormat)) {
            throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.ProtocolAuthorizationIncorrectDataFormat, "type '".concat(typeName, "' must have data format in (").concat(protocolType.dataFormats, "),         instead has '").concat(dataFormat, "'"));
        }
    };
    /**
     * Verifies the actions specified in the given message matches the allowed actions in the rule set.
     * @throws {Error} if action not allowed.
     */
    ProtocolAuthorization.verifyAllowedActions = function (tenant, author, incomingMessageMethod, inboundMessageRuleSet, ancestorMessageChain) {
        var e_2, _a;
        var inboundMessageAction = methodToAllowedActionMap[incomingMessageMethod];
        var 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 ".concat(incomingMessageMethod, ", ").concat(author, " is unauthorized"));
            }
        }
        try {
            for (var actionRules_1 = __values(actionRules), actionRules_1_1 = actionRules_1.next(); !actionRules_1_1.done; actionRules_1_1 = actionRules_1.next()) {
                var actionRule = actionRules_1_1.value;
                if (actionRule.can !== inboundMessageAction) {
                    continue;
                }
                switch (actionRule.who) {
                    case protocols_types_js_1.ProtocolActor.Anyone:
                        return;
                    case protocols_types_js_1.ProtocolActor.Author:
                        var messageForAuthorCheck = ProtocolAuthorization.getMessage(ancestorMessageChain, actionRule.of);
                        if (messageForAuthorCheck !== undefined) {
                            var expectedAuthor = message_js_1.Message.getAuthor(messageForAuthorCheck);
                            if (author === expectedAuthor) {
                                return;
                            }
                        }
                        break;
                    case protocols_types_js_1.ProtocolActor.Recipient:
                        var messageForRecipientCheck = ProtocolAuthorization.getMessage(ancestorMessageChain, actionRule.of);
                        if (messageForRecipientCheck !== undefined) {
                            var expectedAuthor = messageForRecipientCheck.descriptor.recipient;
                            if (author === expectedAuthor) {
                                return;
                            }
                        }
                        break;
                    // default:
                    //    JSON schema validations ensure that there are no other cases
                }
            }
        }
        catch (e_2_1) { e_2 = { error: e_2_1 }; }
        finally {
            try {
                if (actionRules_1_1 && !actionRules_1_1.done && (_a = actionRules_1.return)) _a.call(actionRules_1);
            }
            finally { if (e_2) throw e_2.error; }
        }
        // No action rules were satisfied, author is not authorized
        throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.ProtocolAuthorizationActionNotAllowed, "inbound message action ".concat(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
     */
    ProtocolAuthorization.verifyActionCondition = function (tenant, incomingMessage, messageStore) {
        return __awaiter(this, void 0, void 0, function () {
            var recordsWrite, isInitialWrite, query, result, initialWrite, authorOfInitialWrite;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!(incomingMessage.message.descriptor.method === message_js_1.DwnMethodName.Read)) return [3 /*break*/, 1];
                        return [3 /*break*/, 4];
                    case 1:
                        if (!(incomingMessage.message.descriptor.method === message_js_1.DwnMethodName.Write)) return [3 /*break*/, 4];
                        recordsWrite = incomingMessage;
                        return [4 /*yield*/, recordsWrite.isInitialWrite()];
                    case 2:
                        isInitialWrite = _a.sent();
                        if (!!isInitialWrite) return [3 /*break*/, 4];
                        query = {
                            entryId: recordsWrite.message.recordId
                        };
                        return [4 /*yield*/, messageStore.query(tenant, query)];
                    case 3:
                        result = _a.sent();
                        initialWrite = result[0];
                        authorOfInitialWrite = message_js_1.Message.getAuthor(initialWrite);
                        if (recordsWrite.author !== authorOfInitialWrite) {
                            throw new Error("author of incoming message '".concat(recordsWrite.author, "' must match to author of initial write '").concat(authorOfInitialWrite, "'"));
                        }
                        _a.label = 4;
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    /**
     * 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.
     */
    ProtocolAuthorization.getMessage = function (ancestorMessageChain, protocolPath) {
        var expectedAncestors = protocolPath.split('/');
        // consider moving this check to ProtocolsConfigure message ingestion
        if (expectedAncestors.length > ancestorMessageChain.length) {
            return undefined;
        }
        var i = 0;
        while (true) {
            var expectedDefinitionId = expectedAncestors[i];
            var ancestorMessage = ancestorMessageChain[i];
            var actualDefinitionId = ProtocolAuthorization.getTypeName(ancestorMessage.descriptor.protocolPath);
            if (actualDefinitionId !== expectedDefinitionId) {
                throw new Error("mismatching record schema: expecting ".concat(expectedDefinitionId, " but actual ").concat(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++;
        }
    };
    ProtocolAuthorization.getTypeName = function (protocolPath) {
        return protocolPath.split('/').slice(-1)[0];
    };
    return ProtocolAuthorization;
}());
exports.ProtocolAuthorization = ProtocolAuthorization;
//# sourceMappingURL=protocol-authorization.js.map