The GraphQL schema is code-first, generated to `apps/api/github.schema.gql` and committed. A test checks every type, field, argument and enum value in it against `@octokit/graphql-schema`, GitHub's published schema.
`/api/v3/meta` reports GitHub Enterprise Server `3.17.0`. Introspection and GraphiQL are on in every environment. The REST routes appear in the OpenAPI document under the `
API keys store GitHub scopes in `permissions.scopes`. A key without them, made in Ghost's settings, and a browser session hold every scope. `X-OAuth-Scopes` reports what the key holds.
Organizations own apps too, registered through `/api/organizations/:slug/oauth-apps` by its admins and owners, and then managed at `/api/oauth-apps/:clientId` like a user's. An organization's app has the organization's id in the plugin's
`oauth_client.metadata.verified` marks an app the instance vouches for, shown as a badge on the approval page. Only an operator sets it, in the database or the seed; no API writes it. The seed sets it on `gh`, and adds it to an existing `
`GET /login/oauth/authorize` checks the app and GitHub's callback rule (same scheme, host and port, a path equal to the callback's or under it) before anything redirects, then hands the request to the plugin, which checks the session and the stored consent and sends the user to `<web>/login/oauth/authorize`. The plugin reads an empty `
- A Ghost-owned registry (two tables, PKCE as one hash compare): smaller, but the plugin is kept so Ghost can later act as an OIDC provider without a second migration.
- Handing out the plugin's access tokens: they authenticate nothing in the compat layer, and would be a second token type beside API keys.
`
, on the API host. Users point the GitHub CLI at it with
`
GH_HOST=api.<domain>
`
. Every resolver and controller calls the existing Ghost services, so permissions, numbering, events and notifications stay where they are.
to<GitHubType>
`
mappers are allowed there and nowhere else. Each one is a last resort and carries a one-line comment saying why a query could not select GitHub's shape directly.
O_
`
,
`
R_
`
,
`
I_
`
,
`
PR_
`
,
`
IC_
`
,
`
LA_
`
.
GitHub compatibility
`
tag.
https://HOST/api/graphql
`
. The API host already serves
`
/api
`
and the git transport, so
`
gh repo clone
`
and git credentials resolve against the same host.
`
repo
`
covers
`
public_repo
`
,
`
admin:org
`
covers
`
write:org
`
and
`
read:org
`
, and so on). The compat GraphQL and REST APIs and the git transport all call it. Writes need
`
repo
`
, or
`
public_repo
`
on a public repository. A private repository needs
`
repo
`
, and answers as missing without it, so a token cannot learn that it exists.
`
plugin runs it.
`
apps/api/src/github/oauth/
`
serves it at GitHub's paths on the API host root,
`
/login/device/code
`
and
`
/login/oauth/access_token
`
, in GitHub's wire format. The approval page is
`
<web>/device
`
.
GitHub CLI
`
, holding the requested scopes Ghost knows. The session the plugin creates is deleted at once. API keys never authenticate Ghost's own
`
/api
`
, only the compat layer and git.
gh
`
, client id
`
178c6fc778ccc68e1d6a
`
, is a built-in app: no owner, no secret, device flow on.
`
seedBuiltInRows
`
in
`
packages/db/src/seed.ts
`
inserts it from code after migrating (the deploy migrator, the e2e setup, and
`
seed-github-cli-app
`
for a database migrated with drizzle-kit), not a migration. Users register their own apps through
`
/api/oauth-apps
`
; the service writes through the plugin so secrets are hashed its way, and refuses to change a built-in app. An app's device-flow switch is
`
oauth_client.metadata.deviceFlow
`
, which the device flow's
`
validateClient
`
reads.
`
oauth_client.referenceId
`
and no
`
userId
`
. The plugin fills
`
referenceId
`
from its
`
clientReference
`
hook, and only lets a call whose
`
clientReference
`
matches change the app. The hook reads
`
oauthAppOrganization
`
, an
`
AsyncLocalStorage
`
in
`
apps/api/src/lib/github/oauth-apps.ts
`
: the service runs each plugin call for an organization's app inside it, after checking that the user administers that organization. Outside it the hook returns nothing, and apps belong to users. A built-in app is one with neither
`
userId
`
nor
`
referenceId
`
.
`
referenceId
`
has no foreign key, so
`
afterDeleteOrganization
`
deletes the organization's apps and the keys they minted, and
`
onOrganizationDeleted
`
removes their logos.
gh
`
row without touching its other fields.
/oauth2/consent
`
and
`
/oauth2/continue
`
, which a browser must reach. Ghost calls the rest server-side through
`
auth.api
`
. The OIDC routes (
`
userinfo
`
,
`
register
`
,
`
end-session
`
, discovery) stay blocked until Ghost offers "Sign in with Ghost".
scope
`
as every scope, so a request with no known scope asks for the sentinel scope
`
public
`
, which
`
grantableScopes
`
drops.
grant_type
`
. The plugin checks the secret, the code, its
`
redirect_uri
`
and PKCE. Ghost reads the token's user through introspection, revokes the token, and mints a
`
ghost_pat_
`
key named after the app with
`
metadata.oauthClientId
`
, exactly as for the device flow. Revoking an authorized app deletes the user's keys whose