Skip to content

Add Launchpad OAuth 2 fallback support#6

Merged
jeremy merged 5 commits into
mainfrom
launchpad-oauth-fallback
Jan 20, 2026
Merged

Add Launchpad OAuth 2 fallback support#6
jeremy merged 5 commits into
mainfrom
launchpad-oauth-fallback

Conversation

@jeremy
Copy link
Copy Markdown
Member

@jeremy jeremy commented Jan 20, 2026

Summary

  • Adds Launchpad OAuth 2 as a fallback when BC3 OAuth 2.1 discovery fails
  • Stores oauth_type in credentials for proper token refresh handling
  • Warns when --scope read is used with Launchpad (only supports full access)
  • Updates documentation to explain the two OAuth providers

OAuth Provider Comparison

Feature BC3 OAuth 2.1 Launchpad OAuth 2
Discovery .well-known/oauth-authorization-server Hardcoded endpoints
Client Registration DCR (automatic) Pre-registered
PKCE S256 None
Scopes full, read full only
Auth param response_type=code type=web_server
Token param grant_type=authorization_code type=web_server
Refresh param grant_type=refresh_token type=refresh

Test plan

  • All 326 tests pass
  • Smoke tested BC3 OAuth 2.1 flow in dev (browser consent → token → API calls)
  • Smoke tested Launchpad OAuth 2 fallback in dev (browser consent → token → API calls)
  • Verified --scope read warning appears for Launchpad
  • Verified token refresh uses correct endpoint/params based on stored oauth_type

When BC3 OAuth 2.1 discovery fails (no .well-known endpoint), fall back
to Launchpad OAuth 2 for authentication. This enables bcq to work with
Basecamp instances that don't yet support OAuth 2.1.

Key differences between providers:
- BC3: Uses DCR, PKCE (S256), response_type=code, grant_type params
- Launchpad: Pre-registered clients, no PKCE, type=web_server params

The oauth_type is stored in credentials to ensure proper refresh handling.
Launchpad client credentials can be set via BCQ_CLIENT_ID/BCQ_CLIENT_SECRET
environment variables or oauth_client_id/oauth_client_secret in config.

Warns when --scope read is used with Launchpad (which only supports full
access) and auto-overrides to full scope.
Token refresh now uses the stored oauth_type to select the correct
endpoint and request format:

- Launchpad: Uses BCQ_LAUNCHPAD_TOKEN_URL with type=refresh param
- BC3: Uses discovered token endpoint with grant_type=refresh_token

This ensures refresh works correctly regardless of which OAuth provider
was used for initial authentication. The endpoint is selected based on
stored credentials, not current discovery results, preventing issues if
discovery availability changes between login and refresh.
Tests for:
- auth status displays correct provider (BC3 vs Launchpad)
- JSON output includes oauth_provider field
- _load_launchpad_client loads from env vars and config
- _get_oauth_type defaults to launchpad when discovery fails
- refresh_token uses correct endpoint for each provider
- refresh_token uses correct param format (type=refresh vs grant_type)
- refresh_token preserves oauth_type in credentials
Update bcq auth help to explain:
- Two OAuth providers (BC3 OAuth 2.1 and Launchpad OAuth 2)
- Launchpad always grants full access (--scope read is ignored)
- How to configure Launchpad client credentials
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d856cb8958

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/api.sh Outdated
Address PR review feedback: BC3 refresh was calling _token_endpoint()
which could fall back to Launchpad if discovery fails. This could cause
BC3 tokens to be refreshed against the wrong endpoint.

Fix by storing the token_endpoint in credentials during initial auth,
then preferring the stored endpoint during refresh. Legacy credentials
without stored endpoint fall back to discovery (BC3) or env var
(Launchpad).

Tests verify:
- Stored token_endpoint takes precedence over env vars
- Legacy credentials without stored endpoint still work
@jeremy jeremy merged commit f2b764c into main Jan 20, 2026
@jeremy jeremy deleted the launchpad-oauth-fallback branch January 20, 2026 22:36
jeremy added a commit that referenced this pull request Feb 5, 2026
* Clarify cross-project query limitations in skill docs

- Add invariant #6: project scope is mandatory for resource queries
- Add note above Quick Reference about cross-project limitations
- Fix "My todos" example to include required --in flag
- Add "All todos (cross-project)" example using recordings
- Update decision tree to emphasize recordings as ONLY cross-project option

Addresses #118

* Document --assignee limitation and --card-table requirement

- Clarify --assignee flag only works on todos, not cards/messages
- Add note that cards don't support --assignee filtering
- Document --card-table requirement for projects with multiple card tables
- Add example: bcq cards --card-table <id> --in <project>
- Update columns comment to note --card-table may be needed

Addresses #119

* Document card completion detection and recordings status filtering

- Add card completion detection: parent.type "Kanban::DoneColumn" and completed: true
- Document limitation: Basecamp doesn't track card move timestamps
- Add --status filtering note: recordings default to active status
- Add examples for --status archived and --all flags
- Note that --limit is not supported on timeline commands
- Add decision tree note about status filtering for archived items

Addresses #120

* Revert output pipeline changes, fix project scope docs

Restore json.Number handling in output pipeline to avoid scientific
notation regression with --ids-only. Update SKILL.md to clarify that
project scope can come from --in <project> or .basecamp/config.json.

---------

Co-authored-by: Jeremy Daer <[email protected]>
jeremy added a commit that referenced this pull request Feb 19, 2026
* Add Launchpad OAuth 2 fallback support

When BC3 OAuth 2.1 discovery fails (no .well-known endpoint), fall back
to Launchpad OAuth 2 for authentication. This enables bcq to work with
Basecamp instances that don't yet support OAuth 2.1.

Key differences between providers:
- BC3: Uses DCR, PKCE (S256), response_type=code, grant_type params
- Launchpad: Pre-registered clients, no PKCE, type=web_server params

The oauth_type is stored in credentials to ensure proper refresh handling.
Launchpad client credentials can be set via BCQ_CLIENT_ID/BCQ_CLIENT_SECRET
environment variables or oauth_client_id/oauth_client_secret in config.

Warns when --scope read is used with Launchpad (which only supports full
access) and auto-overrides to full scope.

* Add provider-aware token refresh

Token refresh now uses the stored oauth_type to select the correct
endpoint and request format:

- Launchpad: Uses BCQ_LAUNCHPAD_TOKEN_URL with type=refresh param
- BC3: Uses discovered token endpoint with grant_type=refresh_token

This ensures refresh works correctly regardless of which OAuth provider
was used for initial authentication. The endpoint is selected based on
stored credentials, not current discovery results, preventing issues if
discovery availability changes between login and refresh.

* Add OAuth provider and token refresh tests

Tests for:
- auth status displays correct provider (BC3 vs Launchpad)
- JSON output includes oauth_provider field
- _load_launchpad_client loads from env vars and config
- _get_oauth_type defaults to launchpad when discovery fails
- refresh_token uses correct endpoint for each provider
- refresh_token uses correct param format (type=refresh vs grant_type)
- refresh_token preserves oauth_type in credentials

* Document Launchpad OAuth scope limitations

Update bcq auth help to explain:
- Two OAuth providers (BC3 OAuth 2.1 and Launchpad OAuth 2)
- Launchpad always grants full access (--scope read is ignored)
- How to configure Launchpad client credentials

* Store token_endpoint in credentials for refresh stability

Address PR review feedback: BC3 refresh was calling _token_endpoint()
which could fall back to Launchpad if discovery fails. This could cause
BC3 tokens to be refreshed against the wrong endpoint.

Fix by storing the token_endpoint in credentials during initial auth,
then preferring the stored endpoint during refresh. Legacy credentials
without stored endpoint fall back to discovery (BC3) or env var
(Launchpad).

Tests verify:
- Stored token_endpoint takes precedence over env vars
- Legacy credentials without stored endpoint still work
jeremy added a commit that referenced this pull request Feb 19, 2026
* Clarify cross-project query limitations in skill docs

- Add invariant #6: project scope is mandatory for resource queries
- Add note above Quick Reference about cross-project limitations
- Fix "My todos" example to include required --in flag
- Add "All todos (cross-project)" example using recordings
- Update decision tree to emphasize recordings as ONLY cross-project option

Addresses #118

* Document --assignee limitation and --card-table requirement

- Clarify --assignee flag only works on todos, not cards/messages
- Add note that cards don't support --assignee filtering
- Document --card-table requirement for projects with multiple card tables
- Add example: bcq cards --card-table <id> --in <project>
- Update columns comment to note --card-table may be needed

Addresses #119

* Document card completion detection and recordings status filtering

- Add card completion detection: parent.type "Kanban::DoneColumn" and completed: true
- Document limitation: Basecamp doesn't track card move timestamps
- Add --status filtering note: recordings default to active status
- Add examples for --status archived and --all flags
- Note that --limit is not supported on timeline commands
- Add decision tree note about status filtering for archived items

Addresses #120

* Revert output pipeline changes, fix project scope docs

Restore json.Number handling in output pipeline to avoid scientific
notation regression with --ids-only. Update SKILL.md to clarify that
project scope can come from --in <project> or .basecamp/config.json.

---------

Co-authored-by: Jeremy Daer <[email protected]>
jeremy added a commit that referenced this pull request Mar 5, 2026
Agents were routing "my todos" queries to `recordings todos` which
returns all todos with no assignee data, making per-person filtering
impossible. Five edits steer agents toward `reports assigned` instead:

- Invariant #6: reword to list cross-project exceptions coherently
- Quick reference note: split guidance for assigned work vs type browsing
- Quick reference table: add "My todos (cross-project)" row, annotate
  recordings row with "no assignee data" warning
- Decision tree: add "My assigned work?" branch before type browsing,
  note that recordings cannot filter by person
- Recordings section header: prefer reports assigned for assigned todos
jeremy added a commit that referenced this pull request Mar 5, 2026
* Add hidden --assignee flag to recordings with context-aware redirect

recordings lacks assignee data, so --assignee cannot work. Rather than
silently returning unfiltered results, a hidden flag intercepts the
request and redirects to the correct command:

- With --in/--project: suggests `basecamp todos --assignee <person> --in <project>`
- Without project scope: suggests `basecamp reports assigned [person]`

The guard runs in PreRunE (root) or early RunE (list subcommand) so it
fires before account validation. Hint arguments are %q-quoted for
shell safety, and "me" matching is case-insensitive.

Includes unit tests (11) and e2e tests (4) covering positional args,
--type flag form, --in conditional path, non-"me" assignee preservation,
case-insensitive "me" handling, and hidden flag assertions.

* Fix SKILL.md agent routing for "my todos" queries

Agents were routing "my todos" queries to `recordings todos` which
returns all todos with no assignee data, making per-person filtering
impossible. Five edits steer agents toward `reports assigned` instead:

- Invariant #6: reword to list cross-project exceptions coherently
- Quick reference note: split guidance for assigned work vs type browsing
- Quick reference table: add "My todos (cross-project)" row, annotate
  recordings row with "no assignee data" warning
- Decision tree: add "My assigned work?" branch before type browsing,
  note that recordings cannot filter by person
- Recordings section header: prefer reports assigned for assigned todos
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant