fix(transport): curate BundleLockedException on the management surface (register row 51)

ManagementService references Commons but not Transport, so MapFault could not
see BundleLockedException and the per-bundle 3-strike lockout reached the CLI
as "An internal error occurred (CorrelationId=...)" — the operator got a
correlation id instead of the reason, and no hint that further attempts on
that bundle would be refused.

Applies the pattern register row 44 established for BundleUnlockThrottledException:

- Relocate BundleLockedException from Transport/Import/ to
  Commons/Types/Transport/, beside BundleUnlockThrottledException. Both
  consumers (BundleImporter, CentralUI TransportImport) already carried the
  Commons.Types.Transport using, so no call-site edits were needed.
- Add curated catch arms in ManagementActor.HandlePreviewBundle and
  HandleImportBundle that rethrow as ManagementCommandException, so the
  message survives MapFault's disclosure filter.
- Mirror the row-44 throttle fault-mapping test for both handlers.
- Document the relocation in docs/components/Transport.md and
  docs/requirements/Component-Transport.md.

Tests: ManagementService 499 passed (+2 new), Transport 167 passed.
This commit is contained in:
Joseph Doherty
2026-08-15 11:37:36 -04:00
parent c6769ee95e
commit b449fe201a
6 changed files with 114 additions and 34 deletions
@@ -1,31 +0,0 @@
namespace ZB.MOM.WW.ScadaBridge.Transport.Import;
/// <summary>
/// T-003: thrown by <see cref="BundleImporter.LoadAsync"/> when an encrypted bundle has
/// exceeded the configured failed-unlock attempt limit
/// (<see cref="TransportOptions.MaxUnlockAttemptsPerSession"/>). The lockout is tracked
/// server-side keyed by <c>BundleManifest.ContentHash</c>, so a second tab / CLI caller
/// re-uploading the same bytes hits the same counter and cannot side-step the limit.
/// </summary>
public sealed class BundleLockedException : Exception
{
/// <summary>Number of recorded unlock failures for this bundle.</summary>
public int FailedAttempts { get; }
/// <summary>SHA-256 (hex) of the bundle's content bytes, the lockout's tracking key.</summary>
public string BundleContentHash { get; }
/// <summary>
/// Initializes a new <see cref="BundleLockedException"/>.
/// </summary>
/// <param name="bundleContentHash">SHA-256 hex from <c>BundleManifest.ContentHash</c>.</param>
/// <param name="failedAttempts">Number of failures recorded against this bundle.</param>
public BundleLockedException(string bundleContentHash, int failedAttempts)
: base(
$"Bundle is locked after {failedAttempts} failed unlock attempts. "
+ "Wait for the lockout window to expire or re-export the bundle to obtain a new content hash.")
{
BundleContentHash = bundleContentHash ?? throw new ArgumentNullException(nameof(bundleContentHash));
FailedAttempts = failedAttempts;
}
}