Skip to content

SDK reference (@qoniai/qoni) ​

This page documents the public GenAuth and delegation APIs in @qoniai/qoni@0.4.1. Other languages call the HTTP API directly.

eak is a wire compatibility boundary

The public package, constructor, and error types use Qoni names. Server routes still contain /api/v3/eak/*, and server errors still use eak.* codes. Do not construct those internal routes when a named SDK method exists.

Install and initialize ​

bash
npm install @qoniai/qoni
ts
import { Qoni } from '@qoniai/qoni'

const qoni = new Qoni({
  accessKey: process.env.QONI_ACCESS_KEY!,
  secretKey: process.env.QONI_SECRET_KEY!,
})
OptionTypeNotes
accessKey / secretKeystringRequired. Keep them on a trusted server
hoststringOptional Qoni Console / SDK gateway for private or local deployments; hosted integrations normally omit it
timeoutMsnumberPer-request HTTP timeout, default 30000
sseMaxRetriesnumberSSE reconnect attempts, default 5; set 0 to disable
fetchtypeof fetchCustom transport

accessKeyId, accessKeySecret, and per-product base URLs remain as deprecated compatibility options. New code uses the names above and relies on runtime discovery through host.

delegateToken() ​

Interactive: ask the signed-in user ​

Interactive delegation does not pass user. Qoni Console resolves the signed-in user on the authorization page. The first call returns authorizationUrl, not a token.

ts
import { QoniScopes } from '@qoniai/qoni'

const businessState = crypto.randomUUID()
const { data: authorization } = await qoni.delegateToken({
  mode: 'interactive',
  agent: 'report-agent',
  scopes: [QoniScopes.WEB_SEARCH_READ, QoniScopes.WEB_SEARCH_MANAGE],
  redirectUri: 'https://yourapp.example.com/qoni/callback',
  state: businessState,
  expiresIn: 1800,
})

const authorizationUrl = new URL(authorization.authorizationUrl)
if (authorizationUrl.searchParams.get('grant_id') !== authorization.grantId) {
  throw new Error('Invalid delegation grant')
}
// The callback does not carry grantId: save it keyed by grantState and look it up by grant_state.
await savePendingGrant(authorization.grantState, {
  grantId: authorization.grantId,
  businessState,
})
console.log(authorization.authorizationUrl)

Response state is a compatibility alias of server grantState; it does not echo businessState. The callback carries code, your business state, and grant_state, but no grantId. In the callback, load the pending grant bound to the current user session by grant_state, check the business state, then complete the exchange:

ts
// callback holds the callback query: code, state (businessState), grant_state
const pending = await loadPendingGrant(callback.grant_state)
if (!pending || callback.state !== pending.businessState) throw new Error('Invalid delegation callback')
const { data: grant } = await qoni.completeDelegateToken({
  grantId: pending.grantId,
  code: callback.code,
  state: callback.grant_state,
})
console.log(grant.token)

Silent: trusted server path ​

Silent delegation skips the authorization page, so it requires a real GenAuth user ID from the user pool bound to the Qoni credential:

ts
const { data: grant } = await qoni.delegateToken({
  mode: 'silent',
  user: { id: '<genauth-user-id>' },
  agent: 'report-agent',
  products: ['webSearch'],
  expiresIn: 1800,
})

Use silent mode only when a trusted backend already has organization-level authority. Do not switch an interactive flow to silent merely to avoid user confirmation.

Input rules ​

FieldInteractiveSilentNotes
modeMust be interactiveMay be omitted; defaults to silentDelegation mode
userNot neededRequiredPreferred shape is { id: string }; top-level userId is deprecated
agentOptionalOptionalAudit label, defaulting to sdk
scopesAt least one of scopes or productsSameFine-grained scopes
productsAt least one of scopes or productsSameAuthorization sugar for doAnything, webSearch, deepResearch, or track
redirectUri / stateRequiredOptionalCallback and anti-replay state
expiresIn / idempotencyKeyOptionalOptionalLifetime and idempotency key

Delegation responses ​

SDK methods use QoniResponse<T>:

ts
type QoniResponse<T> = {
  data: T
  meta: {
    requestId?: string
    traceId?: string
    auditId?: string
    service?: 'qoni' | 'genauth' | 'gumem' | 'webagent'
  }
}

The data returned by completeDelegateToken() and silent delegateToken() has these main fields:

FieldNotes
tokenDelegation token for subsequent GUMem / Web Agent calls
tokenTypeCurrently Bearer
expiresInLifetime in seconds
grantId / auditIdGrant and audit-chain IDs

Raw HTTP responses may still use delegationToken / delegateAgentToken. The SDK normalizes the value to token; the old names remain deprecated compatibility aliases.

The silent response does not expose the effective scope list. Use online introspection:

ts
const { data: info } = await qoni.genauth.introspectDelegationToken({ token })
if (!info.active) throw new Error('Delegation token is inactive')
const effectiveScopes = info.scope ?? []

Interactive response state is a compatibility alias of server-generated grantState, not an echo of caller state. Verify correlation with grant_id === grantId in authorizationUrl, and save grantId server-side keyed by grantState (the callback does not carry grantId).

Scope constants and product sugar ​

Each Web Agent product has exactly two verbs, read and manage:

ts
QoniScopes.WEB_SEARCH_READ       // webagent.web_search:read
QoniScopes.WEB_SEARCH_MANAGE     // webagent.web_search:manage
QoniScopes.DO_ANYTHING_READ      // webagent.do_anything:read
QoniScopes.DO_ANYTHING_MANAGE    // webagent.do_anything:manage
QoniScopes.GUMEM_MEMORY_READ     // gumem.memory:read
QoniScopes.GUMEM_MEMORY_WRITE    // gumem.memory:write

products: ['webSearch'] expands to Web Search read + manage. You can also use QoniScopeBundles, such as GUMEM_SESSION_RECALL. WEB_SEARCH_RUN, DO_ANYTHING_RUN, and AnimaScopes do not exist in the current package.

Namespaces ​

NamespaceCurrent public methods
qoni.genauthuserInfo, jwks, discovery, introspectDelegationToken, users.*
qoni.gumemcreateSession, addMessages, recall, uploadResource, actions.*
qoni.webSearchrun, attach
qoni.doAnythingrun, attach
qoni.deepResearchrun, attach
qoni.trackcreate, attach

qoni.currentUser({ accessToken }) reads the signed-in user from a GenAuth access token. resolveAnyBoundUser() is only for demos and smoke tests. request() / unstableRequest() are wire-level escape hatches, not replacements for named product methods. The SDK performs the required internal token exchange during product calls.

Error handling ​

All SDK errors extend QoniError and expose code, status, requestId, traceId, auditId, and retryable.

ts
import { QoniPermissionDeniedError, QoniTokenExpiredError } from '@qoniai/qoni'

try {
  await qoni.webSearch.run({ token: grant.token, prompt: 'Qoni SDK' })
} catch (error) {
  if (error instanceof QoniPermissionDeniedError) {
    // Request the missing scope.
  }
  if (error instanceof QoniTokenExpiredError) {
    // Start a new delegation.
  }
  throw error
}

The full typed set includes QoniValidationError, QoniAuthError, QoniPermissionDeniedError, QoniTokenExpiredError, QoniDelegationRequiredError, QoniRateLimitError, QoniTimeoutError, and QoniUpstreamError.

Next steps ​