All files / lib/esm/Service/VaultRecovery newVaultCopy.js

0% Statements 0/71
0% Branches 0/44
0% Functions 0/7
0% Lines 0/68

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174                                                                                                                                                                                                                                                                                                                                                           
import Vault from "../../Model/Vault";
import Credential from "../../Model/Credential";
/**
 * Uploads a materialized file's plaintext under credential's own encryption key and
 * returns the new ref to assign into `files[]` / the file custom field. `undefined` when the blob was
 * broken (never had plaintext) so callers can drop it instead of assigning a half-built ref.
 *
 * @param credential - Only used to upload the file. No mutations!
 * @param file - The materialized file data to upload. No mutations!
 */
async function uploadMaterializedFile(credential, file) {
    if (file.broken || file.plaintext === undefined) {
        return undefined;
    }
    const uploadResponse = await credential.encryptUploadFile({
        data: file.plaintext,
        filename: file.filename,
        mimetype: file.mimetype ?? '',
        size: file.size ?? file.plaintext.length
    });
    if (!uploadResponse) {
        throw new Error('File upload returned no response: ' + file.filename);
    }
    return {
        filename: file.filename,
        mimetype: file.mimetype ?? '',
        size: file.size ?? file.plaintext.length,
        data: '',
        file_id: uploadResponse.file_id,
        guid: uploadResponse.guid
    };
}
/**
 * Re-uploads every non-broken {@link MaterializedFile} for `credential` and rebuilds `files[]` /
 * `custom_fields[]` from the upload responses, matching positions 1:1 against `decrypted.custom_fields`
 * (both derive from the same, unreordered `collectFileRefs` traversal, see `materialize.ts`).
 * Broken blobs are dropped instead of assigning a dangling/half-built ref.
 * Partial success: one files upload failure is recorded in `errors` and does not stop the remaining files.
 *
 * @param credential - Only used to upload the files. No mutations! No (file) assignments!
 * @param materialized - The materialized credential to build the files and custom fields from.
 * @returns The rebuilt files and custom fields to assign to the credential.
 */
async function buildFilesAndCustomFields(credential, materialized) {
    const errors = [];
    const filesMaterialized = materialized.files.filter((file) => file.source === 'files');
    const customFieldMaterialized = materialized.files.filter((file) => file.source === 'custom_field');
    const files = [];
    for (const materializedFile of filesMaterialized) {
        try {
            const ref = await uploadMaterializedFile(credential, materializedFile);
            if (ref) {
                files.push(ref);
            }
        }
        catch (e) {
            errors.push(`${materializedFile.filename}: ${e instanceof Error ? e.message : String(e)}`);
        }
    }
    const decryptedCustomFields = materialized.decrypted.custom_fields ?? [];
    const custom_fields = [];
    let fileFieldCursor = 0;
    for (const field of decryptedCustomFields) {
        if (field.field_type !== 'file') {
            custom_fields.push(field);
            continue;
        }
        const materializedFile = customFieldMaterialized[fileFieldCursor];
        fileFieldCursor++;
        try {
            const ref = materializedFile ? await uploadMaterializedFile(credential, materializedFile) : undefined;
            if (ref) {
                custom_fields.push({ ...field, value: ref });
            }
            // drop broken file custom fields entirely, same policy as files[] above
        }
        catch (e) {
            errors.push(`${materializedFile?.filename ?? field.label}: ${e instanceof Error ? e.message : String(e)}`);
        }
    }
    return { files, custom_fields, errors };
}
/**
 * Creates a new, plain owned credential in `newVault` from a materialized credential.
 * Never carries over `shared_key` / `set_share_key`: `decrypted` never contains `shared_key` (not in `PAYLOAD_FIELDS`),
 * and `set_share_key` is simply never set on the new credential.
 */
async function copyOneCredential(newVault, materialized) {
    const credential = new Credential(newVault, newVault.getServer());
    credential.updateData(materialized.decrypted);
    const { files, custom_fields, errors } = await buildFilesAndCustomFields(credential, materialized);
    credential.files = files;
    credential.custom_fields = custom_fields;
    const response = await credential.save();
    if (!response) {
        const message = errors.length > 0
            ? 'Server rejected the credential creation. File errors (not the rejection reason): ' + errors.join('; ')
            : 'Server rejected the credential creation.';
        return { success: false, error: message };
    }
    if (errors.length > 0) {
        return { success: false, error: 'File upload incomplete: ' + errors.join('; ') };
    }
    return { success: true };
}
/**
 * Creates a new vault and copies every materialized credential into it as a new, plain owned credential.
 * Partial success is expected and reported per credential; one failure never aborts the remaining copies.
 * The original vault and its credentials are never mutated.
 *
 * Dry-run: no `POST /vaults`, no uploads, no `save()`.
 * Materialization (the network-touching, read-only part) has already happened by the time this is called;
 * every materialized credential is reported as a trivial dry-run success.
 */
export async function copyIntoNewVault(vault, materialized, newVaultName, preferredVaultKeyCandidate, dryRun, onProgress, onError) {
    const progress = {
        doneSteps: 0,
        totalSteps: materialized.length,
        messages: [],
        stage: 'write'
    };
    onProgress?.(progress);
    if (dryRun) {
        const items = materialized.map((credential) => ({
            guid: credential.guid,
            label: credential.label,
            action: 'copied',
            success: true,
            dryRun: true
        }));
        progress.doneSteps = materialized.length;
        onProgress?.(progress);
        return { items };
    }
    const newVault = await Vault.create(newVaultName, preferredVaultKeyCandidate.password, vault.getServer());
    if (!newVault) {
        throw new Error('Failed to create new vault: ' + newVaultName);
    }
    const items = [];
    for (const materializedCredential of materialized) {
        progress.messages.push(`Copying: ${materializedCredential.label}`);
        onProgress?.(progress);
        try {
            const result = await copyOneCredential(newVault, materializedCredential);
            if (!result.success) {
                onError?.(`${materializedCredential.label}: ${result.error}`);
            }
            items.push({
                guid: materializedCredential.guid,
                label: materializedCredential.label,
                action: 'copied',
                success: result.success,
                error: result.error,
                dryRun: false
            });
        }
        catch (e) {
            const message = e instanceof Error ? e.message : String(e);
            onError?.(`${materializedCredential.label}: ${message}`);
            items.push({
                guid: materializedCredential.guid,
                label: materializedCredential.label,
                action: 'copied',
                success: false,
                error: message,
                dryRun: false
            });
        }
        progress.doneSteps++;
        onProgress?.(progress);
    }
    return { items, createdVaultGuid: newVault.guid, createdVaultName: newVault.name };
}