import { DwnConstant } from '../core/dwn-constant.js';
import { Encoder } from '../utils/encoder.js';
import { ENCRYPTION_PROTOCOL_URI } from '../core/constants.js';
import { FilterUtility } from '../utils/filter.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 { grantKeyScopeCoversDeliveredScope, isGrantKeyEligibleRecordsScope, isGrantKeyRecordsScope } from '../utils/grant-key-coverage.js';
export const WRAPPED_GRANT_KEY_FORMAT = 'enbox/wrapped-grant-key@1';
export function assertWrappedGrantKeyEnvelope(envelope) {
    try {
        validateJsonSchema('WrappedGrantKeyEnvelope', envelope);
    }
    catch (error) {
        const detail = error instanceof Error ? error.message : String(error);
        throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyWrappedDeliveryInvalid, `wrapped grantKey envelope is invalid: ${detail}`);
    }
}
export class EncryptionProtocol {
    static uri = ENCRYPTION_PROTOCOL_URI;
    static grantKeyPath = 'grantKey';
    static definition = {
        published: true,
        protocol: EncryptionProtocol.uri,
        types: {
            grantKey: {
                dataFormats: ['application/json'],
                schema: 'https://identity.foundation/dwn/json-schemas/encryption/grant-key.json',
            },
        },
        structure: {
            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) {
        if (message.descriptor.protocolPath === EncryptionProtocol.grantKeyPath) {
            await EncryptionProtocol.preProcessGrantKey(tenant, message, validationStateReader);
        }
    }
    async validateRecord(message, dataBytes) {
        if (message.descriptor.protocolPath === EncryptionProtocol.grantKeyPath) {
            EncryptionProtocol.verifyGrantKeyDelivery(message, dataBytes);
            return;
        }
        throw new DwnError(DwnErrorCode.EncryptionProtocolValidateSchemaUnexpectedRecord, `Unexpected encryption record: ${message.descriptor.protocolPath}`);
    }
    static async preProcessGrantKey(tenant, message, validationStateReader) {
        EncryptionProtocol.verifyInlineWrappedGrantKeyDelivery(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);
        await EncryptionProtocol.verifyGrantKeyScope({
            tenant,
            message,
            grantScope: grant.scope,
            protocol,
            protocolPath,
            validationStateReader,
        });
    }
    static verifyInlineWrappedGrantKeyDelivery(message) {
        if (message.encryption !== undefined) {
            return;
        }
        if (message.descriptor.dataSize > DwnConstant.maxDataSizeAllowedToBeEncoded) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateEncryptedDeliveryMissingEncryption, 'plaintext grantKey records must carry an inline wrapped grantKey envelope.');
        }
    }
    static verifyGrantKeyDelivery(message, dataBytes) {
        if (message.encryption !== undefined) {
            return;
        }
        try {
            const envelope = Encoder.bytesToObject(dataBytes);
            assertWrappedGrantKeyEnvelope(envelope);
        }
        catch (error) {
            if (error instanceof DwnError) {
                throw error;
            }
            const detail = error instanceof Error ? error.message : String(error);
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateEncryptedDeliveryMissingEncryption, `${message.descriptor.protocolPath} records must be encrypted or carry a valid wrapped grantKey envelope: ${detail}`);
        }
    }
    static getRequiredStringTag(message, tag) {
        const value = message.descriptor.tags?.[tag];
        if (typeof value !== 'string') {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyMissingRequiredTag, `grantKey records must include string tag '${tag}'.`);
        }
        return value;
    }
    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 async verifyGrantKeyScope(params) {
        const { grantScope, protocol, protocolPath } = params;
        if (!isGrantKeyRecordsScope(grantScope)) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyGrantScopeMismatch, 'grantKey must reference a Records.Read or Records.Write permission grant.');
        }
        if (!isGrantKeyEligibleRecordsScope(grantScope)) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyGrantScopeMismatch, 'grantKey must not reference a context-scoped permission grant.');
        }
        if (grantScope.protocol !== protocol) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyGrantScopeMismatch, `grantKey protocol ${protocol} is outside the permission grant scope.`);
        }
        const deliveredScope = { protocol, protocolPath };
        if (grantKeyScopeCoversDeliveredScope({ grantScope, deliveredScope })) {
            return;
        }
        if (protocolPath === undefined) {
            throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyGrantScopeMismatch, `grantKey protocolPath ${protocolPath ?? '<protocol>'} is outside the permission grant scope.`);
        }
        const protocolDefinition = await params.validationStateReader.fetchProtocolDefinition(params.tenant, params.protocol, params.message.descriptor.messageTimestamp);
        if (grantKeyScopeCoversDeliveredScope({ grantScope, deliveredScope, protocolDefinition })) {
            return;
        }
        throw new DwnError(DwnErrorCode.EncryptionProtocolValidateGrantKeyGrantScopeMismatch, `grantKey protocolPath ${protocolPath ?? '<protocol>'} is outside the permission grant scope.`);
    }
    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