feat(sync): add provider contracts and OAuth token broker
Draft the generic SyncProvider/SyncObserver contracts and implement OAuthTokenBroker: valid access tokens with a five-minute refresh skew, rotation persisted before use so a new refresh token is never lost, 400/401 refresh rejections erase the secret and surface AccountDisconnected, and no credential material in error messages.
This commit is contained in:
56
include/nocal/sync/provider.hpp
Normal file
56
include/nocal/sync/provider.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace nocal::sync {
|
||||
|
||||
enum class SyncStage {
|
||||
authenticating,
|
||||
refreshing_identity,
|
||||
discovering_calendars,
|
||||
syncing_calendar,
|
||||
completed,
|
||||
};
|
||||
|
||||
struct SyncProgressEvent {
|
||||
SyncStage stage{SyncStage::authenticating};
|
||||
std::string account_id;
|
||||
std::string calendar_name;
|
||||
std::size_t calendars_completed{0};
|
||||
std::size_t calendars_total{0};
|
||||
};
|
||||
|
||||
class SyncObserver {
|
||||
public:
|
||||
virtual ~SyncObserver() = default;
|
||||
virtual void on_sync_progress(const SyncProgressEvent& event) = 0;
|
||||
};
|
||||
|
||||
struct ConnectedAccount {
|
||||
std::string account_id;
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
class SyncProvider {
|
||||
public:
|
||||
virtual ~SyncProvider() = default;
|
||||
|
||||
[[nodiscard]] virtual std::string_view provider_id() const noexcept = 0;
|
||||
|
||||
// Interactive connect: runs provider auth, stores credentials, registers
|
||||
// account. Must not leave partial connected state on failure.
|
||||
virtual ConnectedAccount connect_account() = 0;
|
||||
|
||||
// Syncs one account's calendars and windows; reports progress via observer.
|
||||
// May continue past individual calendar failures but reports them.
|
||||
virtual void sync_account(std::string_view account_id, SyncObserver& observer) = 0;
|
||||
|
||||
// Removes credentials for the account. Cached provider data is left in place
|
||||
// (deterministic IDs make re-add idempotent). Unknown account is an error.
|
||||
virtual void disconnect_account(std::string_view account_id) = 0;
|
||||
};
|
||||
|
||||
} // namespace nocal::sync
|
||||
58
include/nocal/sync/token_broker.hpp
Normal file
58
include/nocal/sync/token_broker.hpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "nocal/sync/oauth.hpp"
|
||||
#include "nocal/sync/oauth_tokens.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace nocal::sync {
|
||||
|
||||
class OAuthSecretStore;
|
||||
class HttpTransport;
|
||||
|
||||
class AccountDisconnected final : public std::runtime_error {
|
||||
public:
|
||||
explicit AccountDisconnected(std::string message)
|
||||
: std::runtime_error(std::move(message)) {}
|
||||
};
|
||||
|
||||
class OAuthTokenBroker {
|
||||
public:
|
||||
// Constructs a broker for one OAuth provider configuration.
|
||||
// `clock` returns the current epoch seconds for expiry calculations.
|
||||
// It must be monotonic-ish (system_clock is fine).
|
||||
OAuthTokenBroker(const OAuthClientConfig& config,
|
||||
HttpTransport& transport,
|
||||
OAuthSecretStore& secret_store,
|
||||
std::function<std::int64_t()> clock);
|
||||
|
||||
// Returns a valid access token for the account, refreshing and persisting
|
||||
// the rotated tokens when the stored access token is expired or within
|
||||
// 5 minutes of expiry.
|
||||
//
|
||||
// Failure semantics:
|
||||
// - Transport error, 429, 5xx, timeout -> throws std::runtime_error;
|
||||
// secret store is NOT touched.
|
||||
// - 400/401 on refresh (invalid_grant, unauthorized_client, etc.) ->
|
||||
// erases the secret from the store, throws AccountDisconnected
|
||||
// (re-auth required).
|
||||
// - Parsing/validation failure on refresh response -> throws
|
||||
// std::runtime_error; secret is NOT erased (may be transient malformed
|
||||
// response).
|
||||
// - Secret store load failure (missing, locked, cancelled) -> throws
|
||||
// std::runtime_error; no erase.
|
||||
[[nodiscard]] OAuthTokens valid_tokens(std::string_view account_id);
|
||||
|
||||
private:
|
||||
const OAuthClientConfig config_;
|
||||
HttpTransport& transport_;
|
||||
OAuthSecretStore& secret_store_;
|
||||
std::function<std::int64_t()> clock_;
|
||||
static constexpr std::int64_t refresh_skew_seconds = 300; // 5 min
|
||||
};
|
||||
|
||||
} // namespace nocal::sync
|
||||
Reference in New Issue
Block a user