codex·agy OAuth 계정 연결 지원 (ChatGPT/Antigravity 네이티브 어댑터)
Some checks failed
API contract / OpenAPI type drift (push) Failing after 1m0s
Some checks failed
API contract / OpenAPI type drift (push) Failing after 1m0s
omniroute와 동일하게 공개 클라이언트 자격증명으로 서버사이드 OAuth 교환을 제공한다. 관리자가 제공자 로그인 후 브라우저에 남는 code를 붙여넣으면 토큰·refresh token·메타데이터(account-id, Code Assist project)를 저장하고 게이트웨이에 push한다. - codex_api 어댑터: chatgpt.com/backend-api/codex/responses (Responses SSE, 401 시 refresh token으로 자가 갱신) - antigravity_api 어댑터: cloudcode-pa v1internal:streamGenerateContent (Gemini 형식 SSE, loadCodeAssist로 프로젝트 발급, 401 자가 갱신) - 자격증명 저장소에 refresh_token_encrypted·extra 컬럼 추가(부트스트랩 SQL 포함), 게이트웨이 push가 구조화 자격증명을 전달
This commit is contained in:
parent
1a925b33e0
commit
45b84faa0d
10 changed files with 581 additions and 18 deletions
|
|
@ -14,6 +14,7 @@ from __future__ import annotations
|
|||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -223,6 +224,16 @@ class StoredCredential:
|
|||
last_verified_at: float | None
|
||||
last_verify_ok: bool | None
|
||||
last_verify_error: str | None
|
||||
refresh_token: str | None = None
|
||||
extra: dict[str, Any] | None = None
|
||||
|
||||
|
||||
async def _credential_table_ready(conn) -> bool:
|
||||
return bool(
|
||||
await conn.fetchval(
|
||||
"SELECT to_regclass('app.admin_provider_credential') IS NOT NULL"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def ensure_table() -> None:
|
||||
|
|
@ -234,10 +245,7 @@ async def ensure_table() -> None:
|
|||
"""
|
||||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
exists = await conn.fetchval(
|
||||
"SELECT to_regclass('app.admin_provider_credential') IS NOT NULL"
|
||||
)
|
||||
if exists:
|
||||
if await _credential_table_ready(conn):
|
||||
return
|
||||
await conn.execute(
|
||||
"""
|
||||
|
|
@ -251,10 +259,19 @@ async def ensure_table() -> None:
|
|||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_verified_at TIMESTAMPTZ,
|
||||
last_verify_ok BOOLEAN,
|
||||
last_verify_error TEXT
|
||||
last_verify_error TEXT,
|
||||
refresh_token_encrypted TEXT,
|
||||
extra JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
ALTER TABLE app.admin_provider_credential
|
||||
ADD COLUMN IF NOT EXISTS refresh_token_encrypted TEXT,
|
||||
ADD COLUMN IF NOT EXISTS extra JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def get_credential_row(provider: str) -> StoredCredential | None:
|
||||
|
|
@ -281,6 +298,10 @@ async def get_credential_row(provider: str) -> StoredCredential | None:
|
|||
last_verified_at=_row_ts(row["last_verified_at"]),
|
||||
last_verify_ok=row["last_verify_ok"],
|
||||
last_verify_error=row["last_verify_error"],
|
||||
refresh_token=decrypt_token(row["refresh_token_encrypted"])
|
||||
if row.get("refresh_token_encrypted")
|
||||
else None,
|
||||
extra=row.get("extra") if isinstance(row.get("extra"), dict) else None,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -299,7 +320,13 @@ async def get_plain_token(provider: str) -> tuple[str, str] | None:
|
|||
|
||||
|
||||
async def save_credential(
|
||||
*, provider: str, token: str, auth_kind: str, updated_by: str
|
||||
*,
|
||||
provider: str,
|
||||
token: str,
|
||||
auth_kind: str,
|
||||
updated_by: str,
|
||||
refresh_token: str | None = None,
|
||||
extra: dict[str, Any] | None = None,
|
||||
) -> StoredCredential:
|
||||
if provider not in PROVIDER_CODES:
|
||||
raise ProviderCredentialError(f"지원하지 않는 provider입니다: {provider}")
|
||||
|
|
@ -314,26 +341,32 @@ async def save_credential(
|
|||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.admin_provider_credential (
|
||||
provider, token_encrypted, token_hint, auth_kind, updated_by, updated_at
|
||||
provider, token_encrypted, token_hint, auth_kind, updated_by, updated_at,
|
||||
refresh_token_encrypted, extra
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, now())
|
||||
VALUES ($1, $2, $3, $4, $5, now(), $6, $7::jsonb)
|
||||
ON CONFLICT (provider) DO UPDATE SET
|
||||
token_encrypted = EXCLUDED.token_encrypted,
|
||||
token_hint = EXCLUDED.token_hint,
|
||||
auth_kind = EXCLUDED.auth_kind,
|
||||
updated_by = EXCLUDED.updated_by,
|
||||
updated_at = now(),
|
||||
refresh_token_encrypted = COALESCE(EXCLUDED.refresh_token_encrypted, refresh_token_encrypted),
|
||||
extra = CASE WHEN EXCLUDED.extra::text = '{}'::jsonb THEN extra ELSE EXCLUDED.extra END,
|
||||
last_verified_at = NULL,
|
||||
last_verify_ok = NULL,
|
||||
last_verify_error = NULL
|
||||
RETURNING provider, token_hint, auth_kind, updated_by, updated_at,
|
||||
last_verified_at, last_verify_ok, last_verify_error
|
||||
last_verified_at, last_verify_ok, last_verify_error,
|
||||
refresh_token_encrypted, extra
|
||||
""",
|
||||
provider,
|
||||
encrypt_token(plain),
|
||||
token_hint(plain),
|
||||
auth_kind,
|
||||
updated_by,
|
||||
encrypt_token(refresh_token) if refresh_token and refresh_token.strip() else None,
|
||||
json.dumps(extra or {}),
|
||||
)
|
||||
return StoredCredential(
|
||||
provider=row["provider"],
|
||||
|
|
@ -344,6 +377,10 @@ async def save_credential(
|
|||
last_verified_at=_row_ts(row["last_verified_at"]),
|
||||
last_verify_ok=row["last_verify_ok"],
|
||||
last_verify_error=row["last_verify_error"],
|
||||
refresh_token=decrypt_token(row["refresh_token_encrypted"])
|
||||
if row.get("refresh_token_encrypted")
|
||||
else None,
|
||||
extra=row.get("extra") if isinstance(row.get("extra"), dict) else None,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -395,12 +432,21 @@ async def push_credentials_to_gateway() -> dict[str, Any]:
|
|||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"SELECT provider, token_encrypted, auth_kind FROM app.admin_provider_credential"
|
||||
"SELECT provider, token_encrypted, auth_kind, refresh_token_encrypted, extra "
|
||||
"FROM app.admin_provider_credential"
|
||||
)
|
||||
providers = {
|
||||
row["provider"]: {
|
||||
"token": decrypt_token(row["token_encrypted"]),
|
||||
"auth_kind": row["auth_kind"],
|
||||
**(
|
||||
{
|
||||
"refresh_token": decrypt_token(row["refresh_token_encrypted"]),
|
||||
}
|
||||
if row["refresh_token_encrypted"]
|
||||
else {}
|
||||
),
|
||||
"extra": row["extra"] if isinstance(row["extra"], dict) else {},
|
||||
}
|
||||
for row in rows
|
||||
if row["provider"] in PROVIDER_CODES
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue