Stateful Jira Cloud emulation covering the platform REST API (v3 and v2), the Jira Software Agile API, JQL search, webhooks, and Atlassian OAuth 2.0 (3LO). Writes record changelog history and dispatch Jira-shaped webhooks.
Start
# Jira only
npx @pleaseai/emulate --service jira
# point clients and the examples below at it
export JIRA_EMULATOR_URL=http://localhost:4000Default URL: http://localhost:4013 when all services are started, or http://localhost:4000 when Jira is the only service.
URL Mapping
| Real Jira URL | Emulator URL |
|---|---|
https://your-domain.atlassian.net/rest/api/3/... |
$JIRA_EMULATOR_URL/rest/api/3/... |
https://your-domain.atlassian.net/rest/api/2/... |
$JIRA_EMULATOR_URL/rest/api/2/... |
https://your-domain.atlassian.net/rest/agile/1.0/... |
$JIRA_EMULATOR_URL/rest/agile/1.0/... |
https://api.atlassian.com/ex/jira/{cloudId}/rest/... |
$JIRA_EMULATOR_URL/ex/jira/{cloudId}/rest/... |
https://auth.atlassian.com/authorize |
$JIRA_EMULATOR_URL/authorize |
https://auth.atlassian.com/oauth/token |
$JIRA_EMULATOR_URL/oauth/token |
https://api.atlassian.com/oauth/token/accessible-resources |
$JIRA_EMULATOR_URL/oauth/token/accessible-resources |
https://api.atlassian.com/me |
$JIRA_EMULATOR_URL/me |
Auth
Use HTTP Basic auth with a seeded user’s email and API token, exactly like Jira Cloud API tokens:
curl -u admin@jira.local:jira_test_token "$JIRA_EMULATOR_URL/rest/api/3/myself"Default credentials: admin@jira.local / jira_test_token (admin) and dev@jira.local / jira_dev_token. API tokens also work as Authorization: Bearer <token>. Wrong credentials return 401 with {"errorMessages":["Client must be authenticated to access this resource."],"errors":{}}. Creating, editing, or deleting projects and webhooks requires an admin; components and versions also accept the project lead.
OAuth scope checks are relaxed by default. Set jira.strict_scopes: true to require granular scopes such as read:jira-work and write:jira-work for OAuth access tokens.
Default Seed
- Project
EMU(id10000) with workflowTo Do,In Progress,Doneand issue typesEpic,Story,Task,Bug,Subtask - Scrum board
1with active sprint1(EMU Sprint 1) - Issue
EMU-1assigned todev@jira.local, with one comment - Custom fields
customfield_10015(Start date),customfield_10016(Story point estimate),customfield_10020(Sprint) - OAuth app
jira_example_client_id/example_client_secret - Cloud ID
11111111-2222-4333-8444-555555555555
Seed Config
jira:
site:
name: acme
users:
- email: admin@example.com
display_name: Admin User
admin: true
api_token: jira_test_token
- email: dev@example.com
display_name: Developer
api_token: jira_dev_token
statuses:
- name: In Review
category: indeterminate
custom_fields:
- name: Team
type: option
options: [Red, Blue]
projects:
- key: ENG
name: Engineering
lead: admin@example.com
statuses: [To Do, In Progress, In Review, Done]
components: [API, Web]
versions: [1.0.0]
sprints:
- project: ENG
name: ENG Sprint 1
state: active
issues:
- project: ENG
summary: Fix local checkout test
type: Bug
description: Reproduce and fix the checkout failure.
status: To Do
priority: High
assignee: dev@example.com
labels: [checkout]
sprint: ENG Sprint 1
comments:
- body: Reproduced locally.
author: admin@example.com
- project: ENG
summary: Add checkout regression test
type: Task
assignee: dev@example.com
links:
- type: Blocks
inward: Fix local checkout test
outward: ENG-2
webhooks:
- name: Local issue events
url: http://localhost:3000/api/webhooks/jira
events: [jira:issue_created, jira:issue_updated, comment_created]
jql: project = ENG
secret: webhook_secret
oauth_apps:
- client_id: jira_example_client_id
client_secret: example_client_secret
name: My Jira App
redirect_uris:
- http://localhost:3000/api/auth/callback/atlassian
scopes: [read:jira-work, write:jira-work, read:jira-user, manage:jira-webhook, offline_access]
strict_scopes: falseIssue parent and link inward/outward accept an issue key or a seeded summary. As in Jira’s API, the outward issue is the source of a link: { type: Blocks, inward: A, outward: B } means B blocks A. Statuses listed on a project that do not exist yet are created, with the category inferred from the name.
REST Surface
Issues (/rest/api/3 and /rest/api/2):
POST /issue,POST /issue/bulk,GET|PUT|DELETE /issue/{idOrKey}PUT /issue/{idOrKey}/assignee,GET|POST /issue/{idOrKey}/transitions,GET /issue/{idOrKey}/changelogGET /issue/createmeta/{project}/issuetypes[/{issueTypeId}],GET /issue/{idOrKey}/editmetaGET|POST /issue/{idOrKey}/comment,GET|PUT|DELETE /issue/{idOrKey}/comment/{id},POST /comment/listGET|POST|DELETE /issue/{idOrKey}/watchersGET|POST /issue/{idOrKey}/worklog,GET|PUT|DELETE /issue/{idOrKey}/worklog/{id}POST /issueLink,GET|DELETE /issueLink/{id},GET /issueLinkType
Search: GET|POST /search/jql (enhanced, nextPageToken, rejects unbounded queries), POST /search/approximate-count, legacy GET|POST /search, GET /issue/picker.
Projects and configuration: /project, /project/search, /project/{key}, /project/{key}/statuses|components|versions, /component, /version, /issuetype, /status, /statuscategory, /priority, /resolution, /field, /label, /mypermissions, /serverInfo.
Users: /myself, /user, /user/search, /users/search, /user/assignable/search, /user/bulk.
Agile (/rest/agile/1.0, also /rest/software/1.0): boards, board issues, backlog, sprints, epics, sprint lifecycle (future to active to closed), moving issues to sprints and the backlog.
Fidelity Notes
- REST API v3 bodies (descriptions, comments, worklog comments) must be Atlassian Document Format. Plain strings return Jira’s
400error. REST API v2 accepts and returns plain strings. GET /issuesupportsfields(*all,summary,status,-comment) andexpand(changelog,renderedFields,names,schema,transitions)./search/jqlreturns only issue IDs and keys unlessfieldsare requested.- Workflows allow any status to move to any other status, like team-managed projects. Transition IDs are
11,21,31, and so on by workflow position. Moving to a Done category sets the resolution; moving out clears it. - Setting
statusthrough edit fails, as in Jira. Use transitions. - Closing a sprint keeps completed issues in it and returns unfinished issues to the backlog.
- In
POST /issueLink,outwardIssueis the source (“from”) issue and receives the optional comment. In the issue view, the source lists the other issue asoutwardIssueand the target lists it asinwardIssue. - Errors use
{ "errorMessages": [...], "errors": { "field": "message" } }.
JQL
Supports AND, OR, NOT, parentheses, =, !=, ~, !~, >, >=, <, <=, IN, NOT IN, IS [NOT] EMPTY, and ORDER BY. Fields include project, key, summary, description, comment, text, status, statusCategory, assignee, reporter, creator, watcher, priority, issuetype, labels, resolution, parent, created, updated, duedate, resolutiondate, sprint, component, fixVersion, and custom fields by cf[id] or name. Functions: currentUser(), now(), startOfDay(), endOfDay(), startOfWeek(), startOfMonth(), startOfYear() (with offsets like startOfDay(-1d)), openSprints(), closedSprints(), futureSprints(), standardIssueTypes(), subTaskIssueTypes(), releasedVersions(), unreleasedVersions(), and linkedIssues(key[, "link text"]). Relative dates such as -7d and -2w work.
Webhooks
- Admin webhooks:
GET|POST /rest/webhooks/1.0/webhook,GET|PUT|DELETE /rest/webhooks/1.0/webhook/{id}with a JQL filter infilters["issue-related-events-section"],excludeBody, andsecret. Secrets sign deliveries withX-Hub-Signature: sha256=<hmac-hex>. - Dynamic webhooks for OAuth apps:
GET|POST|DELETE /rest/api/3/webhook,PUT /rest/api/3/webhook/refresh. Deliveries includematchedWebhookIds. - Events:
jira:issue_created,jira:issue_updated(withchangelogandissue_event_type_namesuch asissue_generic,issue_assigned,issue_commented), sent once per edit even when the edit also adds a comment,jira:issue_deleted,comment_created,comment_updated,comment_deleted.
OAuth 2.0 (3LO)
GET /authorize?audience=api.atlassian.com&client_id=...&scope=...&redirect_uri=...&state=...&response_type=codeshows a user pickerPOST /oauth/tokensupportsauthorization_codeand rotatingrefresh_tokengrants (refresh tokens needoffline_access), with JSON or form bodiesGET /oauth/token/accessible-resourcesreturns the site cloud ID; call the API at/ex/jira/{cloudId}/rest/api/3/...GET /mereturns the Atlassian profile
jira.js
import { createCloudClient } from 'jira.js'
const client = createCloudClient({
host: process.env.JIRA_EMULATOR_URL!,
auth: { type: 'basic', email: 'admin@jira.local', apiToken: 'jira_test_token' },
})
const issue = await client.issues.createIssue({
fields: {
project: { key: 'EMU' },
issuetype: { name: 'Task' },
summary: 'Created locally',
},
})jira.js always sends OAuth 2.0 requests (clients configured with a cloudId) to https://api.atlassian.com/ex/jira/{cloudId}, so point those at the emulator with a proxy or a fetch wrapper, or use Basic auth with host as above. Any HTTP client can call /ex/jira/{cloudId}/rest/api/3/... on the emulator directly.
Inspector
Open GET / for issues, projects, boards and sprints, users, webhooks and deliveries, and auth (API tokens, OAuth apps, cloud IDs). GET /browse/{issueKey} shows an issue page.
Current Limits
Attachments, filters, dashboards, workflow and screen scheme administration, permission schemes, groups, Jira Service Management, Confluence, rate limiting, and exact search relevance are not implemented.