5"""OpenRouter paste-only PKCE authentication.
9Exchange a manually pasted authorization code for a provider key.
13Generate PKCE, display the URL through callbacks, and enforce transport policy.
20 L3 - semantic details without credentials
25No listener, browser launch, refresh, proxy inheritance or redirect following.
26The operator approved this fixed-endpoint sink outside ClientInterface for P6.
27Credentials and transport exception contents never reach output channels.
31- oc_diagnostics (diagnostics)
32- httpx (existing provider transport dependency)
33- option_c_agent_contracts.provider_plugin (OAuth protocols and credentials)
35from __future__
import annotations
41from types
import MappingProxyType
42from typing
import Final, Mapping
43from urllib.parse
import parse_qs, urlencode, urlsplit
46from oc_diagnostics
import _dbg
47from option_c_agent_contracts.provider_plugin
import OAuthCredentials, OAuthLoginCallbacks
49_dbg(
'OC_CHAT.CLIENT',
'module',
'loaded', 4)
51TOKEN_ENDPOINTS: Final[Mapping[str, str]] = MappingProxyType({
52 'openrouter':
'https://openrouter.ai/api/v1/auth/keys',
54CALLBACK_URL: Final =
'http://localhost:3000/'
55AUTHORIZE_URL: Final =
'https://openrouter.ai/auth'
59 """Accept a bare code or the exact callback URL copied from the browser."""
62 parts = urlsplit(text)
63 callback = urlsplit(CALLBACK_URL)
64 if (parts.scheme, parts.netloc, parts.path) != (callback.scheme, callback.netloc, callback.path):
65 raise ValueError(
'Unexpected OAuth callback URL.')
66 codes = parse_qs(parts.query).get(
'code', [])
68 raise ValueError(
'Missing OAuth code.')
70 if not text
or len(text) > 4096
or any(char.isspace()
for char
in text):
71 raise ValueError(
'Invalid OAuth code.')
76 """Use the fixed HTTPS endpoint with no proxy inheritance or redirects."""
77 endpoint = TOKEN_ENDPOINTS[
'openrouter']
78 parts = urlsplit(endpoint)
79 if parts.scheme !=
'https' or parts.hostname !=
'openrouter.ai' or parts.username
or parts.password:
80 raise ValueError(
'Invalid OAuth token endpoint.')
82 with httpx.Client(trust_env=
False, follow_redirects=
False, timeout=30.0)
as client:
83 response = client.post(endpoint, json={
85 'code_verifier': verifier,
86 'code_challenge_method':
'S256',
88 if not 200 <= response.status_code < 300:
89 raise ValueError(
'OAuth exchange refused.')
90 payload = response.json()
92 raise ValueError(
'OAuth exchange failed; run /login openrouter again.')
from None
93 value = payload.get(
'key')
if isinstance(payload, dict)
else None
94 if not isinstance(value, str)
or not value.strip():
95 raise ValueError(
'OAuth exchange returned no usable key.')
100 """Optional capability for the OpenRouter catalogue row only."""
102 def oauth_login(self, callbacks: OAuthLoginCallbacks) -> OAuthCredentials:
103 """Display the URL and exchange the pasted code without opening a UI."""
104 verifier = secrets.token_urlsafe(32)
105 challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode(
'ascii')).digest()).rstrip(b
'=').decode(
'ascii')
106 url = AUTHORIZE_URL +
'?' + urlencode({
107 'callback_url': CALLBACK_URL,
108 'code_challenge': challenge,
109 'code_challenge_method':
'S256',
111 callbacks.on_auth(url)
112 pasted = callbacks.on_prompt(
'Paste the code or final redirect URL from the browser: ')
114 return OAuthCredentials(access=
_exchange(code, verifier), obtained_at=time.time())
117 """OpenRouter keys do not require a refresh flow in this first lane."""
118 raise NotImplementedError(
'Refresh is unavailable; run /login openrouter.')
121 """Return the bearer string expected by the existing provider client."""
str oauth_api_key(self, OAuthCredentials creds)
OAuthCredentials oauth_login(self, OAuthLoginCallbacks callbacks)
OAuthCredentials oauth_refresh(self, OAuthCredentials creds)
str _exchange(str code, str verifier)
str _pasted_code(str value)