mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-31 05:51:08 +00:00
TokenManager was one flat map with no direction tag, written from both
sides of every relationship: the client stored the token it PRESENTS to a
callee under the CALLEE's name, and a token RECEIVED from a caller was
stored under the CALLER's name. Same namespace, last write wins.
This was documented as safe by topology — separate images keep the two
directions apart — and pending only for the shared-runtime migration. That
was wrong. It is live in the DEFAULT out-of-process configuration, and the
chain needs no attacker:
1. capability_module mints one token so M may call B.
2. It pushes it to B -> B's store gets ["M"] = T (inbound)
3. It returns it to M -> M's store gets ["B"] = T (outbound)
4. B then calls M. B's client looks in its OWN store for something to
present to M, finds T because step 2 put it there, and therefore SKIPS
requestModule entirely.
5. M authorizes T, having cached the identical value in step 3.
One grant M->B silently produces B->M, with no handshake, nothing logged,
and capability_module's access policy never consulted. Reproduced on the
pre-split tree, both halves red:
B's outbound lookup for M returned the token M was issued for calling B
Actual: false Expected: true
B authorized at M using the token minted for M -> B
Actual: true Expected: false
A second defect from the same cause: an inbound push CLOBBERS the outbound
cache under the same key, so M's next call to B goes out carrying B's own
inbound token, is rejected, and burns the single re-exchange recovering
from a collision it caused itself.
THREE ROLES, TWO MAPS AND A SCALAR:
m_outbound callee -> what I present when calling out
m_inbound caller -> what I issued to that caller
m_credential MY OWN host-issued credential
The anchor is what made a two-way split look impossible: it is genuinely
both directions, presented outbound to capability_module and compared
against inbound. But it is not a MAP. It is one value under two role labels
— "core" and "capability_module" — that collide with module names by
accident, and adoptCredential already wrote a single credential under every
bootstrap key. As a scalar it has no key namespace, so no reverse lookup
can produce a name from it. A key living in both maps would have been a
rename, not a split.
The hoped-for cheap route did not exist: logos_module_accept_token forwards
to lp_token_save in BOTH backends, and the Qt glue calls that one export
for an inbound caller token in one place and the module's own anchor in
another. The doors were never divided, so this could not be done by routing
two existing C ABI doors to two maps — but it needs no new door either.
Four detector tests red before, green after. Constant time survives at the
instruction level and the comparison count stays input-independent.
KNOWN GAP, deliberately not fixed here: the generated glue writes the same
inbound token through BOTH doors (lidl_gen_cdylib_glue.cpp:371-380). Safe
only while module cdylibs are separate images — which is exactly what the
shared-runtime migration collapses. Closing it means a dedicated inbound
export on the module-impl C ABI: a MINOR bump plus a definition in both
backends in the same wave. Price it before that migration lands, not after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
426 lines
22 KiB
C++
426 lines
22 KiB
C++
#ifndef LOGOS_API_CLIENT_H
|
|
#define LOGOS_API_CLIENT_H
|
|
|
|
#include <QObject>
|
|
#include <QString>
|
|
#include <QVariant>
|
|
#include <QVariantList>
|
|
#include <QMap>
|
|
#include <QStringList>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "logos_call_error.h"
|
|
#include "logos_mode.h"
|
|
#include "logos_shared_api.h"
|
|
#include "logos_subscription_state.h"
|
|
#include "logos_transport_config.h"
|
|
#include <nlohmann/json.hpp>
|
|
|
|
class LogosAPI;
|
|
class LogosAPIConsumer;
|
|
class LogosObject;
|
|
class TokenManager;
|
|
|
|
/**
|
|
* @brief LogosAPIClient provides a high-level interface for remote method calls
|
|
*
|
|
* This class serves as a facade over LogosAPIConsumer, providing a clean interface
|
|
* for applications that need to call remote methods and handle events.
|
|
*
|
|
* LOGOS_SHARED_API for the same reason as TokenManager, plus one of its own:
|
|
* this is the object that reaches the shared TokenManager on the call path, and
|
|
* archives are pulled a whole OBJECT FILE at a time. If the consumer imports
|
|
* TokenManager but not LogosAPIClient, ld pulls logos_api_client.cpp.obj out of
|
|
* liblogos_protocol.a for the client, that object drags token_manager.cpp.obj
|
|
* back in with it, and the duplicate static reappears. See logos_shared_api.h.
|
|
*/
|
|
class LOGOS_SHARED_API LogosAPIClient : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
/**
|
|
* @brief Construct a client with explicit transports for both the
|
|
* target module *and* `capability_module`.
|
|
*
|
|
* Two transports because the SDK's auto-`requestModule` flow inside
|
|
* invokeRemoteMethod{,Async} dials `capability_module` to fetch a
|
|
* per-target token. When the daemon advertises capability_module on
|
|
* a different transport from the target (e.g. CLI on host →
|
|
* core_service over TCP, but capability_module also over TCP on a
|
|
* sibling port), the auto-dial must use the right one. Pre-building
|
|
* the consumer once in the constructor (see m_capability_consumer)
|
|
* keeps the hot path free of per-call lookups.
|
|
*/
|
|
LogosAPIClient(const QString& module_to_talk_to,
|
|
const QString& origin_module,
|
|
TokenManager* token_manager,
|
|
const LogosTransportConfig& target_transport,
|
|
const LogosTransportConfig& capability_transport,
|
|
QObject *parent = nullptr);
|
|
|
|
/**
|
|
* @brief No-transport constructor — both target and
|
|
* capability_module use the process-global default
|
|
* (LocalSocket) via LogosTransportConfigGlobal::getDefault().
|
|
*/
|
|
explicit LogosAPIClient(const QString& module_to_talk_to,
|
|
const QString& origin_module,
|
|
TokenManager* token_manager,
|
|
QObject *parent = nullptr);
|
|
~LogosAPIClient();
|
|
|
|
/**
|
|
* @brief Request a LogosObject handle by name
|
|
* @return LogosObject* handle, or nullptr if failed
|
|
*/
|
|
LogosObject* requestObject(const QString& objectName, Timeout timeout = Timeout());
|
|
|
|
bool isConnected() const;
|
|
QString registryUrl() const;
|
|
bool reconnect();
|
|
|
|
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args = QVariantList(), Timeout timeout = Timeout());
|
|
|
|
/**
|
|
* @brief invokeRemoteMethod with an explicit error out-channel.
|
|
*
|
|
* Fills *err with the canonical {code, message, origin} call error when
|
|
* the failure is detectable (today: "object_unavailable" when the target
|
|
* object cannot be acquired); cleared on success. Generated typed client
|
|
* wrappers call this overload and throw logos::LogosCallError so callers
|
|
* can distinguish a failed call from a legitimately default-valued
|
|
* result.
|
|
*/
|
|
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, Timeout timeout, logos::CallError* err);
|
|
|
|
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg, Timeout timeout = Timeout());
|
|
|
|
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout());
|
|
|
|
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout());
|
|
|
|
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, Timeout timeout = Timeout());
|
|
|
|
QVariant invokeRemoteMethod(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout());
|
|
|
|
// const char* overloads — resolve ambiguity for string literals
|
|
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
|
|
const QVariantList& args = QVariantList(), Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString(objectName), QString(methodName), args, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
|
|
const QVariant& arg, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
|
|
const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, arg4, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const char* objectName, const char* methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString(objectName), QString(methodName), arg1, arg2, arg3, arg4, arg5, timeout); }
|
|
|
|
// std::string overloads — thin wrappers that convert internally
|
|
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
|
|
const QVariantList& args = QVariantList(), Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), args, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
|
|
const QVariant& arg, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, arg4, timeout); }
|
|
|
|
QVariant invokeRemoteMethod(const std::string& objectName, const std::string& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, const QVariant& arg5, Timeout timeout = Timeout())
|
|
{ return invokeRemoteMethod(QString::fromStdString(objectName), QString::fromStdString(methodName), arg1, arg2, arg3, arg4, arg5, timeout); }
|
|
|
|
// const char* onEvent overload
|
|
void onEvent(LogosObject* originObject, const char* eventName,
|
|
std::function<void(const QString&, const QVariantList&)> callback)
|
|
{ onEvent(originObject, QString(eventName), callback); }
|
|
|
|
// std::string onEvent overloads
|
|
void onEvent(LogosObject* originObject, const std::string& eventName,
|
|
std::function<void(const QString&, const QVariantList&)> callback)
|
|
{ onEvent(originObject, QString::fromStdString(eventName), callback); }
|
|
|
|
void onEvent(LogosObject* originObject, const std::string& eventName,
|
|
std::function<void(const std::string&, const QVariantList&)> callback)
|
|
{
|
|
onEvent(originObject, QString::fromStdString(eventName),
|
|
[cb = std::move(callback)](const QString& name, const QVariantList& args) {
|
|
cb(name.toStdString(), args);
|
|
});
|
|
}
|
|
|
|
void onEvent(LogosObject* originObject, const char* eventName,
|
|
std::function<void(const std::string&, const QVariantList&)> callback)
|
|
{
|
|
onEvent(originObject, QString(eventName),
|
|
[cb = std::move(callback)](const QString& name, const QVariantList& args) {
|
|
cb(name.toStdString(), args);
|
|
});
|
|
}
|
|
|
|
using AsyncResultCallback = std::function<void(QVariant)>;
|
|
|
|
/**
|
|
* @brief Async callback with an explicit error out-channel.
|
|
*
|
|
* Mirrors the sync `invokeRemoteMethod(..., CallError*)` overload. Set to
|
|
* code="object_unavailable" when the target object cannot be acquired,
|
|
* cleared on success. Callers that need to distinguish acquire failure
|
|
* from a legitimately empty QVariant result should use this overload.
|
|
*/
|
|
using AsyncResultErrorCallback = std::function<void(QVariant, const logos::CallError&)>;
|
|
|
|
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, AsyncResultCallback callback,
|
|
Timeout timeout = Timeout());
|
|
|
|
/**
|
|
* @brief invokeRemoteMethodAsync with an explicit error out-channel.
|
|
* See AsyncResultErrorCallback docs above.
|
|
*/
|
|
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, AsyncResultErrorCallback callback,
|
|
Timeout timeout = Timeout());
|
|
|
|
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg, AsyncResultCallback callback,
|
|
Timeout timeout = Timeout());
|
|
|
|
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2,
|
|
AsyncResultCallback callback, Timeout timeout = Timeout());
|
|
|
|
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
AsyncResultCallback callback, Timeout timeout = Timeout());
|
|
|
|
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, AsyncResultCallback callback,
|
|
Timeout timeout = Timeout());
|
|
|
|
void invokeRemoteMethodAsync(const QString& objectName, const QString& methodName,
|
|
const QVariant& arg1, const QVariant& arg2, const QVariant& arg3,
|
|
const QVariant& arg4, const QVariant& arg5,
|
|
AsyncResultCallback callback, Timeout timeout = Timeout());
|
|
|
|
/**
|
|
* @brief Register an event listener via LogosObject's callback mechanism
|
|
* @param originObject The LogosObject that will emit the event
|
|
* @param eventName The name of the event to listen for
|
|
* @param callback Function to call when the event is triggered
|
|
*/
|
|
void onEvent(LogosObject* originObject, const QString& eventName,
|
|
std::function<void(const QString&, const QVariantList&)> callback);
|
|
|
|
/**
|
|
* @brief Subscribe to an event on a module that may not be reachable YET.
|
|
*
|
|
* The safe alternative to requestObject() + onEvent() for any caller that
|
|
* subscribes during startup — a UI plugin's Component.onCompleted, a
|
|
* module's initLogos(), a backend's onContextReady(). Never blocks; arms
|
|
* when the module appears, including a mid-session install; warns once on
|
|
* deferral and logs when it arms. See LogosAPIConsumer::onEventWhenAvailable
|
|
* for the full contract, the cost of waiting, and the (deliberate) lack of
|
|
* de-duplication.
|
|
*
|
|
* Adds no member state to this class — see the ABI note below; it forwards
|
|
* to the consumer that already exists.
|
|
*
|
|
* @return A non-zero id for cancelEventSubscription() /
|
|
* eventSubscriptionState(), or 0 if the arguments were refused.
|
|
*/
|
|
quint64 onEventWhenAvailable(const QString& objectName, const QString& eventName,
|
|
std::function<void(const QString&, const QVariantList&)> callback,
|
|
std::function<void(bool)> onArmed = {});
|
|
|
|
quint64 onEventWhenAvailable(const std::string& objectName, const std::string& eventName,
|
|
std::function<void(const std::string&, const QVariantList&)> callback,
|
|
std::function<void(bool)> onArmed = {})
|
|
{
|
|
return onEventWhenAvailable(QString::fromStdString(objectName),
|
|
QString::fromStdString(eventName),
|
|
[cb = std::move(callback)](const QString& name, const QVariantList& args) {
|
|
cb(name.toStdString(), args);
|
|
},
|
|
std::move(onArmed));
|
|
}
|
|
|
|
/**
|
|
* @brief Call `onReady` once, as soon as the module becomes acquirable.
|
|
*
|
|
* The call-path counterpart of onEventWhenAvailable(), for a caller that
|
|
* must invoke a method on a module which may still be starting: it neither
|
|
* fails fast (stranding a UI that will never retry) nor calls straight
|
|
* through into the transport's acquire timeout on the calling thread. Fires
|
|
* exactly once, is not re-armed on reconnect, and holds no subscription.
|
|
* See LogosAPIConsumer::whenObjectAvailable for the full contract.
|
|
*
|
|
* @return A non-zero id accepted by cancelEventSubscription(), or 0.
|
|
*/
|
|
quint64 whenObjectAvailable(const QString& objectName,
|
|
std::function<void(bool)> onReady);
|
|
|
|
/**
|
|
* @brief Stop tracking the subscription with this id.
|
|
*
|
|
* See LogosAPIConsumer::cancelEventSubscription — in particular that it
|
|
* stops the BOOKKEEPING (the retry timer, the watchdog, the reconnect
|
|
* re-arm set) and does not detach the callback from the shared handle.
|
|
*/
|
|
bool cancelEventSubscription(quint64 subscriptionId);
|
|
|
|
/**
|
|
* @brief Whether a subscription id is still pending, armed, or forgotten.
|
|
* For callers that keep their own de-duplication record and need to
|
|
* check it rather than trust it.
|
|
*/
|
|
LogosSubscriptionState eventSubscriptionState(quint64 subscriptionId) const;
|
|
|
|
/**
|
|
* @brief Diagnostics: "<object>::<event>" for every deferred subscription
|
|
* that has not armed yet.
|
|
*/
|
|
QStringList pendingEventSubscriptions() const;
|
|
|
|
/**
|
|
* @brief Emit an event on a LogosObject (for plugins that act as event sources)
|
|
* @param object The LogosObject to emit the event on
|
|
* @param eventName The name of the event
|
|
* @param data The event data
|
|
*/
|
|
void onEventResponse(LogosObject* object, const QString& eventName, const QVariantList& data);
|
|
|
|
/**
|
|
* @brief Backward-compatible overload for QObject-based plugins.
|
|
*
|
|
* Old-API plugins call onEventResponse(this, ...) where `this` is a QObject*.
|
|
* This overload invokes the eventResponse signal on the QObject via QMetaObject.
|
|
*/
|
|
void onEventResponse(QObject* object, const QString& eventName, const QVariantList& data);
|
|
|
|
bool informModuleToken(const QString& authToken, const QString& moduleName, const QString& token);
|
|
bool informModuleToken(const char* authToken, const char* moduleName, const char* token)
|
|
{ return informModuleToken(QString(authToken), QString(moduleName), QString(token)); }
|
|
bool informModuleToken(const std::string& authToken, const std::string& moduleName, const std::string& token);
|
|
bool informModuleToken_module(const QString& authToken, const QString& originModule, const QString& moduleName, const QString& token, int timeoutMs = 20000);
|
|
|
|
TokenManager* getTokenManager() const;
|
|
// The OUTBOUND token for `module_name`: what this client presents when it
|
|
// CALLS `module_name`. Never a token some caller was issued to call US —
|
|
// that half of the store has no reader here, by construction (see the
|
|
// DIRECTION note in token_manager.h).
|
|
QString getToken(const QString& module_name);
|
|
|
|
// nlohmann::json overloads — args is a JSON array, result is a JSON value.
|
|
// These convert between nlohmann::json and QVariant internally so callers
|
|
// never need to touch Qt JSON types.
|
|
nlohmann::json invokeRemoteMethod(const std::string& objectName,
|
|
const std::string& methodName,
|
|
const nlohmann::json& args,
|
|
Timeout timeout = Timeout());
|
|
|
|
// nlohmann::json event callback overload — data arrives as a json array.
|
|
void onEvent(LogosObject* originObject, const std::string& eventName,
|
|
std::function<void(const std::string&, const nlohmann::json&)> callback);
|
|
|
|
private:
|
|
// requestModule handshake against capability_module + cache the minted token
|
|
// in the shared TokenManager. Returns the token ("" on failure). Factors out
|
|
// the first-exchange logic so both the initial fetch and the
|
|
// rejection-driven re-exchange share one path. (Private method — no effect on
|
|
// the ABI-sensitive data layout below.)
|
|
// `timeout` is the CALLER's budget, and it bounds the handshake too. It has
|
|
// to: the exchange runs before the call on an un-tokened target, so a bound
|
|
// that skipped it would describe only the second half of the operation.
|
|
QString mintAndCacheToken(const QString& objectName, Timeout timeout);
|
|
|
|
// Async invoke with a bounded retry budget backing the public
|
|
// invokeRemoteMethodAsync overloads. On a provider rejection sentinel it
|
|
// drops the stale token and re-enters itself with retriesLeft-1, so the retry
|
|
// coalesces through the same m_pendingHandshakes machinery.
|
|
void invokeRemoteMethodAsyncImpl(const QString& objectName, const QString& methodName,
|
|
const QVariantList& args, AsyncResultErrorCallback callback,
|
|
Timeout timeout, int retriesLeft);
|
|
|
|
// ABI note: this private layout is consumed by every plugin that
|
|
// statically links libsdk. Adding a new field in the middle of
|
|
// this section shifts the offsets of subsequent fields and
|
|
// SILENTLY breaks any plugin compiled before the change — it
|
|
// reads m_token_manager at the wrong offset and segfaults on
|
|
// the first cross-process call. New private members MUST be
|
|
// appended to the end. (Long-term cure: pimpl this class so
|
|
// sizeof / offsets become opaque to consumers.)
|
|
LogosAPIConsumer* m_consumer;
|
|
QMap<QString, QString> m_tokens;
|
|
TokenManager* m_token_manager;
|
|
QString m_origin_module;
|
|
// Pre-built consumer for the auto-`requestModule` token-fetch path
|
|
// in invokeRemoteMethod{,Async}. Constructed once with the right
|
|
// transport (see the two-transport ctor) so the hot path doesn't
|
|
// chase a back-pointer to LogosAPI just to look up the transport
|
|
// registry. Null only when `m_consumer` itself is for
|
|
// capability_module (no recursion). In-class default to nullptr
|
|
// so any old constructor that doesn't list this field still
|
|
// leaves a defined value.
|
|
LogosAPIConsumer* m_capability_consumer = nullptr;
|
|
|
|
// Per-target queue of continuations waiting on an in-flight async
|
|
// requestModule handshake. The FIRST async call to an un-tokened target
|
|
// starts exactly one handshake; concurrent calls to the same target queue
|
|
// here and all drain with the single minted token when it resolves. Without
|
|
// this coalescing a fan-out of N first-calls fires N racing handshakes whose
|
|
// distinct tokens overwrite each other on the target — so already-dispatched
|
|
// calls carry a superseded token and get rejected. Touched only on the
|
|
// owner thread (invokeRemoteMethodAsync marshals there), so it needs no
|
|
// lock. Appended last per the ABI note above; defaults to empty.
|
|
QMap<QString, std::vector<std::function<void(const QString&)>>> m_pendingHandshakes;
|
|
|
|
// One-shot latch for the "isolated store with no credential" warning in
|
|
// mintAndCacheToken(). The condition repeats on every call and the message
|
|
// is about the HOST's wiring, so it is worth saying once and not N times.
|
|
// Appended last per the ABI note above; defaults to false.
|
|
bool m_warnedNoCredential = false;
|
|
};
|
|
|
|
#endif // LOGOS_API_CLIENT_H
|