Class CloudAuthManager
/ulticloud login|logout|status.
As of plan 16-09 (D-17) this class owns no state of its own and exposes no generation-shaped or
TokenEntity-shaped public surface -- everything that used to be a fine-grained public
static (the generation triple, saveToken/clearToken/refreshToken/
loadSavedToken, the start/stop pairs for polling and refresh) is gone outright, not
deprecated: CloudSession instance methods now carry that behaviour, reached only through
login(DeviceLoginListener), logout() and
status() below. Keeping a delegating shim for any of them would leave the generation
convention callable, which is exactly what issue #298's second acceptance criterion requires to
stop being true (see plan 16-08's CloudSession javadoc for the mechanism this replaced).
CredentialStaticSurfaceInvariantTest enforces this structurally: no public static method
anywhere in this package may accept or return a TokenEntity or a generation (D-18), so a
static bypass of the session's credential commit cannot be reintroduced unnoticed. As of 6.3.1 the
6.3.0 user token is neither stored nor sent (plan 22-24 removed its store); the server credential is
committed only through CloudSession.commitCredential(ServerCredential).
Marked ApiStatus.Internal -- this class exists to serve CloudLoginCommand's three
@CmdMapping methods and is not part of the framework's public API surface for module
authors.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final classAn immutable view ofstatus()'s result.static interfaceThe outcomes oflogin(DeviceLoginListener).static enumCompleted unlink outcomes; none contains a credential.static final classImmutable result of a manual reconnect request; an accepted attempt runs asynchronously. -
Field Summary
FieldsModifier and TypeFieldDescription(package private) static final StringThe reasonloginWithCode(java.lang.String, com.ultikits.ultitools.utils.CloudAuthManager.DeviceLoginListener)reports for a code that does not have the contract's shape.(package private) static final StringThe message a login reports when an overlapping login already replaced its session.(package private) static final intThe longestsuggestedNameUltiCloud accepts, in code points (contract answer 13).private static final long -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate static booleanprivate static booleandeleteLocally(CloudSession owner, ServerCredential credential) Deletes the retired credential from the local store.static booleanisWellFormedRepairCode(String code) (package private) static ServerCredentiallinkedCredential(CloudSession session) The current credential for status, login and logout.static voidlogin(CloudAuthManager.DeviceLoginListener listener) Links this server to UltiCloud with the RFC 8628 device authorization grant (protocol v2).static voidloginWithCode(String deviceCode, CloudAuthManager.DeviceLoginListener listener) Links this server with a device code UltiCloud has already approved: the single-use code the panel's "Revoke and re-pair" action shows, typed as/ulticloud login <code>(issue #869).static booleanlogout()Tears down cloud features unconditionally, then asynchronously revokes and deletes the protocol v2 credential.static voidStops the captured session synchronously, then revokes and deletes its credential on a Bukkit asynchronous task.(package private) static StringnormaliseSuggestedName(String motd) Requests an immediate connection attempt without waiting for the network.(package private) static Stringprivate static voidstart(CloudAuthManager.DeviceLoginListener listener, Consumer<CloudSession> begin) The checks and the session replacement shared by both logins;beginthen starts the network part on the new session.static CloudAuthManager.CloudStatusstatus()(package private) static String(package private) static String
-
Field Details
-
MAX_SUGGESTED_NAME_CODE_POINTS
static final int MAX_SUGGESTED_NAME_CODE_POINTSThe longestsuggestedNameUltiCloud accepts, in code points (contract answer 13).- See Also:
-
LOGIN_IN_PROGRESS
The message a login reports when an overlapping login already replaced its session.- See Also:
-
INVALID_REPAIR_CODE
The reasonloginWithCode(java.lang.String, com.ultikits.ultitools.utils.CloudAuthManager.DeviceLoginListener)reports for a code that does not have the contract's shape.- See Also:
-
RECONNECT_COOLDOWN_MS
private static final long RECONNECT_COOLDOWN_MS- See Also:
-
-
Constructor Details
-
CloudAuthManager
private CloudAuthManager()
-
-
Method Details
-
login
Links this server to UltiCloud with the RFC 8628 device authorization grant (protocol v2).Runs the already-linked check (a credential held by the current session or stored in
credentials.json), the rate-limit check, then replaces the current session and returns. The device-code request and the polling run on the new session's own poll executor, so a caller on the server's primary thread is never blocked on the network; their outcome reacheslisteneron that executor's thread.The lock structure is the one the 6.3.0 magic-link login used, unchanged:
CloudSession.class, then the captured session's monitor, the re-check, thesessionAtCheck != CloudSession.current()guard,disableCloud(sessionAtCheck),startNew()-- the documented lock order ofCloudSession's class javadoc.- Parameters:
listener- receives every outcome; must not benull- Since:
- 6.3.1
-
loginWithCode
Links this server with a device code UltiCloud has already approved: the single-use code the panel's "Revoke and re-pair" action shows, typed as/ulticloud login <code>(issue #869). No new device code is requested; the code is exchanged through the samePOST /v1/device/tokenpoll a normal login uses, and a grant is stored, and the connection started, exactly as for a normal login. The credential it yields belongs to the server record the code was bound to, so the server keeps its id, name and history.The already-linked check, the rate limit and the session replacement are those of
login(DeviceLoginListener);CloudAuthManager.DeviceLoginListener.onCode(java.lang.String, java.lang.String, java.lang.String, long)is never called. A code that does not have the contract's shape is refused throughCloudAuthManager.DeviceLoginListener.onRequestFailed(java.lang.String)before anything else, and never sent. A used, unknown or expired code ends inCloudAuthManager.DeviceLoginListener.onExpired(). The code is a bearer secret: it is never logged or echoed.- Parameters:
deviceCode- the code from the panellistener- receives every outcome; must not benull- Since:
- 6.4.0
-
isWellFormedRepairCode
- Parameters:
code- the text an operator typed after/ulticloud login, ornull- Returns:
truewhen it has the shape of an UltiCloud device code (^[A-Za-z0-9_-]{43,128}$)- Since:
- 6.4.0
-
start
private static void start(CloudAuthManager.DeviceLoginListener listener, Consumer<CloudSession> begin) The checks and the session replacement shared by both logins;beginthen starts the network part on the new session. -
linkedCredential
The current credential for status, login and logout. The session is its one owner (plan 22-21 fix): every change to the stored entry -- device login, rotation, logout, rejection -- updates the session's copy in the same step, so the two never diverge. The file is read only when the session holds no credential (it has not adopted one yet, for example while cloud features are off at startup), so logout still revokes and deletes a stored credential the running session never loaded.- Returns:
- the credential
sessionholds, else the one stored incredentials.json, elsenull
-
runningFrameworkVersion
- Returns:
- the running framework version, sent verbatim (a
-SNAPSHOTsuffix included): the plugin description's version, elseenv.yml's, elseunknown
-
suggestedServerName
- Returns:
- the server's MOTD as plain text, at most 64 code
points, or
nullwhen there is no server or the MOTD is blank (the field is then omitted; the owner names the server on the confirmation page either way)
-
normaliseSuggestedName
- Parameters:
motd- a legacy-formatted MOTD, ornull- Returns:
- the MOTD without colour codes, with every run of whitespace or control characters
collapsed to one space, trimmed and cut to 64
code points without splitting a surrogate pair;
nullwhen nothing remains
-
storedLegacyServerUuid
- Returns:
- this server's stored install id (
uuidincredentials.json) normalised byLegacyServerId.canonical(String), ornull; reads only, never writes
-
logout
Tears down cloud features unconditionally, then asynchronously revokes and deletes the protocol v2 credential. The revoke and delete finish after this method returns; uselogoutReporting(Consumer)to observe their outcome. In both forms teardown must run even when there is nothing to clear (an expired-but-still-connected session is the case that most needs logout to take effect), and the credential is read only after teardown so an in-flight login that commits mid-teardown is still caught and cleared -- seeCloudLoginCommand.logout(CommandSender)'s own comments for why that order matters.CR-01 (16-REVIEW-cloud.md): this method captures
CloudSession.current()exactly once -- in the statement below, immediately before teardown begins -- and passes that captured reference toPluginInitiationUtils.disableCloud(CloudSession), which acts on and returns that exact instance. The revoke-and-delete decision then acts on the returned reference, never on a second, independent call toCloudSession.current()taken after teardown returns. Readingcurrent()a second time, afterward, used to be exactly the bug: a concurrentlogin()(unsynchronized, and reachable from a different thread via@RunAsync) can install a brand-new session withCloudSession.startNew()in the gap while teardown is running, and that second read would then see the NEW session's (alwaysnull) credential instead of the one actually being logged out of -- concluding "nothing to clear" and leaving the real credential on disk. Acting on the one reference this method already holds makes that race structurally impossible: there is no second read left to disagree with the first. This method deliberately does not callCloudSession.startNew()itself any more either; clearing the torn-down session in place is sufficient, and the next real login installs a fresh session via its ownCloudSession.startNew()call, exactly as it always has.Round-1 external review finding: the paragraph above establishes WHICH session this method acts on, but not what its delete is safe to remove. A concurrent
login()can write a fresh credential to the same shared document while this session's teardown is in flight, so the delete is a compare-and-delete keyed on the retiring credential's id (ServerCredentialStore.deleteIfMatches(String)): it never removes a credential the torn-down session did not hold. Alogin()that installs its new session after this method's single read ofCloudSession.current()has lost the session race regardless --CloudSession.startNew()invalidates whatever session it replaces.- Returns:
trueif a credential existed and cleanup was started, or a pending authentication attempt was cancelled (real-machine UAT fix, plan 16-08 -- seeCloudSession.hasAnythingToClear());falseif there was nothing to clear and nothing pending (teardown still ran regardless)- Throws:
IOException- never as of 6.3.1 (a failed local delete is reported by the asynchronous task); kept in the 6.3.0 signature
-
logoutReporting
Stops the captured session synchronously, then revokes and deletes its credential on a Bukkit asynchronous task. The callback runs on that task, or inline when no credential is held. A local storage failure is reported asCloudAuthManager.LogoutOutcome.LOCAL_DELETE_FAILED, never as deletion.- Parameters:
onDone- receives the completed outcome- Since:
- 6.3.1
-
beginLogout
-
deleteLocally
Deletes the retired credential from the local store. A storage failure is logged with its cause class only and reported asCloudAuthManager.LogoutOutcome.LOCAL_DELETE_FAILED; the session no longer holds the credential either way, so running logout again retries the delete. -
reconnect
Requests an immediate connection attempt without waiting for the network. The ten-second cooldown applies to every invocation, including one refused by the current connection state.- Returns:
- whether an attempt was queued, or why the request was refused
- Since:
- 6.3.1
-
status
- Returns:
- an immutable snapshot of the current session's connection state, for
/ulticloud statusto render -- never the token itself (D-18)
-