---
title: "Emulate Seed"
description: "Generate emulate seed configs for stateful API emulation. Wraps Vercel's emulate tool for GitHub, Vercel, Google OAuth, Slack, Apple Auth, Microsoft Entra, AWS, Okta, Clerk, Resend, Stripe, and MongoDB Atlas APIs — full state machines, not mocks. Use when setting up test environments, CI pipelines, integration tests, or offline development."
canonical: "https://orchestkit.yonyon.ai/docs/reference/skills/emulate-seed"
---

# 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, Okta, Clerk, Resend, Stripe, and MongoDB Atlas APIs — full state machines, not mocks. Use when setting up test environments, CI pipelines, integration tests, or offline development.

<span className="badge badge-gray">Reference</span> <span className="badge badge-yellow">medium</span>

> **Auto-activated** — this skill loads automatically when Claude detects matching context.

<ContextualSkillSidebar slug="emulate-seed" />

> **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, Okta, Clerk, Resend, Stripe, and MongoDB Atlas APIs — full state machines, not mocks. Use when setting up test environments, CI pipelines, integration tests, or offline development.


# Emulate Seed Configs

Generate and manage seed configs for [emulate](https://github.com/vercel-labs/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-engineer`](../../agents/emulate-engineer.md) subagent (`subagent_type: "ork: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 13-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-level `emulate` re-exports `createEmulator` and the CLI.
- **4 new services** (12 total): `mongoatlas:4007`, `okta:4008`, `resend:4009`, `stripe:4010` with drop-in seed YAML blocks.
- **Resend local inbox** — `GET http://localhost:4009/inbox` returns captured emails for assertions without hitting a real provider.
- **Stripe hosted checkout** — real session redirect flow + `checkout.session.completed`/`expired` webhook 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/introspect` plus 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
$ 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](#seed-config) | 1 | HIGH | Setting up emulate.config.yaml for test environments |
| [Service Selection](#service-selection) | 1 | MEDIUM | Choosing GitHub/Vercel/Google for your tests |
| [Webhook Setup](#webhook-setup) | 1 | MEDIUM | Testing webhook delivery with HMAC verification |
| [Parallel CI](#parallel-ci) | 1 | HIGH | Running tests in parallel without port collisions |
| [Auth Tokens](#auth-tokens) | 1 | MEDIUM | Seeding tokens mapped to emulated users |

**Total: 5 rules across 5 categories**

## Quick Start

```bash
# 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 github
```

## Services (0.10.0 — 14 emulators)

> **New across releases:**
> - **0.5.0** — added Clerk, MongoDB Atlas, Stripe, Resend, and Okta emulators; portless integration (embedded emulators without dedicated ports); Google OAuth `hd` claim support; Stripe Checkout + Resend magic link examples; AWS S3 emulator now matches the official SDK wire format.
> - **0.6.0** — expanded Slack (OAuth v2 consent UI, conversations/reactions).
> - **0.6.1** — Vercel Blob store.
> - **0.7.0** — added Linear (13th provider): stateful orgs/teams/issues/cycles + webhooks.
> - **0.8.0** — added Twilio (14th provider): accounts, phone numbers, messages, calls, conversations, messaging services, Verify flows, and simulator endpoints, with a Next.js SMS-verification example.
> - **0.9.0** — added a Nuxt emulator adapter (alongside the Next.js adapter); provider count unchanged.
> - **0.10.0** — stateful GitHub repository contents and commit history (`GET /repos/:owner/:repo/readme`, `GET`/`PUT`/`DELETE /repos/:owner/:repo/contents/:path` where writes produce real commits, `GET /repos/:owner/:repo/commits`, and `GET /repositories/:id` by numeric ID); a readonly `generatedSecrets` array on the Emulator object with a new exported `GeneratedSecret` type; GitHub App JWT verification now accepts PKCS#8 as well as PKCS#1 keys. Provider count unchanged.
>
> All backwards-compatible, with one behaviour change worth knowing: 0.10.0 switched
> Stripe webhook deliveries to Stripe's own `Stripe-Signature` header format. Upstream
> files it under bug fixes rather than breaking changes, but a test asserting the old
> GitHub-style `sha256=` header on a Stripe delivery will now fail. See Webhook Delivery
> below for both formats.

| 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** *(0.5+)* | (on-demand) | Users, sessions, organizations |
| **Linear** *(0.7+)* | (on-demand) | Orgs, teams, issues, cycles, webhooks |
| **Twilio** *(0.8+)* | (on-demand) | Accounts, phone numbers, messages, calls, conversations, messaging services, Verify flows, simulator endpoints |

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.

```typescript
// 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.

```yaml
# 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-level `emulate` package.

```typescript
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

// Auto-generated credentials (0.10.0+): readonly GeneratedSecret[], each
// { service, kind, id, label, value }. A GitHub App emulator, for example,
// generates its own RSA key rather than making you supply one.
github.generatedSecrets

// Cleanup
github.reset()       // Synchronous state wipe
await github.close() // Shut down server
```

`seed` here is a parsed object, not a path. Only the CLI `--seed` flag takes a filename.
For multi-service setup, lifecycle hooks, and the Vitest/Jest wiring, see
`references/upstream.md`. For the ork-side corrections to that API, see
`references/ork-delta.md`.

## Webhook Delivery

Emulate delivers real webhooks with HMAC-SHA256 signatures when state changes. The
signature FORMAT is per-provider, so one generic verifier does not cover every service.
GitHub-format headers remain the default; Stripe has used its own format since 0.10.0.

```typescript
import crypto from 'crypto'

// GitHub format (the default for most services): sha256=<hmac over the raw body>
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))
}

// Stripe format, as of emulate 0.10.0: Stripe-Signature: t=<timestamp>,v1=<hmac>,
// where the HMAC covers `<timestamp>.<raw body>`, NOT the body alone. Verifying a
// Stripe delivery with the GitHub helper above fails on both counts.
function verifyStripeWebhook(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
}
```

See `rules/webhook-setup.md` for webhook receiver patterns.

## CI Integration

```yaml
# .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:4000
```

### Parallel Test Execution

Each test worker gets its own port to avoid race conditions:

```typescript
// 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/Clerk/Linear testing | YES | All 13 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, MongoDB Atlas, Clerk, or Linear
- 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

## Upstream coverage (do not restate)

This skill is a wrap plus our delta. emulate ships its own per-service reference docs;
copying them here only produces something that goes stale on the next release. If a
topic below comes up, read the first-party source, not a paraphrase.

| Topic | First-party source |
|---|---|
| Programmatic API (`createEmulator`, `url`, `reset()`, `close()`), Vitest/Jest wiring, config auto-detection order, token fallback | `references/upstream.md` (synced from `vercel-labs/emulate`, `skills/emulate/SKILL.md`) |
| GitHub endpoint recipes, GitHub App JWT seeding, Octokit and Auth.js base-URL wiring, GitHub OAuth flow | https://github.com/vercel-labs/emulate/blob/main/skills/github/SKILL.md |
| Google OIDC discovery, JWKS, authorize/token/userinfo/revoke, PKCE, `google-auth-library`, Passport, `openid-client` | https://github.com/vercel-labs/emulate/blob/main/skills/google/SKILL.md |
| Vercel endpoint recipes, cursor pagination, team scoping, integration OAuth flow | https://github.com/vercel-labs/emulate/blob/main/skills/vercel/SKILL.md |
| Every other emulator (Slack, Apple, Entra, AWS, Okta, Resend, Stripe, MongoDB Atlas, Clerk, Linear, Twilio) | https://github.com/vercel-labs/emulate/tree/main/skills |

**What stays ours:** `references/ork-delta.md` (the corrections and house conventions that
are not in any vendor doc), `references/cli-reference.md`, `references/api-coverage.md`,
`references/dep-to-emulator-map.json`, `scripts/auto-discover.sh`, and everything in `rules/`.

Read `references/ork-delta.md` before copying any snippet out of a vendor doc. It records
the two API facts vendor prose does not spell out (the exported factory is `createEmulator`,
and `seed` in the programmatic options is an object rather than a path) plus the
`*_API_BASE` env-var convention this repo uses instead of the vendor's `*_EMULATOR_URL`.

## Related Skills

- `testing-integration` — Integration test patterns (emulate as first choice for API tests)
- `testing-e2e` — End-to-end test patterns with emulated backends
- `testing-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.


---

## 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

```yaml
# 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

```typescript
// 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:**

```yaml
# BAD: real GitHub PAT in test config — leaks if committed
tokens:
  my_token:
    login: yonatangross
    value: ghp_abc123realtoken456  # NEVER put real tokens here
```

```typescript
// BAD: hardcoded real token in test file
headers: { Authorization: 'Bearer ghp_abc123realtoken456' }
```

**Correct — descriptive seeded token names:**

```yaml
# GOOD: token name is the bearer value, no real credentials
tokens:
  test_admin:
    login: admin-user
    scopes: [repo, admin:org]
```

```typescript
// GOOD: seeded token name as bearer value
headers: { Authorization: 'Bearer test_admin' }
```

### Permission Testing Pattern

```typescript
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

```typescript
// vitest.setup.ts — each worker gets a unique port
import { readFileSync } from 'node:fs'
import { parse } from 'yaml'
import { createEmulator, type Emulator, type SeedConfig } from 'emulate'

let github: Emulator

const BASE_PORT = 4001
const workerPort = BASE_PORT + parseInt(process.env.VITEST_WORKER_ID || '0')
const seed = parse(readFileSync('./emulate.config.yaml', 'utf8')) as SeedConfig

beforeAll(async () => {
  github = await createEmulator({
    service: 'github',
    port: workerPort,
    seed
  })
  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

```typescript
// jest.setup.ts
const workerPort = 4001 + parseInt(process.env.JEST_WORKER_ID || '0')
```

**Incorrect — all workers hitting the same port:**

```typescript
// BAD: shared port causes race conditions
const github = await createEmulator({ service: 'github', port: 4001 })

// Worker 1 creates a PR, Worker 2 sees it — non-deterministic
```

**Correct — per-worker port isolation:**

```typescript
// GOOD: each worker has isolated state
const workerPort = 4001 + parseInt(process.env.VITEST_WORKER_ID || '0')
const github = await createEmulator({ service: 'github', port: workerPort })

// Worker 1 on :4002, Worker 2 on :4003 — fully isolated
```

### CI Matrix Isolation

```yaml
# .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 }}/4
```

**Key rules:**
- Compute port as `BASE_PORT + worker_id` to guarantee uniqueness per worker
- Create a fresh emulator instance per worker in `beforeAll`, close in `afterAll`
- Use `github.reset()` in `beforeEach` to 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_BASE` per worker so test code uses the correct port
- Import `createEmulator` from `emulate`; `createEmulate` and the `@emulators/emulate`
  package do not exist (see `references/ork-delta.md`)

Reference: `references/upstream.md` for the emulator lifecycle API,
`references/ork-delta.md` for the ork-side corrections to it.


### 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

```yaml
# 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: Developer
```

**Incorrect — hardcoding tokens in test files:**

```typescript
// 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:**

```yaml
# .emulate/test.config.yaml
tokens:
  test_admin:
    login: admin-user
    scopes: [repo, admin:org]
```

```typescript
// 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 `.gitignore` only 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 github` to 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

```bash
# Start GitHub + Vercel together
npx emulate --service github,vercel --seed ./emulate.config.yaml

# Custom ports
npx emulate --service github --port 5001
```

**Incorrect — writing manual GitHub API mocks when emulate covers it:**

```typescript
// 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 stateful
```

**Correct — use emulate for stateful GitHub API testing:**

```typescript
import { createEmulator } from 'emulate'

const github = await createEmulator({ 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 github` whenever testing GitHub API interactions — it covers repos, PRs, issues, comments, reviews, Actions, webhooks, orgs, and teams
- Use `emulate --service vercel` for Vercel platform API testing — projects, deployments, domains, env vars
- Use `emulate --service google` for 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 `--port` when 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

```typescript
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:**

```typescript
// 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:**

```typescript
// 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.timingSafeEqual` for 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-event` header to route different event types
- Clean up the webhook server in `afterAll` to prevent port leaks
- Emulate delivers webhooks on state mutations (create, update, delete) — not on reads

Reference: `references/upstream.md` for the emulator lifecycle API,
`references/ork-delta.md` for the ork-side corrections to it.



---

## References (7)

### Api Coverage

# API Coverage

Full list of supported API endpoints per emulate service.

## GitHub API (`:4001`)

### Repositories
- `GET /repos/:owner/:repo` — Get repository
- `POST /user/repos` — Create user repository
- `POST /orgs/:org/repos` — Create org repository
- `PATCH /repos/:owner/:repo` — Update repository
- `DELETE /repos/:owner/:repo` — Delete repository (cascading: PRs, issues, webhooks)
- `GET /repos/:owner/:repo/topics` — List topics
- `PUT /repos/:owner/:repo/topics` — Replace topics

### Pull Requests
- `GET /repos/:owner/:repo/pulls` — List PRs (cursor pagination)
- `POST /repos/:owner/:repo/pulls` — Create PR
- `GET /repos/:owner/:repo/pulls/:number` — Get PR
- `PATCH /repos/:owner/:repo/pulls/:number` — Update PR
- `PUT /repos/:owner/:repo/pulls/:number/merge` — Merge PR
- `GET /repos/:owner/:repo/pulls/:number/reviews` — List reviews
- `POST /repos/:owner/:repo/pulls/:number/reviews` — Create review
- `GET /repos/:owner/:repo/pulls/:number/comments` — List review comments
- `POST /repos/:owner/:repo/pulls/:number/comments` — Create review comment

### Issues
- `GET /repos/:owner/:repo/issues` — List issues
- `POST /repos/:owner/:repo/issues` — Create issue
- `GET /repos/:owner/:repo/issues/:number` — Get issue
- `PATCH /repos/:owner/:repo/issues/:number` — Update issue
- `GET /repos/:owner/:repo/issues/:number/comments` — List comments
- `POST /repos/:owner/:repo/issues/:number/comments` — Create comment

### Actions / Workflows
- `GET /repos/:owner/:repo/actions/workflows` — List workflows
- `POST /repos/:owner/:repo/actions/workflows/:id/dispatches` — Trigger workflow
- `GET /repos/:owner/:repo/actions/runs` — List workflow runs
- `GET /repos/:owner/:repo/actions/runs/:id` — Get run
- `GET /repos/:owner/:repo/actions/runs/:id/jobs` — List jobs

### Webhooks
- `GET /repos/:owner/:repo/hooks` — List webhooks
- `POST /repos/:owner/:repo/hooks` — Create webhook
- `PATCH /repos/:owner/:repo/hooks/:id` — Update webhook
- `DELETE /repos/:owner/:repo/hooks/:id` — Delete webhook

### Organizations & Teams
- `GET /orgs/:org` — Get organization
- `GET /orgs/:org/repos` — List org repos
- `GET /orgs/:org/teams` — List teams
- `POST /orgs/:org/teams` — Create team
- `GET /orgs/:org/members` — List members

### Users
- `GET /user` — Authenticated user
- `GET /users/:username` — Get user
- `GET /users/:username/repos` — List user repos

## Vercel API (`:4000`)

### Projects
- `GET /v9/projects` — List projects
- `POST /v9/projects` — Create project
- `GET /v9/projects/:id` — Get project
- `PATCH /v9/projects/:id` — Update project
- `DELETE /v9/projects/:id` — Delete project

### Deployments
- `GET /v6/deployments` — List deployments
- `POST /v13/deployments` — Create deployment
- `GET /v13/deployments/:id` — Get deployment
- `DELETE /v13/deployments/:id` — Cancel deployment

### Domains
- `GET /v5/domains` — List domains
- `POST /v5/domains` — Add domain
- `DELETE /v5/domains/:name` — Remove domain

### Environment Variables
- `GET /v9/projects/:id/env` — List env vars
- `POST /v9/projects/:id/env` — Create env var
- `PATCH /v9/projects/:id/env/:envId` — Update env var
- `DELETE /v9/projects/:id/env/:envId` — Delete env var

### Teams
- `GET /v2/teams` — List teams
- `POST /v1/teams` — Create team
- `GET /v2/teams/:id` — Get team

## Google OAuth (`:4002`)

### OAuth 2.0
- `GET /o/oauth2/v2/auth` — Authorization endpoint
- `POST /oauth2/v4/token` — Token exchange
- `GET /oauth2/v2/userinfo` — User info
- `POST /oauth2/revoke` — Token revocation

## Slack Web API (`:4003`)

### Chat & Conversations
- `POST /api/chat.postMessage` — Send message
- `GET /api/conversations.list` — List conversations
- `GET /api/conversations.history` — Get messages in channel
- `POST /api/reactions.add` — Add emoji reaction
- `GET /api/users.list` — List workspace users

### OAuth
- `GET /oauth/v2/authorize` — OAuth v2 consent UI
- `POST /api/oauth.v2.access` — Token exchange

## Apple Authentication (`:4004`)

- `GET /.well-known/openid-configuration` — OIDC discovery
- `GET /auth/keys` — JWKS endpoint (RS256)
- `GET /auth/authorize` — Authorization flow
- `POST /auth/token` — Token exchange
- `POST /auth/revoke` — Token revocation

## Microsoft Entra ID (`:4005`)

- `GET /\{tenant\}/v2.0/.well-known/openid-configuration` — OIDC discovery
- `GET /\{tenant\}/oauth2/v2.0/authorize` — Authorization code + PKCE
- `POST /\{tenant\}/oauth2/v2.0/token` — Token exchange with refresh rotation
- `GET /\{tenant\}/oauth2/v2.0/logout` — Logout endpoint

## AWS (`:4006`)

### S3
- `PUT /\{bucket\}` — Create bucket
- `GET /` — List buckets
- `PUT /\{bucket\}/\{key\}` — Put object
- `GET /\{bucket\}/\{key\}` — Get object
- `DELETE /\{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=N` and 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.

```bash
# 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.yaml
```

### `emulate init`

Generate a starter seed config.

```bash
# 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.yaml
```

### `emulate list`

List available services and their default ports.

```bash
npx emulate list
# github   :4001
# vercel   :4000
# google   :4002
```

## Flags

| 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 |
| Google | 4002 |

When `--port` is specified with multiple services, ports increment from the given base:

```bash
npx emulate --service github,vercel --port 3000
# github -> :3000
# vercel -> :3001
```

## Environment 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) |


### Ork Delta

# emulate-seed: the ork delta

What this skill knows that emulate's own documentation does not. Everything else
(endpoint lists, per-service curl recipes, OAuth walkthroughs, the programmatic API
surface) lives upstream and is deliberately not restated here. See the
"Upstream coverage (do not restate)" table in `SKILL.md` for where each topic went.

Every claim below was checked against the published `emulate@0.9.0` type
declarations on 2026-07-31, which is also this skill's `upstream-version-tested`.

## Import `createEmulator` from `emulate`, never `createEmulate` from `@emulators/emulate`
Why: the retired `sdk-patterns.md` reference used `createEmulate` from `@emulators/emulate` in every snippet and `rules/parallel-ci.md` copied it, but `@emulators/emulate` returns 404 on the npm registry and `emulate@0.9.0` `dist/api.d.ts` exports exactly one factory, `createEmulator`, so none of those snippets could ever have run. Same failure class the house rule in `.claude/rules/skill-authoring.md` calls out: every symbol in an import must actually be exported.
Upstream: https://cdn.jsdelivr.net/npm/emulate@0.9.0/dist/api.d.ts

## Pass `seed` to `createEmulator` as a parsed object, not a YAML path
Why: `EmulatorOptions.seed` is typed `SeedConfig` (an object with a `tokens` map plus per-service keys) in `emulate@0.9.0` `dist/api.d.ts`; only the CLI `--seed` flag takes a file path. The retired `sdk-patterns.md` passed `seed: './emulate.config.yaml'` in six snippets, which type-errors before a test ever starts. Read and parse the YAML yourself, or drive the emulator from the CLI.
Upstream: https://cdn.jsdelivr.net/npm/emulate@0.9.0/dist/api.d.ts

## Point app code at the emulator with `*_API_BASE`, not the vendor's `*_EMULATOR_URL`
Why: house convention. `references/cli-reference.md`, the CI snippet in `SKILL.md`, and every expectation in `test-cases.json` key on `GITHUB_API_BASE`, `VERCEL_API_BASE`, and `GOOGLE_OAUTH_BASE`, while the vendor docs use `GITHUB_EMULATOR_URL` and friends. Copying a variable name out of a vendor snippet leaves the SDK silently pointed at production instead of failing loudly.
Upstream: references/upstream.md, section "Pointing Your App at the Emulator"

## Translate dep-map emulator labels into emulate service ids before passing `--service`
Why: `references/dep-to-emulator-map.json` emits ork's own labels `google-oauth`, `apple-auth`, and `microsoft-entra`, but `emulate@0.9.0`'s `SERVICE_NAME_LIST` is vercel, github, google, slack, apple, microsoft, okta, aws, resend, stripe, mongoatlas, clerk, linear, twilio. Anything `scripts/auto-discover.sh` writes for those three needs mapping before it reaches the CLI or is used as a `SeedConfig` key, or emulate will not recognize the service.
Upstream: https://cdn.jsdelivr.net/npm/emulate@0.9.0/dist/api.d.ts

## Do not vendor-sync emulate's per-service SKILL.md files into this skill
Why: the retired `upstream-github.md`, `upstream-google.md`, and `upstream-vercel.md` were 1133 lines of verbatim output from the repo-root `sync-vercel-skills.sh` covering 3 of the 14 emulators, so they read as full coverage while going stale on every emulate release, and they duplicated `references/upstream.md`. Distilled from those three retired files; no traced incident.
Upstream: https://github.com/vercel-labs/emulate/tree/main/skills


### Upstream Github

&lt;!-- SYNCED from vercel-labs/emulate (skills/github/SKILL.md) --&gt;
&lt;!-- Hash: 1ef2cf7eadb47a76d3a5ff88d47572b73e0d27d51c4f57117575d310e41c7226 --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# GitHub API Emulator

Fully stateful GitHub REST API emulation. Creates, updates, and deletes persist in memory and affect related entities.

## Start

```bash
# GitHub only
npx emulate --service github

# Default port
# http://localhost:4001
```

Or programmatically:

```typescript
import { createEmulator } from 'emulate'

const github = await createEmulator({ service: 'github', port: 4001 })
// github.url === 'http://localhost:4001'
```

For a programmatic GitHub App, omit `private_key` and read the generated RSA key from the instance:

```typescript
const github = await createEmulator({
  service: 'github',
  port: 4001,
  seed: {
    github: {
      users: [{ login: 'octocat' }],
      apps: [{
        app_id: 12345,
        slug: 'my-github-app',
        name: 'My GitHub App',
        installations: [{ installation_id: 100, account: 'octocat' }],
      }],
    },
  },
})

const privateKey = github.generatedSecrets.find(
  secret => secret.kind === 'github.app_private_key' && secret.id === '12345',
)?.value
```

The key remains stable across `github.reset()`. Explicit keys are not included in `generatedSecrets`. CLI seed files still require `private_key`.

## Auth

Pass tokens as `Authorization: Bearer &lt;token&gt;` or `Authorization: token &lt;token&gt;`.

```bash
curl http://localhost:4001/user \
  -H "Authorization: Bearer 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: "&lt;app_id&gt;" \}` using RS256. The emulator verifies the signature and resolves the app.

```yaml
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]
      webhook_url: http://localhost:8080/github/webhook
      webhook_secret: my-webhook-secret
      description: My CI/CD bot
      installations:
        - installation_id: 100
          account: my-org
          repository_selection: all
          permissions:
            contents: read
          events: [push]
          repositories: [my-org/org-repo]
```

## Pointing Your App at the Emulator

### Environment Variable

```bash
GITHUB_EMULATOR_URL=http://localhost:4001
```

### Octokit

```typescript
import { Octokit } from '@octokit/rest'

const octokit = new Octokit({
  baseUrl: process.env.GITHUB_EMULATOR_URL ?? 'https://api.github.com',
  auth: '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

```typescript
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

```yaml
tokens:
  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
      blog: https://github.blog
      twitter_username: github
      site_admin: false
  orgs:
    - login: my-org
      name: My Organization
      description: A test organization
      email: org@example.com
  repos:
    - owner: octocat
      name: hello-world
      description: My first repository
      language: JavaScript
      topics: [hello, world]
      default_branch: main
      private: false
    - owner: my-org
      name: org-repo
      description: An organization repository
      language: TypeScript
  oauth_apps:
    - client_id: Iv1.abc123
      client_secret: secret_abc123
      name: My Web App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/github
```

Repos are auto-initialized with a commit, branch, and README unless `auto_init: false` is set.

## Pagination

All list endpoints support `page` and `per_page` query params with `Link` headers:

```bash
curl "http://localhost:4001/repos/octocat/hello-world/issues?page=1&per_page=10" \
  -H "Authorization: Bearer $TOKEN"
```

## API Endpoints

### Users

```bash
# 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/following

# User hovercard
curl http://localhost:4001/users/octocat/hovercard

# User emails
curl http://localhost:4001/user/emails -H "Authorization: Bearer $TOKEN"
```

### Repositories

```bash
# Get repo
curl http://localhost:4001/repos/octocat/hello-world

# Get repo by numeric ID
curl http://localhost:4001/repositories/1

# 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, transfer
```

### Contents & Commit History

```bash
# Read a file or list a directory at a branch, tag, or commit
curl "http://localhost:4001/repos/octocat/hello-world/contents/README.md?ref=main"

# Download raw file content from the URL advertised by contents and commit responses
curl http://localhost:4001/octocat/hello-world/raw/main/README.md

# Create or update a file and commit the change
curl -X PUT http://localhost:4001/repos/octocat/hello-world/contents/notes.txt \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "Update notes", "content": "aGVsbG8K"}'

# List commits, get a commit with file stats, or compare refs
curl http://localhost:4001/repos/octocat/hello-world/commits
curl http://localhost:4001/repos/octocat/hello-world/commits/main
curl http://localhost:4001/repos/octocat/hello-world/compare/v1.0.0...main
```

### Issues

```bash
# 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 / update / lock / unlock / timeline / events / assignees
```

### Pull Requests

```bash
# 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"}'

# 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 branch
```

### Comments

```bash
# 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!"}'

# Comment by ID (cross-resource)
curl http://localhost:4001/repos/octocat/hello-world/issues/comments/1

# PR review comments
curl http://localhost:4001/repos/octocat/hello-world/pulls/1/comments

# Commit comments
curl http://localhost:4001/repos/octocat/hello-world/commits/abc123/comments

# Repo-wide comment listings
curl http://localhost:4001/repos/octocat/hello-world/issues/comments
curl http://localhost:4001/repos/octocat/hello-world/pulls/comments
curl http://localhost:4001/repos/octocat/hello-world/comments
```

### Reviews

```bash
# List / create / get / update / submit / dismiss reviews
curl http://localhost:4001/repos/octocat/hello-world/pulls/1/reviews
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"}'
```

### Labels & Milestones

Full CRUD for labels and milestones. Add/remove labels from issues, replace all labels. List labels for a milestone.

### Branches & Git Data

```bash
# List branches
curl http://localhost:4001/repos/octocat/hello-world/branches

# 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, tags, matching-refs
```

### Organizations & Teams

```bash
# List all orgs / user's orgs / get org / update org
curl http://localhost:4001/organizations
curl http://localhost:4001/user/orgs -H "Authorization: Bearer $TOKEN"
curl http://localhost:4001/orgs/my-org
curl -X PATCH http://localhost:4001/orgs/my-org \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"description": "Updated org"}'

# Org members: list, get, remove
curl http://localhost:4001/orgs/my-org/members
curl http://localhost:4001/orgs/my-org/members/octocat
curl -X DELETE http://localhost:4001/orgs/my-org/members/octocat \
  -H "Authorization: Bearer $TOKEN"

# Org memberships: get, set (invite/update role)
curl http://localhost:4001/orgs/my-org/memberships/octocat -H "Authorization: Bearer $TOKEN"
curl -X PUT http://localhost:4001/orgs/my-org/memberships/octocat \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role": "admin"}'

# Teams: CRUD
curl http://localhost:4001/orgs/my-org/teams
curl -X POST http://localhost:4001/orgs/my-org/teams \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "engineering", "privacy": "closed"}'

# Team members and memberships
curl http://localhost:4001/orgs/my-org/teams/engineering/members
curl -X PUT http://localhost:4001/orgs/my-org/teams/engineering/memberships/octocat \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role": "maintainer"}'

# Team repos: list, add, remove
curl http://localhost:4001/orgs/my-org/teams/engineering/repos
curl -X PUT http://localhost:4001/orgs/my-org/teams/engineering/repos/my-org/org-repo \
  -H "Authorization: Bearer $TOKEN"

# Legacy team endpoints by ID
curl http://localhost:4001/teams/1
curl http://localhost:4001/teams/1/members
```

### GitHub Apps

```bash
# Get authenticated app (requires JWT auth)
curl http://localhost:4001/app \
  -H "Authorization: Bearer <jwt>"

# List app installations
curl http://localhost:4001/app/installations \
  -H "Authorization: Bearer <jwt>"

# Get installation
curl http://localhost:4001/app/installations/100 \
  -H "Authorization: Bearer <jwt>"

# Create installation access token (mints ghs_... token)
curl -X POST http://localhost:4001/app/installations/100/access_tokens \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"permissions": {"contents": "read"}}'

# Find installation for repo / org / user
curl http://localhost:4001/repos/my-org/org-repo/installation
curl http://localhost:4001/orgs/my-org/installation
curl http://localhost:4001/users/octocat/installation
```

App webhook delivery: when events occur, the emulator POSTs `event_callback` payloads to configured `webhook_url` with `X-GitHub-Event` and `X-Hub-Signature-256` headers.

### Releases

```bash
# 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, generate notes

# Release assets: list, upload
curl http://localhost:4001/repos/octocat/hello-world/releases/1/assets
curl -X POST http://localhost:4001/repos/octocat/hello-world/releases/1/assets \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/octet-stream" \
  -H "name: binary.zip" \
  --data-binary @binary.zip
```

### Webhooks

```bash
# 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 supported
```

### Search

```bash
# 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, labels
```

### Actions

```bash
# 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 CRUD
```

### Checks

```bash
# 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: create, get, rerequest, preferences, list by ref
# Check runs: list for suite, annotations
# Automatic suite status rollup from check run results
```

### OAuth

```bash
# 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"

# OAuth app management (settings)
curl http://localhost:4001/settings/applications -H "Authorization: Bearer $TOKEN"
curl http://localhost:4001/settings/connections/applications/Iv1.abc123 -H "Authorization: Bearer $TOKEN"

# Revoke OAuth app
curl -X POST http://localhost:4001/settings/connections/applications/Iv1.abc123/revoke \
  -H "Authorization: Bearer $TOKEN"
```

### Misc

```bash
curl http://localhost:4001/rate_limit
curl http://localhost:4001/meta
curl http://localhost:4001/emojis
curl http://localhost:4001/versions
curl http://localhost:4001/octocat
curl http://localhost:4001/zen
```

## Common Patterns

### Create Repo, Issue, and PR

```bash
TOKEN="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"}'
```

### GitHub App Installation Token Flow

```bash
# 1. Sign a JWT with { iss: "12345" } using the app's private key (RS256)
# 2. Create an installation access token
curl -X POST $BASE/app/installations/100/access_tokens \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"permissions": {"contents": "read", "issues": "write"}}'
# Returns { "token": "ghs_...", ... }

# 3. Use the installation token to call API endpoints
curl $BASE/repos/my-org/org-repo \
  -H "Authorization: Bearer ghs_..."
```

### OAuth Flow

1. Redirect user to `$GITHUB_EMULATOR_URL/login/oauth/authorize?client_id=...&redirect_uri=...&scope=user+repo&state=...`
2. User picks a seeded user on the emulator's UI
3. Emulator redirects back with `?code=...&state=...`
4. Exchange code for token via `POST /login/oauth/access_token`
5. Use token to call API endpoints


### Upstream Google

&lt;!-- SYNCED from vercel-labs/emulate (skills/google/SKILL.md) --&gt;
&lt;!-- Hash: fc3e31be019eb1ba9a4715976f2c34af9e755dd724c558d3df81b84e70f0a9fb --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# Google OAuth 2.0 / OIDC + Gmail, Calendar & Drive Emulator

OAuth 2.0 and OpenID Connect emulation with authorization code flow, PKCE support, ID tokens, OIDC discovery, refresh tokens, plus Gmail, Google Calendar, and Google Drive REST API surfaces.

## Start

```bash
# Google only
npx emulate --service google

# Default port
# http://localhost:4002
```

Or programmatically:

```typescript
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

```bash
GOOGLE_EMULATOR_URL=http://localhost:4002
```

### OAuth 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` |
| `https://gmail.googleapis.com/gmail/v1/...` | `$GOOGLE_EMULATOR_URL/gmail/v1/...` |
| `https://www.googleapis.com/calendar/v3/...` | `$GOOGLE_EMULATOR_URL/calendar/v3/...` |
| `https://www.googleapis.com/drive/v3/...` | `$GOOGLE_EMULATOR_URL/drive/v3/...` |

### google-auth-library (Node.js)

```typescript
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',
})

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

```typescript
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

```typescript
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

```yaml
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
    - email: admin@acme.com
      name: Admin
      hd: acme.com
  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/google
  labels:
    - id: Label_ops
      user_email: testuser@gmail.com
      name: Ops/Review
      color_background: "#DDEEFF"
      color_text: "#111111"
  messages:
    - id: msg_welcome
      user_email: testuser@gmail.com
      thread_id: thr_welcome
      from: "welcome@example.com"
      to: testuser@gmail.com
      subject: Welcome to the Gmail emulator
      body_text: You can now test Gmail flows locally.
      label_ids: [INBOX, UNREAD, CATEGORY_UPDATES]
      date: "2025-01-04T10:00:00.000Z"
  calendars:
    - id: primary
      user_email: testuser@gmail.com
      summary: testuser@gmail.com
      primary: true
      selected: true
      time_zone: UTC
  calendar_events:
    - id: evt_kickoff
      user_email: testuser@gmail.com
      calendar_id: primary
      summary: Project Kickoff
      start_date_time: "2025-01-10T09:00:00.000Z"
      end_date_time: "2025-01-10T09:30:00.000Z"
      attendees:
        - email: testuser@gmail.com
          display_name: Test User
      conference_entry_points:
        - entry_point_type: video
          uri: https://meet.google.com/example
          label: Google Meet
      hangout_link: https://meet.google.com/example
  drive_items:
    - id: drv_docs
      user_email: testuser@gmail.com
      name: Docs
      mime_type: application/vnd.google-apps.folder
      parent_ids: [root]
    - id: drv_readme
      user_email: testuser@gmail.com
      name: README.md
      mime_type: text/markdown
      parent_ids: [drv_docs]
      data: "# Hello World"
```

When 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`.

### Hosted domain (hd) claim

Google Workspace accounts include an `hd` claim in ID tokens and userinfo responses identifying the user's hosted domain. The emulator derives this automatically from the user's email domain. Consumer domains (`gmail.com`, `googlemail.com`) omit the claim, matching real Google behavior.

To override the derived value, set `hd` on a seeded user. To suppress the claim entirely, set `hd` to an empty string.

## OAuth / OIDC Endpoints

### OIDC Discovery

```bash
curl http://localhost:4002/.well-known/openid-configuration
```

### JWKS

```bash
curl http://localhost:4002/oauth2/v3/certs
```

Returns `\{ "keys": [] \}`. ID tokens are signed with HS256 using an internal secret.

### Authorization

```bash
# 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"
```

Supports `code_challenge` and `code_challenge_method` for PKCE.

### Token Exchange

```bash
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"
```

Also accepts `application/json` body. Returns:

```json
{
  "access_token": "google_...",
  "refresh_token": "google_refresh_...",
  "id_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid email profile"
}
```

### Refresh Token

```bash
curl -X POST http://localhost:4002/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "refresh_token=google_refresh_...&\
client_id=my-client-id.apps.googleusercontent.com&\
client_secret=GOCSPX-secret&\
grant_type=refresh_token"
```

Returns a new `access_token` (no new `refresh_token` or `id_token` on refresh).

### User Info

```bash
curl http://localhost:4002/oauth2/v2/userinfo \
  -H "Authorization: Bearer google_..."
```

### Token Revocation

```bash
curl -X POST http://localhost:4002/oauth2/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=google_..."
```

## Gmail API

All Gmail endpoints are under `/gmail/v1/users/:userId/...` where `:userId` is `me` or the authenticated user's email.

### Messages

```bash
# List messages (filter by labels, search query)
curl "http://localhost:4002/gmail/v1/users/me/messages?labelIds=INBOX&q=from:welcome&maxResults=10" \
  -H "Authorization: Bearer $TOKEN"

# Get message (format: full, metadata, minimal, raw)
curl "http://localhost:4002/gmail/v1/users/me/messages/msg_welcome?format=full" \
  -H "Authorization: Bearer $TOKEN"

# Send message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to": "someone@example.com", "subject": "Hello", "body_text": "Hi there"}'

# Insert message (bypass send)
curl -X POST http://localhost:4002/gmail/v1/users/me/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to": "test@example.com", "from": "me@example.com", "subject": "Test", "body_text": "Body", "labelIds": ["INBOX"]}'

# Import message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/import \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to": "test@example.com", "from": "external@example.com", "subject": "Imported", "body_text": "Content"}'

# Modify labels on a message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/modify \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"addLabelIds": ["STARRED"], "removeLabelIds": ["UNREAD"]}'

# Trash / untrash
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/trash \
  -H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/untrash \
  -H "Authorization: Bearer $TOKEN"

# Delete permanently
curl -X DELETE http://localhost:4002/gmail/v1/users/me/messages/msg_welcome \
  -H "Authorization: Bearer $TOKEN"

# Batch modify
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/batchModify \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["msg_welcome", "msg_build"], "addLabelIds": ["STARRED"]}'

# Batch delete
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/batchDelete \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["msg_welcome"]}'

# Get attachment
curl http://localhost:4002/gmail/v1/users/me/messages/msg_id/attachments/att_id \
  -H "Authorization: Bearer $TOKEN"
```

Upload variants also available at `/upload/gmail/v1/users/:userId/messages`, `.../messages/send`, `.../messages/import`.

### Drafts

```bash
# List drafts
curl http://localhost:4002/gmail/v1/users/me/drafts \
  -H "Authorization: Bearer $TOKEN"

# Create draft
curl -X POST http://localhost:4002/gmail/v1/users/me/drafts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": {"to": "someone@example.com", "subject": "Draft subject", "body_text": "Draft body"}}'

# Get draft (format: full, metadata, minimal, raw)
curl "http://localhost:4002/gmail/v1/users/me/drafts/draft_id?format=full" \
  -H "Authorization: Bearer $TOKEN"

# Update draft
curl -X PUT http://localhost:4002/gmail/v1/users/me/drafts/draft_id \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": {"subject": "Updated subject", "body_text": "Updated body"}}'

# Send draft
curl -X POST http://localhost:4002/gmail/v1/users/me/drafts/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id": "draft_id"}'

# Delete draft
curl -X DELETE http://localhost:4002/gmail/v1/users/me/drafts/draft_id \
  -H "Authorization: Bearer $TOKEN"
```

### Threads

```bash
# List threads (filter by labels, search query)
curl "http://localhost:4002/gmail/v1/users/me/threads?labelIds=INBOX&maxResults=20" \
  -H "Authorization: Bearer $TOKEN"

# Get thread (all messages in thread)
curl "http://localhost:4002/gmail/v1/users/me/threads/thr_welcome?format=full" \
  -H "Authorization: Bearer $TOKEN"

# Modify labels on all messages in thread
curl -X POST http://localhost:4002/gmail/v1/users/me/threads/thr_welcome/modify \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"addLabelIds": ["STARRED"], "removeLabelIds": ["UNREAD"]}'

# Trash / untrash / delete thread
curl -X POST http://localhost:4002/gmail/v1/users/me/threads/thr_welcome/trash \
  -H "Authorization: Bearer $TOKEN"
curl -X DELETE http://localhost:4002/gmail/v1/users/me/threads/thr_welcome \
  -H "Authorization: Bearer $TOKEN"
```

### Labels

```bash
# List labels
curl http://localhost:4002/gmail/v1/users/me/labels \
  -H "Authorization: Bearer $TOKEN"

# Get label
curl http://localhost:4002/gmail/v1/users/me/labels/INBOX \
  -H "Authorization: Bearer $TOKEN"

# Create label
curl -X POST http://localhost:4002/gmail/v1/users/me/labels \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "My Label", "color": {"backgroundColor": "#DDEEFF", "textColor": "#111111"}}'

# Update label (PUT replaces, PATCH merges)
curl -X PATCH http://localhost:4002/gmail/v1/users/me/labels/Label_ops \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Ops/Reviewed"}'

# Delete label (user labels only)
curl -X DELETE http://localhost:4002/gmail/v1/users/me/labels/Label_ops \
  -H "Authorization: Bearer $TOKEN"
```

### History & Watch

```bash
# List history changes since a given historyId
curl "http://localhost:4002/gmail/v1/users/me/history?startHistoryId=1&historyTypes=messageAdded&maxResults=100" \
  -H "Authorization: Bearer $TOKEN"

# Set up push notification watch (stub)
curl -X POST http://localhost:4002/gmail/v1/users/me/watch \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"topicName": "projects/my-project/topics/gmail", "labelIds": ["INBOX"]}'

# Stop watch
curl -X POST http://localhost:4002/gmail/v1/users/me/stop \
  -H "Authorization: Bearer $TOKEN"
```

### Settings

```bash
# List filters
curl http://localhost:4002/gmail/v1/users/me/settings/filters \
  -H "Authorization: Bearer $TOKEN"

# Create filter (auto-label incoming messages matching criteria)
curl -X POST http://localhost:4002/gmail/v1/users/me/settings/filters \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"criteria": {"from": "alerts@example.com"}, "action": {"addLabelIds": ["Label_ops"]}}'

# Delete filter
curl -X DELETE http://localhost:4002/gmail/v1/users/me/settings/filters/filter_id \
  -H "Authorization: Bearer $TOKEN"

# List forwarding addresses
curl http://localhost:4002/gmail/v1/users/me/settings/forwardingAddresses \
  -H "Authorization: Bearer $TOKEN"

# List send-as aliases
curl http://localhost:4002/gmail/v1/users/me/settings/sendAs \
  -H "Authorization: Bearer $TOKEN"
```

## Google Calendar API

### Calendar List

```bash
curl http://localhost:4002/calendar/v3/users/me/calendarList \
  -H "Authorization: Bearer $TOKEN"
```

### Events

```bash
# List events (filter by time range, search, order)
curl "http://localhost:4002/calendar/v3/calendars/primary/events?\
timeMin=2025-01-01T00:00:00Z&timeMax=2025-12-31T23:59:59Z&maxResults=50&orderBy=startTime" \
  -H "Authorization: Bearer $TOKEN"

# Create event
curl -X POST http://localhost:4002/calendar/v3/calendars/primary/events \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"summary": "Team Meeting", "start": {"dateTime": "2025-01-10T14:00:00Z"}, "end": {"dateTime": "2025-01-10T15:00:00Z"}, "attendees": [{"email": "dev@example.com"}]}'

# Delete event
curl -X DELETE http://localhost:4002/calendar/v3/calendars/primary/events/evt_kickoff \
  -H "Authorization: Bearer $TOKEN"
```

### FreeBusy

```bash
curl -X POST http://localhost:4002/calendar/v3/freeBusy \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"timeMin": "2025-01-10T00:00:00Z", "timeMax": "2025-01-10T23:59:59Z", "items": [{"id": "primary"}]}'
```

## Google Drive API

### Files

```bash
# List files (with query filter, pagination, ordering)
curl "http://localhost:4002/drive/v3/files?q='root'+in+parents&pageSize=20" \
  -H "Authorization: Bearer $TOKEN"

# Create file (JSON metadata)
curl -X POST http://localhost:4002/drive/v3/files \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "notes.txt", "mimeType": "text/plain", "parents": ["root"]}'

# Create file with content (multipart/related upload)
curl -X POST http://localhost:4002/upload/drive/v3/files \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: multipart/related; boundary=boundary" \
  --data-binary $'--boundary\r\nContent-Type: application/json\r\n\r\n{"name":"data.csv","mimeType":"text/csv"}\r\n--boundary\r\nContent-Type: text/csv\r\n\r\na,b,c\n1,2,3\r\n--boundary--'

# Get file metadata
curl http://localhost:4002/drive/v3/files/drv_readme \
  -H "Authorization: Bearer $TOKEN"

# Download file content
curl "http://localhost:4002/drive/v3/files/drv_readme?alt=media" \
  -H "Authorization: Bearer $TOKEN"

# Update file (PATCH or PUT; move parents with query params)
curl -X PATCH "http://localhost:4002/drive/v3/files/drv_readme?addParents=folder_id&removeParents=root" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "README-updated.md"}'
```

## Common Patterns

### Full Authorization Code Flow

```bash
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>"
```

### OIDC Discovery-Based Setup

```typescript
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'],
})
```

### Send a Gmail Message and Check the Thread

```bash
TOKEN="test_token_admin"
BASE="http://localhost:4002"

# Send a message
curl -X POST $BASE/gmail/v1/users/me/messages/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to": "someone@example.com", "subject": "Test", "body_text": "Hello"}'

# List threads in INBOX
curl "$BASE/gmail/v1/users/me/threads?labelIds=INBOX" \
  -H "Authorization: Bearer $TOKEN"
```


### Upstream Vercel

&lt;!-- SYNCED from vercel-labs/emulate (skills/vercel/SKILL.md) --&gt;
&lt;!-- Hash: 932a3b3fd95cd3eacb134aa421e01f3dbd8e5f0bff50dcbdf3bb1d38fee57bcf --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# Vercel API Emulator

Fully stateful Vercel REST API emulation with Vercel-style JSON responses and cursor-based pagination.

## Start

```bash
# Vercel only
npx emulate --service vercel

# Default port
# http://localhost:4000
```

Or programmatically:

```typescript
import { createEmulator } from 'emulate'

const vercel = await createEmulator({ service: 'vercel', port: 4000 })
// vercel.url === 'http://localhost:4000'
```

## Auth

Pass tokens as `Authorization: Bearer &lt;token&gt;`. All endpoints accept `teamId` or `slug` query params for team scoping.

```bash
curl http://localhost:4000/v2/user \
  -H "Authorization: Bearer test_token_admin"
```

Team-scoped requests resolve the account from the `teamId` or `slug` query parameter. User-scoped requests resolve the account from the authenticated user.

## Pointing Your App at the Emulator

### Environment Variable

```bash
VERCEL_EMULATOR_URL=http://localhost:4000
```

### Vercel SDK / Custom Fetch

```typescript
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

```typescript
{
  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

```yaml
tokens:
  test_token_admin:
    login: admin
    scopes: []

vercel:
  users:
    - username: developer
      name: Developer
      email: dev@example.com
  teams:
    - slug: my-team
      name: My Team
      description: Engineering team
  projects:
    - name: my-app
      team: my-team
      framework: nextjs
      buildCommand: next build
      outputDirectory: .next
      rootDirectory: null
      nodeVersion: "20.x"
      envVars:
        - key: DATABASE_URL
          value: postgres://localhost/mydb
          type: encrypted
          target: [production, preview]
  integrations:
    - client_id: oac_abc123
      client_secret: secret_abc123
      name: My Vercel App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/vercel
```

## Pagination

Cursor-based pagination using `limit`, `since`, and `until` query params. Responses include a `pagination` object:

```bash
curl "http://localhost:4000/v10/projects?limit=10" \
  -H "Authorization: Bearer $TOKEN"
```

## API Endpoints

### User & Teams

```bash
# Registration check
curl http://localhost:4000/registration

# 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", "email": "new@example.com"}'

# 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 (name, slug, description)
curl -X PATCH http://localhost:4000/v2/teams/my-team \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Updated Team", "description": "New description"}'

# List members
curl http://localhost:4000/v2/teams/my-team/members -H "Authorization: Bearer $TOKEN"

# Add member (by uid or email, with role)
curl -X POST "http://localhost:4000/v2/teams/team_id/members" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email": "dev@example.com", "role": "MEMBER"}'
```

Roles: `OWNER`, `MEMBER`, `DEVELOPER`, `VIEWER`.

### Projects

```bash
# Create project (with optional env vars, git, and build config)
curl -X POST http://localhost:4000/v11/projects \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app", "framework": "nextjs", "buildCommand": "next build", "outputDirectory": ".next", "nodeVersion": "20.x", "environmentVariables": [{"key": "API_KEY", "value": "secret", "type": "encrypted", "target": ["production"]}]}'

# 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 (framework, buildCommand, devCommand, installCommand,
#   outputDirectory, rootDirectory, nodeVersion, serverlessFunctionRegion,
#   publicSource, autoAssignCustomDomains, gitForkProtection,
#   commandForIgnoringBuildStep)
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, protection bypasses)
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: generate, revoke, regenerate
curl -X PATCH http://localhost:4000/v1/projects/my-app/protection-bypass \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"generate": {"note": "CI preview", "scope": "deployment"}}'

# Revoke protection bypass secrets
curl -X PATCH http://localhost:4000/v1/projects/my-app/protection-bypass \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"revoke": ["secret_to_revoke"]}'

# Regenerate protection bypass secrets
curl -X PATCH http://localhost:4000/v1/projects/my-app/protection-bypass \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"regenerate": ["old_secret"]}'
```

### Deployments

```bash
# 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", "meta": {"commit": "abc123"}, "regions": ["iad1"], "gitSource": {"type": "github", "ref": "main", "sha": "abc123", "repoId": "123", "org": "my-org", "repo": "my-app", "message": "Deploy", "authorName": "dev", "commitAuthorName": "dev"}}'

# Targets: "production", "preview", "staging"

# Get deployment (by ID or URL)
curl http://localhost:4000/v13/deployments/dpl_abc123 \
  -H "Authorization: Bearer $TOKEN"

# List deployments (filter by projectId, app, target, state; cursor paginated)
curl "http://localhost:4000/v6/deployments?projectId=my-app&target=production&limit=10" \
  -H "Authorization: Bearer $TOKEN"

# Delete deployment
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 (supports direction, limit)
curl "http://localhost:4000/v3/deployments/dpl_abc123/events?direction=forward&limit=50" \
  -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.txt
```

### Domains

```bash
# Add domain (with optional redirect, gitBranch, customEnvironmentId)
curl -X POST http://localhost:4000/v10/projects/my-app/domains \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "example.com", "redirect": null, "redirectStatusCode": null, "gitBranch": null}'

# *.vercel.app domains are auto-verified

# List domains (cursor paginated)
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"
```

Redirect status codes: `301`, `302`, `307`, `308`.

### Environment Variables

```bash
# List env vars (with decrypt option; filter by gitBranch, customEnvironmentId)
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?upsert=true" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key": "API_KEY", "value": "secret123", "type": "encrypted", "target": ["production", "preview"], "comment": "API key for service"}'

# 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"
```

Env var types: `system`, `encrypted`, `plain`, `secret`, `sensitive`.

### Blob

Implements the Vercel Blob API used by the `@vercel/blob` SDK (`put`, `head`, `list`, `del`). Point the SDK at the emulator with two environment variables:

```bash
VERCEL_BLOB_API_URL=http://localhost:4000/api/blob
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_mystore_secret
```

Any token of the form `vercel_blob_rw_&lt;storeId&gt;_&lt;secret&gt;` is accepted; the store id is parsed from the token.

```typescript
import { put, head, list, del } from '@vercel/blob'

const blob = await put('avatars/user.png', data, { access: 'public' })
// blob.url serves the bytes from the emulator
await head(blob.url)
await list({ prefix: 'avatars/' })
await del(blob.url)
```

Direct HTTP:

```bash
BLOB_TOKEN="vercel_blob_rw_mystore_secret"

# Upload (honors x-add-random-suffix, x-allow-overwrite, x-content-type,
#   x-cache-control-max-age, x-if-match headers)
curl -X PUT "http://localhost:4000/api/blob?pathname=docs/readme.txt" \
  -H "Authorization: Bearer $BLOB_TOKEN" \
  --data-binary @readme.txt

# Metadata (head)
curl "http://localhost:4000/api/blob?url=docs/readme.txt" \
  -H "Authorization: Bearer $BLOB_TOKEN"

# List (prefix, limit, cursor, mode=folded)
curl "http://localhost:4000/api/blob?prefix=docs/" \
  -H "Authorization: Bearer $BLOB_TOKEN"

# Delete
curl -X POST http://localhost:4000/api/blob/delete \
  -H "Authorization: Bearer $BLOB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["docs/readme.txt"]}'

# Serve content (public, no auth; ?download=1 forces attachment)
curl http://localhost:4000/blob/mystore/docs/readme.txt
```

Multipart uploads and client (browser) uploads are not supported yet.

### API Keys

```bash
# Create API key (optional teamId scope)
curl -X POST "http://localhost:4000/v1/api-keys?teamId=team_abc123" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "CI Deploy Key"}'

# List API keys (optional teamId filter)
curl "http://localhost:4000/v1/api-keys?teamId=team_abc123" \
  -H "Authorization: Bearer $TOKEN"

# Delete API key
curl -X DELETE http://localhost:4000/v1/api-keys/ak_abc123 \
  -H "Authorization: Bearer $TOKEN"
```

Created API keys are automatically registered in the token map and can be used as Bearer tokens for all endpoints.

### OAuth / Integrations

```bash
# Authorize (browser flow, shows user picker)
# GET /oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...

# Token exchange (supports PKCE; accepts JSON or form-urlencoded)
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, email_verified, name, preferred_username, picture)
curl http://localhost:4000/login/oauth/userinfo \
  -H "Authorization: Bearer $TOKEN"
```

## Common Patterns

### Create Project and Deploy

```bash
TOKEN="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

1. Redirect user to `$VERCEL_EMULATOR_URL/oauth/authorize?client_id=...&redirect_uri=...&state=...`
2. User picks a seeded user on the emulator's UI
3. Emulator redirects back with `?code=...&state=...`
4. Exchange code for token via `POST /login/oauth/token`
5. 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:

```bash
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

&lt;!-- SYNCED from vercel-labs/emulate (skills/emulate/SKILL.md) --&gt;
&lt;!-- Hash: f948aead4a7cb4ad0bd38af85cb1fdd256d1a8711b143fcc4bf9d7f21b7ac2b8 --&gt;
&lt;!-- Re-sync: bash scripts/sync-vercel-skills.sh --&gt;


# 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

```bash
npx emulate
```

All services start with sensible defaults:

| Service   | Default Port |
|-----------|-------------|
| Vercel    | 4000        |
| GitHub    | 4001        |
| Google    | 4002        |
| Slack     | 4003        |
| Apple     | 4004        |
| Microsoft | 4005        |
| Okta      | 4006        |
| AWS       | 4007        |
| Resend    | 4008        |
| Stripe    | 4009        |
| MongoDB Atlas | 4010   |
| Clerk     | 4011        |
| Linear    | 4012        |
| Twilio    | 4013        |

## CLI

```bash
# Start all services (zero-config)
npx emulate

# Start specific services
npx emulate --service vercel,github

# Custom base port (auto-increments per service)
npx emulate --port 3000

# Use a seed config file
npx emulate --seed config.yaml

# Generate a starter config
npx emulate init

# Generate config for a specific service
npx emulate init --service vercel

# List available services
npx emulate list
```

### Options

| 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) |
| `--base-url` | none | Override advertised base URL (supports `\{service\}` template) |
| `--portless` | off | Serve over HTTPS via portless (auto-registers aliases) |

The port can also be set via `EMULATE_PORT` or `PORT` environment variables.

The advertised base URL (used in OAuth redirects, webhook URLs, etc.) can be overridden via `--base-url`, the `EMULATE_BASE_URL` env var (supports `\{service\}` template), or per-service `baseUrl` in the seed config. When running under portless, the `PORTLESS_URL` env var is also detected automatically.

## Programmatic API

```bash
npm install emulate
```

Each call to `createEmulator` starts a single service:

```typescript
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)* | `'vercel'`, `'github'`, `'google'`, `'slack'`, `'apple'`, `'microsoft'`, `'okta'`, `'aws'`, `'resend'`, `'stripe'`, `'mongoatlas'`, `'clerk'`, `'linear'`, or `'twilio'` |
| `port` | `4000` | Port for the HTTP server |
| `seed` | none | Inline seed data (same shape as YAML config) |
| `baseUrl` | none | Override advertised base URL. Per-service `baseUrl` in seed config takes highest priority, then this option, then `EMULATE_BASE_URL` env var (supports `\{service\}`), then `PORTLESS_URL` (supports `\{service\}`, automatically set by the `portless` CLI wrapper), then `http://localhost:&lt;port&gt;`. |

### 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

```typescript
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_EMULATOR_URL = github.url
  process.env.VERCEL_EMULATOR_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:

1. `emulate.config.yaml` / `.yml`
2. `emulate.config.json`
3. `service-emulator.config.yaml` / `.yml`
4. `service-emulator.config.json`

Or pass `--seed &lt;file&gt;` explicitly. Run `npx emulate init` to generate a starter file.

### Config Structure

```yaml
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/google

slack:
  team:
    name: My Workspace
    domain: my-workspace
  users:
    - name: developer
      real_name: Developer
      email: dev@example.com
  channels:
    - name: general
      topic: General discussion
  bots:
    - name: my-bot
  oauth_apps:
    - client_id: "12345.67890"
      client_secret: example_client_secret
      name: My Slack App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/slack

linear:
  organization:
    name: Acme
    url_key: acme
  users:
    - email: admin@example.com
      name: Admin User
      admin: true
    - email: dev@example.com
      name: Developer
  teams:
    - key: ENG
      name: Engineering
  issues:
    - team: ENG
      title: Fix local checkout test
      state: Todo
      assignee: dev@example.com
  oauth_apps:
    - client_id: lin_example_client_id
      client_secret: example_client_secret
      name: My Linear App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/linear
      scopes: [read, write, issues:create, comments:create]
  tokens:
    - token: lin_test_admin
      user: admin@example.com
      scopes: [read, write, issues:create, comments:create, admin]

apple:
  users:
    - email: testuser@icloud.com
      name: Test User
  oauth_clients:
    - client_id: com.example.app
      team_id: TEAM001
      name: My Apple App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/apple

microsoft:
  users:
    - email: testuser@outlook.com
      name: Test User
  oauth_clients:
    - client_id: example-client-id
      client_secret: example-client-secret
      name: My Microsoft App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/microsoft-entra-id

aws:
  region: us-east-1
  s3:
    buckets:
      - name: my-app-bucket
  sqs:
    queues:
      - name: my-app-events
  iam:
    users:
      - user_name: developer
        create_access_key: true
    roles:
      - role_name: lambda-execution-role
```

### Auth

Tokens map to users. Pass them as `Authorization: Bearer &lt;token&gt;` or `Authorization: token &lt;token&gt;`. When no tokens are configured, a default `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.

## HTTPS with portless

[portless](https://github.com/vercel-labs/portless) gives emulators trusted HTTPS URLs with auto-generated certs. Use the `--portless` flag to auto-register each service as a portless alias:

```bash
npx emulate start --portless
# github  https://github.emulate.localhost
# google  https://google.emulate.localhost
# ...
```

This requires the portless proxy to be running (`portless proxy start`). If portless is not installed, emulate will prompt to install it.

The `--portless` flag overwrites any existing portless aliases matching `*.emulate`. Aliases are removed automatically when emulate shuts down.

For a single service behind portless:

```bash
portless github.emulate emulate start --service github
```

For a custom base URL without portless (any reverse proxy):

```bash
npx emulate start --base-url "https://{service}.myproxy.test"
# or
EMULATE_BASE_URL="https://{service}.myproxy.test" npx emulate start
```

The `PORTLESS_URL` env var is automatically set by the `portless` CLI wrapper when running a command through it (e.g. `portless github.emulate emulate start`), typically to a value like `https://\{service\}.emulate.localhost`. It supports `\{service\}` interpolation, just like `--base-url` and `EMULATE_BASE_URL`. When no explicit `baseUrl` is provided, it is used as a fallback.

Per-service overrides in the seed config (these take highest priority over all other base URL sources):

```yaml
github:
  baseUrl: https://github.emulate.localhost
google:
  baseUrl: https://google.emulate.localhost
```

## Pointing Your App at the Emulator

Set environment variables to override real service URLs:

```bash
VERCEL_EMULATOR_URL=http://localhost:4000
GITHUB_EMULATOR_URL=http://localhost:4001
GOOGLE_EMULATOR_URL=http://localhost:4002
SLACK_EMULATOR_URL=http://localhost:4003
APPLE_EMULATOR_URL=http://localhost:4004
MICROSOFT_EMULATOR_URL=http://localhost:4005
AWS_EMULATOR_URL=http://localhost:4007
LINEAR_EMULATOR_URL=http://localhost:4012
```

Then use these in your app to construct API and OAuth URLs. See each service's skill for SDK-specific override instructions.

## Framework Integration (Embedded Mode)

The `@emulators/adapter-next` package embeds emulators directly into a Next.js app on the same origin. See the **next** skill (`skills/next/SKILL.md`) for full setup, Auth.js configuration, persistence, and font tracing details.

The `@emulators/adapter-nuxt` package embeds emulators directly into a Nuxt app on the same origin. See the **nuxt** skill (`skills/nuxt/SKILL.md`) for the server route, Nuxt config, OAuth configuration, and persistence setup.

## Persistence

By default, all emulator state is in-memory. For persistence across process restarts and serverless cold starts, use a `PersistenceAdapter`.

### Built-in file persistence

```typescript
import { filePersistence } from '@emulators/core'

// CLI or local dev: persists to a JSON file
const adapter = filePersistence('.emulate/state.json')
```

### Custom adapters

```typescript
import type { PersistenceAdapter } from '@emulators/core'

const kvAdapter: PersistenceAdapter = {
  async load() { return await kv.get('emulate-state') },
  async save(data) { await kv.set('emulate-state', data) },
}
```

State is loaded on cold start and saved after every mutating request (POST, PUT, PATCH, DELETE). Saves are serialized to prevent race conditions.

## Architecture

```
packages/
  emulate/           # CLI entry point + programmatic API
  @emulators/
    core/            # HTTP server, Store, plugin interface, middleware
    adapter-next/    # Next.js App Router integration
    adapter-nuxt/    # Nuxt server route integration
    vercel/          # Vercel API service plugin
    github/          # GitHub API service plugin
    google/          # Google OAuth 2.0 / OIDC plugin
    slack/           # Slack Web API, OAuth, incoming webhooks plugin
    linear/          # Linear GraphQL API, OAuth, webhooks plugin
    twilio/          # Twilio Messaging, Verify, Voice, webhooks plugin
    apple/           # Sign in with Apple / OIDC plugin
    microsoft/       # Microsoft Entra ID OAuth 2.0 / OIDC plugin
    aws/             # AWS S3, SQS, IAM, STS plugin
```

The core provides a generic `Store` with typed `Collection&lt;T&gt;` instances supporting CRUD, indexing, filtering, and pagination. Each service plugin registers routes with the shared internal app and uses the store for state.
