# The Engine Under the Hood — @emulators/core

> What the shared core package actually provides: the in-memory Store (collections of entities), the Hono HTTP server, auth middleware, OAuth helpers, webhook dispatcher, and pagination — the pieces every service plugin relies on.

- Repository: vercel-labs/emulate
- GitHub: https://github.com/vercel-labs/emulate
- Human wiki: https://grok-wiki.com/public/wiki/vercel-labs-emulate-ddc6091d171d
- Complete Markdown: https://grok-wiki.com/public/wiki/vercel-labs-emulate-ddc6091d171d/llms-full.txt

## Source Files

- `packages/@emulators/core/src/index.ts`
- `packages/@emulators/core/src/server.ts`
- `packages/@emulators/core/src/plugin.ts`
- `packages/@emulators/core/src/middleware/auth.ts`
- `packages/@emulators/core/src/persistence.ts`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/@emulators/core/src/index.ts](packages/@emulators/core/src/index.ts)
- [packages/@emulators/core/src/server.ts](packages/@emulators/core/src/server.ts)
- [packages/@emulators/core/src/plugin.ts](packages/@emulators/core/src/plugin.ts)
- [packages/@emulators/core/src/store.ts](packages/@emulators/core/src/store.ts)
- [packages/@emulators/core/src/middleware/auth.ts](packages/@emulators/core/src/middleware/auth.ts)
- [packages/@emulators/core/src/middleware/pagination.ts](packages/@emulators/core/src/middleware/pagination.ts)
- [packages/@emulators/core/src/webhooks.ts](packages/@emulators/core/src/webhooks.ts)
- [packages/@emulators/core/src/oauth-helpers.ts](packages/@emulators/core/src/oauth-helpers.ts)
- [packages/@emulators/core/src/persistence.ts](packages/@emulators/core/src/persistence.ts)
</details>

# The Engine Under the Hood — @emulators/core

`@emulators/core` is the shared foundation that every service emulator in this monorepo builds on. It provides a single `createServer` function that wires together an HTTP server, an in-memory data store, authentication middleware, a webhook dispatcher, and pagination helpers — so individual service plugins (GitHub, Stripe, etc.) only need to define their own routes and seed data, not rebuild all the plumbing.

Think of it as a mini-framework: you plug in a `ServicePlugin` and get back a fully configured local API server that speaks the same token-auth, rate-limiting, and pagination conventions as the real services it emulates.

---

## Architecture Overview

```text
┌─────────────────────────────────────────────────────────────┐
│                      createServer(plugin)                    │
│                                                             │
│  ┌──────────┐   ┌───────────────────────────────────────┐  │
│  │  Store   │   │         Hono HTTP App (AppEnv)        │  │
│  │          │   │                                       │  │
│  │ collection│  │  registerFontRoutes(app)              │  │
│  │ getData  │   │  app.onError(createApiErrorHandler)   │  │
│  │ setData  │   │  app.use("*", cors())                 │  │
│  │ snapshot │   │  app.use("*", createErrorHandler)     │  │
│  │ restore  │   │  app.use("*", authMiddleware)         │  │
│  └──────────┘   │  app.use("*", rateLimitMiddleware)    │  │
│                  │  plugin.register(app, store, ...)    │  │
│  ┌───────────┐  │  app.notFound(...)                   │  │
│  │ Webhook   │  └───────────────────────────────────────┘  │
│  │Dispatcher │                                             │
│  │           │   Returns: { app, store, webhooks,         │
│  │ register  │             port, baseUrl, tokenMap }       │
│  │ dispatch  │                                             │
│  └───────────┘                                             │
└─────────────────────────────────────────────────────────────┘
```

Sources: [packages/@emulators/core/src/server.ts:24-105]()

---

## The Store: In-Memory Data Layer

The `Store` is the central data container — a named registry of `Collection<T>` objects, each holding a typed set of entities. It requires no database; all data lives in process memory, making emulators fast and hermetic.

### Entity Shape

Every entity stored in a `Collection` must conform to `Entity`:

```ts
// packages/@emulators/core/src/store.ts
export interface Entity {
  id: number;
  created_at: string;
  updated_at: string;
}
```

When you call `collection.insert(data)`, the store auto-assigns an integer `id` (auto-incrementing unless you supply one explicitly) and stamps `created_at` / `updated_at` with the current ISO timestamp.

Sources: [packages/@emulators/core/src/store.ts:1-7, 99-115]()

### Collection CRUD and Indexing

`Collection<T>` is the typed workhorse:

| Method | What it does |
|---|---|
| `insert(data)` | Adds a new entity, returns it with `id` and timestamps |
| `get(id)` | Retrieves by numeric ID |
| `findBy(field, value)` | Filters by field value, uses index when available |
| `findOneBy(field, value)` | First match for a field value |
| `update(id, data)` | Merges partial data, updates `updated_at`, refreshes indexes |
| `delete(id)` | Removes entity and cleans index entries |
| `all()` | All items as an array |
| `query(options)` | Filter + sort + paginate, returns `PaginatedResult<T>` |
| `count(filter?)` | Efficient count, optionally filtered |
| `clear()` | Empties the collection and resets auto-ID |

When you open a collection with `indexFields`, the `Collection` maintains per-field hash maps (`Map<string, Set<number>>`) for O(1) lookups on those fields. This is why `findBy` on an indexed field is faster than a full scan.

```ts
// A service plugin obtains a collection like this:
const repos = store.collection<Repo>("repos", ["owner_id", "name"]);
```

Sources: [packages/@emulators/core/src/store.ts:63-219]()

### The Store Container

`Store` wraps multiple collections and also provides a freeform key-value bag (`getData` / `setData`) for any data that doesn't fit the entity model (counters, config state, etc.).

```ts
export class Store {
  collection<T extends Entity>(name: string, indexFields?: (keyof T)[]): Collection<T>
  getData<V>(key: string): V | undefined
  setData<V>(key: string, value: V): void
  reset(): void
  snapshot(): StoreSnapshot
  restore(snap: StoreSnapshot): void
}
```

`snapshot()` serializes all collections (including any `Map` or `Set` values via `serializeValue`) into a plain `StoreSnapshot` object. `restore(snap)` reconstructs them. This pair is the persistence hook.

Sources: [packages/@emulators/core/src/store.ts:221-287]()

---

## The HTTP Server: Hono + Middleware Stack

`createServer` builds a [Hono](https://hono.dev/) application and stacks middleware in a fixed order before handing control to the plugin.

```ts
export function createServer(plugin: ServicePlugin, options: ServerOptions = {})
```

### ServerOptions

| Option | Default | Purpose |
|---|---|---|
| `port` | `4000` | TCP port to listen on |
| `baseUrl` | `http://localhost:{port}` | Base URL used in Link headers and docs links |
| `docsUrl` | `https://emulate.dev/{plugin.name}` | URL embedded in error responses |
| `tokens` | `{}` | Pre-seeded token → user mapping |
| `appKeyResolver` | — | Resolves GitHub App RSA keys for JWT auth |
| `fallbackUser` | — | User identity for unknown tokens (dev convenience) |

Sources: [packages/@emulators/core/src/server.ts:15-22]()

### Middleware Layer Order

The layers applied to every request, in order:

```text
Request
  │
  ▼
[Font routes]          — static font assets
  │
  ▼
[CORS]                 — permissive cors() for all origins
  │
  ▼
[Error handler]        — wraps downstream errors into JSON
  │
  ▼
[Auth middleware]      — resolves token/JWT → authUser / authApp on context
  │
  ▼
[Rate limit]           — 5 000 req/hour per token, sliding window with pruning
  │
  ▼
[plugin.register]      — service-specific routes (GitHub, Stripe, …)
  │
  ▼
[notFound]             — 404 JSON fallback
```

Sources: [packages/@emulators/core/src/server.ts:45-103]()

### Rate Limiting

The server tracks per-token request counts in a `Map<string, { remaining, resetAt }>`. The window is 1 hour; the limit is 5 000 requests. Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers to match the real GitHub API convention. When the counter hits zero, the server returns HTTP 403. Stale entries are pruned once per hour.

Sources: [packages/@emulators/core/src/server.ts:52-90]()

---

## Auth Middleware

`authMiddleware` runs on every request and resolves who is calling.

### Two Auth Paths

**1. Bearer token (static):** If the `Authorization` header contains a plain token, it is looked up in the `TokenMap` (`Map<string, AuthUser>`). On a hit, the Hono context variables `authUser`, `authToken`, and `authScopes` are set.

**2. JWT (GitHub App):** If the token starts with `eyJ` (a Base64-encoded JWT header), the middleware extracts the `iss` claim as the app ID, resolves the RSA private key via `appKeyResolver`, and verifies the JWT with `jose`. On success, `authApp` is set instead.

```ts
// packages/@emulators/core/src/middleware/auth.ts
export function authMiddleware(tokens: TokenMap, appKeyResolver?: AppKeyResolver, fallbackUser?: AuthFallback)
```

A `fallbackUser` option lets any unknown token resolve to a default identity — useful in dev environments where you don't care about specific users.

Sources: [packages/@emulators/core/src/middleware/auth.ts:70-112]()

### Guard Helpers

Two route-level guards are exported for use inside service plugins:

| Helper | Blocks when | Response |
|---|---|---|
| `requireAuth()` | `authUser` is absent | HTTP 401 "Requires authentication" |
| `requireAppAuth()` | `authApp` is absent | HTTP 401 "A JSON web token could not be decoded" |

Sources: [packages/@emulators/core/src/middleware/auth.ts:114-144]()

### Token Serialization

The `TokenMap` (a `Map<string, AuthUser>`) can be exported to a plain array with `serializeTokenMap` and restored with `restoreTokenMap`. This supports snapshot/restore workflows where the token state must survive a process restart.

Sources: [packages/@emulators/core/src/middleware/auth.ts:34-48]()

---

## WebhookDispatcher

The `WebhookDispatcher` manages subscriptions and fires real HTTP `POST` requests to registered URLs whenever a service plugin triggers an event. It mirrors the GitHub webhooks model.

### Lifecycle

```text
plugin calls webhooks.register(sub)
    ↓
event happens in service plugin
    ↓
plugin calls webhooks.dispatch(event, action, payload, owner, repo?)
    ↓
dispatcher filters active subscriptions matching owner/repo/event
    ↓
for each match:
  - serialize payload to JSON
  - compute HMAC-SHA256 signature if secret present → X-Hub-Signature-256
  - POST to sub.url with X-GitHub-Event, X-GitHub-Delivery headers
  - record WebhookDelivery (status, duration, success)
  - cap delivery log at 1000 entries
```

Sources: [packages/@emulators/core/src/webhooks.ts:73-138]()

### Data Shapes

```ts
interface WebhookSubscription {
  id: number;
  url: string;
  events: string[];   // ["push", "pull_request"] or ["*"]
  active: boolean;
  secret?: string;
  owner: string;
  repo?: string;      // undefined = org-level hook
}

interface WebhookDelivery {
  id: number;
  hook_id: number;
  event: string;
  action?: string;
  payload: unknown;
  status_code: number | null;
  delivered_at: string;
  duration: number | null;
  success: boolean;
}
```

A 10-second `AbortSignal.timeout` guards each delivery so a slow or unresponsive receiver doesn't stall the emulator.

Sources: [packages/@emulators/core/src/webhooks.ts:1-23, 112-124]()

---

## Pagination

`parsePagination` and `setLinkHeader` are the two helpers that implement RFC 5988-style cursor-free pagination.

```ts
// packages/@emulators/core/src/middleware/pagination.ts
export function parsePagination(c: Context): PaginationParams
export function setLinkHeader(c: Context, totalCount: number, page: number, perPage: number): void
```

`parsePagination` reads `?page=` and `?per_page=` query parameters, defaulting to page 1 and 30 items per page, clamping `per_page` to a maximum of 100.

`setLinkHeader` computes `next`, `last`, `first`, and `prev` URLs and writes them as a single `Link:` header value, matching the GitHub API's pagination contract.

The `Collection.query()` method respects the same `page` / `per_page` options and returns a `PaginatedResult<T>` with `has_next` and `has_prev` flags alongside the sliced items.

Sources: [packages/@emulators/core/src/middleware/pagination.ts:1-37](), [packages/@emulators/core/src/store.ts:162-188]()

---

## OAuth Helpers

`oauth-helpers.ts` provides small, security-minded utilities used by service plugins that implement OAuth flows:

| Function | Purpose |
|---|---|
| `normalizeUri(uri)` | Strips trailing slashes and query strings for redirect URI comparison |
| `matchesRedirectUri(incoming, registered[])` | Checks normalized incoming URI against an allowlist |
| `constantTimeSecretEqual(a, b)` | Timing-safe string comparison using `crypto.timingSafeEqual` to prevent timing attacks |
| `bodyStr(v)` | Safely coerces a form body value to string |
| `parseCookies(header)` | Splits a `Cookie` header into a key→value map |

`constantTimeSecretEqual` is explicitly designed to prevent secret-comparison timing leaks — the buffer lengths must match before calling `timingSafeEqual`.

Sources: [packages/@emulators/core/src/oauth-helpers.ts:1-37]()

---

## Persistence Adapter

The `PersistenceAdapter` interface is a two-method contract that decouples snapshot storage from the rest of the core:

```ts
export interface PersistenceAdapter {
  load(): Promise<string | null>;
  save(data: string): Promise<void>;
}
```

`filePersistence(path)` is the provided implementation. It writes JSON to a file (creating parent directories with `mkdir -p` semantics) and reads it back, returning `null` if the file doesn't exist. Service plugins call `store.snapshot()` to get a serializable object, stringify it, and hand the string to `adapter.save()`.

Sources: [packages/@emulators/core/src/persistence.ts:1-23]()

---

## ServicePlugin Contract

A service plugin is a plain object with three members:

```ts
export interface ServicePlugin {
  name: string;
  register(app: Hono<AppEnv>, store: Store, webhooks: WebhookDispatcher, baseUrl: string, tokenMap?: TokenMap): void;
  seed?(store: Store, baseUrl: string): void;
}
```

- `name` — used by `createServer` to build the default `docsUrl`.
- `register` — called once during server setup; the plugin mounts all its routes here.
- `seed` (optional) — called separately to populate the store with default test data.

This is the only interface a new emulator package must implement. Everything else — the HTTP server, auth, rate limiting, webhooks, pagination, persistence — comes from `@emulators/core` without configuration.

Sources: [packages/@emulators/core/src/plugin.ts:6-18]()

---

## What `createServer` Returns

```ts
return { app, store, webhooks, port, baseUrl, tokenMap };
```

The caller receives all five live objects. This lets a service's entry point start the HTTP listener, let tests reach into `store` to assert state, or let a CLI seed the store before starting the server — without needing any internal knowledge of how the core package was assembled.

Sources: [packages/@emulators/core/src/server.ts:104]()

---

## Summary

`@emulators/core` is a lean, self-contained runtime kit: it gives every service emulator an in-memory store with indexing and pagination, a Hono HTTP server pre-wired with CORS, error handling, JWT/token auth, and per-token rate limiting, a webhook dispatcher that signs and delivers real HTTP POST calls, OAuth comparison utilities that are safe against timing attacks, and a two-method persistence adapter for snapshotting state to disk. A service plugin only needs to implement the three-member `ServicePlugin` interface — `name`, `register`, and optionally `seed` — and the core handles the rest.

Sources: [packages/@emulators/core/src/index.ts:1-79]()
