You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 performTokenRefreshgithub_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.
To see the specific tasks where the Asana app for GitHub is being used, see below:
Summary
Add a
github_appauthentication kind so connections can authenticate to GitHubusing 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 onlyapikey,oauth2, andnone(
packages/core/sdk/src/integration.ts:66,packages/core/sdk/src/http-auth/auth-method.ts). There is no support forGitHub 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_tokenswithAuthorization: Bearer <jwt>, which returns a 1-hour token. Sooauth4webapibased 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_credentialsre-mint model(
packages/core/sdk/src/executor.ts:1454-1477). By mapping a GitHub App onto theexisting OAuth lifecycle columns, the refresh trigger and request-application
paths need no changes.
Storage mapping (no schema migration required)
iss)oauth_client.client_idcore-schema.ts:201oauth_client.client_secret_item_idcore-schema.ts:206"github_app"oauth_client.grant(free text)core-schema.ts:200connection.provider_state(existing JSON column)core-schema.ts:182connection.item_ids.tokenconnection.expires_atcore-schema.ts:175One App (
oauth_client) backs many installation connections, mirroring theexisting "one app backs many connections" OAuth model.
Refresh policy
Same as
client_credentials: no refresh token, re-mint on expiry, and failuressurface as
StorageError(notreauthRequired) since there is no human tore-authenticate. The existing trigger in
resolveConnectionValues(
executor.ts:1588,row.oauth_client != null && shouldRefreshToken({expiresAt}))fires automatically because a
github_appconnection setsoauth_clientandexpires_at. Token application staysAuthorization: Bearer <token>, identicalto
oauth2(packages/plugins/openapi/src/sdk/config.ts:88-107).JWT signing:
josepackages/core/sdkcurrently depends onoauth4webapionly. Sincejosealready appears in
apps/cloudandapps/host-cloudflare, core/sdk must run onCloudflare Workers (edge), where
node:cryptoRS256 signing is unreliable.Use the WebCrypto-based
jose(same model asoauth4webapi) and add it as adirect dependency of
packages/core/sdk.node:cryptois rejected because itwould break the
host-cloudflareruntime.Tasks
mintGithubAppInstallationTokentopackages/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 toOAuth2TokenResponse). Follow theEffect.tryPromise(...).pipe(Effect.catch(failOAuth2WithHttpSummary))pattern. Add
josetopackages/core/sdk/package.json.github_appbranch toperformTokenRefresh(packages/core/sdk/src/executor.ts:1454-1511),reading the installation id from
row.provider_stateand the private keyfrom the provider. Failures map to
StorageError(no reauth). Reuse theexisting token-persist and
expires_atupdate logic unchanged.packages/core/sdk/src/oauth-service.ts, support registering agrant="github_app"oauth_client(private key as secret) and minting aconnection (model on the
client_credentialsimmediate-connect path). Adda
providerState?field toMintOAuthConnectionInput(
oauth-service.ts:76-95) and persistinstallationIdtoconnection.provider_state.endpoints in
packages/core/api/src/oauth/api.tsandpackages/core/api/src/handlers/oauth.ts(no authorization-code redirect).Exact route shape TBD after reviewing existing handler signatures.
whether to add a
"github-app"catalog kind inintegration.ts:66anddescribeOpenApiAuthMethods(
packages/plugins/openapi/src/sdk/plugin.ts:356-379) or surface it as"oauth"with a distinguishing field.mintGithubAppInstallationTokenagainst the@executor-js/emulateGitHubemulator (wire-level: JWT signing + installations endpoint), the
performTokenRefreshgithub_appbranch, and an integration test that anear-expiry connection re-mints on resolve. Run scoped via
vitest run;gate with
format:check/lint/typecheck/test.Impact
secret-storage paths are reused unchanged. Only one new grant branch and one
new helper. No new schema columns (
grantandprovider_stateare reused).signatures verified first).
Optional follow-up
GET /app/installations(signed with the App JWT) so a user can pick one,instead of entering the installation id manually.
Decisions (confirmed)
connection.provider_state.jose(added to core/sdk), notnode:crypto, for edgecompatibility.