@arbidocs/sdk
v0.5.75
Published
Shared operations layer for ARBI — auth, config, SSE, WebSocket, and API operations
Readme
@arbidocs/sdk
TypeScript SDK for the ARBI platform. Handles authentication, workspace encryption, and streaming AI queries.
Start below. The complete developer quickstart and
compatibility table also ship in the npm package at
node_modules/@arbidocs/sdk/docs/; no repository access is required.
Quickstart
npm install @arbidocs/sdkimport { Arbi } from '@arbidocs/sdk'
const arbi = new Arbi({ url: 'https://your-arbi-deployment.example' })
await arbi.login(email, password)
await arbi.selectWorkspace(workspaceId) // an explicit choice
const response = await arbi.responses.create({ input: 'Say hello in one sentence.' })
console.log(response.output_text)Browser-only code imports the same API from @arbidocs/sdk/browser. For a rendered chat, use the basic React example or the website iframe. Node.js needs no IndexedDB polyfill. Keys stay in memory for this SDK instance unless you supply an explicit persistence adapter:
import { Arbi } from '@arbidocs/sdk'Add a chat widget
The installed SDK includes a standalone React starter (in releases containing this guide):
cp -R node_modules/@arbidocs/sdk/examples/react-chat ./my-arbi-chat
cd my-arbi-chat
mv gitignore .gitignore
npm install
cp .env.example .env
# Set the public VITE_ARBI_URL, then:
npm run devSign in inside the widget and explicitly choose a workspace. To deploy, run npm run build
and serve dist/ as a static site. Never put credentials in VITE_ variables. The deployment
must allow this app's origin for API calls. The app uses @arbidocs/blocks/embed, with matching
package versions and stylesheet setup already supplied.
For a website without a React build pipeline, use the frontend deployment's hosted panel:
<iframe src="https://YOUR-ARBI-FRONTEND/embed/chat" title="ARBI chat"
style="width:380px;height:640px;border:0" allow="clipboard-write"></iframe>The panel owns sign-in and workspace choice. Your deployment must allow the host site to frame
this route; the parent CSP must allow the frontend in frame-src. This is authenticated
workspace chat. Embedded storage restrictions can require a separate sign-in.
Lifecycle
const arbi = new Arbi({ url: 'https://www.arbidocs.com' })
await arbi.login(email, password) // or arbi.loginWithKey(email, keyBase64)
await arbi.selectWorkspace(workspaceId)
// ... use the SDK ...
await arbi.logout()| Property | Description |
|----------|-------------|
| arbi.isLoggedIn | Whether the user is authenticated |
| arbi.hasWorkspace | Whether a workspace is selected |
| arbi.getDeploymentDomain() | The domain the user's keys are derived for: the deploymentDomain option, else the hostname of url (e-signing re-derives keys with it) |
Operations
| Namespace | Key methods |
|-----------|-------------|
| arbi.workspaces | list(), create(), update(), delete(), listUsers(), addUsers(), removeUsers() |
| arbi.documents | list(), get(), uploadFile(), uploadUrl(), update(), delete(), download() |
| arbi.conversations | list(), getThreads(), updateTitle(), share(), delete() |
| arbi.responses | create(), parse(), retrieve(), attach(), cancel() — start here for new integrations |
| arbi.assistant | ask(request, handlers), attachRun(responseId, handlers), interject(messageId, text), cancel(responseId), retrieve(query, docIds) |
| arbi.tags | list(), create(), update(), delete() |
| arbi.doctags | assign(), remove(), generate() |
| arbi.contacts | list(), add(), remove() |
| arbi.health | check(), models(), mcpTools() |
Streaming AI queries
const stream = await arbi.responses.create({ input: 'What does section 3 say?', stream: true })
for await (const event of stream) {
if (event.type === 'response.output_text.delta') console.log(event.delta)
if (event.type === 'response.failed') console.error(event.response.error)
}Existing callback integrations
assistant.ask remains supported and uses /v1/responses. New integrations can start with
responses.create above; keep the callback helper when your UI already uses it.
const result = await arbi.assistant.ask(
{
question: 'What does section 3 say?',
docIds: [docId],
previousResponseId: lastMessageId, // for multi-turn conversations
},
{
onToken: (token) => { /* append to UI */ },
onStreamStart: ({ assistant_message_ext_id }) => { /* save for follow-ups */ },
onOutputItemDone: (event) => { /* an agent step settled */ },
onError: (error) => { /* the server's report (string) or the connection (Error) */ },
onComplete: () => { /* done */ },
}
)
// result.text — full accumulated response
// result.assistantMessageExtId — use as previousResponseId for follow-upsWhich workspace a turn runs in
A turn runs in workspaceId (default: the current workspace), read when ask or
startResponse is called and named on the request, so a workspace switch while it is
sent cannot move it. A workspace other than the current one is opened for this session
first (its key is deposited; the current workspace does not change). A follow-up runs in
its thread's workspace: if the server refuses it with 409 workspace_mismatch, the turn
is sent once more there, and onWorkspace(workspaceId) reports where it was accepted.
A 409 workspace_key_required deposits the key again and resends once. When the
workspace cannot be opened, nothing is sent and the call rejects with
ArbiWorkspaceUnavailableError. workspaceConflictOf(error) reads either 409.
assistant.submit and assistant.retrieve take workspaceId the same way.
Session recovery
Browsers persist the signing key encrypted in IndexedDB. Without IndexedDB, each SDK client keeps its own key in memory; it is not shared with other instances or saved to disk. Recover a session without a password:
await arbi.loginWithKey('[email protected]', signingKeyBase64)The SDK also auto-recovers on 401 responses using the stored key.
Security
- Zero-knowledge auth — passwords never leave the client
- E2E workspace encryption — symmetric keys wrapped with your public key
- Encrypted key storage — private keys in IndexedDB encrypted with non-extractable AES-GCM
- Session cleanup — logout ends the live session; the stored identity supports offline unlock and default storage is cleared on logout-everywhere
React
For React apps, use @arbidocs/react instead — it wraps this SDK with hooks and a context provider. See the @arbidocs/react README.
Requirements
- Browser: Any modern browser with Web Crypto API
- Node.js: native fetch/Web Crypto runtime; Node 22+ is used by the executable example and package smoke check (no
fake-indexeddbneeded) - TypeScript: 5.0+ (optional)
Automations
The workflows namespace exposes create/get/list/update/delete/run and listWorkflowRuns. Triggers use schedules or notification types; every saved rule can also run immediately. A document action (annotate_documents or email_documents) returns an execution reference rather than a conversation. Its persisted history contains selected documents, run-label references, result metadata and failures; annotation answers remain document fields. Every execution has a WorkflowRuns record; chat actions also link to a conversation.
import { workflows } from '@arbidocs/sdk'
const run = await workflows.runWorkflow(arbi, 'wfl-example')
if (run.error) throw new Error(run.error)
if (run.execution_id) {
const history = await workflows.listWorkflowRuns(arbi, 'wfl-example')
console.log(history)
}Document selectors can specify IDs, labels resolved at execution time, or latest documents since the previous successful run. Unattended document actions require an assistant with standing workspace access; creating a rule does not grant that access.
For latest-document digests, the first run covers the preceding 24 hours; later runs use the previous successful run cutoff. Failed delivery does not advance it. Emails contain document names and authenticated platform links, without attachments.
Releases
Published versions are listed on npm. Maintainers with repository access can also read the detailed GitHub release notes.
Errors and streaming outcomes
SDK HTTP failures retain their structured server body in ArbiApiError.apiError,
with status, detail, and optional code, requestId, and responseId.
AssistantTurnRefused remains an ArbiApiError when ask() rejects. Network
failures use ArbiTransportError and retain the original cause; native
AbortError objects propagate unchanged.
ask(), attachRun(), and streamSSE() retain their existing callbacks and result
fields. Add onErrorDetails(error) to inspect typed errors alongside the legacy
onError callback, and onOutcome(outcome) to observe the terminal result. SDK
stream results also include outcome and errorDetails. The outcome preserves
the existing assistant msg- identifier and raw terminal response payload.
Its status is completed, failed, cancelled, incomplete, disconnected, or
acknowledged. A control-only acknowledgement has no response identity. The
legacy completed boolean still indicates receipt of response.completed;
use outcome.status to distinguish the server's explicit terminal status.
A local abort or truncated connection reports disconnected, with reason
aborted, connection_closed, or connection_error; it never proves that the
server cancelled execution. Reader failures reject as well as notifying the
outcome. Explicit terminal server events finish reading without waiting for the
connection to close. The first terminal frame wins: later frames, including
conflicting terminals or tokens in the same chunk, are ignored. Background response APIs continue to expose the original
Responses envelope and its status and error fields.
Consumer callback exceptions propagate unchanged. They do not trigger a second
typed error or outcome notification and are not classified as server failures
or transport disconnections. onOutcome fires once when SDK consumption reaches
an outcome; an earlier consumer exception interrupts consumption before that
notification.
Composable response handles
assistant.startResponse(request, { signal }) uses the same request builder and workspace
selection as ask. It returns either an observer for the existing msg-… response, or a
control acknowledgement without inventing a message. Existing ask, callbacks, and attachRun
remain supported.
const started = await arbi.assistant.startResponse({ question: 'Summarize this document', docIds })
if (started.kind === 'response') {
const handle = started.handle
const unsubscribe = handle.subscribe((snapshot) => render(snapshot.displayText))
try {
const outcome = await handle.outcome
if (outcome.status === 'disconnected') {
const stored = await arbi.assistant.getResponse(handle.id)
// Inspect stored status; attachResponse(handle.id) again if still active.
}
} finally {
unsubscribe()
handle.detach()
}
}Use await handle.cancel() for an explicit authenticated server cancellation request.
detach() and the optional AbortSignal only close this reader. A cancellation request does
not settle the handle as cancelled: the terminal server event decides the outcome. Workspace
and native-agent permissions continue to be enforced by the server.
attachResponse('msg-…') observes the existing watch stream, with its bounded replay retention;
a 404 requires stored-response/history reconciliation. Handles do not retry or reconnect in the
background, and an attachment's disconnected outcome does not mean server execution stopped.
Snapshots and their output items are immutable. Subscriptions immediately receive the current
snapshot, including fast completion. Exceptions in a snapshot listener are rethrown in a microtask
without stopping the reader or other listeners. Unknown server statuses and raw terminal results
are preserved. displayText is the existing rendering projection, not a final-answer selector.
handle.events() provides one live raw iterator, beginning when called. It preserves unknown
JSON events/fields and SSE IDs; reasoning frames retain their raw text. Malformed JSON and
conflicting response IDs produce arbi.sdk.protocol_error diagnostics with the original frame.
This is not retrospective replay or a durable resume cursor. Keep existing callbacks when every
startup frame is needed. The iterator buffers at most 256 frames, covering existing tool and
control fixtures; overflow rejects its reads with code RESPONSE_EVENT_OVERFLOW, while snapshot
reduction continues. Return/break the iterator to release its buffer; a second iterator is rejected.
Node example and package check
Run examples/node-response.mjs with Node 22+ after installing
@arbidocs/sdk (node node_modules/@arbidocs/sdk/examples/node-response.mjs). Set ARBI_URL, ARBI_EMAIL, ARBI_PASSWORD and ARBI_WORKSPACE_ID;
ARBI_QUESTION is optional. It explicitly selects the current workspace, submits one Responses
turn, distinguishes disconnect from completion and refetches an accepted response rather than
resubmitting it. Ctrl-C detaches the reader; it does not claim to cancel background execution.
Default Node storage lasts only for that client instance. For persistence across process restarts,
pass persistence: { getSession, saveSession, clearSession } to Arbi or createArbiClient.
The existing SessionPersistence interface is retained; clearSession is optional for existing
adapters and is invoked by logout-everywhere when provided. Protect keys in any custom persistent
store. Normal logout retains the stored identity for the existing offline-unlock behavior;
logout-everywhere clears the default memory adapter. Browser encrypted storage remains unchanged.
Repository maintainers can run node scripts/sdk-node-smoke.mjs after building the client and SDK.
It packs both packages, installs their tarballs/runtime dependencies in a fresh temporary npm
consumer, checks ESM/CommonJS/browser/testing imports and exercises key login, recovery and
Responses over a local fake fetch. It requires the npm registry for dependencies, no deployment
credentials, and no IndexedDB polyfill. It verifies package/runtime behavior, not backend access
control or live deployment integration.
Configuring HTTP transport
React Query still manages React caches, mutations and refetching. The SDK transport underneath it can be configured once for typed requests, Responses streams and multipart API uploads:
const arbi = new Arbi({
url: 'https://arbi.example.com',
transport: {
fetch: customFetch, // optional fetch-compatible adapter
timeoutMs: 30_000, // optional; no deadline by default
onResponse: ({ method, url, status, durationMs }) => {
recordRequest({ method, url, status, durationMs })
},
},
})For a host-owned session, configure transport on createArbiClient and pass that client to
Arbi; its wrapper inherits the host transport and session gate. Configure it on the host,
rather than supplying a different transport to the wrapper. Existing host client adapters
without http retain the previous global-fetch fallback for raw SDK operations.
The timeout lasts until the adapter returns a Response: response headers for native fetch,
or the buffered reply for the XMLHttpRequest upload-progress adapter. It does not cap a live
stream's body. AbortSignal cancellation and sign-out still stop the body after headers.
Metadata hooks receive method, sanitized URL (no query, fragment or credentials), status and
elapsed time; no headers or bodies are passed. onError receives the original fetch failure.
Hooks are opt-in and synchronous; a thrown hook exception rejects the request. No telemetry
or logging is enabled by default.
Default browser uploads retain XMLHttpRequest byte progress. An injected fetch takes precedence over XHR and reports upload completion once rather than promising byte progress. The existing presigned storage PUT adapter, public booking helpers, media downloads and WebSockets are separate transports and are not changed by this API-transport option.
Connection failures are retried only for generated GET/HEAD requests, with the existing bounded backoff. Mutations and response creation are never retried after an ambiguous connection failure. Existing explicit authentication/workspace-state refusal recovery remains, through the same configured transport. This option adds no workspace selector, header, permission or switching.
Validated structured responses
const format = {
type: 'json_schema' as const,
name: 'invoice',
strict: true,
schema: {
type: 'object',
properties: { total: { type: 'number' } },
required: ['total'],
additionalProperties: false,
},
}
const response = await arbi.responses.parse<{ total: number }>({
input: 'Extract the total from: Invoice total 125.50',
text: { format },
})
// response.output_parsed.total is validated; output, usage and metadata are retained.The same text option is accepted by ask, startResponse and background submit.
For streaming/background work, retain the format and validate a completed resource with
await parseStructuredResponse(response, format). Live JSON fragments are provisional.
The parser expects one final assistant output message; it does not join narrative and answer
messages or treat metadata.content as a replacement for standard output items.
Invalid JSON, schema mismatch, refusal, incomplete/cancelled/nonterminal responses and invalid
schemas have distinct error codes. ArbiStructuredOutputError.response preserves the original
response; request/response IDs remain available. No type coercion, default insertion, Markdown
repair or automatic resubmission is performed. The TypeScript generic describes your schema's
shape; runtime validation comes from JSON Schema, not the generic parameter.
Schemas use draft 2020-12 and local references; format annotations are not asserted. Client
validation loads Ajv on demand and uses runtime compilation, with logging disabled. A runtime policy that prohibits
compilation produces STRUCTURED_OUTPUT_VALIDATOR_UNAVAILABLE before sending a request; raw
server-validated Responses remain available through the existing APIs without this client parser.
Requires the backend structured-output capability. It validates the existing model/agent loop's
final answer without changing workspace, identity or tool permissions. Native-agent and
slash/delegation paths that cannot honor this contract reject it explicitly with
STRUCTURED_OUTPUT_UNSUPPORTED; they never silently ignore it. Other request fields and old
callback consumers keep their existing behavior.
Familiar Responses operations
const response = await arbi.responses.create({ input: 'Hello', model: 'cfg-my-config' })
console.log(response.output_text)
const stream = await arbi.responses.create({ input: 'Hello', stream: true })
for await (const event of stream) {
if (event.type === 'response.output_text.delta') process.stdout.write(event.delta)
}
const queued = await arbi.responses.create({ input: 'Review this', background: true })
const current = await arbi.responses.retrieve(queued.id)The current workspace supplies the ARBI workspace context. workspace_ext_id is a wire-level
escape hatch subject to existing server checks; it does not switch the session or grant access.
create and retrieve retain the envelope's output, metadata, usage and terminal state, and add
an output_text convenience field. create also exposes _request_id when the server provides
it. A failed JSON response remains a failed response; inspect status and error.
create({ stream: true }) returns an async iterable of events with a standard type discriminator.
The existing message observer buffers events from the first frame and applies backpressure, so
fast replies and slow consumers do not lose the beginning. Iteration throws on a disconnected
reader; failed/incomplete terminal frames remain observable as events. Breaking iteration detaches
the reader and leaves server execution running. Explicit cancellation uses responses.cancel(id).
The optional stream.handle exposes ARBI snapshots, outcomes, detach and cancellation.
The existing responses.stream and assistant.startResponse methods still return ARBI handles
(or slash-command acknowledgements). Use those helpers for control commands. Streaming background
creation is currently unsupported and rejected before HTTP; create with background: true, then
use responses.attach(id) for existing replay/live events. One con- contains many msg- responses.
For structured results, responses.parse<T>({ input, text: { format } }) returns output_parsed
alongside output_text and the original output fields. It validates the JSON Schema before sending
one synchronous request; failures/refusals/incomplete results throw the existing structured error.
The schema is plain JSON Schema, not a Zod-specific helper. assistant.parseResponse and the
standalone parseStructuredResponse remain supported for existing consumers. These SDK patterns
follow the official Responses streaming guide
and structured-output guide.
Compatibility means the documented /v1/responses subset, not drop-in support for every OpenAI,
Anthropic or Vercel feature. Input is currently a string; tool context uses ARBI's documented tool
map. Authentication, cfg selection, agt identity, workspace grants and deferred execution remain
ARBI contracts. Native local-function tools and arbitrary provider options are not implemented.
See the backend's docs/responses-api.md for the exact supported fields.
Compose an existing configuration
Start from the configuration fetched by agentConfig.get(configId) so omitted settings keep
their current values. No helper creates an identity, grants permissions or contacts MCP servers.
import { composeConfiguration, instructions, capabilities, mcpTools,
documentTools, composeTools } from '@arbidocs/sdk/browser'
const existingConfiguration = { ...await arbi.agentConfig.get(configId), title: 'Evidence review' }
const updated = composeConfiguration(existingConfiguration,
instructions('Answer with evidence.'),
capabilities({ webSearch: true, codeExecution: false }),
mcpTools(existingConfiguration, ['approved_search'], 'add'))
await arbi.agentConfig.save(updated)
const tools = composeTools(existingTools, documentTools({ search: [documentId] }), 'add')
await arbi.responses.create({ input: 'Summarise the evidence', tools })Configuration sections merge one level; arrays replace; explicit null clears. MCP and response-tool
helpers require an explicit add or replace choice. Adding a tool replaces the same name while
preserving other names. Instructions apply to both QueryLLM and the built-in agent persona.
Ordinary cfg- configurations do not require agt- identities. Pinning a configuration to an
existing agent uses the existing agent_ext_id; authorizing its task remains a separate action.
Choose a public layer
| Need | Package / entry |
| --- | --- |
| Responses and domain operations, Node | @arbidocs/sdk |
| Same operations, browser-safe imports | @arbidocs/sdk/browser |
| Typed HTTP and advanced crypto/session access | @arbidocs/client |
| Headless React state and React Query hooks | @arbidocs/react |
| Standalone chat widget | @arbidocs/blocks/embed |
| Rendered composable views | @arbidocs/blocks and capability subpaths |
| Connected rendered views | @arbidocs/blocks/connect |
| Fake fetch, SSE frames, replay scenarios | @arbidocs/sdk/testing (tests only) |
Existing exports remain available. Import domain helpers through the documented package entries;
never import src/ or generated dist/ internals. A React Query hook uses the same session and
request builder as the SDK; applications do not need another chat engine.
The published examples/ directory includes node-response.mjs, stream-response.mjs,
background-response.mjs and structured-extraction.mjs. response-handle.mjs shows the optional
ARBI lifecycle controls. Install the package, set the environment variables documented in
examples/session.mjs, and run with Node 22+. Background inspection accepts an existing response
ID and never resubmits automatically. Tests can import frame and createSseFake from /testing
to exercise streams without a deployment (see the exported helper signatures).
Check setup
Run await arbi.diagnose() or arbi doctor --json to check deployment, session and current-workspace
setup without logging in, switching workspaces or granting access. See the packaged
diagnostics guide.
Human review
A runnable example ships at node_modules/@arbidocs/sdk/examples/human-review. Copy it to a local
folder, rename gitignore to .gitignore, install dependencies, configure .env, then run
npm start -- start. The example prints the existing conversation ID and commands to inspect,
send back, and explicitly approve its draft. It resumes the same workflow conversation across
processes using existing step APIs. See its
README.
