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 { Encoder } from '../../../utils/encoder.js';
import { Encryption } from '../../../utils/encryption.js';
import { EncryptionAlgorithm } from '../../../utils/encryption.js';
import { GeneralJwsSigner } from '../../../jose/jws/general/signer.js';
import { getCurrentTimeInHighPrecision } from '../../../utils/time.js';
import { Jws } from '../../../utils/jws.js';
import { Message } from '../../../core/message.js';
import { ProtocolAuthorization } from '../../../core/protocol-authorization.js';
import { Records } from '../../../utils/records.js';
import { removeUndefinedProperties } from '../../../utils/object.js';
import { Secp256k1 } from '../../../utils/secp256k1.js';
import { authorize, validateAuthorizationIntegrity } from '../../../core/auth.js';
import { Cid, computeCid } from '../../../utils/cid.js';
import { DwnError, DwnErrorCode } from '../../../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js';
import { normalizeProtocolUrl, normalizeSchemaUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../../../utils/url.js';
export class RecordsWrite extends Message {
    constructor(message) {
        super(message);
        this.attesters = RecordsWrite.getAttesters(message);
        // consider converting isInitialWrite() & getEntryId() into properties for performance and convenience
    }
    static parse(message) {
        return __awaiter(this, void 0, void 0, function* () {
            // asynchronous checks that are required by the constructor to initialize members properly
            yield validateAuthorizationIntegrity(message, { allowedProperties: new Set(['recordId', 'contextId', 'attestationCid', 'encryptionCid']) });
            yield RecordsWrite.validateAttestationIntegrity(message);
            const recordsWrite = new RecordsWrite(message);
            yield recordsWrite.validateIntegrity(); // RecordsWrite specific data integrity check
            return recordsWrite;
        });
    }
    /**
     * Creates a RecordsWrite message.
     * @param options.recordId If `undefined`, will be auto-filled as a originating message as convenience for developer.
     * @param options.data Data used to compute the `dataCid`, must be the encrypted data bytes if `options.encryptionInput` is given.
     *                     Must specify `options.dataCid` if `undefined`.
     * @param options.dataCid CID of the data that is already stored in the DWN. Must specify `options.data` if `undefined`.
     * @param options.dataSize Size of data in number of bytes. Must be defined if `options.dataCid` is defined; must be `undefined` otherwise.
     * @param options.dateCreated If `undefined`, it will be auto-filled with current time.
     * @param options.dateModified If `undefined`, it will be auto-filled with current time.
     */
    static create(options) {
        var _a, _b, _c, _d, _e;
        return __awaiter(this, void 0, void 0, function* () {
            if ((options.protocol === undefined && options.protocolPath !== undefined) ||
                (options.protocol !== undefined && options.protocolPath === undefined)) {
                throw new Error('`protocol` and `protocolPath` must both be defined or undefined at the same time');
            }
            if ((options.data === undefined && options.dataCid === undefined) ||
                (options.data !== undefined && options.dataCid !== undefined)) {
                throw new Error('one and only one parameter between `data` and `dataCid` is allowed');
            }
            if ((options.dataCid === undefined && options.dataSize !== undefined) ||
                (options.dataCid !== undefined && options.dataSize === undefined)) {
                throw new Error('`dataCid` and `dataSize` must both be defined or undefined at the same time');
            }
            const dataCid = (_a = options.dataCid) !== null && _a !== void 0 ? _a : yield Cid.computeDagPbCidFromBytes(options.data);
            const dataSize = (_b = options.dataSize) !== null && _b !== void 0 ? _b : options.data.length;
            const currentTime = getCurrentTimeInHighPrecision();
            const descriptor = {
                interface: DwnInterfaceName.Records,
                method: DwnMethodName.Write,
                protocol: options.protocol !== undefined ? normalizeProtocolUrl(options.protocol) : undefined,
                protocolPath: options.protocolPath,
                recipient: options.recipient,
                schema: options.schema !== undefined ? normalizeSchemaUrl(options.schema) : undefined,
                parentId: options.parentId,
                dataCid,
                dataSize,
                dateCreated: (_c = options.dateCreated) !== null && _c !== void 0 ? _c : currentTime,
                dateModified: (_d = options.dateModified) !== null && _d !== void 0 ? _d : currentTime,
                published: options.published,
                datePublished: options.datePublished,
                dataFormat: options.dataFormat
            };
            // generate `datePublished` if the message is to be published but `datePublished` is not given
            if (options.published === true &&
                options.datePublished === undefined) {
                descriptor.datePublished = currentTime;
            }
            // delete all descriptor properties that are `undefined` else the code will encounter the following IPLD issue when attempting to generate CID:
            // Error: `undefined` is not supported by the IPLD Data Model and cannot be encoded
            removeUndefinedProperties(descriptor);
            const author = Jws.extractDid(options.authorizationSignatureInput.protectedHeader.kid);
            // `recordId` computation
            const recordId = (_e = options.recordId) !== null && _e !== void 0 ? _e : yield RecordsWrite.getEntryId(author, descriptor);
            // `contextId` computation
            let contextId;
            if (options.contextId !== undefined) {
                contextId = options.contextId;
            }
            else { // `contextId` is undefined
                // we compute the contextId for the caller if `protocol` is specified (this is the case of the root message of a protocol context)
                if (descriptor.protocol !== undefined) {
                    contextId = yield RecordsWrite.getEntryId(author, descriptor);
                }
            }
            // `attestation` generation
            const descriptorCid = yield computeCid(descriptor);
            const attestation = yield RecordsWrite.createAttestation(descriptorCid, options.attestationSignatureInputs);
            const encryption = yield RecordsWrite.createEncryptionProperty(recordId, contextId, descriptor, options.encryptionInput);
            // `authorization` generation
            const authorization = yield RecordsWrite.createAuthorization(recordId, contextId, descriptorCid, attestation, encryption, options.authorizationSignatureInput);
            const message = {
                recordId,
                descriptor,
                authorization
            };
            if (contextId !== undefined) {
                message.contextId = contextId;
            } // assign `contextId` only if it is defined
            if (attestation !== undefined) {
                message.attestation = attestation;
            } // assign `attestation` only if it is defined
            if (encryption !== undefined) {
                message.encryption = encryption;
            } // assign `encryption` only if it is defined
            Message.validateJsonSchema(message);
            return new RecordsWrite(message);
        });
    }
    /**
     * Convenience method that creates a message by:
     * 1. Copying over immutable properties from the given unsigned message
     * 2. Copying over mutable properties that are not overwritten from the given unsigned message
     * 3. Replace the mutable properties that are given new value
     * @param options.unsignedRecordsWriteMessage Unsigned message that the new RecordsWrite will be based from.
     * @param options.dateModified The new date the record is modified. If not given, current time will be used .
     * @param options.data The new data or the record. If not given, data from given message will be used.
     * @param options.published The new published state. If not given, then will be set to `true` if {options.dateModified} is given;
     * else the state from given message will be used.
     * @param options.publishedDate The new date the record is modified. If not given, then:
     * - will not be set if the record will be unpublished as the result of this RecordsWrite; else
     * - will be set to the same published date as the given message if it wss already published; else
     * - will be set to current time (because this is a toggle from unpublished to published)
     */
    static createFrom(options) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const unsignedMessage = options.unsignedRecordsWriteMessage;
            const currentTime = getCurrentTimeInHighPrecision();
            // inherit published value from parent if neither published nor datePublished is specified
            const published = (_a = options.published) !== null && _a !== void 0 ? _a : (options.datePublished ? true : unsignedMessage.descriptor.published);
            // use current time if published but no explicit time given
            let datePublished = undefined;
            // if given explicitly published dated
            if (options.datePublished) {
                datePublished = options.datePublished;
            }
            else {
                // if this RecordsWrite will publish the record
                if (published) {
                    // the parent was already published, inherit the same published date
                    if (unsignedMessage.descriptor.published) {
                        datePublished = unsignedMessage.descriptor.datePublished;
                    }
                    else {
                        // this is a toggle from unpublished to published, use current time
                        datePublished = currentTime;
                    }
                }
            }
            const createOptions = {
                // immutable properties below, just inherit from the message given
                recipient: unsignedMessage.descriptor.recipient,
                recordId: unsignedMessage.recordId,
                dateCreated: unsignedMessage.descriptor.dateCreated,
                contextId: unsignedMessage.contextId,
                protocol: unsignedMessage.descriptor.protocol,
                protocolPath: unsignedMessage.descriptor.protocolPath,
                parentId: unsignedMessage.descriptor.parentId,
                schema: unsignedMessage.descriptor.schema,
                dataFormat: unsignedMessage.descriptor.dataFormat,
                // mutable properties below
                dateModified: (_b = options.dateModified) !== null && _b !== void 0 ? _b : currentTime,
                published,
                datePublished,
                data: options.data,
                dataCid: options.data ? undefined : unsignedMessage.descriptor.dataCid,
                dataSize: options.data ? undefined : unsignedMessage.descriptor.dataSize,
                // finally still need input for signing
                authorizationSignatureInput: options.authorizationSignatureInput,
                attestationSignatureInputs: options.attestationSignatureInputs
            };
            const recordsWrite = yield RecordsWrite.create(createOptions);
            return recordsWrite;
        });
    }
    authorize(tenant, messageStore) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.message.descriptor.protocol !== undefined) {
                // NOTE: `author` definitely exists because of the earlier `authenticate()` call
                yield ProtocolAuthorization.authorize(tenant, this, this.author, messageStore);
            }
            else {
                yield authorize(tenant, this);
            }
        });
    }
    /**
     * Validates the integrity of the RecordsWrite message assuming the message passed basic schema validation.
     * There is opportunity to integrate better with `validateSchema(...)`
     */
    validateIntegrity() {
        return __awaiter(this, void 0, void 0, function* () {
            // make sure the same `recordId` in message is the same as the `recordId` in `authorization`
            if (this.message.recordId !== this.authorizationPayload.recordId) {
                throw new Error(`recordId in message ${this.message.recordId} does not match recordId in authorization: ${this.authorizationPayload.recordId}`);
            }
            // if the new message is the initial write
            const isInitialWrite = yield this.isInitialWrite();
            if (isInitialWrite) {
                // `dateModified` and `dateCreated` equality check
                const dateCreated = this.message.descriptor.dateCreated;
                const dateModified = this.message.descriptor.dateModified;
                if (dateModified !== dateCreated) {
                    throw new Error(`dateModified ${dateModified} must match dateCreated ${dateCreated} for the initial write`);
                }
                // if the message is also a protocol context root, the `contextId` must match the expected deterministic value
                if (this.message.descriptor.protocol !== undefined &&
                    this.message.descriptor.parentId === undefined) {
                    const expectedContextId = yield this.getEntryId();
                    if (this.message.contextId !== expectedContextId) {
                        throw new Error(`contextId in message: ${this.message.contextId} does not match deterministic contextId: ${expectedContextId}`);
                    }
                }
            }
            // if `contextId` is given in message, make sure the same `contextId` is in the `authorization`
            if (this.message.contextId !== this.authorizationPayload.contextId) {
                throw new Error(`contextId in message ${this.message.contextId} does not match contextId in authorization: ${this.authorizationPayload.contextId}`);
            }
            // if `attestation` is given in message, make sure the correct `attestationCid` is in the `authorization`
            if (this.authorizationPayload.attestationCid !== undefined) {
                const expectedAttestationCid = yield computeCid(this.message.attestation);
                const actualAttestationCid = this.authorizationPayload.attestationCid;
                if (actualAttestationCid !== expectedAttestationCid) {
                    throw new Error(`CID ${expectedAttestationCid} of attestation property in message does not match attestationCid in authorization: ${actualAttestationCid}`);
                }
            }
            // if `encryption` is given in message, make sure the correct `encryptionCid` is in the `authorization`
            if (this.authorizationPayload.encryptionCid !== undefined) {
                const expectedEncryptionCid = yield computeCid(this.message.encryption);
                const actualEncryptionCid = this.authorizationPayload.encryptionCid;
                if (actualEncryptionCid !== expectedEncryptionCid) {
                    throw new DwnError(DwnErrorCode.RecordsWriteValidateIntegrityEncryptionCidMismatch, `CID ${expectedEncryptionCid} of encryption property in message does not match encryptionCid in authorization: ${actualEncryptionCid}`);
                }
            }
            if (this.message.descriptor.protocol !== undefined) {
                validateProtocolUrlNormalized(this.message.descriptor.protocol);
            }
            if (this.message.descriptor.schema !== undefined) {
                validateSchemaUrlNormalized(this.message.descriptor.schema);
            }
        });
    }
    /**
     * Validates the structural integrity of the `attestation` property.
     * NOTE: signature is not verified.
     */
    static validateAttestationIntegrity(message) {
        return __awaiter(this, void 0, void 0, function* () {
            if (message.attestation === undefined) {
                return;
            }
            if (message.attestation.signatures.length !== 1) {
                throw new Error(`Currently implementation only supports 1 attester, but got ${message.attestation.signatures.length}`);
            }
            const payloadJson = Jws.decodePlainObjectPayload(message.attestation);
            const { descriptorCid } = payloadJson;
            // `descriptorCid` validation - ensure that the provided descriptorCid matches the CID of the actual message
            const expectedDescriptorCid = yield computeCid(message.descriptor);
            if (descriptorCid !== expectedDescriptorCid) {
                throw new Error(`descriptorCid ${descriptorCid} does not match expected descriptorCid ${expectedDescriptorCid}`);
            }
            // check to ensure that no other unexpected properties exist in payload.
            const propertyCount = Object.keys(payloadJson).length;
            if (propertyCount > 1) {
                throw new Error(`Only 'descriptorCid' is allowed in attestation payload, but got ${propertyCount} properties.`);
            }
        });
    }
    ;
    /**
     * Computes the deterministic Entry ID of this message.
     */
    getEntryId() {
        return __awaiter(this, void 0, void 0, function* () {
            const entryId = yield RecordsWrite.getEntryId(this.author, this.message.descriptor);
            return entryId;
        });
    }
    ;
    /**
     * Computes the deterministic Entry ID of this message.
     */
    static getEntryId(author, descriptor) {
        return __awaiter(this, void 0, void 0, function* () {
            if (author === undefined) {
                throw new DwnError(DwnErrorCode.RecordsWriteGetEntryIdUndefinedAuthor, 'Property `author` is needed to compute entry ID.');
            }
            const entryIdInput = Object.assign({}, descriptor);
            entryIdInput.author = author;
            const cid = yield computeCid(entryIdInput);
            return cid;
        });
    }
    ;
    /**
     * Checks if the given message is the initial entry of a record.
     */
    isInitialWrite() {
        return __awaiter(this, void 0, void 0, function* () {
            const entryId = yield this.getEntryId();
            return (entryId === this.message.recordId);
        });
    }
    /**
     * Checks if the given message is the initial entry of a record.
     */
    static isInitialWrite(message) {
        return __awaiter(this, void 0, void 0, function* () {
            // can't be the initial write if the message is not a Records Write
            if (message.descriptor.interface !== DwnInterfaceName.Records ||
                message.descriptor.method !== DwnMethodName.Write) {
                return false;
            }
            const recordsWriteMessage = message;
            const author = Message.getAuthor(message);
            const entryId = yield RecordsWrite.getEntryId(author, recordsWriteMessage.descriptor);
            return (entryId === recordsWriteMessage.recordId);
        });
    }
    /**
     * Creates the `encryption` property if encryption input is given. Else `undefined` is returned.
     */
    static createEncryptionProperty(recordId, contextId, descriptor, encryptionInput) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            if (encryptionInput === undefined) {
                return undefined;
            }
            // encrypt the data encryption key once per key derivation scheme
            const keyEncryption = [];
            for (const keyEncryptionInput of encryptionInput.keyEncryptionInputs) {
                const fullDerivationPath = Records.constructKeyDerivationPath(keyEncryptionInput.derivationScheme, recordId, contextId, descriptor);
                // NOTE: right now only `ECIES-ES256K` algorithm is supported for asymmetric encryption,
                // so we will assume that's the algorithm without additional switch/if statements
                const leafPublicKey = yield Records.deriveLeafPublicKey(keyEncryptionInput.publicKey, fullDerivationPath);
                const keyEncryptionOutput = yield Encryption.eciesSecp256k1Encrypt(leafPublicKey, encryptionInput.key);
                const encryptedKey = Encoder.bytesToBase64Url(keyEncryptionOutput.ciphertext);
                const ephemeralPublicKey = yield Secp256k1.publicKeyToJwk(keyEncryptionOutput.ephemeralPublicKey);
                const keyEncryptionInitializationVector = Encoder.bytesToBase64Url(keyEncryptionOutput.initializationVector);
                const messageAuthenticationCode = Encoder.bytesToBase64Url(keyEncryptionOutput.messageAuthenticationCode);
                const encryptedKeyData = {
                    rootKeyId: keyEncryptionInput.publicKeyId,
                    algorithm: (_a = keyEncryptionInput.algorithm) !== null && _a !== void 0 ? _a : EncryptionAlgorithm.EciesSecp256k1,
                    derivationScheme: keyEncryptionInput.derivationScheme,
                    ephemeralPublicKey,
                    initializationVector: keyEncryptionInitializationVector,
                    messageAuthenticationCode,
                    encryptedKey
                };
                keyEncryption.push(encryptedKeyData);
            }
            const encryption = {
                algorithm: (_b = encryptionInput.algorithm) !== null && _b !== void 0 ? _b : EncryptionAlgorithm.Aes256Ctr,
                initializationVector: Encoder.bytesToBase64Url(encryptionInput.initializationVector),
                keyEncryption
            };
            return encryption;
        });
    }
    /**
     * Creates the `attestation` property of a RecordsWrite message if given signature inputs; returns `undefined` otherwise.
     */
    static createAttestation(descriptorCid, signatureInputs) {
        return __awaiter(this, void 0, void 0, function* () {
            if (signatureInputs === undefined || signatureInputs.length === 0) {
                return undefined;
            }
            const attestationPayload = { descriptorCid };
            const attestationPayloadBytes = Encoder.objectToBytes(attestationPayload);
            const signer = yield GeneralJwsSigner.create(attestationPayloadBytes, signatureInputs);
            return signer.getJws();
        });
    }
    /**
     * Creates the `authorization` property of a RecordsWrite message.
     */
    static createAuthorization(recordId, contextId, descriptorCid, attestation, encryption, signatureInput) {
        return __awaiter(this, void 0, void 0, function* () {
            const authorizationPayload = {
                recordId,
                descriptorCid
            };
            const attestationCid = attestation ? yield computeCid(attestation) : undefined;
            const encryptionCid = encryption ? yield computeCid(encryption) : undefined;
            if (contextId !== undefined) {
                authorizationPayload.contextId = contextId;
            } // assign `contextId` only if it is defined
            if (attestationCid !== undefined) {
                authorizationPayload.attestationCid = attestationCid;
            } // assign `attestationCid` only if it is defined
            if (encryptionCid !== undefined) {
                authorizationPayload.encryptionCid = encryptionCid;
            } // assign `encryptionCid` only if it is defined
            const authorizationPayloadBytes = Encoder.objectToBytes(authorizationPayload);
            const signer = yield GeneralJwsSigner.create(authorizationPayloadBytes, [signatureInput]);
            return signer.getJws();
        });
    }
    /**
     * Gets the initial write from the given list or record write.
     */
    static getInitialWrite(messages) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const message of messages) {
                if (yield RecordsWrite.isInitialWrite(message)) {
                    return message;
                }
            }
            throw new Error(`initial write is not found`);
        });
    }
    /**
     * Verifies that immutable properties of the two given messages are identical.
     * @throws {Error} if immutable properties between two RecordsWrite message
     */
    static verifyEqualityOfImmutableProperties(existingWriteMessage, newMessage) {
        const mutableDescriptorProperties = ['dataCid', 'dataSize', 'datePublished', 'published', 'dateModified'];
        // get distinct property names that exist in either the existing message given or new message
        let descriptorPropertyNames = [];
        descriptorPropertyNames.push(...Object.keys(existingWriteMessage.descriptor));
        descriptorPropertyNames.push(...Object.keys(newMessage.descriptor));
        descriptorPropertyNames = [...new Set(descriptorPropertyNames)]; // step to remove duplicates
        // ensure all immutable properties are not modified
        for (const descriptorPropertyName of descriptorPropertyNames) {
            // if property is supposed to be immutable
            if (mutableDescriptorProperties.indexOf(descriptorPropertyName) === -1) {
                const valueInExistingWrite = existingWriteMessage.descriptor[descriptorPropertyName];
                const valueInNewMessage = newMessage.descriptor[descriptorPropertyName];
                if (valueInNewMessage !== valueInExistingWrite) {
                    throw new Error(`${descriptorPropertyName} is an immutable property: cannot change '${valueInExistingWrite}' to '${valueInNewMessage}'`);
                }
            }
        }
        return true;
    }
    /**
     * Gets the DID of the author of the given message.
     */
    static getAttesters(message) {
        var _a, _b;
        const attestationSignatures = (_b = (_a = message.attestation) === null || _a === void 0 ? void 0 : _a.signatures) !== null && _b !== void 0 ? _b : [];
        const attesters = attestationSignatures.map((signature) => Jws.getSignerDid(signature));
        return attesters;
    }
}
//# sourceMappingURL=records-write.js.map