Skip to content

feat(selfhost): record installation app_id and filter foreign-app webhooks - #1419

Merged
JSONbored merged 1 commit into
mainfrom
claude/installations-app-id
Jun 26, 2026
Merged

feat(selfhost): record installation app_id and filter foreign-app webhooks#1419
JSONbored merged 1 commit into
mainfrom
claude/installations-app-id

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Self-host migration prereq (blocker 2a). When the cloud App and a self-host App are installed on the same account during the parallel-run phase, each backend should only act on its own App's installations.

  • Adds a nullable installations.app_id column (Drizzle schema + migration 0071), captured in upsertInstallation from installation events and the App-installation API refresh. A payload that omits app_id (e.g. a pull_request event) never clears the stored value.
  • upsertInstallation now returns the resolved app_id, so the webhook entry can filter without a second DB read.
  • New pure isForeignAppInstallation(ownAppId, installationAppId): returns true only on a positive numeric mismatch with GITHUB_APP_ID; fail-open on any unknown (no own id, unparseable id, or null/unknown installation app_id).
  • Wired at the webhook entry: a foreign-app delivery is acked (webhook_events payload_hash = "foreign_app") without processing, so neither backend acts on the other's installation.

Why it's safe for the live cloud: the per-App webhook secret (GITHUB_WEBHOOK_SECRET) is the primary isolation — each backend already only accepts deliveries signed with its own App's secret — so this is defense-in-depth for a shared-endpoint/secret misconfig. It is fail-open: an unknown or own-matching app_id always processes, so the live single-app path is byte-identical until the column is populated (existing rows backfill lazily on their next installation event).

Part of [[gittensory-selfhost-migration-plan]] Phase 2. No GitHub issue — internal migration prereq.

Scope

  • Backend (src/) + a DB migration — schema.ts, repositories.ts, app.ts, processors.ts, types.ts, migrations/0071_installations_app_id.sql
  • No API/OpenAPI change, no wrangler.jsonc binding/var change (GITHUB_APP_ID already exists)
  • Migration is contiguous (0071, db:migrations:check green)

Validation

  • npm run test:ci — green (4513 passed | 4 skipped); db:migrations:check contiguous
  • npm run test:coverage — every changed line and branch covered (verified against coverage/lcov.info)
  • npm audit --audit-level=moderate — 0 vulnerabilities; typecheck/ui:typecheck clean; git diff --check clean
  • Tests: isForeignAppInstallation (mismatch / match / null-or-unknown fail-open / unparseable-own fail-open); app_id capture + return + preserve-on-omit round trip; a foreign-app webhook is acked without upserting the PR; a matching-app webhook processes normally (no false filtering).

Safety

  • No secrets / wallets / hotkeys / coldkeys / trust scores / reward values added
  • Fail-open by construction — can never drop a legitimate delivery whose app_id is unknown; live single-app path byte-identical
  • Complements (does not replace) per-App webhook-secret signature verification

…hooks

Self-host migration prereq (blocker 2a). When the cloud App and a self-host App
are installed on the same account during the parallel-run phase, a backend
should only act on ITS OWN App's installations.

- Add a nullable installations.app_id column (Drizzle + migration 0071),
  captured in upsertInstallation from installation events / the App-installation
  API refresh; a payload without it never clears the stored value.
- upsertInstallation returns the resolved app_id so the webhook entry can filter
  without a second read.
- New pure isForeignAppInstallation(ownAppId, installationAppId): true ONLY on a
  positive numeric mismatch with GITHUB_APP_ID; fail-open on any unknown.
- Wire it at the webhook entry: a foreign-app delivery is acked
  (webhook_events 'foreign_app') without processing.

Defense-in-depth: the per-App webhook secret (GITHUB_WEBHOOK_SECRET) is the
PRIMARY isolation; this is the belt-and-suspenders for a shared-endpoint/secret
misconfig. FAIL-OPEN — an unknown/own-matching app_id always processes, so the
live single-app path is byte-identical until the column is populated.
@dosubot dosubot Bot added the size:M label Jun 26, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.37%. Comparing base (9b53afa) to head (0ce3f43).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1419   +/-   ##
=======================================
  Coverage   95.37%   95.37%           
=======================================
  Files         192      192           
  Lines       20857    20866    +9     
  Branches     7542     7546    +4     
=======================================
+ Hits        19892    19901    +9     
  Misses        383      383           
  Partials      582      582           
Files with missing lines Coverage Δ
src/db/repositories.ts 96.10% <100.00%> (+<0.01%) ⬆️
src/db/schema.ts 68.32% <ø> (ø)
src/github/app.ts 97.22% <100.00%> (+0.10%) ⬆️
src/queue/processors.ts 88.19% <100.00%> (+0.02%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JSONbored
JSONbored merged commit 8761078 into main Jun 26, 2026
19 checks passed
@JSONbored
JSONbored deleted the claude/installations-app-id branch June 26, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant