Emulate Seed
Generate emulate seed configs for stateful API emulation. Wraps Vercel's emulate tool for GitHub, Vercel, Google OAuth, Slack, Apple Auth, Microsoft Entra, AWS (S3/SQS/IAM), Okta, Clerk, Resend, Stripe, and MongoDB Atlas APIs. Not mocks — full state machines where create-a-PR-and-it-appears-in-the-list, send-an-email-and-retrieve-from-local-inbox. Use when setting up test environments, CI pipelines, integration tests, or offline development.
Auto-activated — this skill loads automatically when Claude detects matching context.
Emulate Seed Configs
Generate and manage seed configs for emulate (Apache-2.0) — Vercel Labs' stateful API emulation tool. Each category has individual rule files in rules/ loaded on-demand.
Paired agent: This skill pairs with the
emulate-engineersubagent (subagent_type: "emulate-engineer"). When a task involves generating a full emulate config from scratch, webhook HMAC setup, CI pipeline integration, or parallel-worker port isolation, spawn the agent rather than handling it inline — it has the full 12-emulator service-port matrix and seed-rules in context.
Not mocks. Emulate provides full state machines with cascading deletes, cursor pagination, webhook delivery, and HMAC signature verification. Create a PR via the API and it appears in GET /repos/:owner/:repo/pulls. Delete a repo and its issues, PRs, and webhooks cascade-delete.
New in 2026-04 (emulate 0.4.x)
- Modular
@emulators/*packages — each service is its own package (@emulators/github,@emulators/stripe, etc.); top-levelemulatere-exportscreateEmulatorand the CLI. - 4 new services (12 total):
mongoatlas:4007,okta:4008,resend:4009,stripe:4010with drop-in seed YAML blocks. - Resend local inbox —
GET http://localhost:4009/inboxreturns captured emails for assertions without hitting a real provider. - Stripe hosted checkout — real session redirect flow +
checkout.session.completed/expiredwebhook delivery, suitable for E2E payment tests. - MongoDB Atlas — Admin API v2 (projects/clusters/DB users) + Data API v1 with full CRUD + aggregate.
- Okta OIDC — full discovery, JWKS,
authorize/token/userinfo/revoke/introspectplus Users/Groups/Apps CRUD. - Entra / Apple / Slack expansions (v0.4.0) — PKCE + refresh rotation (Entra), RS256 JWKS (Apple), OAuth v2 consent UI (Slack).
@emulators/adapter-next— catch-all Next.js route handler runs emulators on the same origin as the app; fixes OAuth callback URL drift on Vercel preview deploys.
Auto-Discovery (M125 #4)
scripts/auto-discover.sh scans the project's package.json, matches deps against references/dep-to-emulator-map.json, and either reports the matches or writes emulate.config.yaml. Three modes:
| Mode | Behavior |
|---|---|
| (default) | Report matched deps + emulator union on stderr; do not write |
--json | Emit machine-readable JSON instead of human report |
--apply | Write emulate.config.yaml (refuses to overwrite without --force) |
$ bash scripts/auto-discover.sh
/ork:emulate-seed --auto — scanning /path/to/package.json
Detected:
@octokit/rest → github · Any GitHub API client
next-auth → google-oauth, apple-auth, microsoft-entra · Default OAuth providers
stripe → stripe
@vercel/blob → aws · @vercel/blob is S3-compatible
Union: apple-auth, aws, github, google-oauth, microsoft-entra, stripe
$ bash scripts/auto-discover.sh --apply
…
✓ Wrote /path/to/emulate.config.yaml with 6 service(s)Multi-emulator deps default to all reasonable providers; the user prunes the YAML afterwards. Unmapped deps are silently skipped — extending coverage is a docs PR (edit references/dep-to-emulator-map.json), not a code change.
/ork:dev reads the resulting emulate.config.yaml at boot — see src/skills/dev/scripts/boot.sh.
Quick Reference
| Category | Rules | Impact | When to Use |
|---|---|---|---|
| Seed Config | 1 | HIGH | Setting up emulate.config.yaml for test environments |
| Service Selection | 1 | MEDIUM | Choosing GitHub/Vercel/Google for your tests |
| Webhook Setup | 1 | MEDIUM | Testing webhook delivery with HMAC verification |
| Parallel CI | 1 | HIGH | Running tests in parallel without port collisions |
| Auth Tokens | 1 | MEDIUM | Seeding tokens mapped to emulated users |
Total: 5 rules across 5 categories
Quick Start
# Install (packages published under @emulators/* scope)
npm install --save-dev emulate
# Start all services
npx emulate
# Start specific services with seed data
npx emulate --service github,stripe --seed ./emulate.config.yaml
# Generate a starter config
npx emulate init --service githubServices (0.5.0 — 13 emulators)
New in 0.5.0 (Apr 2026): Clerk emulator (auth/sessions), portless integration (embedded emulators without dedicated ports), Google OAuth
hdclaim support, Stripe Checkout + Resend magic link examples, AWS S3 emulator now matches the official SDK wire format. Backwards-compatible.
| Service | Default Port | Coverage |
|---|---|---|
| Vercel | :4000 | Projects, deployments, domains, env vars, teams |
| GitHub | :4001 | Repos, PRs, issues, comments, reviews, Actions, webhooks, orgs, teams |
| Google OAuth | :4002 | OAuth 2.0 authorize, token exchange, userinfo |
| Slack | :4003 | Chat, conversations, users, reactions, OAuth v2 with consent UI |
| Apple Auth | :4004 | Sign in with Apple — OIDC discovery, JWKS (RS256), auth flow, token exchange |
| Microsoft Entra | :4005 | OAuth 2.0/OIDC v2.0, authorization code + PKCE, refresh token rotation, v1 token endpoint, Graph /users/\{id\} |
| AWS | :4006 | S3 buckets, SQS queues, IAM users/roles, STS identity |
| MongoDB Atlas (0.4+) | :4007 | Admin API v2 (projects, clusters, DB users) + Data API v1 (full CRUD + aggregate) |
| Okta (0.4+) | :4008 | OIDC discovery, JWKS, authorize/token/userinfo/revoke/introspect, Users/Groups/Apps CRUD |
| Resend (0.4+) | :4009 | Send + batch (100/req), list/retrieve/cancel, domains, API keys, audiences, contacts, local inbox (GET /inbox) |
| Stripe (0.4+) | :4010 | Customers, payment methods, customer sessions, payment intents, charges, products, prices, hosted checkout session w/ webhook delivery |
| Clerk | (on-demand) | Users, sessions, organizations |
See references/api-coverage.md for full endpoint lists.
Next.js Adapter (0.4+) — @emulators/adapter-next
Runs emulators on the same origin as your Next.js app via a catch-all route handler. Fixes the OAuth callback URL drift problem on Vercel preview deploys — no more http://localhost:4001 redirect mismatches.
// next.config.js
const { withEmulate } = require('@emulators/adapter-next')
module.exports = withEmulate({ /* your next config */ })
// app/api/[...emulate]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
export const { GET, POST } = createEmulateHandler({
services: ['github', 'stripe', 'resend'],
persistence: { /* load(), save() or built-in filePersistence */ },
})Seed Config Structure
A seed config pre-populates the emulator with tokens, users, repos, and projects so tests start from a known state.
# emulate.config.yaml
tokens:
dev_token:
login: yonatangross
scopes: [repo, workflow, admin:org]
ci_token:
login: ci-bot
scopes: [repo]
github:
users:
- login: yonatangross
name: Yonatan Gross
- login: ci-bot
name: CI Bot
repos:
- owner: yonatangross
name: my-project
private: false
default_branch: main
topics: [typescript, testing]
vercel:
users:
- username: yonatangross
email: yonaigross@gmail.com
projects:
- name: my-docs
framework: next
# NEW in 0.4.x — drop-in seed blocks
okta:
users:
- login: alice@example.com
firstName: Alice
lastName: Smith
groups: [{ name: Everyone }, { name: Admins }]
apps: [{ name: My Web App }]
authorization_servers:
- name: default
audiences: ["api://default"]
resend:
domains: [{ name: example.com }]
api_keys: [{ name: default }]
# In tests: GET http://localhost:4009/inbox to assert captured emails
stripe:
customers:
- name: Test Customer
email: customer@example.com
products: [{ name: Pro Plan }, { name: Starter Plan }]
prices:
- { product: Pro Plan, unit_amount: 4900, currency: usd, recurring: { interval: month } }
- { product: Starter Plan, unit_amount: 1900, currency: usd, recurring: { interval: month } }
# Webhook delivery fires on checkout.session.completed / expired
mongoatlas:
projects: [{ name: my-project }]
clusters: [{ project: my-project, name: my-cluster }]
database_users: [{ project: my-project, username: app-user }]See rules/seed-config.md for full schema and best practices.
Programmatic SDK
Service packages live under the
@emulators/*scope (e.g.,@emulators/github,@emulators/stripe). The programmatic API (createEmulator) is exported from the top-levelemulatepackage.
import { createEmulator } from 'emulate'
const github = await createEmulator({ service: 'github', port: 4001 })
// github.url -> 'http://localhost:4001'
// State is real — create a PR and it appears in the list
const res = await fetch(`${github.url}/repos/org/repo/pulls`, {
method: 'POST',
headers: { Authorization: 'Bearer dev_token' },
body: JSON.stringify({ title: 'Test PR', head: 'feature', base: 'main' })
})
const prs = await fetch(`${github.url}/repos/org/repo/pulls`)
// -> includes the PR we just created
// Cleanup
github.reset() // Synchronous state wipe
await github.close() // Shut down serverSee references/sdk-patterns.md for advanced patterns (multi-service, lifecycle hooks).
Webhook Delivery
Emulate delivers real webhooks with HMAC-SHA256 signatures when state changes:
import crypto from 'crypto'
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}See rules/webhook-setup.md for webhook receiver patterns.
CI Integration
# .github/workflows/test.yml
jobs:
test:
steps:
- uses: actions/checkout@v4
- name: Start emulate
run: npx emulate --service github --seed .emulate/ci.yaml &
- name: Wait for emulate
run: sleep 2
- name: Run tests
run: npm test
env:
GITHUB_API_BASE: http://localhost:4001
VERCEL_API_BASE: http://localhost:4000Parallel Test Execution
Each test worker gets its own port to avoid race conditions:
// vitest.config.ts
const workerPort = 4001 + parseInt(process.env.VITEST_WORKER_ID || '0')See rules/parallel-ci.md for full parallel isolation patterns.
Decision Matrix
| Tool | When to Use | Stateful? | Platforms |
|---|---|---|---|
| emulate (FIRST CHOICE) | GitHub/Vercel/Google/Slack/Apple/Entra/AWS/Okta/Resend/Stripe/MongoDB testing | YES | All 12 services |
| Pact | Contract verification between services | No | Any |
| MSW | In-browser/Node HTTP mocking | No | Any |
| Nock | Node.js HTTP intercept | No | Any |
| WireMock | HTTP stub server | Partial | Any |
Use emulate when:
- Testing code that calls GitHub, Vercel, Google, Slack, Apple, Entra, AWS, Okta, Resend, Stripe, or MongoDB Atlas
- You need state persistence across multiple API calls in a test
- You want webhook delivery with real HMAC signatures (GitHub, Stripe)
- You need cascading side-effects (delete repo -> PRs cascade-delete)
- You need to assert on sent emails without hitting a real provider (Resend local
/inbox) - You need hosted Stripe checkout sessions with real redirect flow in tests
Use MSW/Nock when:
- Mocking arbitrary HTTP APIs not covered by emulate
- You need in-browser interception (MSW)
- Tests only need single request/response pairs
Related Skills
testing-integration— Integration test patterns (emulate as first choice for API tests)testing-e2e— End-to-end test patterns with emulated backendstesting-unit— Unit test patterns (use emulate for API-dependent units)security-patterns— Auth token patterns (emulate token seeding)
CLI Reference
See references/cli-reference.md for all CLI flags and commands.
SDK Patterns
See references/sdk-patterns.md for programmatic createEmulate() usage.
Rules (5)
Auth Tokens: Token Seeding and Credential Hygiene — MEDIUM
Auth Token Seeding
Emulate maps token strings to emulated users with configurable scopes. Tests use these seeded token names as Bearer tokens — no real credentials involved.
Token Configuration
# emulate.config.yaml
tokens:
admin_token:
login: admin-user
scopes: [repo, admin:org, workflow, delete_repo]
dev_token:
login: dev-user
scopes: [repo, workflow]
readonly_token:
login: viewer
scopes: [repo:read]
ci_token:
login: ci-bot
scopes: [repo, actions]Using Seeded Tokens
// The token name IS the Bearer value
const adminRes = await fetch(`${GITHUB_API_BASE}/orgs/my-org/repos`, {
method: 'POST',
headers: { Authorization: 'Bearer admin_token' },
body: JSON.stringify({ name: 'new-repo' })
})
// -> 201 Created (admin_token has admin:org scope)
const readonlyRes = await fetch(`${GITHUB_API_BASE}/orgs/my-org/repos`, {
method: 'POST',
headers: { Authorization: 'Bearer readonly_token' },
body: JSON.stringify({ name: 'new-repo' })
})
// -> 403 Forbidden (readonly_token lacks admin:org scope)Incorrect — using real tokens in test configs:
# BAD: real GitHub PAT in test config — leaks if committed
tokens:
my_token:
login: yonatangross
value: ghp_abc123realtoken456 # NEVER put real tokens here// BAD: hardcoded real token in test file
headers: { Authorization: 'Bearer ghp_abc123realtoken456' }Correct — descriptive seeded token names:
# GOOD: token name is the bearer value, no real credentials
tokens:
test_admin:
login: admin-user
scopes: [repo, admin:org]// GOOD: seeded token name as bearer value
headers: { Authorization: 'Bearer test_admin' }Permission Testing Pattern
describe('permission checks', () => {
it('admin can delete repos', async () => {
const res = await fetch(`${GITHUB_API_BASE}/repos/org/repo`, {
method: 'DELETE',
headers: { Authorization: 'Bearer admin_token' }
})
expect(res.status).toBe(204)
})
it('readonly user cannot delete repos', async () => {
const res = await fetch(`${GITHUB_API_BASE}/repos/org/repo`, {
method: 'DELETE',
headers: { Authorization: 'Bearer readonly_token' }
})
expect(res.status).toBe(403)
})
})Key rules:
- Token names in the config are the literal Bearer strings used in API requests
- Never put real GitHub PATs, Vercel tokens, or Google credentials in seed configs
- Use descriptive token names:
admin_token,ci_token,readonly_token - Map each token to a user login defined in the same config
- Test permission boundaries by creating tokens with different scope sets
- Keep token configs in committed files for reproducibility — they contain no secrets
Reference: rules/seed-config.md
Parallel CI: Per-Worker Port Isolation — HIGH
Parallel CI Port Isolation
When running tests in parallel (Vitest, Jest workers, CI matrix), each worker needs its own emulator instance on a unique port to prevent shared state and race conditions.
Per-Worker Port Offset
// vitest.setup.ts — each worker gets a unique port
import { createEmulate } from '@emulators/emulate'
import type { Emulator } from '@emulators/emulate'
let github: Emulator
const BASE_PORT = 4001
const workerPort = BASE_PORT + parseInt(process.env.VITEST_WORKER_ID || '0')
beforeAll(async () => {
github = await createEmulate({
service: 'github',
port: workerPort,
seed: './emulate.config.yaml'
})
process.env.GITHUB_API_BASE = github.url
})
afterAll(async () => {
await github.close()
})
beforeEach(() => {
github.reset() // Wipe state between tests, keep server running
})Jest Worker Isolation
// jest.setup.ts
const workerPort = 4001 + parseInt(process.env.JEST_WORKER_ID || '0')Incorrect — all workers hitting the same port:
// BAD: shared port causes race conditions
const github = await createEmulate({ service: 'github', port: 4001 })
// Worker 1 creates a PR, Worker 2 sees it — non-deterministicCorrect — per-worker port isolation:
// GOOD: each worker has isolated state
const workerPort = 4001 + parseInt(process.env.VITEST_WORKER_ID || '0')
const github = await createEmulate({ service: 'github', port: workerPort })
// Worker 1 on :4002, Worker 2 on :4003 — fully isolatedCI Matrix Isolation
# .github/workflows/test.yml
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- name: Start emulate
run: |
PORT_OFFSET=$((4001 + ${{ matrix.shard }} * 100))
npx emulate --service github --port $PORT_OFFSET --seed .emulate/ci.yaml &
echo "GITHUB_API_BASE=http://localhost:$PORT_OFFSET" >> $GITHUB_ENV
- name: Wait for emulate
run: sleep 2
- name: Run tests
run: npm test -- --shard=${{ matrix.shard }}/4Key rules:
- Compute port as
BASE_PORT + worker_idto guarantee uniqueness per worker - Create a fresh emulator instance per worker in
beforeAll, close inafterAll - Use
github.reset()inbeforeEachto wipe state between tests within a worker - In CI matrix builds, use shard index with a multiplier (e.g.,
* 100) to avoid port overlap between shards - Never rely on a single shared emulator instance for parallel test execution
- Always set
GITHUB_API_BASEper worker so test code uses the correct port
Reference: references/sdk-patterns.md
Seed Config: emulate.config.yaml Structure — HIGH
Seed Config Structure
The emulate.config.yaml file pre-populates the emulator with tokens, users, repos, and projects so every test starts from a known, reproducible state.
Config Sections
# emulate.config.yaml — full structure
tokens:
dev_token:
login: dev-user # Maps this token string to a user
scopes: [repo, workflow, admin:org]
read_only_token:
login: reader
scopes: [repo:read]
github:
users:
- login: dev-user
name: Developer
- login: reader
name: Read-Only User
orgs:
- login: my-org
name: My Organization
repos:
- owner: my-org
name: backend
private: false
default_branch: main
topics: [api, typescript]
- owner: dev-user
name: side-project
private: true
default_branch: main
vercel:
users:
- username: dev-user
email: dev@example.com
projects:
- name: frontend
framework: next
- name: docs
framework: astro
google:
users:
- email: dev@example.com
name: DeveloperIncorrect — hardcoding tokens in test files:
// BAD: tokens scattered across test files, no shared state
const res = await fetch('http://localhost:4001/repos/org/repo', {
headers: { Authorization: 'Bearer ghp_realtoken123' }
})Correct — centralized seed config:
# .emulate/test.config.yaml
tokens:
test_admin:
login: admin-user
scopes: [repo, admin:org]// Tests reference seeded token names
const res = await fetch(`${GITHUB_API_BASE}/repos/org/repo`, {
headers: { Authorization: 'Bearer test_admin' }
})Key rules:
- Place configs in
.emulate/directory or project root - One config per environment:
ci.config.yaml,dev.config.yaml,test.config.yaml - Token names are the literal Bearer strings used in requests — keep them descriptive
- Every token must map to a user defined in the same config's users section
- Add
.emulate/to.gitignoreonly if it contains environment-specific overrides - Commit shared base configs (e.g.,
emulate.config.yaml) to the repo for reproducibility - Use
npx emulate init --service githubto generate a starter config
Reference: references/cli-reference.md
Service Selection: GitHub, Vercel, and Google — MEDIUM
Service Selection
Choose the right emulate service based on which APIs your code interacts with. Use emulate as the first choice whenever the target API is covered.
Service Defaults
| Service | Flag | Port | Use When |
|---|---|---|---|
| GitHub | --service github | :4001 | Repos, PRs, issues, reviews, Actions, webhooks, orgs |
| Vercel | --service vercel | :4000 | Projects, deployments, domains, env vars, teams |
| Google OAuth | --service google | :4002 | OAuth 2.0 flows, token exchange, userinfo |
Multi-Service
# Start GitHub + Vercel together
npx emulate --service github,vercel --seed ./emulate.config.yaml
# Custom ports
npx emulate --service github --port 5001Incorrect — writing manual GitHub API mocks when emulate covers it:
// BAD: hand-rolled mock that doesn't maintain state
const mockListPRs = jest.fn().mockResolvedValue([])
const mockCreatePR = jest.fn().mockResolvedValue({ number: 1 })
// After createPR, listPRs still returns [] — not statefulCorrect — use emulate for stateful GitHub API testing:
import { createEmulate } from '@emulators/emulate'
const github = await createEmulate({ service: 'github', port: 4001 })
// Create PR via API
await fetch(`${github.url}/repos/org/repo/pulls`, {
method: 'POST',
headers: { Authorization: 'Bearer dev_token' },
body: JSON.stringify({ title: 'Fix bug', head: 'fix', base: 'main' })
})
// PR now appears in list — state is real
const prs = await (await fetch(`${github.url}/repos/org/repo/pulls`)).json()
expect(prs).toHaveLength(1)
expect(prs[0].title).toBe('Fix bug')
await github.close()Key rules:
- Use
emulate --service githubwhenever testing GitHub API interactions — it covers repos, PRs, issues, comments, reviews, Actions, webhooks, orgs, and teams - Use
emulate --service vercelfor Vercel platform API testing — projects, deployments, domains, env vars - Use
emulate --service googlefor Google OAuth flows — authorize, token exchange, userinfo - Combine services with comma separation:
--service github,vercel - Fall back to MSW/Nock only for APIs emulate does not cover
- Custom ports via
--portwhen defaults conflict with existing services
Reference: references/api-coverage.md
Webhook Setup: HMAC Delivery and Verification — MEDIUM
Webhook Setup
Emulate delivers real webhooks with HMAC-SHA256 signatures when state changes occur (PR created, issue opened, deployment completed). Configure a webhook receiver in your tests to verify delivery and signatures.
Webhook Receiver Pattern
import http from 'http'
import crypto from 'crypto'
const WEBHOOK_SECRET = 'test-webhook-secret'
const receivedEvents: Array<{ event: string; payload: object }> = []
const webhookServer = http.createServer((req, res) => {
let body = ''
req.on('data', chunk => { body += chunk })
req.on('end', () => {
// Verify HMAC signature
const signature = req.headers['x-hub-signature-256'] as string
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
res.writeHead(401)
res.end('Invalid signature')
return
}
receivedEvents.push({
event: req.headers['x-github-event'] as string,
payload: JSON.parse(body)
})
res.writeHead(200)
res.end('OK')
})
})
webhookServer.listen(9876)Incorrect — skipping signature verification in tests:
// BAD: no signature check — production HMAC bugs go undetected
webhookServer.on('request', (req, res) => {
let body = ''
req.on('data', chunk => { body += chunk })
req.on('end', () => {
receivedEvents.push(JSON.parse(body)) // Just trust it
res.writeHead(200).end()
})
})Correct — verify HMAC even in tests:
// GOOD: same verification logic as production
const signature = req.headers['x-hub-signature-256'] as string
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
res.writeHead(401).end('Invalid signature')
return
}Key rules:
- Always verify HMAC-SHA256 signatures in test webhook receivers — mirrors production behavior
- Use
crypto.timingSafeEqualfor constant-time comparison to prevent timing attacks - Store the webhook secret in the seed config and share it with the receiver
- Check the
x-github-eventheader to route different event types - Clean up the webhook server in
afterAllto prevent port leaks - Emulate delivers webhooks on state mutations (create, update, delete) — not on reads
Reference: references/sdk-patterns.md
References (7)
Api Coverage
API Coverage
Full list of supported API endpoints per emulate service.
GitHub API (:4001)
Repositories
GET /repos/:owner/:repo— Get repositoryPOST /user/repos— Create user repositoryPOST /orgs/:org/repos— Create org repositoryPATCH /repos/:owner/:repo— Update repositoryDELETE /repos/:owner/:repo— Delete repository (cascading: PRs, issues, webhooks)GET /repos/:owner/:repo/topics— List topicsPUT /repos/:owner/:repo/topics— Replace topics
Pull Requests
GET /repos/:owner/:repo/pulls— List PRs (cursor pagination)POST /repos/:owner/:repo/pulls— Create PRGET /repos/:owner/:repo/pulls/:number— Get PRPATCH /repos/:owner/:repo/pulls/:number— Update PRPUT /repos/:owner/:repo/pulls/:number/merge— Merge PRGET /repos/:owner/:repo/pulls/:number/reviews— List reviewsPOST /repos/:owner/:repo/pulls/:number/reviews— Create reviewGET /repos/:owner/:repo/pulls/:number/comments— List review commentsPOST /repos/:owner/:repo/pulls/:number/comments— Create review comment
Issues
GET /repos/:owner/:repo/issues— List issuesPOST /repos/:owner/:repo/issues— Create issueGET /repos/:owner/:repo/issues/:number— Get issuePATCH /repos/:owner/:repo/issues/:number— Update issueGET /repos/:owner/:repo/issues/:number/comments— List commentsPOST /repos/:owner/:repo/issues/:number/comments— Create comment
Actions / Workflows
GET /repos/:owner/:repo/actions/workflows— List workflowsPOST /repos/:owner/:repo/actions/workflows/:id/dispatches— Trigger workflowGET /repos/:owner/:repo/actions/runs— List workflow runsGET /repos/:owner/:repo/actions/runs/:id— Get runGET /repos/:owner/:repo/actions/runs/:id/jobs— List jobs
Webhooks
GET /repos/:owner/:repo/hooks— List webhooksPOST /repos/:owner/:repo/hooks— Create webhookPATCH /repos/:owner/:repo/hooks/:id— Update webhookDELETE /repos/:owner/:repo/hooks/:id— Delete webhook
Organizations & Teams
GET /orgs/:org— Get organizationGET /orgs/:org/repos— List org reposGET /orgs/:org/teams— List teamsPOST /orgs/:org/teams— Create teamGET /orgs/:org/members— List members
Users
GET /user— Authenticated userGET /users/:username— Get userGET /users/:username/repos— List user repos
Vercel API (:4000)
Projects
GET /v9/projects— List projectsPOST /v9/projects— Create projectGET /v9/projects/:id— Get projectPATCH /v9/projects/:id— Update projectDELETE /v9/projects/:id— Delete project
Deployments
GET /v6/deployments— List deploymentsPOST /v13/deployments— Create deploymentGET /v13/deployments/:id— Get deploymentDELETE /v13/deployments/:id— Cancel deployment
Domains
GET /v5/domains— List domainsPOST /v5/domains— Add domainDELETE /v5/domains/:name— Remove domain
Environment Variables
GET /v9/projects/:id/env— List env varsPOST /v9/projects/:id/env— Create env varPATCH /v9/projects/:id/env/:envId— Update env varDELETE /v9/projects/:id/env/:envId— Delete env var
Teams
GET /v2/teams— List teamsPOST /v1/teams— Create teamGET /v2/teams/:id— Get team
Google OAuth (:4002)
OAuth 2.0
GET /o/oauth2/v2/auth— Authorization endpointPOST /oauth2/v4/token— Token exchangeGET /oauth2/v2/userinfo— User infoPOST /oauth2/revoke— Token revocation
Slack Web API (:4003)
Chat & Conversations
POST /api/chat.postMessage— Send messageGET /api/conversations.list— List conversationsGET /api/conversations.history— Get messages in channelPOST /api/reactions.add— Add emoji reactionGET /api/users.list— List workspace users
OAuth
GET /oauth/v2/authorize— OAuth v2 consent UIPOST /api/oauth.v2.access— Token exchange
Apple Authentication (:4004)
GET /.well-known/openid-configuration— OIDC discoveryGET /auth/keys— JWKS endpoint (RS256)GET /auth/authorize— Authorization flowPOST /auth/token— Token exchangePOST /auth/revoke— Token revocation
Microsoft Entra ID (:4005)
GET /\{tenant\}/v2.0/.well-known/openid-configuration— OIDC discoveryGET /\{tenant\}/oauth2/v2.0/authorize— Authorization code + PKCEPOST /\{tenant\}/oauth2/v2.0/token— Token exchange with refresh rotationGET /\{tenant\}/oauth2/v2.0/logout— Logout endpoint
AWS (:4006)
S3
PUT /\{bucket\}— Create bucketGET /— List bucketsPUT /\{bucket\}/\{key\}— Put objectGET /\{bucket\}/\{key\}— Get objectDELETE /\{bucket\}/\{key\}— Delete object
SQS
POST /— CreateQueue, SendMessage, ReceiveMessage, DeleteMessage
IAM & STS
POST /— CreateUser, CreateRole, GetCallerIdentity, AssumeRole
Stateful Behaviors
All services maintain full state:
- Cascading deletes: Delete a repo and its PRs, issues, and webhooks are removed
- Cursor pagination: List endpoints support
?per_page=N&page=Nand Link headers - Auto-incrementing IDs: PRs, issues, comments get sequential IDs
- Webhook delivery: State mutations trigger webhook POST to configured URLs with HMAC signatures
- Scope enforcement: Token scopes are checked — insufficient scopes return 403
Cli Reference
CLI Reference
All emulate CLI commands and flags.
Commands
emulate (default)
Start emulate services.
# Start all services with defaults
npx emulate
# Specific services
npx emulate --service github
npx emulate --service github,vercel
npx emulate --service github,vercel,google
# With seed data
npx emulate --seed ./emulate.config.yaml
npx emulate --service github --seed .emulate/dev.yaml
# Custom port (overrides default for first service)
npx emulate --service github --port 5001
# Combine flags
npx emulate --service github,vercel --port 3000 --seed ./config.yamlemulate init
Generate a starter seed config.
# Generate config for GitHub
npx emulate init --service github
# Generate config for all services
npx emulate init
# Output to specific file
npx emulate init --service github > .emulate/github.yamlemulate list
List available services and their default ports.
npx emulate list
# github :4001
# vercel :4000
# google :4002Flags
| Flag | Short | Default | Description |
|---|---|---|---|
--service | -s | all | Comma-separated services: github, vercel, google |
--port | -p | per-service | Override default port for the first listed service |
--seed | none | Path to YAML seed config file | |
--help | -h | Show help | |
--version | -v | Show version |
Port Defaults
When no --port is specified:
| Service | Default Port |
|---|---|
| Vercel | 4000 |
| GitHub | 4001 |
| 4002 |
When --port is specified with multiple services, ports increment from the given base:
npx emulate --service github,vercel --port 3000
# github -> :3000
# vercel -> :3001Environment Variables
Set these in your test runner or CI to redirect API calls to the emulator:
| Variable | Example | SDK/Tool |
|---|---|---|
GITHUB_API_BASE | http://localhost:4001 | Octokit, gh CLI |
VERCEL_API_BASE | http://localhost:4000 | Vercel SDK |
GOOGLE_OAUTH_BASE | http://localhost:4002 | Google Auth libraries |
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Clean shutdown |
| 1 | Config parse error or port conflict |
| 130 | Interrupted (Ctrl+C) |
Sdk Patterns
SDK Patterns
Programmatic usage of emulate via the createEmulate() API (v0.3.0+, @emulators/* scope).
Basic Usage
import { createEmulate } from '@emulators/emulate'
const github = await createEmulate({
service: 'github',
port: 4001,
seed: './emulate.config.yaml' // Optional seed file
})
console.log(github.url) // 'http://localhost:4001'
// Use the emulated API
const res = await fetch(`${github.url}/repos/org/repo`)
const repo = await res.json()
// Cleanup
github.reset() // Synchronous — wipes all state, keeps server running
await github.close() // Async — shuts down server and frees portMulti-Service Setup
import { createEmulate } from '@emulators/emulate'
const [github, vercel] = await Promise.all([
createEmulate({ service: 'github', port: 4001, seed: './config.yaml' }),
createEmulate({ service: 'vercel', port: 4000, seed: './config.yaml' }),
])
// Both share the same seed config — tokens, users, projects
const ghRes = await fetch(`${github.url}/repos/org/repo`)
const vcRes = await fetch(`${vercel.url}/v9/projects`)
// Cleanup both
await Promise.all([github.close(), vercel.close()])Test Framework Integration
Vitest
// vitest.setup.ts
import { createEmulate, type Emulator } from '@emulators/emulate'
let github: Emulator
beforeAll(async () => {
const workerPort = 4001 + parseInt(process.env.VITEST_WORKER_ID || '0')
github = await createEmulate({
service: 'github',
port: workerPort,
seed: '.emulate/test.yaml'
})
process.env.GITHUB_API_BASE = github.url
})
afterAll(async () => {
await github.close()
})
beforeEach(() => {
github.reset() // Fresh state per test
})Jest
// jest.setup.ts
import { createEmulate, type Emulator } from '@emulators/emulate'
let github: Emulator
beforeAll(async () => {
const workerPort = 4001 + parseInt(process.env.JEST_WORKER_ID || '0')
github = await createEmulate({
service: 'github',
port: workerPort,
seed: '.emulate/test.yaml'
})
process.env.GITHUB_API_BASE = github.url
})
afterAll(async () => {
await github.close()
})
beforeEach(() => {
github.reset()
})Emulator API
createEmulate(options)
Creates and starts an emulator instance.
interface EmulatorOptions {
service: 'github' | 'vercel' | 'google'
port?: number // Default: 4001 (github), 4000 (vercel), 4002 (google)
seed?: string // Path to YAML seed config
}
const emulator: Emulator = await createEmulate(options)emulator.url
The base URL of the running emulator.
github.url // 'http://localhost:4001'emulator.reset()
Synchronously wipes all state and re-applies the seed config. The server stays running — useful for resetting between tests without the overhead of restarting.
github.reset() // Instant — no await neededemulator.close()
Asynchronously shuts down the server and frees the port. Call in afterAll.
await github.close()URL Patterns
When using emulate with existing SDKs, set the base URL:
Octokit
import { Octokit } from '@octokit/rest'
const octokit = new Octokit({
baseUrl: process.env.GITHUB_API_BASE || 'https://api.github.com',
auth: 'dev_token' // Seeded token name
})
const { data: repos } = await octokit.repos.listForOrg({ org: 'my-org' })Vercel SDK
import { Vercel } from '@vercel/sdk'
const vercel = new Vercel({
baseUrl: process.env.VERCEL_API_BASE || 'https://api.vercel.com',
bearerToken: 'dev_token'
})fetch
const base = process.env.GITHUB_API_BASE || 'https://api.github.com'
const res = await fetch(`${base}/repos/org/repo/pulls`, {
headers: { Authorization: 'Bearer dev_token' }
})State Lifecycle
createEmulate() -> seed applied -> tests run -> reset() -> tests run -> close()
^ ^
| |
Initial state State wiped, seed re-appliedcreateEmulate()— Starts server, applies seed config- Tests run — State accumulates (created PRs, issues, etc.)
reset()— Wipes state, re-applies seed — server stays upclose()— Shuts down server, frees port
Upstream Github
<!-- SYNCED from vercel-labs/emulate (skills/github/SKILL.md) --> <!-- Hash: 635cb8ee698f5fe8b6fb4fafa9d5aaf0d9cd697c71fd980d1b56d34c328bda00 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->
GitHub API Emulator
Fully stateful GitHub REST API emulation. Creates, updates, and deletes persist in memory and affect related entities.
Start
# GitHub only
npx emulate --service github
# Default port
# http://localhost:4001Or programmatically:
import { createEmulator } from 'emulate'
const github = await createEmulator({ service: 'github', port: 4001 })
// github.url === 'http://localhost:4001'Auth
Pass tokens as Authorization: Bearer <token> or Authorization: token <token>.
curl http://localhost:4001/user \
-H "Authorization: Bearer gho_test_token_admin"Public repo endpoints work without auth. Private repos and write operations require a valid token. When no token is provided, requests fall back to the first seeded user.
GitHub App JWT
Configure apps in the seed config with a private key. Sign a JWT with \{ iss: "<app_id>" \} using RS256. The emulator verifies the signature and resolves the app.
github:
apps:
- app_id: 12345
slug: my-github-app
name: My GitHub App
private_key: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
permissions:
contents: read
issues: write
events: [push, pull_request]
installations:
- installation_id: 100
account: my-org
repository_selection: allPointing Your App at the Emulator
Environment Variable
GITHUB_EMULATOR_URL=http://localhost:4001Octokit
import { Octokit } from '@octokit/rest'
const octokit = new Octokit({
baseUrl: process.env.GITHUB_EMULATOR_URL ?? 'https://api.github.com',
auth: 'gho_test_token_admin',
})OAuth URL Mapping
| Real GitHub URL | Emulator URL |
|---|---|
https://github.com/login/oauth/authorize | $GITHUB_EMULATOR_URL/login/oauth/authorize |
https://github.com/login/oauth/access_token | $GITHUB_EMULATOR_URL/login/oauth/access_token |
https://api.github.com/user | $GITHUB_EMULATOR_URL/user |
Auth.js / NextAuth.js
import GitHub from '@auth/core/providers/github'
GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
authorization: {
url: `${process.env.GITHUB_EMULATOR_URL}/login/oauth/authorize`,
},
token: {
url: `${process.env.GITHUB_EMULATOR_URL}/login/oauth/access_token`,
},
userinfo: {
url: `${process.env.GITHUB_EMULATOR_URL}/user`,
},
})Seed Config
tokens:
gho_test_token_admin:
login: admin
scopes: [repo, user, admin:org, admin:repo_hook]
github:
users:
- login: octocat
name: The Octocat
email: octocat@github.com
bio: I am the Octocat
company: GitHub
location: San Francisco
orgs:
- login: my-org
name: My Organization
description: A test organization
repos:
- owner: octocat
name: hello-world
description: My first repository
language: JavaScript
topics: [hello, world]
auto_init: true
oauth_apps:
- client_id: Iv1.abc123
client_secret: secret_abc123
name: My Web App
redirect_uris:
- http://localhost:3000/api/auth/callback/githubPagination
All list endpoints support page and per_page query params with Link headers:
curl "http://localhost:4001/repos/octocat/hello-world/issues?page=1&per_page=10" \
-H "Authorization: Bearer gho_test_token_admin"API Endpoints
Users
# Authenticated user
curl http://localhost:4001/user -H "Authorization: Bearer $TOKEN"
# Update profile
curl -X PATCH http://localhost:4001/user \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"bio": "Hello!"}'
# Get user by username
curl http://localhost:4001/users/octocat
# List users
curl http://localhost:4001/users
# User repos / orgs / followers / following
curl http://localhost:4001/users/octocat/repos
curl http://localhost:4001/users/octocat/orgs
curl http://localhost:4001/users/octocat/followers
curl http://localhost:4001/users/octocat/followingRepositories
# Get repo
curl http://localhost:4001/repos/octocat/hello-world
# Create user repo
curl -X POST http://localhost:4001/user/repos \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "new-repo", "private": false}'
# Create org repo
curl -X POST http://localhost:4001/orgs/my-org/repos \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "org-project"}'
# Update repo
curl -X PATCH http://localhost:4001/repos/octocat/hello-world \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "Updated description"}'
# Delete repo (cascades issues, PRs, etc.)
curl -X DELETE http://localhost:4001/repos/octocat/hello-world \
-H "Authorization: Bearer $TOKEN"
# Topics, languages, contributors, forks, collaborators, tags, transferIssues
# List issues (filter by state, labels, assignee, milestone, creator, since)
curl "http://localhost:4001/repos/octocat/hello-world/issues?state=open&labels=bug"
# Create issue
curl -X POST http://localhost:4001/repos/octocat/hello-world/issues \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Bug report", "body": "Details here", "labels": ["bug"]}'
# Get issue
curl http://localhost:4001/repos/octocat/hello-world/issues/1
# Update issue
curl -X PATCH http://localhost:4001/repos/octocat/hello-world/issues/1 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}'
# Lock/unlock, timeline, events, assigneesPull Requests
# List PRs
curl "http://localhost:4001/repos/octocat/hello-world/pulls?state=open"
# Create PR
curl -X POST http://localhost:4001/repos/octocat/hello-world/pulls \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Feature", "head": "feature-branch", "base": "main"}'
# Get PR
curl http://localhost:4001/repos/octocat/hello-world/pulls/1
# Update PR
curl -X PATCH http://localhost:4001/repos/octocat/hello-world/pulls/1 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Updated title"}'
# Merge PR (enforces branch protection)
curl -X PUT http://localhost:4001/repos/octocat/hello-world/pulls/1/merge \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"merge_method": "squash"}'
# Commits, files, requested reviewers, update branchComments
# Issue comments: full CRUD
curl http://localhost:4001/repos/octocat/hello-world/issues/1/comments
curl -X POST http://localhost:4001/repos/octocat/hello-world/issues/1/comments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "Looks good!"}'
# Review comments on PRs
curl http://localhost:4001/repos/octocat/hello-world/pulls/1/comments
# Commit comments
curl http://localhost:4001/repos/octocat/hello-world/commits/abc123/commentsReviews
# List reviews
curl http://localhost:4001/repos/octocat/hello-world/pulls/1/reviews
# Create review (with inline comments)
curl -X POST http://localhost:4001/repos/octocat/hello-world/pulls/1/reviews \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"event": "APPROVE", "body": "LGTM"}'
# Get, update, submit, dismiss reviewsLabels & Milestones
Full CRUD for labels and milestones. Add/remove labels from issues, replace all labels.
Branches & Git Data
# List branches
curl http://localhost:4001/repos/octocat/hello-world/branches
# Get branch
curl http://localhost:4001/repos/octocat/hello-world/branches/main
# Branch protection CRUD (status checks, PR reviews, enforce admins)
curl -X PUT http://localhost:4001/repos/octocat/hello-world/branches/main/protection \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"required_status_checks": {"strict": true, "contexts": ["ci"]}}'
# Refs, commits, trees (recursive), blobs, tagsOrganizations & Teams
# Get org
curl http://localhost:4001/orgs/my-org
# Org members, teams, repos
curl http://localhost:4001/orgs/my-org/members
curl http://localhost:4001/orgs/my-org/teamsReleases
# Create release
curl -X POST http://localhost:4001/repos/octocat/hello-world/releases \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tag_name": "v1.0.0", "name": "v1.0.0"}'
# List, get, latest, by tag, assets, generate notesWebhooks
# Create webhook (real HTTP delivery on state changes)
curl -X POST http://localhost:4001/repos/octocat/hello-world/hooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"config": {"url": "http://localhost:8080/webhook"}, "events": ["push", "pull_request"]}'
# Full CRUD, ping, test, deliveries
# Org webhooks also supportedSearch
# Search repositories
curl "http://localhost:4001/search/repositories?q=language:JavaScript+user:octocat"
# Search issues and PRs
curl "http://localhost:4001/search/issues?q=repo:octocat/hello-world+is:open"
# Search users, code, commits, topics, labelsActions
# Workflows: list, get, enable/disable, dispatch
# Workflow runs: list, get, cancel, rerun, delete, logs
# Jobs: list, get, logs
# Artifacts: list, get, delete
# Secrets: repo + org CRUDChecks
# Create check run
curl -X POST http://localhost:4001/repos/octocat/hello-world/check-runs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "CI", "head_sha": "abc123", "status": "completed", "conclusion": "success"}'
# Check suites, annotations, rerequest, list by ref
# Automatic suite status rollup from check run resultsOAuth
# Authorize (browser flow -- shows user picker)
# GET /login/oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
# Token exchange
curl -X POST http://localhost:4001/login/oauth/access_token \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"client_id": "Iv1.abc123", "client_secret": "secret_abc123", "code": "<code>"}'
# User emails
curl http://localhost:4001/user/emails -H "Authorization: Bearer $TOKEN"Misc
curl http://localhost:4001/rate_limit
curl http://localhost:4001/meta
curl http://localhost:4001/octocat
curl http://localhost:4001/zenCommon Patterns
Create Repo, Issue, and PR
TOKEN="gho_test_token_admin"
BASE="http://localhost:4001"
# Create repo
curl -X POST $BASE/user/repos \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-project"}'
# Create issue
curl -X POST $BASE/repos/admin/my-project/issues \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "First issue"}'
# Create PR
curl -X POST $BASE/repos/admin/my-project/pulls \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "First PR", "head": "feature", "base": "main"}'OAuth Flow
- Redirect user to
$GITHUB_EMULATOR_URL/login/oauth/authorize?client_id=...&redirect_uri=...&scope=user+repo&state=... - User picks a seeded user on the emulator's UI
- Emulator redirects back with
?code=...&state=... - Exchange code for token via
POST /login/oauth/access_token - Use token to call API endpoints
Upstream Google
<!-- SYNCED from vercel-labs/emulate (skills/google/SKILL.md) --> <!-- Hash: 030d521bf26fe630f0b2efb5447785e1b704ae8eb10eb46cec23a487dbbe9fd0 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->
Google OAuth 2.0 / OIDC Emulator
OAuth 2.0 and OpenID Connect emulation with authorization code flow, PKCE support, ID tokens, and OIDC discovery.
Start
# Google only
npx emulate --service google
# Default port
# http://localhost:4002Or programmatically:
import { createEmulator } from 'emulate'
const google = await createEmulator({ service: 'google', port: 4002 })
// google.url === 'http://localhost:4002'Pointing Your App at the Emulator
Environment Variable
GOOGLE_EMULATOR_URL=http://localhost:4002OAuth URL Mapping
| Real Google URL | Emulator URL |
|---|---|
https://accounts.google.com/o/oauth2/v2/auth | $GOOGLE_EMULATOR_URL/o/oauth2/v2/auth |
https://oauth2.googleapis.com/token | $GOOGLE_EMULATOR_URL/oauth2/token |
https://www.googleapis.com/oauth2/v2/userinfo | $GOOGLE_EMULATOR_URL/oauth2/v2/userinfo |
https://accounts.google.com/.well-known/openid-configuration | $GOOGLE_EMULATOR_URL/.well-known/openid-configuration |
https://www.googleapis.com/oauth2/v3/certs | $GOOGLE_EMULATOR_URL/oauth2/v3/certs |
google-auth-library (Node.js)
import { OAuth2Client } from 'google-auth-library'
const GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'
const client = new OAuth2Client({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectUri: 'http://localhost:3000/api/auth/callback/google',
})
// Override the endpoints
const authorizeUrl = client.generateAuthUrl({
access_type: 'offline',
scope: ['openid', 'email', 'profile'],
})
// Replace the host in authorizeUrl with GOOGLE_URL, or construct manually:
const emulatorAuthorizeUrl = `${GOOGLE_URL}/o/oauth2/v2/auth?client_id=${process.env.GOOGLE_CLIENT_ID}&redirect_uri=...&scope=openid+email+profile&response_type=code&state=...`Auth.js / NextAuth.js
import Google from '@auth/core/providers/google'
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
url: `${process.env.GOOGLE_EMULATOR_URL}/o/oauth2/v2/auth`,
params: { scope: 'openid email profile' },
},
token: {
url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/token`,
},
userinfo: {
url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/v2/userinfo`,
},
})Passport.js
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
const GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'
new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/api/auth/callback/google',
authorizationURL: `${GOOGLE_URL}/o/oauth2/v2/auth`,
tokenURL: `${GOOGLE_URL}/oauth2/token`,
userProfileURL: `${GOOGLE_URL}/oauth2/v2/userinfo`,
}, verifyCallback)Seed Config
google:
users:
- email: testuser@gmail.com
name: Test User
given_name: Test
family_name: User
picture: https://lh3.googleusercontent.com/a/default-user
email_verified: true
locale: en
- email: dev@example.com
name: Developer
oauth_clients:
- client_id: my-client-id.apps.googleusercontent.com
client_secret: GOCSPX-secret
name: My App
redirect_uris:
- http://localhost:3000/api/auth/callback/googleWhen no OAuth clients are configured, the emulator accepts any client_id. With clients configured, strict validation is enforced for client_id, client_secret, and redirect_uri.
API Endpoints
OIDC Discovery
curl http://localhost:4002/.well-known/openid-configurationReturns the standard OIDC discovery document with all endpoints pointing to the emulator:
{
"issuer": "http://localhost:4002",
"authorization_endpoint": "http://localhost:4002/o/oauth2/v2/auth",
"token_endpoint": "http://localhost:4002/oauth2/token",
"userinfo_endpoint": "http://localhost:4002/oauth2/v2/userinfo",
"revocation_endpoint": "http://localhost:4002/oauth2/revoke",
"jwks_uri": "http://localhost:4002/oauth2/v3/certs",
"response_types_supported": ["code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["HS256"],
"scopes_supported": ["openid", "email", "profile"],
"code_challenge_methods_supported": ["plain", "S256"]
}JWKS
curl http://localhost:4002/oauth2/v3/certsReturns \{ "keys": [] \}. ID tokens are signed with HS256 using an internal secret.
Authorization
# Browser flow -- redirects to a user picker page
curl -v "http://localhost:4002/o/oauth2/v2/auth?\
client_id=my-client-id.apps.googleusercontent.com&\
redirect_uri=http://localhost:3000/api/auth/callback/google&\
scope=openid+email+profile&\
response_type=code&\
state=random-state&\
nonce=random-nonce"Query parameters:
| Param | Description |
|---|---|
client_id | OAuth client ID |
redirect_uri | Callback URL |
scope | Space-separated scopes (openid email profile) |
state | Opaque state for CSRF protection |
nonce | Nonce for ID token (optional) |
code_challenge | PKCE challenge (optional) |
code_challenge_method | plain or S256 (optional) |
The emulator renders an HTML page where you select a seeded user. After selection, it redirects to redirect_uri with ?code=...&state=....
Token Exchange
curl -X POST http://localhost:4002/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "code=<authorization_code>&\
client_id=my-client-id.apps.googleusercontent.com&\
client_secret=GOCSPX-secret&\
redirect_uri=http://localhost:3000/api/auth/callback/google&\
grant_type=authorization_code"Returns:
{
"access_token": "google_...",
"id_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email profile"
}The id_token is a JWT (HS256) containing sub, email, email_verified, name, given_name, family_name, picture, locale, and optional nonce.
For PKCE, include code_verifier in the token request.
User Info
curl http://localhost:4002/oauth2/v2/userinfo \
-H "Authorization: Bearer google_..."Returns:
{
"sub": "user-uid",
"email": "testuser@gmail.com",
"email_verified": true,
"name": "Test User",
"given_name": "Test",
"family_name": "User",
"picture": "https://lh3.googleusercontent.com/a/default-user",
"locale": "en"
}Token Revocation
curl -X POST http://localhost:4002/oauth2/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=google_..."Returns 200 OK. The token is removed from the emulator's token map.
Common Patterns
Full Authorization Code Flow
GOOGLE_URL="http://localhost:4002"
CLIENT_ID="my-client-id.apps.googleusercontent.com"
CLIENT_SECRET="GOCSPX-secret"
REDIRECT_URI="http://localhost:3000/api/auth/callback/google"
# 1. Open in browser -- user picks a seeded account
# $GOOGLE_URL/o/oauth2/v2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+profile&response_type=code&state=abc
# 2. After user selection, emulator redirects to:
# $REDIRECT_URI?code=<code>&state=abc
# 3. Exchange code for tokens
curl -X POST $GOOGLE_URL/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "code=<code>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code"
# 4. Fetch user info with the access_token
curl $GOOGLE_URL/oauth2/v2/userinfo \
-H "Authorization: Bearer <access_token>"PKCE Flow
# Generate code_verifier and code_challenge
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)
CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '+/' '-_')
# 1. Authorize with challenge
# $GOOGLE_URL/o/oauth2/v2/auth?...&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256
# 2. Token exchange with verifier
curl -X POST $GOOGLE_URL/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "code=<code>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code&code_verifier=$CODE_VERIFIER"OIDC Discovery-Based Setup
Libraries that support OIDC discovery (like openid-client) can auto-configure from the discovery document:
import { Issuer } from 'openid-client'
const googleIssuer = await Issuer.discover(
process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'
)
const client = new googleIssuer.Client({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uris: ['http://localhost:3000/api/auth/callback/google'],
})Upstream Vercel
<!-- SYNCED from vercel-labs/emulate (skills/vercel/SKILL.md) --> <!-- Hash: 768ffadf22343415962e0be8d04606ae43e47944331a8e04bb1e683da0e642c3 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->
Vercel API Emulator
Fully stateful Vercel REST API emulation with Vercel-style JSON responses and cursor-based pagination.
Start
# Vercel only
npx emulate --service vercel
# Default port
# http://localhost:4000Or programmatically:
import { createEmulator } from 'emulate'
const vercel = await createEmulator({ service: 'vercel', port: 4000 })
// vercel.url === 'http://localhost:4000'Auth
Pass tokens as Authorization: Bearer <token>. All endpoints accept teamId or slug query params for team scoping.
curl http://localhost:4000/v2/user \
-H "Authorization: Bearer gho_test_token_admin"When no token is provided, requests fall back to the first seeded user.
Pointing Your App at the Emulator
Environment Variable
VERCEL_EMULATOR_URL=http://localhost:4000Vercel SDK / Custom Fetch
const VERCEL_API = process.env.VERCEL_EMULATOR_URL ?? 'https://api.vercel.com'
const res = await fetch(`${VERCEL_API}/v10/projects`, {
headers: { Authorization: `Bearer ${token}` },
})OAuth URL Mapping
| Real Vercel URL | Emulator URL |
|---|---|
https://vercel.com/integrations/oauth/authorize | $VERCEL_EMULATOR_URL/oauth/authorize |
https://api.vercel.com/login/oauth/token | $VERCEL_EMULATOR_URL/login/oauth/token |
https://api.vercel.com/login/oauth/userinfo | $VERCEL_EMULATOR_URL/login/oauth/userinfo |
Auth.js / NextAuth.js
{
id: 'vercel',
name: 'Vercel',
type: 'oauth',
authorization: {
url: `${process.env.VERCEL_EMULATOR_URL}/oauth/authorize`,
},
token: {
url: `${process.env.VERCEL_EMULATOR_URL}/login/oauth/token`,
},
userinfo: {
url: `${process.env.VERCEL_EMULATOR_URL}/login/oauth/userinfo`,
},
clientId: process.env.VERCEL_CLIENT_ID,
clientSecret: process.env.VERCEL_CLIENT_SECRET,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}
},
}Seed Config
tokens:
gho_test_token_admin:
login: admin
scopes: []
vercel:
users:
- username: developer
name: Developer
email: dev@example.com
teams:
- slug: my-team
name: My Team
projects:
- name: my-app
team: my-team
framework: nextjs
integrations:
- client_id: oac_abc123
client_secret: secret_abc123
name: My Vercel App
redirect_uris:
- http://localhost:3000/api/auth/callback/vercelPagination
Cursor-based pagination using limit, since, and until query params. Responses include a pagination object:
curl "http://localhost:4000/v10/projects?limit=10" \
-H "Authorization: Bearer $TOKEN"API Endpoints
User & Teams
# Authenticated user
curl http://localhost:4000/v2/user -H "Authorization: Bearer $TOKEN"
# Update user
curl -X PATCH http://localhost:4000/v2/user \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "New Name"}'
# List teams (cursor paginated)
curl http://localhost:4000/v2/teams -H "Authorization: Bearer $TOKEN"
# Get team (by ID or slug)
curl http://localhost:4000/v2/teams/my-team -H "Authorization: Bearer $TOKEN"
# Create team
curl -X POST http://localhost:4000/v2/teams \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"slug": "new-team", "name": "New Team"}'
# Update team
curl -X PATCH http://localhost:4000/v2/teams/my-team \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Updated Team"}'
# List members, add member
curl http://localhost:4000/v2/teams/my-team/members -H "Authorization: Bearer $TOKEN"Projects
# Create project (with optional env vars and git integration)
curl -X POST http://localhost:4000/v11/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "framework": "nextjs"}'
# List projects (search, cursor pagination)
curl "http://localhost:4000/v10/projects?search=my-app" \
-H "Authorization: Bearer $TOKEN"
# Get project (includes env vars)
curl http://localhost:4000/v9/projects/my-app \
-H "Authorization: Bearer $TOKEN"
# Update project
curl -X PATCH http://localhost:4000/v9/projects/my-app \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"framework": "remix"}'
# Delete project (cascades deployments, domains, env vars)
curl -X DELETE http://localhost:4000/v9/projects/my-app \
-H "Authorization: Bearer $TOKEN"
# Promote aliases status
curl http://localhost:4000/v1/projects/my-app/promote/aliases \
-H "Authorization: Bearer $TOKEN"
# Protection bypass
curl -X PATCH http://localhost:4000/v1/projects/my-app/protection-bypass \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"revoke": false}'Deployments
# Create deployment (auto-transitions to READY)
curl -X POST http://localhost:4000/v13/deployments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "target": "production"}'
# Get deployment (by ID or URL)
curl http://localhost:4000/v13/deployments/dpl_abc123 \
-H "Authorization: Bearer $TOKEN"
# List deployments (filter by project, target, state)
curl "http://localhost:4000/v6/deployments?projectId=my-app&target=production" \
-H "Authorization: Bearer $TOKEN"
# Delete deployment (cascades)
curl -X DELETE http://localhost:4000/v13/deployments/dpl_abc123 \
-H "Authorization: Bearer $TOKEN"
# Cancel building deployment
curl -X PATCH http://localhost:4000/v12/deployments/dpl_abc123/cancel \
-H "Authorization: Bearer $TOKEN"
# List deployment aliases
curl http://localhost:4000/v2/deployments/dpl_abc123/aliases \
-H "Authorization: Bearer $TOKEN"
# Get build events/logs
curl http://localhost:4000/v3/deployments/dpl_abc123/events \
-H "Authorization: Bearer $TOKEN"
# List deployment files
curl http://localhost:4000/v6/deployments/dpl_abc123/files \
-H "Authorization: Bearer $TOKEN"
# Upload file (by SHA digest)
curl -X POST http://localhost:4000/v2/files \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "x-vercel-digest: sha256hash" \
--data-binary @file.txtDomains
# Add domain (with verification challenge)
curl -X POST http://localhost:4000/v10/projects/my-app/domains \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "example.com"}'
# List domains
curl http://localhost:4000/v9/projects/my-app/domains \
-H "Authorization: Bearer $TOKEN"
# Get, update, remove domain
curl http://localhost:4000/v9/projects/my-app/domains/example.com \
-H "Authorization: Bearer $TOKEN"
# Verify domain
curl -X POST http://localhost:4000/v9/projects/my-app/domains/example.com/verify \
-H "Authorization: Bearer $TOKEN"Environment Variables
# List env vars (with decrypt option)
curl "http://localhost:4000/v10/projects/my-app/env?decrypt=true" \
-H "Authorization: Bearer $TOKEN"
# Create env vars (single, batch, or upsert)
curl -X POST http://localhost:4000/v10/projects/my-app/env \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "API_KEY", "value": "secret123", "type": "encrypted", "target": ["production", "preview"]}'
# Get env var
curl http://localhost:4000/v10/projects/my-app/env/env_abc123 \
-H "Authorization: Bearer $TOKEN"
# Update env var
curl -X PATCH http://localhost:4000/v9/projects/my-app/env/env_abc123 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "newsecret"}'
# Delete env var
curl -X DELETE http://localhost:4000/v9/projects/my-app/env/env_abc123 \
-H "Authorization: Bearer $TOKEN"OAuth / Integrations
# Authorize (browser flow -- shows user picker)
# GET /oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
# Token exchange (supports PKCE)
curl -X POST http://localhost:4000/login/oauth/token \
-H "Content-Type: application/json" \
-d '{"client_id": "oac_abc123", "client_secret": "secret_abc123", "code": "<code>", "redirect_uri": "http://localhost:3000/api/auth/callback/vercel"}'
# User info (returns sub, email, name, preferred_username, picture)
curl http://localhost:4000/login/oauth/userinfo \
-H "Authorization: Bearer $TOKEN"API Keys
# Manage API keys for programmatic accessCommon Patterns
Create Project and Deploy
TOKEN="gho_test_token_admin"
BASE="http://localhost:4000"
# Create project
curl -X POST $BASE/v11/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "framework": "nextjs"}'
# Add env var
curl -X POST $BASE/v10/projects/my-app/env \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "DATABASE_URL", "value": "postgres://...", "type": "encrypted", "target": ["production"]}'
# Create deployment
curl -X POST $BASE/v13/deployments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "target": "production"}'OAuth Integration Flow
- Redirect user to
$VERCEL_EMULATOR_URL/oauth/authorize?client_id=...&redirect_uri=...&state=... - User picks a seeded user on the emulator's UI
- Emulator redirects back with
?code=...&state=... - Exchange code for token via
POST /login/oauth/token - Fetch user info via
GET /login/oauth/userinfo
PKCE is supported -- pass code_challenge and code_challenge_method on authorize, then code_verifier on token exchange.
Team-Scoped Requests
All endpoints accept teamId or slug query params:
curl "http://localhost:4000/v10/projects?teamId=team_abc123" \
-H "Authorization: Bearer $TOKEN"
curl "http://localhost:4000/v10/projects?slug=my-team" \
-H "Authorization: Bearer $TOKEN"Upstream
<!-- SYNCED from vercel-labs/emulate (skills/emulate/SKILL.md) --> <!-- Hash: 4cbe1c29594d746595ad412fc40dd5e3be129d48b284ce95680a714bdf73a660 --> <!-- Re-sync: bash scripts/sync-vercel-skills.sh -->
Service Emulation with emulate
Local drop-in replacement services for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation -- not mocks.
Quick Start
npx emulateAll services start with sensible defaults:
| Service | Default Port |
|---|---|
| Vercel | 4000 |
| GitHub | 4001 |
| 4002 |
CLI
# Start all services (zero-config)
emulate
# Start specific services
emulate --service vercel,github
# Custom base port (auto-increments per service)
emulate --port 3000
# Use a seed config file
emulate --seed config.yaml
# Generate a starter config
emulate init
# Generate config for a specific service
emulate init --service vercel
# List available services
emulate listOptions
| Flag | Default | Description |
|---|---|---|
-p, --port | 4000 | Base port (auto-increments per service) |
-s, --service | all | Comma-separated services to enable |
--seed | auto-detect | Path to seed config (YAML or JSON) |
The port can also be set via EMULATE_PORT or PORT environment variables.
Programmatic API
npm install emulateEach call to createEmulator starts a single service:
import { createEmulator } from 'emulate'
const github = await createEmulator({ service: 'github', port: 4001 })
const vercel = await createEmulator({ service: 'vercel', port: 4002 })
github.url // 'http://localhost:4001'
vercel.url // 'http://localhost:4002'
await github.close()
await vercel.close()Options
| Option | Default | Description |
|---|---|---|
service | (required) | 'github', 'vercel', or 'google' |
port | 4000 | Port for the HTTP server |
seed | none | Inline seed data (same shape as YAML config) |
Instance Methods
| Method | Description |
|---|---|
url | Base URL of the running server |
reset() | Wipe the store and replay seed data |
close() | Shut down the HTTP server, returns a Promise |
Vitest / Jest Setup
import { createEmulator, type Emulator } from 'emulate'
let github: Emulator
let vercel: Emulator
beforeAll(async () => {
;[github, vercel] = await Promise.all([
createEmulator({ service: 'github', port: 4001 }),
createEmulator({ service: 'vercel', port: 4002 }),
])
process.env.GITHUB_URL = github.url
process.env.VERCEL_URL = vercel.url
})
afterEach(() => { github.reset(); vercel.reset() })
afterAll(() => Promise.all([github.close(), vercel.close()]))Configuration
Configuration is optional. The CLI auto-detects config files in this order:
emulate.config.yaml/.ymlemulate.config.jsonservice-emulator.config.yaml/.ymlservice-emulator.config.json
Or pass --seed <file> explicitly. Run emulate init to generate a starter file.
Config Structure
tokens:
my_token:
login: admin
scopes: [repo, user]
vercel:
users:
- username: developer
name: Developer
email: dev@example.com
teams:
- slug: my-team
name: My Team
projects:
- name: my-app
team: my-team
framework: nextjs
integrations:
- client_id: oac_abc123
client_secret: secret_abc123
name: My Vercel App
redirect_uris:
- http://localhost:3000/api/auth/callback/vercel
github:
users:
- login: octocat
name: The Octocat
email: octocat@github.com
orgs:
- login: my-org
name: My Organization
repos:
- owner: octocat
name: hello-world
language: JavaScript
auto_init: true
oauth_apps:
- client_id: Iv1.abc123
client_secret: secret_abc123
name: My Web App
redirect_uris:
- http://localhost:3000/api/auth/callback/github
google:
users:
- email: testuser@example.com
name: Test User
oauth_clients:
- client_id: my-client-id.apps.googleusercontent.com
client_secret: GOCSPX-secret
redirect_uris:
- http://localhost:3000/api/auth/callback/googleAuth
Tokens map to users. Pass them as Authorization: Bearer <token> or Authorization: token <token>. When no tokens are configured, a default gho_test_token_admin is created for the admin user.
Each service also has a fallback user -- if no token is provided, requests authenticate as the first seeded user.
Pointing Your App at the Emulator
Set environment variables to override real service URLs:
GITHUB_EMULATOR_URL=http://localhost:4001
VERCEL_EMULATOR_URL=http://localhost:4000
GOOGLE_EMULATOR_URL=http://localhost:4002Then use these in your app to construct API and OAuth URLs. See each service's skill for SDK-specific override instructions.
Architecture
packages/
emulate/ # CLI entry point + programmatic API
@emulators/
core/ # HTTP server (Hono), Store, plugin interface, middleware
vercel/ # Vercel API service plugin
github/ # GitHub API service plugin
google/ # Google OAuth 2.0 / OIDC pluginThe core provides a generic Store with typed Collection<T> instances supporting CRUD, indexing, filtering, and pagination. Each service plugin registers routes on the shared Hono app and uses the store for state.
Dream
Nightly memory consolidation — prunes stale entries, merges duplicates, resolves contradictions, rebuilds MEMORY.md index. Use when memory files have accumulated over many sessions and need cleanup. Do NOT use for storing new decisions (use remember) or searching memory (use memory).
Errors
Error pattern analysis and troubleshooting for Claude Code sessions. Categorizes errors (network, auth, model, tool, memory, permission) with known resolution patterns, searches memory for prior occurrences, and suggests recovery steps. Delegates to debug-investigator agent for complex root cause analysis. Use when handling errors, fixing failures, or troubleshooting session issues.
Last updated on