From 8b37ab8150640c19a6bb00538606c17e12013c19 Mon Sep 17 00:00:00 2001 From: atlas-models-bot Date: Fri, 14 Aug 2026 04:18:41 +0000 Subject: [PATCH 1/7] chore: sync live model catalog (408 models) --- README.md | 4 ++-- docs/README.es.md | 4 ++-- docs/README.fr.md | 4 ++-- docs/README.ja.md | 4 ++-- docs/README.ko.md | 4 ++-- docs/README.zh-CN.md | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 72a0571..15caa33 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,10 @@ - 🎬 **Video** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 - 🎨 **Image** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (62) — DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 · Grok 4.5 +- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Music · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explore more** — [all 407 live models »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explore more** — [all 408 live models »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Contents diff --git a/docs/README.es.md b/docs/README.es.md index 2cf23db..a925b61 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -33,10 +33,10 @@ - 🎬 **Vídeo** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 - 🎨 **Imagen** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (62) — DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 · Grok 4.5 +- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Música · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explora más** — [los 407 modelos en vivo »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explora más** — [los 408 modelos en vivo »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Contenido diff --git a/docs/README.fr.md b/docs/README.fr.md index a5413ad..b914e5d 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -33,10 +33,10 @@ - 🎬 **Vidéo** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 - 🎨 **Image** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (62) — DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 · Grok 4.5 +- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Musique · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explorer plus** — [les 407 modèles en ligne »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explorer plus** — [les 408 modèles en ligne »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Sommaire diff --git a/docs/README.ja.md b/docs/README.ja.md index 376eb75..880699c 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -33,10 +33,10 @@ - 🎬 **動画** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 - 🎨 **画像** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (62) — DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 · Grok 4.5 +- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **音声 (TTS · 音楽 · 音声認識)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **さらに探す** — [全 407 モデル »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **さらに探す** — [全 408 モデル »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 目次 diff --git a/docs/README.ko.md b/docs/README.ko.md index 49ca9c8..5b3a35c 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -33,10 +33,10 @@ - 🎬 **비디오** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 - 🎨 **이미지** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (62) — DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 · Grok 4.5 +- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **오디오 (TTS · 음악 · STT)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **더 살펴보기** — [전체 407개 모델 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **더 살펴보기** — [전체 408개 모델 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 목차 diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 57048df..7d0a092 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -33,10 +33,10 @@ - 🎬 **视频** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 - 🎨 **图片** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **大语言模型** (62) — DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 · Grok 4.5 +- 💬 **大语言模型** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **音频(TTS · 音乐 · 语音识别)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **探索更多** — [全部 407 个在线模型 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **探索更多** — [全部 408 个在线模型 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 目录 From 2028152a853f3a86e2f5227d247c4a2269a0a12a Mon Sep 17 00:00:00 2001 From: atlas-models-bot Date: Sat, 15 Aug 2026 03:05:14 +0000 Subject: [PATCH 2/7] chore: sync live model catalog (397 models) --- README.md | 6 +++--- docs/README.es.md | 6 +++--- docs/README.fr.md | 6 +++--- docs/README.ja.md | 6 +++--- docs/README.ko.md | 6 +++--- docs/README.zh-CN.md | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 15caa33..67476ce 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ -- 🎬 **Video** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 +- 🎬 **Video** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash - 🎨 **Image** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 +- 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Music · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explore more** — [all 408 live models »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explore more** — [all 397 live models »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Contents diff --git a/docs/README.es.md b/docs/README.es.md index a925b61..a80ba8e 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -30,13 +30,13 @@ -- 🎬 **Vídeo** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 +- 🎬 **Vídeo** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash - 🎨 **Imagen** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 +- 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Música · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explora más** — [los 408 modelos en vivo »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explora más** — [los 397 modelos en vivo »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Contenido diff --git a/docs/README.fr.md b/docs/README.fr.md index b914e5d..be62123 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -30,13 +30,13 @@ -- 🎬 **Vidéo** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 +- 🎬 **Vidéo** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash - 🎨 **Image** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 +- 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Musique · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explorer plus** — [les 408 modèles en ligne »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explorer plus** — [les 397 modèles en ligne »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Sommaire diff --git a/docs/README.ja.md b/docs/README.ja.md index 880699c..7912020 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -30,13 +30,13 @@ -- 🎬 **動画** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 +- 🎬 **動画** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash - 🎨 **画像** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 +- 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **音声 (TTS · 音楽 · 音声認識)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **さらに探す** — [全 408 モデル »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **さらに探す** — [全 397 モデル »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 目次 diff --git a/docs/README.ko.md b/docs/README.ko.md index 5b3a35c..1f368ee 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -30,13 +30,13 @@ -- 🎬 **비디오** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 +- 🎬 **비디오** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash - 🎨 **이미지** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **LLM** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 +- 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **오디오 (TTS · 음악 · STT)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **더 살펴보기** — [전체 408개 모델 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **더 살펴보기** — [전체 397개 모델 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 목차 diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 7d0a092..cd69b4b 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -30,13 +30,13 @@ -- 🎬 **视频** (186) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Wan 2.7 Spicy · Seedance 2.0 Mini · HappyHorse-1.1 +- 🎬 **视频** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash - 🎨 **图片** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 -- 💬 **大语言模型** (63) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 +- 💬 **大语言模型** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **音频(TTS · 音乐 · 语音识别)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **探索更多** — [全部 408 个在线模型 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **探索更多** — [全部 397 个在线模型 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 目录 From 381db7dbc5b2ba2829b210e0304872b62b7fed0f Mon Sep 17 00:00:00 2001 From: atlas-models-bot Date: Sun, 16 Aug 2026 03:13:18 +0000 Subject: [PATCH 3/7] chore: sync live model catalog (395 models) --- README.md | 6 +++--- docs/README.es.md | 6 +++--- docs/README.fr.md | 6 +++--- docs/README.ja.md | 6 +++--- docs/README.ko.md | 6 +++--- docs/README.zh-CN.md | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 67476ce..50dc445 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ -- 🎬 **Video** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash -- 🎨 **Image** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 +- 🎬 **Video** (174) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash +- 🎨 **Image** (116) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 - 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Music · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explore more** — [all 397 live models »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explore more** — [all 395 live models »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Contents diff --git a/docs/README.es.md b/docs/README.es.md index a80ba8e..15d4804 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -30,13 +30,13 @@ -- 🎬 **Vídeo** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash -- 🎨 **Imagen** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 +- 🎬 **Vídeo** (174) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash +- 🎨 **Imagen** (116) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 - 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Música · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explora más** — [los 397 modelos en vivo »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explora más** — [los 395 modelos en vivo »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Contenido diff --git a/docs/README.fr.md b/docs/README.fr.md index be62123..b26e1b0 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -30,13 +30,13 @@ -- 🎬 **Vidéo** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash -- 🎨 **Image** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 +- 🎬 **Vidéo** (174) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash +- 🎨 **Image** (116) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 - 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **Audio (TTS · Musique · ASR)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **Explorer plus** — [les 397 modèles en ligne »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **Explorer plus** — [les 395 modèles en ligne »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## Sommaire diff --git a/docs/README.ja.md b/docs/README.ja.md index 7912020..2d86c20 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -30,13 +30,13 @@ -- 🎬 **動画** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash -- 🎨 **画像** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 +- 🎬 **動画** (174) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash +- 🎨 **画像** (116) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 - 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **音声 (TTS · 音楽 · 音声認識)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **さらに探す** — [全 397 モデル »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **さらに探す** — [全 395 モデル »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 目次 diff --git a/docs/README.ko.md b/docs/README.ko.md index 1f368ee..705a796 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -30,13 +30,13 @@ -- 🎬 **비디오** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash -- 🎨 **이미지** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 +- 🎬 **비디오** (174) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash +- 🎨 **이미지** (116) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 - 💬 **LLM** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **오디오 (TTS · 음악 · STT)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **더 살펴보기** — [전체 397개 모델 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **더 살펴보기** — [전체 395개 모델 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 목차 diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index cd69b4b..ab0a5f5 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -30,13 +30,13 @@ -- 🎬 **视频** (175) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash -- 🎨 **图片** (117) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 +- 🎬 **视频** (174) — Seedance 2.5 · MiniMax H3 · Youchuan V8.2 · Seedance 2.0 Mini · HappyHorse-1.1 · Gemini Omni Flash +- 🎨 **图片** (116) — Seedream v5.0 Pro · Qwen Image 3.0 · Reve 2.1 · Youchuan V8.2 - 🧊 **3D** (7) — Seed3D 2.0 · Hunyuan 3D Rapid · Hunyuan 3D Pro · Tripo H3.1 - 💬 **大语言模型** (64) — Grok 4.6 · DeepSeek V4 Flash 0731 · Qwen3.8 Max · Kimi K3 - 🔊 **音频(TTS · 音乐 · 语音识别)** (18) — Seed Audio 1.0 · xAI TTS v1 · ElevenLabs v3 · Suno chirp-v4-5-all -- 📚 **探索更多** — [全部 397 个在线模型 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) +- 📚 **探索更多** — [全部 395 个在线模型 »](https://www.atlascloud.ai/models?utm_source=github&utm_campaign=mcp-server) ## 目录 From 2261a44d107677b3c46932157e3172e46d5ec312 Mon Sep 17 00:00:00 2001 From: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:36:30 +0800 Subject: [PATCH 4/7] feat(auth): add upstream OIDC preflight check and staging validation overlay Public production requires AUTH_IDENTITY_MODE=upstream-oidc, but nothing in the repository let an operator confirm a candidate issuer before requesting DNS, secrets, and a deployment window, and the only way to exercise the mode was to promote straight to production. - scripts/check-upstream-oidc.mjs mirrors the discovery-time rules enforced by src/auth/upstream-oidc.ts and src/auth/config.ts, needs no build step or client secret, and prints the AUTH_UPSTREAM_ENDPOINT_HOSTS value implied by the discovery document. Endpoints commonly live on hosts other than the issuer, so that value is easy to get wrong by hand. - deploy/kubernetes/staging-upstream-oidc.example applies the production identity and credential profile to the staging hosts while keeping PLUGIN_RELEASE_TIER=staging. - docs/UPSTREAM_OIDC_REQUIREMENTS.md records every provider requirement, including the id_token email_verified boolean that discovery cannot prove. --- deploy/kubernetes/README.md | 37 +++ .../kustomization.yaml | 6 + .../staging-upstream-oidc-patch.yaml | 67 +++++ docs/UPSTREAM_OIDC_REQUIREMENTS.md | 95 ++++++ package.json | 1 + scripts/check-upstream-oidc.mjs | 272 ++++++++++++++++++ 6 files changed, 478 insertions(+) create mode 100644 deploy/kubernetes/staging-upstream-oidc.example/kustomization.yaml create mode 100644 deploy/kubernetes/staging-upstream-oidc.example/staging-upstream-oidc-patch.yaml create mode 100644 docs/UPSTREAM_OIDC_REQUIREMENTS.md create mode 100755 scripts/check-upstream-oidc.mjs diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md index 9b8c0eb..49df1ba 100644 --- a/deploy/kubernetes/README.md +++ b/deploy/kubernetes/README.md @@ -197,6 +197,21 @@ Register this exact upstream callback URL with the identity provider: https://mcp-auth.atlascloud.ai/upstream/callback ``` +`docs/UPSTREAM_OIDC_REQUIREMENTS.md` states every rule the provider must +satisfy. Confirm a candidate issuer before requesting DNS, secrets, or a +deployment window; the check needs no build step, client secret, or cluster +access: + +```bash +node scripts/check-upstream-oidc.mjs https://issuer.example.com +``` + +It prints one `PASS`/`FAIL` line per rule plus the exact +`AUTH_UPSTREAM_ENDPOINT_HOSTS` value implied by the discovery document, and +exits non-zero on failure. `email_verified: true` in the ID token is the one +mandatory behavior discovery cannot prove; confirm it with the provider +directly. + Create these additional Kubernetes Secret keys out of band: - `generation-confirmation-secret`: at least 32 random bytes, shared by every @@ -252,3 +267,25 @@ The directory is intentionally named `.example`; do not apply the rendered output until every external production gate above is satisfied. Both staging and production reuse the single reviewed manifest in `base/staging.yaml`, so there is no duplicated deployment source to drift. + +### Validating upstream OIDC on staging first + +`staging-upstream-oidc.example/` applies the production identity and credential +profile to the staging hosts: `AUTH_IDENTITY_MODE=upstream-oidc`, +`MCP_CREDENTIAL_MODE=redis-subject-map`, the encrypted credential keyring, and +removal of `OIDC_USERS_JSON` and `MCP_ATLAS_SUBJECT_KEYS_JSON`. It keeps +`PLUGIN_RELEASE_TIER=staging`, so a staging-labeled upstream issuer is accepted +there and rejected by the production gate. + +```bash +kubectl kustomize deploy/kubernetes/staging-upstream-oidc.example +``` + +It reads the same additional Secret keys as production, so populate +`auth-upstream-*` and `credential-encryption-keys-json` in +`mcp-servers/atlascloud-openai-plugin` before applying, and register the +staging Auth callback with the provider. This overlay retires the reviewer +password path on staging: `scripts/codex-oauth-e2e.mjs` and +`npm run test:codex-oauth:chrome-live` both read a reviewer password from stdin +and cannot drive an upstream sign-in, so validate the browser flow manually +while it is active. diff --git a/deploy/kubernetes/staging-upstream-oidc.example/kustomization.yaml b/deploy/kubernetes/staging-upstream-oidc.example/kustomization.yaml new file mode 100644 index 0000000..ec54086 --- /dev/null +++ b/deploy/kubernetes/staging-upstream-oidc.example/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../base +patches: + - path: staging-upstream-oidc-patch.yaml diff --git a/deploy/kubernetes/staging-upstream-oidc.example/staging-upstream-oidc-patch.yaml b/deploy/kubernetes/staging-upstream-oidc.example/staging-upstream-oidc-patch.yaml new file mode 100644 index 0000000..0734dec --- /dev/null +++ b/deploy/kubernetes/staging-upstream-oidc.example/staging-upstream-oidc-patch.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: atlascloud-openai-mcp + namespace: mcp-servers +spec: + template: + spec: + containers: + - name: mcp + env: + - name: MCP_CREDENTIAL_MODE + value: redis-subject-map + - name: MCP_CREDENTIAL_REDIS_PREFIX + value: atlascloud:openai-plugin:credential + - name: MCP_CREDENTIAL_ENCRYPTION_KEYS_JSON + valueFrom: + secretKeyRef: + name: atlascloud-openai-plugin + key: credential-encryption-keys-json + - name: MCP_ATLAS_SUBJECT_KEYS_JSON + $patch: delete +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: atlascloud-openai-auth + namespace: mcp-servers +spec: + template: + spec: + containers: + - name: auth + env: + - name: AUTH_IDENTITY_MODE + value: upstream-oidc + - name: AUTH_UPSTREAM_ISSUER_URL + valueFrom: + secretKeyRef: + name: atlascloud-openai-plugin + key: auth-upstream-issuer-url + - name: AUTH_UPSTREAM_CLIENT_ID + valueFrom: + secretKeyRef: + name: atlascloud-openai-plugin + key: auth-upstream-client-id + - name: AUTH_UPSTREAM_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: atlascloud-openai-plugin + key: auth-upstream-client-secret + - name: AUTH_UPSTREAM_SCOPES + value: openid,email,profile + - name: AUTH_UPSTREAM_ENDPOINT_HOSTS + valueFrom: + secretKeyRef: + name: atlascloud-openai-plugin + key: auth-upstream-endpoint-hosts + - name: AUTH_CREDENTIAL_REDIS_PREFIX + value: atlascloud:openai-plugin:credential + - name: AUTH_CREDENTIAL_ENCRYPTION_KEYS_JSON + valueFrom: + secretKeyRef: + name: atlascloud-openai-plugin + key: credential-encryption-keys-json + - name: OIDC_USERS_JSON + $patch: delete diff --git a/docs/UPSTREAM_OIDC_REQUIREMENTS.md b/docs/UPSTREAM_OIDC_REQUIREMENTS.md new file mode 100644 index 0000000..085c785 --- /dev/null +++ b/docs/UPSTREAM_OIDC_REQUIREMENTS.md @@ -0,0 +1,95 @@ +# Upstream OIDC provider requirements + +`AUTH_IDENTITY_MODE=upstream-oidc` federates sign-in to an external OpenID +Connect provider. The Auth service validates the provider strictly and +fail-closed at startup and on every sign-in, so a provider that misses one of +the requirements below cannot be used by relaxing configuration. Hand this +document to whoever operates the identity service, then verify their answer +with the preflight check. + +## Preflight check + +Run this before any deployment change. It needs no build step, no client +secret, and no cluster access: + +```bash +node scripts/check-upstream-oidc.mjs https://issuer.example.com +``` + +The check reports one `PASS`/`FAIL` line per rule, prints the exact +`AUTH_UPSTREAM_ENDPOINT_HOSTS` value implied by the discovery document, and +exits non-zero on the first failure. Endpoints frequently live on hosts other +than the issuer, and every such host must be listed explicitly. + +## Discovery document + +The provider must serve `/.well-known/openid-configuration` over HTTPS. + +| Requirement | Why it is enforced | +|---|---| +| Reachable with **no HTTP redirect** | The fetch uses `redirect: "error"`; an `http`→`https` or trailing-slash redirect fails the request | +| `content-type` includes `application/json` | A non-JSON content type is rejected before parsing | +| Body under 64 KB | Bounded read against hostile or oversized responses | +| `issuer` equals the configured issuer after trailing-slash normalization | Prevents issuer substitution | +| `response_types_supported` includes `code` | Only the authorization code flow is used | +| `code_challenge_methods_supported` includes `S256` | PKCE S256 is mandatory | +| `scopes_supported` includes every requested scope | Default `openid`, `email`, `profile` | +| `token_endpoint_auth_methods_supported` includes `client_secret_basic` | A confidential client is required in production | +| `id_token_signing_alg_values_supported` includes `RS256`, `ES256`, or `PS256` | Symmetric ID-token signatures are refused | +| `authorization_endpoint`, `token_endpoint`, `jwks_uri` present | All three are validated as endpoints | + +Every endpoint must be HTTPS on port 443, carry no userinfo or fragment, and +resolve to a host listed in `AUTH_UPSTREAM_ENDPOINT_HOSTS`. + +## Issuer URL + +- A bare origin: no path, query, or fragment. `https://id.example.com` is + valid, `https://example.com/oauth` is not. +- No `dev`, `development`, `stage`, `staging`, or `test` label in the hostname + for a `PLUGIN_RELEASE_TIER=production` release. A staging tier may use one. +- Public DNS only. IP literals, `localhost`, `*.localhost`, `*.local`, and + single-label hosts are refused in production. + +## Client registration + +- Confidential client with a secret of at least 32 characters in production. +- Register this exact redirect URI, matching the Auth issuer host of the + environment being deployed: + +```text +https://mcp-auth.atlascloud.ai/upstream/callback +``` + + A staging validation of the same flow additionally needs the staging Auth + host's callback registered, for example + `https://atlascloud-auth.dev.atlascloud.ai/upstream/callback`. +- The authorization request always sends `response_type=code`, `state`, + `nonce`, `code_challenge`, and `code_challenge_method=S256`. + +## ID token claims + +The ID token from the code exchange is verified against the provider JWKS with +a 30-second clock tolerance. These claims are required and cannot be waived: + +| Claim | Requirement | +|---|---| +| `iss` | Equals the configured issuer | +| `aud` | Equals `AUTH_UPSTREAM_CLIENT_ID` | +| `sub` | Stable per user, 1–512 characters | +| `nonce` | Echoes the value from the authorization request | +| `email` | A valid address, at most 254 characters | +| `email_verified` | The **boolean** `true`; the string `"true"` is rejected | +| `iat`, `exp` | Present | + +`name` is optional; the local part of the email is used when it is absent. + +A provider that cannot assert `email_verified: true` will fail every sign-in +even when discovery passes the preflight check. Confirm this claim explicitly +before scheduling a rollout. + +## Identity mapping + +The plugin never stores the upstream subject directly. It derives +`oidc-` so a subject cannot be +replayed across issuers. Changing the issuer origin therefore re-keys every +linked account: existing users must link their Atlas API key again. diff --git a/package.json b/package.json index 8ef5bf2..f10cb27 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:codex-oauth:chrome-live": "node scripts/codex-oauth-chrome-e2e.mjs", "test:coverage": "tsx --test --experimental-test-coverage test/**/*.test.ts", "validate:production-config": "npm run build && node scripts/validate-production-config.mjs", + "check:upstream-oidc": "node scripts/check-upstream-oidc.mjs", "check": "npm run build && npm test && npm audit --audit-level=high" }, "engines": { diff --git a/scripts/check-upstream-oidc.mjs b/scripts/check-upstream-oidc.mjs new file mode 100755 index 0000000..572e791 --- /dev/null +++ b/scripts/check-upstream-oidc.mjs @@ -0,0 +1,272 @@ +#!/usr/bin/env node + +import { isIP } from "node:net"; + +const MAX_JSON_BYTES = 64 * 1024; +const REQUEST_TIMEOUT_MS = 10_000; +const ASYMMETRIC_ALGORITHMS = ["RS256", "ES256", "PS256"]; +const DEVELOPMENT_LABELS = ["dev", "development", "stage", "staging", "test"]; +const REQUIRED_METADATA_FIELDS = [ + "issuer", + "authorization_endpoint", + "token_endpoint", + "jwks_uri", + "response_types_supported", + "scopes_supported", + "code_challenge_methods_supported", + "token_endpoint_auth_methods_supported", + "id_token_signing_alg_values_supported", +]; + +const issuerInput = process.argv[2] ?? process.env.AUTH_UPSTREAM_ISSUER_URL; +const scopes = (process.env.AUTH_UPSTREAM_SCOPES ?? "openid,email,profile") + .split(",") + .map((scope) => scope.trim()) + .filter(Boolean); +const configuredHosts = (process.env.AUTH_UPSTREAM_ENDPOINT_HOSTS ?? "") + .split(",") + .map((host) => host.trim().toLowerCase()) + .filter(Boolean); +const releaseTier = process.env.PLUGIN_RELEASE_TIER ?? "production"; +const authMethod = process.env.AUTH_UPSTREAM_CLIENT_SECRET === "" ? "none" : "client_secret_basic"; +const callbackUrl = process.env.AUTH_UPSTREAM_CALLBACK_URL ?? + "https://mcp-auth.atlascloud.ai/upstream/callback"; + +if (!issuerInput) { + console.error( + "usage: node scripts/check-upstream-oidc.mjs \n" + + " AUTH_UPSTREAM_ISSUER_URL= node scripts/check-upstream-oidc.mjs" + ); + process.exit(2); +} + +const results = []; +let issuer; + +function record(ok, rule, detail) { + results.push({ ok, rule, detail }); +} + +function check(rule, condition, detail) { + record(Boolean(condition), rule, detail); + return Boolean(condition); +} + +function isPlainHostname(host) { + if (host.includes("*") || /[:\\/@?#\s]/.test(host)) return false; + try { + const parsed = new URL(`https://${host}`); + return parsed.hostname.toLowerCase() === host.toLowerCase() && parsed.port === ""; + } catch { + return false; + } +} + +function isDevelopmentOrStagingHostname(hostname) { + return hostname + .toLowerCase() + .split(".") + .some((label) => DEVELOPMENT_LABELS.includes(label)); +} + +async function boundedJson(url, label) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + try { + const response = await fetch(url, { + method: "GET", + redirect: "error", + signal: controller.signal, + headers: { Accept: "application/json" }, + }); + if (!response.ok) throw new Error(`${label} returned HTTP ${response.status}`); + const bytes = await response.arrayBuffer(); + if (bytes.byteLength > MAX_JSON_BYTES) throw new Error(`${label} response is too large`); + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.toLowerCase().includes("application/json")) { + throw new Error(`${label} did not return JSON (content-type: ${contentType || "absent"})`); + } + return JSON.parse(new TextDecoder().decode(bytes)); + } finally { + clearTimeout(timer); + } +} + +function checkEndpoint(name, value, allowedHosts) { + let url; + try { + url = new URL(value); + } catch { + return check(`${name} is a valid URL`, false, String(value)); + } + const unsafe = + url.protocol !== "https:" || + url.username || + url.password || + url.hash || + (url.port && url.port !== "443") || + !allowedHosts.has(url.hostname.toLowerCase()); + return check( + `${name} is a safe OIDC endpoint`, + !unsafe, + unsafe + ? `${url.origin} must be https, port 443, no credentials or fragment, host in AUTH_UPSTREAM_ENDPOINT_HOSTS (${[...allowedHosts].join(",")})` + : url.origin + ); +} + +try { + issuer = new URL(issuerInput); +} catch { + console.error(`AUTH_UPSTREAM_ISSUER_URL is not a valid URL: ${issuerInput}`); + process.exit(2); +} + +check("issuer URL carries no credentials", !issuer.username && !issuer.password, issuer.origin); +check( + "issuer URL is a bare origin", + issuer.pathname === "/" && !issuer.search && !issuer.hash, + `${issuer.origin}${issuer.pathname}${issuer.search}${issuer.hash}` +); +check("issuer URL uses https", issuer.protocol === "https:", issuer.protocol); +if (releaseTier === "production") { + check( + "issuer host has no dev/staging/test label", + !isDevelopmentOrStagingHostname(issuer.hostname), + issuer.hostname + ); +} +check("scopes include openid and email", scopes.includes("openid") && scopes.includes("email"), scopes.join(",")); + +const allowedHosts = new Set( + configuredHosts.length > 0 ? configuredHosts : [issuer.hostname.toLowerCase()] +); +check( + "endpoint hosts include the issuer host", + allowedHosts.has(issuer.hostname.toLowerCase()), + [...allowedHosts].join(",") +); +check( + "endpoint hosts are exact public hostnames", + [...allowedHosts].every( + (host) => + isPlainHostname(host) && + (releaseTier !== "production" || + (isIP(host) === 0 && + host !== "localhost" && + !host.endsWith(".localhost") && + !host.endsWith(".local") && + host.includes("."))) + ), + [...allowedHosts].join(",") +); + +const discovery = new URL("/.well-known/openid-configuration", issuer); +let metadata; +try { + metadata = await boundedJson(discovery, "Upstream OIDC discovery"); + check("discovery document is reachable without redirects", true, discovery.toString()); +} catch (error) { + check("discovery document is reachable without redirects", false, `${discovery} — ${error.message}`); +} + +if (metadata) { + const missing = REQUIRED_METADATA_FIELDS.filter((field) => metadata[field] === undefined); + check("discovery advertises every required field", missing.length === 0, missing.join(",") || "all present"); + + const arrayField = (field) => (Array.isArray(metadata[field]) ? metadata[field] : []); + const expectedIssuer = issuer.toString().replace(/\/$/, ""); + check( + "discovery issuer matches the configured issuer exactly", + typeof metadata.issuer === "string" && metadata.issuer.replace(/\/$/, "") === expectedIssuer, + `${metadata.issuer} vs ${expectedIssuer}` + ); + check( + "authorization code flow is supported", + arrayField("response_types_supported").includes("code"), + arrayField("response_types_supported").join(",") + ); + check( + "PKCE S256 is supported", + arrayField("code_challenge_methods_supported").includes("S256"), + arrayField("code_challenge_methods_supported").join(",") + ); + const missingScopes = scopes.filter((scope) => !arrayField("scopes_supported").includes(scope)); + check("every requested scope is advertised", missingScopes.length === 0, missingScopes.join(",") || scopes.join(",")); + check( + `token endpoint auth method ${authMethod} is supported`, + arrayField("token_endpoint_auth_methods_supported").includes(authMethod), + arrayField("token_endpoint_auth_methods_supported").join(",") + ); + const algorithms = arrayField("id_token_signing_alg_values_supported").filter((algorithm) => + ASYMMETRIC_ALGORITHMS.includes(algorithm) + ); + check( + "an asymmetric ID-token algorithm is supported", + algorithms.length > 0, + algorithms.join(",") || arrayField("id_token_signing_alg_values_supported").join(",") + ); + + checkEndpoint("authorization_endpoint", metadata.authorization_endpoint, allowedHosts); + checkEndpoint("token_endpoint", metadata.token_endpoint, allowedHosts); + const jwksOk = checkEndpoint("jwks_uri", metadata.jwks_uri, allowedHosts); + + if (jwksOk) { + try { + const jwks = await boundedJson(new URL(metadata.jwks_uri), "Upstream JWKS"); + check( + "JWKS exposes at least one signing key", + Array.isArray(jwks.keys) && jwks.keys.length > 0, + Array.isArray(jwks.keys) ? `${jwks.keys.length} key(s)` : "keys[] absent" + ); + } catch (error) { + check("JWKS exposes at least one signing key", false, error.message); + } + } +} + +const failures = results.filter((result) => !result.ok); +for (const result of results) { + console.log(`${result.ok ? "PASS" : "FAIL"} ${result.rule}${result.detail ? ` — ${result.detail}` : ""}`); +} + +if (metadata) { + const observedHosts = new Set([issuer.hostname.toLowerCase()]); + for (const field of ["authorization_endpoint", "token_endpoint", "jwks_uri"]) { + try { + observedHosts.add(new URL(metadata[field]).hostname.toLowerCase()); + } catch { + continue; + } + } + console.log(""); + console.log(`AUTH_UPSTREAM_ENDPOINT_HOSTS=${[...observedHosts].join(",")}`); +} + +console.log(""); +console.log("Not verifiable from discovery — confirm with the identity provider:"); +console.log(` - ${callbackUrl} is registered as an exact redirect URI`); +console.log(" - the ID token carries sub, nonce, email, email_verified, iat, and exp"); +console.log(" - email_verified is the boolean true, not the string \"true\""); +console.log(" - the ID token audience equals AUTH_UPSTREAM_CLIENT_ID"); +if (releaseTier === "production") { + console.log(" - AUTH_UPSTREAM_CLIENT_SECRET is at least 32 characters"); +} + +console.log(""); +if (failures.length === 0) { + console.log( + "UPSTREAM_OIDC_PRECHECK_PASS", + `issuer=${issuer.hostname}`, + `tier=${releaseTier}`, + `checks=${results.length}` + ); +} else { + console.log( + "UPSTREAM_OIDC_PRECHECK_FAIL", + `issuer=${issuer.hostname}`, + `tier=${releaseTier}`, + `failed=${failures.length}/${results.length}` + ); + process.exit(1); +} From 9114151e2043a7dfa5d17cf4561fcaaa9107e6b3 Mon Sep 17 00:00:00 2001 From: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:06:26 +0800 Subject: [PATCH 5/7] fix(auth): accept upstream OIDC response metadata --- src/auth/app.ts | 12 +++++++----- test/auth-server.test.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/auth/app.ts b/src/auth/app.ts index 22d49b9..d27cf47 100644 --- a/src/auth/app.ts +++ b/src/auth/app.ts @@ -68,7 +68,9 @@ const upstreamCallbackSchema = z iss: z.string().url().optional(), session_state: z.string().min(1).max(4096).optional(), }) - .strict(); + // OIDC providers may append response metadata such as `scope`, `authuser`, + // `hd`, or `prompt`. Validate the fields we consume and discard the rest. + .strip(); const credentialLinkSchema = formSchema.extend({ link_ticket: oneTimeTokenSchema, atlas_api_key: z.string().min(16).max(4096), @@ -909,10 +911,6 @@ export function createAuthorizationApp( if (!rawState.success) { throw new errors.InvalidRequest("invalid upstream OIDC callback state"); } - const pending = await federated.federatedStore.consumeUpstreamAuthorization(rawState.data); - if (!pending) { - throw new errors.InvalidRequest("upstream OIDC callback state is expired or already used"); - } const parsed = upstreamCallbackSchema.safeParse(request.query); if (!parsed.success) { throw new errors.InvalidRequest("upstream OIDC authorization did not return a valid code"); @@ -923,6 +921,10 @@ export function createAuthorizationApp( ) { throw new errors.InvalidRequest("upstream OIDC callback issuer does not match"); } + const pending = await federated.federatedStore.consumeUpstreamAuthorization(rawState.data); + if (!pending) { + throw new errors.InvalidRequest("upstream OIDC callback state is expired or already used"); + } const interaction = await provider.Interaction.find(pending.interactionUid); if ( !interaction || diff --git a/test/auth-server.test.ts b/test/auth-server.test.ts index 44a1fb3..31da643 100644 --- a/test/auth-server.test.ts +++ b/test/auth-server.test.ts @@ -1352,10 +1352,21 @@ test("federated OIDC login links a validated Atlas key before issuing downstream assert.ok(upstreamState); assert.equal(harness.identityStore.authorizations.size, 1); + const malformedUpstreamCallback = new URL(`${harness.baseUrl}/upstream/callback`); + malformedUpstreamCallback.searchParams.set("state", upstreamState); + malformedUpstreamCallback.searchParams.set("iss", "https://identity.example"); + response = await requestWithCookies(jar, malformedUpstreamCallback); + assert.equal(response.status, 400); + assert.equal(harness.identityStore.authorizations.size, 1); + const upstreamCallback = new URL(`${harness.baseUrl}/upstream/callback`); upstreamCallback.searchParams.set("code", "valid-upstream-code"); upstreamCallback.searchParams.set("state", upstreamState); upstreamCallback.searchParams.set("iss", "https://identity.example"); + upstreamCallback.searchParams.set("scope", "openid email profile"); + upstreamCallback.searchParams.set("authuser", "0"); + upstreamCallback.searchParams.set("hd", "atlascloud.ai"); + upstreamCallback.searchParams.set("prompt", "consent"); response = await requestWithCookies(jar, upstreamCallback); assert.equal(response.status, 200); let html = await response.text(); @@ -1513,5 +1524,6 @@ test("federated OIDC login links a validated Atlas key before issuing downstream mismatchCallback.searchParams.set("iss", "https://evil.example"); response = await requestWithCookies(mismatchJar, mismatchCallback); assert.equal(response.status, 400); + assert.equal(harness.identityStore.authorizations.size, 1); assert.equal(harness.upstreamClient.exchanges, 2); }); From 86d8c9a1b2bbc2c149fde980ef04946f11adafb5 Mon Sep 17 00:00:00 2001 From: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:11:10 +0800 Subject: [PATCH 6/7] chore(deploy): pin upstream callback fix image --- deploy/kubernetes/base/staging.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/kubernetes/base/staging.yaml b/deploy/kubernetes/base/staging.yaml index 75dd7b0..da51f63 100644 --- a/deploy/kubernetes/base/staging.yaml +++ b/deploy/kubernetes/base/staging.yaml @@ -139,7 +139,7 @@ spec: type: RuntimeDefault containers: - name: mcp - image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:2cb3bd9c811403d1eb7649635f184e0bb9a5e44c3ef320e2a2ffc24efa8a79ff + image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:bdcbd38822b2ff02f05b74ed510d2ebc49d022f88d4960e87178f74677cbe5f0 imagePullPolicy: IfNotPresent args: ["node", "dist/http.js"] securityContext: @@ -290,7 +290,7 @@ spec: type: RuntimeDefault containers: - name: auth - image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:2cb3bd9c811403d1eb7649635f184e0bb9a5e44c3ef320e2a2ffc24efa8a79ff + image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:bdcbd38822b2ff02f05b74ed510d2ebc49d022f88d4960e87178f74677cbe5f0 imagePullPolicy: IfNotPresent args: ["node", "dist/auth.js"] securityContext: From 1f72a61dfb7109417a220e432fdd37a61bd4cbe0 Mon Sep 17 00:00:00 2001 From: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:12:22 +0800 Subject: [PATCH 7/7] fix(auth): decouple Codex refresh tokens from sessions --- deploy/kubernetes/README.md | 2 +- deploy/kubernetes/base/staging.yaml | 4 +-- .../production.example/production-patch.yaml | 6 ++-- src/auth/app.ts | 6 ++++ src/auth/config.ts | 2 +- test/auth-server.test.ts | 34 ++++++++++++++----- 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md index 49df1ba..f8486d1 100644 --- a/deploy/kubernetes/README.md +++ b/deploy/kubernetes/README.md @@ -47,7 +47,7 @@ Staging uses the longest OAuth lifetimes accepted by the current auth config: | Credential | Lifetime | Behavior | |---|---:|---| | Access token | 3,600 seconds (1 hour) | Short-lived bearer token | -| Refresh token | 604,800 seconds (7 days) | Rotated on refresh; the consumed token allows at most 2 retries inside a fixed 30-second window, then strict replay detection resumes | +| Refresh token | 7,776,000 seconds (90 days) | Independent from the 8-hour browser session and rotated on refresh; the consumed token allows at most 2 retries inside a fixed 30-second window, then strict replay detection resumes | | Authorization grant | 31,536,000 seconds (1 year) | Upper bound for an actively refreshed connection; must not be shorter than the refresh-token lifetime | | Dynamic public client (ChatGPT or Codex) | 31,536,000 seconds (1 year) | Re-registration is normally unnecessary during this period | diff --git a/deploy/kubernetes/base/staging.yaml b/deploy/kubernetes/base/staging.yaml index da51f63..e697cbd 100644 --- a/deploy/kubernetes/base/staging.yaml +++ b/deploy/kubernetes/base/staging.yaml @@ -290,7 +290,7 @@ spec: type: RuntimeDefault containers: - name: auth - image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:bdcbd38822b2ff02f05b74ed510d2ebc49d022f88d4960e87178f74677cbe5f0 + image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:5b9a71a5699980145eb9d1641085aa79c89cf28bfec299f3c0087a0267082d5a imagePullPolicy: IfNotPresent args: ["node", "dist/auth.js"] securityContext: @@ -325,7 +325,7 @@ spec: - name: AUTH_ACCESS_TOKEN_TTL_SECONDS value: "3600" - name: AUTH_REFRESH_TOKEN_TTL_SECONDS - value: "604800" + value: "7776000" - name: AUTH_REFRESH_TOKEN_REUSE_GRACE_SECONDS value: "30" - name: AUTH_REFRESH_TOKEN_REUSE_MAX_ATTEMPTS diff --git a/deploy/kubernetes/production.example/production-patch.yaml b/deploy/kubernetes/production.example/production-patch.yaml index 5bee6c8..f1970b7 100644 --- a/deploy/kubernetes/production.example/production-patch.yaml +++ b/deploy/kubernetes/production.example/production-patch.yaml @@ -8,7 +8,7 @@ spec: spec: containers: - name: mcp - image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:9e3ea858953d01b55eba0a1dce66793c7b5cc19dccb232cd24b4d4aa484022fb + image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:5b9a71a5699980145eb9d1641085aa79c89cf28bfec299f3c0087a0267082d5a env: - name: PLUGIN_RELEASE_TIER value: production @@ -61,7 +61,7 @@ spec: spec: containers: - name: auth - image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:9e3ea858953d01b55eba0a1dce66793c7b5cc19dccb232cd24b4d4aa484022fb + image: registry.atlascloud.ai/vcv-dg/atlascloud-mcp-openai-plugin@sha256:5b9a71a5699980145eb9d1641085aa79c89cf28bfec299f3c0087a0267082d5a env: - name: PLUGIN_RELEASE_TIER value: production @@ -97,6 +97,8 @@ spec: key: auth-upstream-endpoint-hosts - name: AUTH_ACCESS_TOKEN_TTL_SECONDS value: "600" + - name: AUTH_REFRESH_TOKEN_TTL_SECONDS + value: "7776000" - name: AUTH_CREDENTIAL_REDIS_PREFIX value: atlascloud:openai-plugin:credential - name: AUTH_CREDENTIAL_ENCRYPTION_KEYS_JSON diff --git a/src/auth/app.ts b/src/auth/app.ts index d27cf47..a2a1e06 100644 --- a/src/auth/app.ts +++ b/src/auth/app.ts @@ -651,6 +651,12 @@ function providerConfiguration( interactions: { url: async (_ctx, interaction) => `/interaction/${interaction.uid}`, }, + // Codex DCR clients support refresh_token but do not request the OIDC + // offline_access scope. Keep their rotating refresh tokens independent + // from the short-lived interactive browser session; otherwise the token + // remains in Redis but becomes unusable as soon as that session expires. + expiresWithSession: async (ctx) => + !ctx.oidc.client?.grantTypeAllowed("refresh_token"), issueRefreshToken: async (_ctx, client) => client.grantTypeAllowed("refresh_token"), jwks: config.jwks, pkce: { required: () => true }, diff --git a/src/auth/config.ts b/src/auth/config.ts index 8f2f97e..842114e 100644 --- a/src/auth/config.ts +++ b/src/auth/config.ts @@ -116,7 +116,7 @@ const envSchema = z.object({ AUTH_DYNAMIC_CLIENT_TTL_SECONDS: z.coerce.number().int().min(86400).max(31536000).default(7776000), AUTH_GRANT_TTL_SECONDS: z.coerce.number().int().min(3600).max(31536000).optional(), AUTH_ACCESS_TOKEN_TTL_SECONDS: z.coerce.number().int().min(300).max(3600).default(600), - AUTH_REFRESH_TOKEN_TTL_SECONDS: z.coerce.number().int().min(3600).max(604800).default(86400), + AUTH_REFRESH_TOKEN_TTL_SECONDS: z.coerce.number().int().min(3600).max(7776000).default(7776000), AUTH_REFRESH_TOKEN_REUSE_GRACE_SECONDS: z.coerce.number().int().min(0).max(120).default(30), AUTH_REFRESH_TOKEN_REUSE_MAX_ATTEMPTS: z.coerce.number().int().min(0).max(3).default(2), }); diff --git a/test/auth-server.test.ts b/test/auth-server.test.ts index 31da643..e152f91 100644 --- a/test/auth-server.test.ts +++ b/test/auth-server.test.ts @@ -47,11 +47,15 @@ const REVIEWER_PASSWORD = "correct-horse-battery-staple"; const CALLBACK = "https://chatgpt.com/connector/oauth/atlascloud-test-callback"; const CODEX_CALLBACK = "http://127.0.0.1:43123/callback/0jfyHq2aS9Px"; +interface TestAuthorizationStore extends AuthorizationStore { + clearModel(modelName: string): void; +} + function inMemoryStore( refreshTokenReuseGraceSeconds = 0, refreshTokenReuseMaxAttempts = 0, nowSeconds: () => number = () => Math.floor(Date.now() / 1000) -): AuthorizationStore { +): TestAuthorizationStore { const models = new Map>(); const revokeGrant = (grantId: string): void => { for (const records of models.values()) { @@ -132,7 +136,13 @@ function inMemoryStore( }, }; }; - return { adapter: factory, async ready() { return true; } }; + return { + adapter: factory, + async ready() { return true; }, + clearModel(modelName) { + models.get(modelName)?.clear(); + }, + }; } class MemoryFederatedStore implements FederatedIdentityStore { @@ -240,6 +250,7 @@ async function fixture(): Promise<{ server: import("node:http").Server; publicJwk: JWK; auditEvents: AuthorizationAuditEvent[]; + store: TestAuthorizationStore; }> { const port = await freePort(); const baseUrl = `http://127.0.0.1:${port}`; @@ -284,12 +295,13 @@ async function fixture(): Promise<{ refreshTokenReuseMaxAttempts: 2, }; const auditEvents: AuthorizationAuditEvent[] = []; + const store = inMemoryStore( + config.refreshTokenReuseGraceSeconds, + config.refreshTokenReuseMaxAttempts + ); const { app } = createAuthorizationApp( config, - inMemoryStore( - config.refreshTokenReuseGraceSeconds, - config.refreshTokenReuseMaxAttempts - ), + store, { auditLogger: (event) => auditEvents.push(event) } ); return { @@ -298,6 +310,7 @@ async function fixture(): Promise<{ server: await listenAuthorizationApp(app, config), publicJwk, auditEvents, + store, }; } @@ -590,6 +603,7 @@ test("production auth config requires HTTPS and password-protected Redis", async assert.equal(loaded.resource.toString(), "https://mcp.example.com/mcp"); assert.equal(loaded.refreshTokenReuseGraceSeconds, 30); assert.equal(loaded.refreshTokenReuseMaxAttempts, 2); + assert.equal(loaded.refreshTokenTtlSeconds, 90 * 24 * 60 * 60); assert.equal(loaded.grantTtlSeconds, loaded.dynamicClientTtlSeconds); assert.throws( () => loadAuthorizationServerConfig({ @@ -851,7 +865,7 @@ test("OAuth server exposes compliant metadata and rejects unsafe DCR", async (t) assert.equal(stripped.admin, undefined); }); -test("DCR and PKCE rotate refresh tokens with bounded concurrent retry tolerance", async (t) => { +test("Codex scopes keep refresh tokens valid after the browser session expires", async (t) => { const harness = await fixture(); t.after(() => closeServer(harness.server)); const client = await dynamicRegister(harness.baseUrl); @@ -865,7 +879,7 @@ test("DCR and PKCE rotate refresh tokens with bounded concurrent retry tolerance authorization.searchParams.set("client_id", clientId as string); authorization.searchParams.set("redirect_uri", CALLBACK); authorization.searchParams.set("response_type", "code"); - authorization.searchParams.set("scope", ["openid", "email", "offline_access", ...REMOTE_SCOPES].join(" ")); + authorization.searchParams.set("scope", ["openid", "email", ...REMOTE_SCOPES].join(" ")); authorization.searchParams.set("code_challenge", challenge); authorization.searchParams.set("code_challenge_method", "S256"); authorization.searchParams.set("resource", harness.config.resource.toString()); @@ -1075,6 +1089,10 @@ test("DCR and PKCE rotate refresh tokens with bounded concurrent retry tolerance assert.equal(invalidUserinfoResponse.status, 401); assert.match(invalidUserinfoResponse.headers.get("www-authenticate") ?? "", /invalid_token/); + // Codex does not request offline_access. Its refresh token must remain usable + // after the interactive browser session has expired or been removed. + harness.store.clearModel("Session"); + const refreshWithOriginal = () => fetch(`${harness.baseUrl}/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" },