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

@charlie-labs/github-api

v1.4.2

Published

A typed, explicit, resource-oriented GitHub API client for pull-request review tools.

Downloads

1,650

Readme

@charlie-labs/github-api

A server-side TypeScript client for retrieving GitHub data used by pull-request review tools. The package keeps values close to GitHub's REST and GraphQL shapes while making pagination, resource costs, revision safety, cancellation, and operational behavior explicit.

Version 1.x is Worker-first: it targets Cloudflare Workers/workerd without nodejs_compat or Node polyfills while preserving its GitHub.com, Node.js 22+, and ESM compatibility contract. This README is the package API guide; the generated export inventory is docs/api-reference.md, observable guarantees are in docs/runtime-contracts.md, and pure projection behavior is in docs/projections.md. Package boundaries and evolution policy are recorded in docs/design.md and docs/maintenance.md, and prescriptive implementation and review rules are in docs/engineering-standards.md. GitHub Enterprise Server is not supported. Custom HTTPS API origins remain allowed as before but are experimental and do not imply GHES support. Plaintext HTTP is test-only: the scheme and lexical localhost hostname are matched with narrow ASCII case-insensitivity, while canonical dotted-decimal IPv4 literals in 127.0.0.0/8 and exact IPv6 loopback ::1 (written [::1] in a URL) are also accepted. All other raw authority syntax must remain canonical. Base URL credentials, queries, and fragments are rejected, and this validation performs no DNS resolution.

Building on Cloudflare? Start with Build a GitHub data Worker on Cloudflare for the recommended adoption path and the exact deployable starter.

1.x surface

  • Explicit anonymous-public REST access, static PATs, and asynchronous token providers.
  • A Worker-native /github-app subpath for bounded App JWT creation, downscoped installation-token minting, explicit revocation, and a reusable single-flight token provider.
  • A package-owned, injectable streaming HTTP transport.
  • Bounded JSON responses, cancellation, response-start and body-idle timeouts.
  • Exact safe-read retries, shared primary/secondary rate-limit pauses, and a serialized mutation dispatch lane.
  • Bounded route-compatible same-origin redirects for high-level reads; mutation and cross-origin redirects are rejected.
  • Safe normalized REST/GraphQL errors, sanitized request events, and rate-limit observations.
  • pulls.get().
  • Repository pulls.list.page(), .pages(), .all(), and .allWithMeta().
  • pulls.files.page(), .pages(), .all(), and .allWithMeta().
  • Paginated REST pulls.commits, pulls.reviews, and pulls.reviewComments reads.
  • Revision-guarded pulls.filesAtRevision().
  • Exact-byte, revision-guarded pulls.diff.get() and pulls.diff.stream().
  • Exact Git commit/tree/blob reads and bounded sources.getFile() / sources.getFiles() acquisition.
  • Generated GraphQL documents and read-only paginated pulls.reviewThreads, pulls.reviewThreads.comments, pulls.latestReviews, pulls.statusChecks, pulls.timeline, and pulls.viewedFiles resources.
  • A bounded one-request pulls.reviewFirstPaint.get() acquisition and an additive head-only pulls.revisionProbe.get() GraphQL read.
  • Credentialed, viewer-neutral, revision-verified pulls.mergeabilitySnapshot.get() acquisition, backed by checked-in generated PullMergeabilitySnapshot, PullMergeabilitySnapshotContinuation, and PullMergeabilityRevisionProbe documents plus stable snapshot DTO types.
  • Direct review-comment and review-thread replies, review-thread resolve/unresolve, viewer-specific file mark/unmark, and revision-preconditioned pull-request merge.
  • Paginated REST checks.forRef, statuses.forRef, and aggregate-preserving statuses.combinedForRef reads.
  • Protected-branch, branch-protection, repository/organization ruleset, and branch-applicable active-rule reads.
  • Stateless, resource-bound REST and GraphQL cursors, collection budgets, and explicit completeness.
  • Pure review-thread, status-check, timeline, latest-review, viewed-file, pull-ref source locator, and pull-file patch projections.
  • Typed and explicitly untyped raw REST calls, plus code-generated and raw-string GraphQL calls.
  • Optional, authorization-namespace-isolated conditional caching for eligible REST JSON reads; every hit is revalidated and no cache is enabled by default.
  • A lean /integration subpath with explicit execution profiles, cache/resource descriptors, physical-attempt coordination contracts, and pure webhook invalidation planning.
  • A Worker-native /webhooks subpath for exact-byte GitHub HMAC verification, verify-before-parse invalidation planning, bounded portable messages, and opaque delivery keys.
  • A lean /pulls subpath with REST-only, generated review-read, and first-paint-only clients for measured Worker bundles.

Development

Requirements:

  • Bun 1.3 or newer for development.
  • Cloudflare Workers/workerd with Web Platform APIs and no nodejs_compat, or Node.js 22 or newer, for package consumers.
bun install
bun install --cwd examples/cloudflare --frozen-lockfile
bun install --cwd examples/cloudflare/minimal --frozen-lockfile
bun run check

check:package is the complete independently publishable package gate. check:cloudflare packs that build and verifies both repository-only Cloudflare consumers against the tarball under workerd:

  • the minimal two-Worker starter, which is the copyable introduction to public-repository anonymous or GitHub App reads, a private Service Binding, and hot plus conditional caching; and
  • the comprehensive reference lab, which adds the deterministic GitHub fixture, invalidation Worker, Durable Objects, Queue, KV/R2, live-test isolation, and performance harness.

The starter independently installs and locks the published @charlie-labs/[email protected]. The root verifier replaces that dependency with a freshly packed artifact so every package change is tested against both Cloudflare consumers before publication.

prepack intentionally runs only check:package; publishing never depends on example-only packages. The isolated real-GitHub compatibility suite is manual-only and remains outside normal CI, packaging, and publication; maintainers should follow docs/live-testing.md.

Deterministic consumer tests can use the stable testing subpath without retaining authorization-bearing requests:

import { createGitHubApi } from "@charlie-labs/github-api";
import {
  createGitHubEventRecorder,
  createMockGitHubTransport,
  githubJsonResponse,
} from "@charlie-labs/github-api/testing";

const recorder = createGitHubEventRecorder();
const transport = createMockGitHubTransport((request) =>
  githubJsonResponse(request, { number: 417 }),
);

const github = createGitHubApi({
  auth: { token: "fixture-token" },
  onEvent: recorder.onEvent,
  transport,
});

The mock handler receives the live request inside the test boundary; the helper does not record requests or serialize their headers.

Quick start

Choose GitHub authentication explicitly. Missing auth never means anonymous:

import { createGitHubApi } from "@charlie-labs/github-api";

const withToken = createGitHubApi({
  auth: { token: process.env.GITHUB_TOKEN! },
});

const withProvider = createGitHubApi({
  auth: {
    async tokenProvider({ operation, signal }) {
      return await credentialBroker.tokenForGitHub({ operation, signal });
    },
  },
});

const anonymousPublic = createGitHubApi({
  auth: { anonymous: true },
});

const github = withToken;
const pull = await github.pulls.get({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
});

const openPulls = await github.pulls.list.allWithMeta({
  owner: "acme",
  repo: "widgets",
  filters: {
    state: "open",
    base: "main",
    head: { owner: "acme", ref: "feature/report" },
  },
  order: { by: "updated", direction: "desc" },
});

const files = await github.pulls.files.allWithMeta({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
});

const statuses = await github.pulls.statusChecks.allWithMeta({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  headOid: pull.head.sha,
});

const commits = await github.pulls.commits.allWithMeta({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
});

Fetch-backed transport

The root entrypoint exports the package's standard Fetch-to-transport adapter:

export type GitHubFetch = (request: Request) => Response | Promise<Response>;

export interface GitHubFetchTransportOptions {
  readonly fetch?: GitHubFetch;
}

export function createGitHubFetchTransport(
  options?: GitHubFetchTransportOptions,
): GitHubTransport;

Omit fetch to use the runtime's standard global fetch:

import { createGitHubApi, createGitHubFetchTransport } from "@charlie-labs/github-api";

const github = createGitHubApi({
  auth: { token: process.env.GITHUB_TOKEN! },
  transport: createGitHubFetchTransport(),
});

A trusted server-side Fetch-compatible boundary can instead route the complete GitHub request. For example, a private Cloudflare Service Binding can be used as the upstream:

interface Bindings {
  readonly GITHUB_TOKEN: string;
  readonly GITHUB_UPSTREAM: {
    fetch(request: Request): Promise<Response>;
  };
}

const github = createGitHubApi({
  auth: { token: env.GITHUB_TOKEN },
  transport: createGitHubFetchTransport({
    fetch: (request) => env.GITHUB_UPSTREAM.fetch(request),
  }),
});

createGitHubApi({ transport }) remains the single request-execution customization seam; there is no createGitHubApi({ fetch }) option. Consumers may supply Fetch-compatible fixture or fake behavior at that seam, but the package provides no fixture data, scenario engine, or fake GitHub behavior.

Security: a custom fetch receives fully formed requests, potentially including authorization headers. Run it only across a trusted server-side boundary, never a public, client-side, or otherwise untrusted hop, and do not log or persist request headers, bodies, credentials, or response bodies.

The remaining examples use github as a credentialed client. The package never reads a named environment variable itself. An asynchronous provider is called for every physical attempt, so it can rotate an expiring credential for the same principal. Static and provider credentials must be non-empty strings with no whitespace, ASCII control characters, or DEL; accepted opaque strings, including non-ASCII non-whitespace characters, are forwarded unchanged. The package reports provider rejection as a generic provider failure and a successfully resolved malformed value as an invalid returned credential, without retaining provider errors or credential text. If the caller cancels while a provider is pending, cancellation wins when the provider settles.

Anonymous mode sends no Authorization header. It can use the package-owned REST GET/HEAD resources when GitHub exposes the selected repository data publicly. Package-owned GraphQL operations, mutations, viewer-specific resources, and every raw path reject locally because they require authentication or bypass the reviewed public surface. An upstream permission failure remains the ordinary normalized GitHub error; the package never falls back between anonymous and credentialed modes.

Application/user authentication and GitHub API authentication are separate concerns. A Worker may authenticate its own caller while deliberately choosing an anonymous-public, installation-token, or user-token GitHub client. The application owns the repository allowlist and public exposure policy. GitHub limits unauthenticated REST requests to 60 per hour per originating IP, so anonymous mode is intended for bounded, heavily cached public reads—not an uncached arbitrary GitHub proxy.

Reuse one client for each credential/principal and authorization context. Scheduling and rate-limit pauses are client-local, so creating many clients can bypass their protection; a token provider should rotate credentials only within the same visibility and rate-limit context.

GitHub App installation authentication

Use a static token for a simple operator-controlled integration whose one credential, visibility, and lifetime are managed outside this package. Use a GitHub App installation provider for a long-running service that is installed on repositories, should mint short-lived credentials, or needs explicit repository and permission downscoping. Both plug into the same root-client authentication contract.

The tree-shakeable @charlie-labs/github-api/github-app subpath implements the GitHub App credential protocol without reading environment variables or Worker bindings:

import { createGitHubApi } from "@charlie-labs/github-api";
import {
  createGitHubAppTokenProvider,
  type GitHubAppTokenProvider,
} from "@charlie-labs/github-api/github-app";

interface GitHubAppBindings {
  // Prefer the App's client ID as GitHub's JWT issuer.
  GITHUB_APP_ID: string;
  GITHUB_APP_INSTALLATION_ID: string;
  GITHUB_APP_PRIVATE_KEY: string;
  GITHUB_REPOSITORY_ID: string;
}

let tokenProvider: GitHubAppTokenProvider | undefined;
let github: ReturnType<typeof createGitHubApi> | undefined;

function githubFor(env: GitHubAppBindings) {
  if (github !== undefined) {
    return github;
  }
  tokenProvider ??= createGitHubAppTokenProvider({
    appId: env.GITHUB_APP_ID,
    installationId: Number(env.GITHUB_APP_INSTALLATION_ID),
    privateKey: env.GITHUB_APP_PRIVATE_KEY,
    repositoryIds: [Number(env.GITHUB_REPOSITORY_ID)],
    permissions: {
      contents: "read",
      pull_requests: "read",
    },
  });

  github = createGitHubApi({
    auth: { tokenProvider },
  });
  return github;
}

The application—not the package—reads and protects the App private key, App ID, installation ID, and repository IDs. Create one provider for one App and API origin, then reuse it across clients and requests in that isolate. Do not create a provider for every request or use one provider to multiplex unrelated authorization contexts.

The provider delegates installation-token requests, in-isolate caching, expiration handling, refresh, and concurrent-request coalescing to @octokit/auth-app. provider.refresh() performs Octokit's documented refresh: true cache bypass and returns the complete installation authentication result. The callable provider returns only its token so it is directly assignable to root auth.tokenProvider.

The appId option names the JWT iss field for compatibility with GitHub's terminology. Pass the GitHub App client ID (GitHub's recommended issuer) or a legacy positive numeric App ID. The caller is responsible for supplying that GitHub-issued identity; package preflight enforces only bounded, safe issuer syntax and cannot prove that GitHub issued the value.

repositoryIds, repositoryNames, and permissions use Octokit's installation authentication options directly. Omitting them asks GitHub for the installation's available repositories and granted permissions. Prefer an explicit repository and permission downscope for a data Worker. A provider copies these values when it is created, so later mutation of caller-owned objects or arrays cannot silently change its credential scope.

The package intentionally follows Octokit's response behavior: GitHub's reported token scope is returned rather than compared with the requested scope. A freshly minted result contains the response's repository and permission metadata. A result reconstructed from Octokit's serialized cache may contain the requested metadata instead. Treat GitHub authorization failures on subsequent API requests as authoritative; applications that need a separate policy assertion should apply it explicitly.

For lower-level or short-lived workflows the subpath also exports:

  • createGitHubAppJwt() for Octokit-compatible RS256 App JWT creation from a caller-supplied PKCS#1 or PKCS#8 PEM key;
  • mintGitHubAppInstallationToken() for one fresh Octokit installation-token request; and
  • revokeGitHubAppInstallationToken() for explicit token revocation through Octokit's request layer.

A short-lived workflow that requires cleanup can call mint, use the token, and call revoke in its own finally block. Keeping that control flow in the application avoids a package-specific callback/error protocol. Ordinary root-client requests never revoke a cached provider token.

One-shot mint and revoke helpers accept an injected fetch, custom API base, and caller AbortSignal. App JWT signing uses the standard timing and claim behavior from universal-github-app-jwt, the same signer Octokit uses: iat is 30 seconds before the observed clock and exp is 10 minutes after iat. The optional injected clock exists for deterministic tests; timing overrides and cancellation are not part of the local signing API. Provider refresh follows Octokit's operation-level behavior: an already-aborted package operation is rejected before authentication begins, but aborting one waiter does not cancel Octokit's shared refresh. Private keys, JWTs, tokens, authorization headers, and response bodies are removed from package-created errors.

The optional GitHubAppTokenCache is Octokit's get(key)/set(key, value) cache contract. Octokit owns the opaque key and serialized value. Its cache key does not currently include the App ID or API base URL, so do not share one cache namespace across Apps or origins. The package does not add persistence, delete/invalidation, leases, or distributed coordination around that contract.

When global refresh serialization matters, make a credential-sharded Durable Object own one provider and expose a bounded token-broker RPC. Do not route every cached GitHub data read through that object: Workers Cache should satisfy a fresh data hit before credential coordination runs.

Lean pull clients and first paint

Latency-sensitive Workers that need only pull reads can avoid importing the root client's raw, mutation, diff, source, projection, and unrelated resource surfaces:

import {
  createGitHubRestPullApi,
  createGitHubReviewFirstPaintApi,
  createGitHubReviewPullApi,
} from "@charlie-labs/github-api/pulls";

const publicPulls = createGitHubRestPullApi({ auth: { anonymous: true } });
const firstPaintPulls = createGitHubReviewFirstPaintApi({
  auth: { token: await tokenForSamePrincipal() },
});
const reviewPulls = createGitHubReviewPullApi({
  auth: { token: await tokenForSamePrincipal() },
});

const summary = await publicPulls.pulls.get({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
});

const publicOpenPulls = await publicPulls.pulls.list.page({
  owner: "acme",
  repo: "widgets",
  filters: { state: "open" },
  order: { by: "updated", direction: "desc" },
  pageSize: 50,
});

const firstPaint = await firstPaintPulls.pulls.reviewFirstPaint.get({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  expectedHeadOid: summary.head.sha,
  reviewThreadPageSize: 10,
  nestedCommentPageSize: 5,
  statusPageSize: 20,
  latestReviewPageSize: 10,
  timelinePageSize: 10,
  includeViewedFiles: false,
});

createGitHubRestPullApi() accepts anonymous or credentialed auth and exposes only pulls.get, pulls.list, pulls.files, pulls.commits, pulls.reviews, pulls.reviewComments, and pulls.filesAtRevision. createGitHubReviewFirstPaintApi() is the smallest credentialed GraphQL surface: it exposes only the one-request pulls.reviewFirstPaint.get() path. Its physical module boundary lets a Worker bundle retain only the generated PullReviewFirstPaint operation. createGitHubReviewPullApi() requires credentialed auth and exposes only read-only generated GraphQL pull resources. None of the lean factories exposes raw requests, mutations, diff acquisition, Git/source resources, or projections. Use the root createGitHubApi() when those capabilities are needed.

The separate credentialed createGitHubMergeabilityApi() factory exposes only pulls.mergeabilitySnapshot.get() plus events and rate-limit observation:

import {
  createGitHubMergeabilityApi,
  expectedPullRevisionFromPullListItem,
} from "@charlie-labs/github-api/pulls";

const mergeabilityApi = createGitHubMergeabilityApi({
  auth: { token: await tokenForSamePrincipal() },
});
const snapshot = await mergeabilityApi.pulls.mergeabilitySnapshot.get({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  expectedRevision: expectedPullRevisionFromPullListItem(summary),
});

ExpectedPullRevision extends the existing PullRevision vocabulary with baseRefName, so its exact tuple is headSha, baseSha, and baseRefName. expectedPullRevisionFromPullListItem() snapshots and validates those fields from the Octokit-derived PullRequestSummary; it is pure and performs no I/O. The helper is exported from both the package root and /pulls.

The pull request's baseSha and the current target of its named base branch are distinct GitHub identities and may differ after the branch advances. The snapshot preserves the former in revision.baseSha/pullRequest.baseOid and the latter in pullRequest.baseRef.target.oid; comparison.baseTarget.oid matches that current base-ref target. Revision verification never substitutes the mutable branch target for the pull request's observed base SHA.

An unpaginated expected-revision acquisition is exactly two GraphQL requests: the composite snapshot and trailing full-revision probe. Omitting expectedRevision adds a discovery probe for exactly three requests and allows one discard/retry if any revision tuple field moves. Four fixed-size connections advance together, so continuation count follows maximum page depth, not summed depth. Nullable acquisition roots and late non-GraphQL continuation failures become explicit partial sections only after successful trailing verification; any non-empty GraphQL errors response rejects and discards data.

The checked Cloudflare bundle fixtures exercise this supported factory boundary without exporting internal documents. The general parsed-module baseline (baseline.json) measures the mergeability bundle at 114,289 raw bytes, 31,204 gzip bytes, and 41 parsed modules; its root namespace grew by 50,697 raw/11,342 gzip bytes over the 350,461/80,742 control. The separate optimization baseline (optimization-baseline.json) measures the mergeability candidate at 114,289 raw bytes, 31,204 gzip bytes, 41 parsed modules, and 25 optimization-contributing modules; its broad-REST root grew by 48,431 raw/10,845 gzip bytes over the 349,519/80,301 control. The optimization import audit retains only PullMergeabilitySnapshot, PullMergeabilitySnapshotContinuation, and PullMergeabilityRevisionProbe. Both gzip growth measurements remain below their reviewed 12,000-byte general and 11,000-byte optimization feature-growth ceilings. These are deterministic local bundle measurements, not deployed latency or live GitHub cost claims.

The verified GraphQL operation observes applicable policy through Ref.rules. GraphQL permission, schema, or selected-field errors reject the operation and discard partial data. Only nullable roots and late non-GraphQL continuation failures can produce revision-verified partial snapshots. This method never hides a REST fallback inside the logical snapshot, because that would change both physical request counts and revision-observation timing. A separately cacheable REST compatibility component may be considered only if live parity fails and that component receives separate contract, permission, request-plan, and cache review.

reviewFirstPaint.get() is one named logical operation and one generated GraphQL request. It returns a purpose-built PullReviewFirstPaint, not a generic response envelope. Defaults are 10 threads, five comments per thread, 20 status contexts, 10 latest reviews, and 10 timeline items. Viewed-file state is omitted unless includeViewedFiles: true, in which case its default page is 25 items and remains credential/viewer-specific.

Each returned top-level or nested page has its own resource-bound opaque cursor. Continue them explicitly through reviewThreads, reviewThreads.comments, statusChecks, latestReviews, timeline, or viewedFiles; one first-paint cursor never advances another connection. expectedHeadOid is an optional same-response head check. It prevents returning first-paint data for a different observed head, but a one-request observation is not a before/after snapshot guard.

When consistency across acquisition time matters more than the single-request latency path, the broader review client exposes an explicit composite:

const verifiedFirstPaint = await reviewPulls.pulls.reviewFirstPaint.getAtRevision({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  expectedHeadOid: summary.head.sha,
});

getAtRevision() performs a generated head probe, the bounded first-paint read, and a second generated head probe under one logical request ID, deadline, cancellation signal, scheduler/rate-limit context, and terminal event. It also checks the head selected by the middle response. Movement at any boundary raises GitHubPullHeadChangedError, and no first-paint value is returned. The operation-narrow factory intentionally omits this three-request method so using .get() cannot retain the probe document accidentally.

Outer reviewThreads pages are also viewer-specific because their selected viewerCan* fields depend on the authenticated viewer. Integration descriptors therefore require viewer isolation for those pages and for first paint; the nested comment-only resource does not select viewer capability fields.

const thread = firstPaint.reviewThreads.items[0];
const nextComments =
  thread?.comments.nextCursor === undefined
    ? undefined
    : await reviewPulls.pulls.reviewThreads.comments.page({
        owner: "acme",
        repo: "widgets",
        pullNumber: 417,
        threadId: thread.id,
        pageSize: 20,
        cursor: thread.comments.nextCursor,
      });

const allLatestReviews = await reviewPulls.pulls.latestReviews.allWithMeta({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  pageSize: 50,
});

When only pull/head identity is needed, the additive generated probe avoids selecting the complete REST pull representation:

const revision = await reviewPulls.pulls.revisionProbe.get({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  expectedHeadOid: summary.head.sha,
});

The probe returns the generated pull identity selection directly and raises the same typed head-movement error when expectedHeadOid differs. Existing pulls.filesAtRevision() and pulls.diff methods continue to use REST pull reads before and after acquisition throughout 1.x; the new probe does not silently change their upstream source or completion semantics.

Worker integration primitives

@charlie-labs/github-api/integration is a side-effect-free policy subpath for data Workers. It owns no Cache API entry, KV namespace, R2 bucket, Durable Object, Queue, webhook route, or GitHub App token lifecycle.

Execution profiles expand one latency intent into ordinary per-call controls:

import { resolveGitHubExecutionProfile } from "@charlie-labs/github-api/integration";

const profile = resolveGitHubExecutionProfile("interactive", {
  pageSize: 40,
});

const firstPage = await github.pulls.files.page({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  ...profile.request,
  ...profile.retry,
  pageSize: profile.collection.pageSize,
});

interactive favors a strict overall deadline and a small first page, background-complete favors bounded complete traversal and refresh work, and streaming favors fast response start and explicit verified completion. Resolution is deterministic and deeply frozen. Spreading the controls into a call snapshots them with the rest of that logical operation; omitting a profile preserves the existing client defaults. cacheIntent is guidance for the caller's storage layer, not hidden package I/O.

Exact-content operations still require their named byte limit. For a streaming diff, map the reviewed profile value explicitly:

const streaming = resolveGitHubExecutionProfile("streaming");
const diff = await github.pulls.diff.stream({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  baseSha: pull.base.sha,
  headSha: pull.head.sha,
  ...streaming.request,
  ...streaming.retry,
  maxBytes: streaming.request.maxResponseBytes,
});

The explicit maxBytes bounds exact diff bytes; maxResponseBytes separately bounds the composite pull-verification JSON.

Resource planning is intentionally two stage. Plan before the read, then finalize only with evidence from a successful complete result:

import {
  finalizeGitHubResourcePlan,
  planGitHubRead,
} from "@charlie-labs/github-api/integration";

const plan = await planGitHubRead({
  auth: { mode: "anonymous" },
  operation: "pulls.get",
  arguments: {
    owner: "acme",
    repo: "widgets",
    pullNumber: 417,
  },
});

const pull = await anonymousPublic.pulls.get({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
});

const descriptor = finalizeGitHubResourcePlan(plan, { state: "success" });

The descriptor supplements pull; it never wraps or mutates the GitHub value. It carries a versioned opaque key, canonical resource and operation, representation, auth partition, revision/cursor dimensions, freshness ceilings, invalidation tags, verification state, and an explicit storage decision. Credentialed planning requires the same authorization namespace concept used by the conditional cache, but emits only a SHA-256-derived opaque partition. Token-authenticated public data remains credential-partitioned. Custom API identities are hashed and private URLs are never emitted. Finalization accepts only the exact deeply frozen plan object issued by planGitHubRead() in that runtime instance; copying, mutating, forging, or serializing the provisional plan removes that capability. Finalize beside the read, then pass the completed descriptor across a Service Binding if needed. For pulls.diff.stream, a bare completion assertion is never sufficient: consume or cancel the body, await stream.completion, and pass that exact package-issued result to finalization. Only its registered verified result can permit storage, and it must match the plan's repository, pull, and revision. pulls.mergeabilitySnapshot.get uses the same capability principle: pass the exact package-issued snapshot object. Copies, foreign snapshots, partial snapshots, incomplete collections, attributed issues, and revision mismatches produce no descriptor.

Mergeability planning accepts an optional cache policy:

const plan = await planGitHubRead({
  auth: {
    mode: "credentialed",
    authorizationNamespace: "installation:42/visibility:v3",
    installationId: 42,
  },
  cache: { invalidation: "webhook-generation-fenced" },
  operation: "pulls.mergeabilitySnapshot.get",
  arguments: { owner: "acme", repo: "widgets", pullNumber: 417 },
});

The default is ttl-only. Complete raw mergeable: "UNKNOWN" snapshots have a five-second fresh ceiling in either mode. Other complete snapshots have a 15-second ceiling in TTL-only mode and a five-minute ceiling only when the generation-fenced mode was explicitly selected. Both use 15 seconds of stale-while-revalidate and 30 seconds of stale-if-error. Tags express dependencies; their presence does not prove webhook delivery health.

Generation fencing and exact-key single-flight remain consumer-owned. On a miss: read the repository generation, acquire the exact-key lease, recheck the cache, obtain a complete verified snapshot, reread the generation, finalize and store only if the generation is unchanged, then release the lease. A fresh hit performs no authentication, coordination, or upstream work.

Use the fixed github-anonymous-public-v1 partition only for data actually read by an anonymous client. Authenticated observations must never be relabeled public. Cache policy by class is conservative:

| Resource/result state | Storage guidance | | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | Verified SHA-addressed object or exact source | May be long-lived after object verification; a recursive tree must also be non-truncated | | Verified pull revision or completed diff stream | May be stored only after final revision/completion verification | | Mutable pull/review/check/status state | Short bounded freshness; prefer webhook invalidation and bounded stale handling | | Complete mergeability snapshot | Full response only; exact returned headSha/baseSha/baseRefName capability and verified tuple required | | Viewer-specific state | Credentialed and storable only with an explicitly viewer-isolated namespace | | Lazy pages, unverified flat hard-capped collections, partial/truncated results, failures, cancellation, overflow, or rate-limit responses | No store | | Anonymous 404 | No store by default; it is not durable proof that a resource is public or absent |

An optional coordinator can enforce cross-isolate policy around every physical attempt:

const coordinated = createGitHubApi({
  auth: { tokenProvider },
  coordination: {
    rateBucket: "installation:opaque-42",
    coordinator,
    background: executionContext,
  },
});

acquire() receives only safe operation/resource/auth-bucket dimensions, one sanitized abort signal, and the logical deadline when present. Its lease is released exactly once after the attempt, including retries, redirects, GraphQL errors at HTTP 200, body failures, stream cancellation, and revision probes. observe() receives bounded rate/cost/retry facts. The integration must choose an opaque rateBucket; the package never hashes or persists a token as identity. Supplying background.waitUntil() permits post-attempt observation to be registered as background work, but permit release remains deterministic.

Use @charlie-labs/github-api/webhooks at a framework-neutral webhook boundary. The caller still owns the HTTP route and must collect the request body without decoding, normalizing, or re-encoding it:

import {
  encodeGitHubInvalidationMessage,
  verifyAndPlanGitHubWebhook,
} from "@charlie-labs/github-api/webhooks";

// Enforce a transport/content-length/stream limit before allocating this
// buffer. The helper's maxBodyBytes is a second, mandatory defense before HMAC.
const exactBody = await request.arrayBuffer();
const verified = await verifyAndPlanGitHubWebhook({
  body: exactBody,
  headers: request.headers,
  secret: env.GITHUB_WEBHOOK_SECRET,
  maxBodyBytes: 256 * 1024,
  maxPlanResources: 64,
  maxPlanRefreshCandidates: 32,
});

if (verified.plan.state === "planned") {
  const queueBody = encodeGitHubInvalidationMessage({
    deliveryId: verified.delivery.deliveryId,
    namespace: "tenant:acme", // optional and explicitly integration-owned
    plan: verified.plan,
    plannedAt: new Date().toISOString(),
  });
  await env.INVALIDATIONS.send(queueBody);
}

The convenience helper copies and bounds the exact bytes, strictly parses only X-GitHub-Delivery, X-GitHub-Event, and X-Hub-Signature-256, verifies the lowercase sha256=<64 hex> HMAC, then—and only then—strictly decodes UTF-8 and JSON and invokes invalidationPlanForWebhook(). The default body limit is 256 KiB and may be configured up to 16 MiB. String secrets use UTF-8; exact secret byte views are also accepted. The HMAC authenticates the raw body, not the event or delivery headers; treat those headers as validated transport metadata, not as independently signed authorization claims.

Unknown events/actions remain explicit frozen unsupported plans. Authentication, UTF-8, JSON, complexity, and configured planner-budget failures are distinct safe GitHubWebhookError codes. The helpers never log, persist, enqueue, purge, acknowledge, retry, fetch replacement state, or treat the webhook payload as authoritative GitHub state.

For branch push, create, and delete events, the plan retains the canonical full git-ref resource and full-ref checks tag and also emits the exact nonempty suffix of refs/heads/ as a second checks tag only when that suffix does not begin with refs/, preventing namespace collisions. This lets descriptors planned with either main or refs/heads/main overlap without aliasing tags or other ref namespaces. Supported pull_request_review actions (submitted, edited, and dismissed) include pulls.latestReviews.allWithMeta; review comment and thread actions do not. Additive expansion still obeys configured resource/candidate bounds all-or-error. This is minor additive behavior: consumers may observe additional tags or the latest-review refresh candidate, with no schema/version change. A custom refresh-candidate bound below 4 now returns an unsupported plan for those review-state events instead of a truncated plan.

Portable messages are deterministic UTF-8 JSON with a strict version-1 schema. Only a planned invalidation can be encoded. The message contains the plan, canonical delivery ID and event, caller-supplied canonical timestamp, and an optional bounded, well-formed opaque namespace; it contains no secret, signature, raw body, arbitrary header, or token. The default encoded/decoded limit is 64 KiB and may be configured up to 1 MiB. Decoding rejects unknown versions, extra or missing fields, noncanonical arrays, malformed values, and over-budget input. githubWebhookDeliveryKey() separately derives an opaque, namespace-separated ghwd1: idempotency key from the versioned canonical delivery ID. It does not deduplicate, sequence, persist, lease, retry, dead-letter, or acknowledge at-least-once broker deliveries.

Consumers that already authenticate and decode webhooks elsewhere can keep using the pure planner directly:

import { invalidationPlanForWebhook } from "@charlie-labs/github-api/integration";

const invalidation = await invalidationPlanForWebhook({
  eventName: authenticatedEventName,
  payload: authenticatedSizeBoundedPayload,
});

Fetch only what you need

Each high-level method maps to one explicit GitHub resource. Start with pulls.get() only when pull metadata or exact base/head identities are needed; use filters such as review-comment since, timeline since, check-run checkName/status/filter/appId, and ruleset targets to avoid fetching irrelevant data. Use .page() when one page is enough. The package does not poll null mergeability, fetch rendered Markdown variants, combine REST file statistics with viewer-specific GraphQL state, or issue hidden policy lookups.

The explicitly composite methods do make companion requests:

  • pulls.filesAtRevision() reads the pull before and after all file pages.
  • pulls.diff.get() and .stream() read the pull before and after the diff.
  • sources.getFile() and .getFiles() traverse the requested commit, trees, and raw blob objects.
  • pulls.reviewFirstPaint.get() deliberately selects several small, independently pageable review connections in one bounded generated GraphQL request.
  • pulls.mergeabilitySnapshot.get() combines a viewer-neutral first page, synchronized continuation rounds, and a trailing full-revision probe under one request ID, deadline, cancellation context, budget, and event sequence. Its deeply frozen snapshot reconciles collection totals and reports bounded per-section diagnostics when a late continuation cannot complete.

Raw calls acquire one page and never aggregate it.

Return shapes and pagination

One-to-one reads and mutations return GitHub values directly. A high-level method never returns a generic { data, metadata } envelope. Operational status, headers, validators, logical request correlation, retries, and rate limits belong to events; raw.rest returns the complete Octokit response envelope. Response-derived failures may additionally expose the sanitized GitHub response request ID as the optional error-only GitHubApiError.githubRequestId for support correlation. Persistence timestamps, freshness, storage keys, and invalidation state belong to the consumer.

The named semantic acquisitions are deliberate exceptions: PullRequestFilesAtRevision, PullRequestDiff, PullRequestDiffStream, and AcquiredSourceContent carry only the revision, completeness, object identity, Git mode, or content-classification fields needed to interpret their data safely. PullReviewFirstPaint is the separately justified bounded review-page container: its connection values remain GitHub-shaped pages with independent continuations. None carries a generic metadata bag or transport and persistence metadata.

Paginated resources have four forms:

const first = await github.pulls.files.page({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  pageSize: 50,
});

for await (const page of github.pulls.files.pages({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  pageSize: 50,
  maxPages: 20,
  maxItems: 1_000,
})) {
  consume(page.items);
}

const flatFiles = await github.pulls.files.all({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
});

const filesWithMeta = await github.pulls.files.allWithMeta({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
});

pulls.list uses those same four forms and returns GitHub's pull-request list summary items. filters.state, filters.base, and the structured filters.head: { owner, ref } map directly to GitHub's server-side controls; order.by and order.direction map to sort and direction. Omitted controls remain omitted so GitHub owns its defaults. Each page is one list request with no hidden pulls.get() calls, mergeability lookup, or other per-pull enrichment. The operation supports explicit anonymous-public clients for public repositories and is also exposed by the lean /pulls entrypoint.

Defaults are 100 items per page, at most 100 pages and 10,000 items, and at most 16 MiB for each JSON response. Per-call pageSize, maxPages, maxItems, and maxResponseBytes override client defaults. Cursors are opaque, bounded, resource/locator/filter/page-size/API-version-bound, stateless values that may be persisted and resumed by another client in the same API context:

await cursorStore.put("pull-files", first.nextCursor);
const next = first.nextCursor
  ? await github.pulls.files.page({
      owner: "acme",
      repo: "widgets",
      pullNumber: 417,
      pageSize: 50,
      cursor: first.nextCursor,
    })
  : undefined;

Collections are fetched sequentially without sorting or deduplication. GitHub lists are mutable, so even completeness: { state: "complete" } means only that the documented traversal terminated without a known hard cap; it is not a snapshot. Nested review-thread comment pageInfo remains authoritative and is not silently collected with outer thread pages.

statuses.combinedForRef.all() is the sole non-flat .all() result. It keeps the aggregate state, resolved SHA, repository, totalCount, items, and whether aggregate state changed while collecting. Every other ordinary paginated .all() returns a flat array.

Every status-check page re-verifies that the pull still points to headOid. Viewed-file state is specific to the authenticated GitHub viewer; the package does not persist or mirror it and keeps it separate from REST file patches/statistics.

pulls.reviews and pulls.reviewComments request GitHub's raw-Markdown representation. Review-comment filters (sort, direction, and since) are passed through exactly and are bound into continuation cursors. Review-comment and timeline since values use YYYY-MM-DDTHH:MM:SS[.fraction]Z: uppercase UTC with complete seconds, an optional one-to-nine-digit fraction, and a valid Gregorian calendar date and time. Other ISO 8601 variants, offsets, leap seconds, and normalized overflow are rejected before authentication. Accepted strings are forwarded unchanged and their exact values are cursor-bound. GitHub exposes at most 3,000 pull files and 250 commits through the corresponding REST endpoints. At either exact boundary GitHub cannot prove whether more items exist, so use pulls.commits.allWithMeta() when the difference between complete and possibly-truncated matters, and inspect the same metadata from pulls.files.allWithMeta() or pulls.filesAtRevision(). Raising local budgets does not overcome either upstream cap.

The six GraphQL collection resources paginate their named connections, so .all() and .allWithMeta() may issue multiple requests. Each review-thread node contains one nested comment page; the ordinary thread read defaults to 100 nested comments but accepts nestedCommentPageSize, while first paint defaults to five. Continue a thread's comments through pulls.reviewThreads.comments with that thread ID. Pull threads, latest reviews, timeline, and viewed state usually require Pull requests read permission. Status rollups can additionally depend on Checks read and Commit statuses read permissions. The default timeline is an intentionally curated review timeline, not every GitHub timeline union member; use a consumer-generated document through raw.graphql.execute() for another selection.

In that curated timeline, AssignedEvent and UnassignedEvent project as kind: "assigned" and kind: "unassigned", while LabeledEvent and UnlabeledEvent project as kind: "labeled" and kind: "unlabeled". All four expose id, actor, and occurredAt; the original nullable GitHub-shaped payload remains at source.assignee or source.label. No event-level url is exposed or synthesized for these variants.

Already-fetched GraphQL and REST values can be reconciled without hidden I/O:

import {
  buildReviewThreads,
  buildStatusChecks,
} from "@charlie-labs/github-api/projections";

const contexts = await github.pulls.statusChecks.all({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  headOid: pull.head.sha,
});

const projected = buildStatusChecks(contexts);

const [threads, comments, reviews] = await Promise.all([
  github.pulls.reviewThreads.all({
    owner: "acme",
    repo: "widgets",
    pullNumber: 417,
  }),
  github.pulls.reviewComments.all({
    owner: "acme",
    repo: "widgets",
    pullNumber: 417,
  }),
  github.pulls.reviews.all({
    owner: "acme",
    repo: "widgets",
    pullNumber: 417,
  }),
]);

const projectedThreads = buildReviewThreads({ threads, comments, reviews });

Projection results retain every raw source record and report ambiguities as data. They do not make independently fetched resources an atomic snapshot. Independent REST/GraphQL calls and pure projections are not one GitHub transaction. Boundary guards cannot detect an A-to-B-to-A movement that returns to the expected refs, and status/review collections can still change while their pages are read.

Revision-guarded pull diff

Both diff methods require the exact reviewed base/head SHAs and an explicit decoded-byte limit. The package requests GitHub's authenticated pull diff with identity content coding, preserves arbitrary representation bytes, and verifies the pull at both boundaries:

const revision = {
  baseSha: pull.base.sha,
  headSha: pull.head.sha,
};

const diff = await github.pulls.diff.get({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  ...revision,
  maxBytes: 8 * 1024 * 1024,
});

consumeExactBytes(diff.bytes);

get() returns nothing until final verification succeeds. stream() verifies before returning, but successful consumption is not the same as verified completion: final pull verification occurs before the stream closes successfully, and the separate completion promise always resolves exactly once rather than rejecting.

const streamed = await github.pulls.diff.stream({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  ...revision,
  maxBytes: 8 * 1024 * 1024,
});

for await (const bytes of streamed.body) {
  consumeExactBytes(bytes);
}

const completion = await streamed.completion;
if (completion.state !== "verified") {
  throw completion.error;
}

Cancelling the body early aborts upstream work, skips final verification, and settles completion as cancelled. A slow consumer does not trigger the body-idle timer while it is not asking for another chunk; backpressure is preserved. Consumers requiring all-or-error verified bytes should prefer get().

GitHub does not guarantee that the pull-level diff is complete. The package always returns completeness: { state: "not-guaranteed-by-github", ... } and does not infer completeness from size, line count, file markers, status, or the optional per-file patch. It never substitutes compare-commits, whose merge base can differ. Boundary checks also cannot detect an A-to-B-to-A ref change.

Exact source acquisition

Build current base/head locators only from the guarded file collection. This keeps repositories and exact Git object IDs aligned, including forked pulls:

import {
  buildPullRefFileLocators,
  inspectPullFilePatch,
} from "@charlie-labs/github-api/projections";

const guardedFiles = await github.pulls.filesAtRevision({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  baseSha: pull.base.sha,
  headSha: pull.head.sha,
  maxPages: 30,
  maxItems: 3_000,
});

const locations = buildPullRefFileLocators(guardedFiles);
const head = locations.files[0]?.head;
if (head?.state === "candidate") {
  const content = await github.sources.getFile({
    ...head.locator,
    maxBytes: 2 * 1024 * 1024,
    maxRequests: 64,
    maxPathComponents: 64,
  });
  consumeSource(content);
}

const patch = inspectPullFilePatch(guardedFiles.items[0]!);

Current content at pull.base.sha is not necessarily the old side of GitHub's pull diff: renames, merge-base choice, and later base-branch changes make that alignment unprovable. Every locator therefore reports diffAlignment: "not-guaranteed". PullRequestFile.patch is optional and has no completeness contract; use pulls.diff when the pull-level representation is needed.

Batch acquisition keeps component traversal and concurrency 1 as its 1.x compatibility defaults. It preserves input order, reuses identical commit/tree/blob work only inside that logical call, chooses the lowest failing started input, and returns all results or throws:

const sourceFiles = await github.sources.getFiles({
  owner: "acme",
  repo: "widgets",
  commitSha: pull.head.sha,
  paths: ["src/index.ts", "package.json"],
  maxBytesPerFile: 2 * 1024 * 1024,
  maxTotalBytes: 4 * 1024 * 1024,
  maxRequests: 128,
  maxPathComponentsPerPath: 64,
  maxResponseBytes: 8 * 1024 * 1024,
  traversal: "adaptive",
  concurrency: 4,
  maxRecursiveTreeEntries: 25_000,
});

traversal: "adaptive" is opt-in. For a measured deep multi-file shape it may fetch one recursive tree after verifying the exact commit, but only uses that tree when it is complete, non-truncated, well formed, within the caller's entry limit, and contains every required path prefix. It otherwise falls back to the same bounded component walk. Shallow or too-small requests skip the recursive attempt when it is predicted to waste a request. No fallback uses a branch or moving ref. A result that actually used the complete recursive index sets source.traversal: "recursive"; component and fallback traces keep the existing shape.

concurrency is an opt-in positive integer from 1 through 8. It uses the same client physical-request scheduler and one logical request identity, deadline, signal, and source request/byte budgets. New paths stop scheduling after a terminal failure, already-started work settles deterministically, and no partial array escapes. Aggregate-byte reservations prevent concurrent blobs from racing beyond maxTotalBytes; returned repeated-blob values retain independent mutable byte views. Queued reads are served round-robin across logical operations and FIFO within each operation, so one batch cannot continually retake every newly available client-local permit. Choose concurrency from deployed tail latency and secondary-rate evidence rather than assuming 8 is universally faster.

JSON response bytes, physical commit/tree/blob requests (including retries and revalidations), path components, each file, and the batch total have separate budgets. Duplicate or empty paths fail before I/O. No successful subset is returned after a later failure.

Source results preserve Git mode and exact bytes:

  • ordinary 100644 and executable 100755 files are classified as strict UTF-8 text, binary, or a strict Git LFS v1 pointer;
  • 120000 symlinks return their link bytes and are never followed;
  • 160000 gitlinks return the submodule commit and are never cloned;
  • an LFS pointer exposes pointer metadata but the LFS object is never fetched;
  • missing tree paths are distinct from inaccessible repositories/objects, truncated required trees, unsupported shapes, and GitHub's blob-size limit;
  • removed head versions and deleted/inaccessible fork repositories remain explicit locator states and never fall back to the base repository.

The package never follows download_url, raw_url, response-provided object URLs, archives, or other origins.

Raw escape hatch

Raw calls cover uncommon fields and endpoints while retaining the credentialed client's authentication, scheduling, budgets, cancellation, redaction, events, timeouts, and normalized errors:

const repositoryResponse = await github.raw.rest.request("GET /repos/{owner}/{repo}", {
  owner: "acme",
  repo: "widgets",
});

const untypedRepositoryResponse = await github.raw.rest.requestUntyped(
  "GET /repos/{owner}/{repo}",
  {
    path: { owner: "acme", repo: "widgets" },
    headers: { accept: "application/vnd.github+json" },
  },
);

const customResult = await github.raw.graphql.execute(MyGeneratedDocument, {
  owner: "acme",
  repo: "widgets",
});

const untypedResult = await github.raw.graphql.executeUntyped(
  `query RepositoryId($owner: String!, $repo: String!) {
    repository(owner: $owner, name: $repo) { id }
  }`,
  { owner: "acme", repo: "widgets" },
);

const selected = await github.raw.graphql.executeUntyped(
  `query ViewerLogin { viewer { login } }
   query ViewerId { viewer { id } }`,
  {},
  { operationName: "ViewerId" },
);

raw.rest.request() accepts only routes from the package's pinned Octokit endpoint map. requestUntyped() has an explicit structured path/query/body grammar and accepts only same-origin URLs beneath the configured API base path. A Uint8Array body is sent as exact copied bytes; every other defined body is JSON-serialized, and GET/HEAD reject bodies. Allowed endpoint headers may be supplied, but credentials, framing, API version, content coding, validators, request identity, transport, base URL, and redirect behavior remain package-owned. The separate-origin release-asset upload route is rejected rather than having credentials rewritten or forwarded across origins.

Every raw REST or GraphQL method is authenticated-only. Anonymous clients must use the reviewed high-level REST hierarchy.

raw.graphql.execute() infers its result and variables from a generated TypedDocumentNode. executeUntyped() parses a string document and returns unknown; callers must validate or narrow it. A multi-operation untyped document requires an exact operationName.

Raw calls acquire one upstream page and intentionally provide no pagination helpers or completeness claim. Follow REST links from the full response envelope, or pass GraphQL pageInfo.endCursor, in an explicit subsequent call.

Cancellation, timeouts, events, and errors

A caller signal governs the complete logical operation across scheduler waits, token/cache callbacks, retries, companion reads, pages, and body consumption:

const controller = new AbortController();
const pending = github.pulls.filesAtRevision({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  baseSha: pull.base.sha,
  headSha: pull.head.sha,
  signal: controller.signal,
});

controller.abort("request superseded");
await pending;

responseStartTimeoutMs measures dispatch until response headers for each physical attempt. bodyIdleTimeoutMs measures only a pending next-chunk read; it is not a whole-operation deadline and does not run while a stream consumer is applying backpressure. Use AbortSignal.timeout(...), or combine it with a caller signal, for an overall deadline:

const github = createGitHubApi({
  auth: { token: process.env.GITHUB_TOKEN! },
  responseStartTimeoutMs: 15_000,
  bodyIdleTimeoutMs: 20_000,
});

await github.pulls.get({
  owner: "acme",
  repo: "widgets",
  pullNumber: 417,
  signal: AbortSignal.timeout(60_000),
});

High-level values intentionally omit validators. Read bounded, redacted ETag/Last-Modified observations from events, or use a raw REST response when full response metadata is required:

const github = createGitHubApi({
  auth: { token: process.env.GITHUB_TOKEN! },
  onEvent(event) {
    if (event.type === "physical-request-finish") {
      observeValidators(event.etag, event.lastModified);
    }
  },
});

const response = await github.raw.rest.request("GET /repos/{owner}/{repo}", {
  owner: "acme",
  repo: "widgets",
});
observeValidators(response.headers.etag, response.headers["last-modified"]);

Errors expose package-owned typed fields and bounded allowlisted details, never an arbitrary upstream response body, partial GraphQL data, mutation content, authorization headers, or an unsafe transport cause:

import {
  GitHubApiError,
  GitHubHttpError,
  GitHubRateLimitError,
} from "@charlie-labs/github-api";

try {
  await github.pulls.get({
    owner: "acme",
    repo: "widgets",
    pullNumber: 417,
  });
} catch (error) {
  if (error instanceof GitHubApiError) {
    reportRequestCorrelation(error.requestId, error.githubRequestId);
  }

  if (error instanceof GitHubRateLimitError) {
    if (error.retryAt !== undefined) {
      scheduleAfter(error.retryAt);
    } else {
      report(error.rateLimitKind, error.retryAfterMs);
    }
  } else if (error instanceof GitHubHttpError) {
    report(error.status, error.kind, error.details.documentationUrl);
  } else if (error instanceof GitHubApiError) {
    report(error.kind, error.operation);
  } else {
    throw error;
  }
}

requestId identifies the logical package operation across its pages, retries, and other physical attempts. Optional githubRequestId identifies only the GitHub response that triggered the failure and is absent when no safe response request ID exists.

Conditional REST cache

No cache is active by default. An injected adapter stores bounded, content-decoded JSON bytes only for eligible successful REST GET responses with a validator; each physical collection page has its own entry. Every hit is conditionally revalidated, never served as fresh by a TTL.

import type {
  ConditionalRestCacheAdapter,
  ConditionalRestCacheEntry,
} from "@charlie-labs/github-api";

const entries = new Map<string, ConditionalRestCacheEntry>();
const adapter: ConditionalRestCacheAdapter = {
  async get(key) {
    return entries.get(key);
  },
  async set(key, entry) {
    entries.set(key, entry);
  },
  async delete(key) {
    entries.delete(key);
  },
};

const github = createGitHubApi({
  auth: { token: process.env.GITHUB_TOKEN! },
  cache: {
    adapter,
    authorizationNamespace: "acme-review-bot/visibility-v3",
  },
});

The namespace identifies one authorization/visibility context, not just a user. Rotate it whenever repository visibility, membership, SSO authorization, permissions/scopes, or another access property could narrow or change results. Token rotation may retain it only when that context is intentionally equivalent.

Entries use the versioned github-rest-json-utf8-v1 representation with a body SHA-256 digest, copied bytes, bounded validators, and a storage timestamp. The digest detects accidental corruption; the adapter is a trusted component inside the consumer's security boundary. Cached private-repository JSON is sensitive: apply access control, encryption at rest, retention/deletion policy, tenant isolation, and an appropriate backup policy. The package does not encrypt entries or choose their lifetime.

GraphQL, raw REST, mutations, diffs, decoded source bytes, and Git blob bodies are never cached. Exact commit/tree JSON used during source traversal may be revalidated independently. Adapter failures fail open except caller cancellation, which stops the operation.

Installation repositories

Use the authenticated installation namespace to enumerate repositories visible to the current GitHub App installation token without dropping to raw:

const repositories = await github.installation.repositories.all({
  pageSize: 100,
  maxPages: 20,
  maxItems: 2_000,
});

installation.repositories exposes page(), lazy pages(), flat all(), and allWithMeta() over GET /installation/repositories. Items use the pinned Octokit repository schema directly, including repository ID, owner login, name, full_name, private, visibility, archived, and html_url. GitHub's total_count is returned as page().totalCount and is used with validated Link continuations to reject an omitted next page. A successful allWithMeta() reports { state: "complete" }; this means the bounded pagination traversal ended, not that multiple pages formed an atomic snapshot.

The resource requires credentialed authentication and is intended for an installation access token. Anonymous mode rejects before transport. Ordinary read retry, cancellation, response/page/item budgets, normalized errors, conditional REST cache partitioning, and test-transport behavior apply.

Checks and commit statuses

Use the exact REST resources when their endpoint-specific fields matter:

const checkRuns = await github.checks.forRef.all({
  owner: "acme",
  repo: "widgets",
  ref: pull.head.sha,
  filter: "latest",
});

const statusHistory = await github.statuses.forRef.all({
  owner: "acme",
  repo: "widgets",
  ref: pull.head.sha,
});

const combined = await github.statuses.combinedForRef.all({
  owner: "acme",
  repo: "widgets",
  ref: "main",
});

Check-run appId accepts a positive GitHub App database ID or GitHub's exact -1 sentinel for runs created by GitHub Actions; zero and other negative values are rejected before authentication.

statuses.combinedForRef.all() is the sole non-flat .all() result: it keeps GitHub's aggregate state, resolved sha, repository, and totalCount alongside items. After the first page resolves a branch or tag, every continuation cursor is bound to that exact SHA, including when resumed by a new client. Aggregate status can still change while the pages are read, so the result exposes stateChangedDuringCollection.

The package preserves GitHub's checks and statuses; it does not decide which ones are required or whether a pull is ready to merge. Check runs typically require Checks read permission, while commit status resources typically require Commit statuses read permission, subject to repository visibility and organization policy. See GitHub's endpoint documentation for check runs, listed statuses, and [combined status](htt