Thank you for being patient! We're working hard on resolving the issue
The connector splits credential lookup (host-side) from credential shape (provider-side).
AuthProvider (provider-side)A unit struct in your connector module that names the credential type:
pub struct MyProviderAuth;
impl AuthProvider for MyProviderAuth {
const KIND: &'static str = "myprovider";
type AuthId = str;
type Credentials = Auth<MyProviderOAuthCredentials>;
}
Three pieces:
KIND — the stable string id under which the host stores
this provider's credentials. Match-arm key for the host's
UserAuth::sourced_credentials impl.type AuthId — the per-account identifier type. Almost
always str. Lookups receive &str (e.g. an authId derived
from the rowkey segment).type Credentials — the typed payload returned to the
caller via UserAuthExt::auth::<MyProviderAuth>(...). Usually
Auth<YourOAuthCredentials> from your OAuth helper.UserAuth (host-side)The connector defines this trait:
#[async_trait]
pub trait UserAuth: Send + Sync {
async fn sourced_credentials(
&self,
kind: &'static str,
aid: &str,
) -> anyhow::Result<Option<Box<dyn Any + Send + Sync>>>;
}
The host implements it once. The match-arm pattern:
async fn sourced_credentials(
&self,
kind: &'static str,
aid: &str,
) -> anyhow::Result<Option<Box<dyn Any + Send + Sync>>> {
match kind {
"garmin" => …box up Garmin creds…,
"whoop" => …box up Whoop creds…,
"google" => …box up Google creds…,
"myprovider" => …box up MyProvider creds…, // ← new arm
_ => Err(anyhow!("unknown auth kind {kind}")),
}
}
This is the only host-side change needed for credential lookup.
Everything else flows through the typed auth::<P>(id) extension.
fn client<'a>(
user: &'a dyn UserAuth,
_deps: &'a dyn MyProviderBackendDeps,
p: &'a Self::Params,
) -> BoxFuture<'a, anyhow::Result<Self::Client>> {
Box::pin(async move {
let creds = user
.auth::<MyProviderAuth>(&p.auth_id)
.await
.context("myprovider auth lookup")?
.ok_or_else(|| anyhow!("no myprovider auth for {}", p.auth_id))?;
// build your HTTP client from `creds`
Ok(MyProviderClient { http: Arc::new(crate::Client::from(creds)) })
})
}
The auth::<P>(id) blanket impl on UserAuth takes care of:
user.sourced_credentials(P::KIND, id).Box<dyn Any> to
P::Credentials.Option<P::Credentials> to your row body.If the credential type doesn't match — i.e. the host returned a
different type than P::Credentials — auth::<P> errors with a
clear message. So the host's match-arm bug shows up immediately
at the call site.
Distinct from UserAuth: RowKeyAuthProvider is the host-side
trait that answers "what email is this actor?" + "is this email
an operator for scope X?". Used by guards (e.g. the weather guard
that admits only operator-tester@…). Most providers don't
interact with this directly.
The connector's auth surface (AuthProvider + UserAuth) names
the credential type and lookup but is intentionally silent
on:
MyProviderOAuthCredentials blob (typically encrypted JSON
in lona_user_auth or equivalent) keyed by
(user_id, kind, auth_id). UserAuth::sourced_credentials
loads from that store at request time.Auth<...> handle from
user.auth::<MyProviderAuth>(id). If the host's refresh
fails, the lookup returns None and the row body surfaces a
clear "no myprovider auth" error.A typical host-side flow per provider:
1. User clicks "Connect MyProvider" in settings UI
→ host opens OAuth popup
2. Provider redirects to host callback URL with auth code
→ host exchanges code for { access_token, refresh_token }
3. Host stores the typed credentials in its auth store
→ keyed by (user_id, "myprovider", auth_id)
4. Future row dispatch resolves via UserAuth::sourced_credentials
→ host's match arm reads from the auth store
→ if access_token expired, host runs refresh, updates store,
returns fresh credentials
5. Row body builds HTTP client from credentials, makes API
calls, persists cells
The provider crate participates in steps 4–5 only; steps 1–3 are entirely host concern. Future work (see the gaps section in adding an integration) may collapse some of this into a connector-side OAuth helper crate.
Client. The grab-bag is a
request-scoped thing; credentials should only ever be resolved
via UserAuth so the host can rotate or revoke them.&str AuthId in places that should be typed.
The AuthId associated type exists so the row body can declare
its expectations precisely. Keep &str confined to
UserAuth::sourced_credentials (where it crosses the dyn
boundary) and Params deserialization.