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

v3.0.0

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, 8 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 | | query dynamic tables | Dynamic Data + Query DSL | | 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. Create a table with owner_id, title, status, and optional metadata columns.
  4. Add a column, inspect the table, then delete the column and inspect again.
  5. Add a table grant, list grants, revoke the grant, then assert the same grant id has revokedAt.
  6. Insert a row, get it by id, patch it, list it with a filter, count it, browse admin rows, then delete it.
  7. Assert row get after delete returns 404 or 410.
  8. Delete the table and assert inspect after delete returns 404 or 410.
  9. Soft-delete the app, then permanent-delete the app.

3. Deletion semantics that prevent false positives

  • Row delete: prove it by a follow-up get returning 404 or 410.
  • Table delete: prove it by a follow-up inspect returning 404 or 410.
  • Table grant delete: AX Hub currently soft-revokes. listGrants can still return the grant, but revokedAt must be set. Do not assert hard disappearance.
  • 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/data/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, defineSchema, where } from '@ax-hub/sdk'

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

const crm = sdk.tenant('acme').app('crm')
const Orders = defineSchema({
  table: 'orders',
  columns: {
    id: 'uuid',
    status: { type: 'enum', values: ['paid', 'pending'] as const },
    total: 'number',
  },
})

const app = await sdk.tenant('acme').apps.create({ slug: 'crm', name: 'CRM' })
const paid = await crm.data.table(Orders).list({
  where: where(Orders.cols.status).eq('paid'),
})

console.log(app.slug, paid.total)

5분 안에 첫 app ship 가능.

Resource Catalog

| Namespace | Methods | |-----------|---------| | sdk.apps | create, list, listAll, get, update, delete, permanent, listMine, signIconUploadURL, signIconDarkUploadURL, 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.categories | tenant category read (list, get); CUD moved to @ax-hub/admin-sdk | | sdk.apps.discover | catalog search facade | | sdk.apps.templates | app template listing | | sdk.apps.tables | list, create, delete, addColumn, dropColumn, listGrants, addGrant, revokeGrant (schema admin) | | sdk.publicationRequests | get, approve, reject, listPending (reviewer/admin namespace — separate from owner-scoped sdk.apps.publication) | | sdk.deployments | create, list, listAll, get, cancel, rollback | | sdk.identity | pat.*, oauth.*, oidc.*, deviceCode.*, 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.data | /data/{tenantSlug}/{appSlug}/{table} CRUD + bulk + typed DSL |

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 vs sdk.publicationRequests

  • sdk.apps.publication.*owner-scoped: submit/list/unpublish own app. App ID is the input.
  • sdk.publicationRequests.*reviewer/admin-scoped: get/approve/reject any publication request, list all pending. Publication request ID is the input. Requires publication_reviewer (approve/reject) or platform admin (listPending) roles.

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.tables.inspect(app.id, 'orders')
await acme.app('crm').data.table('orders').list()

Flat/root APIs remain available for non-tenant routes (sdk.identity.oauth.exchangeCode, sdk.identity.oidc.jwks, sdk.identity.me) 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.

const refreshClient = new AxHubClient({ baseUrl, token: jwt, tokenType: 'jwt' })
const sdk = new AxHubClient({
  baseUrl,
  token: jwt,
  tokenType: 'jwt',
  onRefresh: async () => (await refreshClient.identity.oauth.refreshTokens({ refreshToken })).accessToken,
})

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.

Dynamic Data + Query DSL

apps.tables.* owns DDL. data.discover() and data.table(schema) own runtime CRUD with PAT/data-ring auth.

Quick start — discover first

Agents can start without hand-writing a schema:

const orders = await sdk.tenant('acme').app('crm').data.discover<{
  id: string
  status: 'paid' | 'pending'
  total: number
}>('orders')

await orders.insert({ status: 'paid', total: 99 })

discover(table, { fresh, ttlMs }) introspects the table, caches schema per client, de-duplicates concurrent fetches, and returns the same DataTableClient surface as data.table(schema).

Recommended path:

| User | API | |------|-----| | Agent / CI | data.discover('orders') — runtime schema, no handwritten shape | | App code | data.table(Orders) — explicit schema kept near the feature | | Avoid | data.table('orders') for writes — no local validation/projection checks |

Explicit schema + DSL

const Orders = defineSchema({
  table: 'orders',
  columns: { status: { type: 'enum', values: ['paid', 'pending'] as const }, total: 'number' },
})
const page = await sdk.tenant('acme').app('crm').data.table(Orders).list({
  where: and(where(Orders.cols.status).eq('paid'), where(Orders.cols.total).gt(100)),
})

like.contains/startsWith/endsWith automatically escapes %, _, and \\. Use raw() only when you intentionally own SQL-like syntax.

Zod-compatible validation

defineSchema(..., { validate }) accepts a Zod-compatible object with safeParse(). Invalid inserts/updates fail before network with ValidationError; invalid validator objects fail with ConfigurationError.

const OrdersChecked = defineSchema({
  table: 'orders',
  columns: { total: 'number' },
}, {
  validate: {
    safeParse: (row: unknown) => (row as { total?: number }).total! > 0
      ? { success: true }
      : { success: false, error: { issues: [{ path: ['total'], code: 'too_small' }] } },
  },
})
await sdk.tenant('acme').app('crm').data.table(OrdersChecked).insert({ total: 99 })

Projection

Use select to request and type-narrow only the columns you need. The SDK sends backend _select=id,total, validates unknown columns when a schema is available, and returns Pick<Row, K>.

const OrdersProjection = defineSchema({ table: 'orders', columns: { id: 'uuid', total: 'number' } })
const ordersProjection = sdk.tenant('acme').app('crm').data.table(OrdersProjection)
const minimal = await ordersProjection.list({ select: ['id', 'total'] as const })
minimal.items[0].total // number

Offset pagination

Backend-main data routes are offset/page based. Use pageSize with the numeric string cursor returned by the previous page. after, before, direction, and v1:/v2: keyset tokens are rejected with LegacyCursorError because the backend does not support keyset cursors.

total is optional: use items and nextCursor for iteration, or call count() when you need a backend count for the same pushable where filter.

const OrdersCursor = defineSchema({ table: 'orders', columns: { id: 'uuid' } })
const ordersCursor = sdk.tenant('acme').app('crm').data.table(OrdersCursor)
const first = await ordersCursor.list({ pageSize: 50 })
const next = first.nextCursor
  ? await ordersCursor.list({ cursor: first.nextCursor, pageSize: 50 })
  : null
void next

Mock mode

Use mock mode for backend-free unit tests, examples, and agent CI. Fixtures are isolated per client and still exercise where/projection/offset pagination/Zod behavior. Keep fixture sets small enough for in-memory tests (recommended ≤10K rows per process); use the real backend for load/perf scenarios.

const sdk = new AxHubClient({
  mode: 'mock',
  fixtures: { 'acme/crm/orders': [{ id: 'o1', total: 99, status: 'paid' }] },
})
const paid = await sdk.tenant('acme').app('crm').data.discover('orders')

Production mock mode throws MockInProductionError unless AX_HUB_ALLOW_MOCK_IN_PROD is set to a truthy value. The SDK normalizes both env vars: NODE_ENV is matched case- and whitespace-insensitive against 'production', and the opt-in accepts any of '1' / 'true' / 'yes' / 'on' (case- and whitespace-insensitive). Mock parity also throws NotFoundError on get/update/delete of missing rows and ConflictError on duplicate insert — drop-in behaviour matches the real backend.

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.