mpfaffenberger / mpfaffenberger/code_puppy

OAuth provider plugins (chatgpt_oauth / claude_code_oauth / copilot_auth + bedrock/foundry) re-implement PKCE, token refresh, and <provider>_models.json plumbing in parallel

Open
#456 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
814
Forks
278
Avg merge
2d 5h
Merged PRs (30d)
76

Description

Theme

Three OAuth-style provider plugins each ship a 500-650 line utils.py that is the same machine with provider constants swapped in. (Distinct from #406, which covers credential lookup in core config.py/model_factory.py/provider_credentials.py — this is the plugin-tier auth/token/models-file stack.)

1. PKCE / OAuth context scaffolding (chatgpt vs claude_code)

plugins/chatgpt_oauth/utils.py:29-112 vs plugins/claude_code_oauth/utils.py:33-112:

  • class OAuthContext dataclass
  • _urlsafe_b64encode, _generate_code_verifier, _compute_code_challenge (verifier differs only in token_hex(64) vs _urlsafe_b64encode(token_bytes(64)))
  • prepare_oauth_context, assign_redirect_uri, build_authorization_url — identical shape, only CHATGPT_OAUTH_CONFIG vs CLAUDE_CODE_OAUTH_CONFIG keys and a couple of query params differ.

Drift already visible: claude_code caches the context in a module global (get_oauth_context/clear_oauth_context); chatgpt adds a 4-minute expires_at; claude_code validates nothing about ports while chatgpt enforces required_port. None of these differences are provider-mandated — they are divergent maintenance.

2. refresh/load/save token flows
  • chatgpt_oauth/utils.py:141-256 (load_stored_tokens, get_valid_access_token, refresh_access_token, save_tokens)
  • claude_code_oauth/utils.py:129-311 (same five concepts + expiry-buffer helpers)
  • copilot_auth/utils.py:287-382 (_persist_session, _load_persisted_session, get_valid_session_token — same load-check-expiry-refresh-persist loop for device-flow tokens)

All three are: read JSON token file -> check expiry -> POST refresh -> merge -> write JSON -> return access token, each with its own logging strings and subtle differences in which fields survive the merge.

3. <provider>_models.json load/save/remove (5x)

The same three functions exist five times:

  • chatgpt_oauth/utils.py:258-279,514-530load_chatgpt_models / save_chatgpt_models / remove_chatgpt_models
  • claude_code_oauth/utils.py:313-322,371-381,642-658load_claude_models / save_claude_models / remove_claude_code_models
  • copilot_auth/utils.py:408-451load_copilot_models / save_copilot_models / remove_copilot_models
  • aws_bedrock/utils.py:17-44,129load_extra_models / save_extra_models / remove_bedrock_models_from_config
  • azure_foundry/utils.py:102-135,318load_extra_models / save_extra_models / remove_foundry_models_from_config

Each is try/open/json.load with a logged fallback, json.dump(indent=2), and a remove that filters by a provider marker key. Drift example: copilot validates isinstance(data, dict) and warns on non-object JSON; chatgpt and claude_code silently return whatever json.load gives them, so a corrupted chatgpt_models.json containing a list propagates into model config.

Proposed shared abstraction

Two small modules under code_puppy/plugins/_provider_common/ (builtin-tier shared lib, no hook changes needed):

# pkce.py
@dataclass
class PKCEContext: ...
def new_pkce_context(*, urlsafe: bool = True) -> PKCEContext: ...
def build_authorization_url(cfg: Mapping, ctx: PKCEContext, extra_params: Mapping = ()) -> str: ...

# token_store.py
class JsonTokenStore:
    def __init__(self, path: Path): ...
    def load(self) -> dict | None: ...
    def save(self, tokens: dict) -> bool: ...
    def refresh(self, *, token_url, client_id, merge_keys=("id_token",)) -> str | None: ...

# models_file.py
class ProviderModelsFile:
    def __init__(self, path: Path, source_marker: str): ...
    def load(self) -> dict: ...      # with isinstance(dict) validation (copilot's behavior)
    def save(self, models: dict) -> bool: ...
    def remove_all(self) -> int: ... # filter by source_marker

What stays per-plugin: provider config dicts, model-entry builders (_build_model_entry, settings inference), model filtering (filter_latest_claude_models), and device-flow specifics. The win: a fourth OAuth provider plugin becomes ~150 lines instead of ~600, and token-file corruption handling is fixed in one place.

Filed by Zen Reviewer C (code-puppy-60635a) — DRY review round

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by comparing the named ranges in plugins/chatgpt_oauth/utils.py, plugins/claude_code_oauth/utils.py, copilot_auth/utils.py, aws_bedrock/utils.py, and azure_foundry/utils.py, then inspect the proposed code_puppy/plugins/_provider_common/ modules. Done means the shared PKCE, token-store, and models-file behavior replaces duplicated plugin logic while provider-specific builders and device-flow details remain intact; no tests are named in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
authentication, tooling
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.