mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 20:11:07 +00:00
When a provider rejects a call for a stale/unrecognized token it now returns a structured "unauthorized" sentinel (logos_rpc_status.h) instead of a bare QVariant(). LogosAPIClient detects it below the typed wrapper, drops the cached token, re-runs capability_module.requestModule and retries the call once — closing the gap where a stale token was reused forever (the consumer-latched-dead failure mode) and lazily recovering the common provider-reload case. The return VALUE is the only provider->consumer channel available on every transport (qt_local/qt_remote/plain) without an ABI break, since the QtRO dispatch slot returns a single QVariant — hence a value sentinel. Backward compatible: - OLD consumers convert the sentinel identically to QVariant() for every scalar/string/LogosResult return, so they keep seeing today's empty/failed result. - OLD providers return bare QVariant(); a NEW consumer never matches the sentinel and so never re-exchanges against them. The retry is bounded to one attempt and fires ONLY on the explicit sentinel (never a legitimately-empty result), so no loops and no misfire. Downstream note: logos-qt-sdk's test_auth_token_enforcement.cpp asserts !isValid() on unauthorized calls; those become isUnauthorizedSentinel() when it re-pins (the security property — no provider dispatch — is unchanged). Tests: tests/protocol/test_token_reexchange.cpp covers provider-side emission, sync/async re-exchange+retry, bounded retry (no loop), the false-positive guard (a legit empty return must not re-exchange), and old-consumer decode. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
2.7 KiB
C++
68 lines
2.7 KiB
C++
#ifndef LOGOS_RPC_STATUS_H
|
|
#define LOGOS_RPC_STATUS_H
|
|
|
|
#include <QVariant>
|
|
#include <QVariantMap>
|
|
#include <QJsonObject>
|
|
#include <QString>
|
|
#include <QMetaType>
|
|
|
|
// A provider returns this reserved single-key map INSTEAD of a bare QVariant()
|
|
// when it rejects a call for an unrecognized/stale auth token. Backward
|
|
// compatible by construction:
|
|
//
|
|
// * OLD consumers convert it exactly like a bare QVariant() for every
|
|
// scalar / string / LogosResult return (`.toString()` -> "",
|
|
// `qvariant_cast<T>` -> T{}, `.value<LogosResult>()` -> default), so they
|
|
// keep seeing today's "empty/failed" result — nothing new to handle.
|
|
// * NEW consumers (LogosAPIClient) detect it BELOW the generated typed
|
|
// wrapper, drop the stale token, re-run capability_module.requestModule and
|
|
// retry the call once. The sentinel is stripped before it ever reaches the
|
|
// wrapper.
|
|
//
|
|
// The key is namespaced + double-underscored so it never collides with a real
|
|
// field, and the detector requires size()==1 with the exact key AND value, so a
|
|
// legitimate map result can't false-match. It is the ONLY provider->consumer
|
|
// signal available on every transport (qt_local/qt_remote/plain) without an ABI
|
|
// break, because the QtRO dispatch slot returns a single QVariant.
|
|
namespace logos {
|
|
|
|
inline constexpr char kRpcStatusKey[] = "__logos_rpc_status__";
|
|
inline constexpr char kRpcStatusUnauthorized[] = "unauthorized";
|
|
|
|
// The provider-side rejection value. QVariantMap round-trips faithfully on all
|
|
// three transports (qt_local pass-through; qt_remote QtRO-serialized; plain via
|
|
// qvariant_rpc_value.cpp map<->JSON), so the consumer always sees a QVariantMap.
|
|
inline QVariant makeUnauthorizedSentinel()
|
|
{
|
|
QVariantMap m;
|
|
m.insert(QString::fromLatin1(kRpcStatusKey),
|
|
QString::fromLatin1(kRpcStatusUnauthorized));
|
|
return m;
|
|
}
|
|
|
|
// True only for the exact rejection sentinel. Handles QVariantMap (the normal
|
|
// case on every transport) and QJsonObject (defensive: some json_convert paths
|
|
// historically produced QJsonObject). Never matches a normal empty result.
|
|
inline bool isUnauthorizedSentinel(const QVariant& v)
|
|
{
|
|
const QString key = QString::fromLatin1(kRpcStatusKey);
|
|
const QString want = QString::fromLatin1(kRpcStatusUnauthorized);
|
|
switch (v.userType()) {
|
|
case QMetaType::QVariantMap: {
|
|
const QVariantMap m = v.toMap();
|
|
return m.size() == 1 && m.value(key).toString() == want;
|
|
}
|
|
case QMetaType::QJsonObject: {
|
|
const QJsonObject o = v.toJsonObject();
|
|
return o.size() == 1 && o.value(key).toString() == want;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
} // namespace logos
|
|
|
|
#endif // LOGOS_RPC_STATUS_H
|