feat(java-client): CLI-15 ReplayGap surface + CLI-04 typed-command parity (5/5 complete)
Completes both findings across all five clients — done locally on the Mac now that homebrew openjdk@17 is available (JAVA_HOME=/opt/homebrew/opt/openjdk@17). CLI-15: new MxEventStreamItem record + MxEventStream.nextItem() surfaces the gateway's ReplayGap sentinel as a typed, non-terminal signal (isReplayGap()/ replayGap()/event()); existing Iterator<MxEvent> path unchanged, sentinel never swallowed/synthesized. Javadoc covers gap semantics + resume contract. CLI-04: typed single-item helpers on MxGatewaySession — Phase 1 adviseSupervisory/writeSecured/writeSecured2/authenticateUser/archestrAUserToId, Phase 2 addBufferedItem/setBufferedUpdateInterval/suspend/activate (unregister already present). Each routes through invokeCommand -> ensureProtocolSuccess + ensureMxAccessSuccess (same validation as bulk). MXAccess parity preserved. Credentials flow only into the request proto; exceptions carry only the reply and gRPC status text is scrubbed via MxGatewaySecrets.redactCredentials — tests assert the password/secured value is absent from getMessage()/toString()/CLI output. New CLI subcommands write-secured/authenticate-user (credential via --password/--password-env, prints only the user id). gradle test: 106 tests, 0 failures (58 client + 48 cli); no generated churn. Shared docs: ClientLibrariesDesign + CLAUDE.md updated to "all five clients". Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
This commit is contained in:
+119
-7
@@ -155,6 +155,8 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
commandLine.addSubcommand("advise", new AdviseCommand(clientFactory));
|
||||
commandLine.addSubcommand(
|
||||
"advise-supervisory", new AdviseSupervisoryCommand(clientFactory));
|
||||
commandLine.addSubcommand("write-secured", new WriteSecuredCommand(clientFactory));
|
||||
commandLine.addSubcommand("authenticate-user", new AuthenticateUserCommand(clientFactory));
|
||||
commandLine.addSubcommand("subscribe-bulk", new SubscribeBulkCommand(clientFactory));
|
||||
commandLine.addSubcommand("unsubscribe-bulk", new UnsubscribeBulkCommand(clientFactory));
|
||||
commandLine.addSubcommand("read-bulk", new ReadBulkCommand(clientFactory));
|
||||
@@ -1074,6 +1076,106 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
name = "write-secured",
|
||||
description = "Invokes MXAccess WriteSecured (verified single-item write).")
|
||||
static final class WriteSecuredCommand extends GatewayCommand {
|
||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
||||
String sessionId;
|
||||
|
||||
@Option(names = "--server-handle", required = true, description = "MXAccess server handle.")
|
||||
int serverHandle;
|
||||
|
||||
@Option(names = "--item-handle", required = true, description = "MXAccess item handle.")
|
||||
int itemHandle;
|
||||
|
||||
@Option(names = "--type", defaultValue = "string", description = "Value type.")
|
||||
String type;
|
||||
|
||||
@Option(names = "--value", required = true, description = "Value text (credential-sensitive; never echoed).")
|
||||
String value;
|
||||
|
||||
@Option(names = "--current-user-id", defaultValue = "0", description = "MXAccess current user id.")
|
||||
int currentUserId;
|
||||
|
||||
@Option(names = "--verifier-user-id", defaultValue = "0", description = "MXAccess verifier user id.")
|
||||
int verifierUserId;
|
||||
|
||||
WriteSecuredCommand(MxGatewayCliClientFactory clientFactory) {
|
||||
super(clientFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
||||
// The secured write value is credential-sensitive: it goes only
|
||||
// into the request. The reply printed below never carries it.
|
||||
MxCommandReply reply = client.session(sessionId)
|
||||
.writeSecuredRaw(
|
||||
serverHandle, itemHandle, currentUserId, verifierUserId, parseValue(type, value));
|
||||
writeOutput("write-secured", common, json, reply, () -> reply.getKind().name());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
name = "authenticate-user",
|
||||
description = "Invokes MXAccess AuthenticateUser and prints the resolved user id.")
|
||||
static final class AuthenticateUserCommand extends GatewayCommand {
|
||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
||||
String sessionId;
|
||||
|
||||
@Option(names = "--server-handle", required = true, description = "MXAccess server handle.")
|
||||
int serverHandle;
|
||||
|
||||
@Option(names = "--verify-user", required = true, description = "Galaxy user name to authenticate.")
|
||||
String verifyUser;
|
||||
|
||||
@Option(
|
||||
names = "--password",
|
||||
description = "Galaxy user password (credential; prefer --password-env). Never echoed.")
|
||||
String password = "";
|
||||
|
||||
@Option(
|
||||
names = "--password-env",
|
||||
defaultValue = "MXGATEWAY_VERIFY_PASSWORD",
|
||||
description = "Environment variable holding the password when --password is omitted.")
|
||||
String passwordEnv;
|
||||
|
||||
AuthenticateUserCommand(MxGatewayCliClientFactory clientFactory) {
|
||||
super(clientFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer call() {
|
||||
// Resolve the credential from the flag or environment. It flows only
|
||||
// into the request; it is never written to output, logs, or errors.
|
||||
String resolvedPassword = password == null || password.isBlank()
|
||||
? System.getenv(passwordEnv)
|
||||
: password;
|
||||
if (resolvedPassword == null) {
|
||||
resolvedPassword = "";
|
||||
}
|
||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
||||
int userId = client.session(sessionId)
|
||||
.authenticateUser(serverHandle, verifyUser, resolvedPassword);
|
||||
PrintWriter out = common.spec.commandLine().getOut();
|
||||
if (json) {
|
||||
Map<String, Object> output = new LinkedHashMap<>();
|
||||
output.put("command", "authenticate-user");
|
||||
output.put("options", common.redactedJsonMap());
|
||||
output.put("verifyUser", verifyUser);
|
||||
output.put("userId", userId);
|
||||
out.println(jsonObject(output));
|
||||
} else {
|
||||
out.println(userId);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Command(name = "subscribe-bulk", description = "Invokes MXAccess SubscribeBulk.")
|
||||
static final class SubscribeBulkCommand extends GatewayCommand {
|
||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
||||
@@ -1864,6 +1966,11 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
|
||||
MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId);
|
||||
|
||||
MxCommandReply writeSecuredRaw(
|
||||
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, MxValue value);
|
||||
|
||||
int authenticateUser(int serverHandle, String verifyUser, String verifyUserPassword);
|
||||
|
||||
List<SubscribeResult> subscribeBulk(int serverHandle, List<String> items);
|
||||
|
||||
List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles);
|
||||
@@ -1982,13 +2089,7 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
|
||||
@Override
|
||||
public MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle) {
|
||||
return session.invokeCommand(MxCommand.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY)
|
||||
.setAdviseSupervisory(
|
||||
mxaccess_gateway.v1.MxaccessGateway.AdviseSupervisoryCommand.newBuilder()
|
||||
.setServerHandle(serverHandle)
|
||||
.setItemHandle(itemHandle))
|
||||
.build());
|
||||
return session.adviseSupervisoryRaw(serverHandle, itemHandle);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1996,6 +2097,17 @@ public final class MxGatewayCli implements Callable<Integer> {
|
||||
return session.writeRaw(serverHandle, itemHandle, value, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MxCommandReply writeSecuredRaw(
|
||||
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, MxValue value) {
|
||||
return session.writeSecuredRaw(serverHandle, itemHandle, currentUserId, verifierUserId, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int authenticateUser(int serverHandle, String verifyUser, String verifyUserPassword) {
|
||||
return session.authenticateUser(serverHandle, verifyUser, verifyUserPassword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> items) {
|
||||
return session.subscribeBulk(serverHandle, items);
|
||||
|
||||
+67
@@ -168,6 +168,49 @@ final class MxGatewayCliTests {
|
||||
assertTrue(run.output().contains("\"kind\":\"MX_COMMAND_KIND_ADVISE_SUPERVISORY\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeSecuredCommandForwardsUserIdsAndValue() {
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(
|
||||
factory,
|
||||
"write-secured",
|
||||
"--session-id", "session-cli",
|
||||
"--server-handle", "12",
|
||||
"--item-handle", "34",
|
||||
"--type", "int32",
|
||||
"--value", "77",
|
||||
"--current-user-id", "100",
|
||||
"--verifier-user-id", "200",
|
||||
"--json");
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
assertEquals(77, factory.client.session.lastWriteSecuredValue.getInt32Value());
|
||||
assertEquals(100, factory.client.session.lastWriteSecuredCurrentUserId);
|
||||
assertEquals(200, factory.client.session.lastWriteSecuredVerifierUserId);
|
||||
assertTrue(run.output().contains("\"kind\":\"MX_COMMAND_KIND_WRITE_SECURED\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void authenticateUserCommandForwardsCredentialAndPrintsUserIdWithoutEchoingPassword() {
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(
|
||||
factory,
|
||||
"authenticate-user",
|
||||
"--session-id", "session-cli",
|
||||
"--server-handle", "3",
|
||||
"--verify-user", "operator",
|
||||
"--password", "super-secret-pw",
|
||||
"--json");
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
// The credential reaches the session (request) but never the output.
|
||||
assertEquals("operator", factory.client.session.lastAuthenticateUser);
|
||||
assertEquals("super-secret-pw", factory.client.session.lastAuthenticatePassword);
|
||||
assertTrue(run.output().contains("\"userId\":4242"), run.output());
|
||||
assertFalse(run.output().contains("super-secret-pw"), "password must never be echoed");
|
||||
assertFalse(run.errors().contains("super-secret-pw"), "password must never be echoed to stderr");
|
||||
}
|
||||
|
||||
// ---- ping subcommand (D4) ----
|
||||
|
||||
@Test
|
||||
@@ -1257,6 +1300,11 @@ final class MxGatewayCliTests {
|
||||
private boolean adviseCalled;
|
||||
private boolean adviseSupervisoryCalled;
|
||||
private MxValue lastWriteValue;
|
||||
private MxValue lastWriteSecuredValue;
|
||||
private int lastWriteSecuredCurrentUserId;
|
||||
private int lastWriteSecuredVerifierUserId;
|
||||
private String lastAuthenticateUser;
|
||||
private String lastAuthenticatePassword;
|
||||
private String lastPingMessage;
|
||||
private long lastReadBulkTimeoutMs;
|
||||
private List<String> lastReadBulkItems;
|
||||
@@ -1341,6 +1389,25 @@ final class MxGatewayCliTests {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MxCommandReply writeSecuredRaw(
|
||||
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, MxValue value) {
|
||||
lastWriteSecuredValue = value;
|
||||
lastWriteSecuredCurrentUserId = currentUserId;
|
||||
lastWriteSecuredVerifierUserId = verifierUserId;
|
||||
return MxCommandReply.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE_SECURED)
|
||||
.setProtocolStatus(ok())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int authenticateUser(int serverHandle, String verifyUser, String verifyUserPassword) {
|
||||
lastAuthenticateUser = verifyUser;
|
||||
lastAuthenticatePassword = verifyUserPassword;
|
||||
return 4242;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> items) {
|
||||
List<SubscribeResult> results = new ArrayList<>();
|
||||
|
||||
Reference in New Issue
Block a user