mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
Add Codex endpoint support for OpenAI OAuth flow
Route OpenAI OAuth users through the ChatGPT Codex backend API with required headers (ChatGPT-Account-Id, originator). The Codex endpoint requires streaming-only requests, omits unsupported fields (temperature, max_output_tokens, top_p), and uses a different error format. Also persists the account ID from OAuth tokens and updates CLI docs for `fabro ps -q`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e09a6de72f
commit
fd9260c3c5
7 changed files with 105 additions and 31 deletions
|
|
@ -1 +1 @@
|
||||||
610aff744609268ae879a58acfe6c9a6bb0572e0
|
e09a6de72fe7c69e8868abf5412c088bde05bece
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ fabro ps # active runs only
|
||||||
fabro ps -a # all runs including completed
|
fabro ps -a # all runs including completed
|
||||||
fabro ps --workflow deploy --label env=prod
|
fabro ps --workflow deploy --label env=prod
|
||||||
fabro ps --json
|
fabro ps --json
|
||||||
|
fabro ps -q # run IDs only, one per line
|
||||||
|
fabro ps -qa # all run IDs
|
||||||
```
|
```
|
||||||
|
|
||||||
The table shows run ID, status, workflow name, goal, and timing.
|
The table shows run ID, status, workflow name, goal, and timing.
|
||||||
|
|
@ -91,6 +93,7 @@ The table shows run ID, status, workflow name, goal, and timing.
|
||||||
| `--label <KEY=VALUE>` | Filter by label (repeatable, AND semantics) |
|
| `--label <KEY=VALUE>` | Filter by label (repeatable, AND semantics) |
|
||||||
| `--orphans` | Include orphan directories (no `manifest.json`) |
|
| `--orphans` | Include orphan directories (no `manifest.json`) |
|
||||||
| `--json` | Output as JSON |
|
| `--json` | Output as JSON |
|
||||||
|
| `-q, --quiet` | Only display full run IDs, one per line (no headers or footers). Takes precedence over `--json`. |
|
||||||
|
|
||||||
## `fabro rm`
|
## `fabro rm`
|
||||||
|
|
||||||
|
|
@ -361,7 +364,7 @@ fabro system df -v
|
||||||
|
|
||||||
## `fabro graph`
|
## `fabro graph`
|
||||||
|
|
||||||
Render a workflow graph as SVG or PNG. Requires [Graphviz](https://graphviz.org) (`dot`) to be installed.
|
Render a workflow graph as SVG or PNG. Requires [Graphviz](https://graphviz.org) (`dot`) to be installed. SVG output includes styled defaults (teal node strokes, clean typography, transparent background) and automatic dark mode support via `prefers-color-scheme` media queries.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
fabro graph workflow.fabro
|
fabro graph workflow.fabro
|
||||||
|
|
|
||||||
|
|
@ -280,14 +280,22 @@ fn detect_binary_on_path(binary: &str) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert OAuth tokens to env var pairs for ~/.fabro/.env.
|
/// Convert OAuth tokens to env var pairs for ~/.fabro/.env.
|
||||||
fn openai_oauth_env_pairs(access_token: &str, refresh_token: &str) -> Vec<(String, String)> {
|
fn openai_oauth_env_pairs(
|
||||||
vec![
|
access_token: &str,
|
||||||
|
refresh_token: &str,
|
||||||
|
account_id: Option<&str>,
|
||||||
|
) -> Vec<(String, String)> {
|
||||||
|
let mut pairs = vec![
|
||||||
("OPENAI_API_KEY".to_string(), access_token.to_string()),
|
("OPENAI_API_KEY".to_string(), access_token.to_string()),
|
||||||
(
|
(
|
||||||
"OPENAI_REFRESH_TOKEN".to_string(),
|
"OPENAI_REFRESH_TOKEN".to_string(),
|
||||||
refresh_token.to_string(),
|
refresh_token.to_string(),
|
||||||
),
|
),
|
||||||
]
|
];
|
||||||
|
if let Some(id) = account_id {
|
||||||
|
pairs.push(("CHATGPT_ACCOUNT_ID".to_string(), id.to_string()));
|
||||||
|
}
|
||||||
|
pairs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -681,9 +689,11 @@ pub async fn run_install() -> Result<()> {
|
||||||
{
|
{
|
||||||
Ok(tokens) => {
|
Ok(tokens) => {
|
||||||
tracing::info!("OpenAI OAuth browser flow completed");
|
tracing::info!("OpenAI OAuth browser flow completed");
|
||||||
|
let account_id = fabro_openai_oauth::extract_account_id(&tokens);
|
||||||
env_pairs.extend(openai_oauth_env_pairs(
|
env_pairs.extend(openai_oauth_env_pairs(
|
||||||
&tokens.access_token,
|
&tokens.access_token,
|
||||||
&tokens.refresh_token,
|
&tokens.refresh_token,
|
||||||
|
account_id.as_deref(),
|
||||||
));
|
));
|
||||||
configured_providers.push(Provider::OpenAi);
|
configured_providers.push(Provider::OpenAi);
|
||||||
openai_via_oauth = true;
|
openai_via_oauth = true;
|
||||||
|
|
@ -1005,22 +1015,29 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openai_oauth_env_pairs_sets_api_key() {
|
fn openai_oauth_env_pairs_sets_api_key() {
|
||||||
let pairs = openai_oauth_env_pairs("tok", "ref");
|
let pairs = openai_oauth_env_pairs("tok", "ref", None);
|
||||||
assert!(pairs.contains(&("OPENAI_API_KEY".to_string(), "tok".to_string())));
|
assert!(pairs.contains(&("OPENAI_API_KEY".to_string(), "tok".to_string())));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openai_oauth_env_pairs_sets_refresh_token() {
|
fn openai_oauth_env_pairs_sets_refresh_token() {
|
||||||
let pairs = openai_oauth_env_pairs("tok", "ref");
|
let pairs = openai_oauth_env_pairs("tok", "ref", None);
|
||||||
assert!(pairs.contains(&("OPENAI_REFRESH_TOKEN".to_string(), "ref".to_string())));
|
assert!(pairs.contains(&("OPENAI_REFRESH_TOKEN".to_string(), "ref".to_string())));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openai_oauth_env_pairs_count() {
|
fn openai_oauth_env_pairs_count() {
|
||||||
let pairs = openai_oauth_env_pairs("tok", "ref");
|
let pairs = openai_oauth_env_pairs("tok", "ref", None);
|
||||||
assert_eq!(pairs.len(), 2);
|
assert_eq!(pairs.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn openai_oauth_env_pairs_with_account_id() {
|
||||||
|
let pairs = openai_oauth_env_pairs("tok", "ref", Some("acct_123"));
|
||||||
|
assert!(pairs.contains(&("CHATGPT_ACCOUNT_ID".to_string(), "acct_123".to_string())));
|
||||||
|
assert_eq!(pairs.len(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
// -- Session secret (server only) --
|
// -- Session secret (server only) --
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,16 @@ impl Client {
|
||||||
}
|
}
|
||||||
if let Ok(key) = std::env::var("OPENAI_API_KEY") {
|
if let Ok(key) = std::env::var("OPENAI_API_KEY") {
|
||||||
let mut adapter = providers::OpenAiAdapter::new(key);
|
let mut adapter = providers::OpenAiAdapter::new(key);
|
||||||
if let Ok(base_url) = std::env::var("OPENAI_BASE_URL") {
|
if let Ok(account_id) = std::env::var("CHATGPT_ACCOUNT_ID") {
|
||||||
|
// Codex OAuth: route through chatgpt.com backend with required headers
|
||||||
|
adapter = adapter
|
||||||
|
.with_base_url("https://chatgpt.com/backend-api/codex")
|
||||||
|
.with_codex_mode();
|
||||||
|
let mut headers = std::collections::HashMap::new();
|
||||||
|
headers.insert("ChatGPT-Account-Id".to_string(), account_id);
|
||||||
|
headers.insert("originator".to_string(), "fabro".to_string());
|
||||||
|
adapter = adapter.with_default_headers(headers);
|
||||||
|
} else if let Ok(base_url) = std::env::var("OPENAI_BASE_URL") {
|
||||||
adapter = adapter.with_base_url(base_url);
|
adapter = adapter.with_base_url(base_url);
|
||||||
}
|
}
|
||||||
if let Ok(org_id) = std::env::var("OPENAI_ORG_ID") {
|
if let Ok(org_id) = std::env::var("OPENAI_ORG_ID") {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ pub fn parse_error_body(
|
||||||
.get("error")
|
.get("error")
|
||||||
.and_then(|e| e.get("message"))
|
.and_then(|e| e.get("message"))
|
||||||
.and_then(serde_json::Value::as_str)
|
.and_then(serde_json::Value::as_str)
|
||||||
|
// Codex endpoint returns {"detail": "..."} instead of {"error": {"message": "..."}}
|
||||||
|
.or_else(|| v.get("detail").and_then(serde_json::Value::as_str))
|
||||||
.unwrap_or("Unknown error")
|
.unwrap_or("Unknown error")
|
||||||
.to_string();
|
.to_string();
|
||||||
let error_code = v
|
let error_code = v
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ pub struct Adapter {
|
||||||
pub(crate) http: super::http_api::HttpApi,
|
pub(crate) http: super::http_api::HttpApi,
|
||||||
org_id: Option<String>,
|
org_id: Option<String>,
|
||||||
project_id: Option<String>,
|
project_id: Option<String>,
|
||||||
|
/// When true, always use streaming (required by the Codex endpoint).
|
||||||
|
codex_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Adapter {
|
impl Adapter {
|
||||||
|
|
@ -30,9 +32,16 @@ impl Adapter {
|
||||||
http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL),
|
http: super::http_api::HttpApi::new(api_key, DEFAULT_BASE_URL),
|
||||||
org_id: None,
|
org_id: None,
|
||||||
project_id: None,
|
project_id: None,
|
||||||
|
codex_mode: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_codex_mode(mut self) -> Self {
|
||||||
|
self.codex_mode = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
|
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
|
||||||
self.http.base_url = base_url.into();
|
self.http.base_url = base_url.into();
|
||||||
|
|
@ -83,6 +92,23 @@ impl Adapter {
|
||||||
}
|
}
|
||||||
req
|
req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Complete a request by streaming and collecting the final response.
|
||||||
|
/// Used for the Codex endpoint which requires `stream: true`.
|
||||||
|
async fn complete_via_stream(&self, request: &Request) -> Result<Response, SdkError> {
|
||||||
|
use futures::StreamExt;
|
||||||
|
let mut event_stream = self.stream(request).await?;
|
||||||
|
let mut last_response: Option<Response> = None;
|
||||||
|
while let Some(event) = event_stream.next().await {
|
||||||
|
if let Ok(StreamEvent::Finish { response, .. }) = event {
|
||||||
|
last_response = Some(*response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_response.ok_or_else(|| SdkError::Network {
|
||||||
|
message: "Stream ended without a finish event".into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Request types (Responses API format) ---
|
// --- Request types (Responses API format) ---
|
||||||
|
|
@ -111,6 +137,7 @@ struct ApiRequest {
|
||||||
stop: Option<Vec<String>>,
|
stop: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
metadata: Option<std::collections::HashMap<String, String>>,
|
metadata: Option<std::collections::HashMap<String, String>>,
|
||||||
|
store: bool,
|
||||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||||
stream: bool,
|
stream: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +375,10 @@ fn translate_response_format(format: &ResponseFormat) -> Option<serde_json::Valu
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an `ApiRequest` from a unified `Request`.
|
/// Build an `ApiRequest` from a unified `Request`.
|
||||||
fn build_api_request(request: &Request, stream: bool) -> ApiRequest {
|
///
|
||||||
|
/// When `codex_mode` is true, unsupported fields (`temperature`, `max_output_tokens`, `top_p`)
|
||||||
|
/// are omitted and empty instructions are sent as `""` (required by the Codex endpoint).
|
||||||
|
fn build_api_request(request: &Request, stream: bool, codex_mode: bool) -> ApiRequest {
|
||||||
let (instructions, input) = translate_input(&request.messages);
|
let (instructions, input) = translate_input(&request.messages);
|
||||||
let api_tools = request.tools.as_ref().map(|t| translate_tools(t));
|
let api_tools = request.tools.as_ref().map(|t| translate_tools(t));
|
||||||
let tool_choice = request.tool_choice.as_ref().map(translate_tool_choice);
|
let tool_choice = request.tool_choice.as_ref().map(translate_tool_choice);
|
||||||
|
|
@ -361,26 +391,37 @@ fn build_api_request(request: &Request, stream: bool) -> ApiRequest {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(translate_response_format);
|
.and_then(translate_response_format);
|
||||||
|
|
||||||
|
let instructions = if codex_mode {
|
||||||
|
Some(instructions.unwrap_or_default())
|
||||||
|
} else {
|
||||||
|
instructions
|
||||||
|
};
|
||||||
|
|
||||||
ApiRequest {
|
ApiRequest {
|
||||||
model: request.model.clone(),
|
model: request.model.clone(),
|
||||||
input,
|
input,
|
||||||
instructions,
|
instructions,
|
||||||
temperature: request.temperature,
|
temperature: if codex_mode {
|
||||||
max_output_tokens: request.max_tokens,
|
None
|
||||||
top_p: request.top_p,
|
} else {
|
||||||
|
request.temperature
|
||||||
|
},
|
||||||
|
max_output_tokens: if codex_mode { None } else { request.max_tokens },
|
||||||
|
top_p: if codex_mode { None } else { request.top_p },
|
||||||
tools: api_tools,
|
tools: api_tools,
|
||||||
tool_choice,
|
tool_choice,
|
||||||
reasoning,
|
reasoning,
|
||||||
text,
|
text,
|
||||||
stop: request.stop_sequences.clone(),
|
stop: request.stop_sequences.clone(),
|
||||||
metadata: request.metadata.clone(),
|
metadata: request.metadata.clone(),
|
||||||
|
store: false,
|
||||||
stream,
|
stream,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize an `ApiRequest` to JSON and merge any `provider_options.openai` keys into it.
|
/// Serialize an `ApiRequest` to JSON and merge any `provider_options.openai` keys into it.
|
||||||
fn build_request_body(request: &Request, stream: bool) -> serde_json::Value {
|
fn build_request_body(request: &Request, stream: bool, codex_mode: bool) -> serde_json::Value {
|
||||||
let api_request = build_api_request(request, stream);
|
let api_request = build_api_request(request, stream, codex_mode);
|
||||||
let mut body = serde_json::to_value(&api_request).unwrap_or_else(|_| serde_json::json!({}));
|
let mut body = serde_json::to_value(&api_request).unwrap_or_else(|_| serde_json::json!({}));
|
||||||
|
|
||||||
if let Some(openai_opts) = request
|
if let Some(openai_opts) = request
|
||||||
|
|
@ -883,10 +924,15 @@ impl ProviderAdapter for Adapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn complete(&self, request: &Request) -> Result<Response, SdkError> {
|
async fn complete(&self, request: &Request) -> Result<Response, SdkError> {
|
||||||
|
// Codex endpoint requires streaming; collect the stream into a response.
|
||||||
|
if self.codex_mode {
|
||||||
|
return self.complete_via_stream(request).await;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(tc) = &request.tool_choice {
|
if let Some(tc) = &request.tool_choice {
|
||||||
crate::provider::validate_tool_choice(self, tc)?;
|
crate::provider::validate_tool_choice(self, tc)?;
|
||||||
}
|
}
|
||||||
let request_body = build_request_body(request, false);
|
let request_body = build_request_body(request, false, false);
|
||||||
let url = format!("{}/responses", self.http.base_url);
|
let url = format!("{}/responses", self.http.base_url);
|
||||||
|
|
||||||
let mut req = self.build_request(&url).json(&request_body);
|
let mut req = self.build_request(&url).json(&request_body);
|
||||||
|
|
@ -942,7 +988,7 @@ impl ProviderAdapter for Adapter {
|
||||||
if let Some(tc) = &request.tool_choice {
|
if let Some(tc) = &request.tool_choice {
|
||||||
crate::provider::validate_tool_choice(self, tc)?;
|
crate::provider::validate_tool_choice(self, tc)?;
|
||||||
}
|
}
|
||||||
let request_body = build_request_body(request, true);
|
let request_body = build_request_body(request, true, self.codex_mode);
|
||||||
let url = format!("{}/responses", self.http.base_url);
|
let url = format!("{}/responses", self.http.base_url);
|
||||||
|
|
||||||
let http_resp = self
|
let http_resp = self
|
||||||
|
|
@ -1040,7 +1086,7 @@ mod tests {
|
||||||
let mut request = minimal_request();
|
let mut request = minimal_request();
|
||||||
request.metadata = Some(metadata);
|
request.metadata = Some(metadata);
|
||||||
|
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
let meta = body.get("metadata").expect("metadata should be present");
|
let meta = body.get("metadata").expect("metadata should be present");
|
||||||
assert_eq!(meta["user_id"], "u123");
|
assert_eq!(meta["user_id"], "u123");
|
||||||
assert_eq!(meta["session"], "s456");
|
assert_eq!(meta["session"], "s456");
|
||||||
|
|
@ -1049,7 +1095,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn build_request_body_omits_metadata_when_none() {
|
fn build_request_body_omits_metadata_when_none() {
|
||||||
let request = minimal_request();
|
let request = minimal_request();
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
assert!(body.get("metadata").is_none());
|
assert!(body.get("metadata").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1063,7 +1109,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
assert_eq!(body["store"], true);
|
assert_eq!(body["store"], true);
|
||||||
assert_eq!(body["previous_response_id"], "resp_abc123");
|
assert_eq!(body["previous_response_id"], "resp_abc123");
|
||||||
}
|
}
|
||||||
|
|
@ -1078,7 +1124,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
// provider_options should override the base field
|
// provider_options should override the base field
|
||||||
assert_eq!(body["temperature"], 0.9);
|
assert_eq!(body["temperature"], 0.9);
|
||||||
}
|
}
|
||||||
|
|
@ -1092,7 +1138,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
// anthropic options should not leak into the OpenAI request
|
// anthropic options should not leak into the OpenAI request
|
||||||
assert!(body.get("thinking").is_none());
|
assert!(body.get("thinking").is_none());
|
||||||
}
|
}
|
||||||
|
|
@ -1100,7 +1146,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn build_request_body_no_provider_options() {
|
fn build_request_body_no_provider_options() {
|
||||||
let request = minimal_request();
|
let request = minimal_request();
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
assert_eq!(body["model"], "gpt-4o");
|
assert_eq!(body["model"], "gpt-4o");
|
||||||
// stream field is omitted when false (skip_serializing_if)
|
// stream field is omitted when false (skip_serializing_if)
|
||||||
assert!(body.get("stream").is_none());
|
assert!(body.get("stream").is_none());
|
||||||
|
|
@ -1109,7 +1155,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn build_request_body_stream_flag() {
|
fn build_request_body_stream_flag() {
|
||||||
let request = minimal_request();
|
let request = minimal_request();
|
||||||
let body = build_request_body(&request, true);
|
let body = build_request_body(&request, true, false);
|
||||||
assert!(body["stream"].as_bool().unwrap_or(false));
|
assert!(body["stream"].as_bool().unwrap_or(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1126,7 +1172,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
assert_eq!(body["metadata"]["trace_id"], "t789");
|
assert_eq!(body["metadata"]["trace_id"], "t789");
|
||||||
assert_eq!(body["store"], true);
|
assert_eq!(body["store"], true);
|
||||||
}
|
}
|
||||||
|
|
@ -1509,7 +1555,7 @@ mod tests {
|
||||||
let mut request = minimal_request();
|
let mut request = minimal_request();
|
||||||
request.stop_sequences = Some(vec!["END".to_string(), "STOP".to_string()]);
|
request.stop_sequences = Some(vec!["END".to_string(), "STOP".to_string()]);
|
||||||
|
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
let stop = body.get("stop").expect("stop should be present");
|
let stop = body.get("stop").expect("stop should be present");
|
||||||
let arr = stop.as_array().expect("stop should be an array");
|
let arr = stop.as_array().expect("stop should be an array");
|
||||||
assert_eq!(arr.len(), 2);
|
assert_eq!(arr.len(), 2);
|
||||||
|
|
@ -1520,7 +1566,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn build_request_body_omits_stop_when_none() {
|
fn build_request_body_omits_stop_when_none() {
|
||||||
let request = minimal_request();
|
let request = minimal_request();
|
||||||
let body = build_request_body(&request, false);
|
let body = build_request_body(&request, false, false);
|
||||||
assert!(body.get("stop").is_none());
|
assert!(body.get("stop").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,7 @@ pub fn build_authorize_url(
|
||||||
("response_type", "code"),
|
("response_type", "code"),
|
||||||
("client_id", client_id),
|
("client_id", client_id),
|
||||||
("redirect_uri", redirect_uri),
|
("redirect_uri", redirect_uri),
|
||||||
(
|
("scope", "openid profile email offline_access"),
|
||||||
"scope",
|
|
||||||
"openid profile email offline_access api.responses.write",
|
|
||||||
),
|
|
||||||
("code_challenge", &pkce.challenge),
|
("code_challenge", &pkce.challenge),
|
||||||
("code_challenge_method", "S256"),
|
("code_challenge_method", "S256"),
|
||||||
("state", state),
|
("state", state),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue