feat(courtlistener): add /api/proxy/courtlistener proxy for ChittyCounsel - #234
feat(courtlistener): add /api/proxy/courtlistener proxy for ChittyCounsel#234chitcommit wants to merge 3 commits into
Conversation
…nsel Adds a CourtListener REST v4 proxy mirroring the established google.js route pattern, with CourtListener-specific behaviors per spec: - Token auth uses `Authorization: Token <key>` (CL convention, not Bearer) - Token resolved from `env.COURTLISTENER_API_TOKEN` (Cloudflare Secrets Store binding declared in wrangler.jsonc for top-level + staging + production envs against store e914522471964c3c8cf1e601770edcc3), falling back to the credential broker - Fail-closed 503 `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE` when token is not provisioned (no operator credential paste path) - Resource allowlist enforced before any upstream call: search, opinions, clusters, dockets, docket-entries, recap-documents, audio, citation-lookup, people, courts - Routes: GET /search, GET /:resource[/:id], POST /citation-lookup - 15-min Cache API wrap on GETs; bypass via `X-Bypass-Cache: 1` - Upstream error bodies truncated to 200 chars; token never echoed Consumer: ChittyCounsel (chittycorp/CHITTYCOUNSEL PR #4). Synthetic env credential plane status: binding declared in all three env blocks (top-level + staging + production). The secret VALUE `COURTLISTENER_API_TOKEN` is NOT yet present in Cloudflare Secrets Store e914522471964c3c8cf1e601770edcc3 — verified via `wrangler secrets-store secret list`. Until chico/credential provisioner writes the value, the 503 fail-closed path is exercised. Deploy is safe and non-disruptive (no behavior change to existing routes); operator approval still required before `npm run deploy:production`. Test coverage: 21 unit tests covering token resolution (3 sources + fail-closed), search, allowlist enforcement (incl. 404 on unknown resource without upstream call), citation-lookup POST forwarding, Cache API HIT/MISS + bypass, upstream error truncation, and Token (not Bearer) auth scheme. Mirrors established google-routes.test.js pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
chittyconnect | 6389d45 | Jun 16 2026, 05:14 PM |
|
Warning Review limit reached
More reviews will be available in 22 minutes and 51 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughAdds a CourtListener REST API proxy with token-based authentication, resource allowlisting, and optional response caching. Includes router integration, configuration management, OpenAPI documentation, and comprehensive test coverage for token resolution, endpoint forwarding, cache behavior, and security controls. ChangesCourtListener Proxy Implementation
Sequence DiagramsequenceDiagram
participant Client
participant ProxyRouter
participant TokenResolver
participant UpstreamAPI
participant Cache as Cache API
Client->>ProxyRouter: GET /search?q=term
ProxyRouter->>TokenResolver: getCourtListenerToken()
TokenResolver-->>ProxyRouter: token
ProxyRouter->>Cache: Check cache
Cache-->>ProxyRouter: miss
ProxyRouter->>UpstreamAPI: GET /search/?q=term + Authorization: Token
UpstreamAPI-->>ProxyRouter: 200 JSON
ProxyRouter->>Cache: Store with TTL
ProxyRouter-->>Client: 200 JSON + X-Cache: MISS
Client->>ProxyRouter: POST /citation-lookup
ProxyRouter->>TokenResolver: getCourtListenerToken()
TokenResolver-->>ProxyRouter: token
ProxyRouter->>UpstreamAPI: POST /citation-lookup/ + body + Authorization: Token
UpstreamAPI-->>ProxyRouter: 200 citations
ProxyRouter-->>Client: 200 citations
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Status — credential lane wired, awaiting non-paste secret populationChittyConnect concierge re-dispatch checkpoint: Exhaustive credential search results (no CourtListener token exists on this infrastructure):
What's ready to merge (this PR): route + tests + wrangler binding all green on Test/Lint/CodeQL/Security/compliance. Code is fail-closed — when Remaining blockers (both require human signoff, neither requires a chat paste):
Pairs with: chittycorp/CHITTYCOUNSEL PR #4 (counsel-side Reference: |
There was a problem hiding this comment.
Pull request overview
Adds a new CourtListener REST v4 proxy under /api/proxy/courtlistener/* to support ChittyCounsel, following the established proxy patterns (token injection, allowlisting, cache wrapping, OpenAPI docs, and unit tests).
Changes:
- Adds
src/api/routes/courtlistener.jsimplementing GET/search, GET/:resource[/:id](allowlisted), and POST/citation-lookupwith token resolution and Cache API wrapping for GETs. - Mounts the new proxy router at
/api/proxy/courtlistenerand documents the endpoints inpublic/openapi.json. - Adds CourtListener secrets-store bindings to
wrangler.jsoncand introduces a new unit test suite for the route.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/api/routes/courtlistener.js |
New CourtListener proxy implementation (token resolution, allowlist, cache wrap, error handling). |
src/api/router.js |
Mounts the CourtListener proxy router under /api/proxy/courtlistener. |
wrangler.jsonc |
Adds COURTLISTENER_API_TOKEN secrets-store binding at top-level + staging + production. |
public/openapi.json |
Documents CourtListener proxy endpoints and parameters. |
tests/api/courtlistener-routes.test.js |
Adds route-level unit tests mirroring the existing proxy testing approach. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const body = await upstream.text().catch(() => ""); | ||
| return c.json( | ||
| { error: `CourtListener API ${upstream.status}: ${body.slice(0, 200)}` }, | ||
| upstream.status, | ||
| ); |
| const text = await upstream.text().catch(() => ""); | ||
| return c.json( | ||
| { error: `CourtListener API ${upstream.status}: ${text.slice(0, 200)}` }, | ||
| upstream.status, | ||
| ); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b39b08ac1a
ℹ️ 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".
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@public/openapi.json`:
- Around line 1336-1339: OpenAPI currently omits the proxy's 502 error for
CourtListener endpoints; update the CourtListener operation responses in
public/openapi.json to include a 502 response (e.g., "description": "Bad Gateway
- upstream fetch failed or invalid JSON") for the same GET and POST proxy
operations described in src/api/routes/courtlistener.js (the proxy handlers that
perform upstream fetch/JSON parsing), ensuring each operation that lists
200/404/503 also lists 502 so the spec matches runtime behavior.
In `@src/api/routes/courtlistener.js`:
- Around line 77-82: getCredential is being passed an undeclared env fallback
("COURTLISTENER_API_TOKEN_FALLBACK") which reintroduces a third credential path;
remove that fallback argument so the call uses the broker/secrets-only
resolution. Locate the call to getCredential in courtlistener.js and change the
invocation to pass the env, the secret path
("integrations/courtlistener/api_token") and the service name ("CourtListener")
only (i.e., drop the fallbackEnvVar parameter), keeping the getCredential
signature usage consistent with credential-helper.js.
- Around line 107-123: The cached-response path returns a HIT before verifying
the CourtListener token, violating fail-closed; fix by obtaining and validating
the token (call getCourtListenerToken(c.env) and check !token with
failClosedNoToken) before returning any cached Response in the cache.match
branch (while preserving the existing try/catch and X-Cache header logic in the
cache handling code paths such as the cache hit branch and the functions
getCourtListenerToken and failClosedNoToken).
In `@tests/api/courtlistener-routes.test.js`:
- Around line 147-155: The test uses a loose length check
(expect(body.error.length).toBeLessThan(400)) which doesn't enforce the 200-char
truncation contract; update the assertions in the "propagates upstream non-2xx
status and truncates body to 200 chars" test to (1) assert the response contains
the first 200 characters of longBody
(expect(body.error).toContain(longBody.slice(0,200))), (2) find where that
200-char prefix appears (const prefixIndex =
body.error.indexOf(longBody.slice(0,200));
expect(prefixIndex).toBeGreaterThan(-1)), and (3) assert the total length is at
most that prefix position + 200
(expect(body.error.length).toBeLessThanOrEqual(prefixIndex + 200)), optionally
also assert the 201st char is not present
(expect(body.error).not.toContain(longBody.slice(200,201))). This replaces the
loose expect(body.error.length).toBeLessThan(400) check and uses longBody,
get("/search"...), and body.error to locate and verify truncation.
- Around line 163-170: Update the "never echoes the API token in error bodies"
test to also assert the actual token value is absent from the error string:
after getting body.error, add an assertion that it does not contain the token
from the test env (use the env token variable used when calling get, e.g.
env.COURT_LISTENER_TOKEN or fallback to the literal "test-cl-token" used in the
mock). Keep the existing Authorization check and ensure you reference the test's
get("/search", env, ...) call and body.error in the assertion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cf58de92-34cb-4823-a95e-84ceb5cb73e3
📒 Files selected for processing (5)
public/openapi.jsonsrc/api/router.jssrc/api/routes/courtlistener.jstests/api/courtlistener-routes.test.jswrangler.jsonc
C1 — Drop `Cache-Control: public, max-age=900` from proxy responses.
The Worker Cache API stores responses internally and does not require
this header for HIT semantics. Emitting `public, max-age=…` would also
instruct downstream browsers/CDNs to cache responses from an
authenticated proxy. Matches google.js.
I1 — Reject unknown resources with `400 { error: "path_not_allowed",
resource }` per spec (was 404). Updated openapi.json accordingly:
generic-resource path now declares 400 with a typed error schema;
{resource}/{id} retains a separate 404 for upstream not-found.
I3 — Defense-in-depth: scrub the resolved API token from upstream
error bodies on both GET and POST paths before truncation. If
CourtListener ever reflected the Authorization header value back in
its error response, the token would no longer leak through the proxy.
I5 — Align getCredential(...) call signature with google.js:
(env, brokerPath, fallbackEnvVar, logPrefix). The fallback env var
name now matches the Secrets Store binding name (was a bogus
`_FALLBACK` suffix), so a single configured env value works whether
delivered as a Secrets Store binding or a plain Worker env var.
I6 — Pin upstream Content-Type to application/json on POST
/citation-lookup. CourtListener's citation-lookup endpoint only
accepts JSON; forwarding an arbitrary client-supplied Content-Type
would let a caller confuse the upstream parser.
Tests
-----
- Update existing 404-allowlist test to assert 400 path_not_allowed +
resource field.
- Strengthen "never echoes the API token" assertion to verify scrub
marker `[REDACTED]` and absence of the literal token string.
- Cache-miss test asserts Cache-Control header is now absent (was
asserting max-age=900).
- New: success-path body does not contain the token; CL 429 status
propagates verbatim; POST scrubs token in error body; POST pins
Content-Type to application/json regardless of client header;
POST never consults Cache API.
Deferred (not blocking):
- I2 (dead citation-lookup in GET allowlist): intentional — the
allowlist entry causes the generic GET route to match and emit
405 instead of 400 path_not_allowed, which is the correct method
signal for a wrong-verb call.
- I4 (cache key not tenant-scoped): intentional and documented inline
— CourtListener data is public Free Law Project content and
identical across tenants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Review-fix pass on AddressedC1 — Dropped I1 — Unknown resources now return I3 — Added I5 — Aligned I6 — Pinned upstream New tests (I7)
Deferred (not blocking — intentional)
ValidationBaseline was 463 passed (+6 new tests, all green). No deploy performed. |
|
Deploy deferred pending credential routing. Blocker tracked in #237 — Code/tests on this PR are merge-ready independently. Will redeploy and re-run |
- clGet: resolve token BEFORE consulting the Cache API so a cache HIT can no longer bypass the fail-closed 503 contract (codex P2). Previously a cached URL kept serving after the token was removed/unavailable. - credential-helper call: drop the undeclared COURTLISTENER_API_TOKEN env fallback arg (coderabbit) — resolution is now exactly Secrets Store binding then broker, no third silent credential path. - router CORS: add X-Bypass-Cache to allowHeaders so the documented browser bypass path passes preflight from allowed origins (codex P2). - openapi.json: document the 502 (Bad Gateway) response on all four CourtListener proxy operations to match runtime behavior (coderabbit). - wrangler.jsonc: fix indentation of the two COURTLISTENER_API_TOKEN secrets_store_secrets entries to match surrounding array items (copilot). - tests: tighten truncation assertions to the exact 200-char contract and add a regression test asserting fail-closed 503 even on a cache hit when no token is available. 28/28 courtlistener tests pass; eslint clean on changed files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
@copilot review — all prior findings addressed in 6389d45 (fail-closed cache bypass, token-redaction test, env fallback, 502 docs, CORS header, wrangler indentation); 28/28 tests pass, all review threads resolved. Please re-review the head commit so the copilot_code_review gate clears. |
Summary
Adds a CourtListener REST v4 proxy at
/api/proxy/courtlistener/*for ChittyCounsel (PR https://github.com/chittycorp/CHITTYCOUNSEL/pull/4). Mirrors the establishedgoogle.jsproxy pattern with CourtListener-specific behaviors per spec.Endpoints
GET /api/proxy/courtlistener/search— unified search, query passthroughGET /api/proxy/courtlistener/:resource[/:id]— allowlisted resource fetchPOST /api/proxy/courtlistener/citation-lookup— citation lookup, body forwarded verbatimAllowlist:
search, opinions, clusters, dockets, docket-entries, recap-documents, audio, citation-lookup, people, courts. Unknown resources → 404 before any upstream call.Spec compliance vs google.js (deltas)
Authorization: Token <key>(CL convention, not Bearer)503 POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLEwhen token absentX-Bypass-Cache: 1escape/citation-lookupbody forwardingCredential plane (synthetic env model)
Token is delivered via Cloudflare Secrets Store binding
COURTLISTENER_API_TOKENdeclared inwrangler.jsoncat:dev)env.staging.secrets_store_secretsenv.production.secrets_store_secretsAll three reference store
e914522471964c3c8cf1e601770edcc3.Status of the secret VALUE: NOT yet present in the store — verified via
npx wrangler secrets-store secret list e914522471964c3c8cf1e601770edcc3 --remote --per-page 100(no entry matchingcourt). This is a known provisioning gap that chico owns downstream. Until the value is written, the 503 fail-closed path is exercised — deploy of this PR is safe and non-disruptive (no behavior change to existing routes; new routes simply return 503 to consumers).No operator credential paste path. No
op://URI dependency.Tests
21 new unit tests in
tests/api/courtlistener-routes.test.js, mirroring the establishedgoogle-routes.test.jspattern (vi.mockoncredential-helper— repo-local convention for route unit tests; real service-integration smoke is the post-deploycurlevidence in the test plan below).Files
src/api/routes/courtlistener.js(new) — proxy implementationsrc/api/router.js— mounts at/api/proxy/courtlistenerwrangler.jsonc— 3secrets_store_secretsbinding entries (top + staging + production)public/openapi.json— 4 path entries underCourtListenertagtests/api/courtlistener-routes.test.js(new) — 21 testsTest plan
npx vitest run tests/api/courtlistener-routes.test.js— 21/21 passnpx vitest run tests/api/— 137/137 pass (no regressions)npx eslint src/api/routes/courtlistener.js tests/api/courtlistener-routes.test.js— cleanwrangler.jsoncparses (JSONC strip-comments + JSON.parse)public/openapi.jsonparsesnpm run deploy:staging/npm run deploy:production(safe-deploy)COURTLISTENER_API_TOKENvalue into Cloudflare Secrets Storee914522471964c3c8cf1e601770edcc3🤖 Generated with Claude Code
Summary by CodeRabbit
New Features