Skip to content

Delete and revoke member credentials when an install is removed - #30

Merged
LeeJMorel merged 2 commits into
mainfrom
fix/uninstall-credentials-and-delivery
Sep 20, 2026
Merged

LeeJMorel merged 2 commits into
mainfrom
fix/uninstall-credentials-and-delivery

Conversation

@LeeJMorel

Copy link
Copy Markdown
Member

Two reported faults, both on the app side.

Uninstall left every member token behind

A lifecycle 404 forgot the workspace row, which is only routing. The sealed access and refresh tokens in connections were never deleted, the sync never reconciled member connections against what Initiative still held, and nothing ever asked GitHub to end the authorizations — while the public docs promise each credential is deleted and the app told to stop using them.

Removal now deletes the credentials and revokes the grant at GitHub. The grant rather than the token: a refresh token sits beside every access token and would otherwise mint a replacement. Subscriptions go with them — a delivery secret for a guild the app is no longer in is a credential that outlived its install.

Deliberately not triggered by a turned-off app or a cleared workspace connection: the docs promise an off switch keeps the setup, so neither touches a credential. The sweep is keyed on the credentials themselves rather than the workspace table, so an app disabled then removed cannot be missed. Per-member convergence rides on the config read, which already lists who is still connected and was being discarded.

Webhook handling was synchronous, unretried and rate-limit blind

Deliveries now become rows in an outbox and the request answers immediately; delivery retries with exponential backoff and gives up after ~30 minutes. Install changes resync after the response rather than inside it. GitHub's three rate-limit signals are read and told apart from a plain permission 403. The installation's repository listing is cached 15 minutes and dropped on the webhook that says it changed, instead of being paged every sync.

This composes with the existing runDeliveryOnce ledger: that still wraps the work, and the work is now just an enqueue.

Schema

The outbox arrives as an appended step (0003-outbox), so a database that already ran the initial schema takes it on next boot rather than being dropped.

Verification

Rebased onto current main (kit v0.20.0, vitest 5). Locally: typecheck, build, manifest/catalog/env-contract/github-app all pass, and 175 tests across 13 files pass. The 12 Postgres-backed test files could not be run locally — no database available on that machine — so CI is the real gate for those.

Two reported faults, both on the app side.

Uninstall left every member token behind. A lifecycle 404 forgot the
workspace row, which is only routing; the sealed access and refresh
tokens in `connections` were never deleted, the sync never reconciled
member connections against what Initiative still held, and nothing ever
asked GitHub to end the authorizations. The public documentation
promises each credential is deleted and the app told to stop using them.

Removal now deletes the credentials and revokes the grant at GitHub —
the grant rather than the token, since a refresh token sits beside every
access token and would mint a replacement. Subscriptions go with them. A
turned-off app and a cleared workspace connection deliberately do not
trigger any of it: the documentation promises an off switch keeps the
setup. The sweep is keyed on the credentials themselves rather than on
the workspace table, so an app disabled and then removed cannot be
missed. Per-member convergence rides on the config read, which already
lists who is still connected and was being discarded.

Webhook handling was synchronous, unretried and blind to rate limits.
Deliveries now become rows in an `outbox` and the request answers;
delivery retries with exponential backoff and gives up after about half
an hour. Install changes resync after the response rather than inside
it. GitHub's three rate-limit signals are read and told apart from a
plain permission 403, and the installation's repository listing is
cached for fifteen minutes and dropped on the webhook that says it
changed, instead of being paged on every sync.

The outbox arrives as an appended schema step, so a database that has
already run the initial schema takes it on the next boot rather than
being dropped.
The route answers with what it wrote down rather than what it sent, so
the field it reports is queued. The lease test still asserted the old
published, which is the one thing CI caught.
@LeeJMorel
LeeJMorel merged commit 7398d21 into main Sep 20, 2026
2 checks passed
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