Skip to content

Support GitHub App installation tokens as a github_app auth kind #2

Description

@amondnet

Summary

Add a github_app authentication kind so connections can authenticate to GitHub
using GitHub App installation tokens, in addition to the existing PAT (apikey)
and OAuth 2.0 (oauth2) methods.

Today GitHub is exposed only via the OpenAPI (github-rest) and GraphQL
(github-graphql) presets, and the shared HTTP auth vocabulary supports only
apikey, oauth2, and none
(packages/core/sdk/src/integration.ts:66,
packages/core/sdk/src/http-auth/auth-method.ts). There is no support for
GitHub App private keys, installation tokens, or App JWTs.

Background / Design

GitHub App installation tokens cannot be obtained through a standard OAuth2 token
endpoint. The flow is: sign an RS256 JWT with the App private key, then
POST /app/installations/{installation_id}/access_tokens with
Authorization: Bearer <jwt>, which returns a 1-hour token. So oauth4webapi
based helpers (refreshAccessToken / exchangeClientCredentials,
packages/core/sdk/src/oauth-helpers.ts:584,643) cannot be reused directly,
but the surrounding lifecycle machinery can.

Key insight: an installation token has an expiry and no refresh token, which is
structurally identical to the existing client_credentials re-mint model
(packages/core/sdk/src/executor.ts:1454-1477). By mapping a GitHub App onto the
existing OAuth lifecycle columns, the refresh trigger and request-application
paths need no changes.

Storage mapping (no schema migration required)

GitHub App element Stored at Reference
App ID (or Client ID, JWT iss) oauth_client.client_id core-schema.ts:201
Private key (PEM) provider secret via oauth_client.client_secret_item_id core-schema.ts:206
grant identifier "github_app" oauth_client.grant (free text) core-schema.ts:200
Installation ID connection.provider_state (existing JSON column) core-schema.ts:182
Installation token (1h) provider secret via connection.item_ids.token existing OAuth model
Expiry connection.expires_at core-schema.ts:175

One App (oauth_client) backs many installation connections, mirroring the
existing "one app backs many connections" OAuth model.

Refresh policy

Same as client_credentials: no refresh token, re-mint on expiry, and failures
surface as StorageError (not reauthRequired) since there is no human to
re-authenticate. The existing trigger in resolveConnectionValues
(executor.ts:1588, row.oauth_client != null && shouldRefreshToken({expiresAt}))
fires automatically because a github_app connection sets oauth_client and
expires_at. Token application stays Authorization: Bearer <token>, identical
to oauth2 (packages/plugins/openapi/src/sdk/config.ts:88-107).

JWT signing: jose

packages/core/sdk currently depends on oauth4webapi only. Since jose
already appears in apps/cloud and apps/host-cloudflare, core/sdk must run on
Cloudflare Workers (edge), where node:crypto RS256 signing is unreliable.
Use the WebCrypto-based jose (same model as oauth4webapi) and add it as a
direct dependency of packages/core/sdk. node:crypto is rejected because it
would break the host-cloudflare runtime.

Tasks

  • Phase 1 - Token mint helper: Add mintGithubAppInstallationToken to
    packages/core/sdk/src/oauth-helpers.ts (jose RS256 JWT, iss=appId,
    ~9 min exp, then POST /app/installations/{id}/access_tokens, map the
    {token, expires_at} response to OAuth2TokenResponse). Follow the
    Effect.tryPromise(...).pipe(Effect.catch(failOAuth2WithHttpSummary))
    pattern. Add jose to packages/core/sdk/package.json.
  • Phase 2 - Refresh dispatch: Add a github_app branch to
    performTokenRefresh (packages/core/sdk/src/executor.ts:1454-1511),
    reading the installation id from row.provider_state and the private key
    from the provider. Failures map to StorageError (no reauth). Reuse the
    existing token-persist and expires_at update logic unchanged.
  • Phase 3 - Connection setup: In
    packages/core/sdk/src/oauth-service.ts, support registering a
    grant="github_app" oauth_client (private key as secret) and minting a
    connection (model on the client_credentials immediate-connect path). Add
    a providerState? field to MintOAuthConnectionInput
    (oauth-service.ts:76-95) and persist installationId to
    connection.provider_state.
  • Phase 4 - HTTP API / handlers: Add GitHub App register + connect
    endpoints in packages/core/api/src/oauth/api.ts and
    packages/core/api/src/handlers/oauth.ts (no authorization-code redirect).
    Exact route shape TBD after reviewing existing handler signatures.
  • Phase 5 - Catalog projection (display only, lower priority): decide
    whether to add a "github-app" catalog kind in integration.ts:66 and
    describeOpenApiAuthMethods
    (packages/plugins/openapi/src/sdk/plugin.ts:356-379) or surface it as
    "oauth" with a distinguishing field.
  • Phase 6 - Tests (Effect Vitest): unit test
    mintGithubAppInstallationToken against the @executor-js/emulate GitHub
    emulator (wire-level: JWT signing + installations endpoint), the
    performTokenRefresh github_app branch, and an integration test that a
    near-expiry connection re-mints on resolve. Run scoped via vitest run;
    gate with format:check / lint / typecheck / test.

Impact

  • Mostly additive: existing OAuth refresh-trigger, request-application, and
    secret-storage paths are reused unchanged. Only one new grant branch and one
    new helper. No new schema columns (grant and provider_state are reused).
  • Largest open question is the Phase 4 API shape (needs the existing handler
    signatures verified first).

Optional follow-up

  • Installation discovery UX: a helper that lists installations via
    GET /app/installations (signed with the App JWT) so a user can pick one,
    instead of entering the installation id manually.

Decisions (confirmed)

  • Installation id is stored in connection.provider_state.
  • JWT signing uses jose (added to core/sdk), not node:crypto, for edge
    compatibility.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions