Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
node_modules
**/node_modules
dist
**/dist
artifacts
.superpowers
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A GitHub-only fork of [Vercel Labs emulate](https://github.com/vercel-labs/emulate) for local integration tests. It retains the upstream GitHub API and shared core, with a standalone library, CLI, and localhost demo. Licensed under Apache-2.0 with upstream attribution and third-party notices preserved.

This baseline demonstrates OAuth App sign-in, GitHub App tokens for a seeded installation, API calls, and signed webhook delivery. Browser-driven GitHub App installation is the next iteration. See the [roadmap](docs/roadmap.md) for the remaining flow gaps.
The emulator supports OAuth App sign-in, browser GitHub App installation with account and repository selection, installation-token API calls, and signed webhook delivery. Installation forms have bounded, expiring storage, and invalid setup callbacks fall back to a local settings URL. See [GitHub App installation](docs/github-app-installation.md) for the browser flow and discovery endpoints, and the [roadmap](docs/roadmap.md) for remaining gaps.

## Run the demo

Expand Down Expand Up @@ -108,7 +108,7 @@ The root build script keeps esbuild's working directory in the system temporary

## Upstream contributions

`main` keeps upstream history and layout. The default `github-only` branch contains this reduced edition. Upstream GitHub/core source is unchanged in the baseline; wrapper and packaging changes are kept separate from later protocol improvements. Follow the [contribution procedure](docs/upstream-workflow.md) to propose behavior-only patches against Vercel's full repository after tests prove them.
`main` keeps upstream history and layout. The default `github-only` branch contains this reduced edition. Protocol improvements and their regression tests stay in the preserved upstream package paths, separate from wrapper and packaging changes. Follow the [contribution procedure](docs/upstream-workflow.md) to propose behavior-only patches against Vercel's full repository after tests prove them.

Retained packages:

Expand Down
42 changes: 42 additions & 0 deletions docs/github-app-installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# GitHub App installation

Seed a GitHub App with an optional `setup_url` to exercise browser installation after OAuth sign-in. The existing demo uses a seeded installation; use a consuming application or browser test to drive this flow.

```json
{
"github": {
"users": [{ "login": "developer" }],
"orgs": [{ "login": "example-org", "members": [{ "login": "developer", "role": "admin" }] }],
"repos": [{ "owner": "example-org", "name": "widget", "private": true }],
"apps": [{
"app_id": 12345,
"slug": "example-app",
"name": "Example App",
"setup_url": "http://localhost:3005/github/setup",
"webhook_url": "http://localhost:3005/webhooks",
"webhook_secret": "local-webhook-secret",
"permissions": { "contents": "read", "metadata": "read" },
"events": ["installation", "issues", "pull_request"],
"installations": []
}]
}
}
```

The standalone wrapper generates omitted App keys. Direct `seedFromConfig` callers must supply a key or materialize the configuration first. Callback and webhook addresses must be reachable from their respective browser and emulator processes.

1. Complete OAuth sign-in through `/login/oauth/authorize` and `/login/oauth/callback`. The callback establishes the `_emu_session` browser cookie.
2. Open `/apps/example-app/installations/new?state=YOUR_OPAQUE_STATE` in that browser. The picker offers the signed-in user's own account and organizations where that user has the `admin` role.
3. Choose **Install on example-org**. Select **All repositories** or **Only select repositories**, then check the desired repositories and choose **Install**. Repository checkboxes have accessible names such as **Select repository: example-org/widget**.
4. The emulator creates the installation and dispatches an `installation` webhook with action `created`, installation/account metadata, selected repositories, and the sender. App webhook delivery uses the configured secret for `X-Hub-Signature-256` and is best effort.
5. The browser returns to the configured `setup_url` with `installation_id`, `setup_action=install`, and the original `state`. Existing callback query parameters are retained. Query or form fields cannot override the callback URL. The callback must be an absolute HTTP(S) URL. An omitted, malformed, relative, or non-HTTP(S) callback uses the installation's local settings URL; an installation settings UI is not implemented. The redirect is resolved before saving the installation.

Installation forms expire after 10 minutes, are bound to their user, App, and account, and can be submitted once. Pending forms are limited to 32 per user across accounts and 1024 per emulator instance. Reaching either limit evicts the oldest applicable form. Reopen the installation page after expiry or eviction. The server rechecks membership and repository ownership at submission, rejects empty selected sets and cross-account IDs with `422`, and rejects a duplicate App/account installation with `409`. Form sessions and pending forms are in memory and reset with the emulator.

## Discover memberships and installed repositories

`GET /user/memberships/orgs?state=active&per_page=100` requires a user token with `read:org`, `write:org`, or `admin:org`. Responses include `state: "active"`, `role: "admin" | "member"`, and organization metadata. The emulator has active memberships only; requesting `state=pending` returns an empty list. Installation tokens cannot call this endpoint. OAuth-authenticated `GET /user` and `PATCH /user` report the token's granted scopes in `X-OAuth-Scopes`.

Use an App JWT for `GET /repositories/:id/installation`. It returns only an installation belonging to that App whose current selection includes the repository, or `404`.

Exchange an App JWT at `POST /app/installations/:installation_id/access_tokens`, then call `GET /installation/repositories` with the resulting installation token. The response contains `total_count` and full `repositories` metadata. Results must belong to the installation account and satisfy both the token's requested scope and the current installation selection. Revoked or suspended installations are rejected. Membership and repository listings use `page`, `per_page` (up to 100), and `Link` headers.
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

Make Vercel's existing GitHub emulator runnable as a GitHub-only fork. Prove OAuth App sign-in, a GitHub App installation-token exchange against a seeded installation, authenticated API changes, and signed webhook reception over real localhost HTTP.

This baseline does not provide browser-driven GitHub App installation. It does not claim complete GitHub OAuth or API compatibility. Keep the remaining gaps visible while the working subset is validated.
The follow-up implementation adds [browser GitHub App installation](github-app-installation.md), setup callbacks, signed creation webhooks, and scoped repository discovery. Complete GitHub OAuth and API compatibility remain outside the verified subset.

## Next behavior to prove

Exercise the application's full connection journey: user authorization, account and repository selection, installation setup callback, discovery of installations/repositories, and subsequent API access and webhooks. Add focused regression tests before changes, and keep behavior commits suitable for upstream review. Check existing upstream PR #265 and issue #328 before duplicating installation-discovery work.
Verify the supported connection journey in consuming applications: OAuth user authorization, account and repository selection, setup callback, scoped discovery, subsequent API access, and signed webhooks. The protocol regression suite covers these emulator contracts. Keep behavior commits suitable for upstream review.

## State consistency

Expand Down
27 changes: 25 additions & 2 deletions docs/verification.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Baseline verification
# Verification

## Original baseline

Verified on 2026-11-03 with Node 24.21.0 and pnpm 11.2.2.

Expand All @@ -18,4 +20,25 @@ The retained `packages/@emulators/core` and `packages/@emulators/github` trees h

A browser run also completed sign-in as `octocat`, returned through the OAuth callback, created an issue using the seeded installation, and displayed one signature-verified webhook. The local demonstration used `127.0.0.98:3005` because another application owned `127.0.0.1:3005`.

These checks establish the baseline's existing seeded-installation flow. They do not establish browser-driven GitHub App installation, SQLite behavior, native Git transport, or complete GitHub protocol fidelity. Those remain roadmap work.
These checks establish the baseline's existing seeded-installation flow. They do not establish browser-driven GitHub App installation, SQLite behavior, native Git transport, or complete GitHub protocol fidelity. The follow-up checks below cover the added installation contracts. SQLite and native Git transport remain roadmap work.


## Browser installation follow-up

Verified on 2026-11-03 with Node 24.21.0 and pnpm 11.2.2.

| Check | Result |
| --- | --- |
| `pnpm build` | Pass for core, GitHub, wrapper, CLI, and declarations |
| `pnpm type-check` | Pass for all three packages |
| `pnpm format:check` | Pass |
| `pnpm lint` | Pass with 17 existing warnings and zero errors |
| `pnpm test` | 266 tests pass: core 103, GitHub 157, wrapper 6 |
| `pnpm test:smoke` | Pass for the original seeded-installation HTTP flow |
| `node scripts/package-smoke.mjs` | Pass for an external consumer of the packed package |

The new route tests first failed on missing endpoints or missing OAuth scope headers. The passing suite covers browser session reuse, authorized account selection, repeated repository checkboxes, opaque setup state, configured callback ownership, nonce replay and user/App/account binding, current membership checks, duplicate installation rejection, and repository ownership. An actual local HTTP receiver verifies the `installation.created` payload and HMAC signature from the existing App webhook dispatcher.

API regression tests cover paginated membership roles and scope enforcement, granted OAuth scope headers, full installation repository metadata, token/current-installation scope intersection, revoked and suspended installations, and App JWT ownership for repository installation lookup. These tests exercise emulator routes and real webhook delivery; the consuming application's complete browser journey requires its own end-to-end test.

The review follow-up reproduced 13 failing cases before adding pending-form capacity limits and safe setup-callback fallback. All 23 installation-flow tests now pass, including oldest-form eviction, retained neighboring forms, user isolation, expiry, malformed and non-HTTP callbacks, preserved opaque state, and exactly one signed webhook after installation. The built CLI help reports the verified form limits and callback behavior.
2 changes: 1 addition & 1 deletion examples/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ try {
assert.equal(event.payload.installation.id, 100);

console.log("PASS: OAuth redirects, user identity, App JWT, installation token, API mutation, and verified webhook agree.");
console.log("Browser GitHub App installation remains a separate, unimplemented flow.");
console.log("Browser GitHub App installation is covered separately by the protocol regression tests.");
} finally {
await demo.close();
}
16 changes: 15 additions & 1 deletion packages/@emulators/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ npm install @emulators/github
### Users
- `GET /user` — authenticated user
- `PATCH /user` — update profile
- `GET /user/memberships/orgs` returns active organization memberships with admin/member roles, `state` filtering, and pagination. Requires `read:org`, `write:org`, or `admin:org`.
- OAuth-authenticated `GET /user` and `PATCH /user` include the granted `X-OAuth-Scopes` header.
- `GET /users/:username` — get user
- `GET /users` — list users
- `GET /users/:username/repos` — list user repos
Expand Down Expand Up @@ -131,6 +133,15 @@ npm install @emulators/github
- Check suites: create, get, preferences, rerequest, list by ref. Ref based lookups accept branch and tag refs containing slashes.
- Automatic suite status rollup from check run results

### GitHub Apps

- `GET /apps/:slug/installations/new` displays account and repository selection using the browser session established by OAuth sign-in.
- `POST /apps/:slug/installations/new` creates an installation, dispatches `installation.created`, and redirects to the App's configured `setup_url`.
- `GET /installation/repositories` returns paginated repository metadata for an installation token, intersected with the current installation scope.
- `GET /repositories/:id/installation` finds the current installation for the authenticated App JWT.

See [GitHub App installation](../../../docs/github-app-installation.md) for configuration and callback behavior.

### Misc
- `GET /_emulate/installation-tokens` — inspect secret-free GitHub App installation-token metadata
- `GET /rate_limit` — rate limit status
Expand Down Expand Up @@ -182,7 +193,10 @@ github:
permissions:
contents: read
issues: write
events: [push, pull_request]
events: [installation, push, pull_request]
setup_url: "http://localhost:3005/github/setup"
webhook_url: "http://localhost:3005/webhooks"
webhook_secret: "local-webhook-secret"
installations:
- installation_id: 100
account: my-org
Expand Down
Loading
Loading