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

@ax-hub/sdk

v6.3.1

Published

agent-first Node.js SDK for AX Hub. Designed for Claude, Codex, and other coding agents.

Readme

@ax-hub/sdk

agent-first Node.js SDK for AX Hub. Designed for Claude, Codex, and other coding agents — one client, 7 bounded contexts, typed errors, async iterators for SSE streams, generated drift inventory, and no hidden Korean substring matching.

이 SDK를 개발/유지보수하려는 컨트리뷰터라면 → 내부 아키텍쳐·동작 원리·하네스·e2e 플로우를 설명한 온보딩 문서 docs/ARCHITECTURE.md부터 읽으세요. (이 README는 SDK 사용자용 API 가이드입니다.)

거버넌스(admin) surface 를 찾고 있다면 → tenants CRUD / authz / audit / identity-providers / category CUD 는 @ax-hub/sdk 에서 분리되어 별도 패키지 @ax-hub/admin-sdk 로 이동했습니다 (1.0.0). 0.x 에서 올라온다면 docs/MIGRATION-1.0.md 의 매핑 표를 보세요.

I want to...

| Goal | Section | |------|---------| | make my first API call | Magic Moment | | scope work to a tenant/app | Tenant Scoping | | choose JWT vs PAT or OAuth | Authentication | | access the app database | App Database | | debug an error | Errors & Debugging | | use admin/governance APIs | @ax-hub/admin-sdk | | upgrade from 0.x | Migration & Upgrade |

Agent field guide from live QA (2026-06-08)

Use this section when an autonomous agent only has the README and must ship against AX Hub safely.

1. Runtime inputs

export AX_HUB_PAT="<short-lived PAT>"
export AX_HUB_TENANT_ID="cc1e58f1-8e46-4ac7-96c1-190c4cdd5b70"   # test tenant
export AX_HUB_TENANT_SLUG="test"
  • PAT auth is tokenType: 'pat' and is sent as X-Api-Key.
  • JWT auth is tokenType: 'jwt' and is sent as Authorization: Bearer.
  • Never log the token. Redact env dumps before saving QA artifacts.

2. Fastest safe live loop

  1. Create a timestamp-suffixed private app in tenant test.
  2. Set an env var, list env vars, then delete it and assert it is absent.
  3. Enable raw DB mode (rawDb.enable), then introspect the physical DB with rawDb.tables.
  4. Soft-delete the app and confirm it is gone with a follow-up get.

3. Deletion semantics that prevent false positives

  • App delete: prove it by a follow-up get returning 404 or 410.
  • Deploy without git/bootstrap source can return a precondition-style 4xx. That proves error handling, not a deployment failure.

4. Production evidence already collected

  • Node production mutation suite exercised app/env/comments/likes/table/columns/grants/OAuth/publication/deploy/git preconditions with exit 0.
  • A real app bootstrap/deploy wait succeeded in production: app d31958ad-4a9b-4dcc-8951-64a1f3060c3d, deployment d3a48ce3-0f9c-4bab-aa07-863c31c44460, final status succeeded, followed by app permanent delete.
  • Go, Java, Kotlin, Python, and Ruby each hit 189 generated backend operation facades against the same production test tenant with SDK exceptions 0 and backend 5xx 0.
  • Go, Java, Kotlin, Python, and Ruby each passed the strict destructive DB loop above: 22 live steps, 17 assertions, 7 cleanup calls.

Magic Moment

import { AxHubClient } from '@ax-hub/sdk'

const sdk = new AxHubClient({
  token: process.env.AX_HUB_PAT!,
  tokenType: 'pat',
})

const acme = sdk.tenant('acme')
const app = await acme.apps.create({ slug: 'crm', name: 'CRM' })

// Enable a dedicated Postgres role; DATABASE_URL is injected on the next deploy.
await sdk.apps.rawDb.enable(app.id)
const tables = await sdk.apps.rawDb.tables(app.id)

console.log(app.slug, tables.length)

5분 안에 첫 app ship 가능.

Resource Catalog

| Namespace | Methods | |-----------|---------| | sdk.apps | create, list, listAll, get, update, delete, listMine, resourcePresets, listEnvVars, setEnvVar, getEnvVar, deleteEnvVar | | sdk.apps.publication | submit, list, unpublish (owner-scoped lifecycle) | | sdk.apps.access | grant, revoke, me (self-grant; me() returns null on 404) | | sdk.apps.likes | like, unlike, me (idempotent — backend returns liked/deleted booleans) | | sdk.apps.comments | add, list, listAll, delete (1-500 char client-side validation) | | sdk.apps.oauthClients | create (⚠ clientSecret surfaced ONCE), delete | | sdk.apps.git | connect, installStart (GitHub App install flow) | | sdk.apps.discover | catalog search facade | | sdk.apps.templates | app template listing | | sdk.deployments | create, list, listAll, get, cancel, rollback | | sdk.identity | pat.*, oauth.get, oauth.revokeOwnGrant, systemOAuthClients.get, me (identity-provider governance moved to @ax-hub/admin-sdk) | | sdk.tenants | get (read own tenant); CRUD + members/invitations/email-domains/icon moved to @ax-hub/admin-sdk | | sdk.gateway | sessions (create / end), query.run (SQL via session), invoke (REST via session), me (connectors / resources / grants) | | sdk.apps.rawDb | enable, disable, tables, tableRows (raw Postgres role + DB introspection) |

The committed generated route inventory currently tracks the pinned backend swagger snapshot; future backend-only BCs are surfaced by npm run route-inventory-diff.

sdk.apps.list vs sdk.apps.listMine

  • list() — returns apps in the resolved tenant (via defaultTenantId/defaultTenantSlug or withTenant). Tenant-scoped.
  • listMine() — returns the caller's workspace apps (owned + apps they've been granted access to), regardless of tenant ownership. Useful for per-user "my dashboard" views. Wraps GET /me/apps/workspace.

1.0: apps.create() requires a tenant context (defaultTenantId or sdk.tenant(...)). Without one it throws TenantIdRequiredError. See Migration.

sdk.apps.publication

  • sdk.apps.publication.*owner-scoped: submit/list/unpublish own app. App ID is the input. Reviewer/admin approval flows are not part of the developer surface; operate them via the backend directly.

sdk.apps.oauthClients.create — raw secret surfaced once

const c = await sdk.apps.oauthClients.create('app_abc', {
  name: 'web',
  redirectUris: ['https://myapp.com/cb'],
  scopes: ['read', 'write'],
})
storeSecret(c.clientSecret) // <-- this is your ONLY chance. Backend keeps only a hash.
//                              Subsequent list/get methods will NOT include the secret.

Lose clientSecret? Delete the client and create a new one.

Tenant Scoping

Prefer scoped clients in examples:

const acme = sdk.tenant('acme')
const app = await acme.apps.create({ slug: 'crm', name: 'CRM' })
await sdk.apps.rawDb.tables(app.id)

Flat/root APIs remain available for non-tenant routes (sdk.identity.me, sdk.identity.pat.*, sdk.identity.systemOAuthClients.get) and for backwards compatibility. There is no deprecation warning for flat calls.

Authentication

PAT (recommended for agents) — single token, immutable, sent as X-Api-Key on data-ring requests.

const sdk = new AxHubClient({ baseUrl, token, tokenType: 'pat' })

JWT with refresh — caller-provided refresh callback; concurrent 401s share a single in-flight refresh. The SDK does not issue tokens: onRefresh returns a fresh access token minted by your own auth flow.

const sdk = new AxHubClient({
  baseUrl,
  token: jwt,
  tokenType: 'jwt',
  onRefresh: async () => {
    const res = await fetch(`${baseUrl}/oauth/token`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ grant_type: 'refresh_token', refresh_token: refreshToken }),
    })
    const body = (await res.json()) as { access_token: string }
    return body.access_token
  },
})

OAuth Two Worlds

  • sdk.apps.oauthClients — an app acts as IdP for its own external users.
  • sdk.identity.systemOAuthClients.get — fetch a system OAuth client by id (the only global OAuth-client route). Creating one is app-scoped via sdk.apps.oauthClients.create.

App Database

DB access is via sdk.apps.rawDb.*, which enables a dedicated Postgres role and introspects the physical database. (The managed dynamic-table API sdk.apps.tables.* was removed — see CHANGELOG; use raw DB mode below.)

Raw DB mode

await sdk.apps.rawDb.enable(appId)
const tables = await sdk.apps.rawDb.tables(appId)
const rows = await sdk.apps.rawDb.tableRows(appId, 'orders', { perPage: 100 })
void [tables, rows]

rawDb.enable issues a dedicated Postgres role; DATABASE_URL is injected on the next deploy (the connection string is never returned). Use tables / tableRows for information_schema introspection of the physical DB.

Errors & Debugging

Error Types

All /api/v1/* 4xx/5xx responses become typed AxHubError subclasses. error.category (9 enum from backend spec) drives base class, error.code selects specific subclass.

| Class | Status | retryable | Hint | |-------|--------|-------------|------| | UnauthenticatedError (+ TokenMissingError, TokenExpiredError, TokenInvalidError) | 401 | true (re-auth) | refresh token | | PermissionDeniedError (+ NotAdminError, ForbiddenError, NotMemberError, NotAllowedError) | 403 | false | request grant | | NotFoundError (+ PermanentlyDeletedError, InvitationExpiredError) | 404 / 410 | false | abort | | ConflictError (+ SlugTakenError, AlreadyMemberError, AlreadyDeletedError, AlreadyRevokedError, AlreadySettledError, AlreadyAccessedError, PendingExistsError, InvalidStateTransitionError, SchemaNameTakenError, DomainTakenError, NotDeletedError, LastAdminError, DuplicateError) | 409 | false | try different value | | ValidationError (+ InvalidValueError, RequiredError, EmptyError, BadRequestError) | 400 / 422 | false | fix fields[] | | PreconditionFailedError | 412 | false | reconcile state | | RateLimitedError | 429 | true | sleep retry.afterMs | | InternalServerError | 500 | false | escalate to human | | UnavailableError (+ AppUnavailableError) | 502 / 503 / 504 | true | exponential backoff | | NetworkError, TimeoutError, DecodeError, AbortError | — | varies | — | | OAuthError (+ specific RFC 6749 codes incl. InvalidTargetError) | — | varies (per code) | — |

try {
  await sdk.apps.create({ slug: 'taken', name: 'X' })
} catch (e) {
  if (e instanceof SlugTakenError) {
    // retry with different slug; e.fields[0].name === 'slug'
  }
}

Gateway (session-scoped connector access)

sdk.tenant(slug).gateway is member-facing — open a session against a connector you hold an active grant on, then run SQL or proxy REST calls through it:

  • me.connectors() / me.connectorResources(id) / me.grants() — discover the connectors, resource trees, and grants available to you.
  • sessions.create({ connectorId }) / sessions.end(id) — open / close an 8h session (snapshots your grant's preset).
  • query.run({ sessionId, sql, params }) — parameterized SQL read through a session.
  • invoke({ sessionId, method, path, body }) — proxy a REST call through a session (REST-API connectors).

Admin-only connector governance is not in the SDK — manage those in the AX Hub console.

| Situation | What you get | |-----------|--------------| | sessions.create(...) without an active grant on the connector | NotFoundError thrown — no grant means the connector is not exposed (strict zero-trust). | | query.run(...) / invoke(...) policy deny | PermissionDeniedError thrown (403) — the session preset forbids the action. There is no in-band allowed flag; catch the typed error. | | query.run(...) / invoke(...) on an expired session | UnauthenticatedError thrown — sessions live ~8h; open a fresh one. |

const gw = sdk.tenant(slug).gateway
const session = await gw.sessions.create({ connectorId: 'con_1' })
try {
  const { rows } = await gw.query.run({
    sessionId: session.id,
    sql: 'SELECT id, name FROM employees LIMIT ?', params: [20],
  })
  renderRows(rows)
} finally {
  await gw.sessions.end(session.id)
}

Webhook Handling

import { verifyWebhook } from '@ax-hub/sdk'

const result = verifyWebhook({
  rawBody,
  secret: process.env.AX_HUB_WEBHOOK_SECRET!,
  signature: headers.get('x-ax-hub-signature')!,
  timestamp: headers.get('x-ax-hub-timestamp') ?? undefined,
})

Verification uses HMAC SHA-256, timing-safe comparison, timestamp tolerance, and optional replay cache.

Idempotency

HttpClient supports per-call idempotencyKey and generated Idempotency-Key on explicitly idempotent SDK calls. Empty keys fail fast with typed validation.

Codegen workflow

Generated inventory is committed under codegen/generated/.

npm run generate
npm run extract-codes
npm run route-inventory-diff

CI fails if swagger route inventory and generated files drift.

Migration & Upgrade

0.x → 1.0.0 is a hard cut (no prior deprecation). Two breaking changes:

  1. admin governance 36 op moved to the new @ax-hub/admin-sdk package.
  2. apps.create() requires a tenant context (defaultTenantId or sdk.tenant(...)).

See docs/MIGRATION-1.0.md for the full old→new mapping table and CHANGELOG.md [1.0.0] for the complete change list.

Concepts

  • Tenant scoping. defaultTenantId/defaultTenantSlug on constructor, client.withTenant(slug) for per-call switching, TenantSlugRequiredError/TenantIdRequiredError when ambiguous/missing.
  • Pagination. list({ pageSize, cursor }) for single page. listAll({ pageSize }) for async iterator that yields {type:'item', value:T} or {type:'drift', addedSince} when the backend's total grows mid-iteration.
  • Rate limiting. Default strategy 'sleep' — SDK honors Retry-After and silently retries. Use rateLimitStrategy: 'throw' to surface RateLimitedError(retry.afterMs) to caller.
  • Request correlation. SDK auto-generates X-Request-Id (ULID) on every request. Backend echoes it OR replaces with its own req_xxx prefix if absent. AxHubError.requestId always present.
  • Token redaction. Authorization / X-Api-Key / Cookie are always replaced with ***REDACTED*** in debug logs and Error.toJSON() output.
  • Debug mode. new AxHubClient({ debug: true, logger: pino() }) — opt-in structured request/response logging. Default off.
  • Language. Backend error.message is Korean (user-facing) by design. error.code and error.category are machine-readable (snake_case English) and stable across translations. Agents should branch on code/category; humans see the Korean message.

Branded ID types (optional)

For callers wanting compile-time guards against mixing IDs (e.g., passing tenantId where appId is expected):

import { type AppId, type DeploymentId, asAppId } from '@ax-hub/sdk'

const appId: AppId = asAppId('app_abc')
const depId: DeploymentId = asDeploymentId('dep_xyz')

await sdk.deployments.create(appId)  // OK
// await sdk.deployments.create(depId)  // type error — DeploymentId ≠ AppId

The SDK's own methods still accept plain string — branded types are opt-in for caller code.

Performance

SDK overhead (excluding network + backend time), measured via npm run bench against an in-memory fake fetch:

| Operation | p99 | |-----------|-----| | apps.create happy path | < 0.08ms | | apps.create 409 conflict (error dispatch) | < 0.08ms | | Error dispatch alone (wrapped envelope → typed subclass) | < 0.005ms | | SSE frame parser (100 frames / chunk) | < 0.07ms |

Target was < 10ms p99 — 100x+ headroom in every path.

Backend dependency

Pinned against backend main (189 routes, 43 error codes). Re-generate types via npm run generate + npm run extract-codes after backend swagger updates.

License

Apache-2.0. See LICENSE.