Skip to content

BC5 OAuth Ask #1: resource-first discovery (5 SDKs)#369

Merged
jeremy merged 38 commits into
mainfrom
oauth-discovery-transition
Jul 17, 2026
Merged

BC5 OAuth Ask #1: resource-first discovery (5 SDKs)#369
jeremy merged 38 commits into
mainfrom
oauth-discovery-transition

Conversation

@jeremy

@jeremy jeremy commented Jul 15, 2026

Copy link
Copy Markdown
Member

Resource-first OAuth discovery

Why this exists

Basecamp is introducing a first-party OAuth 2.1 authorization server. Until now these SDKs spoke to a single, well-known OAuth host (37signals' Launchpad). As Basecamp moves toward per-service authorization servers, an SDK can no longer assume where a given API's OAuth endpoints live — it has to discover them.

This PR makes that discovery a first-class, standards-based capability so application developers never hardcode OAuth URLs again. Point an SDK at a Basecamp API host and it resolves the correct authorization server for you — correctly even during the upcoming migration window, when both the legacy host and the new first-party server may be advertised at the same time.

What you get — one uniform API across Go, TypeScript, Python, Ruby, and Kotlin

Three composable operations, identical in shape and behavior in every SDK:

  • discover(issuer) — RFC 8414 Authorization Server Metadata.
  • discoverProtectedResource(origin) — RFC 9728 Protected Resource Metadata.
  • discoverFromResource(origin) — the orchestrator: start from an API host, read its protected-resource metadata, and select the correct authorization server using a documented selection-and-fallback policy.

Where this fits — part 1 of 2

  1. Discovery — this PR.
  2. Device flow (RFC 8628) — BC5 OAuth: device flow (RFC 8628, 5 SDKs) #370, stacked on this branch. Browserless sign-in for CLIs and input-constrained devices.

Both land ahead of the server-side rollout so the client ecosystem is ready the moment the new authorization server goes live. Nothing here waits on that rollout to be useful (see Compatibility).

Compatibility — safe to adopt today

  • Additive and backward-compatible. Existing discover(...) usage is unchanged; the resource-first API is opt-in.
  • Graceful through the migration. Until the new server advertises resource metadata, discoverFromResource transparently falls back to today's Launchpad flow. Verified live: a Basecamp API host with no resource metadata yet resolves to a soft resource_discovery_failed fallback and proceeds against Launchpad, and discover("https://launchpad.37signals.com") binds and resolves against the current production metadata.
  • One forward-looking model change: authorization_endpoint is now optional in the discovered config, because some authorization servers (e.g. device-only) legitimately omit it. Authorization-code callers should assert it is present before use; token_endpoint remains required.

Design intent

  • Security-first, both hops. Discovery fetches are SSRF-hardened: HTTPS-only origins are validated before any socket opens, redirects are suppressed, timeouts are bounded by a monotonic wall-clock deadline, and response bodies are read under a streaming cap that aborts before an oversized body is buffered. Issuer and resource identifiers are bound to the advertised value code-point-exact (no normalization), and the credentialed authorization fetch can never be redirected to a foreign origin by a hand-constructed config.
  • Selection and fallback are explicit, not heuristic guesswork. An explicit expectedIssuer selects by exact match. Otherwise a documented Basecamp-profile rule selects the single non-Launchpad issuer, treats two or more as an ambiguous-issuer error rather than guessing, and falls back to Launchpad when none is advertised. A soft fallback (to Launchpad) happens only for resource_discovery_failed / no_as_advertised; once a first-party issuer is committed, every later failure is a hard, typed error that never silently issues a Launchpad request.
  • One contract, five languages. A shared specification plus a language-agnostic conformance fixture suite (make oauth-fixtures-check) keep all five SDKs behaviorally identical — same selection rules, same fallback state machine, same rejection of malformed metadata (missing/empty/null endpoints, non-array list fields, non-origin issuers, and parser-differential authority tricks).

Verification

make check — all five SDKs (plus Swift), the cross-SDK OAuth conformance suite, and the schema/lint gates — passes at the branch tip. Discovery is additionally verified live against production Launchpad metadata.


Summary by cubic

Implements resource-first OAuth discovery (RFC 9728 + RFC 8414) across Go, TypeScript, Python, Ruby, and Kotlin with strict issuer/resource binding, SSRF hardening, and a shared conformance suite. Update: Go now makes RegistrationEndpoint optional and normalizes origin ports numerically to avoid :000443 misclassification during issuer selection.

  • New Features

    • Discovery ops: discover(issuer), discoverProtectedResource(origin), and discoverFromResource(origin, expectedIssuer?) with code‑point issuer/resource binding while fetching from normalized origins.
    • Selection: honors expectedIssuer; excludes Launchpad by profile; dedupes duplicate issuers; ambiguity and binding mismatches are hard errors; soft fallback only on resource_discovery_failed or no_as_advertised.
    • Security: origin‑root validation via platform parsers; HTTPS‑only (localhost exempt); redirects suppressed; bounded timeouts; streaming‑capped reads; non‑2xx → api_error. Rejects bare ?/#, missing/extra‑slash authority, dangling/signed/zero ports, empty userinfo, invalid characters (C0/space/backslash), and dot‑segment paths. TypeScript truncates non‑2xx discovery bodies in error messages to 500 characters. Ruby adds a monotonic wall‑clock deadline over streaming reads and Ruby/Python normalize non‑finite/invalid discovery timeouts so the deadline cannot be disabled (Python guards oversized ints; Ruby rejects non‑real numerics).
    • Conformance: cross‑SDK fixtures + JSON Schema with make oauth-fixtures-check; scenarios cover raw‑string binding, deduped issuers, present‑null metadata, origin‑root rejections (incl. dot‑segment paths), SSRF defenses, and unmodeled endpoint validation.
    • Per‑SDK: TypeScript/Python add dot‑segment raw‑path rejection; Kotlin validates every advertised *_endpoint as a non‑empty string; Go propagates caller context cancellation on both hop‑1 and hop‑2 (no soft fallback; no Launchpad request) and makes RegistrationEndpoint optional with numeric port normalization to canonicalize origins. Earlier updates retained — e.g., TypeScript origin validation (missing/extra‑slash authority, non‑string inputs), and Ruby/Kotlin appending the device endpoint to keep positional APIs stable.
  • Migration

    • Auth‑code flows must assert authorization_endpoint before use.
    • Prefer discoverFromResource; set expectedIssuer when multiple issuers are advertised.
    • TypeScript: use resourceBaseUrl (mutually exclusive with baseUrl; presence‑checked).
    • Python: pass None explicitly for a missing authorization_endpoint; positional field order unchanged.
    • Ruby/Kotlin: device_authorization_endpoint is appended in config types; positional callers remain stable.

Written for commit be0f992. Summary will update on new commits.

Review in cubic

Copilot AI review requested due to automatic review settings July 15, 2026 14:46
@github-actions github-actions Bot added documentation Improvements or additions to documentation typescript Pull requests that update TypeScript code ruby Pull requests that update the Ruby SDK go kotlin conformance Conformance test suite enhancement New feature or request and removed documentation Improvements or additions to documentation labels Jul 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements BC5 OAuth “resource-first discovery” (RFC 9728 + RFC 8414) across Go, TypeScript, Python, Ruby, and Kotlin, backed by shared cross-SDK conformance fixtures and SSRF-hardening requirements. This establishes the first PR in a stacked series for the BC5 OAuth go-live SDK contract and standardizes error taxonomy + endpoint optionality (notably authorization_endpoint).

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Changes:

  • Add resource-first discovery ops (discoverProtectedResource, discoverFromResource) + stage-sensitive fallback, plus model updates (nullable/optional authorization_endpoint, device/grant fields).
  • Add SSRF hardening for both discovery hops (HTTPS-only origins, redirect suppression, bounded/streaming body reads, standardized non-2xx → api_error).
  • Introduce shared conformance/oauth fixture set + JSON Schema validation, and wire oauth-fixtures-check into make conformance.

Reviewed changes

Copilot reviewed 72 out of 73 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
typescript/tests/oauth/resource-discovery.test.ts New fixture-driven TS tests for resource-first discovery + SSRF behaviors.
typescript/tests/oauth/oauth.test.ts Align TS discovery non-2xx classification to api_error.
typescript/tests/oauth/interactive-login.test.ts Add issuer-binding correctness + fatal-after-selection tests for resource-first login.
typescript/src/oauth/types.ts Extend OAuth types: optional auth endpoint, protected-resource metadata, selection/fallback types.
typescript/src/oauth/interactive-login.ts Add resource-first discovery mode + mutual exclusivity with legacy baseUrl; assert auth-code capability.
typescript/src/oauth/index.ts Re-export new resource-first discovery APIs and types.
typescript/README.md Document BC5 resource-first discovery API + selection/fallback semantics and migration notes.
SPEC.md Define the cross-SDK contract for resource-first discovery, origin-root profile, fallback state machine, and SSRF rules.
ruby/test/test_helper.rb Update Ruby test helpers to stub hop-1 protected-resource discovery (not AS discovery).
ruby/test/basecamp/services/authorization_service_test.rb Expand Ruby authorization service tests for soft fallback, committed-issuer behavior, and cross-origin BC5 issuer.
ruby/test/basecamp/oauth_test.rb Align Ruby non-2xx discovery classification to api_error (+ http_status assertion).
ruby/test/basecamp/oauth_ssrf_test.rb New Ruby SSRF tests for bounded streaming reads + redirect-middleware rejection + redirect suppression.
ruby/test/basecamp/oauth_resource_discovery_test.rb New Ruby fixture-driven tests for cross-SDK conformance scenarios.
ruby/README.md Document Ruby resource-first discovery API + fallback/raise semantics and SSRF hardening.
ruby/lib/basecamp/services/authorization_service.rb Move authorization service out of generated/ and implement resource-first discovery + issuer-scoped cross-origin allowance.
ruby/lib/basecamp/security.rb Add origin-root validation helper (require_origin_root!) and document cross-origin trust expansion.
ruby/lib/basecamp/oauth/resource.rb Implement RFC 9728 protected-resource fetch + code-point binding + structural validation of authorization_servers.
ruby/lib/basecamp/oauth/protected_resource_metadata.rb Add Ruby model preserving absent vs empty authorization_servers.
ruby/lib/basecamp/oauth/fetcher.rb Add shared SSRF-hardened fetcher with redirect suppression + bounded streaming read cap.
ruby/lib/basecamp/oauth/discovery.rb Update RFC 8414 discovery to issuer-bind, relax auth endpoint requirement, validate endpoints + grant types.
ruby/lib/basecamp/oauth/discovery_selection_error.rb Add typed hard selection error for “fatal-after-selection” cases.
ruby/lib/basecamp/oauth/discovery_result.rb Add result type representing selected vs soft fallback outcomes.
ruby/lib/basecamp/oauth/config.rb Update Ruby OAuth config model: optional auth endpoint + device/grant fields.
ruby/lib/basecamp/oauth.rb Add protected-resource discovery + orchestrator implementing selection and stage-sensitive fallback.
ruby/lib/basecamp/http.rb Allow one additional discovered issuer origin for credentialed get_absolute requests (in addition to Launchpad).
ruby/lib/basecamp/generated/services/authorization_service.rb Delete generated authorization service (replaced by handwritten runtime service).
python/tests/oauth/test_resource_discovery.py New Python fixture-driven resource-first discovery tests + origin-root and absent-vs-empty assertions.
python/tests/oauth/test_discovery.py Update Python localhost discovery test to satisfy issuer-binding requirement.
python/src/basecamp/oauth/errors.py Add DiscoverySelectionError with typed reason tokens and taxonomy mapping.
python/src/basecamp/oauth/discovery.py Implement RFC 9728 + RFC 8414 composable ops + orchestrator; add SSRF-hardened streaming fetch.
python/src/basecamp/oauth/config.py Expand Python OAuth models for optional endpoints, protected-resource metadata, and fallback result types.
python/src/basecamp/oauth/init.py Re-export new discovery APIs, models, and require_origin_root.
python/src/basecamp/_security.py Add Python require_origin_root implementing the origin-root profile (incl. IPv6 + default port dropping).
python/README.md Document Python resource-first discovery + migration notes for optional authorization_endpoint.
Makefile Add oauth-fixtures-check (pinned check-jsonschema via uvx) and run it as part of conformance.
kotlin/sdk/src/commonTest/kotlin/com/basecamp/sdk/OAuthDiscoveryTest.kt Add Kotlin commonTest scenarios mirroring shared OAuth fixtures + SSRF assertions.
kotlin/sdk/src/commonTest/kotlin/com/basecamp/sdk/ErrorTest.kt Include new DiscoverySelection exception in Kotlin error-code mapping tests.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/Urls.kt Extract shared URL parsing + origin-root/secure-endpoint helpers (Ktor parser based).
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/Pagination.kt Remove local URL parsing helpers in favor of Urls.kt shared helpers.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/oauth/Exchange.kt Enforce secure token endpoint before credential POST (localhost exempt).
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/oauth/Discovery.kt Implement resource-first discovery + SSRF hardening and bounded reads for Kotlin.
kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/BasecampException.kt Add Kotlin DiscoverySelection exception type with taxonomy-aligned codes.
kotlin/README.md Document Kotlin resource-first discovery + nullable authorizationEndpoint consumer guidance.
go/README.md Document Go resource-first discovery APIs, options, and migration notes.
go/pkg/basecamp/oauth/types.go Expand Go OAuth types for optional endpoints, protected-resource metadata, fallback result, and hard-selection errors.
go/pkg/basecamp/oauth/resource_discovery_test.go Add Go fixture-driven conformance tests + origin-root profile unit tests.
go/pkg/basecamp/oauth/oauth_test.go Update Go discovery tests for issuer binding and trailing-slash origin-root acceptance.
go/pkg/basecamp/oauth/discovery.go Implement Go resource-first discovery with origin-root validation, SSRF hardening, bounded reads, and redirect suppression.
conformance/oauth/schema.json Add JSON Schema for OAuth discovery fixtures.
conformance/oauth/README.md Document fixture intent, validation, and placeholder substitution rules.
conformance/oauth/fixtures/01-two-hop-happy-path.json Add fixture scenario 01.
conformance/oauth/fixtures/02-no-as-advertised-absent.json Add fixture scenario 02.
conformance/oauth/fixtures/03-no-as-advertised-empty-array.json Add fixture scenario 03.
conformance/oauth/fixtures/04-only-launchpad.json Add fixture scenario 04.
conformance/oauth/fixtures/05-resource-mismatch.json Add fixture scenario 05.
conformance/oauth/fixtures/06-hop1-transport-failure.json Add fixture scenario 06.
conformance/oauth/fixtures/07-issuer-binding-mismatch.json Add fixture scenario 07.
conformance/oauth/fixtures/08-as-metadata-500.json Add fixture scenario 08.
conformance/oauth/fixtures/09-ambiguous-issuers.json Add fixture scenario 09.
conformance/oauth/fixtures/10-expected-issuer-selected.json Add fixture scenario 10.
conformance/oauth/fixtures/11-expected-issuer-unavailable.json Add fixture scenario 11.
conformance/oauth/fixtures/12-empty-string-endpoint.json Add fixture scenario 12.
conformance/oauth/fixtures/13-device-only-as.json Add fixture scenario 13.
conformance/oauth/fixtures/14-origin-root-http-nonlocalhost.json Add fixture scenario 14.
conformance/oauth/fixtures/15-origin-root-malformed-port.json Add fixture scenario 15.
conformance/oauth/fixtures/16-origin-root-path-rejected.json Add fixture scenario 16.
conformance/oauth/fixtures/17-origin-root-ipv6-localhost-accept.json Add fixture scenario 17.
conformance/oauth/fixtures/18-ssrf-oversized-body.json Add fixture scenario 18.
conformance/oauth/fixtures/19-ssrf-redirect-not-followed.json Add fixture scenario 19.
conformance/oauth/fixtures/20-invalid-issuer-origin.json Add fixture scenario 20.
conformance/oauth/fixtures/21-authorization-servers-not-array.json Add fixture scenario 21.
conformance/oauth/fixtures/22-grant-types-not-array.json Add fixture scenario 22.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ruby/lib/basecamp/security.rb Outdated

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

Copy link
Copy Markdown

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: b0121732bb

ℹ️ 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 go/pkg/basecamp/oauth/discovery.go Outdated
Comment thread typescript/src/oauth/discovery.ts Outdated
Comment thread python/src/basecamp/oauth/discovery.py Outdated
Comment thread kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/oauth/Discovery.kt Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 73 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread ruby/lib/basecamp/http.rb Outdated
Comment thread go/pkg/basecamp/oauth/resource_discovery_test.go
Comment thread ruby/lib/basecamp/oauth/fetcher.rb
Comment thread ruby/lib/basecamp/oauth/resource.rb Outdated
Comment thread typescript/src/oauth/types.ts
Comment thread typescript/src/oauth/interactive-login.ts Outdated
Comment thread ruby/test/basecamp/oauth_resource_discovery_test.rb Outdated
Comment thread conformance/oauth/schema.json
Comment thread ruby/lib/basecamp/services/authorization_service.rb Outdated
@jeremy
jeremy force-pushed the oauth-discovery-transition branch from b012173 to 336df9d Compare July 15, 2026 19:34
Copilot AI review requested due to automatic review settings July 15, 2026 19:34
@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed documentation Improvements or additions to documentation labels Jul 15, 2026
@jeremy

jeremy commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

Review round 2 — pushed (history folded, 6 commits/branch preserved)

Addressed the genuine blockers from the automated + manual review. Fixes are folded into their owning per-SDK commits (discovery-layer → PR #369, device-layer → PR #370); both branch tips pass full make check.

Discovery (PR #369):

  • Kotlin: coroutine CancellationException now propagates (no silent Launchpad fallback after a cancelled discovery); requireOriginRoot rejects empty userinfo.
  • Ruby: get_absolute(allow_origin:) is no longer a general credential escape hatch — allow_origin is re-validated through require_origin_root! (fails closed on non-https/path/query/junk), so the bearer token can only reach Launchpad, the configured base, or a discovery-selected-and-validated issuer.
  • Go & Ruby: origin parsing rejects out-of-range ports (1–65535).
  • TS & Python: issuer-mismatch is classified via a structured marker (module-private to the discovery module), not error-message text.
  • Python & Ruby: response field type validation (strings/ints/arrays-of-strings), not truthiness.

Device flow (PR #370):

  • Bounded/streaming reads on device-auth + token responses (TS, Python, Ruby, Kotlin; Go already bounded).
  • Redirect suppression on all device POSTs (Go ErrUseLastResponse, Kotlin followRedirects=false, TS redirect:"manual", Python follow_redirects=False, Ruby injected-client guard).
  • TS: malformed 2xx token response → api_error (not retryable transport). Kotlin: empty access_token rejected.
  • Polling deadline now bounds everything: per-request timeout always set, backoff wait clamped so it can't overshoot the monotonic expires_in deadline, deadline re-checked after the display hook.
  • READMEs: device examples now derive the config from resource-first discovery (not Launchpad, which advertises no device endpoint).

Dismissed — false positive: the Ruby IPv6 claim is incorrect under the project Ruby — URI.parse("http://[::1]:3000").host preserves [::1], and require_origin_root!/localhost? already handle it. No change.

Trivy (Go SDK) check: the failure is a scanner-version/DB discrepancy on an unchanged go.sum (neither PR touches go.mod/go.sum); local trivy — 0.69.1 and a fresh :2 DB — reports zero HIGH/CRITICAL. It reproduces on main, so it's pre-existing and needs a CI-side Trivy pin/.trivyignore or a dependency bump on main, not a change in these feature PRs.

Still under the agreed merge hold pending bc3 #9471.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 74 out of 75 changed files in this pull request and generated 5 comments.

Comment thread python/src/basecamp/_security.py Outdated
Comment thread ruby/lib/basecamp/oauth/discovery.rb
Comment thread ruby/lib/basecamp/oauth.rb Outdated

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

Copy link
Copy Markdown

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: 336df9d781

ℹ️ 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 typescript/README.md
Comment thread typescript/src/oauth/discovery.ts
Comment thread ruby/lib/basecamp/oauth.rb Outdated
@jeremy
jeremy marked this pull request as draft July 15, 2026 23:54
@jeremy
jeremy force-pushed the oauth-discovery-transition branch from 336df9d to 64e2ef3 Compare July 16, 2026 02:10
@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed documentation Improvements or additions to documentation labels Jul 16, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed documentation Improvements or additions to documentation labels Jul 16, 2026
@jeremy
jeremy force-pushed the oauth-discovery-transition branch from c9a5fb3 to 3b2d1c3 Compare July 16, 2026 04:14

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

Copy link
Copy Markdown

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: ccdae8b82a

ℹ️ 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 kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/oauth/Discovery.kt Outdated
Kotlin only checked its four modeled endpoint fields, so an unmodeled key
such as revocation_endpoint present with null or an empty string slipped
through ignoreUnknownKeys during decode — accepting metadata that SPEC §16
requires be rejected (any present *_endpoint must be a non-empty string).

Iterate every *_endpoint key on the parsed JsonObject before decoding and
reject a present null, empty string, or non-string value uniformly, matching
Go's rejectEmptyEndpoints and the TS/Python/Ruby generic loops. Drops the
now-redundant modeled-field empty-string loop in bindAsMetadata.

Adds shared fixture 38 (unmodeled revocation_endpoint: "") exercising all
four fixture-driven SDKs, plus Kotlin embedded mirrors for the empty and
null variants.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 91 out of 92 changed files in this pull request and generated no new comments.

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

Copy link
Copy Markdown

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: 59ed7e4778

ℹ️ 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 go/pkg/basecamp/oauth/discovery.go
DiscoverFromResource's hop-1 failure branch converted every non-usage error
into a soft resource_discovery_failed fallback (nil error), so a caller who
cancelled the context or hit their deadline received a fallback result and a
correct consumer would then proceed to a credentialed Launchpad request —
exactly what the abort was meant to prevent.

Check the parent ctx.Err() before returning the soft fallback and propagate
the cancellation. fetchDiscoveryDocument derives its own per-fetch timeout as
a CHILD context, so a non-nil parent Err() is unambiguously caller-initiated;
the SDK's own timeout leaves the parent untouched and still soft-falls-back.

Hop 2 already surfaces cancellation through *SelectionError's Unwrap chain and
never contacts Launchpad, so this is scoped to hop 1. Adds tests for both the
caller-cancellation and SDK-own-timeout paths.

The other four SDKs are unaffected: TS's AbortController is internal-timeout
only (no caller signal), Python/Ruby are synchronous (interrupts propagate as
BaseException past the OAuthError rescues), and Kotlin's hop-1 catch is typed
to BasecampException while fetchDiscoveryDocument rethrows CancellationException.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 91 out of 92 changed files in this pull request and generated 1 comment.

Comment thread typescript/src/oauth/discovery.ts
The TypeScript hop failure path interpolated the entire bounded response body
into the BasecampError message. The bounded read still permits up to
maxBodyBytes (1 MiB default), so a large or hostile non-2xx body could produce
an enormous error string — log spam, memory pressure, and potential echoing of
sensitive server content.

Cap the embedded body with the existing truncateErrorMessage helper (500
chars), matching Go's truncateBody, Python's truncate, and Ruby's
Security.truncate. Kotlin already omits the body from its discovery error
message entirely. Adds a test asserting a 200 KB body yields a bounded message.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 91 out of 92 changed files in this pull request and generated no new comments.

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

Copy link
Copy Markdown

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: b7765336a1

ℹ️ 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 ruby/lib/basecamp/oauth/fetcher.rb
req.options.timeout bounds only each individual socket read, and Faraday resets
it on every on_data chunk — so a slow-drip peer trickling one byte before each
read timeout can hold a discovery fetch open arbitrarily long while staying
under max_body_bytes. The SSRF-hardened path had no deadline over the whole
streaming read, unlike the other four SDKs.

Add a monotonic (CLOCK_MONOTONIC) wall-clock deadline threaded into the shared
bounded_reader: it raises the moment the whole read outlives timeout, mapped to
a retryable network OauthError. This matches Python's monotonic deadline over
httpx.stream, Go's context.WithTimeout, TypeScript's abort timer, and Kotlin's
Ktor requestTimeoutMillis. Adds a slow-drip streaming test asserting the read
aborts on the deadline before the in-cap body completes.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread ruby/test/basecamp/oauth_ssrf_test.rb Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 91 out of 92 changed files in this pull request and generated 2 comments.

Comment thread ruby/lib/basecamp/oauth/discovery_selection_error.rb Outdated
Comment thread python/src/basecamp/_security.py

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

Copy link
Copy Markdown

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: 21cacc85b8

ℹ️ 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 go/pkg/basecamp/oauth/discovery.go
jeremy added 2 commits July 16, 2026 23:07
The hop-1 fix guarded the soft-fallback path; the committed-issuer AS metadata
fetch had the same gap: a caller cancelling (or its deadline expiring) during
hop 2 was reclassified as ErrASFetchFailed/api_error rather than surfacing as
cancellation. The cause was still reachable via the SelectionError unwrap chain,
but the top-level classification misrepresented a caller abort as a server-side
metadata failure.

Check the parent ctx.Err() before wrapping as ErrASFetchFailed, mirroring the
hop-1 guard. Adds a hop-2 cancellation test asserting context.Canceled
surfaces and the error is not misclassified as ErrASFetchFailed.
… guard

The DiscoverySelectionError comment claimed capability AND expected-issuer are
consumer/usage-shaped, but only capability_unavailable maps to validation;
expected_issuer_unavailable is api_error (an unadvertised issuer is a metadata
fault, matching the other four SDKs). Correct the comment to match the code.

Also guard SlowDripAdapter#call against a nil on_data, matching the sibling
StreamingAdapter, so the test adapter degrades to a buffered response instead of
a NoMethodError if reused without a streaming callback.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 91 out of 92 changed files in this pull request and generated 1 comment.

Comment thread python/src/basecamp/oauth/errors.py Outdated

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

Copy link
Copy Markdown

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: d3dfa5c095

ℹ️ 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 python/src/basecamp/oauth/discovery.py
jeremy added 2 commits July 16, 2026 23:24
…g in Python

A caller passing timeout=float("inf") (or nan/0/negative/non-numeric) disabled
BOTH httpx's bound and the wall-clock deadline (time.monotonic() > inf never
trips), so a slow-drip endpoint could hold the SSRF-hardened fetch open
indefinitely despite the bounded-timeout contract. Add _normalize_timeout,
mirroring the existing _normalize_body_cap, and apply it at the fetch chokepoint
before both httpx and the deadline.

Also corrects the DiscoverySelectionError docstring: it claimed
expected_issuer_unavailable is consumer/usage-shaped, but only
capability_unavailable maps to validation — every other reason is api_error,
matching the code and the other four SDKs.
The Ruby initializers normalized max_body_bytes but stored @timeout raw, so a
Float::INFINITY (or nil/non-numeric/non-positive) timeout made the new
monotonic wall-clock deadline infinite (now + inf never trips) — the same
slow-drip hang just fixed in Python. Add a shared Fetcher.normalize_timeout and
apply it in both Discovery and Resource before building the client and computing
the deadline. Adds a test covering both initializers.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="ruby/lib/basecamp/oauth/discovery.rb">

<violation number="1" location="ruby/lib/basecamp/oauth/discovery.rb:29">
P3: Complex timeout values now fail construction with `NoMethodError` instead of being treated as invalid and normalized to the default. Restrict the accepted timeout type to real positive numerics (or make `normalize_timeout` safely reject values without `positive?`).</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread python/src/basecamp/oauth/discovery.py Outdated
Comment thread ruby/lib/basecamp/oauth/discovery.rb

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 91 out of 92 changed files in this pull request and generated no new comments.

jeremy added 2 commits July 16, 2026 23:39
_normalize_timeout passed an int straight to math.isfinite/float, both of which
raise OverflowError on an int too large to convert to float (e.g. 10**400) —
leaking an unexpected exception instead of falling back to the default. Convert
under try/except OverflowError first, then finite/positive-check the float.
normalize_timeout called finite?/positive? on any Numeric, but Complex defines
neither (complex numbers have no ordering), so a Complex timeout raised
NoMethodError during construction. Gate on real? first so Complex normalizes to
the default; Integer, Float, and Rational are all real and answer both.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 91 out of 92 changed files in this pull request and generated 2 comments.

Comment thread typescript/src/oauth/discovery.ts Outdated
Comment thread go/pkg/basecamp/oauth/types.go Outdated

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

Copy link
Copy Markdown

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: 8f63c4905b

ℹ️ 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 go/pkg/basecamp/oauth/discovery.go Outdated
jeremy added 2 commits July 16, 2026 23:59
…rically

RegistrationEndpoint was typed string while the other optional endpoints
(AuthorizationEndpoint, DeviceAuthorizationEndpoint) are *string — it could not
represent absent-vs-present and diverged from the cross-SDK nullable contract.
Type it *string and map it through directly.

Also normalize the origin-root port numerically. net/url's Port() returns the
RAW token, so a leading-zero default like https://host:000443 passed the range
check (atoi -> 443) yet the string default-port drop ('000443' != '443') left a
non-canonical ':000443' origin. Because isLaunchpadIssuer compares normalized
origins, a Launchpad advertisement spelled that way would be misread as a
distinct BC5 issuer (only-Launchpad metadata could hard-fail as committed, or
create false ambiguity beside BC5). Parse the port once, reuse the int for both
the range check and canonical rendering (leading-zero default drops;
leading-zero non-default renders without zeros). The other four SDKs' parsers
return an int port and normalize this for free. Adds origin-root test cases.
The DiscoverySelectionError comment claimed capability AND expected-issuer are
consumer/usage-shaped, but only capability_unavailable maps to validation;
expected_issuer_unavailable is api_error. Correct the comment to match the code
and the other four SDKs (same fix as the Ruby and Python docstrings).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 91 out of 92 changed files in this pull request and generated 1 comment.

Comment thread typescript/src/oauth/discovery.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaking change to public API conformance Conformance test suite enhancement New feature or request go kotlin ruby Pull requests that update the Ruby SDK typescript Pull requests that update TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants