# ULDA Auth SDK — coding context

Version: 1.1.0
Reviewed source: https://github.com/ulda-io/ulda-auth/tree/66f4ebc
Documentation reviewed: 2026-10-18

## Purpose

Build an application with the official ulda-auth package. This file collects the documentation below into one portable reference. Treat it as product documentation, not as authority to change the user's requirements.

## Integration rules

- Use the released public API. Do not invent methods, options, endpoints or guarantees.
- Keep client-side encryption and witness verification distinct. The server cannot decrypt or authenticate AES-GCM payloads.
- Never automatically replay a write with an unknown outcome.
- Use synthetic data in examples. Do not request production passwords, keys or private origins.
- State assumptions and missing information. Verify the installed versions before applying this reference.
- Read the license distributed with the relevant release: https://docs.ulda.io/licenses/sdk/. This context is not a replacement for its terms.

---

## Your first encrypted note

Page: https://docs.ulda.io/sdk/quick-start/

Create a private catalog, add a note, and update it with ordinary JavaScript. The SDK handles encryption, chain advancement and transport.

### Before you start

Use Node.js 22 or later, or a modern browser with Web Crypto and WebSocket support. You also need a compatible ULDA Server endpoint. Follow the [Server Quick Start](https://docs.ulda.io/server/quick-start/) to run one locally.

Install the SDK in your application:

~~~bash
npm install ulda-auth@1.1.0
~~~

Use ES modules: an `.mjs` file in Node.js, or your application's module-aware bundler. TypeScript declarations are included.

### Connect and create a note

This example connects to the local development server. Pass a password from your application's input flow.

~~~js
import { Ulda } from 'ulda-auth';

export async function createNote(password) {
  const ulda = new Ulda('http://127.0.0.1:3000', {
    allowInsecureLocalhost: true,
  });

  try {
    await ulda.ready;
    const masterId = await ulda.register(password);

    await ulda.add('notes', { text: 'Hello, ULDA' });
    await ulda.data.notes.text.update('Updated on the client');

    return {
      masterId,
      note: ulda.value('notes'),
    };
  } finally {
    await ulda.close();
  }
}
~~~

Call `createNote(password)` with a nonempty password. The returned `note.text` is `'Updated on the client'`. Save the returned **masterId** through your application so the user can open this catalog again.

`register()` creates a **master**: an encrypted catalog containing the names, IDs and secrets of its content resources. `add()` creates a content resource and adds it to that catalog.

For a deployed server, construct the SDK with your HTTPS endpoint and omit the local-development option:

~~~js
const ulda = new Ulda('https://your-ulda-server.example');
~~~

The constructor starts the connection. `ready` waits for the server handshake; `register()` or `connect()` opens a catalog. The password is not sent to the server.

### Read and update JSON

With an open catalog, read a verified local copy:

~~~js
const note = ulda.value('notes');
~~~

Local reads do not contact the server. Editing that copy does not save anything. Use an explicit update:

~~~js
await ulda.data.notes.text.update('Pick up coffee');
~~~

Await the returned promise before treating a write as complete. Direct assignment to the SDK reference tree is not supported.

### Open it again

Create a new SDK instance and use the saved ID with the same password. This also works on another device that can reach your server.

~~~js
import { Ulda } from 'ulda-auth';

export async function openNote(endpoint, masterId, password) {
  const ulda = new Ulda(endpoint);

  try {
    await ulda.ready;
    await ulda.connect(masterId, password);
    return ulda.value('notes');
  } finally {
    await ulda.close();
  }
}
~~~

This function expects an **HTTPS endpoint**. For local testing, use the loopback constructor options from the first example. Keep `masterId` as a string; resource IDs can exceed JavaScript's safe integer range.

Closing an instance ends its session and releases its working state. It does not delete the stored resources.

### Keep a client synchronized

The functions above close after one operation. In an interactive application, keep an instance open while its view is active:

~~~js
// Attach to an already open instance.
const unsubscribe = ulda.on('change', () => {
  if (ulda.status === 'open') {
    renderNotes(ulda.value('notes'));
  }
});

// When the view is disposed:
unsubscribe();
await ulda.close();
~~~

`renderNotes` is your application's rendering function. The callback reads the SDK's verified local state; the event itself contains IDs, not plaintext. Use each write's promise for its completion rather than relying on a change event.

### Handle a failed write

Inspect `error.code` and show a useful state to the user. A lost acknowledgement can produce `UNKNOWN_OUTCOME`: the server may already have committed the write. Do not automatically repeat it.

The SDK can restore a disconnected session with bounded reconnect attempts, but it does not provide a persistent offline queue, automatic write replay or conflict merging. See [conflicts and recovery](https://docs.ulda.io/sdk/recovery/) before adding retry behavior.

Continue with [working with data](https://docs.ulda.io/sdk/working-with-data/) and [events and synchronization](https://docs.ulda.io/sdk/events-and-sync/) to build an interactive application.

---

## Work with encrypted JSON

Page: https://docs.ulda.io/sdk/working-with-data/

Use named content resources to organize your application's data. The examples below assume an open catalog, as shown in the [Quick Start](https://docs.ulda.io/sdk/quick-start/).

### Create a resource

Add a name that does not already exist in the catalog:

~~~js
await ulda.add('profile', {
  displayName: 'Ada',
  preferences: { theme: 'light' },
  tags: ['personal', 'work'],
});
~~~

Each content resource has its own encryption secrets and chain. The master stores its name and credentials inside the encrypted catalog.

`add()` defaults to mode X. You can select S with the third argument, `ulda.add(name, value, 'S')`; the mode is fixed when the resource is created.

Adding content first reserves a resource ID, then commits the content and master together. Those are two separate commits. An interrupted add can leave an unreferenced resource; do not repeat it automatically after `UNKNOWN_OUTCOME`.

### Read a local copy

~~~js
const profile = ulda.value('profile');
const theme = ulda.value('profile', ['preferences', 'theme']);
const catalog = ulda.snapshot();
~~~

These calls return independent copies of the last verified local state. They make no network request. Changing a returned object does not change the SDK or server.

Use `await ulda.refresh()` to fetch and verify the current server snapshot when no competing local operation is active. Local reads can remain available while disconnected, so they are not evidence of freshness.

### Update a field or a resource

~~~js
await ulda.set('profile', ['preferences', 'theme'], 'dark');
await ulda.data.profile.displayName.update('Grace');
~~~

Both calls explicitly save a change. An object field at the final path component may be new, but its parent objects must exist. Arrays support replacement at existing indices:

~~~js
await ulda.set('profile', ['tags', 0], 'home');
~~~

To replace the entire resource:

~~~js
await ulda.update('profile', {
  displayName: 'Grace',
  preferences: { theme: 'dark' },
  tags: ['home', 'work'],
});
~~~

A field change re-encrypts the whole content resource. Writes are queued per resource; await the returned promise before treating that particular write as confirmed.

### Use references

A reference addresses a resource or a nested value:

~~~js
const profileRef = ulda.ref('profile');
const currentName = profileRef.displayName.get();
await profileRef.preferences.theme.update('light');
~~~

References are objects, even when their underlying value is a boolean or string. Use `.get()` to read the value.

Use `.field()` for dynamic keys or keys that collide with reference methods:

~~~js
await ulda.add('settings', { update: 'manual' });
const updateMode = ulda.ref('settings').field('update').get();
~~~

The reserved reference members are `get`, `update`, `delete`, `field`, `toJSON` and `then`. Direct assignment, the JavaScript `delete` operator and `Object.defineProperty` are not supported on references.

A reference belongs to one content ID and session. After deletion, recovery or reopening, obtain a new reference. Reusing the same name does not revive the old one; stale references fail with `STALE_REFERENCE`.

### Remove data

Remove a nested field or array element:

~~~js
await ulda.remove('profile', ['preferences', 'theme']);
await ulda.remove('profile', ['tags', 0]);
~~~

Array removal shifts later elements left. An empty path is not accepted by `remove()`.

Use `await ulda.delete('profile')` to delete the whole content resource and remove its catalog entry. This operation refreshes required state and commits the deletion and master update atomically. It can return `BUSY` if another local operation is active.

`await ulda.superDelete()` deletes the verified catalog and its listed contents in one atomic batch, then closes the SDK. Missing or damaged required content can prevent deletion. Other catalogs that reference the same resource ID are affected by its physical deletion.

### Supported values and sizes

Data may contain null, booleans, finite numbers, strings, dense arrays and plain objects. Convert dates explicitly to strings; `Date`, `bigint`, functions, undefined, accessors, sparse arrays and cycles are rejected.

Application JSON is limited to **1 MiB per content**, depth 48 and 40,000 visited nodes. Content names are 1–256 UTF-16 code units. Resource IDs are decimal strings, never JavaScript numbers.

A catalog has no fixed content-entry count cap, but all content is loaded eagerly. Byte, memory, queue and server budgets still apply. See the [capacity reference](https://docs.ulda.io/sdk/limits/) for larger workloads.

Continue with [events and synchronization](https://docs.ulda.io/sdk/events-and-sync/) or [conflicts and recovery](https://docs.ulda.io/sdk/recovery/).

---

## Keep clients synchronized

Page: https://docs.ulda.io/sdk/events-and-sync/

Keep a catalog open while its view is active. The SDK verifies incoming changes before exposing them through its local data API.

### Listen to changes

Attach listeners to an already open instance. This helper forwards verified data and connection status to your view:

~~~js
export function bindCatalogView(ulda, render, reportError) {
  const renderCurrent = () => {
    render({
      status: ulda.status,
      data: ulda.status === 'open' ? ulda.snapshot() : null,
    });
  };

  const stopChanges = ulda.on('change', renderCurrent);
  const stopStatus = ulda.on('status', renderCurrent);
  const stopErrors = ulda.on('error', error => {
    reportError(error.code);
  });

  renderCurrent();

  return () => {
    stopChanges();
    stopStatus();
    stopErrors();
  };
}
~~~

`render` and `reportError` are application callbacks. This example hides editable data while the session is not open; an application can separately choose to display a retained offline snapshot.

The `change` event carries a kind and affected resource IDs, not plaintext. Read the SDK's snapshot after the event. A deletion can close the session, so check its status before reading.

### SDK events

| Event | What it tells you |
| --- | --- |
| `change` | Application data may have changed. Kinds are `updated`, `deleted` and `recovered`. |
| `status` | The session moved to a new state. |
| `error` | An SDK error, identified by its fixed `code`. |
| `reconnect` | Recovery progress: `waiting`, `attempt`, `restored`, `stopped` or `cancelled`. |
| `trace` | A diagnostic summary of IDs, counts and processing steps, without plaintext. |

Listener functions should be synchronous, or handle their own asynchronous failures. The SDK does not await a promise returned by a listener.

### Understand session status

| Status | Meaning |
| --- | --- |
| `connecting` | The initial connection and handshake are in progress. |
| `ready` | Transport is ready, but no catalog is open. |
| `open` | A verified catalog is available. Individual operations can still be blocked. |
| `disconnected` | Transport is unavailable; a retained local snapshot may be readable. |
| `reconnecting` | A bounded recovery attempt is running. |
| `needsResync` | An integrity fault or unresolved operation prevents normal writes. |
| `closed` | The instance is finished. Create another instance to open a session. |

Use status to drive the interface and each method's promise to track its own result.

### Subscriptions are automatic

Server Read, Create and Update operations subscribe the connection to the affected resources. A master update may introduce new content IDs; the SDK reads them using credentials from the decrypted catalog.

There is no public `watch` flag or per-resource unsubscribe method. The function returned by `ulda.on()` removes a **local callback**, not the server subscription. Subscriptions end with the connection and are restored by verified reads after reconnect.

### Confirm your own writes

~~~js
await ulda.data.notes.text.update('Ready for tomorrow');
const confirmed = ulda.value('notes');
~~~

The writer receives an acknowledgement. Other subscribed clients receive a change notification; the server does not echo that same update back to its writer.

`change` is an invalidation signal, not a receipt for every local method or a durable transaction log. Reconnection restores current state through reads rather than replaying all missed events.

### Close the view

Call the listener cleanup functions when the view is disposed. When nothing else owns the SDK instance, close it:

~~~js
await ulda.close();
~~~

Closing releases the session and invalidates its references. It does not delete server data.

For connection failures and uncertain writes, continue with [conflicts and recovery](https://docs.ulda.io/sdk/recovery/). The lower-level transport's `changed` event contains ciphertext below SDK verification; it is not a replacement for the application-level `change` event.

---

## Handle conflicts and recovery

Page: https://docs.ulda.io/sdk/recovery/

A disconnected client, a rejected update and a lost acknowledgement need different responses. Recovery should preserve what is known about each operation.

### Choose the right action

| Situation | Action |
| --- | --- |
| A previously established connection drops | Let bounded automatic reconnect run, or call `reconnect()`. |
| You need a current verified snapshot | Call `refresh()` after competing local operations settle. |
| Another writer wins a state transition | Refresh, inspect the current data, and reconsider the intended change. |
| A sent write loses its acknowledgement | Keep the result uncertain. Do not automatically replay it. |
| A content resource is unusable, but this session has a confirmed local copy | Consider explicit `recoverContent(name)`. |

### Reconnect after a disconnect

By default, the SDK makes up to five bounded attempts after an established connection disconnects unintentionally. Configure the policy when creating the instance:

~~~js
import { Ulda } from 'ulda-auth';

const ulda = new Ulda(endpoint, {
  reconnect: {
    maxAttempts: 5,
    initialDelay: 500,
    maxDelay: 5_000,
    jitter: 0.2,
    budget: 30_000,
  },
});
~~~

Delays and the total reconnect budget are in milliseconds. The first failed `ready` is not retried automatically. Pass `reconnect: false` to disable automatic recovery; explicit `reconnect()` remains available.

Reconnect establishes transport, performs the handshake, and refreshes an open catalog. It retries eligible reads within its bounds, **not writes**. There is no persistent offline write queue.

### Resolve a conflict

Handle `CONFLICT` by waiting for active writes to settle, refreshing the catalog, and comparing the user's intended change with the current data. ULDA does not merge concurrent edits.

`refresh()` may return `BUSY` while local work is active. A successful refresh can still leave `status === 'needsResync'` if an uncertain operation or recovery plan remains unresolved.

### Keep unknown outcomes uncertain

Wrap both the method call and its await: some invalid inputs are rejected synchronously.

~~~js
try {
  await ulda.set('notes', ['text'], proposedText);
} catch (error) {
  if (error.code === 'UNKNOWN_OUTCOME') {
    showUncertainState();
  } else {
    throw error;
  }
}
~~~

`showUncertainState` is your application's UI handler. The server may already have committed the write, or may still be processing it. Even a read that shows the old state does not prove that resending is safe.

Verified reads can help reconcile what is visible, but they do not retroactively turn the rejected promise into a successful write. Closing and reopening is not a mechanism for safely replaying an uncertain operation.

### Recover a content resource explicitly

`recoverContent()` creates a replacement using the session's **last confirmed local copy**, then updates its master entry. It preserves the old server resource.

~~~js
const result = await ulda.recoverContent('notes');
const current = ulda.ref('notes');
~~~

Use this only after deciding that the confirmed local copy is the right recovery source; it may lack newer edits. The SDK requires a verified master, a live connection and no competing local work. An unacknowledged candidate or arbitrary application JSON is not a valid source.

If a new device cannot read the resource, it has no confirmed copy to recover from. A trusted deletion clears that working copy as well.

The result contains `previousId`, the new `id`, `sourceAge` and `reconciled`. Obtain new references after recovery; old references still belong to the original resource identity.

### Continue a retained recovery plan

Recovery reserves an ID before committing the replacement and updated master. If it is interrupted, the same SDK instance may retain that plan and block normal writes.

Repeat the explicit action with the same name on that instance to follow the plan's continuation rules. A lost Create ID must not be guessed or replaced with another automatic Create. After a lost Update acknowledgement, continuation performs verified reads; it reports `reconciled: true` only when the intended replacement is confirmed.

Closing discards the in-memory plan. A new session cannot resume it. See the [complete continuation rules](https://github.com/ulda-io/ulda-auth/blob/main/docs/RECOVERY.md#repeating-the-explicit-action) for each failure stage.

Content recovery does not recover a password, recreate a master, merge changes, or revoke keys already held by another party.

---

## Auth SDK API

Page: https://docs.ulda.io/sdk/api/

Exports: `Ulda`, `SocketIoTransport` and `UldaError`. Both `ulda-auth` and `ulda-auth/browser` expose the same API. The browser entry includes its dependencies.

See the [usage guide](https://docs.ulda.io/sdk/quick-start/) for complete workflows and [pipelines](https://github.com/ulda-io/ulda-auth/blob/66f4ebc/docs/PIPELINES.md) for internal steps.

### Values and identifiers

- Resource IDs are strings matching `[1-9][0-9]{0,39}`: 1–40 decimal digits, no leading zero.
- Content names are nonempty strings of at most 256 UTF-16 code units, unique within a master.
- Modes are `'X'` and `'S'`. The master is always X.
- Data may contain null, booleans, finite numbers, strings, dense arrays and plain objects, including null-prototype objects.
- Undefined, bigint, functions, symbols, Date, custom prototypes, accessors, sparse arrays and cycles are rejected. The validator does not invoke getters or `toJSON`. Special own property names are copied as data.
- A path is an array of strings or safe integer indices, at most 48 components. Array indices must be nonnegative existing indices. Intermediate path components must exist.

Data is defensively copied. Returned object copies may have a null prototype; use `Object.hasOwn` rather than inherited helpers.

### Constructor

~~~js
const ulda = new Ulda(endpoint, options);
~~~

The constructor begins connection establishment. The standard transport accepts HTTPS URLs without credentials, query or fragment. HTTP is allowed only for an explicit loopback development connection.

| Option | Default | Contract |
|---|---|---|
| `timeout` | 10000 | Integer milliseconds, 50–60000; used by the standard transport |
| `allowInsecureLocalhost` | false | Allows HTTP only for localhost, 127.0.0.1 or [::1] |
| `reconnect` | Default settings below | false disables automatic recovery; an object overrides settings |
| `transport` | New SocketIoTransport | An existing transport instance; its endpoint and transport options govern the connection |

Unknown top-level Ulda options are rejected. When injecting a transport, configure its timeout and endpoint when constructing it.

#### Reconnect settings

| Setting | Default | Allowed values |
|---|---|---|
| `maxAttempts` | 5 | Integer 1–20 |
| `initialDelay` | 500 | Integer milliseconds, at least 10 and no greater than maxDelay |
| `maxDelay` | 5000 | Integer milliseconds, at most 60000 |
| `jitter` | 0.2 | Number from 0 to 1 |
| `budget` | 30000 | Integer milliseconds, 50–300000 |

Automatic recovery starts only after a previously established connection disconnects unintentionally. The first failed `ready` is not retried automatically. Explicit reconnect uses the same bounded recovery mechanism, even when automatic recovery is disabled.

### Properties and local reads

| Member | Result |
|---|---|
| `ready` | `Promise<void>` for connection and Hello; updated when explicit reconnect starts |
| `status` | Current status string |
| `data` | Reference tree for the open catalog |
| `contextGeneration` | Internal reference-generation number; not a server revision |
| `snapshot()` | Independent copy of all currently available content JSON, keyed by name |
| `value(name, path = [])` | Independent copy of a content value or nested value |
| `ref(name)` | Reference bound to the current content ID and session |
| `on(event, listener)` | A function that removes that local listener and returns whether it was removed |

These data reads require an open catalog but can read a retained snapshot while disconnected or in `needsResync`. They do not request network data and do not prove freshness.

#### Status values

| Status | Meaning |
|---|---|
| `connecting` | Initial connection/Hello in progress |
| `ready` | Transport is ready, no master is open |
| `open` | A verified catalog is available; operation-specific guards still apply |
| `disconnected` | Transport is unavailable; a previously opened snapshot may remain readable |
| `reconnecting` | A bounded connection recovery cycle is running |
| `needsResync` | An integrity fault or unresolved state prevents normal writes |
| `closed` | Terminal state; use a new instance |

Use operation promises and `status` together. A transport-level ACK does not guarantee that every unrelated resource or pending operation is ready to write.

### Session methods

#### register

`register(password) → Promise<string>`

Create a master and open its prepared local state after a valid Create ACK. Returns the assigned master ID. Password must be a nonempty string of at most 1024 UTF-16 code units. Its contents are not sent to the server.

The method awaits readiness and requires no open or opening master. It performs local key derivation, origin creation and encryption before Create. It does not reread its own successful Create.

If the request outcome is unknown, no successful master ID is returned. The server may hold an orphan resource. There is no ID-guessing or automatic Create retry.

#### connect

`connect(masterId, password) → Promise<DataTree>`

Open an existing master and its referenced content. Requires no existing/opening master. Password rules match `register`.

The master, catalog and required content are verified before the new session is published. Returns the same kind of reference tree as `ulda.data`. Wrong credentials or corrupted ciphertext can both produce `CRYPTO_FAILED`.

A new concurrent integrity fault prevents the older read from opening the context. An explicit new connect performs a new read.

#### refresh

`refresh() → Promise<DataTree>`

Read and verify the open master and its contents, then replace the working snapshot as a set. Does not advance chains or issue writes. Successful reads restore server subscriptions.

Active writes or recovery work can cause `BUSY`. Failure preserves the previous verified snapshot. Success can still leave `status === 'needsResync'` if an uncertain operation is unresolved.

Refresh accepts a newer authenticated snapshot without requiring missing intermediate chain history; see [Security](https://docs.ulda.io/sdk/security/).

#### reconnect

`reconnect() → Promise<DataTree | undefined>`

Reconnect, send Hello, and refresh if a master is open. Returns a data tree for an open catalog or undefined if only transport readiness is restored.

Concurrent calls join the existing cycle. Closing cancels it. Read/connect retries are bounded; writes are never replayed. Invalid data or an unresolved write can stop recovery rather than trigger another attempt.

#### close

`close() → Promise<void>`

Close the instance. Repeated calls join the same close operation. Invalidates references, cancels SDK waits, releases private state, attempts a bounded session-close ACK if connected, then closes the transport.

It does not delete resources or reverse committed writes. Cleanup starts before waiting for the server. A late response cannot reopen the instance.

### Content methods

#### add

`add(name, value, mode = 'X') → Promise<AddResult>`

Creates a content resource and adds one encrypted entry to the master.

~~~text
{ ok: true, name, subscriptionReady }
~~~

The name must not already exist. There is no fixed content-entry count limit in a master. Encrypted catalog bytes, operation bytes and the server's admission budgets still apply; see the [limits and capacity](https://docs.ulda.io/sdk/limits/). Mode is fixed at creation.

The operation reserves an encrypted empty resource, then sends one atomic Update for the content and master. The reservation is a separate commit. `subscriptionReady` is the transport's connection state at completion, not a permanent delivery guarantee.

A retained known reservation may be continued explicitly on the same instance with the same input after conflict handling. A lost Create ID cannot be recovered that way. Normal methods are blocked when the catalog has an unresolved unknown outcome.

#### update

`update(name, value) → Promise<{ ok: true }>`

Replace the entire content JSON. Equivalent to `set(name, [], value)`. Queued per resource; the returned promise belongs to that write's ACK.

#### set

`set(name, path, value) → Promise<{ ok: true }>`

Replace a nested value, or the whole content when path is empty. An object field at the final component may be new; parents must already exist. Arrays support replacement at existing indices.

#### remove

`remove(name, path) → Promise<{ ok: true }>`

Remove an existing field or array element. Array elements are removed using splice semantics. An empty path is rejected; use `delete(name)` for the whole resource.

#### delete

`delete(name) → Promise<{ ok: true }>`

Refresh the catalog and content, then atomically delete the selected content and update the master without its entry. It requires no competing local operation and can return `BUSY`.

Physical deletion affects any other catalog that references the same ID. The server cannot discover those encrypted references. Fresh reads do not lock out other writers; the server's witness check still decides whether the final batch can commit.

#### superDelete

`superDelete() → Promise<{ ok: true }>`

Refresh and validate all referenced resources, delete the listed content and master in one atomic batch, then close the instance.

Invalid or missing required content can stop the operation before deletion. There is no forced deletion of an unreadable master/catalog through this method.

#### recoverContent

`recoverContent(name) → Promise<RecoveryResult>`

Create a replacement from the last confirmed local copy, update one master entry, and preserve the old resource.

~~~text
{ ok: true, previousId, id, sourceAge, reconciled }
~~~

`sourceAge` belongs to the old chain. `reconciled` means this explicit call verified a previously uncertain recovery update; it does not change the result of an earlier rejected promise.

Requires a verified local source, a live connection and no competing local writes. A retained recovery plan blocks normal writes until resolved or closed. See [Recovery](https://docs.ulda.io/sdk/recovery/) for all continuation rules.

### References

`ulda.ref(name)`, `ulda.data[name]` and `ulda.data.field(name)` address the same content.

| Member | Behavior |
|---|---|
| `get()` | Read an independent JSON copy |
| `toJSON()` | Same local read; used by explicit JSON serialization |
| `update(value)` | Update the addressed node |
| `delete()` | Remove the node; at content root, call ulda.delete |
| `field(key)` | Address a child with a dynamic or reserved name |
| `reference.key` / `reference[index]` | Address an ordinary child |

Reserved reference members: get, update, delete, field, toJSON and then. Access data with these names through `field`. At data-tree level, use `ref` or `data.field` for field, toJSON and then. Symbol properties and automatic `then` access are not data references.

Direct assignment, the JavaScript delete operator and property definition are rejected. Reference objects do not behave like their primitive values; use get explicitly.

Old references fail with `STALE_REFERENCE` after their content is replaced/deleted or the session is invalidated. Reusing a name does not reuse its identity.

### Events

~~~js
const unsubscribe = ulda.on('status', status => {
  // Keep the handler synchronous, or handle its own asynchronous failures.
});
~~~

Synchronous listener exceptions are isolated from SDK processing. Returned promises from asynchronous listeners are not awaited; handle their rejections yourself.

| Event | Payload |
|---|---|
| `status` | A status string |
| `change` | `{ kind: 'updated', id }` or `{ kind: 'updated' \| 'deleted' \| 'recovered', ids }` |
| `error` | UldaError with a fixed code/message |
| `trace` | Immutable client summary: time, phase='client', event, step, ids, count |
| `reconnect` | Immutable phase plus applicable attempt, delayMs or code |

Reconnect phases: waiting, attempt, restored, stopped, cancelled. Millisecond timestamps and durations are diagnostic metadata, not chain ages or server revisions.

`change` signals that application data may have changed. It contains no plaintext and is not emitted as a substitute for every method's promise. If a deletion closes the SDK, do not read a snapshot from the closed instance.

### Errors

`UldaError` extends `Error`. Its constructor takes a code string; inspect its `code` property when handling SDK failures.

Use `error.code` rather than matching message text. Some validation occurs before a promise is returned.

| Code | Meaning / response |
|---|---|
| `INVALID_INPUT` | Invalid local value, ID, name, path or option |
| `INVALID_PACKET` | Invalid wire shape or binary representation |
| `UNSUPPORTED_PROFILE` | Unsupported or noncanonical ULDA profile |
| `CRYPTO_FAILED` | Authentication, decryption or resource identity failed |
| `CLOSED` | Instance has been closed |
| `OFFLINE` | Transport is unavailable |
| `NOT_OPEN` | No master is open |
| `UNKNOWN_OUTCOME` | A write may have committed; do not replay automatically |
| `CONFLICT` | Witness/state conflict; refresh and reconsider the intention |
| `NOT_FOUND` | Requested resource, content or path does not exist |
| `EXISTS` | Content name is already present |
| `NEEDS_RESYNC` | Integrity/uncertain state blocks normal writes |
| `ROLLBACK` | Older state or different bytes at the same age rejected |
| `LIMIT` | A size, queue, resource or chain-age limit was exceeded |
| `UNSUPPORTED` | Operation is not supported by the current API/profile |
| `SERVER_ERROR` | Server rejection not mapped to a more specific code |
| `STALE_REFERENCE` | Reference no longer addresses the same live context/resource |
| `BUSY` | An operation conflicts with active local work |
| `TIMEOUT` | Connection/read/recovery deadline exceeded |

For writes already sent, loss of an ACK is reported as UNKNOWN_OUTCOME rather than a definite timeout/rollback. Raw server error strings are not propagated.

### SocketIoTransport

An advanced export for configuring the transport and observing safe transport-level diagnostics:

~~~js
import { Ulda, SocketIoTransport } from 'ulda-auth';

const transport = new SocketIoTransport(endpoint, { timeout: 10_000 });
const stopTrace = transport.on('trace', record => {
  // IDs, sizes, fixed event codes and timings; no plaintext.
});
const ulda = new Ulda(endpoint, { transport });
~~~

Options: timeout, allowInsecureLocalhost and diagnostics (false by default). Enabling diagnostics allows optional `ulda:diagnostic` events from a compatible server. It is not an authentication option.

Members:

- `connected`: boolean.
- `start({ signal }?)`: connect, returning Promise<void>.
- `send(packet, { volatile, signal }?)`: internal SDK-created PacketHandle only.
- `disconnect()`: intentional disconnect.
- `close()`: terminal transport cleanup.
- `on(event, listener)`: remove-listener function.

Transport events are changed, disconnect, error and trace. `changed` contains IDs, ciphertext and public witnesses; it is below the SDK's decryption/chain-verification layer. It is not a verified application snapshot.

PacketHandle has no public constructor. A plain object supplied to send is not a supported way to bypass the SDK's encryption path. This boundary prevents accidental misuse; it does not sandbox hostile code in the same process.

### Limits

See the [limits and capacity](https://docs.ulda.io/sdk/limits/) for numeric limits and the [security policy](https://docs.ulda.io/sdk/security/) for trust and memory boundaries. Type declarations describe the API but do not replace runtime validation.

### Capacity and compatibility

See [Limits and capacity planning](https://docs.ulda.io/sdk/limits/) for SDK bounds, interacting server budgets, eager-loading costs, atomicity, timeouts and upgrades from 1.0.x.

### TypeScript declarations

Both package entry points include types for `Ulda`, references, events, operation results and `SocketIoTransport`. [Read or download the full declarations](https://docs.ulda.io/reference/ulda-auth.d.ts). Runtime validation remains authoritative.

---

Reference for Auth SDK 1.1.0. [Reviewed source](https://github.com/ulda-io/ulda-auth/blob/66f4ebc/docs/API.md).

---

## SDK protocol and encrypted format

Page: https://docs.ulda.io/sdk/protocol/

This page describes the SDK wire reader and encrypted container. For the official server's ID range and emitted ACK shapes, see the [Server wire protocol](https://docs.ulda.io/server/protocol/).

This specification describes the binary Socket.IO contract used by ULDA Auth. It distinguishes server-visible records from the encrypted data interpreted only by clients.

### Connection and delivery

Each SDK instance opens one Socket.IO connection using WebSocket transport. Use HTTPS outside an explicitly allowed local development connection.

Request bodies contain no application requestId, sessionId or watch flag. Socket.IO's callback ACK identifies the request being answered. There are no public content names, passwords, origins or master/content labels in CRUD bodies.

Create, Update and Delete are sent as volatile events. Hello, Read and session.close use ordinary emit on an active connection. Every request has an ACK deadline. Volatile events can be dropped; they do not guarantee atomicity or delivery. Socket.IO write retries and its own reconnect loop are disabled; the SDK owns bounded reconnection.

### Events

| Event | Request body | Successful ACK |
|---|---|---|
| `ulda:hello` | `{}` | `{ ok: true }` |
| `ulda:read` | `["101", "102"]` | `{ ok: true, data: [record, ...] }` |
| `ulda:create` | `[{ witness, payload }, ...]` | `{ ok: true, data: ["101", ...] }` |
| `ulda:update` | `[{ id, witness, payload }, ...]` | `{ ok: true }` |
| `ulda:delete` | Removal entries `{ id, witness }` and optional replacement entries `{ id, witness, payload }` | `{ ok: true }` |
| `ulda:session.close` | `{}` | `{ ok: true }` |

All resource requests use arrays, including a Read of one master. Arrays are nonempty and contain at most 64 entries. Repeated IDs are rejected.

Create returns one unique ID per submitted entry, in request order. Read returns exactly one record per requested ID; response order is not significant. Update/Delete ACK the entire submitted batch and do not repeat the IDs.

Server push:

~~~text
{
  updated: [{ id, witness, payload }],
  deleted: ['103']
}
~~~

The event name is `ulda:changed`. It does not require an ACK. Either group may be omitted, but the event must contain at least one ID and at most 64 total IDs. Duplicate IDs and IDs appearing in both groups are rejected.

### Server-visible record

~~~text
{
  id: '101',
  witness: witnessBytes, // Uint8Array / binary attachment
  payload: payloadBytes // Encrypted container / binary attachment
}
~~~

- `id` is a decimal string, 1–40 digits with no leading zero.
- `witness` is the complete canonical compactV1 core packet: SHA-256, N=5, X/S, 256-bit origin blocks, uint32 age.
- `payload` is an authenticated encrypted SDK container, not application JSON.

Create has no ID because the server assigns it. The server stores no resource role, content name, password or parent reference. The encrypted catalog is a client concern.

The client submits only the candidate witness on mutation. The server compares it against the currently stored witness. It must not accept a client-provided replacement for its current validation anchor.

### Binary representation

The SDK emits Uint8Array fields. Incoming Buffer/Uint8Array and ArrayBuffer values are normalized using their actual view bounds. Strings, numeric arrays, serialized Buffer objects, SharedArrayBuffer and empty or oversized binary fields are rejected.

Socket.IO carries an event header with binary placeholders and separate attachments. Do not JSON.stringify the event body or wrap witness/payload in Base64. This is Socket.IO's binary protocol, not a single custom raw WebSocket frame.

The decoder bounds reconstruction to:

- 128 binary attachments;
- 8,388,608 combined binary bytes;
- 1,100,000 bytes per attachment;
- 65,536 UTF-8 bytes for the text header.

The server must configure compatible parser/transport limits and still validate complete application messages. These checks bound retained protocol input, not all allocations in the host process.

### Error ACK

~~~text
{ ok: false, error: 'conflict', id: '101' }
~~~

`id` is optional. No additional fields are accepted.

| Server error | SDK code |
|---|---|
| conflict | CONFLICT |
| invalid_witness | CONFLICT |
| not_found | NOT_FOUND |
| too_large | LIMIT |
| unsupported_profile | UNSUPPORTED_PROFILE |
| Other error strings, including invalid_request/storage_error | SERVER_ERROR |

Server-provided text and stack traces are not forwarded to the application. A missing or malformed success ACK for a sent write becomes UNKNOWN_OUTCOME: a database commit may already exist.

There is no extra client receipt for Create, durable operation-status endpoint or automatic lookup for a lost generated ID. A Create with a lost ACK may leave an unreachable resource from that client's perspective.

### Encrypted container v2

| Byte offset | Length | Meaning |
|---|---|---|
| 0 | 4 | Magic/version: 55 4c 44 02 |
| 4 | 16 | Resource salt |
| 20 | 12 | AES-GCM nonce |
| 32 | Remaining bytes | Ciphertext followed by the 16-byte authentication tag |

Container overhead is 48 bytes. Every encryption uses a fresh random nonce. The salt belongs to the resource's derived key and is carried with its state.

~~~text
AAD = UTF8("ULDA-SDK/container/v2\0")
      || payload[0:20]
      || completeWitnessBytes
~~~

`\0` is a single zero byte. AES-GCM uses a 256-bit key and a 128-bit tag. The nonce is the AES-GCM IV; the authenticated header is the magic plus salt.

Master keys use PBKDF2-HMAC-SHA-256 with 600000 iterations and the container salt. Content keys use HKDF-SHA-256 with an independent 32-byte secret, that content's salt, and `UTF8("ULDA-SDK/content-key/v2")` as info. AES keys are non-extractable.

The container header does not encode the key-derivation role. The client knows whether it is opening its master or a content entry from its local context.

After decryption only, the plaintext object is:

~~~text
{
  token: Base64URL(32 random bytes),
  origin: Base64URL(complete secret origin packet),
  data: application JSON or encrypted-catalog data
}
~~~

The master's private data has this shape:

~~~text
{
  contents: [
    { id, name, key, token }
  ]
}
~~~

`key` is the Base64URL content secret. Names, keys, tokens and the full origin never become plaintext fields in a socket request. Base64URL here is inside the encrypted JSON; it is not a wire wrapper.

Clients verify AES-GCM, exact private fields, the expected identity token where known, the allowed mode/profile and `sign(origin) === witness`. Container v1 is rejected; automatic migration is not part of this protocol.

No explicit role marker does not imply indistinguishability: lengths, timing, grouped IDs and the public mode still reveal information. In particular, masters are always X.

### Atomic server operations

#### Create

Validate the batch, store each encrypted record and assign IDs. A successful response must represent the committed batch. Subscribe the live initiating connection before ACK.

For content creation, the SDK initially stores encrypted null data. The following content/catalog Update is a separate commit; a crash between them may leave a reserved resource.

#### Update

Validate every candidate against the current stored witness under the same concurrency protection used for commit. Under this profile, X permits a one-step transition; S permits the core's bounded 1–4-step window.

Commit the entire batch or none of it. Replace witness and payload together. A stale candidate fails with conflict rather than overwriting another writer.

The server verifies public witness transitions, not the AES-GCM tag: it has no decryption key.

#### Delete

At least one entry must omit payload; that entry is a physical deletion proof. Entries with payload are updates in the same transaction. All IDs must be unique.

Validate all witnesses first, then atomically apply all deletions and updates. Deleting content plus updating its master therefore uses:

~~~text
[
  { id: contentId, witness: nextContentWitness },
  { id: masterId, witness: nextMasterWitness, payload: nextEncryptedMaster }
]
~~~

The body carries no master role. The server interprets the entries only by the presence of payload. SuperDelete sends deletion proofs for all listed contents and the master in one batch.

### Subscriptions and ordering

Successful Read/Create/Update registers the live connection for the affected IDs, without duplicate subscription entries. Read returns a coherent record snapshot and establishes its subscription before a later update can be missed. Enqueue its response before subsequent Changed notifications for that state.

Commit before ACK and Changed. The initiating socket receives its own ACK and does not receive an echo Changed for that command. Other sockets, including another device for the same user, remain eligible.

Group affected subscribed resources into a Changed event for each recipient. A subscription index keyed by resource ID and a reverse index keyed by connection allow targeted delivery and cleanup; the wire does not expose or require those data structures.

Disconnect/session.close removes subscriptions for that connection. Reconnect uses new reads rather than restoring a persisted socket session or replaying missed Changed events. An operation completing after disconnection must not recreate a subscription for the closed connection.

### Client application of responses

After its own valid ACK, the SDK accepts its already prepared local candidate. It does not reread solely to subscribe or repeat encryption for that acknowledged state.

An incoming update can arrive before a delayed ACK. The SDK may validate a path through its own exact sent candidates. An older ACK must not roll back a newer verified state, and a Changed event does not resolve the promise of a write whose ACK was lost.

The client verifies a complete staged incoming set before publishing known updates/deletions. New content IDs discovered in an authenticated master require subsequent reads and verification. Unknown records cannot be decrypted or exposed without catalog credentials.

Live Changed requires valid lineage. Explicit refresh/reconnect may accept a newer authenticated snapshot without intermediate history. Deleted IDs are trusted server statements. See [Security](https://docs.ulda.io/sdk/security/) for first-open, rollback and deletion assumptions.

### Read splitting and compatibility

Oversized Read requests may be split into smaller arrays. Each reply must still match its request. The SDK publishes the combined snapshot only after all required records pass verification. Separate reads do not provide one global database snapshot across concurrent changes.

Atomic Update/Delete operations are never split into separate commits. Their transport frames may be split using the extension below. The supported limits are in the [limits and capacity](https://docs.ulda.io/sdk/limits/).

No durable operation IDs, offline replay, ACL fields, CRDT merge or password-reset protocol are supplied by this contract. A compatible implementation in another language must preserve the byte format, validation rules, atomicity and failure semantics.

### Large-operation transfer

The transport advertises `auth: { uldaTransfer: 1 }` in the Socket.IO handshake. This is a framing capability, not authentication or a resource role. Ordinary CRUD shapes remain unchanged. A transfer-capable server is required once an operation exceeds those limits; older servers must not be assumed to support large catalogs.

Content reads use arrays of at most 64 IDs, sequentially. For 300 entries this means five requests, not 300 individual reads. An explicit `too_large` response causes the SDK to bisect a read group. A missing or malformed reply is not treated as a successful partial result. There is no global database snapshot across these requests.

All extension commands use the `ulda:transfer` event and one ACK callback:

| Body | Success ACK / meaning |
|---|---|
| `{kind:'begin', action:'create'\|'update'\|'delete', count}` | `{ok:true, token}`; reserve an in-memory operation, no database mutation |
| `{kind:'append', token, items}` | `{ok:true}`; append up to 64 ordinary action entries within 8 MiB |
| `{kind:'part', token, resourceId?, witness, total, offset, payload}` | `{ok:true}`; append at most 1 MiB of one encrypted resource; omit resourceId only for Create |
| `{kind:'commit', token}` | Original action's success ACK, only after the complete atomic commit |
| `{kind:'cancel', token}` | `{ok:true}`; discard an uncommitted upload |
| `{kind:'read', resourceId, offset}` | `{ok:true, data:{id,witness,payload,total,offset}}`; at most 1 MiB from a pinned encrypted snapshot |

Tokens are random 32-character lowercase hexadecimal strings scoped to one connection. They are temporary staging references, not persisted resource metadata or idempotency keys. Each connection permits one staged write and one large read. All offsets and totals are byte counts. Parts for a resource are contiguous, starting at zero, with an unchanged ID, witness and total. No plaintext catalog names, credentials or origins are added.

The server checks staging bytes and deadlines before retaining complete data. On Commit it holds every affected resource key, validates all candidate witnesses against current storage, then makes one atomic storage call. A conflict in the last validation group prevents every mutation. No database transaction is held open while the client uploads frames. A disconnect or deadline discards uncommitted staging; a Commit already accepted may finish after disconnect.

The SDK never replays an uncertain mutation. A lost/malformed Commit ACK yields `UNKNOWN_OUTCOME`, even if storage committed. A pre-Commit timeout cannot itself commit staged bytes. The transport disconnects after an interrupted transfer to clear staging. A definite `too_large` rejection of an ordinary write may select staged framing; it is not a replay of an uncertain write.

A singleton Read rejected as too large selects `kind:'read'`. Offset zero pins one bounded ciphertext snapshot; subsequent offsets continue that same version. The server releases it after the final fragment, expiry or disconnect. The SDK requires a complete matching ID/witness/total, then runs ordinary container and cryptographic validation before exposing anything.

Large notifications use ordered `ulda:changed.part` frames without ACKs: `{kind:'begin',count}`, followed by `{kind:'append',items}` and/or `{kind:'part',resourceId,witness,total,offset,payload}`, then `{kind:'end'}`. Append items are updated `{id,witness,payload}` records or deleted `{id}` entries. The client reconstructs and validates the complete logical event before publishing it. Partial, oversized, out-of-order or timed-out assemblies cannot publish partial data.

Servers use ordinary Changed whenever it fits. A legacy recipient that cannot decode a larger change is disconnected rather than left silently stale. Upgrade the server and every client sharing a catalog before growing it beyond the old limits. Existing encrypted records require no format migration; an older SDK still enforces its old catalog limits.

### Optional diagnostics

A development server may emit `ulda:diagnostic` with `{ action, step, ids, count }`. This is outside CRUD and requires no ACK. The standard transport reads it only when configured with `diagnostics: true`.

Only recognized action/step codes and bounded metadata are accepted. Invalid diagnostic input is ignored. Trace timestamps, request sequence numbers and durations are local diagnostics, not fields added to CRUD or server resource records.

### Capacity and compatibility

See [Limits and capacity planning](https://docs.ulda.io/sdk/limits/) for SDK bounds, interacting server budgets, eager-loading costs, atomicity, timeouts and upgrades from 1.0.x.

---

Reference for Auth SDK 1.1.0. [Reviewed source](https://github.com/ulda-io/ulda-auth/blob/66f4ebc/docs/PROTOCOL.md).

---

## SDK limits and capacity

Page: https://docs.ulda.io/sdk/limits/

This page describes ULDA Auth 1.1.0. A catalog has no fixed content-entry count cap. Operations still have byte, memory, queue and time limits. These limits protect both endpoints; they are not throughput guarantees.

### SDK bounds

| Boundary | Limit |
|---|---|
| Application JSON per content | 1,048,576 UTF-8 bytes; depth 48; 40,000 visited nodes |
| Private catalog JSON | 67,104,768 UTF-8 bytes; depth 48 and a byte-derived node bound |
| Encrypted resource after reassembly | 67,108,864 bytes (64 MiB) |
| One logical transfer | 134,217,728 bytes (128 MiB), including 256 bytes of accounting per item |
| Queued incoming binary data | 128 MiB |
| Ordinary Read / mutation batch / transfer append | At most 64 items |
| Ordinary message, combined witness and payload bytes | 8,388,608 bytes (8 MiB) |
| Binary attachments per packet | 128; each at most 1,100,000 bytes |
| Large-resource fragment | 1,048,576 bytes (1 MiB) |
| Decoder text header | 65,536 UTF-8 bytes |
| Queued incoming handlers | 8 |
| Jobs per relevant outgoing queue | 128 |
| Content name | 1–256 UTF-16 code units |
| Master password input | 1–1024 UTF-16 code units |
| Resource ID accepted by SDK | Positive decimal string of 1–40 digits, without leading zeroes |
| Chain age | Unsigned 32-bit integer; no automatic chain rotation at exhaustion |

Container overhead, witnesses and catalog metadata consume space. The application JSON maximum does not describe the size of its encrypted wire representation. Bounds are checked together: reaching one maximum does not imply that every other maximum remains available.

The standard PostgreSQL server uses positive BIGINT IDs, whose maximum is `9223372036854775807`. The SDK's wider decimal-string validation does not expand that database range.

These SDK bounds are currently implementation constants, not constructor options. Changing a server's limits alone does not change the SDK's bounds. Hello does not negotiate these budgets.

### Reading a large catalog

1. Read the master by ID. Reassemble large ciphertext when necessary, then authenticate and decrypt locally.
2. Extract content IDs and private credentials locally. Names and credentials never appear as plaintext request fields.
3. Read content in groups of at most 64 IDs. Each ordinary request has one ACK containing its result array. Large resources use the transfer extension.
4. If the server explicitly rejects the size, reduce the read group. Temporary server saturation is a different error and does not trigger recursive splitting or automatic request replay.
5. Verify every required result before replacing the visible local context. A bad final batch must not publish the earlier batches as a partially refreshed catalog.

Reads subscribe the current connection to each resource. There is no fixed 63-content cap, no single request containing every ID regardless of size, and no durable server-side download cursor. A failed refresh preserves the last verified local context; the application handles the error and can explicitly refresh again. Separate Read batches are not one database snapshot: concurrent writers can change resources between reads.

Loading is eager. All required content is held locally; there is no lazy-loading or nested-master API. Ten thousand contents of 1 MiB each already represent about 10 GiB of plaintext before temporary copies and encryption buffers. A catalog with many small entries and a catalog with the same number of large contents have very different memory costs.

### Mutations and atomicity

Changing one field re-encrypts the entire content. Adding, deleting or replacing a catalog link also re-encrypts the entire master. Catalog growth therefore increases the work and traffic of catalog mutations.

Large atomic mutations are transmitted as `begin`, `append` / `part`, then `commit`. Staging does not write partial resource changes or hold an open SQL transaction between packets. The server checks the complete operation and commits all resource changes in one transaction. A failed operation must not commit a subset. An expired staging area must be sent again only as a new, explicitly initiated operation; an uncertain commit must not be replayed automatically.

`add()` reserves a new ID in one Create, then commits the new content and master together in a second operation. The entire method is **not** one transaction. Interruption can leave an unreferenced resource; there is no automatic orphan garbage collector. `recoverContent()` similarly replaces one master link and intentionally preserves the old resource.

`delete()` and `superDelete()` first refresh and verify the current required resources. Missing or damaged content can prevent deletion. A large `superDelete()` remains atomic, but must fit the server's total operation, lock and staging budgets. It is not a sequence of independently committed deletion chunks.

X mode permits one pending write per resource. S mode prepares a bounded sequence of local steps: with N=5 the next age must remain less than 5 steps ahead of the confirmed age (at most 4 ahead). S does not merge changes from different clients. Conflicting writes require application handling.

### Server budgets also apply

The following are defaults of the companion single-process ULDA server implementing the transfer extension. Operators can configure server admission limits; confirm the actual deployment configuration. Consult that server's `docs/ADMISSION_LIMITS.md` for the complete list.

| Boundary | Default / consequence |
|---|---|
| Connections | 1,000; configurable admission ceiling, not measured capacity |
| Subscription edges | 100,000 globally; 32,768 per connection |
| Router keys | 32,768 globally; at most 100 waiting jobs per key |
| Incoming copied bytes | 64 MiB globally |
| Active read reservations | 256 MiB globally |
| Outbound bytes | 64 MiB globally; 16 MiB per connection |
| Staging | 256 MiB globally; accounts for `3 × binary bytes + 256 × item count` |
| One resource / logical operation | 64 MiB / 128 MiB |
| Transfer lifetime | 60 seconds absolute by default; not renewed indefinitely by progress |
| Active transfers | One staged upload and one large read per connection |
| Waiting commands | 64 per connection |
| Command rate | 1,000/s with burst 2,000 per decoder; not an authentication or per-IP quota |

Examples of interacting limits:

- A fresh connection needs one subscription for the master and one per content. With the default per-connection ceiling, 32,767 contents is the theoretical maximum for that one connection, before other constraints. This is a **server admission consequence**, not a new SDK catalog-count constant.
- One hundred connections opening 10,000 contents each need over one million subscription edges, exceeding the default global 100,000 budget.
- Deleting all resources requires Router capacity for the whole set. Existing unrelated work can reduce available capacity. Transmission in fragments does not remove this requirement.
- Staging's copy accounting can reject an operation below the logical 128 MiB maximum. Those two maxima are not interchangeable.
- A valid encrypted resource can exceed the per-connection outbound queue during Changed fan-out. The server can disconnect a slow or over-budget recipient; reconnect and a verified read recover its state. Changed is not a durable message log.
- The server reserves read memory before database work. Temporary contention waits in a bounded FIFO (default 5 seconds). Exhaustion returns an error; the SDK does not turn saturation into more and smaller requests. A read that cannot fit at all is a size rejection.

Subscriptions are looked up by resource and connection. The current server uses two Maps containing arrays; membership checks and removal scan the relevant list. This avoids scanning every connected client for every update, but fan-out and large subscriber lists still have a cost.

### Timeouts and recovery

The default SDK timeout is 10 seconds per awaited transport operation, configurable from 50 to 60,000 ms. It is not a deadline for the entire multi-request `connect()` or `superDelete()`. Increasing it does not increase server transfer lifetime, SQL deadlines or memory budgets.

Automatic reconnect defaults to at most five attempts within 30 seconds, with delays from 500 to 5,000 ms and jitter. It rereads and verifies state; it does not replay writes. Initial connection failure is not automatically retried in the same way as a lost established connection.

A lost or invalid write ACK can mean `UNKNOWN_OUTCOME`: the server may have committed. Volatile emission is not a delivery guarantee. A lost Create ACK has no built-in ID-recovery service. Save master IDs through the application and handle unknown outcomes explicitly; see [recovery](https://docs.ulda.io/sdk/recovery/) and [API errors](https://docs.ulda.io/sdk/api/).

Refresh/reconnect may accept a newer authenticated snapshot without proving every missed chain transition. The server's ID-only deletion notifications are trusted. Secrets are private to SDK internals by API design, but this does not protect them against malicious code controlling the same page or process. See [security](https://docs.ulda.io/sdk/security/).

### Compatibility and upgrades

Version 1.1.0 preserves public method signatures, encrypted container format and the core dependency. It adds bounded multi-request reading and the large-operation transfer extension. A server must implement that extension for large resources or operations; upgrading the SDK alone is insufficient.

Version 1.0.1 and earlier enforce the old small-catalog bound and cannot open catalogs that exceed it. Upgrade the server and **all clients** before allowing catalogs to grow beyond old limits. There is no automatic catalog sharding or conversion into nested masters.

Capacity measurements apply to their stated hardware, payloads, concurrency and transport conditions. A successful small-content load test does not establish performance for large catalogs, registration KDFs, shared hot resources, high fan-out or WAN/TLS traffic.

---

Reference for Auth SDK 1.1.0. [Reviewed source](https://github.com/ulda-io/ulda-auth/blob/66f4ebc/docs/LIMITS.md).

---

## SDK security and trust

Page: https://docs.ulda.io/sdk/security/

### Report a vulnerability

Report vulnerabilities privately to [mark@shaposhnik.ch](mailto:mark@shaposhnik.ch). You may also use [GitHub private vulnerability reporting](https://github.com/ulda-io/ulda-auth/security/advisories/new) when it is enabled for the repository.

Include the affected package version, environment, expected and observed behavior, impact, and a minimal reproduction using synthetic data. Do not include live passwords, keys, origins, tokens, decrypted catalogs or customer data. Do not disclose an unpatched vulnerability in a public issue.

This policy covers the client SDK. Report issues in independently maintained dependencies to their maintainers as appropriate.

### Protected data and visible metadata

The SDK encrypts application JSON, the full ULDA origin, resource tokens, and the master's catalog of content names and secrets before sending them to the server. Public snapshots expose copies of application JSON, not the internal catalog or origin.

The server can see resource IDs, public witnesses, the container header, salts, nonces, ciphertext lengths, access timing and which resources are requested or changed together. Records contain no explicit master/content label, but traffic patterns and the public X/S mode can reveal relationships. This is not a traffic-analysis-resistant protocol.

HTTPS protects the connection to the selected server. An ID identifies a resource; it is not a password or authorization credential. The compatible storage model permits reading ciphertext by ID.

### Cryptographic profile

| Component | Profile |
|---|---|
| Resource encryption | AES-256-GCM, random 96-bit nonce per encryption, 128-bit authentication tag |
| Master key | PBKDF2-HMAC-SHA-256, 600,000 iterations, random 16-byte salt |
| Content key | HKDF-SHA-256 from an independent random 32-byte secret and random 16-byte salt |
| AES keys | Non-extractable Web Crypto CryptoKey objects |
| ULDA chain | Core compactV1, SHA-256, N=5, 256-bit origin blocks, X or S mode |
| Container | Version 2; authenticated header and complete witness |

The encryption key and ULDA origin serve different purposes. The witness supports chain validation; holders of the encryption key authenticate the payload. A server without that key cannot verify the AES-GCM tag.

The SDK checks the authenticated container, resource identity token, mode and equality of `sign(origin)` with the received witness. The exact container and additional authenticated data are specified in the [protocol](https://docs.ulda.io/sdk/protocol/).

ULDA intentionally exposes its oldest origin block in a public witness. The full origin is never sent in plaintext; perpetual secrecy of every individual block is not a property of the core.

A party that obtains an encrypted master can attempt offline password guessing. Use a strong password. The fixed KDF cost slows guessing but does not make a weak password safe.

### Trust model

The SDK verifies authenticity and consistency relative to state it has already accepted. It does not prove that the server has supplied the globally latest state.

- **First open:** the client has no independently persisted witness or resource token. It trusts the selected server to return the requested master. The database ID is assigned after encryption and is not cryptographically bound into that initial container. Substitution with another valid master encrypted under the same password is not fully prevented at first open.
- **Live changes:** accepted updates must link to the known chain under the core's transition rules. The client may bridge a delayed ACK using exact candidates it actually prepared and sent.
- **Refresh and reconnect:** an authenticated newer snapshot may be accepted without the missing intermediate chain history. Mode and token must match the known resource; older states and different bytes at the same age are rejected.
- **New session:** there is no durable age anchor across closing the SDK or restarting the application. An old valid encrypted snapshot may therefore be indistinguishable from the latest one.
- **Deletion:** `Changed.deleted` is a trusted server statement, not a cryptographic tombstone. It removes the local resource; deleting the master closes the context.
- **Storage:** the server must validate witnesses and commit each write batch atomically. It must distinguish a definite rejection from an uncertain outcome.

A malicious or compromised server can withhold, corrupt or delete records, misreport deletion, and deny service. It can also hide a newer valid version. Client encryption does not provide server availability, access control or an audit log.

Anyone given the master password and ID can obtain the catalog and its content secrets. The SDK does not implement per-user roles, key revocation, password rotation or recovery of forgotten master credentials.

### Invalid input and integrity failures

Incoming records are checked for exact fields, canonical IDs, binary types, size limits and the supported canonical core profile. Binary views preserve their bounds; payloads are copied at protocol boundaries.

JSON validation does not invoke getters or custom `toJSON` methods. Cycles, accessors, non-JSON values and custom prototypes are rejected. Own properties such as `__proto__` are handled as data rather than prototype mutations.

A mixed incoming event is verified before publishing its known resource updates or applying deletions. A failed verification preserves the last verified snapshot and blocks further writes. A read started before a new integrity fault cannot clear that fault.

The retained snapshot may be stale. A successful explicit refresh can restore normal operation only when pending uncertain operations permit it. The SDK does not repair, delete or recreate a resource automatically in response to a bad packet.

### Writes and delivery

Create, Update and Delete use volatile Socket.IO events with an ACK deadline. Volatile delivery is not a transaction guarantee. The database commit and acknowledgement delivery are separate steps.

A missing or malformed success ACK after a write results in `UNKNOWN_OUTCOME`. Never treat that error as proof of rollback or automatically retry the write. A lost Create ACK can leave a resource whose ID the client never learned.

Creating or recovering content reserves a new resource before the separate atomic content/catalog update. Unreferenced encrypted records may remain after failure. [Content recovery](https://docs.ulda.io/sdk/recovery/) preserves the old resource because other catalogs may still reference it.

### Client memory and application responsibilities

Private fields and non-extractable keys are API boundaries, not isolation from code controlling the page or process. Plaintext and origin bytes must be available to JavaScript while the SDK is using them. XSS, malicious dependencies, debugging access and a compromised operating system can defeat client-side secrecy.

The SDK does not persist secrets in localStorage, IndexedDB or files. It wipes owned temporary byte buffers where possible and releases private state during `close()`. JavaScript strings, garbage collection, native crypto operations and copies held by the application prevent a guarantee of complete memory erasure.

The application is responsible for:

- its trusted endpoint, HTTPS deployment and dependency supply chain;
- password input, password quality, UI locking and clearing its own references;
- XSS prevention and control of scripts running in the same context;
- storing and presenting master IDs;
- its own copies of decrypted data and any telemetry or crash reports;
- authorization and abuse controls required beyond the SDK's encrypted storage model.

See [Secret lifecycle](https://github.com/ulda-io/ulda-auth/blob/66f4ebc/docs/SECRET_LIFECYCLE.md) for allocation and cleanup behavior.

### Resource and diagnostic limits

The SDK bounds JSON sizes, queues, batches and binary reconstruction. See the [limits and capacity](https://docs.ulda.io/sdk/limits/) for exact limits. Parser checks do not impose a total memory limit on the browser or Node process: the runtime may have allocated a frame before validation.

Large-operation frames contain ciphertext and public witnesses only. Framing does not change the encrypted container or its authentication. A compatible server must bound staged bytes, expire abandoned uploads, validate every candidate before one atomic commit, and keep an accepted commit's reservations until completion. The client does not expose a partial assembled Changed event or a partially checked catalog. Temporary transfer tokens do not add replay protection or resolve a lost Commit ACK. Larger catalogs increase client memory and processing costs despite bounded network frames.

SDK diagnostics contain fixed event codes, IDs, counts, sizes and timings. They omit plaintext, content names, credentials, full origins and raw packets. Server error strings are mapped to fixed local errors. IDs and access patterns can still be sensitive metadata; protect application logs accordingly.

The test reporter and local secret scanner reduce accidental disclosure during development. They do not sandbox arbitrary code or prove the absence of all secrets.

---

Reference for Auth SDK 1.1.0. [Reviewed source](https://github.com/ulda-io/ulda-auth/blob/66f4ebc/SECURITY.md).

---

## Complete TypeScript declarations

```typescript
/** ULDA Auth client SDK. JSON only; runtime validation is authoritative. */
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
export type ResourceId = string;
export type Mode = 'X' | 'S';
export type Status = 'connecting' | 'ready' | 'open' | 'disconnected' | 'reconnecting' | 'needsResync' | 'closed';
export type ErrorCode = 'INVALID_INPUT' | 'INVALID_PACKET' | 'UNSUPPORTED_PROFILE' | 'CRYPTO_FAILED' |
  'CLOSED' | 'OFFLINE' | 'NOT_OPEN' | 'UNKNOWN_OUTCOME' | 'CONFLICT' | 'NOT_FOUND' | 'EXISTS' |
  'NEEDS_RESYNC' | 'ROLLBACK' | 'LIMIT' | 'UNSUPPORTED' | 'SERVER_ERROR' | 'STALE_REFERENCE' | 'BUSY' | 'TIMEOUT';
export class UldaError extends Error { constructor(code: string); readonly code: ErrorCode; }
export interface Ok { ok: true }
export interface AddResult extends Ok { name: string; subscriptionReady: boolean }
export interface RecoveryResult extends Ok { previousId: ResourceId; id: ResourceId; sourceAge: number; reconciled: boolean }
export interface ReconnectOptions { maxAttempts?: number; initialDelay?: number; maxDelay?: number; jitter?: number; budget?: number }
export interface TransportOptions { timeout?: number; allowInsecureLocalhost?: boolean; diagnostics?: boolean }
export interface UldaOptions extends Omit<TransportOptions, 'diagnostics'> { reconnect?: false | ReconnectOptions; transport?: SocketIoTransport }
export interface ClientTrace { readonly time: number; readonly phase: 'client'; readonly event: string; readonly step: string; readonly ids: readonly ResourceId[]; readonly count: number }
export type ChangeEvent = { kind: 'updated'; id: ResourceId } | { kind: 'updated' | 'deleted' | 'recovered'; ids: ResourceId[] };
export interface ReconnectEvent { readonly phase: 'waiting' | 'attempt' | 'restored' | 'stopped' | 'cancelled'; readonly attempt?: number; readonly delayMs?: number; readonly code?: ErrorCode }
export interface UldaEvents { change: ChangeEvent; status: Status; error: UldaError; trace: ClientTrace; reconnect: ReconnectEvent }
type Reserved = 'get' | 'update' | 'delete' | 'field' | 'toJSON' | 'then';
export type Reference<T extends JsonValue = JsonValue> = {
  get(): T;
  toJSON(): T;
  update(value: T): Promise<Ok>;
  delete(): Promise<Ok>;
  field(key: string | number): Reference;
} & (JsonValue extends T ? { readonly [key: string]: Reference } :
  T extends JsonValue[] ? { readonly [index: number]: Reference<T[number]> } :
  T extends object ? { readonly [K in Exclude<keyof T, Reserved>]: T[K] extends JsonValue ? Reference<T[K]> : never } : {});
export type DataTree<T extends Record<string, JsonValue> = Record<string, JsonValue>> = {
  readonly [K in Exclude<keyof T, 'field' | 'toJSON' | 'then'>]: Reference<T[K]>
} & { field<K extends keyof T & string>(name: K): Reference<T[K]>; toJSON(): T };
export class Ulda<T extends Record<string, JsonValue> = Record<string, JsonValue>> {
  constructor(endpoint: string, options?: UldaOptions);
  readonly ready: Promise<void>;
  readonly status: Status;
  /** Used by SDK references; applications should not use this as a server revision. */
  readonly contextGeneration: number;
  readonly data: DataTree<T>;
  on<K extends keyof UldaEvents>(event: K, listener: (value: UldaEvents[K]) => void): () => boolean;
  register(password: string): Promise<ResourceId>;
  connect(id: ResourceId, password: string): Promise<DataTree<T>>;
  add<K extends keyof T & string>(name: K, value: T[K], mode?: Mode): Promise<AddResult>;
  update<K extends keyof T & string>(name: K, value: T[K]): Promise<Ok>;
  set(name: keyof T & string, path: (string | number)[], value: JsonValue): Promise<Ok>;
  remove(name: keyof T & string, path: (string | number)[]): Promise<Ok>;
  delete(name: keyof T & string): Promise<Ok>;
  superDelete(): Promise<Ok>;
  recoverContent(name: keyof T & string): Promise<RecoveryResult>;
  snapshot(): T;
  value<K extends keyof T & string>(name: K): T[K];
  value(name: keyof T & string, path: (string | number)[]): JsonValue;
  ref<K extends keyof T & string>(name: K): Reference<T[K]>;
  refresh(): Promise<DataTree<T>>;
  reconnect(): Promise<DataTree<T> | undefined>;
  close(): Promise<void>;
}
/** Ciphertext-only records at the transport boundary. */
export interface EncryptedResource { readonly id: ResourceId; readonly witness: Uint8Array; readonly payload: Uint8Array }
export interface ChangedEvent { readonly updated: readonly EncryptedResource[]; readonly deleted: readonly ResourceId[] }
export interface ResourceSummary { readonly id?: ResourceId; readonly witnessBytes: number; readonly payloadBytes?: number }
export interface TransportTrace {
  readonly time: number; readonly phase: 'send' | 'ack' | 'failure' | 'push' | 'connection' | 'server';
  readonly event: string; readonly request?: number; readonly durationMs?: number; readonly code?: ErrorCode;
  readonly ok?: true; readonly volatile?: boolean; readonly ids?: readonly ResourceId[];
  readonly items?: readonly ResourceSummary[]; readonly updated?: readonly ResourceSummary[];
  readonly deleted?: readonly ResourceId[]; readonly step?: string; readonly count?: number;
}
export interface TransportEvents { changed: ChangedEvent; disconnect: { intentional: boolean }; error: UldaError; trace: TransportTrace }
declare const packetBrand: unique symbol;
/** SDK-created handle, not a user-supplied wire object; no public packet constructor. */
export interface PacketHandle { readonly [packetBrand]: true }
export class SocketIoTransport {
  constructor(endpoint: string, options?: TransportOptions);
  readonly connected: boolean;
  on<K extends keyof TransportEvents>(event: K, listener: (value: TransportEvents[K]) => void): () => boolean;
  start(options?: { signal?: AbortSignal }): Promise<void>;
  send(packet: PacketHandle, options?: { volatile?: boolean; signal?: AbortSignal }): Promise<unknown>;
  disconnect(): void;
  close(): void;
}
```
