import { checkActor } from '../core/protocol-authorization-action.js';
import { Encoder } from '../utils/encoder.js';
import { Encryption } from '../utils/encryption.js';
import { ENCRYPTION_PROTOCOL_URI } from '../core/constants.js';
import { FilterUtility } from '../utils/filter.js';
import { Jws } from '../utils/jws.js';
import { Message } from '../core/message.js';
import { Records } from '../utils/records.js';
import { validateJsonSchema } from '../schema-validator.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { getRuleSetAtPath, isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
export class EncryptionProtocol {
    static uri = ENCRYPTION_PROTOCOL_URI;
    static audienceEpochPath = 'audienceEpoch';
    static audienceKeyPath = 'audienceKey';
    static grantKeyPath = 'grantKey';
    static definition = {
        published: true,
        protocol: EncryptionProtocol.uri,
        types: {
            audienceEpoch: {
                dataFormats: ['application/json'],
                schema: 'https://identity.foundation/dwn/json-schemas/encryption/audience-epoch.json',
            },
            audienceKey: {
                dataFormats: ['application/json'],
                schema: 'https://identity.foundation/dwn/json-schemas/encryption/audience-key.json',
            },
            grantKey: {
                dataFormats: ['application/json'],
                schema: 'https://identity.foundation/dwn/json-schemas/encryption/grant-key.json',
            },
        },
        structure: {
            audienceEpoch: {
                $actions: [
                    { can: ['create', 'read'], who: 'anyone' },
                ],
                $immutable: true,
                $tags: {
                    $allowUndefinedTags: false,
                    $requiredTags: ['protocol', 'contextId', 'role', 'epoch', 'keyId'],
                    contextId: { type: 'string' },
                    epoch: { minimum: 1, type: 'integer' },
                    keyId: { pattern: '^[A-Za-z0-9_-]{43}$', type: 'string' },
                    protocol: { type: 'string' },
                    role: { type: 'string' },
                },
            },
            audienceKey: {
                $actions: [
                    { can: ['create'], who: 'anyone' },
                    { can: ['read'], of: 'audienceKey', who: 'recipient' },
                ],
                $immutable: true,
                $tags: {
                    $allowUndefinedTags: false,
                    $requiredTags: ['protocol', 'contextId', 'role', 'epoch', 'keyId'],
                    contextId: { type: 'string' },
                    epoch: { minimum: 1, type: 'integer' },
                    keyId: { pattern: '^[A-Za-z0-9_-]{43}$', type: 'string' },
                    protocol: { type: 'string' },
                    role: { type: 'string' },
                },
            },
            grantKey: {
                $actions: [
                    { can: ['create'], who: 'anyone' },
                    { can: ['read'], of: 'grantKey', who: 'recipient' },
                ],
                $immutable: true,
                $tags: {
                    $allowUndefinedTags: false,
                    $requiredTags: ['grantId', 'protocol', 'keyId'],
                    grantId: { type: 'string' },
                    keyId: { maxLength: 43, minLength: 43, pattern: '^[A-Za-z0-9_-]{43}$', type: 'string' },
                    protocol: { type: 'string' },
                    protocolPath: { type: 'string' },
                },
            },
        }
    };
    get uri() {
        return EncryptionProtocol.uri;
    }
    get definition() {
        return EncryptionProtocol.definition;
    }
    async preProcessWrite(tenant, message, validationStateReader) {
        switch (message.descriptor.protocolPath) {
            case EncryptionProtocol.audienceEpochPath:
                await EncryptionProtocol.preProcessAudienceEpoch(tenant, message, validationStateReader);
                return;
            case EncryptionProtocol.audienceKeyPath:
                await EncryptionProtocol.preProcessAudienceKey(tenant, message, validationStateReader);
                return;
            case EncryptionProtocol.grantKeyPath:
                await EncryptionProtocol.preProcessGrantKey(tenant, message, validationStateReader);
                return;
            default:
                return;
        }
    }
    async validateRecord(message, dataBytes) {
        switch (message.descriptor.protocolPath) {
            case EncryptionProtocol.audienceEpochPath:
                await EncryptionProtocol.validateAudienceEpoch(message, dataBytes);
                return;
            case EncryptionProtocol.audienceKeyPath:
            case EncryptionProtocol.grantKeyPath:
                EncryptionProtocol.verifyEncryptedDelivery(message);
                return;
            default:
                throw new DwnError(DwnErrorCode.EncryptionProtocolValidateSchemaUnexpectedRecord, `Unexpected encryption record: ${message.descriptor.protocolPath}`);
        }
    }
    static async preProcessAudienceEpoch(tenant, message, validationStateReader) {
        if (message.encryption !== undefined) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceEpochEncrypted, 'audienceEpoch records must be plaintext.');
        }
        const tags = EncryptionProtocol.getAudienceTags(message, 'audienceEpoch');
        const { ruleSet, protocolDefinition } = await EncryptionProtocol.verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader);
        await EncryptionProtocol.verifyWriterCanCreateRole({
            tenant,
            message,
            tags,
            ruleSet,
            protocolDefinition,
            validationStateReader,
        });
    }
    static async preProcessAudienceKey(tenant, message, validationStateReader) {
        EncryptionProtocol.verifyEncryptedDelivery(message);
        const recipient = message.descriptor.recipient;
        if (recipient === undefined) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceKeyRecipientMissing, 'audienceKey records must have a recipient.');
        }
        const tags = EncryptionProtocol.getAudienceTags(message, 'audienceKey');
        const { ruleSet, protocolDefinition } = await EncryptionProtocol.verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader);
        await EncryptionProtocol.verifyWriterCanCreateRole({
            tenant,
            message,
            tags,
            ruleSet,
            protocolDefinition,
            validationStateReader,
        });
        const matchingEpochs = await validationStateReader.queryAudienceEpochs({
            tenant,
            protocol: tags.protocol,
            contextId: tags.contextId,
            role: tags.role,
            epoch: tags.epoch,
            keyId: tags.keyId,
        });
        if (matchingEpochs.length === 0) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceEpochMissing, 'audienceKey references a missing audienceEpoch.');
        }
        const hasRoleRecord = await validationStateReader.hasMatchingRoleRecord({
            tenant,
            protocol: tags.protocol,
            protocolPath: tags.role,
            recipient,
            contextIdPrefix: tags.contextId === '' ? undefined : tags.contextId,
        });
        if (!hasRoleRecord) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceKeyRoleRecordMissing, 'audienceKey recipient is not an active holder of the referenced role.');
        }
    }
    static async preProcessGrantKey(tenant, message, validationStateReader) {
        EncryptionProtocol.verifyEncryptedDelivery(message);
        const grantId = EncryptionProtocol.getRequiredStringTag(message, 'grantId');
        const protocol = EncryptionProtocol.getRequiredStringTag(message, 'protocol');
        const protocolPath = EncryptionProtocol.getOptionalStringTag(message, 'protocolPath');
        EncryptionProtocol.getRequiredStringTag(message, 'keyId');
        const grant = await validationStateReader.fetchGrant(tenant, grantId);
        await EncryptionProtocol.verifyGrantActive(tenant, message, grant, validationStateReader);
        EncryptionProtocol.verifyGrantKeyAuthor(message, grant);
        EncryptionProtocol.verifyGrantKeyRecipient(message, grant);
        EncryptionProtocol.verifyGrantKeyScope(grant.scope, protocol, protocolPath);
    }
    static verifyEncryptedDelivery(message) {
        if (message.encryption === undefined) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateEncryptedDeliveryMissingEncryption, `${message.descriptor.protocolPath} records must be encrypted.`);
        }
    }
    static async validateAudienceEpoch(message, dataBytes) {
        if (message.encryption !== undefined) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceEpochEncrypted, 'audienceEpoch records must be plaintext.');
        }
        const dataObject = JSON.parse(Encoder.bytesToString(dataBytes));
        validateJsonSchema('AudienceEpoch', dataObject);
        const tags = EncryptionProtocol.getAudienceTags(message, 'audienceEpoch');
        EncryptionProtocol.verifyAudiencePayloadMatchesTags(dataObject, tags);
        const keyId = await Encryption.getKeyId(dataObject.publicKeyJwk);
        if (keyId !== dataObject.keyId) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceEpochKeyIdMismatch, 'audienceEpoch keyId must match publicKeyJwk thumbprint.');
        }
    }
    static verifyAudiencePayloadMatchesTags(payload, tags) {
        if (payload.protocol !== tags.protocol ||
            payload.contextId !== tags.contextId ||
            payload.role !== tags.role ||
            payload.epoch !== tags.epoch ||
            payload.keyId !== tags.keyId) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceTagsMismatch, 'audienceEpoch tags must match payload fields.');
        }
    }
    static getRequiredStringTag(message, tag, recordType = 'grantKey') {
        const value = message.descriptor.tags?.[tag];
        if (typeof value !== 'string') {
            const errorCode = recordType === 'grantKey'
                ? DwnErrorCode.EncryptionProtocolValidateGrantKeyMissingRequiredTag
                : DwnErrorCode.EncryptionProtocolValidateAudienceMissingRequiredTag;
            throw new DwnError(errorCode, `${recordType} records must include string tag '${tag}'.`);
        }
        return value;
    }
    static getRequiredIntegerTag(message, tag, recordType) {
        const value = message.descriptor.tags?.[tag];
        if (!Number.isInteger(value)) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceMissingRequiredTag, `${recordType} records must include integer tag '${tag}'.`);
        }
        return value;
    }
    static getAudienceTags(message, recordType) {
        return {
            protocol: EncryptionProtocol.getRequiredStringTag(message, 'protocol', recordType),
            contextId: EncryptionProtocol.getRequiredStringTag(message, 'contextId', recordType),
            role: EncryptionProtocol.getRequiredStringTag(message, 'role', recordType),
            epoch: EncryptionProtocol.getRequiredIntegerTag(message, 'epoch', recordType),
            keyId: EncryptionProtocol.getRequiredStringTag(message, 'keyId', recordType),
        };
    }
    static getOptionalStringTag(message, tag) {
        const value = message.descriptor.tags?.[tag];
        if (value === undefined) {
            return undefined;
        }
        if (typeof value !== 'string') {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyMissingRequiredTag, `grantKey tag '${tag}' must be a string.`);
        }
        return value;
    }
    static async verifyGrantActive(tenant, message, grant, validationStateReader) {
        const timestamp = message.descriptor.messageTimestamp;
        if (timestamp < grant.dateGranted) {
            throw new DwnError(DwnErrorCode.GrantAuthorizationGrantNotYetActive, 'grantKey references a permission grant that is not active yet.');
        }
        if (timestamp >= grant.dateExpires) {
            throw new DwnError(DwnErrorCode.GrantAuthorizationGrantExpired, 'grantKey references an expired permission grant.');
        }
        const oldestRevocation = await validationStateReader.fetchOldestGrantRevocation(tenant, grant.id);
        if (oldestRevocation !== undefined && oldestRevocation.descriptor.messageTimestamp <= timestamp) {
            throw new DwnError(DwnErrorCode.GrantAuthorizationGrantRevoked, `grantKey references revoked permission grant ${grant.id}.`);
        }
    }
    static verifyGrantKeyAuthor(message, grant) {
        if (Message.getAuthor(message) === grant.grantor) {
            return;
        }
        throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyAuthorMismatch, `grantKey author must be permission grantor ${grant.grantor}.`);
    }
    static verifyGrantKeyRecipient(message, grant) {
        if (message.descriptor.recipient === grant.grantee) {
            return;
        }
        throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyRecipientMismatch, `grantKey recipient must be permission grantee ${grant.grantee}.`);
    }
    static verifyGrantKeyScope(grantScope, protocol, protocolPath) {
        if (!EncryptionProtocol.isReadScope(grantScope)) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyGrantScopeMismatch, 'grantKey must reference a read permission grant.');
        }
        if ('protocol' in grantScope && grantScope.protocol !== undefined && grantScope.protocol !== protocol) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyGrantScopeMismatch, `grantKey protocol ${protocol} is outside the permission grant scope.`);
        }
        if (!('protocolPath' in grantScope) || grantScope.protocolPath === undefined) {
            return;
        }
        if (protocolPath === undefined || !EncryptionProtocol.isBoundaryAwareSubtree(grantScope.protocolPath, protocolPath)) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyGrantScopeMismatch, `grantKey protocolPath ${protocolPath ?? '<protocol>'} is outside the permission grant scope.`);
        }
    }
    static isReadScope(scope) {
        return (scope.interface === DwnInterfaceName.Records || scope.interface === DwnInterfaceName.Messages) &&
            scope.method === DwnMethodName.Read;
    }
    static isBoundaryAwareSubtree(scopePath, candidatePath) {
        return candidatePath === scopePath || candidatePath.startsWith(scopePath + '/');
    }
    static async verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader) {
        EncryptionProtocol.verifyRoleAudienceContext(tags.role, tags.contextId);
        const protocolDefinition = await validationStateReader.fetchProtocolDefinition(tenant, tags.protocol, message.descriptor.messageTimestamp);
        const ruleSet = getRuleSetAtPath(tags.role, protocolDefinition.structure);
        if (ruleSet?.$role !== true || ruleSet.$keyAgreement === undefined) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateRoleAudienceInvalid, `role audience path '${tags.role}' must be a role path with $keyAgreement.`);
        }
        return { protocolDefinition, ruleSet };
    }
    static verifyRoleAudienceContext(rolePath, contextId) {
        const parentDepth = rolePath.split('/').length - 1;
        if ((parentDepth === 0 && contextId !== '') || (parentDepth > 0 && contextId === '')) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateRoleAudienceContextInvalid, 'role audience contextId is inconsistent with role path depth.');
        }
    }
    static async verifyWriterCanCreateRole(input) {
        const author = Message.getAuthor(input.message);
        if (author === input.tenant) {
            return;
        }
        if (author === undefined) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceWriterUnauthorized, 'audience key records must be authored by a DID authorized to create role records.');
        }
        const actionRules = input.ruleSet.$actions ?? [];
        const recordChain = await EncryptionProtocol.constructRoleParentChain(input.tenant, input.tags, input.validationStateReader);
        const signaturePayload = Jws.decodePlainObjectPayload(input.message.authorization.signature);
        for (const actionRule of actionRules) {
            if (!actionRule.can.includes(ProtocolAction.Create)) {
                continue;
            }
            if (actionRule.who === ProtocolActor.Anyone) {
                return;
            }
            if (await EncryptionProtocol.roleRuleAllowsAuthor(input.tenant, author, actionRule, input.tags, input.protocolDefinition, input.validationStateReader, signaturePayload.protocolRole)) {
                return;
            }
            if (await checkActor(author, actionRule, recordChain, input.protocolDefinition)) {
                return;
            }
        }
        throw new DwnError(DwnErrorCode.EncryptionProtocolValidateAudienceWriterUnauthorized, 'audience key records must be authored by a DID authorized to create role records.');
    }
    static async constructRoleParentChain(tenant, tags, validationStateReader) {
        if (tags.contextId === '') {
            return [];
        }
        const parentRecordId = tags.contextId.split('/').pop();
        if (parentRecordId === undefined || parentRecordId === '') {
            return [];
        }
        return validationStateReader.constructRecordChain(tenant, parentRecordId);
    }
    static async roleRuleAllowsAuthor(tenant, author, actionRule, tags, protocolDefinition, validationStateReader, invokedRole) {
        if (actionRule.role === undefined || invokedRole !== actionRule.role) {
            return false;
        }
        const roleRef = actionRule.role;
        return EncryptionProtocol.hasInvokedRole(tenant, author, roleRef, tags, protocolDefinition, validationStateReader);
    }
    static async hasInvokedRole(tenant, author, roleRef, tags, protocolDefinition, validationStateReader) {
        const resolved = EncryptionProtocol.resolveRoleReference(roleRef, tags.protocol, protocolDefinition);
        if (resolved === undefined) {
            return false;
        }
        const ancestorDepth = resolved.protocolPath.split('/').length - 1;
        let contextIdPrefix;
        if (ancestorDepth > 0) {
            contextIdPrefix = tags.contextId.split('/').slice(0, ancestorDepth).join('/');
            if (contextIdPrefix === '') {
                return false;
            }
        }
        return validationStateReader.hasMatchingRoleRecord({
            tenant,
            protocol: resolved.protocol,
            protocolPath: resolved.protocolPath,
            recipient: author,
            contextIdPrefix,
        });
    }
    static resolveRoleReference(roleRef, currentProtocol, protocolDefinition) {
        if (!isCrossProtocolRef(roleRef)) {
            return { protocol: currentProtocol, protocolPath: roleRef };
        }
        const parsed = parseCrossProtocolRef(roleRef);
        if (parsed === undefined) {
            return undefined;
        }
        const protocol = protocolDefinition.uses?.[parsed.alias];
        if (protocol === undefined) {
            return undefined;
        }
        return { protocol, protocolPath: parsed.protocolPath };
    }
    mapErrorToStatusCode(errorCode) {
        if (errorCode.startsWith('EncryptionProtocolValidate')) {
            return 400;
        }
        return undefined;
    }
    constructAdditionalMessageFilter(filter) {
        const { protocol, messageTimestamp } = filter;
        if (protocol === undefined) {
            return undefined;
        }
        const taggedFilter = {
            protocol: EncryptionProtocol.uri,
            ...Records.convertTagsFilter({ protocol }),
        };
        if (messageTimestamp !== undefined) {
            const messageTimestampFilter = FilterUtility.convertRangeCriterion(messageTimestamp);
            if (messageTimestampFilter) {
                taggedFilter.messageTimestamp = messageTimestampFilter;
            }
        }
        return taggedFilter;
    }
}
//# sourceMappingURL=encryption.js.map