fix(transport): serialise the export content once and pack those bytes (review 2026-10-21 04/P8a)

This commit is contained in:
Joseph Doherty
2026-10-22 09:21:28 -04:00
parent 93fbd2e030
commit 56aa386784
3 changed files with 60 additions and 3 deletions
@@ -159,6 +159,10 @@ public sealed class BundleExporter : IBundleExporter
SaltB64: string.Empty,
IvB64: string.Empty);
// Review 2026-10-21 04/P8a: serialise the content graph ONCE — the same bytes
// seed the manifest hash and are what Pack writes (or encrypts).
var contentBytes = _bundleSerializer.SerializeContentBytes(contentDto);
var templateManifest = _manifestBuilder.Build(
sourceEnvironment: sourceEnvironment,
exportedBy: user,
@@ -170,11 +174,11 @@ public sealed class BundleExporter : IBundleExporter
// (either fresh ciphertext or the JSON it just serialized), so the
// upstream hash here only matters for the plaintext path — Pack
// trusts the supplied hash and the serializer is deterministic.
contentBytes: _bundleSerializer.SerializeContentBytes(contentDto));
contentBytes: contentBytes);
// 5. Pack the zip; with a passphrase, Pack re-encrypts and re-stamps
// EncryptionMetadata (fresh salt/iv) + ContentHash.
var zipStream = _bundleSerializer.Pack(contentDto, templateManifest, passphrase, _encryptor);
var zipStream = _bundleSerializer.PackContentBytes(contentBytes, templateManifest, passphrase, _encryptor);
// 6. Audit — entityId is the bundle's overall SHA-256 over the zip bytes
// (opaque to caller but uniquely identifies this export). Action
@@ -57,6 +57,28 @@ public sealed class BundleSerializer
BundleSecretEncryptor? encryptor)
{
ArgumentNullException.ThrowIfNull(content);
return PackContentBytes(SerializeContentBytes(content), manifest, passphrase, encryptor);
}
/// <summary>
/// Packs already-serialised content (the output of <see cref="SerializeContentBytes"/>)
/// and the manifest into a ZIP stream, optionally encrypting the content. Lets a caller
/// that has already serialised the DTO — e.g. to hash it for the manifest — pack those
/// same bytes instead of serialising the whole graph a second time (review 2026-10-21
/// 04/P8a).
/// </summary>
/// <param name="contentBytes">The serialised bundle content JSON.</param>
/// <param name="manifest">The bundle manifest describing the content and encryption metadata.</param>
/// <param name="passphrase">Passphrase used for encryption; null produces a plaintext bundle.</param>
/// <param name="encryptor">Encryptor required when <paramref name="passphrase"/> is supplied.</param>
/// <returns>A seeked-to-start <see cref="MemoryStream"/> containing the bundle ZIP archive.</returns>
public Stream PackContentBytes(
byte[] contentBytes,
BundleManifest manifest,
string? passphrase,
BundleSecretEncryptor? encryptor)
{
ArgumentNullException.ThrowIfNull(contentBytes);
ArgumentNullException.ThrowIfNull(manifest);
if (passphrase is not null && encryptor is null)
@@ -68,7 +90,6 @@ public sealed class BundleSerializer
throw new ArgumentException("Manifest must carry EncryptionMetadata when packing an encrypted bundle.", nameof(manifest));
}
var contentBytes = SerializeContentBytes(content);
byte[] payload;
string payloadEntryName;
BundleManifest finalManifest;