Skip to content

Commit 6d371bd

Browse files
authored
Add Linear emulator (#180)
* Add Linear emulator - Add a stateful Linear service with GraphQL, OAuth, webhooks, inspector, and seed support. - Register Linear in the CLI package and default service config. - Document the supported surface and cover it with Linear SDK conformance tests. * Reject Linear refresh tokens for API auth * Fix Linear strict scope enforcement * Fix Linear issue or filters * Fix Linear emulator edge cases * Fix Linear emulator edge cases * Fix Linear seed override handling * Fix Linear SDK compatibility * Fix Linear issue mutation validation * Fix Linear SDK mutation compatibility * Fix Linear OAuth Basic auth parsing * Fix Linear GraphQL validation edge cases * Fix Linear docs site wiring
1 parent 025ee33 commit 6d371bd

32 files changed

Lines changed: 5704 additions & 27 deletions

README.md

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ All services start with sensible defaults. No config file needed:
1616
- **Slack** on `http://localhost:4003`
1717
- **Apple** on `http://localhost:4004`
1818
- **Microsoft** on `http://localhost:4005`
19-
- **AWS** on `http://localhost:4006`
19+
- **Okta** on `http://localhost:4006`
20+
- **AWS** on `http://localhost:4007`
21+
- **Resend** on `http://localhost:4008`
22+
- **Stripe** on `http://localhost:4009`
23+
- **MongoDB Atlas** on `http://localhost:4010`
24+
- **Clerk** on `http://localhost:4011`
25+
- **Linear** on `http://localhost:4012`
2026

2127
## CLI
2228

@@ -141,7 +147,7 @@ afterAll(() => Promise.all([github.close(), vercel.close()]))
141147

142148
| Option | Default | Description |
143149
|--------|---------|-------------|
144-
| `service` | *(required)* | Service name: `'vercel'`, `'github'`, `'google'`, `'slack'`, `'apple'`, `'microsoft'`, or `'aws'` |
150+
| `service` | *(required)* | Service name: `'vercel'`, `'github'`, `'google'`, `'slack'`, `'apple'`, `'microsoft'`, `'okta'`, `'aws'`, `'resend'`, `'stripe'`, `'mongoatlas'`, `'clerk'`, or `'linear'` |
145151
| `port` | `4000` | Port for the HTTP server |
146152
| `seed` | none | Inline seed data (same shape as YAML config) |
147153
| `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:<port>`. |
@@ -332,6 +338,51 @@ slack:
332338
- team:read
333339
strict_scopes: false
334340

341+
linear:
342+
organization:
343+
name: Acme
344+
url_key: acme
345+
users:
346+
- email: admin@example.com
347+
name: Admin User
348+
admin: true
349+
- email: dev@example.com
350+
name: Developer
351+
teams:
352+
- key: ENG
353+
name: Engineering
354+
states:
355+
- name: Backlog
356+
type: backlog
357+
- name: Todo
358+
type: unstarted
359+
- name: In Progress
360+
type: started
361+
- name: Done
362+
type: completed
363+
labels:
364+
- name: Bug
365+
color: "#d92d20"
366+
team: ENG
367+
issues:
368+
- team: ENG
369+
title: Fix local checkout test
370+
state: Todo
371+
assignee: dev@example.com
372+
labels: [Bug]
373+
oauth_apps:
374+
- client_id: lin_example_client_id
375+
client_secret: example_client_secret
376+
name: My Linear App
377+
redirect_uris:
378+
- http://localhost:3000/api/auth/callback/linear
379+
scopes: [read, write, issues:create, comments:create]
380+
tokens:
381+
- token: lin_test_admin
382+
user: admin@example.com
383+
scopes: [read, write, issues:create, comments:create, admin]
384+
strict_scopes: false
385+
335386
apple:
336387
users:
337388
- email: testuser@icloud.com
@@ -447,6 +498,20 @@ slack:
447498
- "http://localhost:3000/api/auth/callback/slack"
448499
```
449500

501+
### Linear OAuth Apps
502+
503+
```yaml
504+
linear:
505+
oauth_apps:
506+
- client_id: "lin_example_client_id"
507+
client_secret: "example_client_secret"
508+
name: "My Linear App"
509+
redirect_uris:
510+
- "http://localhost:3000/api/auth/callback/linear"
511+
scopes: [read, write, issues:create, comments:create]
512+
actor: user
513+
```
514+
450515
### Apple OAuth Clients
451516

452517
```yaml
@@ -771,6 +836,35 @@ Slack scope checks are relaxed by default so local tests can use simple bearer t
771836

772837
Current Slack limits: Slack Connect, Enterprise Grid admin APIs, Audit Logs API, SCIM, Legal Holds, Socket Mode, slash command and interaction simulation, user groups, reminders, stars, calls, canvases, lists, functions, workflows, chat streaming, legacy `files.upload`, exact rate limiting, and paid-plan behavior are not implemented.
773838

839+
## Linear API
840+
841+
Stateful Linear GraphQL API emulation with seeded organizations, users, teams, workflow states, issues, comments, labels, projects, cycles, OAuth apps, tokens, webhooks, and basic agent sessions. GraphQL reads and writes mutate in-memory state and use Relay-style connections with opaque cursors. OAuth supports authorization code, PKCE, refresh token, revoke, client credentials, and `actor=app` tokens for local app-actor tests. Supported writes dispatch Linear-shaped webhook payloads with `Linear-Delivery`, `Linear-Event`, and `Linear-Signature` headers when webhooks are configured.
842+
843+
### GraphQL
844+
845+
- `POST /graphql` - GraphQL endpoint for queries and mutations
846+
- `GET /graphql` - query-string GraphQL endpoint for tooling
847+
- Queries: `viewer`, `organization`, `users`, `user`, `teams`, `team`, `workflowStates`, `workflowState`, `issues`, `issue`, `comments`, `comment`, `issueLabels`, `issueLabel`, `projects`, `project`, `cycles`, `cycle`, `webhooks`, `webhook`, `agentSessions`, `agentSession`
848+
- Mutations: `issueCreate`, `issueUpdate`, `issueDelete`, `issueArchive`, `issueUnarchive`, `commentCreate`, `commentUpdate`, `commentDelete`, `issueLabelCreate`, `issueLabelUpdate`, `issueLabelDelete`, `issueAddLabel`, `issueRemoveLabel`, `webhookCreate`, `webhookDelete`, `agentSessionCreateOnIssue`, `agentSessionCreateOnComment`, `agentSessionUpdate`, `agentActivityCreate`
849+
850+
### OAuth
851+
852+
- `GET /oauth/authorize` - authorization endpoint with local user picker
853+
- `POST /oauth/authorize/callback` - local user picker callback that creates an authorization code
854+
- `POST /oauth/token` - authorization code, refresh token, and client credentials grants
855+
- `POST /oauth/revoke` - revoke access or refresh tokens
856+
857+
OAuth app `actor` config is authoritative. Apps configured with `actor: user` use authorization code flows. Apps configured with `actor: app` use the app install flow and can request client credentials tokens.
858+
859+
### Webhooks And Inspector
860+
861+
- `webhookCreate` / `webhookDelete` manage local webhook subscriptions
862+
- `GET /` - tabbed local inspector for issues, teams, users, projects, agents, auth records, webhook subscriptions, and deliveries
863+
864+
Linear scope checks are relaxed by default so local tests can use simple bearer tokens or the seeded `lin_test_admin` token. Set `linear.strict_scopes: true` in seed config to require `read`, `write`, `issues:create`, `comments:create`, or `admin` on supported GraphQL operations.
865+
866+
Current Linear limits: full schema coverage, exact production rate limiting, notification inbox behavior, rich document APIs, customer APIs, initiative APIs, exact search relevance, and production agent behavior are not implemented. Agent support is a focused local-test subset.
867+
774868
## Apple Sign In
775869

776870
Sign in with Apple emulation with authorization code flow, PKCE support, RS256 ID tokens, and OIDC discovery.
@@ -954,6 +1048,7 @@ packages/
9541048
github/ # GitHub API service
9551049
google/ # Google OAuth 2.0 / OIDC + Gmail, Calendar, Drive
9561050
slack/ # Slack Web API, OAuth v2, incoming webhooks
1051+
linear/ # Linear GraphQL API, OAuth, webhooks
9571052
apple/ # Apple Sign In / OIDC
9581053
microsoft/ # Microsoft Entra ID OAuth 2.0 / OIDC + Graph /me
9591054
aws/ # AWS S3, SQS, IAM, STS
@@ -975,6 +1070,8 @@ Tokens are configured in the seed config and map to users. Pass them as `Authori
9751070

9761071
**Slack**: All Web API endpoints require `Authorization: Bearer <token>`. Seeded OAuth apps create local installation records, and OAuth v2 flow with user picker UI creates scoped bot tokens. Optional strict scope mode returns `missing_scope` when a token lacks a required method scope.
9771072

1073+
**Linear**: GraphQL accepts `Authorization: Bearer <token>` or a bare personal API key value. Seeded Linear tokens map to users or app actors, OAuth apps support local authorization code and client credentials flows, and optional strict scope mode checks supported GraphQL operations.
1074+
9781075
**Apple**: OIDC authorization code flow with RS256 ID tokens. On first auth per user/client pair, a `user` JSON blob is included.
9791076

9801077
**Microsoft**: OIDC authorization code flow with PKCE support. Also supports client credentials grants. Microsoft Graph `/v1.0/me` available.

apps/web/app/api/docs-chat/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const maxDuration = 60;
1212

1313
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
1414

15-
const SYSTEM_PROMPT = `You are a helpful documentation assistant for emulate, a local drop-in replacement for Vercel, GitHub, Google, Slack, Apple, Microsoft, AWS, Okta, MongoDB Atlas, Resend, and Stripe APIs used in CI and no-network sandboxes.
15+
const SYSTEM_PROMPT = `You are a helpful documentation assistant for emulate, a local drop-in replacement for Vercel, GitHub, Google, Slack, Apple, Microsoft, Okta, AWS, Resend, Stripe, MongoDB Atlas, Clerk, and Linear APIs used in CI and no-network sandboxes.
1616
1717
emulate provides fully stateful, production-fidelity API emulation, not mocks. The CLI is installed as the "emulate" npm package and run via "npx emulate". It also supports a programmatic API via createEmulator and a Next.js adapter (@emulators/adapter-next) for embedding emulators in your app.
1818

apps/web/app/docs/architecture/page.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ packages/
1010
github/ # GitHub API service
1111
google/ # Google OAuth 2.0 / OIDC + Gmail, Calendar, Drive
1212
slack/ # Slack Web API, OAuth v2, incoming webhooks
13+
linear/ # Linear GraphQL API, OAuth, webhooks
1314
apple/ # Apple Sign In / OIDC
1415
microsoft/ # Microsoft Entra ID OAuth 2.0 / OIDC + Graph /me
1516
aws/ # AWS S3, SQS, IAM, STS
1617
okta/ # Okta identity provider / OIDC
1718
mongoatlas/ # MongoDB Atlas Admin API + Data API
1819
resend/ # Resend email API
1920
stripe/ # Stripe billing and payments API
21+
clerk/ # Clerk Backend API and OAuth
2022
apps/
2123
web/ # Documentation site (Next.js)
2224
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { pageMetadata } from "@/lib/page-metadata";
2+
3+
export const metadata = pageMetadata("linear");
4+
5+
export default function Layout({ children }: { children: React.ReactNode }) {
6+
return children;
7+
}

apps/web/app/docs/linear/page.mdx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Linear API
2+
3+
Stateful Linear GraphQL API emulation with organizations, users, teams, workflow states, issues, comments, labels, projects, cycles, OAuth apps, tokens, webhooks, and basic agent sessions.
4+
5+
## Start
6+
7+
```bash
8+
npx emulate --service linear
9+
```
10+
11+
When Linear is the only enabled service, it starts on `http://localhost:4000`. When all services are started, it uses `http://localhost:4012`.
12+
13+
## URL Mapping
14+
15+
| Real Linear URL | Emulator URL |
16+
|-----------------|--------------|
17+
| `https://api.linear.app/graphql` | `$LINEAR_EMULATOR_URL/graphql` |
18+
| `https://linear.app/oauth/authorize` | `$LINEAR_EMULATOR_URL/oauth/authorize` |
19+
| `https://api.linear.app/oauth/token` | `$LINEAR_EMULATOR_URL/oauth/token` |
20+
| `https://api.linear.app/oauth/revoke` | `$LINEAR_EMULATOR_URL/oauth/revoke` |
21+
22+
## GraphQL
23+
24+
- `POST /graphql` - GraphQL endpoint for queries and mutations
25+
- `GET /graphql` - query-string GraphQL endpoint for tooling
26+
27+
Supported queries:
28+
29+
- `viewer`
30+
- `organization`
31+
- `users`, `user`
32+
- `teams`, `team`
33+
- `workflowStates`, `workflowState`
34+
- `issues`, `issue`
35+
- `comments`, `comment`
36+
- `issueLabels`, `issueLabel`
37+
- `projects`, `project`
38+
- `cycles`, `cycle`
39+
- `webhooks`, `webhook`
40+
- `agentSessions`, `agentSession`
41+
42+
Supported mutations:
43+
44+
- `issueCreate`, `issueUpdate`, `issueDelete`, `issueArchive`, `issueUnarchive`
45+
- `commentCreate`, `commentUpdate`, `commentDelete`
46+
- `issueLabelCreate`, `issueLabelUpdate`, `issueLabelDelete`
47+
- `issueAddLabel`, `issueRemoveLabel`
48+
- `webhookCreate`, `webhookDelete`
49+
- `agentSessionCreateOnIssue`, `agentSessionCreateOnComment`, `agentSessionUpdate`
50+
- `agentActivityCreate`
51+
52+
Connections use Relay-style cursors with `nodes`, `edges`, and `pageInfo`.
53+
54+
## Auth
55+
56+
GraphQL accepts `Authorization: Bearer <token>` or a bare personal API key value. The default seeded token is `lin_test_admin`.
57+
58+
Scope checks are relaxed by default. Set `linear.strict_scopes: true` to require supported operation scopes such as `read`, `write`, `issues:create`, `comments:create`, and `admin`.
59+
60+
## OAuth
61+
62+
- `GET /oauth/authorize` - authorization endpoint with local user picker
63+
- `POST /oauth/authorize/callback` - local callback used by the user picker
64+
- `POST /oauth/token` - authorization code, refresh token, and client credentials grants
65+
- `POST /oauth/revoke` - revoke access or refresh tokens
66+
67+
OAuth apps can use `actor: user` or `actor: app`. The configured actor is authoritative. User actor apps use authorization code flows. App actor apps use the app install flow and can request client credentials tokens.
68+
69+
## Seed Config
70+
71+
```yaml
72+
linear:
73+
organization:
74+
name: Acme
75+
url_key: acme
76+
users:
77+
- email: admin@example.com
78+
name: Admin User
79+
admin: true
80+
- email: dev@example.com
81+
name: Developer
82+
teams:
83+
- key: ENG
84+
name: Engineering
85+
issues:
86+
- team: ENG
87+
title: Fix local checkout test
88+
state: Todo
89+
assignee: dev@example.com
90+
oauth_apps:
91+
- client_id: lin_example_client_id
92+
client_secret: example_client_secret
93+
name: My Linear App
94+
redirect_uris:
95+
- http://localhost:3000/api/auth/callback/linear
96+
scopes: [read, write, issues:create, comments:create]
97+
tokens:
98+
- token: lin_test_admin
99+
user: admin@example.com
100+
scopes: [read, write, issues:create, comments:create, admin]
101+
```
102+
103+
## Webhooks
104+
105+
Create local webhook subscriptions through `webhookCreate` or seed config. Supported writes dispatch Linear-shaped payloads with these headers:
106+
107+
- `Linear-Delivery`
108+
- `Linear-Event`
109+
- `Linear-Signature`
110+
111+
## Inspector
112+
113+
- `GET /` - tabbed inspector for issues, teams, users, projects, agent sessions, OAuth apps, tokens, webhook subscriptions, and webhook deliveries
114+
115+
## Current Limits
116+
117+
Full Linear schema coverage, exact production rate limiting, notification inbox behavior, rich document APIs, customer APIs, initiative APIs, exact search relevance, and full production agent behavior are not implemented.

apps/web/app/docs/page.mdx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Getting Started
22

3-
Local drop-in replacement for Vercel, GitHub, Google, Slack, Apple, Microsoft, AWS, Okta, MongoDB Atlas, Resend, and Stripe APIs. Built for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation. Not mocks.
3+
Local drop-in replacement for Vercel, GitHub, Google, Slack, Apple, Microsoft, Okta, AWS, Resend, Stripe, MongoDB Atlas, Clerk, and Linear APIs. Built for CI and no-network sandboxes. Fully stateful, production-fidelity API emulation. Not mocks.
44

55
## Quick Start
66

@@ -16,11 +16,13 @@ All services start with sensible defaults. No config file needed:
1616
- **Slack** on `http://localhost:4003`
1717
- **Apple** on `http://localhost:4004`
1818
- **Microsoft** on `http://localhost:4005`
19-
- **AWS** on `http://localhost:4006`
20-
- **Okta** on `http://localhost:4007`
21-
- **MongoDB Atlas** on `http://localhost:4008`
22-
- **Resend** on `http://localhost:4009`
23-
- **Stripe** on `http://localhost:4010`
19+
- **Okta** on `http://localhost:4006`
20+
- **AWS** on `http://localhost:4007`
21+
- **Resend** on `http://localhost:4008`
22+
- **Stripe** on `http://localhost:4009`
23+
- **MongoDB Atlas** on `http://localhost:4010`
24+
- **Clerk** on `http://localhost:4011`
25+
- **Linear** on `http://localhost:4012`
2426

2527
## CLI
2628

apps/web/app/docs/programmatic-api/page.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ afterAll(() => Promise.all([github.close(), vercel.close()]))
5959
<tr>
6060
<td><code>service</code></td>
6161
<td><em>(required)</em></td>
62-
<td>Service name: <code>'vercel'</code>, <code>'github'</code>, <code>'google'</code>, <code>'slack'</code>, <code>'apple'</code>, <code>'microsoft'</code>, <code>'aws'</code>, <code>'okta'</code>, <code>'mongoatlas'</code>, <code>'resend'</code>, or <code>'stripe'</code></td>
62+
<td>Service name: <code>'vercel'</code>, <code>'github'</code>, <code>'google'</code>, <code>'slack'</code>, <code>'apple'</code>, <code>'microsoft'</code>, <code>'okta'</code>, <code>'aws'</code>, <code>'resend'</code>, <code>'stripe'</code>, <code>'mongoatlas'</code>, <code>'clerk'</code>, or <code>'linear'</code></td>
6363
</tr>
6464
<tr>
6565
<td><code>port</code></td>
@@ -109,7 +109,7 @@ afterAll(() => Promise.all([github.close(), vercel.close()]))
109109
Each emulator is published as its own `@emulators/*` package. Install only the ones you need:
110110

111111
```bash
112-
npm install @emulators/github @emulators/google @emulators/stripe
112+
npm install @emulators/github @emulators/google @emulators/linear
113113
```
114114

115115
### Available packages
@@ -126,13 +126,15 @@ npm install @emulators/github @emulators/google @emulators/stripe
126126
<tr><td><code>@emulators/github</code></td><td>GitHub API</td></tr>
127127
<tr><td><code>@emulators/google</code></td><td>Google OAuth, Gmail, Calendar, Drive</td></tr>
128128
<tr><td><code>@emulators/slack</code></td><td>Slack Web API, OAuth v2, webhooks</td></tr>
129+
<tr><td><code>@emulators/linear</code></td><td>Linear GraphQL API, OAuth, webhooks</td></tr>
129130
<tr><td><code>@emulators/apple</code></td><td>Apple Sign In / OIDC</td></tr>
130131
<tr><td><code>@emulators/microsoft</code></td><td>Microsoft Entra ID, Graph API</td></tr>
131132
<tr><td><code>@emulators/aws</code></td><td>AWS S3, SQS, IAM, STS</td></tr>
132133
<tr><td><code>@emulators/okta</code></td><td>Okta identity provider / OIDC</td></tr>
133134
<tr><td><code>@emulators/mongoatlas</code></td><td>MongoDB Atlas Admin API + Data API</td></tr>
134135
<tr><td><code>@emulators/resend</code></td><td>Resend email API</td></tr>
135136
<tr><td><code>@emulators/stripe</code></td><td>Stripe billing and payments API</td></tr>
137+
<tr><td><code>@emulators/clerk</code></td><td>Clerk Backend API and OAuth</td></tr>
136138
<tr><td><code>@emulators/core</code></td><td>Shared store, middleware, and utilities</td></tr>
137139
<tr><td><code>@emulators/adapter-next</code></td><td>Next.js App Router integration</td></tr>
138140
</tbody>

apps/web/components/docs-mobile-nav.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const sections: NavSection[] = [
2626
{ href: "/docs/github", label: "GitHub" },
2727
{ href: "/docs/google", label: "Google" },
2828
{ href: "/docs/slack", label: "Slack" },
29+
{ href: "/docs/linear", label: "Linear" },
2930
{ href: "/docs/apple", label: "Apple" },
3031
{ href: "/docs/microsoft", label: "Microsoft Entra ID" },
3132
{ href: "/docs/aws", label: "AWS" },

apps/web/components/docs-nav.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const sections: NavSection[] = [
2424
{ href: "/docs/github", label: "GitHub" },
2525
{ href: "/docs/google", label: "Google" },
2626
{ href: "/docs/slack", label: "Slack" },
27+
{ href: "/docs/linear", label: "Linear" },
2728
{ href: "/docs/apple", label: "Apple" },
2829
{ href: "/docs/microsoft", label: "Microsoft Entra ID" },
2930
{ href: "/docs/aws", label: "AWS" },

apps/web/lib/docs-navigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const allDocsPages: NavItem[] = [
1212
{ name: "GitHub API", href: "/docs/github" },
1313
{ name: "Google API", href: "/docs/google" },
1414
{ name: "Slack API", href: "/docs/slack" },
15+
{ name: "Linear API", href: "/docs/linear" },
1516
{ name: "Apple Sign In", href: "/docs/apple" },
1617
{ name: "Microsoft Entra ID", href: "/docs/microsoft" },
1718
{ name: "AWS", href: "/docs/aws" },

0 commit comments

Comments
 (0)