OAuth connect for forge credentials in a multi-person Landrace runner
Discovered from README cross-links · today
Added PKCE OAuth connect and background token refresh for GitLab and GitHub.
What they did
Working through Task 6 of a nine-task remote-mode plan for Landrace, the author added an OAuth connect path so a person can link GitLab or GitHub by authorizing an OAuth app instead of pasting a personal access token. They created src/people/oauth.ts and extended ConnectionDef with an optional oauth spec, then declared provider specs in the GitLab and GitHub integrations: authorize and token URLs (GitLab honoring a configurable base URL), scopes (api for GitLab, repo for GitHub), and the instance secret names holding the client id and secret. When those secrets are unset, the connection falls back to the existing paste form. The implementation uses fetch and node:crypto only, with no new dependency: PKCE S256 challenges, 32 random bytes of state held in memory with a ten-minute TTL and bound to the person, and rejection of unknown, expired, or other-person state. Two routes were added to the UI server: GET /connect/:id/start redirects to the provider, and GET /connect/:id/callback exchanges the code, stores the credential, and redirects back to /#integrations. The redirect URI derives from publicUrl or the host and port. A refresher in the CLI start path runs every 60 seconds and refreshes tokens when expiry is within ten minutes, updating the person's live secretValues map in place. The UI page shows a Connect button when OAuth is configured and the paste form otherwise. Tests were written first and expected to fail, then the code was implemented, and docs/integrations.md was updated with OAuth app creation steps and the callback URL.
How it went
The task's named tests pass, along with pnpm test, pnpm typecheck, pnpm lint, and pnpm build. A live check against a real GitLab OAuth app on loopback confirmed the token is stored. The work was committed as feat: OAuth connect for forge credentials, keeping the nine-task sequence intact so later tasks stay unblocked.
Worth knowing
The post is a task specification plus checklist, so it documents intent and acceptance criteria more than runtime detail. OAuth state lives in memory, so a restart mid-flow invalidates pending connects, and a multi-process deployment would not share state. Client id and secret must be configured as instance secrets before the Connect button appears; otherwise users silently get the paste form. The live verification covered GitLab on loopback only, not GitHub or a remote host. Remote non-loopback binding is still refused until the OIDC task lands, so this is not yet usable as a shared remote instance.
Wiring
Order: write failing tests in tests/people/oauth.test.ts and tests/ui/people.test.ts, run them to confirm failure, then implement. src/people/oauth.ts holds the OAuthSpec type, beginConnect, finishConnect, and refreshIfNeeded, using fetch for token exchange and node:crypto for the S256 challenge and random state. src/hooks/contracts.ts gains ConnectionDef.oauth; integrations/gitlab/index.ts and integrations/github/index.ts supply the provider specs. src/ui/server.ts registers the start and callback routes, reading client id and secret from instance secrets by name and falling back to the paste form when unset. src/ui/page.ts renders the Connect button conditionally. src/cli/start.ts starts a 60-second interval that calls refreshIfNeeded per person and writes refreshed values into the live secretValues map. Credentials persist through CredentialStore at ~/.landrace/credentials/<person>.json with 0600 file mode and atomic writes. The callback URL to register with each provider is <publicUrl>/connect/<id>/callback, documented in docs/integrations.md.
Try it yourself
In this repo, implement OAuth (PKCE) connect for forge credentials as described in the remote plan's Task 6. Create src/people/oauth.ts and add an optional oauth field to ConnectionDef in src/hooks/contracts.ts with shape { authorizeUrl, tokenUrl, scopes, clientIdSecret, clientSecretSecret, field }. Declare specs for GitLab (base URL configurable, scopes ["api"], secrets gitlabOauthClientId/gitlabOauthClientSecret) and GitHub (scopes ["repo"], secrets githubOauthClientId/githubOauthClientSecret). Implement beginConnect with PKCE S256, 32-byte random state kept in memory with a 10-minute TTL bound to the person, finishConnect exchanging the code, and refreshIfNeeded refreshing when expiry is within 10 minutes. Add GET /connect/:id/start and GET /connect/:id/callback routes that store the credential and redirect to /#integrations, using redirect URI ${publicUrl ?? host:port}/connect/<id>/callback. Show a Connect button in the UI when the client secrets are set, otherwise the paste form. Add a 60-second background refresher in src/cli/start.ts that updates the person's live secretValues in place. Use fetch and node:crypto only, add no dependency, write failing tests first, then run pnpm test, pnpm typecheck, pnpm lint and pnpm build.