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
npm install @qoniai/qoniimport { Qoni } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
})| Option | Type | Notes |
|---|---|---|
accessKey / secretKey | string | Required. Keep them on a trusted server |
host | string | Optional Qoni Console / SDK gateway for private or local deployments; hosted integrations normally omit it |
timeoutMs | number | Per-request HTTP timeout, default 30000 |
sseMaxRetries | number | SSE reconnect attempts, default 5; set 0 to disable |
fetch | typeof fetch | Custom 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.
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:
// 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:
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
| Field | Interactive | Silent | Notes |
|---|---|---|---|
mode | Must be interactive | May be omitted; defaults to silent | Delegation mode |
user | Not needed | Required | Preferred shape is { id: string }; top-level userId is deprecated |
agent | Optional | Optional | Audit label, defaulting to sdk |
scopes | At least one of scopes or products | Same | Fine-grained scopes |
products | At least one of scopes or products | Same | Authorization sugar for doAnything, webSearch, deepResearch, or track |
redirectUri / state | Required | Optional | Callback and anti-replay state |
expiresIn / idempotencyKey | Optional | Optional | Lifetime and idempotency key |
Delegation responses
SDK methods use QoniResponse<T>:
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:
| Field | Notes |
|---|---|
token | Delegation token for subsequent GUMem / Web Agent calls |
tokenType | Currently Bearer |
expiresIn | Lifetime in seconds |
grantId / auditId | Grant 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:
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:
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:writeproducts: ['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
| Namespace | Current public methods |
|---|---|
qoni.genauth | userInfo, jwks, discovery, introspectDelegationToken, users.* |
qoni.gumem | createSession, addMessages, recall, uploadResource, actions.* |
qoni.webSearch | run, attach |
qoni.doAnything | run, attach |
qoni.deepResearch | run, attach |
qoni.track | create, 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.
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
- Get started: Your first delegation in 30 minutes
- Reference: API reference, Token and claim reference