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 | /** * `undefined` when the blob or reference is broken (matches importer-passmanjson.js dropping/failing that entry). */ function toExportJsonFile(file) { if (file.broken || file.plaintext === undefined || file.file_id === undefined || file.guid === undefined) { return undefined; } return { file_id: file.file_id, filename: file.filename, guid: file.guid, size: file.size ?? file.plaintext.length, file_data: file.plaintext, created: file.created ?? new Date().getTime(), mimetype: file.mimetype ?? '', }; } /** * Splits `materialized.files` back into a `files[]` array and file-type `custom_fields[].value` payloads, * matching positions 1:1 against `decrypted.custom_fields` (both derive from the same, unreordered * `collectFileRefs` traversal). Broken blobs are dropped rather than exported half-broken. */ function buildFilesAndCustomFields(materialized) { 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) { const built = toExportJsonFile(materializedFile); if (built) { files.push(built); } } 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++; const built = materializedFile ? toExportJsonFile(materializedFile) : undefined; if (built) { custom_fields.push({ ...field, value: built }); } // drop broken file custom fields entirely, same policy as files[] } return { files, custom_fields }; } /** * Builds the classic top-level-array Passman JSON export format, matching what * `importer-passmanjson.js` actually reads: no `vault_id` / `vault_key` / `shared_key` / `guid` / `acl`, * files inlined as `file_data` (re-uploaded and re-encrypted by the importer on read). * * Known lossy, by design: the importer reads `item.otp.qrCode`, while the decrypted `otp` carries * `qr_uri.image`. `secret` / `type` / `issuer` / `label` import fine; the QR image does not. The classic * export has the same gap, so the decrypted shape is kept as-is rather than inventing a `qrCode` field. */ export function buildPassmanJson(materialized) { const entries = materialized.map((credential) => { const { files, custom_fields } = buildFilesAndCustomFields(credential); const decrypted = credential.decrypted; return { user_id: credential.user_id, vault_id: credential.vault_id, credential_id: credential.credential_id, label: credential.label, username: decrypted.username ?? null, password: decrypted.password ?? null, email: decrypted.email ?? null, url: decrypted.url ?? null, description: decrypted.description ?? null, tags: decrypted.tags ?? null, custom_fields, otp: decrypted.otp ?? {}, compromised: decrypted.compromised ?? null, files, created: decrypted.created ?? null, changed: decrypted.changed ?? null, expire_time: decrypted.expire_time ?? null, delete_time: decrypted.delete_time ?? null, renew_interval: decrypted.renew_interval ?? null, hidden: decrypted.hidden ?? false, icon: decrypted.icon ?? null, }; }); return JSON.stringify(entries); } |