From a60ad6a92168ef0aac689ee01ec4ef7737cc5bbf Mon Sep 17 00:00:00 2001 From: Thomas Durieux Date: Wed, 9 Sep 2026 22:08:12 +0200 Subject: [PATCH] feat: add read-only GitHub App access alongside OAuth --- .gitignore | 3 + README.md | 5 +- docker-compose.github-app.yml | 9 + docs/github-app-migration-plan.md | 133 +++++++ docs/github-app-setup.md | 150 +++++++ public/asset-manifest.json | 2 +- public/i18n/locale-en.json | 23 +- public/partials/anonymize.htm | 9 + public/partials/connections.htm | 44 ++ public/partials/dashboard.htm | 2 + public/partials/header.htm | 3 +- public/partials/home.htm | 2 +- public/partials/signin.htm | 8 + public/script/app.js | 98 ++++- public/script/routes.js | 2 + public/script/templates.js | 4 + public/script/vendor.min.js | 40 +- src/config.ts | 34 +- src/core/Gist.ts | 5 + src/core/GitHubUtils.ts | 40 +- src/core/PullRequest.ts | 2 + src/core/Repository.ts | 16 +- src/core/User.ts | 20 +- src/core/credential-crypto.ts | 12 +- src/core/credentials.ts | 2 +- src/core/github-app.ts | 258 ++++++++++++ src/core/github-token-context.ts | 12 + src/core/migrate-credentials.ts | 1 + .../anonymizedPullRequests.schema.ts | 2 + .../anonymizedPullRequests.types.ts | 2 + .../anonymizedRepositories.schema.ts | 2 + .../anonymizedRepositories.types.ts | 2 + .../model/credentials/credentials.model.ts | 16 +- src/core/model/github-installation.ts | 15 + src/core/model/repository-access.schema.ts | 8 + src/core/redact-secrets.ts | 4 +- src/core/repository-access.types.ts | 7 + src/core/source/GitHubRepository.ts | 11 +- src/queue/processes/downloadRepository.ts | 10 +- src/server/database.ts | 2 + src/server/index.ts | 3 + src/server/routes/connection.ts | 21 +- src/server/routes/github-app.ts | 261 ++++++++++++ src/server/routes/option.ts | 2 + src/server/routes/pullRequest-private.ts | 11 +- src/server/routes/repository-private.ts | 95 ++--- src/server/routes/user.ts | 21 +- test/github-app.test.js | 376 ++++++++++++++++++ test/vue-ui.test.js | 83 +++- 49 files changed, 1754 insertions(+), 139 deletions(-) create mode 100644 docker-compose.github-app.yml create mode 100644 docs/github-app-migration-plan.md create mode 100644 docs/github-app-setup.md create mode 100644 public/partials/connections.htm create mode 100644 public/partials/signin.htm create mode 100644 src/core/github-app.ts create mode 100644 src/core/github-token-context.ts create mode 100644 src/core/model/github-installation.ts create mode 100644 src/core/model/repository-access.schema.ts create mode 100644 src/core/repository-access.types.ts create mode 100644 src/server/routes/github-app.ts create mode 100644 test/github-app.test.js diff --git a/.gitignore b/.gitignore index 78ef489..17b179a 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,6 @@ tmp/ temp/ # End of https://www.gitignore.io/api/node + +# GitHub App signing key +/secrets/github-app.pem diff --git a/README.md b/README.md index 151342d..bb4bb46 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,12 @@ AUTH_CALLBACK=http://localhost:5000/github/auth - `GITHUB_TOKEN` — create one at with the `repo` scope. - `CREDENTIAL_KEYS` / `CREDENTIAL_ACTIVE_KEY_ID` — generate a key with `openssl rand -base64 32`. Existing installations must follow the [credential migration guide](docs/credential-encryption.md) before starting this release. -- `CLIENT_ID` / `CLIENT_SECRET` — from a new GitHub App at . +- `CLIENT_ID` / `CLIENT_SECRET` — from an OAuth App at . - The App's callback must be `https:///github/auth` (matching `AUTH_CALLBACK`). +To enable read-only access to selected private repositories alongside OAuth, follow +the [GitHub App setup guide](docs/github-app-setup.md). + **3. Start the server** ```bash diff --git a/docker-compose.github-app.yml b/docker-compose.github-app.yml new file mode 100644 index 0000000..753d66e --- /dev/null +++ b/docker-compose.github-app.yml @@ -0,0 +1,9 @@ +# Enable with: docker compose -f docker-compose.yml -f docker-compose.github-app.yml up -d +# App settings are supplied through the existing .env file for both services. +services: + anonymous_github: + volumes: + - ./secrets/github-app.pem:/run/secrets/github-app.pem:ro + streamer: + volumes: + - ./secrets/github-app.pem:/run/secrets/github-app.pem:ro diff --git a/docs/github-app-migration-plan.md b/docs/github-app-migration-plan.md new file mode 100644 index 0000000..70d2d0a --- /dev/null +++ b/docs/github-app-migration-plan.md @@ -0,0 +1,133 @@ +# GitHub App migration with OAuth coexistence + +Status: proposed implementation plan. No authentication behavior changes in this document. + +## Outcome and scope + +Add a GitHub App that reads selected private repositories, while keeping the existing OAuth App operational throughout the transition. Users can connect both to the same Anonymous GitHub account and migrate individual anonymized resources without changing their URLs, ownership, settings, or cached content. Make the GitHub App the preferred connection after validation; removing OAuth is a separate future decision. + +GitHub Apps support granular read permissions and installation repository selection. The existing OAuth `repo` scope cannot provide the equivalent read-only private repository grant. A GitHub App still uses an OAuth web flow for user authorization; the migration changes the application and credential model, not the underlying sign-in protocol. See [GitHub's comparison](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps). + +Read-only describes access to GitHub. Anonymizing a private repository still publishes the configured anonymized content through this service; preserve that explanation in the creation flow. + +## Current implementation and affected areas + +| Area | Current behavior | Required change | +| --- | --- | --- | +| `src/server/routes/connection.ts` | `/github/login` requests `repo`; `/github/auth` saves one GitHub token; sessions contain the local user ID | Separate provider callbacks and credential writes, shared account identity | +| `src/core/credentials.ts`, `model/credentials/credentials.model.ts`, `credential-crypto.ts` | Encrypted credentials indexed by owner and provider; provider enum only accepts `github` | Preserve existing OAuth envelopes and add distinct App user credentials | +| `src/core/GitHubUtils.ts` | OAuth reset after seven days, `/user` token check, global `GITHUB_TOKEN` fallback; throttling keyed by token suffix | Provider-specific validation/renewal, explicit access resolution, stable rate-limit identities | +| `src/core/User.ts` | Repository discovery through `/user/repos`, cached on the user | Discover App installations and user-accessible repositories; merge provider availability | +| `src/server/routes/repository-private.ts`, `src/core/Repository.ts` | Creation, claim, preview, branches, updates and diagnostics pass owner tokens | Resolve the selected connection consistently and persist resource bindings | +| `src/core/source/GitHubRepository.ts`, `GitHubBase.ts`, `GitHubDownload.ts`, `GitHubStream.ts` | Metadata, commits, README, Pages, trees, raw content and archives use token-based clients | Use renewable access contexts across requests and stream retries | +| `src/core/PullRequest.ts`, `src/core/Gist.ts` and their routes/models | Separate owner-token lookup and fallback paths | Explicit provider handling; retain OAuth compatibility for both features | +| `src/queue`, `src/streamer`, scheduler callers | Downloads and updates run without an interactive session | Resolve bindings at execution time, renew tokens, stop on revoked access | +| `src/server/routes/user.ts` | Account removal revokes the OAuth grant | Revoke each user grant using its own client; remove local bindings | +| `src/config.ts`, Compose files, `README.md`, frontend partials/scripts/locales | OAuth-only configuration and connection UI | Dual configuration, connection management, deployment instructions | + +The README currently calls the registration at `/settings/applications/new` a GitHub App; implementation documentation should identify it as an OAuth App and provide a separate GitHub App registration procedure. + +## Proposed access model + +### Separate identity, user authorization, and installation access + +Use the GitHub numeric user ID in `externalIDs.github` to resolve the same local account for either login. Preserve local IDs, admin flags, quotas, coauthors and API tokens. App linking must reject a different GitHub identity and disabled accounts. Do not automatically link App accounts by username; handle legacy accounts without a verified GitHub ID through explicit recovery. + +Use an App user access token for identity, installation discovery, and verification that the connecting user can access the selected repository. Use an installation access token for bound repository downloads and scheduled updates. Installation access is independent of the individual user, so an installation ID supplied by a browser is never sufficient authorization. GitHub documents the user/app permission intersection in [user token generation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app). + +For a new binding or a manual source change, verify the local owner and the GitHub user's current access through the App user token. For background synchronization, revalidate that owner's repository access at the start of each job/update cycle, with a short bounded cache for streaming requests. Refresh the user grant server-side; if it expires or is revoked, pause new source access until reauthorization. This deliberately prevents an organization installation from preserving a departed user's ability to synchronize private content. Coauthor permission to manage an anonymized resource does not grant permission to attach another installation or replace its owner's connection. + +### Permissions + +Register a public-installable GitHub App for personal and organization accounts. Recommend “Only select repositories” during installation. + +| Permission | Level | Purpose | +| --- | --- | --- | +| Repository metadata | Read | Repository identity and metadata | +| Contents | Read | Commits, branches, trees, README, files and archives | +| Pull requests | Read | Preserve PR metadata and diff support | +| Pages | Read | Preserve the current Pages source lookup | + +Request no repository write permissions. Verify PR issue-comment reads with the Pull requests permission before finalizing the manifest; add Issues read only if an actual required endpoint demands it. Private email access is unnecessary for the initial migration; preserve available profile data and tolerate absent email addresses. + +Pages lookup specifically requires Pages read; lack of Pages access must not make ordinary repository import fail. See [Pages endpoint permissions](https://docs.github.com/en/rest/pages/pages#get-a-github-pages-site). + +Gists are a separate capability. Keep existing gist resources on OAuth in the first release, and show that requirement for App-only users. Do not send installation tokens to gist endpoints. Evaluate App user-token support for gist content, comments and raw downloads as a later compatibility step using the [gist API reference](https://docs.github.com/en/rest/gists/gists). OAuth retirement depends on resolving this gap. + +### Persistence and credential lifecycle + +- Keep `provider: "github"` meaning legacy OAuth. Add `github-app-user` with encrypted access and refresh tokens, expiry timestamps, grant status and a concurrency version. Login through either provider must not overwrite the other. +- Preserve the existing ciphertext/AAD contract for OAuth. Extend encryption with a distinct field/purpose binding for refresh tokens so access and refresh ciphertexts cannot be swapped. Extend credential verification, migration tooling, hidden projections and redaction accordingly. +- Add an installations collection keyed by App ID and installation ID, recording GitHub account ID/type, repository selection, permissions, suspension/deletion status and reconciliation time. Store user-to-installation associations separately; organization installations can serve multiple local users. +- Add an explicit access binding to anonymized repositories and PRs: `kind` (`oauth` or `github-app`), local credential owner, stable GitHub repository ID, and installation ID for App resources. A missing binding means legacy OAuth during compatibility rollout. Keep bindings out of public anonymized responses. +- Treat shared repository metadata as metadata, never as proof of access. Partition discovery/authorization caches by user, provider and installation; invalidate them on connection changes. +- Store no installation tokens in MongoDB resource records, sessions, job payloads, public responses or URLs. Cache them briefly in process, keyed by App/installation/repository/permissions, with expiry skew and shared in-flight minting. Give workers and streamers secure access to the App key and resolver. + +Mint installation tokens restricted to the bound repository and required read permissions. GitHub installation tokens expire after one hour; renew before expiry and retry an idempotent read once after an authentication failure. Long downloads must reacquire archive URLs/tokens when restarting rather than persist signed URLs. See [installation token generation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app). + +Enable expiring App user tokens. Store returned expiry values, rotate access/refresh pairs atomically, and serialize refresh across processes to avoid consuming the same refresh token twice. GitHub currently documents eight-hour access tokens and six-month refresh tokens. Keep this separate from the existing OAuth reset endpoint. See [refreshing user tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens). + +### Provider selection and failure behavior + +Introduce a central access service with operations for user identity, repository discovery, repository access and resource access. Return an authenticated client/access context with provider, repository scope and expiry, rather than letting callers guess a token type. Installation tokens cannot be validated with the current `/user` check. + +| Situation | Behavior | +| --- | --- | +| Existing resource without binding | Resolve legacy OAuth | +| New resource with verified App access | Default to App; record binding | +| New resource available through both | Prefer App, allow explicit OAuth selection | +| Resource explicitly bound to OAuth | Continue OAuth until the owner migrates it | +| App unavailable, suspended, revoked or repository deselected | Pause source access; show reconnect/configure action | +| Organization approval pending | Keep the existing OAuth resource working; leave migration pending | +| No private repository grant | Deny source access; do not use another user's or global token | + +Never silently fall back from an App binding to OAuth or `GITHUB_TOKEN`. This prevents a removed App grant from being bypassed by broader retained credentials. Restrict any public fallback path to independently verified public resources. Preserve CLI user-supplied token support as a distinct context. + +Keep rate-limit behavior across token renewal by using provider-aware quota identities (installation for installation access, GitHub user for user access), while retaining GitHub response-driven backoff. Do not log token suffixes as identifiers in new paths. Distinguish expired credentials, revoked access, missing repository, missing permission, rate limit and transient upstream failures. + +## User transition + +1. Add “Connect GitHub App — read-only repository access” alongside “Connect with OAuth — legacy repository access.” Retain the existing OAuth callback URL; introduce dedicated App authorize/callback/setup routes. +2. Bind authorization and installation setup to short-lived, single-use session state with a fixed local return path. Validate installation ownership/access through GitHub APIs after callback. Support installations initiated on GitHub, canceled flows and organization approval delays without trusting setup query parameters. +3. Discover installations using the App user token and paginate `/user/installations/{installation_id}/repositories`. Merge with OAuth discovery by GitHub repository ID, showing connection availability without exposing installation-wide repositories the user cannot access. See [installation endpoints](https://docs.github.com/en/rest/apps/installations). +4. Provide a migration preview listing eligible resources and blocked resources, with reasons such as repository not selected, approval pending, missing permission or gist compatibility. +5. On explicit migration, verify App access and read the resource's configured commit (or PR) before conditionally updating its binding. Preserve all anonymization settings, URLs and caches. A partial batch reports per-resource results and can be rerun; jobs read the latest binding and reject stale results if the binding changes while running. +6. Display each resource's connection and reconnect action. Connecting the App alone does not migrate resources or revoke OAuth. +7. Offer OAuth disconnect after listing remaining dependencies, including gists. A new sign-in does not reduce an existing OAuth grant's scope. Complete removal of the broad grant requires explicit revocation; warn about affected resources in that concrete disconnect flow. + +## Revocation, webhooks and retained content + +Add a webhook route that verifies `X-Hub-Signature-256` against the raw body before JSON processing. Deduplicate deliveries, queue durable processing, and reconcile authoritative state for delayed/out-of-order events. Handle installation creation/deletion/suspension/unsuspension, repository selection changes and user authorization revocation. See [GitHub webhook events](https://docs.github.com/en/webhooks/webhook-events-and-payloads). + +Invalidate tokens, discovery caches and access checks when a grant changes; block new upstream reads for affected resources. Reconcile periodically and on access failures so missed webhooks do not indefinitely preserve access. Repositories transferred to a different installation require revalidation and explicit rebinding; numeric repository IDs remain the identity across renames. + +Proposed retention policy: revocation pauses source synchronization but preserves already published anonymized snapshots under existing expiration/removal rules. Show that policy when connecting/disconnecting. Account/resource removal continues deleting content according to current behavior. On account deletion, revoke both user grants and remove that user's bindings, but do not uninstall a shared organization installation or erase other users' connections. + +## Implementation sequence and release gates + +1. **Access abstraction and compatibility.** Inventory all GitHub calls, add provider-aware contexts and explicit fallback rules, adapt OAuth callers, and add optional binding fields. Gate: existing OAuth, CLI, API-token login, repository, gist and PR paths pass without requiring App configuration. +2. **App infrastructure.** Add installation/grant models, encryption extensions, token issuance/refresh and dual configuration. Proposed settings: `GITHUB_APP_ENABLED`, `GITHUB_APP_ID`, `GITHUB_APP_SLUG`, `GITHUB_APP_CLIENT_ID`, `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_PRIVATE_KEY_FILE`, `GITHUB_APP_CALLBACK`, `GITHUB_APP_WEBHOOK_SECRET`; retain `CLIENT_ID`, `CLIENT_SECRET`, `AUTH_CALLBACK` for OAuth. Validate only enabled providers. Gate: test App accesses a selected private repository with read permissions and cannot access an unselected one. +3. **Connection and lifecycle.** Add App login/link/setup, verified discovery, webhooks, reconciliation, account deletion and frontend connection states. Gate: both logins resolve the same account; multi-user organization installations cannot leak access; cancellation and pending approval preserve OAuth behavior. +4. **Repository and PR integration.** Route creation, previews, branch/commit/Pages reads, updates, claims, admin diagnostics, streams, archives and queued work through the resolver. Gate: App-only private repository and PR workflows succeed, including renewal during delayed jobs; gist compatibility is clearly represented. +5. **Migration UX and tooling.** Add preview, per-resource switching, OAuth disconnect dependency checks, provider labels and retryable batch results. Any administrative migration tool defaults to dry-run and requires verified owner authorization before switching bindings. Gate: mixed-provider resources for one account operate independently without changing public URLs. +6. **Staged rollout.** Deploy schema/read compatibility to all API instances, workers and streamers before enabling App writes. Enable for maintainers, then an opt-in cohort, then make App the new-connection default. Keep separate controls for new App connections/migrations and serving existing App bindings. Publish setup, key rotation and recovery instructions in README/docs and Compose configuration. + +Do not relabel or re-encrypt existing `github` credentials in bulk. An optional idempotent backfill can mark legacy bindings as OAuth after every deployed reader supports them. Extend the existing credential migration verifier before App credentials are written, so it does not misclassify their provider or envelope format. + +Monitor success/error rates by provider, token issuance/refresh failures, permission failures, paused resources, webhook lag and migration outcomes. Never include private names, installation metadata or credentials in public error responses. Advance rollout only after the cohort exercises token expiration, queued work and revocation without regressions. + +Rollback disables new App connections and migrations while keeping the dual-provider resolver running for already migrated resources. Roll back only to an App-aware release once bindings exist. Switching a resource back to OAuth requires explicit owner selection and a verified remaining OAuth grant; restoring a global fallback is not a rollback mechanism. + +## Validation checklist + +- Both login providers, account linking, disabled/deleted accounts, mismatched identity, callback state replay, unchanged ID-only sessions and API-token login. +- OAuth-only, App-only and mixed accounts; personal/organization installs; selected/all repositories; pagination; users sharing an installation with different repository access. +- Private metadata, branches, README, commit/tree/raw reads, archives, streaming, Pages and PR comments/diff; preserve OAuth gist and CLI behavior. +- App user refresh rotation under concurrency; installation token expiry during queued work and retries; transient GitHub errors and shared rate-limit backoff. +- Forged installation/repository IDs, cached private metadata, coauthor/admin paths and all global-token fallback branches. +- Suspension, deselection, uninstall, user grant revocation, lost organization membership, repository rename/transfer, webhook duplicates/out-of-order delivery and missed-event reconciliation. +- Migration cancellation, partial success, reruns and in-flight jobs; stable anonymous URLs and options; OAuth disconnect dependency detection; account deletion with shared installations. +- MongoDB encryption/projection/migration integration tests, token redaction and no secrets in sessions, queues, responses or stored archive URLs. +- Run `npm test`, `npm run lint`, `npm run build`, relevant UI checks, and existing MongoDB integration tests with a disposable `TEST_MONGODB_URI`. Perform sandbox GitHub App acceptance checks against disposable repositories before rollout; inspect granted permissions rather than issuing writes against real repositories. + +Completion means existing OAuth users continue operating, App users can anonymize selected private repositories using only read permissions, users can migrate resource by resource, and revocation/expiry never bypasses the chosen connection. diff --git a/docs/github-app-setup.md b/docs/github-app-setup.md new file mode 100644 index 0000000..5901a73 --- /dev/null +++ b/docs/github-app-setup.md @@ -0,0 +1,150 @@ +# GitHub App setup and OAuth transition + +The GitHub App is opt-in for operators. Existing OAuth configuration and resources +continue working with `GITHUB_APP_ENABLED=false` (the default). This feature uses +existing repository streaming/proxy paths and does not introduce ZIP uploads or +permanent repository copies. + +## Register the GitHub App + +Create a GitHub App at https://github.com/settings/apps/new. This is separate from +the existing OAuth App; retain its client credentials and callback. + +Configure: + +- Callback URL: `https://YOUR_HOST/github/app/callback`. +- Setup URL: `https://YOUR_HOST/github/app/setup`; enable **Redirect on update**. +- Webhook URL: `https://YOUR_HOST/github/app/webhook`; enable webhooks and generate + a strong random webhook secret. +- Repository permissions: **Contents: read-only**, **Metadata: read-only**, + **Pull requests: read-only**, **Pages: read-only**. Do not grant write permissions. + Pull requests read also permits [reading PR issue comments](https://docs.github.com/en/rest/issues/comments#list-issue-comments); Issues permission is unnecessary. +- Keep user access token expiration enabled. Private email permission is unnecessary. +- Leave **Request user authorization (OAuth) during installation** unchecked. + Anonymous GitHub authorizes the user before opening installation; the setup + redirect must remain available afterward. +- Allow installation on any account if this is a public service. +- Generate a private key and record the App ID, slug, client ID and client secret. + +GitHub delivers installation, installation repository selection, and App user +revocation lifecycle events. The webhook verifies raw request bytes before parsing +JSON. Failed processing returns an error for operational visibility; use GitHub's +delivery redelivery controls after an outage. Pending installation checks also +retry on repository access. Access checks verify the user's current repository +access before each new upstream access. + +GitHub documents [registration](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app), +[return redirects](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/about-the-setup-url), +and [repository preselection for OAuth migration](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/migrating-oauth-apps-to-github-apps). + +## Environment and deployment + +Set these on every API process, background worker and streamer: + +```dotenv +GITHUB_APP_ENABLED=true +GITHUB_APP_NEW_CONNECTIONS=true +GITHUB_OAUTH_ENABLED=true +GITHUB_APP_ID=123456 +GITHUB_APP_SLUG=your-app-slug +GITHUB_APP_CLIENT_ID=Iv.your-client-id +GITHUB_APP_CLIENT_SECRET=your-client-secret +GITHUB_APP_CALLBACK=https://YOUR_HOST/github/app/callback +GITHUB_APP_WEBHOOK_SECRET=your-random-webhook-secret +GITHUB_APP_PRIVATE_KEY_FILE=/run/secrets/github-app.pem +``` + +Supply the PEM key through a read-only secret mount at that path on **both** +`anonymous_github` and `streamer`. Alternatively, `GITHUB_APP_PRIVATE_KEY` accepts +the actual multiline PEM value from your secret manager. Never commit the PEM or +secrets. The optional Compose override `docker-compose.github-app.yml` mounts +`./secrets/github-app.pem` in both services: + +```sh +docker compose -f docker-compose.yml -f docker-compose.github-app.yml up -d --build +``` + +Keep the existing `CLIENT_ID`, `CLIENT_SECRET`, `AUTH_CALLBACK`, `SESSION_SECRET` +and credential encryption keyring unchanged. The App adds encrypted credentials +under `github-app-user`; legacy `github` credentials remain OAuth credentials. +Refresh tokens have a distinct authenticated encryption purpose. The existing +credential verification command now checks both envelopes. + +Deploy this release to all readers/workers **before** enabling App connections. +Existing records without a connection binding retain OAuth behavior; no bulk +credential migration is necessary for an already encryption-capable deployment. +Do not rerun plaintext cleanup just to enable the App. + +## User flow + +Sign in offers App and OAuth choices. Both resolve the same existing account by +its GitHub numeric user ID. A signed-in user cannot attach a different GitHub +identity. Legacy accounts without a verified GitHub ID require account recovery; +App login never automatically links by username or email. + +On the anonymization form, **Connect read-only GitHub access** starts user +authorization and then repository installation. **Allow repository access on +GitHub** opens installation/configuration directly, preserving the current draft +in this browser tab for 30 minutes. Confirm access on GitHub and return to the +form. Existing installations have direct account-specific configuration links. +GitHub may require organization administrator approval. Use **Refresh access +after approval** on the Connections page when approval is delayed. + +App-connected accounts default to the App for new repository/PR access. The +explicit **Use existing OAuth access** choice handles repositories not yet +available through the App. An App error never silently selects OAuth. Gists +continue using OAuth in this release. + +**GitHub connections** lists each resource's current connection. First check +read-only access, then switch the resource. The switch validates the existing +commit or PR and conditionally updates the binding; it preserves the anonymous +URL, settings and existing content. Busy resources must finish before switching. +Each resource can be switched back to a verified existing OAuth grant. + +OAuth can be explicitly revoked once all dependent resources, including gists, +have been migrated or removed and a working App sign-in remains. Merely connecting +the App does not revoke or narrow the OAuth grant. + +## Access lifecycle and rollback + +Installation tokens are short-lived, minted for one repository, and cached only +in process. Encrypted App user grants allow server-side renewal without a browser +session. Token refresh is serialized through MongoDB with conditional writes, +so a concurrent login or revocation cannot be overwritten. Background source +reads verify the owner's current App access; losing user access stops new reads. +App-bound resources never use the global token or another user's grant. + +Uninstall, deselection, suspension and user grant revocation block new upstream +reads. Already published anonymized content follows existing expiration/removal +settings. Account removal revokes the user's grants and removes their resources; +it does not uninstall a shared organization installation. + +Failed installation webhook checks remain pending in MongoDB and are retried on +the next repository access. Access stays blocked until reconciliation succeeds. +Revision checks prevent older responses from undoing newer lifecycle events. +User-revocation events verify the current grant, so delayed deliveries cannot +revoke a working grant created by reconnecting. + +Set `GITHUB_APP_NEW_CONNECTIONS=false` to pause new App sign-ins/installations and +migrations while continuing to serve existing App resources. Setting +`GITHUB_APP_ENABLED=false` also disables existing App access; it does not fall +back to OAuth. After creating App bindings, rollback must stay on an App-aware +release with the same encryption keys. OAuth removal is not part of this release. + +## Validation before enabling production + +Run `npm test`, `npm run lint`, `npm run build`, and `npm run test:ui`. +MongoDB integration tests require a disposable test server: + +```sh +TEST_MONGODB_URI=mongodb://127.0.0.1:27029 npm test +``` + +The tests create and drop separately named databases. Validate the configured App +against a disposable private GitHub repository: sign in, install with only that +repository selected, import, view files, refresh, migrate an OAuth resource, and +remove App access. Verify a second unselected private repository is inaccessible. +Exercise a private PR including comments and a Pages-enabled repository. Confirm +signed webhook deliveries arrive successfully and that reconnecting restores +access. Live GitHub acceptance requires a registered App and cannot be simulated +by the local test suite. diff --git a/public/asset-manifest.json b/public/asset-manifest.json index a2631fa..65bcfa2 100644 --- a/public/asset-manifest.json +++ b/public/asset-manifest.json @@ -1,6 +1,6 @@ { "core.min.js": "core.c5bd53363a.min.js", - "vendor.min.js": "vendor.0f10f9accb.min.js", + "vendor.min.js": "vendor.b17b42ea2b.min.js", "mermaid.min.js": "mermaid.f848a72d16.min.js", "all.min.css": "all.7ac8730b9a.min.css", "markdown.min.js": "markdown.ad7b1d71c3.min.js", diff --git a/public/i18n/locale-en.json b/public/i18n/locale-en.json index 8e6877a..42e3701 100644 --- a/public/i18n/locale-en.json +++ b/public/i18n/locale-en.json @@ -114,7 +114,28 @@ "storage_read_error": "An error occurred while reading the file from storage — please try again.", "upstream_error": "A temporary error occurred while fetching from GitHub — please try again.", "token_expired": "Your GitHub access token has expired. Please log out and log in again to refresh it.", - "job_is_active": "This job is currently running — wait for it to finish or remove it first." + "job_is_active": "This job is currently running — wait for it to finish or remove it first.", + "github_app_access_required": "Allow this repository on GitHub using the read-only access button, then retry. Organization access may require administrator approval.", + "github_app_reconnect_required": "Reconnect your GitHub App account to restore read-only access.", + "github_app_disabled": "New GitHub App connections are currently unavailable.", + "github_oauth_required": "Connect legacy GitHub OAuth to use this feature.", + "github_identity_mismatch": "Use the same GitHub account that you are signed in with here.", + "github_app_permissions_invalid": "The GitHub App must be configured with read-only permissions.", + "github_unavailable": "GitHub is temporarily unavailable. Please retry.", + "connection_changed": "The repository connection changed. Reload and try again.", + "repository_busy": "Wait for the repository operation to finish before changing its connection.", + "invalid_connection": "Select a supported GitHub connection.", + "github_oauth_disabled": "Legacy OAuth sign-in is currently disabled.", + "invalid_auth_state": "The authorization session expired. Please start again from Anonymous GitHub.", + "invalid_webhook_signature": "The webhook signature is invalid.", + "webhook_processing_failed": "Webhook processing failed. Retry the delivery.", + "github_app_authorization_cancelled": "GitHub authorization was cancelled.", + "github_account_link_required": "This username belongs to an existing account. Sign in to that account before connecting GitHub.", + "github_app_refresh_busy": "GitHub credentials are being renewed. Please retry shortly.", + "github_grant_revocation_failed": "Unable to revoke the GitHub grant. Please retry.", + "another_login_required": "Connect and verify another sign-in method before disconnecting OAuth.", + "oauth_resources_remaining": "Some resources still require OAuth. Migrate or remove them before disconnecting.", + "invalid_github_path": "The GitHub repository path is invalid." }, "WARNINGS": { "page_not_enabled_on_repo": "GitHub Pages is not enabled on this repository. Enable it in the repository's Settings → Pages on GitHub, then refresh.", diff --git a/public/partials/anonymize.htm b/public/partials/anonymize.htm index 2a2e8ad..d86bf8f 100644 --- a/public/partials/anonymize.htm +++ b/public/partials/anonymize.htm @@ -1,4 +1,13 @@
+
+ Connection: {{ githubConnection === 'github-app' ? 'GitHub App (read-only)' : 'Legacy OAuth' }}. Change connection + Repository access: + + + + +

Return here after confirming access on GitHub. Anonymization publishes the configured content, including content from private repositories. Gists currently use OAuth.

+
diff --git a/public/partials/connections.htm b/public/partials/connections.htm new file mode 100644 index 0000000..9ca7b5a --- /dev/null +++ b/public/partials/connections.htm @@ -0,0 +1,44 @@ +
+
My work / GitHub connections
+

GitHub connections

+

Connect the GitHub account you use here. Your anonymizations and URLs stay the same.

+ +
+
+

Read-only GitHub App

+

{{ connections.appError }}. Reconnect to restore access.

+ {{ connections.appConnected ? 'Reconnect GitHub account' : 'Connect GitHub account' }} + Allow repositories on GitHub +

GitHub opens directly to the access screen. Select repositories and confirm. Organization access may require an administrator's approval.

+ + +
+
+

Legacy OAuth

+

{{ connections.oauthConnected ? 'Connected' : 'Not connected' }}. Existing OAuth anonymizations continue to use this connection.

+ Connect legacy OAuth +

{{ connections.gistCount }} gist(s) still require OAuth.

+ +

Migrate all dependent resources before revoking OAuth. Revocation removes GitHub's broad OAuth repository grant.

+
+
+

Repository connections

+

Check read-only access, then switch each resource. Connecting the App alone does not change existing resources.

+

Removing GitHub access stops fetching new source content. Already published anonymized content follows its existing expiration and removal settings.

+
+ {{ resource.id }} — {{ resource.name }} ({{ resource.type }})
+ Connection: {{ resource.connection === 'github-app' ? 'GitHub App (read-only)' : 'Legacy OAuth' }} +
+ + +
+ +
+
+
+
diff --git a/public/partials/dashboard.htm b/public/partials/dashboard.htm index 302ab11..538bce3 100644 --- a/public/partials/dashboard.htm +++ b/public/partials/dashboard.htm @@ -12,6 +12,8 @@
+

Manage GitHub connections and read-only access

+
- @@ -75,6 +75,7 @@