npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hameddk/jira-cloud-client

v0.1.0

Published

Pure REST API client for Jira Cloud. Search, issues, transitions, project versions, and users — with auto-pagination and rate-limit handling. Caller supplies the access token. Zero deps. Node 18+.

Readme

@hameddk/jira-cloud-client

Pure REST API client for Jira Cloud.

  • Search, issues, transitions, project versions, users
  • Auto-pagination for /search/jql and /project/{key}/version
  • Rate-limit aware with configurable auto-retry (HTTP 429, Retry-After)
  • Provider-agnostic auth — caller supplies the access token resolver
  • ADF helpers (adfToPlainText, plainTextToAdf) for issue descriptions
  • Field-shape parsers for Jira's quirky custom-field values
  • Zero dependencies, ESM, Node ≥ 18

Status: 0.1.0 — early. The documented surface is stable.

Install

npm install @hameddk/jira-cloud-client

Quick start

import { createJiraCloudClient, listAccessibleResources } from '@hameddk/jira-cloud-client';

// 1. Resolve cloudId once (or pass a string if you already know it).
const resources = await listAccessibleResources(myAccessToken);
const cloudId = resources[0].id;

// 2. Build the client. Pass an async resolver for the access token —
//    typically your OAuth client's getValidAccessToken().
const jira = createJiraCloudClient({
  cloudId,
  getAccessToken: () => oauth.getValidAccessToken(),
  options: {
    pageSize: 100,
    autoRetryRateLimit: true,         // see "Rate limiting" below
    onRateLimit: (retryAfterSec, attempt) =>
      console.warn(`[jira] rate limited, retry ${attempt} in ${retryAfterSec}s`),
  },
});

// 3. Use it.
const issues = await jira.searchIssuesAll({
  jql: 'project = ABC AND resolved >= -30d',
  fields: ['summary', 'status', 'assignee'],
});

await jira.transitionIssue('ABC-1', '21');

const versions = await jira.listProjectVersions('ABC');

Public API

Factory

const jira = createJiraCloudClient({
  cloudId,                                    // string OR async resolver
  getAccessToken,                             // () => Promise<string|null>
  options: {
    baseUrl: 'https://api.atlassian.com',
    pageSize: 100,
    autoRetryRateLimit: true,                 // false | true | { maxRetries, maxDelayMs }
    onRateLimit: (retryAfterSec, attempt) => {},
    fetch: customFetch,                       // testing only
    sleep: customSleep,                       // testing only
  },
});

Methods

| Method | Returns | Notes | |---|---|---| | searchIssues({ jql, fields?, maxResults?, nextPageToken? }) | { issues, nextPageToken } | Single page. POST /search/jql. | | searchIssuesAll({ jql, fields?, maxTotal? }) | JiraIssue[] | Auto-paginates. | | getIssue(key, { fields?, expand? }) | JiraIssue | | | updateIssue(key, { fields }) | void | PUT /issue/{key} | | transitionIssue(key, transitionId) | void | POST /issue/{key}/transitions | | getIssueTransitions(key) | { transitions: [{id,name,to}] } | Available transitions for the workflow. | | getIssueStatusChangelog(key) | [{fromStatus,toStatus,date,author}] | Sorted ascending by date. | | getIssueEditMeta(key) | raw editmeta body | Pass-through. | | getIssueAssignee(key) | JiraUser \| null | Convenience: GET issue with ?fields=assignee. | | getUser(accountId) | JiraUser | Email lower-cased. | | searchAssignableUsers({ issueKey, query?, maxResults? }) | JiraUser[] | | | listProjectVersions(projectKey) | ProjectVersion[] | Auto-paginates. |

Standalone helpers

listAccessibleResources(accessToken, { baseUrl?, fetch? })
  // → AccessibleResource[]
  // GET /oauth/token/accessible-resources — discover Cloud IDs the token can reach.

Field-shape parsers

These take a raw field value that you have already extracted via your own field-id lookup. They handle Jira's various shape conventions (number, string, { value }, { name }, arrays).

import {
  coerceNumericField,    // → number | null
  formatOptionField,     // → string | null
  parseLexoRankValue,    // → string | null
  adfToPlainText,        // ADF document → plain text
  plainTextToAdf,        // plain text → minimal ADF
} from '@hameddk/jira-cloud-client';

// Example: pull a story-points custom field
const fieldId = await myAppConfig.get('jira_story_points_field'); // your concern
const points = coerceNumericField(issue.fields[fieldId]);         // toolkit's concern

Custom field IDs are your concern. The toolkit doesn't know which custom field on your Jira site holds story points, T-shirt size, or LexoRank. Look up the field id (typically from app config) and pass the raw value. This separation keeps the toolkit instance-agnostic.

Authentication

The toolkit doesn't do OAuth — it expects you to pass a getAccessToken() resolver returning a valid bearer token (or null if not authenticated). Typical wiring with @hameddk/oauth-toolkit:

const jira = createJiraCloudClient({
  cloudId,
  getAccessToken: () => atlassianOAuth.getValidAccessToken(),
});

The resolver is called on every request — no internal caching. Cache yourself if your token-fetch is expensive. If the resolver returns null, a JiraAuthError is thrown without an HTTP call.

cloudId resolution

cloudId accepts either a string or an async resolver. The resolver runs on every request — there is no internal caching, so cache yourself if the lookup is expensive (e.g. by calling listAccessibleResources once at app start and storing the result).

Rate limiting

⚠️ Behavior change vs. raw fetch. With autoRetryRateLimit: true (the default), this toolkit transparently retries HTTP 429 responses. If your previous code threw on the first 429, callers will now wait and succeed instead. This can mask tokens that fail with 401 before a 429, but does not retry 401 itself. If you depend on observing 429s directly, set autoRetryRateLimit: false.

Three forms:

| Value | Behavior | |---|---| | true (default) | Up to 2 retries, max 30s wait per retry. | | false | No retry. First 429 throws JiraRateLimitError. | | { maxRetries, maxDelayMs } | Custom limits. |

When 429 is received:

  • The toolkit reads Retry-After (seconds or HTTP-date) — falls back to 1s.
  • If the wait would exceed maxDelayMs, it throws JiraRateLimitError immediately rather than blocking that long.
  • After maxRetries, it throws JiraRateLimitError carrying the last retryAfter and provider body.
  • An optional onRateLimit(retryAfterSec, attempt) callback fires before each retry — useful for logging. Throwing inside the callback is swallowed; it cannot break the retry loop.

Errors

import {
  JiraError,             // base
  JiraConfigError,       // bad config / missing required arg
  JiraAuthError,         // 401, 403, or getAccessToken returned null
  JiraNotFoundError,     // 404, carries `resource`
  JiraRateLimitError,    // 429 after retries (or immediately if disabled)
  JiraApiError,          // other 4xx/5xx, carries `status` + `body`
} from '@hameddk/jira-cloud-client';

All errors extend JiraError. The provider's response body is preserved verbatim where available.

What this library does not do

  • Doesn't perform OAuth — bring your own token resolver.
  • Doesn't persist anything. No DB, no filesystem.
  • Doesn't compute cycle-time, lead-time, throughput, or other metrics — that's business logic.
  • Doesn't know about your custom field IDs — pass raw values to the parsers.
  • Doesn't validate JQL syntax — Jira returns 400 if it's bad.

License

MIT © 2026 Hamed Sattari