From ed2bebec3d9df11e29197754889b276130b75c0c Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 23 Apr 2026 14:59:36 -0700 Subject: [PATCH 1/7] Add basecampAuthRoutableUrl trait and guard its consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marks Upload.download_url and CampfireLineAttachment.download_url as API-host URLs that require the two-hop authenticated-then-signed fetch rather than pre-signed external URLs. The trait's /// block carries the full consumer contract (hop mechanics, test-fidelity requirements, retry semantics, pagination exemption, public-vs-internal primitive division) so hand-rolled helpers can implement the flow without reading SDK source. Guards regression with scripts/check-auth-routable-consumers.sh — a call-site deny-list that fails if fetchSignedDownload is invoked outside go/pkg/basecamp/download.go. After PR #278, the only legitimate caller is fetchAPIDownload, which runs the authenticated first hop before the signed fetch; any other caller is either re-inventing the two-hop flow or skipping hop 1. Wired into make check via auth-routable-check. Fixes the TypeScript uploads test fixture that still used example.com, so the spec-shape-only test no longer teaches the wrong URL shape. openapi.json diff is exactly two new x-basecamp-auth-routable-url: {} entries on the Upload and CampfireLineAttachment download_url property schemas; no generator in any SDK consumes field-level x-basecamp-* extensions, so no downstream code churn. --- Makefile | 10 +++- openapi.json | 6 ++- scripts/check-auth-routable-consumers.sh | 27 ++++++++++ spec/basecamp-traits.smithy | 54 +++++++++++++++++++ spec/basecamp.smithy | 3 ++ .../src/generated/openapi-stripped.json | 6 ++- typescript/tests/services/uploads.test.ts | 2 +- 7 files changed, 102 insertions(+), 6 deletions(-) create mode 100755 scripts/check-auth-routable-consumers.sh diff --git a/Makefile b/Makefile index c9ddeb10..99081117 100644 --- a/Makefile +++ b/Makefile @@ -227,6 +227,14 @@ go-check-drift: @echo "==> Checking service layer drift..." @./scripts/check-service-drift.sh +.PHONY: auth-routable-check + +# Check that hop-2-only primitives are not called outside the authenticated +# download path. Guards against regressing the @basecampAuthRoutableUrl contract. +auth-routable-check: + @echo "==> Checking auth-routable consumer invariants..." + @./scripts/check-auth-routable-consumers.sh + #------------------------------------------------------------------------------ # TypeScript SDK targets #------------------------------------------------------------------------------ @@ -575,7 +583,7 @@ tools: #------------------------------------------------------------------------------ # Run all checks (Smithy + Go + TypeScript + Ruby + Kotlin + Swift + Python + Behavior Model + Conformance + Provenance + Actions lint) -check: lint-actions sync-spec-version-check smithy-check behavior-model-check provenance-check sync-api-version-check go-check-drift kt-check-drift go-check ts-check rb-check kt-check swift-check py-check conformance +check: lint-actions sync-spec-version-check smithy-check behavior-model-check provenance-check sync-api-version-check go-check-drift auth-routable-check kt-check-drift go-check ts-check rb-check kt-check swift-check py-check conformance @echo "==> All checks passed" # Clean all build artifacts diff --git a/openapi.json b/openapi.json index 1444563b..7e0383d9 100644 --- a/openapi.json +++ b/openapi.json @@ -21089,7 +21089,8 @@ "format": "int64" }, "download_url": { - "type": "string" + "type": "string", + "x-basecamp-auth-routable-url": {} } } }, @@ -27568,7 +27569,8 @@ "x-go-type-skip-optional-pointer": true }, "download_url": { - "type": "string" + "type": "string", + "x-basecamp-auth-routable-url": {} }, "filename": { "type": "string" diff --git a/scripts/check-auth-routable-consumers.sh b/scripts/check-auth-routable-consumers.sh new file mode 100755 index 00000000..7e5d9993 --- /dev/null +++ b/scripts/check-auth-routable-consumers.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Guard: fetchSignedDownload is the hop-2-only primitive for fields tagged +# @basecampAuthRoutableUrl. It must be called only from download.go (via +# fetchAPIDownload, which performs the authenticated hop 1 first). Any +# other caller is either re-inventing the two-hop flow or skipping hop 1. + +VIOLATIONS=$(grep -rnE --include='*.go' --exclude='*_test.go' \ + '\bfetchSignedDownload[[:space:]]*\(' go/pkg/basecamp/ \ + | grep -v '/download\.go:' || true) + +if [ -n "${VIOLATIONS}" ]; then + echo "ERROR: fetchSignedDownload call-site outside go/pkg/basecamp/download.go" + echo "" + echo "${VIOLATIONS}" + echo "" + echo "Consumers of @basecampAuthRoutableUrl fields (e.g., Upload.download_url," + echo "CampfireLineAttachment.download_url) MUST route through the two-hop helper" + echo "Client.fetchAPIDownload (or the public AccountClient.DownloadURL), which" + echo "performs the authenticated first hop before the signed fetch." + echo "" + echo "See spec/basecamp-traits.smithy: basecampAuthRoutableUrl contract." + exit 1 +fi + +echo "auth-routable consumer check: passed" diff --git a/spec/basecamp-traits.smithy b/spec/basecamp-traits.smithy index 8e3ea8fd..43957936 100644 --- a/spec/basecamp-traits.smithy +++ b/spec/basecamp-traits.smithy @@ -94,6 +94,60 @@ structure basecampSensitive { redact: Boolean } +/// Marks a URL member whose value must be fetched through the configured +/// API host with the SDK's Bearer credential; the API host 302s to a +/// short-lived signed URL that the consumer follows without auth. +/// +/// Contract for consumers (SDKs and hand-rolled helpers): +/// 1. Rewrite the URL's scheme + host to the configured API base URL, +/// preserving path, query, and fragment. +/// 2. Issue an authenticated GET (Bearer credential + SDK User-Agent) +/// with auto-redirect disabled so the 3xx is captured. +/// 3. On 301/302/303/307/308, read Location, close the first body, +/// and GET the resolved URL with a bare transport — no auth +/// headers, no logging middleware. The signature is the credential. +/// 4. On direct 2xx, stream the first response body as-is. +/// 5. Tests that exercise the download flow MUST stub with a URL +/// whose host matches the configured API base and whose path +/// matches the fixture shape (e.g. +/// `/{accountId}/buckets/{bucketId}/uploads/{id}/download/{filename}`), +/// AND MUST assert the unauthenticated hop actually fired. +/// "No assertion fired" and "assertion fired and passed" are +/// indistinguishable otherwise — both masked the bug behind +/// PR #278. Schema-shape-only tests (unmarshaling assertions that +/// set `download_url` without exercising transport) are exempt but +/// should still use an API-host-shaped URL for clarity. +/// +/// The two-hop flow is not automatically retried end-to-end: streaming +/// body ownership passes to the caller after hop 2, so a retry would +/// double-consume. The authenticated first hop may be retried internally +/// per the SDK's standard retry policy (see PR #278 in the Go SDK). +/// +/// This trait does NOT apply to pagination `Link: rel=next` URLs, +/// which are fetched as same-origin authed GETs without a redirect +/// step (see Go `Client.followPagination` at `client.go:499`). +/// +/// Every SDK implements hops 1–4 in a language-native primitive; +/// external (cross-package/application-level) consumers MUST call the +/// public client method, not the raw HTTP client. References: Go +/// `AccountClient.DownloadURL`, TypeScript `BasecampClient.downloadURL` +/// (backed by `createDownloadURL`), Python `AccountClient.download_url` +/// / `AsyncAccountClient.download_url` (backed by `download_sync` / +/// `download_async`), Ruby `AccountClient#download_url`, Swift +/// `AccountClient.downloadURL`, Kotlin `AccountClient.downloadURL`. +/// +/// SDK-internal service code (e.g., Go service methods that already +/// own an `OperationInfo` lifecycle, like `UploadsService.Download`) +/// MUST call the internal two-hop helper directly (Go: +/// `Client.fetchAPIDownload`) rather than the public primitive — +/// otherwise the public primitive's own `OnOperationStart`/`OnOperationEnd` +/// fires nested inside the caller's, creating a double-logged request. +/// +/// Emits x-basecamp-auth-routable-url extension to OpenAPI for SDK code generators. +@trait(selector: "structure > member") +@specificationExtension(as: "x-basecamp-auth-routable-url") +structure basecampAuthRoutableUrl {} + // ============================================================================ // Legacy Traits - Keep for backward compatibility (not emitted to OpenAPI) // ============================================================================ diff --git a/spec/basecamp.smithy b/spec/basecamp.smithy index dd892f11..67f0b341 100644 --- a/spec/basecamp.smithy +++ b/spec/basecamp.smithy @@ -45,6 +45,7 @@ use basecamp.traits#basecampPagination use basecamp.traits#basecampIdempotent use basecamp.traits#basecampMultipart use basecamp.traits#basecampSensitive +use basecamp.traits#basecampAuthRoutableUrl /// Basecamp API @restJson1 @@ -3038,6 +3039,7 @@ structure Upload { byte_size: Long width: Integer height: Integer + @basecampAuthRoutableUrl download_url: String filename: String boosts_count: Integer @@ -3840,6 +3842,7 @@ structure CampfireLineAttachment { filename: String content_type: String byte_size: Long + @basecampAuthRoutableUrl download_url: String } diff --git a/typescript/src/generated/openapi-stripped.json b/typescript/src/generated/openapi-stripped.json index 7cfc526c..409ac073 100644 --- a/typescript/src/generated/openapi-stripped.json +++ b/typescript/src/generated/openapi-stripped.json @@ -18765,7 +18765,8 @@ "format": "int64" }, "download_url": { - "type": "string" + "type": "string", + "x-basecamp-auth-routable-url": {} } } }, @@ -25244,7 +25245,8 @@ "x-go-type-skip-optional-pointer": true }, "download_url": { - "type": "string" + "type": "string", + "x-basecamp-auth-routable-url": {} }, "filename": { "type": "string" diff --git a/typescript/tests/services/uploads.test.ts b/typescript/tests/services/uploads.test.ts index 735f4636..a908b045 100644 --- a/typescript/tests/services/uploads.test.ts +++ b/typescript/tests/services/uploads.test.ts @@ -37,7 +37,7 @@ describe("UploadsService", () => { filename: "report.pdf", content_type: "application/pdf", byte_size: 1024000, - download_url: "https://example.com/download/report.pdf", + download_url: "https://3.basecampapi.com/12345/blobs/abcd/download/report.pdf", status: "active", }; From dd79220a319ae3260274c1031e87e16208576796 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 23 Apr 2026 16:01:01 -0700 Subject: [PATCH 2/7] Tighten auth-routable guard; drop drifting line ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review on PR #282: scripts/check-auth-routable-consumers.sh now enforces two rules instead of just a file-level exemption: 1. No fetchSignedDownload references in non-test Go code outside download.go (unchanged). 2. Exactly one method-call site (`.fetchSignedDownload(`) inside download.go. The function declaration lacks the leading `.` so it's excluded; a second call site silently bypassing rule 1 now fails the guard and surfaces in review (cubic-dev-ai). spec/basecamp-traits.smithy drops the hardcoded `client.go:499` line reference for `Client.followPagination` — line numbers drift, the symbol/file is enough (copilot-pull-request-reviewer). --- scripts/check-auth-routable-consumers.sh | 36 +++++++++++++++++++----- spec/basecamp-traits.smithy | 2 +- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/scripts/check-auth-routable-consumers.sh b/scripts/check-auth-routable-consumers.sh index 7e5d9993..fb006b72 100755 --- a/scripts/check-auth-routable-consumers.sh +++ b/scripts/check-auth-routable-consumers.sh @@ -2,18 +2,22 @@ set -euo pipefail # Guard: fetchSignedDownload is the hop-2-only primitive for fields tagged -# @basecampAuthRoutableUrl. It must be called only from download.go (via -# fetchAPIDownload, which performs the authenticated hop 1 first). Any -# other caller is either re-inventing the two-hop flow or skipping hop 1. +# @basecampAuthRoutableUrl. It must be called only from download.go, and only +# from the single call site inside fetchAPIDownload that performs the +# authenticated hop 1 first. Any other caller is either re-inventing the +# two-hop flow or skipping hop 1. -VIOLATIONS=$(grep -rnE --include='*.go' --exclude='*_test.go' \ +# Rule 1: any reference to fetchSignedDownload( in non-test Go code outside +# download.go is a violation. The broad `\b...\(` pattern intentionally also +# matches doc-comment examples — stale examples warrant review. +EXTERNAL=$(grep -rnE --include='*.go' --exclude='*_test.go' \ '\bfetchSignedDownload[[:space:]]*\(' go/pkg/basecamp/ \ | grep -v '/download\.go:' || true) -if [ -n "${VIOLATIONS}" ]; then - echo "ERROR: fetchSignedDownload call-site outside go/pkg/basecamp/download.go" +if [ -n "${EXTERNAL}" ]; then + echo "ERROR: fetchSignedDownload reference outside go/pkg/basecamp/download.go" echo "" - echo "${VIOLATIONS}" + echo "${EXTERNAL}" echo "" echo "Consumers of @basecampAuthRoutableUrl fields (e.g., Upload.download_url," echo "CampfireLineAttachment.download_url) MUST route through the two-hop helper" @@ -24,4 +28,22 @@ if [ -n "${VIOLATIONS}" ]; then exit 1 fi +# Rule 2: download.go must contain exactly one method-call site of +# fetchSignedDownload — the one inside fetchAPIDownload. The function +# declaration `func (c *Client) fetchSignedDownload(` lacks the leading `.` +# and is excluded by this pattern. A second call site in download.go would +# silently bypass Rule 1's file-level exemption, so this rule surfaces it. +CALL_SITES=$(grep -cE '\.fetchSignedDownload[[:space:]]*\(' go/pkg/basecamp/download.go || true) + +if [ "${CALL_SITES}" != "1" ]; then + echo "ERROR: download.go has ${CALL_SITES} call site(s) of fetchSignedDownload, expected exactly 1" + echo "" + grep -nE '\.fetchSignedDownload[[:space:]]*\(' go/pkg/basecamp/download.go || true + echo "" + echo "The only legitimate call is inside Client.fetchAPIDownload, after the" + echo "authenticated first hop. If you are adding a genuinely new caller," + echo "update this guard to reflect the new invariant." + exit 1 +fi + echo "auth-routable consumer check: passed" diff --git a/spec/basecamp-traits.smithy b/spec/basecamp-traits.smithy index 43957936..c67d4a29 100644 --- a/spec/basecamp-traits.smithy +++ b/spec/basecamp-traits.smithy @@ -125,7 +125,7 @@ structure basecampSensitive { /// /// This trait does NOT apply to pagination `Link: rel=next` URLs, /// which are fetched as same-origin authed GETs without a redirect -/// step (see Go `Client.followPagination` at `client.go:499`). +/// step (see Go `Client.followPagination` in `client.go`). /// /// Every SDK implements hops 1–4 in a language-native primitive; /// external (cross-package/application-level) consumers MUST call the From 0fac3ec6ef4e07cb148531210bac588338d014f1 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 23 Apr 2026 16:02:26 -0700 Subject: [PATCH 3/7] Anchor download.go path match in auth-routable guard Address PR #282 review: the previous filter `grep -v '/download.go:'` matched any filename suffix, so a forbidden `fetchSignedDownload` call in, e.g., `go/pkg/basecamp/oauth/download.go` would have been silently allowlisted. Anchor to the exact path `^go/pkg/basecamp/download.go:` (chatgpt-codex-connector). --- scripts/check-auth-routable-consumers.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-auth-routable-consumers.sh b/scripts/check-auth-routable-consumers.sh index fb006b72..86972a59 100755 --- a/scripts/check-auth-routable-consumers.sh +++ b/scripts/check-auth-routable-consumers.sh @@ -12,7 +12,7 @@ set -euo pipefail # matches doc-comment examples — stale examples warrant review. EXTERNAL=$(grep -rnE --include='*.go' --exclude='*_test.go' \ '\bfetchSignedDownload[[:space:]]*\(' go/pkg/basecamp/ \ - | grep -v '/download\.go:' || true) + | grep -v '^go/pkg/basecamp/download\.go:' || true) if [ -n "${EXTERNAL}" ]; then echo "ERROR: fetchSignedDownload reference outside go/pkg/basecamp/download.go" From 253b220ec4459bd5a96ae7bc977cb56ca96ccaae Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 23 Apr 2026 16:09:13 -0700 Subject: [PATCH 4/7] Wire auth-routable-check into CI; POSIX-portable lint patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #282 review follow-ups: Run the guard in CI (chatgpt-codex-connector). A lint wired only into make check does not protect pull requests — the existing GitHub Actions test-go job invokes individual checks directly, not the omnibus target. Add an auth-routable consumer check step alongside the existing check-service-drift.sh step in test.yml. The test-go job already runs with contents: read permissions and other shell-based checks; this follows the same pattern and introduces no new privileges or secrets. Portabilize the grep patterns (copilot-pull-request-reviewer). Rule 1 previously used `grep -r --include=... --exclude=...` and `\b` — both GNU/BSD grep extensions outside POSIX. Switch to `git grep` with a pathspec (`:!*_test.go` excludes test files; only tracked files are scanned, which also naturally ignores build artifacts) and a POSIX word-boundary pattern `(^|[^[:alnum:]_])`. Rule 2 already used `\.` so no change was needed there. Both failure paths re-verified after the switch: external reference (vaults.go), subpath escape (oauth/download.go), and second call site in download.go. --- .github/workflows/test.yml | 3 +++ scripts/check-auth-routable-consumers.sh | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99b77987..8b86a80b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,6 +70,9 @@ jobs: - name: Check service layer drift run: ../scripts/check-service-drift.sh + - name: Check auth-routable consumer invariants + run: ../scripts/check-auth-routable-consumers.sh + - name: Test with coverage run: | # Measure coverage for hand-written code only (generated code is excluded) diff --git a/scripts/check-auth-routable-consumers.sh b/scripts/check-auth-routable-consumers.sh index 86972a59..4b99d597 100755 --- a/scripts/check-auth-routable-consumers.sh +++ b/scripts/check-auth-routable-consumers.sh @@ -8,10 +8,13 @@ set -euo pipefail # two-hop flow or skipping hop 1. # Rule 1: any reference to fetchSignedDownload( in non-test Go code outside -# download.go is a violation. The broad `\b...\(` pattern intentionally also -# matches doc-comment examples — stale examples warrant review. -EXTERNAL=$(grep -rnE --include='*.go' --exclude='*_test.go' \ - '\bfetchSignedDownload[[:space:]]*\(' go/pkg/basecamp/ \ +# download.go is a violation. Uses `git grep` (only tracked files, clean +# pathspec for excluding tests) and a POSIX word-boundary — `(^|[^[:alnum:]_])` +# rather than `\b` — so the pattern works on any POSIX ERE, not just the +# GNU/BSD extension. The broad match intentionally also catches doc-comment +# examples; stale examples warrant review. +EXTERNAL=$(git grep -nE '(^|[^[:alnum:]_])fetchSignedDownload[[:space:]]*\(' \ + -- 'go/pkg/basecamp/' ':!*_test.go' \ | grep -v '^go/pkg/basecamp/download\.go:' || true) if [ -n "${EXTERNAL}" ]; then From 6da8ef5ab18a9c4dd75a243deb51e2231377c74f Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 23 Apr 2026 16:11:52 -0700 Subject: [PATCH 5/7] Close method-value bypass; list both fixture shapes PR #282 follow-up review: Widen Rule 1's pattern to catch method-value uses of fetchSignedDownload (chatgpt-codex-connector). The previous pattern required the identifier to be followed by `(`, so `fn := c.fetchSignedDownload` outside download.go would capture the method value and evade the guard; calling `fn(ctx, url)` later would bypass the two-hop invariant without tripping CI. New pattern matches the identifier followed by any non-word char or end-of-line, which covers direct calls, method-value captures, and bare-identifier mentions (the last being the plan's intentional soft signal for stale doc examples). Verified with a simulated capture. List both fixture path shapes in the trait docs (copilot-pull-request-reviewer). The trait is applied to both Upload.download_url and CampfireLineAttachment.download_url, whose real fixtures use different path shapes: Upload uses `/{accountId}/blobs/{blob}/download/{filename}` (spec/fixtures/uploads/ get.json) while CampfireLineAttachment uses `/{accountId}/buckets/{bucketId}/uploads/{id}/download/{filename}` (spec/fixtures/campfires/upload_line_get.json). The docs previously showed only the second shape, misleading Upload test authors. --- scripts/check-auth-routable-consumers.sh | 11 +++++++---- spec/basecamp-traits.smithy | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/scripts/check-auth-routable-consumers.sh b/scripts/check-auth-routable-consumers.sh index 4b99d597..51000dc5 100755 --- a/scripts/check-auth-routable-consumers.sh +++ b/scripts/check-auth-routable-consumers.sh @@ -7,13 +7,16 @@ set -euo pipefail # authenticated hop 1 first. Any other caller is either re-inventing the # two-hop flow or skipping hop 1. -# Rule 1: any reference to fetchSignedDownload( in non-test Go code outside +# Rule 1: any reference to fetchSignedDownload in non-test Go code outside # download.go is a violation. Uses `git grep` (only tracked files, clean # pathspec for excluding tests) and a POSIX word-boundary — `(^|[^[:alnum:]_])` # rather than `\b` — so the pattern works on any POSIX ERE, not just the -# GNU/BSD extension. The broad match intentionally also catches doc-comment -# examples; stale examples warrant review. -EXTERNAL=$(git grep -nE '(^|[^[:alnum:]_])fetchSignedDownload[[:space:]]*\(' \ +# GNU/BSD extension. The identifier is matched whether it is followed by `(` +# (a direct call) or any other non-word boundary — catches method-value +# captures like `fn := c.fetchSignedDownload` that would bypass a +# `\(`-anchored pattern, plus doc-comment mentions (stale examples warrant +# review). +EXTERNAL=$(git grep -nE '(^|[^[:alnum:]_])fetchSignedDownload([^[:alnum:]_]|$)' \ -- 'go/pkg/basecamp/' ':!*_test.go' \ | grep -v '^go/pkg/basecamp/download\.go:' || true) diff --git a/spec/basecamp-traits.smithy b/spec/basecamp-traits.smithy index c67d4a29..6b1cca05 100644 --- a/spec/basecamp-traits.smithy +++ b/spec/basecamp-traits.smithy @@ -109,9 +109,11 @@ structure basecampSensitive { /// 4. On direct 2xx, stream the first response body as-is. /// 5. Tests that exercise the download flow MUST stub with a URL /// whose host matches the configured API base and whose path -/// matches the fixture shape (e.g. -/// `/{accountId}/buckets/{bucketId}/uploads/{id}/download/{filename}`), -/// AND MUST assert the unauthenticated hop actually fired. +/// matches the fixture shape for the resource: +/// `/{accountId}/blobs/{blob}/download/{filename}` for Upload, +/// `/{accountId}/buckets/{bucketId}/uploads/{id}/download/{filename}` +/// for CampfireLineAttachment. The test MUST also assert the +/// unauthenticated hop actually fired. /// "No assertion fired" and "assertion fired and passed" are /// indistinguishable otherwise — both masked the bug behind /// PR #278. Schema-shape-only tests (unmarshaling assertions that From f79674757c53bd2040946347f056e5fdcf84871a Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 23 Apr 2026 17:04:04 -0700 Subject: [PATCH 6/7] Relax test-fidelity contract to match merged primitive tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 5 previously required test stub URLs to use the configured API base host. After rebasing onto main, that conflicts with two already- merged conventions: - Go's `download_test.go` (PR #278) uses `storage.3.basecamp.com` for ~16 primitive-level tests so the SDK's host-rewrite step is visible in test setup. - The TS `UploadsService.download` test added by PR #281 follows the same pattern at the service level. Both correctly assert the auth-hop sequence (Bearer on hop 1, none on hop 2), which is the actual load-bearing guard against the PR #278 bug shape — that bug passed because the test never asserted the auth hop fired, not because of the host choice. Relax step 5 to make the auth-hop assertion the explicit MUST, document both URL host conventions as acceptable (with API-host preferred for fixture-shape mirroring), and keep the schema-shape-only exemption. --- spec/basecamp-traits.smithy | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/spec/basecamp-traits.smithy b/spec/basecamp-traits.smithy index 6b1cca05..2fecf8dd 100644 --- a/spec/basecamp-traits.smithy +++ b/spec/basecamp-traits.smithy @@ -107,18 +107,24 @@ structure basecampSensitive { /// and GET the resolved URL with a bare transport — no auth /// headers, no logging middleware. The signature is the credential. /// 4. On direct 2xx, stream the first response body as-is. -/// 5. Tests that exercise the download flow MUST stub with a URL -/// whose host matches the configured API base and whose path -/// matches the fixture shape for the resource: -/// `/{accountId}/blobs/{blob}/download/{filename}` for Upload, -/// `/{accountId}/buckets/{bucketId}/uploads/{id}/download/{filename}` -/// for CampfireLineAttachment. The test MUST also assert the -/// unauthenticated hop actually fired. +/// 5. Tests that exercise the download flow MUST assert that BOTH +/// hops fired with the correct auth: capture each request's +/// Authorization header and check Bearer on the metadata fetch +/// and authenticated download hop, none on the signed hop. /// "No assertion fired" and "assertion fired and passed" are -/// indistinguishable otherwise — both masked the bug behind -/// PR #278. Schema-shape-only tests (unmarshaling assertions that -/// set `download_url` without exercising transport) are exempt but -/// should still use an API-host-shaped URL for clarity. +/// indistinguishable otherwise — both masked the bug behind PR +/// #278. The host of the URL stubbed in metadata may either +/// match the configured API base (mirrors what real fixtures +/// return — e.g. `/{accountId}/blobs/{blob}/download/{filename}` +/// for Upload, `/{accountId}/buckets/{bucketId}/uploads/{id}/download/{filename}` +/// for CampfireLineAttachment) or use a different host like +/// `storage.3.basecamp.com/...` to make the SDK's host-rewrite +/// step visible (the convention in primitive tests like Go's +/// `download_test.go`). Either is acceptable; the auth-hop +/// assertions are what enforce the contract. Schema-shape-only +/// tests (unmarshaling assertions that set `download_url` +/// without exercising transport) are exempt but should still +/// use an API-host-shaped URL for clarity. /// /// The two-hop flow is not automatically retried end-to-end: streaming /// body ownership passes to the caller after hop 2, so a retry would From 98e185cac9351d54ccf5c61f4aa1acb346ae1da4 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Thu, 23 Apr 2026 17:10:35 -0700 Subject: [PATCH 7/7] Make auth-routable guard cwd-independent and Go-scoped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #282 review (Codex P1, cubic-dev-ai P1+P2, Copilot ×2): The CI test-go job runs every shell step from working-directory: go and invokes the script as `../scripts/check-auth-routable-consumers.sh`. With cwd=go/, the previous script's `git grep` pathspec `go/pkg/basecamp/` resolves to `go/go/pkg/basecamp/` (silent no-op, guard passes vacuously) and the literal path `go/pkg/basecamp/download.go` hits "No such file or directory" (Rule 2 fails for the wrong reason, breaking CI before tests run). Reproduced locally by cd'ing into go/ and running the script directly. Resolve paths from the script location instead, matching the same SCRIPT_DIR/REPO_DIR pattern check-service-drift.sh already uses: - `git -C "$REPO_DIR" grep ...` for Rule 1 (pathspec resolves at REPO_DIR, not caller cwd). - Absolute "$DOWNLOAD_GO" for Rule 2's grep on a single file. Also scope Rule 1's pathspec back to `*.go` files via `:(glob)go/pkg/basecamp/**/*.go`, matching the previous --include='*.go' behavior. The earlier git-grep refactor dropped the Go-only restriction, which let non-Go files (JSON, docs) be subject to the soft-signal heuristic (cubic-dev-ai P2). Verified: passes from repo root, from go/ (CI cwd simulation), and from /tmp; both failure paths still pinpoint correctly when invoked from go/; a JSON file mentioning fetchSignedDownload no longer trips Rule 1. --- scripts/check-auth-routable-consumers.sh | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/check-auth-routable-consumers.sh b/scripts/check-auth-routable-consumers.sh index 51000dc5..51efdb7b 100755 --- a/scripts/check-auth-routable-consumers.sh +++ b/scripts/check-auth-routable-consumers.sh @@ -7,6 +7,14 @@ set -euo pipefail # authenticated hop 1 first. Any other caller is either re-inventing the # two-hop flow or skipping hop 1. +# Resolve paths relative to the script location, not the caller's cwd. The +# CI test-go job invokes this from `working-directory: go` as +# `../scripts/check-auth-routable-consumers.sh`; without this, `git grep` +# pathspecs would resolve under `go/go/...` (silent no-op) and the literal +# path to download.go would not exist. Same pattern as check-service-drift.sh. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" + # Rule 1: any reference to fetchSignedDownload in non-test Go code outside # download.go is a violation. Uses `git grep` (only tracked files, clean # pathspec for excluding tests) and a POSIX word-boundary — `(^|[^[:alnum:]_])` @@ -15,9 +23,11 @@ set -euo pipefail # (a direct call) or any other non-word boundary — catches method-value # captures like `fn := c.fetchSignedDownload` that would bypass a # `\(`-anchored pattern, plus doc-comment mentions (stale examples warrant -# review). -EXTERNAL=$(git grep -nE '(^|[^[:alnum:]_])fetchSignedDownload([^[:alnum:]_]|$)' \ - -- 'go/pkg/basecamp/' ':!*_test.go' \ +# review). The pathspec scopes the scan to *.go files via globstar so non-Go +# files (url-routes.json, api-provenance.json, etc.) aren't subject to the +# soft-signal heuristic. +EXTERNAL=$(git -C "$REPO_DIR" grep -nE '(^|[^[:alnum:]_])fetchSignedDownload([^[:alnum:]_]|$)' \ + -- ':(glob)go/pkg/basecamp/**/*.go' ':!*_test.go' \ | grep -v '^go/pkg/basecamp/download\.go:' || true) if [ -n "${EXTERNAL}" ]; then @@ -39,12 +49,13 @@ fi # declaration `func (c *Client) fetchSignedDownload(` lacks the leading `.` # and is excluded by this pattern. A second call site in download.go would # silently bypass Rule 1's file-level exemption, so this rule surfaces it. -CALL_SITES=$(grep -cE '\.fetchSignedDownload[[:space:]]*\(' go/pkg/basecamp/download.go || true) +DOWNLOAD_GO="$REPO_DIR/go/pkg/basecamp/download.go" +CALL_SITES=$(grep -cE '\.fetchSignedDownload[[:space:]]*\(' "$DOWNLOAD_GO" || true) if [ "${CALL_SITES}" != "1" ]; then echo "ERROR: download.go has ${CALL_SITES} call site(s) of fetchSignedDownload, expected exactly 1" echo "" - grep -nE '\.fetchSignedDownload[[:space:]]*\(' go/pkg/basecamp/download.go || true + grep -nE '\.fetchSignedDownload[[:space:]]*\(' "$DOWNLOAD_GO" || true echo "" echo "The only legitimate call is inside Client.fetchAPIDownload, after the" echo "authenticated first hop. If you are adding a genuinely new caller,"