11//! In-app subscription authentication.
22//!
33//! Lets BitFun sign in to another product's subscription (Codex/ChatGPT,
4- //! Antigravity/Google, OpenCode) with an OpenCode-style in-app OAuth flow,
4+ //! Antigravity/Google, OpenCode, Grok Build/SuperGrok ) with an in-app OAuth flow,
55//! and use the resulting tokens to authenticate AI requests. Secret material
66//! is stored in the operating-system credential vault; the local JSON file
77//! contains non-secret account metadata only.
1010
1111mod antigravity;
1212mod codex;
13+ mod grok;
1314mod jwt;
1415mod oauth_server;
1516mod opencode;
@@ -38,6 +39,7 @@ pub enum SubscriptionProvider {
3839 Codex ,
3940 Antigravity ,
4041 Opencode ,
42+ Grok ,
4143}
4244
4345/// Transport policy shared by subscription-auth requests.
@@ -61,14 +63,16 @@ impl SubscriptionHttpOptions {
6163
6264impl SubscriptionProvider {
6365 /// All providers, in display order.
64- pub const ALL : [ SubscriptionProvider ; 3 ] = [ Self :: Codex , Self :: Antigravity , Self :: Opencode ] ;
66+ pub const ALL : [ SubscriptionProvider ; 4 ] =
67+ [ Self :: Codex , Self :: Antigravity , Self :: Opencode , Self :: Grok ] ;
6568
6669 /// Stable store key / serde tag for this provider.
6770 pub fn key ( self ) -> & ' static str {
6871 match self {
6972 Self :: Codex => "codex" ,
7073 Self :: Antigravity => "antigravity" ,
7174 Self :: Opencode => "opencode" ,
75+ Self :: Grok => "grok" ,
7276 }
7377 }
7478
@@ -78,6 +82,7 @@ impl SubscriptionProvider {
7882 "codex" => Some ( Self :: Codex ) ,
7983 "antigravity" => Some ( Self :: Antigravity ) ,
8084 "opencode" => Some ( Self :: Opencode ) ,
85+ "grok" => Some ( Self :: Grok ) ,
8186 _ => None ,
8287 }
8388 }
@@ -86,7 +91,8 @@ impl SubscriptionProvider {
8691 match self {
8792 Self :: Codex => "Codex (ChatGPT)" ,
8893 Self :: Antigravity => "Antigravity (Google)" ,
89- Self :: Opencode => "OpenCode" ,
94+ Self :: Opencode => "OpenCode (Go/Zen)" ,
95+ Self :: Grok => "Grok Build (SuperGrok)" ,
9096 }
9197 . to_string ( )
9298 }
@@ -96,6 +102,7 @@ impl SubscriptionProvider {
96102 Self :: Codex => codex:: suggested ( ) ,
97103 Self :: Antigravity => antigravity:: suggested ( ) ,
98104 Self :: Opencode => opencode:: suggested ( ) ,
105+ Self :: Grok => grok:: suggested ( ) ,
99106 }
100107 }
101108}
@@ -313,10 +320,12 @@ pub(crate) fn store_lock(provider: SubscriptionProvider) -> &'static tokio::sync
313320 static CODEX : tokio:: sync:: Mutex < ( ) > = tokio:: sync:: Mutex :: const_new ( ( ) ) ;
314321 static ANTIGRAVITY : tokio:: sync:: Mutex < ( ) > = tokio:: sync:: Mutex :: const_new ( ( ) ) ;
315322 static OPENCODE : tokio:: sync:: Mutex < ( ) > = tokio:: sync:: Mutex :: const_new ( ( ) ) ;
323+ static GROK : tokio:: sync:: Mutex < ( ) > = tokio:: sync:: Mutex :: const_new ( ( ) ) ;
316324 match provider {
317325 SubscriptionProvider :: Codex => & CODEX ,
318326 SubscriptionProvider :: Antigravity => & ANTIGRAVITY ,
319327 SubscriptionProvider :: Opencode => & OPENCODE ,
328+ SubscriptionProvider :: Grok => & GROK ,
320329 }
321330}
322331
@@ -543,6 +552,9 @@ pub async fn start_login_with_options(
543552 SubscriptionProvider :: Opencode => {
544553 opencode:: begin_login ( begin_cancel. clone ( ) , expected_revision, options) . await
545554 }
555+ SubscriptionProvider :: Grok => {
556+ grok:: begin_login ( begin_cancel. clone ( ) , expected_revision, options) . await
557+ }
546558 }
547559 } ;
548560 let started_result = tokio:: select! {
@@ -801,6 +813,7 @@ pub async fn resolve_with_options(
801813 SubscriptionProvider :: Codex => codex:: resolve ( options) . await ,
802814 SubscriptionProvider :: Antigravity => antigravity:: resolve ( options) . await ,
803815 SubscriptionProvider :: Opencode => opencode:: resolve ( options) . await ,
816+ SubscriptionProvider :: Grok => grok:: resolve ( options) . await ,
804817 }
805818}
806819
@@ -820,6 +833,21 @@ pub async fn resolve_opencode_with_options(
820833 opencode:: resolve_for ( plan, format, options) . await
821834}
822835
836+ /// Resolves a Grok Build credential for a concrete model. The adapter owns
837+ /// both the subscription endpoint and model-routing header so the OAuth token
838+ /// can never be sent to an arbitrary URL supplied by model configuration.
839+ pub async fn resolve_grok ( model : & str ) -> Result < ResolvedCredential > {
840+ resolve_grok_with_options ( model, & SubscriptionHttpOptions :: default ( ) ) . await
841+ }
842+
843+ /// Resolves a Grok Build credential with an explicit transport policy.
844+ pub async fn resolve_grok_with_options (
845+ model : & str ,
846+ options : & SubscriptionHttpOptions ,
847+ ) -> Result < ResolvedCredential > {
848+ grok:: resolve_for ( model, options) . await
849+ }
850+
823851/// Forces a resolve (which refreshes and saves), then returns the account entry.
824852pub async fn refresh_account ( provider : SubscriptionProvider ) -> Result < SubscriptionAccount > {
825853 refresh_account_with_options ( provider, & SubscriptionHttpOptions :: default ( ) ) . await
@@ -878,13 +906,21 @@ mod tests {
878906 serde_json:: to_value( SubscriptionProvider :: Antigravity ) . unwrap( ) ,
879907 serde_json:: json!( "antigravity" )
880908 ) ;
909+ assert_eq ! (
910+ serde_json:: to_value( SubscriptionProvider :: Grok ) . unwrap( ) ,
911+ serde_json:: json!( "grok" )
912+ ) ;
881913 let parsed: SubscriptionProvider =
882914 serde_json:: from_value ( serde_json:: json!( "opencode" ) ) . unwrap ( ) ;
883915 assert_eq ! ( parsed, SubscriptionProvider :: Opencode ) ;
884916 assert_eq ! (
885917 SubscriptionProvider :: from_key( "codex" ) ,
886918 Some ( SubscriptionProvider :: Codex )
887919 ) ;
920+ assert_eq ! (
921+ SubscriptionProvider :: from_key( "grok" ) ,
922+ Some ( SubscriptionProvider :: Grok )
923+ ) ;
888924 assert_eq ! ( SubscriptionProvider :: from_key( "unknown" ) , None ) ;
889925 }
890926
0 commit comments