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

@brownandroot/api

v9.0.0

Published

Unified TypeScript API for Brown & Root APIHub data.

Readme

@brownandroot/api

Unified TypeScript API for Brown & Root APIHub data.

Installation

bun add @brownandroot/api

One Import Path

Use only:

import { employees, workorders, clearCache } from '@brownandroot/api'

No domain subpath imports are required.

Setup

Add to your app .env:

APIHUB_URL=https://your-apihub-url.com
APIHUB_API_KEY=your-api-key

Enable SvelteKit remote functions if needed:

kit: {
	experimental: {
		remoteFunctions: true
	}
}

Unified Domain Contract

Each listable domain exposes the same shape:

  • getAll(filters?)
  • getDropdown(filters?)
  • get(id) -> returns item, or null when APIHub answers 404

Every one of these throws ApiHubReadError when the read fails (see below).

dropdown(filters?) remains available as a backward-compatible alias.

Domains:

  • employees
  • workorders
  • costcodes
  • paytypes
  • businessUnits
  • jobtypejobsteps
  • chat
  • llm
  • rag

Caching Behavior

  • Browser/client: IndexedDB stale-while-revalidate cache for getAll and dropdown
  • Server/SSR: in-memory TTL cache for getAll and dropdown
  • Browser reads go through the package's remote queries. Server reads call APIHub directly (8.0.0), because SvelteKit copies every remote query result a request runs into the response. So what your server code reads through these domain objects stays on the server unless you return it. llm.getLogs and rag.list are still remote queries on both sides.
  • Single-record get(id) reads are fresh and return null when not found
  • A failed read is never cached. The next call asks APIHub again, in the browser too (8.0.0).
  • A stale cached list is still served while a background refresh fails. A cached EMPTY list is re-checked after 30 seconds, and if that re-check fails the read throws rather than serve the [].

Failed reads throw (7.0.0)

Every domain read throws ApiHubReadError when it fails:

  • getAll, getDropdown/dropdown and get on every domain
  • workorders.byBu, workorders.search, businessUnits.jtjs
  • employees.getAlternateBadges, getSupervisorChain, getJdeFromEmail, getJdeAccess

"Fails" means any non-2xx answer except a 404 on a single lookup, plus a timeout or a dropped connection. That includes a 400: an unscoped cost code or work order getDropdown is refused by APIHub and throws.

A 404 on a single lookup is an answer, not a failure. get(id) returns null and getJdeFromEmail returns { jde: null, employee: null }.

employees.get(id) is stricter (8.0.0). It returns null only when APIHub answers its own {"error":"Employee not found"}. Any other 404 (an unmatched route, an APIHub without the route, a proxy, a stopped App Service) throws ApiHubReadError with status 404, in the browser and on the server. The other get(id) routes answer the same {"error":"Not found"} body as an unmatched route, so they still return null on any 404.

Before 7.0.0 most of these returned [], null or a default on failure, so an outage looked like "there are none". Now an empty list means the data really is empty.

import { paytypes, isApiHubReadError } from '@brownandroot/api'

try {
	options = await paytypes.getDropdown()
} catch (error) {
	if (!isApiHubReadError(error)) throw error
	loadError = "Couldn't load pay types. Try again." // error.operation, error.status, error.cause
}

ApiHubReadError has operation (e.g. paytypes.getAll), status (HTTP status, or null for a timeout or a dropped connection) and cause (the original error). Use isApiHubReadError rather than instanceof if your app could end up with two copies of the package.

Retry

Call the read again. That is the whole retry (8.0.0): a failed read is not cached anywhere, so the next call asks APIHub again.

In the browser this used to need help. SvelteKit keeps a failed remote query cached, rejected, per arguments, so the same call got the same failure back without a request. The package now refreshes a query it finds holding a failure before it reads it. It does this once per call and only after a failure: a healthy read is not fetched twice, nothing retries on its own, and a failure that persists throws again. Drop any refreshAll() you added before a retry.

During an outage, each call to a failing read is one request. The background refresh of a stale cached list is the exception: one at a time per list, and not again within 30 seconds of a failure.

<script lang="ts">
	import { paytypes } from '@brownandroot/api'
	let attempt = $state(0)
</script>

{#key attempt}
	{#await paytypes.getDropdown()}
		Loading...
	{:then list}
		<!-- ... -->
	{:catch}
		Couldn't load pay types. <button onclick={() => attempt++}>Retry</button>
	{/await}
{/key}

Upgrading from 6.x: find every call that relied on the fallback (?? [], an empty list read as "none", getJdeAccess read as "no access") and decide what the screen should show when the read fails. A caller that still wants the old fail-closed getJdeAccess catches and returns { canViewRates: false, securityLevel: 0 } itself.

Error Diagnostics

The package now throws structured ApiHubError instances for request-layer failures.

ApiHubError includes:

  • status
  • path
  • url
  • responseBody
  • retriable
  • service

Request layer behavior:

  • timeout with AbortController (default 60000ms)
  • retries for idempotent GETs on 429/502/503/504 (default retryCount: 2)
  • optional onError callback via ApiHubClient options

Dropdown defaults:

  • Domains with isActive automatically default isActive: true for getDropdown(filters?)
  • Caller can override by passing isActive explicitly

Examples

Workorders

import { workorders } from '@brownandroot/api'

const rows = await workorders.getAll({ businessUnitId: 'BU001' })
const options = await workorders.getDropdown({ businessUnitId: 'BU001' })
const one = await workorders.get('WO001') // Workorder | null

Employees

import { employees } from '@brownandroot/api'

const crew = await employees.getAll({ hbu: 'TX01', q: 'john' })
const list = await employees.getDropdown({ businessUnitId: 'BU001' })
const emp = await employees.get('12345')

const chain = await employees.getSupervisorChain('12345')
const jde = await employees.getJdeFromEmail('[email protected]')

Since 8.0.0 these return Employee, which leaves out dob and identityHash. Server code that needs either one reads through the client, which returns the full EmployeeRecord:

import { getApiHubClientFromEnv } from '@brownandroot/api'

const record = await getApiHubClientFromEnv().getEmployee('12345') // EmployeeRecord

Server-only calls (@brownandroot/api/privileged)

Pay rates, PTO balances, the JT/JS override, and every write that changes an employee or the knowledge store. They are plain functions, not remote functions, so a browser cannot call them. Call them from server code after your own permission check. (6.0.0 moved updateEmployee and the RAG upload and delete here: as remote commands, any signed-in user could change anyone's roles.)

import { getEmployeePrivileged, updateEmployee, uploadDocument } from '@brownandroot/api/privileged'

const rates = await getEmployeePrivileged('12345')
await updateEmployee('12345', { alternateBadge: 'X-77' })

Identity check (9.0.0)

verifyIdentity takes the first three letters of each name, the birth date and the last four of the SSN, and answers the employee or null. It is server-only because those last four are only 10,000 guesses: as a root remote command (before 9.0.0) any signed-in user could find anyone's.

Limit attempts per address or session in your own code before calling it. APIHub also limits the route (5 attempts per person, 1,000 per caller address, in 15 minutes) and answers 429 past that, which throws IdentityRateLimitedError. APIHub's limit is shared by every user of your app, so it is a backstop, not your limit.

import { verifyIdentity, isIdentityRateLimitedError } from '@brownandroot/api/privileged'

try {
	const employee = await verifyIdentity({ first3FirstName: 'joh', first3LastName: 'doe', dob: '1985-03-15', ssn4: '4321' })
	if (!employee) return fail(400, { message: 'Those details do not match.' })
} catch (error) {
	if (isIdentityRateLimitedError(error)) return fail(429, { message: 'Too many attempts. Try again later.' })
	throw error
}

Chat

import { chat, rag } from '@brownandroot/api'

const result = await chat.complete({
	messages: [{ role: 'user', content: 'What tools do you have?' }],
	grounded: true,
})

console.log(result.message.content)
console.log(result.trace.toolCalls)

for await (const event of chat.stream({
	messages: [{ role: 'user', content: 'Summarize the overtime policy.' }],
})) {
	if (event.type === 'delta') {
		process.stdout.write(event.chunk)
	}
}

chat.complete and chat.stream are server-side APIs. They read API Hub credentials from APIHUB_URL and APIHUB_API_KEY by default, so consuming apps do not need to manually construct an ApiHubClient for the common case.

Chat request options:

  • grounded — default true; enables Brown & Root grounded chat behavior, including knowledge retrieval and the default tool bundle
  • instructions — appended to the default system prompt
  • systemPrompt — fully replaces the default system prompt
  • userContext — optional personalization context
  • source — optional application identifier for logging

Default grounded tool bundle includes:

  • searchDocs
  • searchDocsByType
  • listKnowledgeDocuments
  • searchBusinessUnits
  • searchWorkOrders
  • searchAppCatalog
  • searchAppCatalogRecords
  • getAppCatalogOverview

When the user asks broad app catalog questions (for example, "what is the app catalog" or "show everything in the app catalog"), the stream agent now prefers getAppCatalogOverview before generating the final response.

If systemPrompt is provided, it takes precedence over instructions.

Legacy LLM and RAG

import { llm, rag } from '@brownandroot/api'

const logs = await llm.getLogs()
const chat = await llm.chat({
	messages: [{ role: 'user', content: 'Summarize this document.' }],
	source: 'my-app',
	user: 'jane.doe',
	enableRag: true,
})

const docs = await rag.list()
const results = await rag.search({ query: 'overtime policy', topK: 5 })

llm.chat remains available for lower-level request/response usage.

llm.chat RAG flags:

  • enableRag: true tells the backend to enrich the chat prompt with retrieved document context before generating a response.
  • enableRag: false (or omitted) sends a normal chat request without forcing retrieval augmentation.
  • enableRag is the single supported parameter for toggling default tools/RAG retrieval.

Cache Management

import { clearCache } from '@brownandroot/api'

clearCache()
clearCache('workorders')
clearCache('employees')

Call clearCache(entity) after mutations so next reads fetch fresh data.

Advanced Direct Client

ApiHubClient is still available for advanced usage (streaming/proxy control/custom behavior):

import { ApiHubClient } from '@brownandroot/api'

const client = new ApiHubClient({
	baseUrl: process.env.APIHUB_URL!,
	apiKey: process.env.APIHUB_API_KEY!,
})

You can also let the package resolve those env vars for you:

import { getApiHubClientFromEnv } from '@brownandroot/api'

const client = getApiHubClientFromEnv()