///|
/// Provider-neutral `/login` orchestration.
///
/// The router exposes authentication capabilities and delegates prompt,
/// validation, credential interpretation, and slot rebuilding to provider
/// extensions. It never parses provider settings or endpoint details.
///|
priv struct LoginSelection {
provider_id : String?
login_method : String?
}
///|
fn optional_login_string(
fields : Map[String, Json],
key : String,
) -> String? raise @posoco.CommandError {
match fields.get(key) {
None => None
Some(Json::String(value)) =>
if value.length() == 0 {
raise @posoco.CommandError::InvalidArgs(
reason="/login " + key + " must be a non-empty string",
)
} else {
Some(value)
}
Some(_) =>
raise @posoco.CommandError::InvalidArgs(
reason="/login " + key + " must be a string",
)
}
}
///|
fn login_command_selection(
args : Json,
) -> LoginSelection raise @posoco.CommandError {
match args {
Json::Null => { provider_id: None, login_method: None, }
Json::String(provider_id) =>
if provider_id.length() == 0 {
{ provider_id: None, login_method: None, }
} else {
{ provider_id: Some(provider_id), login_method: None, }
}
Json::Object(fields) =>
{
provider_id: optional_login_string(fields, "provider"),
login_method: optional_login_string(fields, "method"),
}
_ =>
raise @posoco.CommandError::InvalidArgs(
reason="/login expects no arguments, a provider string, or {provider, method}",
)
}
}
///|
fn oauth_error_summary(err : @oauth.OAuthError) -> String {
match err {
@oauth.OAuthError::AuthPending => "auth_pending"
@oauth.OAuthError::SlowDown => "slow_down"
@oauth.OAuthError::AccessDenied => "access_denied"
@oauth.OAuthError::ExpiredToken => "expired_token"
@oauth.OAuthError::InvalidGrant => "invalid_grant"
@oauth.OAuthError::HttpError(_) => "http_error"
@oauth.OAuthError::ParseError(_) => "parse_error"
@oauth.OAuthError::Cancelled => "cancelled"
}
}
///|
fn auth_methods_for_slot(slot : ModelSlot) -> Array[AuthMethod] {
let result : Array[AuthMethod] = []
match slot.api_key {
Some(_) => result.push(AuthMethod::ApiKey)
None => ()
}
match slot.oauth {
Some(_) => result.push(AuthMethod::OAuth)
None => ()
}
result
}
///|
fn auth_methods_json(methods : Array[AuthMethod]) -> Json {
let values : Array[Json] = []
for auth_method in methods {
values.push(Json::string(auth_method.to_id()))
}
Json::array(values)
}
///|
/// List each provider once with its provider-owned auth methods.
pub fn RouterModelPort::list_auth_capabilities(self : RouterModelPort) -> Json {
let entries : Array[Json] = []
let seen : Array[String] = []
for id in self.slot_order {
match self.slots.get(id) {
Some(slot) =>
if !seen.contains(slot.provider_id) {
seen.push(slot.provider_id)
entries.push(
Json::object(
Map::from_array([
("provider", Json::string(slot.provider_id)),
("methods", auth_methods_json(auth_methods_for_slot(slot))),
]),
),
)
}
None =>
abort(
"posoco_ext_llm router invariant violated: ordered slot is missing: " +
id,
)
}
}
Json::array(entries)
}
///|
/// Backward-compatible OAuth-only provider id list.
pub fn RouterModelPort::list_oauth_provider_ids(
self : RouterModelPort,
) -> Array[String] {
let result : Array[String] = []
for id in self.slot_order {
match self.slots.get(id) {
Some(slot) =>
match slot.oauth {
Some(_) =>
if !result.contains(slot.provider_id) {
result.push(slot.provider_id)
}
None => ()
}
None =>
abort(
"posoco_ext_llm router invariant violated: ordered slot is missing: " +
id,
)
}
}
result
}
///|
fn RouterModelPort::find_oauth_provider(
self : RouterModelPort,
provider_id : String,
) -> &@oauth.OAuthProvider? {
for id in self.slot_order {
match self.slots.get(id) {
Some(slot) =>
if slot.provider_id == provider_id {
match slot.oauth {
Some(provider) => return Some(provider)
None => ()
}
}
None =>
abort(
"posoco_ext_llm router invariant violated: ordered slot is missing: " +
id,
)
}
}
None
}
///|
fn RouterModelPort::find_api_key_provider(
self : RouterModelPort,
provider_id : String,
) -> &ApiKeyFactory? {
for id in self.slot_order {
match self.slots.get(id) {
Some(slot) =>
if slot.provider_id == provider_id {
match slot.api_key {
Some(provider) => return Some(provider)
None => ()
}
}
None =>
abort(
"posoco_ext_llm router invariant violated: ordered slot is missing: " +
id,
)
}
}
None
}
///|
fn RouterModelPort::rebuild_oauth_slots(
self : RouterModelPort,
provider_id : String,
credential : @oauth.Credential,
) -> Int {
let replacements : Array[(String, ModelSlot)] = []
for id in self.slot_order {
match self.slots.get(id) {
Some(slot) =>
if slot.provider_id == provider_id {
match slot.rebuild_on_credential {
Some(factory) => replacements.push((id, factory(credential)))
None => ()
}
}
None =>
abort(
"posoco_ext_llm router invariant violated: ordered slot is missing: " +
id,
)
}
}
for pair in replacements {
let (id, replacement) = pair
self.slots[id] = replacement
}
replacements.length()
}
///|
fn RouterModelPort::rebuild_api_key_slots(
self : RouterModelPort,
provider_id : String,
credential : @oauth.ApiKeyCredential,
) -> Int {
let replacements : Array[(String, ModelSlot)] = []
for id in self.slot_order {
match self.slots.get(id) {
Some(slot) =>
if slot.provider_id == provider_id {
match slot.rebuild_on_api_key {
Some(factory) => replacements.push((id, factory(credential)))
None => ()
}
}
None =>
abort(
"posoco_ext_llm router invariant violated: ordered slot is missing: " +
id,
)
}
}
for pair in replacements {
let (id, replacement) = pair
self.slots[id] = replacement
}
replacements.length()
}
///|
fn RouterModelPort::provider_methods(
self : RouterModelPort,
provider_id : String,
) -> Array[AuthMethod] {
for id in self.slot_order {
match self.slots.get(id) {
Some(slot) =>
if slot.provider_id == provider_id {
return auth_methods_for_slot(slot)
}
None =>
abort(
"posoco_ext_llm router invariant violated: ordered slot is missing: " +
id,
)
}
}
[]
}
///|
fn parse_auth_method(
requested : String,
) -> AuthMethod raise @posoco.CommandError {
match requested {
"oauth" => AuthMethod::OAuth
"api_key" => AuthMethod::ApiKey
_ =>
raise @posoco.CommandError::InvalidArgs(
reason="/login method must be 'oauth' or 'api_key'",
)
}
}
///|
fn RouterModelPort::select_auth_method(
self : RouterModelPort,
provider_id : String,
requested : String?,
) -> AuthMethod raise @posoco.CommandError {
match requested {
Some(requested) => {
let selected = parse_auth_method(requested)
if !self.provider_methods(provider_id).contains(selected) {
raise @posoco.CommandError::InvalidArgs(
reason="provider does not expose auth method: " + requested,
)
}
selected
}
None => {
let methods = self.provider_methods(provider_id)
if methods.length() == 1 {
methods[0]
} else if methods.is_empty() {
raise @posoco.CommandError::InvalidArgs(
reason="provider has no authentication method: " + provider_id,
)
} else {
raise @posoco.CommandError::InvalidArgs(
reason="/login requires method for provider " + provider_id,
)
}
}
}
}
///|
async fn RouterModelPort::invoke_oauth_login(
self : RouterModelPort,
provider_id : String,
) -> @posoco.CommandOutcome raise @posoco.CommandError {
let provider = match self.find_oauth_provider(provider_id) {
Some(provider) => provider
None =>
return @posoco.CommandOutcome::Failure(
reason="provider has no OAuth method: " + provider_id,
)
}
let interaction = match self.auth_interaction {
Some(interaction) => interaction
None =>
raise @posoco.CommandError::ExecutionFailed(
"OAuth auth interaction is not configured",
)
}
let credential = provider.login(interaction) catch {
err =>
raise @posoco.CommandError::ExecutionFailed(
"OAuth login failed for " +
provider_id +
": " +
oauth_error_summary(err),
)
}
let credential_provider_id = provider.provider_id()
match self.provider_credential_store {
Some(store) =>
store.write(provider_id, @oauth.ProviderCredential::OAuth(credential)) catch {
err =>
raise @posoco.CommandError::ExecutionFailed(
"OAuth credential persistence failed for " +
provider_id +
": " +
oauth_error_summary(err),
)
}
None => {
let store = match self.cred_store {
Some(store) => store
None =>
raise @posoco.CommandError::ExecutionFailed(
"OAuth credential store is not configured",
)
}
store.write(credential_provider_id, credential) catch {
err =>
raise @posoco.CommandError::ExecutionFailed(
"OAuth credential persistence failed for " +
provider_id +
": " +
oauth_error_summary(err),
)
}
// Legacy hosts may expose both stores. A successful explicit login
// replaces the previous method so a later recomposition cannot silently
// select OAuth again.
match self.find_api_key_provider(provider_id) {
Some(api_key_provider) =>
match self.api_key_store {
Some(api_key_store) =>
api_key_store.delete(api_key_provider.credential_id()) catch {
err =>
raise @posoco.CommandError::ExecutionFailed(
"API-key credential cleanup failed for " +
provider_id +
": " +
oauth_error_summary(err),
)
}
None => ()
}
None => ()
}
}
}
self.credentials[credential_provider_id] = credential
let rebuilt = self.rebuild_oauth_slots(provider_id, credential)
@posoco.CommandOutcome::Success(
feedback="Logged in to " + provider_id,
structured=Some(
Json::object(
Map::from_array([
("provider", Json::string(provider_id)),
("method", Json::string("oauth")),
("rebuilt_slots", Json::number(rebuilt.to_double())),
]),
),
),
ui_hint=Some(@posoco.UiHint::RefreshModelList),
)
}
///|
async fn RouterModelPort::invoke_api_key_login(
self : RouterModelPort,
provider_id : String,
) -> @posoco.CommandOutcome raise @posoco.CommandError {
let provider = match self.find_api_key_provider(provider_id) {
Some(provider) => provider
None =>
return @posoco.CommandOutcome::Failure(
reason="provider has no api_key method: " + provider_id,
)
}
let interaction = match self.auth_prompt {
Some(interaction) => interaction
None =>
raise @posoco.CommandError::ExecutionFailed(
"API-key auth prompt is not configured",
)
}
let credential = provider.login(interaction) catch {
err =>
raise @posoco.CommandError::ExecutionFailed(
"API-key login failed for " +
provider_id +
": " +
oauth_error_summary(err),
)
}
let credential_provider_id = provider.credential_id()
match self.provider_credential_store {
Some(store) =>
store.write(provider_id, @oauth.ProviderCredential::ApiKey(credential)) catch {
err =>
raise @posoco.CommandError::ExecutionFailed(
"API-key credential persistence failed for " +
provider_id +
": " +
oauth_error_summary(err),
)
}
None => {
let store = match self.api_key_store {
Some(store) => store
None =>
raise @posoco.CommandError::ExecutionFailed(
"API-key credential store is not configured",
)
}
store.write(credential_provider_id, credential) catch {
err =>
raise @posoco.CommandError::ExecutionFailed(
"API-key credential persistence failed for " +
provider_id +
": " +
oauth_error_summary(err),
)
}
// Legacy dual stores are reconciled on every explicit method choice.
match self.find_oauth_provider(provider_id) {
Some(oauth_provider) =>
match self.cred_store {
Some(oauth_store) =>
oauth_store.delete(oauth_provider.provider_id()) catch {
err =>
raise @posoco.CommandError::ExecutionFailed(
"OAuth credential cleanup failed for " +
provider_id +
": " +
oauth_error_summary(err),
)
}
None => ()
}
None => ()
}
}
}
self.api_key_credentials[credential_provider_id] = credential
let rebuilt = self.rebuild_api_key_slots(provider_id, credential)
@posoco.CommandOutcome::Success(
feedback="Logged in to " + provider_id,
structured=Some(
Json::object(
Map::from_array([
("provider", Json::string(provider_id)),
("method", Json::string("api_key")),
("rebuilt_slots", Json::number(rebuilt.to_double())),
]),
),
),
ui_hint=Some(@posoco.UiHint::RefreshModelList),
)
}
///|
async fn RouterModelPort::invoke_login(
self : RouterModelPort,
args : Json,
) -> @posoco.CommandOutcome raise @posoco.CommandError {
let selection = login_command_selection(args)
let provider_id = match selection.provider_id {
None =>
return @posoco.CommandOutcome::Success(
feedback="Available authentication methods",
structured=Some(self.list_auth_capabilities()),
ui_hint=Some(@posoco.UiHint::None),
)
Some(value) => value
}
let selected_method = self.select_auth_method(
provider_id,
selection.login_method,
)
match selected_method {
AuthMethod::OAuth => self.invoke_oauth_login(provider_id)
AuthMethod::ApiKey => self.invoke_api_key_login(provider_id)
}
}