Merge branch 'fix/archreview-p2' into main (P2 tier: completeness & polish)

# Conflicts:
#	archreview/remediation/00-tracking.md
#	clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayCliSecretRedactor.cs
#	clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs
#	src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs
This commit is contained in:
Joseph Doherty
2026-07-12 22:12:41 -04:00
91 changed files with 7680 additions and 681 deletions
@@ -848,6 +848,525 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Unregisters a previously registered client from the MXAccess session
/// (MXAccess <c>Unregister</c>), releasing its ServerHandle.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task UnregisterAsync(
int serverHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await UnregisterRawAsync(serverHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Unregisters a previously registered client without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> UnregisterRawAsync(
int serverHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.Unregister,
Unregister = new UnregisterCommand { ServerHandle = serverHandle },
},
cancellationToken);
}
/// <summary>
/// Subscribes to supervisory events for an item (MXAccess <c>AdviseSupervisory</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task AdviseSupervisoryAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await AdviseSupervisoryRawAsync(serverHandle, itemHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Subscribes to supervisory events for an item without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AdviseSupervisoryRawAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.AdviseSupervisory,
AdviseSupervisory = new AdviseSupervisoryCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
cancellationToken);
}
/// <summary>
/// Adds a buffered item to the MXAccess session (MXAccess <c>AddBufferedItem</c>),
/// returning an ItemHandle.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemDefinition">The item tag address.</param>
/// <param name="itemContext">Additional context for the item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item handle assigned to the new buffered item.</returns>
public async Task<int> AddBufferedItemAsync(
int serverHandle,
string itemDefinition,
string itemContext,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await AddBufferedItemRawAsync(
serverHandle,
itemDefinition,
itemContext,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.AddBufferedItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Adds a buffered item to the MXAccess session without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemDefinition">The item tag address.</param>
/// <param name="itemContext">Additional context for the item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AddBufferedItemRawAsync(
int serverHandle,
string itemDefinition,
string itemContext,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.AddBufferedItem,
AddBufferedItem = new AddBufferedItemCommand
{
ServerHandle = serverHandle,
ItemDefinition = itemDefinition,
ItemContext = itemContext ?? string.Empty,
},
},
cancellationToken);
}
/// <summary>
/// Sets the buffered-item update interval on the MXAccess session
/// (MXAccess <c>SetBufferedUpdateInterval</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="updateIntervalMilliseconds">The buffered update interval, in milliseconds.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task SetBufferedUpdateIntervalAsync(
int serverHandle,
int updateIntervalMilliseconds,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await SetBufferedUpdateIntervalRawAsync(
serverHandle,
updateIntervalMilliseconds,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Sets the buffered-item update interval without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="updateIntervalMilliseconds">The buffered update interval, in milliseconds.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> SetBufferedUpdateIntervalRawAsync(
int serverHandle,
int updateIntervalMilliseconds,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.SetBufferedUpdateInterval,
SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
{
ServerHandle = serverHandle,
UpdateIntervalMilliseconds = updateIntervalMilliseconds,
},
},
cancellationToken);
}
/// <summary>
/// Suspends updates for an item (MXAccess <c>Suspend</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item's MXSTATUS_PROXY as reported by the worker, or <c>null</c> if the reply omitted it.</returns>
public async Task<MxStatusProxy?> SuspendAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await SuspendRawAsync(serverHandle, itemHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.Suspend?.Status;
}
/// <summary>
/// Suspends updates for an item without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> SuspendRawAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.Suspend,
Suspend = new SuspendCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
cancellationToken);
}
/// <summary>
/// Resumes updates for a suspended item (MXAccess <c>Activate</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The item's MXSTATUS_PROXY as reported by the worker, or <c>null</c> if the reply omitted it.</returns>
public async Task<MxStatusProxy?> ActivateAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await ActivateRawAsync(serverHandle, itemHandle, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.Activate?.Status;
}
/// <summary>
/// Resumes updates for a suspended item without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> ActivateRawAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.Activate,
Activate = new ActivateCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
},
cancellationToken);
}
/// <summary>
/// Writes a secured value to an item on the MXAccess server (MXAccess <c>WriteSecured</c>).
/// </summary>
/// <remarks>
/// MXAccess parity: <c>WriteSecured</c> fails when it is issued before a value-bearing
/// NMX body or before a prior <c>AuthenticateUser</c> + <c>AdviseSupervisory</c>. That
/// native failure is surfaced unchanged — the client does not pre-validate or reorder it.
/// The <paramref name="value"/> is credential-sensitive and must never reach logs; the
/// client mirrors the single-item WriteSecured redaction contract.
/// </remarks>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The secured value to write.</param>
/// <param name="currentUserId">The current operator user id.</param>
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task WriteSecuredAsync(
int serverHandle,
int itemHandle,
MxValue value,
int currentUserId,
int verifierUserId,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await WriteSecuredRawAsync(
serverHandle,
itemHandle,
value,
currentUserId,
verifierUserId,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Writes a secured value to an item without error checking. See
/// <see cref="WriteSecuredAsync"/> for the parity and redaction contract.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The secured value to write.</param>
/// <param name="currentUserId">The current operator user id.</param>
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> WriteSecuredRawAsync(
int serverHandle,
int itemHandle,
MxValue value,
int currentUserId,
int verifierUserId,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(value);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.WriteSecured,
WriteSecured = new WriteSecuredCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
CurrentUserId = currentUserId,
VerifierUserId = verifierUserId,
Value = value,
},
},
cancellationToken);
}
/// <summary>
/// Writes a secured value and timestamp to an item (MXAccess <c>WriteSecured2</c>).
/// </summary>
/// <remarks>
/// Same parity and redaction contract as <see cref="WriteSecuredAsync"/>: the native
/// failure that occurs before a value-bearing NMX body or a prior authenticate is
/// surfaced unchanged, and the credential-sensitive <paramref name="value"/> must never
/// reach logs.
/// </remarks>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The secured value to write.</param>
/// <param name="timestampValue">The timestamp to write with the value.</param>
/// <param name="currentUserId">The current operator user id.</param>
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task WriteSecured2Async(
int serverHandle,
int itemHandle,
MxValue value,
MxValue timestampValue,
int currentUserId,
int verifierUserId,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await WriteSecured2RawAsync(
serverHandle,
itemHandle,
value,
timestampValue,
currentUserId,
verifierUserId,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
}
/// <summary>
/// Writes a secured value and timestamp to an item without error checking. See
/// <see cref="WriteSecured2Async"/> for the parity and redaction contract.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="value">The secured value to write.</param>
/// <param name="timestampValue">The timestamp to write with the value.</param>
/// <param name="currentUserId">The current operator user id.</param>
/// <param name="verifierUserId">The verifier (secondary approver) user id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> WriteSecured2RawAsync(
int serverHandle,
int itemHandle,
MxValue value,
MxValue timestampValue,
int currentUserId,
int verifierUserId,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(value);
ArgumentNullException.ThrowIfNull(timestampValue);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.WriteSecured2,
WriteSecured2 = new WriteSecured2Command
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
CurrentUserId = currentUserId,
VerifierUserId = verifierUserId,
Value = value,
TimestampValue = timestampValue,
},
},
cancellationToken);
}
/// <summary>
/// Authenticates an MXAccess verify-user (MXAccess <c>AuthenticateUser</c>), returning
/// the resolved user id used by <c>WriteSecured</c> / <c>WriteSecured2</c>.
/// </summary>
/// <remarks>
/// The <paramref name="verifyUserPassword"/> is a raw MXAccess credential. It is never
/// logged and never placed on the exception path: gateway/MXAccess failures surface only
/// reply-derived diagnostics (kind, HRESULT, MXSTATUS_PROXY), never the request payload.
/// </remarks>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="verifyUser">The user to verify.</param>
/// <param name="verifyUserPassword">The verify-user credential. Never logged.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The authenticated user id.</returns>
public async Task<int> AuthenticateUserAsync(
int serverHandle,
string verifyUser,
string verifyUserPassword,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await AuthenticateUserRawAsync(
serverHandle,
verifyUser,
verifyUserPassword,
cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.AuthenticateUser?.UserId ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Authenticates an MXAccess verify-user without error checking. See
/// <see cref="AuthenticateUserAsync"/> for the credential-handling contract.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="verifyUser">The user to verify.</param>
/// <param name="verifyUserPassword">The verify-user credential. Never logged.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> AuthenticateUserRawAsync(
int serverHandle,
string verifyUser,
string verifyUserPassword,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(verifyUser);
ArgumentNullException.ThrowIfNull(verifyUserPassword);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.AuthenticateUser,
AuthenticateUser = new AuthenticateUserCommand
{
ServerHandle = serverHandle,
VerifyUser = verifyUser,
VerifyUserPassword = verifyUserPassword,
},
},
cancellationToken);
}
/// <summary>
/// Resolves an ArchestrA user GUID to its MXAccess user id
/// (MXAccess <c>ArchestrAUserToId</c>).
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The resolved MXAccess user id.</returns>
public async Task<int> ArchestraUserToIdAsync(
int serverHandle,
string userIdGuid,
CancellationToken cancellationToken = default)
{
MxCommandReply reply = await ArchestraUserToIdRawAsync(serverHandle, userIdGuid, cancellationToken)
.ConfigureAwait(false);
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
return reply.ArchestraUserToId?.UserId ?? reply.ReturnValue.Int32Value;
}
/// <summary>
/// Resolves an ArchestrA user GUID to its MXAccess user id without error checking.
/// </summary>
/// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The raw server reply.</returns>
public Task<MxCommandReply> ArchestraUserToIdRawAsync(
int serverHandle,
string userIdGuid,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(userIdGuid);
return InvokeCommandAsync(
new MxCommand
{
Kind = MxCommandKind.ArchestraUserToId,
ArchestraUserToId = new ArchestrAUserToIdCommand
{
ServerHandle = serverHandle,
UserIdGuid = userIdGuid,
},
},
cancellationToken);
}
/// <summary>
/// Invokes an MXAccess command on this session.
/// </summary>
@@ -881,6 +1400,34 @@ public sealed class MxGatewaySession : IAsyncDisposable
cancellationToken);
}
/// <summary>
/// Streams events as typed <see cref="MxEventStreamItem"/> values, surfacing
/// the gateway's reconnect-replay gap sentinel as an observable, typed signal.
/// </summary>
/// <remarks>
/// When resuming with a stale <paramref name="afterWorkerSequence"/> (older than
/// the oldest event still retained in the session replay ring), the gateway emits
/// a single gap sentinel at the head of the stream. It arrives here as an item
/// with <see cref="MxEventStreamItem.IsReplayGap"/> true and
/// <see cref="MxEventStreamItem.ReplayGap"/> populated, meaning the consumer
/// missed events and MUST discard local state and re-snapshot. To resume without
/// incurring another gap, reconnect with
/// <c>afterWorkerSequence = item.ReplayGap.OldestAvailableSequence - 1</c>.
/// All other events pass through with <see cref="MxEventStreamItem.IsReplayGap"/>
/// false. Use <see cref="StreamEventsAsync"/> when raw generated
/// <see cref="MxEvent"/> messages are needed instead.
/// </remarks>
/// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of typed event items.</returns>
public IAsyncEnumerable<MxEventStreamItem> StreamEventItemsAsync(
ulong afterWorkerSequence = 0,
CancellationToken cancellationToken = default)
{
return StreamEventsAsync(afterWorkerSequence, cancellationToken)
.AsStreamItemsAsync(cancellationToken);
}
/// <summary>
/// Closes the session and releases resources.
/// </summary>