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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,8 @@ export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
})
```

GitHub App seeds may omit `private_key`. Retain the handler to call server-only `generatedSecrets()`; explicit keys are excluded. Persisted snapshots contain generated keys, so keep the backend private.

### Auth.js / NextAuth configuration

Point your provider at the emulator paths on the same origin:
Expand Down Expand Up @@ -1143,7 +1145,7 @@ import { filePersistence } from '@emulators/core'
persistence: filePersistence('.emulate/state.json'),
```

The persistence adapter is called on cold start (load) and after every mutating request (save). Saves are serialized via an internal queue to prevent race conditions.
The persistence adapter loads on cold start and saves after mutations. Generated identities also require atomic create-or-read `initialize`; see `@emulators/core`.

## Nuxt Integration

Expand Down Expand Up @@ -1186,6 +1188,8 @@ export default defineEventHandler(createEmulateHandler({
}))
```

GitHub App seeds may omit `private_key`. Retain the handler to call server-only `generatedSecrets()`; explicit keys are excluded. Persisted snapshots contain generated keys, so keep the backend private.

### Nuxt config

Emulator UI pages use bundled fonts. Wrap your Nuxt config so Nitro traces the core package assets into production builds:
Expand Down Expand Up @@ -1236,7 +1240,7 @@ export default defineEventHandler(createEmulateHandler({
}))
```

The persistence adapter is called on cold start (load) and after every mutating request (save). Saves are serialized via an internal queue to prevent race conditions.
The persistence adapter loads on cold start and saves after mutations. Generated identities also require atomic create-or-read `initialize`; see `@emulators/core`.

## Architecture

Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/docs/github/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const privateKey = github.generatedSecrets.find(

The generated RSA-2048 PKCS#1 key remains stable across `reset()` calls. Explicit keys are never included in `generatedSecrets`.

The Next.js and Nuxt adapters also generate omitted keys. Their returned server handlers expose `generatedSecrets()`, and persistence restores the same identity across cold starts. Keep persisted snapshots private because they contain the signing key. Custom persistence backends must implement atomic `initialize()` semantics when generated identities are used.

The CLI can generate omitted keys when you request a private delivery file:

```bash
Expand Down
8 changes: 7 additions & 1 deletion apps/web/app/docs/nextjs/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ This creates the following routes:
- `/emulate/github/**` serves the GitHub emulator
- `/emulate/google/**` serves the Google emulator

## GitHub App Private Keys

GitHub App seeds may omit `private_key`; retain the handler and call server-only `generatedSecrets()`. Keep persisted generated keys private and implement atomic `initialize`.

## Auth.js / NextAuth Configuration

Point your provider at the emulator paths on the same origin:
Expand Down Expand Up @@ -117,6 +121,8 @@ import { filePersistence } from '@emulators/core'
persistence: filePersistence('.emulate/state.json'),
```

Generated identities require atomic create-or-read `initialize`; see `@emulators/core`.

### How It Works

- **Cold start**: The adapter loads state from the persistence adapter. If found, it restores the full Store and token map (skipping seed). If not found, it seeds from config and saves the initial state.
Expand All @@ -134,4 +140,4 @@ persistence: filePersistence('.emulate/state.json'),
## Limitations

- Requires the Node.js runtime (not Edge) since emulators use `crypto.randomBytes`
- Concurrent serverless instances writing to the same persistence adapter use last-write-wins semantics (acceptable for dev/preview traffic)
- Concurrent mutations use last-write-wins semantics. Generated identities use `initialize` to select the initial snapshot atomically across cold starts.
8 changes: 7 additions & 1 deletion apps/web/app/docs/nuxt/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ This creates these routes:
- `/emulate/github/**` serves the GitHub emulator
- `/emulate/google/**` serves the Google emulator

## GitHub App Private Keys

GitHub App seeds may omit `private_key`; retain the handler and call server-only `generatedSecrets()`. Keep persisted generated keys private and implement atomic `initialize`.

## Nuxt Config

Emulator UI pages use bundled fonts. Wrap your Nuxt config so Nitro traces the core package assets into production builds:
Expand Down Expand Up @@ -106,6 +110,8 @@ import { filePersistence } from '@emulators/core'
persistence: filePersistence('.emulate/state.json'),
```

Generated identities require atomic create-or-read `initialize`; see `@emulators/core`.

### How It Works

- **Cold start**: The adapter loads state from the persistence adapter. If found, it restores the full Store and token map. If not found, it seeds from config and saves the initial state.
Expand Down Expand Up @@ -137,4 +143,4 @@ export default defineEventHandler(createEmulateHandler(config, { routePrefix: '/
## Limitations

- Requires a Node-compatible Nuxt server runtime since emulators use Node APIs
- Concurrent serverless instances writing to the same persistence adapter use last write wins semantics, which is acceptable for dev and preview traffic
- Concurrent mutations use last-write-wins semantics. Generated identities use `initialize` to select the initial snapshot atomically across cold starts.
6 changes: 5 additions & 1 deletion packages/@emulators/adapter-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { createEmulateHandler } from '@emulators/adapter-next'
import * as github from '@emulators/github'
import * as google from '@emulators/google'

export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
export const emulator = createEmulateHandler({
services: {
github: {
emulator: github,
Expand All @@ -43,8 +43,12 @@ export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
},
},
})

export const { GET, POST, PUT, PATCH, DELETE } = emulator
```

GitHub App seeds may omit `private_key`. Read generated keys with the handler's server-only `generatedSecrets()` method. Explicit keys are excluded; persisted snapshots contain generated keys and require a private backend with atomic `initialize`.

## Auth.js / NextAuth configuration

Point your provider at the emulator paths on the same origin:
Expand Down
5 changes: 4 additions & 1 deletion packages/@emulators/adapter-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@
"build": "tsup --clean",
"dev": "tsup --watch",
"clean": "rm -rf dist .turbo",
"test": "vitest run",
"type-check": "tsc --noEmit",
"lint": "eslint src"
},
"dependencies": {
"@emulators/core": "workspace:*"
},
"devDependencies": {
"@emulators/github": "workspace:*",
"tsup": "^8",
"typescript": "^5.7"
"typescript": "^5.7",
"vitest": "^4.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as github from "@emulators/github";
import { githubAppIdentityContract, type TestPersistence } from "../../../../../tests/contracts/github-app-identity.js";
import { createEmulateHandler, type EmulateHandlerConfig } from "../index.js";
function config(persistence?: TestPersistence, privateKey?: string): EmulateHandlerConfig {
return {
services: {
github: {
emulator: github,
seed: { apps: [{ app_id: 123, slug: "embedded", name: "Embedded", private_key: privateKey }] },
},
},
persistence,
};
}
githubAppIdentityContract<EmulateHandlerConfig, ReturnType<typeof createEmulateHandler>>({
createEmulateHandler,
config,
async createExplicitPrivateKey() {
const prepared = await github.materializeGitHubSeedConfig({ apps: [{ app_id: 123, slug: "key", name: "Key" }] });
return prepared.config.apps![0]!.private_key!;
},
requestApp(handler, authorization, method = "GET") {
return handler[method](
new Request("http://localhost/emulate/github/app", {
method,
headers: authorization ? { Authorization: authorization } : undefined,
}),
{ params: Promise.resolve({ path: ["github", "app"] }) },
);
},
});
Loading
Loading