Skip to content

fix(github): cache the App JWT so App-level reads cache-hit (#1940) - #1981

Merged
JSONbored merged 1 commit into
mainfrom
claude/app-jwt-cache
Jul 1, 2026
Merged

fix(github): cache the App JWT so App-level reads cache-hit (#1940)#1981
JSONbored merged 1 commit into
mainfrom
claude/app-jwt-cache

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

createAppJwt minted a fresh App JWT (RS256 sign) on every call. Beyond the wasted CPU, the rotating JWT changes the auth-scoped response-cache key on every call (responseCacheKey hashes the authorization header), so the metadata cache class never hits for App-level reads (GET /app/installations/{id}) whenever two reads land more than a second apart — the exact heavy path used by the per-repo backfill / refresh-installation-health.

This caches the minted JWT for a margin of its ~9-minute validity (APP_JWT_REUSE_MS = 8 min), so repeated App-JWT reads share one signature and one stable cache key — the metadata cache class becomes effective, and the RS256 mint runs once per window instead of per call. Keyed by App id so a different App (or a test that swaps the private key) never reuses another's token, and cleared in clearInstallationTokenCacheForTest.

Advances #1940 (rec #8 from the #1936 audit).

Scope

Validation

  • git diff --check
  • npm run typecheck
  • npm run test:coverage — added a regression test that advances fake time between two getAppInstallation reads and asserts the second is served from the response cache (fetches === 1) instead of re-fetching (it fails without the JWT cache, since a rotated JWT would miss). The test also covers the re-mint-after-window and different-App branches; the changed region is 100% covered (statements + branches).
  • npm run test:ci
  • npm audit --audit-level=moderate

If any required check was skipped, explain why:

  • No ui:openapi / cf-typegen / migration regen: single-function backend change, no API/schema/binding/DB change.

Safety

  • No secrets/wallets/hotkeys/trust-scores/etc. exposed (the JWT is held only in process memory, exactly like the existing installation-token cache; never logged or persisted).
  • No public GitHub text change.
  • The response-cache key still hashes the auth material (responseCacheKey is unchanged); this only makes the App JWT itself stable within its validity window, so no cross-tenant cache widening.
  • No API/OpenAPI/MCP change. No UI change.

Notes

  • The reuse window (8 min) sits safely inside the JWT's 9-min validity (iat backdated 60s, exp +540s), so a reused token is always still valid at GitHub.

@dosubot dosubot Bot added the size:S label Jul 1, 2026
@loopover-orb

loopover-orb Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-01 04:56:30 UTC

2 files · 1 AI reviewer · no blockers · readiness 86/100 · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review

Review summary
The change correctly caches App JWTs per App id while also invalidating on same-App private-key rotation, which directly addresses the response-cache key churn described in the PR. The reuse window leaves a one-minute margin before the JWT's stated expiration, and the test covers the important cache-hit, rotation, expiry, and multi-App branches. I do not see a reachable correctness break in the visible diff.

Nits — 5 non-blocking
  • nit: src/github/app.ts:361 adds a very long explanatory comment that mixes implementation details, operational history, and rationale; keep the invariant-focused parts near the code and move the incident-level detail to the PR/issue.
  • nit: src/github/app.ts:367 stores a second full copy of the private key in process memory; comparing a derived fingerprint would preserve rotation invalidation while reducing secret duplication.
  • src/github/app.ts:367: consider caching `{ privateKeyFingerprint, jwt, expiresAtMs }` instead of the raw PEM, using the same exact-key-change semantics with less secret material retained.
  • src/github/app.ts:361: trim the comment to the durable invariant, e.g. reuse JWTs for less than their validity window because the auth header participates in GitHub response-cache keys.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (size label size:S; no linked issue context).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 68 registered-repo PR(s), 58 merged, 274 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 68 PR(s), 274 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 68 PR(s), 274 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Explain no-issue PR.
  • Triage stale or unlinked PRs.
  • No action.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@loopover-orb loopover-orb Bot added gittensor gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. labels Jul 1, 2026
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.61%. Comparing base (cb6637b) to head (94155a1).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1981   +/-   ##
=======================================
  Coverage   95.61%   95.61%           
=======================================
  Files         218      218           
  Lines       24306    24315    +9     
  Branches     8817     8818    +1     
=======================================
+ Hits        23240    23249    +9     
  Misses        436      436           
  Partials      630      630           
Files with missing lines Coverage Δ
src/github/app.ts 98.00% <100.00%> (+0.09%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JSONbored

Copy link
Copy Markdown
Owner Author

Fixed the blocker — the reviewers were right, thanks. createAppJwt's cache identity now includes the private key (not just the App id), so a same-App credential rotation invalidates the cache immediately and never serves a JWT signed by the now-revoked old key. Added a regression test that rotates the key mid-window and asserts an immediate re-mint. Rebased on current main and re-ran the full gate (green, patch coverage 100%).

createAppJwt re-signed a fresh App JWT on every call. Beyond the wasted RS256
CPU, the rotating JWT changes the auth-scoped response-cache key every call, so
the metadata cache class never hits for App-level reads (/app/installations/{id})
when two reads land more than a second apart — the heavy path used by the
per-repo backfill / refresh-installation-health.

Cache the minted JWT for a margin of its ~9-min validity (8 min). A Map keyed by
App id (so a process alternating between App identities keeps a JWT per App
instead of evicting one for another) with the private key held in the entry, so a
same-App CREDENTIAL ROTATION invalidates immediately and never serves a JWT
signed by the now-revoked old key. Cleared in clearInstallationTokenCacheForTest.
@JSONbored
JSONbored force-pushed the claude/app-jwt-cache branch from 792ac45 to 94155a1 Compare July 1, 2026 04:50
@dosubot dosubot Bot added size:S and removed size:M labels Jul 1, 2026
@JSONbored

Copy link
Copy Markdown
Owner Author

Also folded in the perf nit: the JWT cache is now a Map keyed by App id, so a process that alternates between App identities keeps a per-App JWT instead of thrashing a single entry (added a test asserting alternating appA/appB reads don't evict each other). Gate green, patch coverage 100%.

@JSONbored
JSONbored merged commit 8b69c4d into main Jul 1, 2026
12 checks passed
@JSONbored
JSONbored deleted the claude/app-jwt-cache branch July 1, 2026 05:01
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

fix(github): complete budget attribution for rate-limit admission

1 participant