npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xcitedbs/client

v0.6.12

Published

XCiteDB BaaS client SDK

Readme

@xcitedbs/client

TypeScript/JavaScript client for the XCiteDB BaaS HTTP API and WebSocket notifications.

Install

From npm:

npm install @xcitedbs/client

From this repo:

cd sdks/javascript && npm install && npm run build

Link or npm pack / publish as needed.

Reference

  • SDK permissions matrix — which credential class (sk_live, sk_test, pk_*, app-user JWT, platform admin) is accepted by each SDK method. Answers most "why am I getting 403?" questions before you read code.
  • Deploying functionsjs (buildFunction, JIT-versioned) vs component (deployFunction, client-side build) deploy paths and their version-tracking contract.
  • Writing functions — the xcite.* host API surface, including xcite.api loopback scope and binary-body channels.

Debugging

  • new XCiteDBClient({ baseUrl, apiKey, debug: true }) traces every request and response to console.debug with auth headers and password/secret/token fields redacted. Pass a function for custom routing (Sentry, OTLP, etc.). Also enabled by XCITEDB_DEBUG=1 env var. Toggle at runtime with client.setDebug(true).
  • Errors thrown by the SDK are XCiteDBError (or one of the typed subclasses); use isXCiteDBError(e) from @xcitedbs/client in catch blocks to type-narrow without an instanceof quirk across duplicate bundles.
  • xcitedb bundle plan|ensure|apply|status (the CLI installed by this package) prints the server's view of your ./xcitedb control-plane bundle without writing — see xcitedb -h.

Usage

import { XCiteDBClient } from '@xcitedbs/client';

const client = new XCiteDBClient({
  baseUrl: 'http://localhost:8080',
  apiKey: process.env.XCITEDB_API_KEY,
  context: { branch: '', date: '' },
});

await client.health();
const docs = await client.queryByIdentifier('/test1', 'FirstMatch');
await client.put('app.settings', { theme: 'dark' });

Smart diff (smartDiff)

client.smartDiff(from, to, target, metadata?, options?) compares two XML provision trees and writes the result as a single annotated diff document to target. Useful when you need both sides of a structural change rendered together (rather than a list of changed identifiers as compare returns).

Refs. Each of from / to / target is a SmartDiffRef: an identifier plus either a checkpoint_id/commit_id, or a branch + optional date/date_key. branch: "" (or "main") is the root timeline. Omitting date/date_key reads the branch's live tip.

How matching works. A "provision candidate" is an element carrying identifier, db:identifier, or db:xcitepath. Three passes:

  1. Cross-tree UUID anchors via db:doc_id. Candidates whose nearest UUID-matched ancestor differs across A and B are tagged MovedParent.
  2. Inside each matched parent pair, direct candidate children are aligned by LCS — by UUID when present, else by tag + first 30 chars of plain text. LCS hits → Matched. UUID-matched pairs that broke order → MovedOrder. Remaining → Inserted (B-only) or Deleted (A-only).
  3. At each leaf-matched pair, diff-match-patch runs over the concatenated plain text. Inline markup from B is preserved; <ins> wrappers are split at every tag boundary so they never cross inline markup.

Output schema. The result document's root copies B's root tag and attrs, plus diff:document="true". Inside:

| Outcome | Emission | |---|---| | Matched | Copy of B's tag, recursing into children | | Inserted (B-only) | <ins> wrapping a clone of the B subtree | | Deleted (A-only) | <del> wrapping a clone of A; diff:src-xcitepath, diff:src-identifier | | MovedParent (B side) | <move-to diff:src-xcitepath=… diff:dst-xcitepath=…> containing the matched body | | MovedParent (A side) | <move-from diff:src-xcitepath=… diff:dst-xcitepath=… diff:src-identifier=…/> stub | | MovedOrder | <move-to …> at B's position (no <move-from> — parent didn't change) | | Text change in leaf | <ins> / <del> around plain text, with B's inline markup preserved |

Every <ins>, <del>, <move-to>, <move-from> element carries every caller-supplied metadata entry as diff:<key>="<value>" attributes — handy for tagging diffs by author, comment id, review round, etc.

await client.smartDiff(
  { identifier: '/book/ch1', branch: 'main', date: '2024-01-01' },
  { identifier: '/book/ch1', branch: 'main' }, // live tip
  { identifier: '/diffs/book/ch1@2024-01-01', branch: 'main' },
  { author: 'alice', review_round: '3' },
  { diffText: true, excludeTags: ['db:revisionMeta'], maxTextDiffBytes: 1 << 20 }
);

Tiny worked example (A → B → diff):

<!-- A -->
<chapter db:identifier="/book/ch1"><para>Old line.</para></chapter>

<!-- B -->
<chapter db:identifier="/book/ch1"><para>New line.</para></chapter>

<!-- diff output -->
<chapter db:identifier="/book/ch1" diff:document="true">
  <para>
    <del diff:author="alice">Old</del><ins diff:author="alice">New</ins> line.
  </para>
</chapter>

stats. The 201 body includes per-category counters over provision candidates only (textual <ins>/<del> inside leaves are not counted):

| Field | Means | |---|---| | matched | Matched pairs at each tree level (cumulative across recursion) | | ins | B-only candidates emitted as <ins> (+1 for pure-insert case where A is empty) | | del | A-only candidates emitted as <del> (+1 for pure-delete case where B is empty) | | moved_parent | Candidates that moved under a different UUID-matched ancestor; counted once on the B side | | moved_order | Candidates that stayed under the same parent but broke sibling order |

Options.

| Option | Default | Effect | |---|---|---| | diffText | true | When false, every leaf matched pair degrades to one <del> + one <ins> block (no per-character diff) | | excludeTags | [] | Element local-name strings to skip when collecting candidates. Matched by exact node.name() (namespace-prefix-sensitive) | | maxTextDiffBytes | 1 MiB | Per-leaf-pair byte threshold; oversize leaves degrade to whole-block <del> + <ins> | | maxSourceBytes | 8 MiB | Combined approx-size cap on A + B; over → 413 smart_diff_sources_too_large with limit_bytes / from_bytes / to_bytes |

Target lifecycle. The result is a regular XCiteDB XML document: shredded, ABAC-gated per descendant id, FTS-indexed where enabled. Read it back with queryByIdentifier(target.identifier). It does not auto-expire — delete with deleteDocument(). Re-running smartDiff on the same target silently overwrites if the existing document is itself a smart-diff (root has diff:document="true"); otherwise 409 target_not_smart_diff. Recognize a smart-diff document by checking that attribute.

ABAC interaction. Subtrees the requester is denied (PolicyAction::Read) are silently redacted from both sources before the diff runs — so denied content never echoes back via <del> or <ins>. Writes to the target are then ABAC-gated again per descendant identifier in the diff payload; one denial fails the whole call.

Errors: 400 (bad ref / unparseable date), 403 (read on source, write on target, reserved namespace), 404 no_source_content (both sources empty), 409 target_not_smart_diff, 413 smart_diff_sources_too_large, 423 lock conflict on target write, 503 admission/queue full.

Full-text search (temporal)

client.search() accepts TextSearchQuery with optional at_date (point-in-time), date_from / date_to (range overlap), and mode: 'fts' for pure keyword search. Omit temporal fields for the default “current” posting view. Hits may include valid_from / valid_to (7-character internal date keys returned by the server).

await client.search({
  query: 'installation guide',
  mode: 'fts',
  at_date: '2024-06-01',
  limit: 20,
});

WebSocket

client.subscribe(
  { pattern: '/us/bills/*', event_type: '*' },
  (ev) => console.log(ev)
);

With JWT in browsers, tokens are passed as access_token query parameter on the WebSocket URL. API keys can use api_key query parameter.

Test sessions (ephemeral and overlay)

Call POST /api/v1/test/sessions with your normal API key or Bearer (same project context as usual). Use XCiteDBClient.createTestSession({ baseUrl, apiKey, … }) to get a client that sends X-Test-Session on requests.

  • Default: isolated empty LMDB under the server’s _test/<uuid>/ (writes never touch production).
  • Overlay: pass overlay: true in createTestSession options (or POST body {"overlay":true}). The server layers a writable LMDB on top of the current project’s on-disk data opened read-only so you can debug against real data; changes still live only under _test/<uuid>/. Use project-scoped credentials or platform Bearer + X-Project-Id as when calling production APIs.
  • Live-key safety check. createTestSession refuses by default when apiKey looks like a live credential (sk_live_… / pk_live_…). Test sessions ARE isolated server-side, but a buggy test that forgets X-Test-Session would write to production with a live key — test keys have a server-side gate that catches that mistake; live keys do not. Override with allowLiveKey: true on the call or XCITEDB_ALLOW_LIVE_KEY_IN_TESTS=1 in the env when you genuinely want live-key wet tests.

Tear down with destroyTestSession() (or DELETE /api/v1/test/sessions/current with the session header). With the same API key / Bearer and no X-Test-Session, use listTestSessions(), destroyAllTestSessions(), and destroyTestSessionByToken(token) to clear leaked sessions before large suites (avoids the default per-credential concurrent cap). See llms.txt / llms-full.txt in this package for full behavior, limits, and auth notes.

Deploying functions

Functions are provisioned by a server-side, idempotent script using an admin/editor key (never a browser bundle) — the same pass as your ABAC/trigger setup.

  • JS/TS — deploy the manifest, then compile source server-side (no toolchain or wasm artifact needed):

    await client.deployFunction('greet', { runtime: 'js', identity_mode: 'as_caller' });
    await client.buildFunction('greet', { source: tsSource });   // compiles + activates
  • Rust / compiledruntime: 'component', precompiled off-platform to wasm32-wasip2, bytes shipped inline:

    await client.deployFunction('settle', {
      runtime: 'component', identity_mode: 'as_role', role: 'billing',
      wasm_b64: readFileSync('wasm/settle.wasm').toString('base64'),
    });

buildFunction / activateFunctionVersion / listFunctionVersions cover the JS build lifecycle; setFunctionSecrets sets declared secret values. For the provisioning-script pattern, the JS-vs-component trade-off, platform invariants, and a worked Vercel example (adaptable to Netlify/Fly/Docker), see docs/DEPLOY_FUNCTIONS.md.

Control plane (the deploy is the migration)

Check a desired-state bundle in next to your app and reconcile it on every boot with one idempotent call — no Web UI or human CLI in the loop:

await client.controlPlane.ensure('./xcitedb');   // ~1 read when converged

Bundle layout (<root>/, human/AI-edited, always the truth):

xcitedb/
  xcitedb.toml                 # scope + [auto_apply] profile (safe), prune=false
  control-plane/               # full desired state
    isolation.yaml             security.yaml
    policies/<id>.yaml         triggers/<id>.yaml
    settings/<area>.json       # cors|rate_limits|search|doc_conf|asset_storage|email_provider|backup|…
    functions/<name>/manifest.yaml  (+ dist/<name>.wasm, src/)
    shared-secrets.json        # SDK-only deploy-env secret refs (see below)
  migrations/0003_*.xmig       # ordered imperative bridges (append-only)
  xcitedb.lock                 # GENERATED — mirrors /_xcitedb/migrations
  • ensure(root, opts?) — read bundle → POST /api/v1/admin/migrate. Idempotent; converged ⇒ one-read 200 noop; concurrent boots collapse onto a single reconcile via a coordinator advisory lock.
  • plan(root, opts?) — dry-run: classify + explain, writes nothing (/api/v1/admin/migrate/plan).
  • apply(root, { planHash?, allowPrune?, forceUnsafe?, scope? }) — explicit (non-fast-path) apply with drift-revert. Safe profile auto- applies additive/idempotent (GREEN) and drift-revert (YELLOW) changes; destructive ones (RED, e.g. prune) are blocked-but-surfaced and require explicit allowPrune/forceUnsafe (or xcitedb.toml [auto_apply]).
  • Hash contract: the SDK computes the same canonical hash the server does and sends it as control_plane_hash; a mismatch is refused with hash_mismatch (don't put non-integral floats in the bundle — out of contract). Auth: the client's key gates entry only; reconcile writes execute server-side as type=system. Use an admin/editor key (server-side script, never a browser bundle). Full design: docs/CONTROL_PLANE_MIGRATIONS_PLAN.md.

secrets[] in a manifest.yaml / settings area declares secret names only — values never enter the repo. The reconciler asserts presence and fails the plan naming any missing key; supply values out-of-band (the shared-secret surface below, the in-app settings forms, or deploy-env:).

Shared secrets (SMTP / S3 / LLM API keys, …)

Unified tenant secrets. Every platform credential (SMTP password, S3 backup/mirror keys, embedding/LLM API keys, per-target asset-storage keys) lives in one tenant-wide store encrypted under a zero-conf per-tenant keyring. Values are write-only — the server never returns them. Manage them by canonical name:

await client.listSharedSecrets();                       // inventory: {name,label,area,set}, never values
await client.setSharedSecret('search.llm.api_key', v);  // write-only (also: rotate = re-set)
await client.deleteSharedSecret('email.smtp.password'); // clear (also clears any legacy copy)
await client.getSecretsKeyring();                       // {provisioned, active_backend, slots[]}
await client.addSecretsKeyringPassphrase(pass);         // optional off-disk operator unlock slot

Canonical shared-secret names you'll write by here:

| Name | Used for | | --- | --- | | email.smtp.password | Outbound email — SMTP auth (legacy path; plugin email-send is the canonical replacement) | | email.api.key | Outbound email — provider HTTPS API (legacy path; plugin email-send is canonical) | | email.inbound.secret | Optional defense-in-depth on top of the inbound URL token | | backup.s3.secret_access_key | Project backup BYO-S3 destination credentials | | mirror.s3.secret_access_key | Mirror-sync S3 source credentials | | asset.storage.<target>.secret_access_key | Per-target asset-storage S3 credentials (one per named target) | | search.embedding.api_key | Vector embedding provider | | search.llm.api_key | LLM completion provider |

Admin app-user or an API key bearing the least-privilege secrets:write capability is authorized (the cap is not admin elevation). Mint a scoped key: POST /api/v1/project/keys with { "caps": ["secrets:write"] }.

Deploy-env zero-conf sync

Feed secrets from your app's deployment environment (Vercel project env / host env / a gitignored local file) into XciteDB on every pnpm dev / deploy, through the one ensure() call:

import { XCiteDBClient, resolveDeployCredential } from '@xcitedbs/client';
const client = new XCiteDBClient({
  baseUrl: process.env.XCITEDB_URL!,
  ...resolveDeployCredential(),     // XCITEDB_API_KEY / .xcitedb/credential today; OIDC = Phase 2, no code change
});
await client.controlPlane.ensure('./xcitedb');   // migrate + sync deploy-env secrets

Declare the mapping in <bundle>/control-plane/shared-secrets.json (SDK-only; never sent to the server — only resolved values are pushed, write-only):

[{ "name": "search.llm.api_key", "from": "deploy-env:OPENAI_API_KEY" }]

ensure() resolves each deploy-env:VAR from process.env (or a gitignored .xcitedb.secrets for local dev), pushes only changed values (idempotent via a gitignored salted-HMAC .secrets-sync.lock), never logs values, and treats missing vars as non-fatal name-only warnings. skipSecretSync: true does migrate only. Full bootstrap + Vercel wiring: docs/DEPLOY_SECRETS_ZERO_CONF.md; the no-stored-secret roadmap: docs/security/architecture/oidc-workload-federation.md.

Gotchas and conventions

These trip up most first-time users; they're listed once here so you don't hit them twice.

Search (search) tokenizes on hyphens

The full-text index treats - as a token boundary, so client.search({ query: 'Test - Article' }) won't match a document titled exactly "Test - Article" — it tokenizes to Test, Article and ranks any doc containing both. Use search only for relevance queries.

For exact-title or exact-meta lookups, use unquery with a meta-equality filter:

client.unquery(
  { match_start: '/users/<u>/docs/' },
  [{ title: 'title', match: '$equals(title, $string("Test - Article"))' }]
);

list* methods return wrapper objects, not bare arrays

For consistency, every list* family method returns an object with the data nested under a named key:

| Method | Wrapper shape | |---|---| | listIdentifiers | { identifiers: string[], total, offset, limit } | | listCheckpoints | { checkpoints: CheckpointRecord[], total, branch } | | listUserWorkspaces | { user_workspaces: [...] } | | listBookmarks | { bookmarks: [...], total } |

Don't for…of the result directly — use for (const x of ids.identifiers), not for (const x of ids).

Reading meta — getMeta and queryMeta

The read sibling of addMeta / appendMeta / clearMeta is getMeta (alias of queryMeta). With an empty path it returns the whole top-level meta object, which by convention is keyed by tool/flow:

const meta = await client.getMeta(docId);            // { lint: {...}, summary: {...}, ... }
const lint = await client.getMeta(docId, 'lint');    // just the lint sub-object

For an array-typed or keyed-object meta path (e.g. an items list, or a per-tool dictionary), pair with appendItem to push and removeItem to atomically delete by key — removeItem matches against id / item_id (or by string equality for primitive arrays) and writes back inside one server-side write txn, avoiding the read/modify/write race that read-then-set on the client would create.

Overlay test sessions — visibility matrix

createTestSession({ overlay: true }) lets you read production data and write into an isolated overlay. The visibility model is not "read-through everywhere" — these are the layers that participate in the overlay→base fallback:

| API | Sees prod data? | |---|---| | Document and meta reads (anywhere in the doc store) | yes | | Branch table existence checks (e.g. _uw/<owner>/<slug>) | yes | | Per-branch identifier scans on base-only branches (listIdentifiers, compare) | yes | | User-workspace records (listUserWorkspaces, getUserWorkspace) | yes | | Branch tip / commit lookups (listCheckpoints, compare against checkpoints) | yes | | publishUserWorkspace writes | sandboxed in overlay — does not touch prod | | createCheckpoint, addMeta, addDocument, etc. | sandboxed in overlay |

Anything you write inside an overlay session lives under _test/<uuid>/ and is dropped when the session is destroyed. To inspect a real production conflict mutation-free, call the production admin client directly with { autoResolve: 'none' }.

compare() ref shapes

{ branch } without date or checkpoint_id resolves to the branch's live tip, not its base. On a branch with zero checkpoints the server returns 400 branch_has_no_commits rather than producing an ambiguous diff — pass an explicit date or checkpoint_id instead.

compare() accepts both matchStart (camelCase, our convention) and match_start (snake_case, matching XCiteQuery) on the options object; they're equivalent.

withContext / fork

fork(partial) returns a child client with the merged context (workspace, date, prefix). withContext(partial) is a one-line alias for the same operation.

Context management & per-call overrides

The client holds a mutable default context (setContext({ workspace, date, prefix, … })) used to populate X-Workspace / X-Date / X-Prefix on every request. For one-off background work you can flip it and flip it back, but for any flow where two operations might overlap in time the mutable default is a footgun: a fire-and-forget callback that calls setContext mid-flight can land an in-flight request on a different workspace than you intended.

Three patterns, in order of preference:

  1. Per-call opts.context — every data-plane method accepts an opts: { context?: Partial<DatabaseContext> } argument that wins over defaultContext for that one HTTP request. Examples:

    await client.writeXmlDocumentsBatch(items, { context: { workspace: draft.branch } });
    await client.queryByIdentifier(id, undefined, undefined, undefined, { context: { workspace: 'main' } });
    await client.txn(req, { context: { workspace: 'main' } });
    await client.acquireLock(id, { ttlSeconds: 60, context: { workspace: draftBranch } });

    This is the right shape for batches, locks, and anything inside a publish/onPublished flow where a concurrent UI handler might switch the active workspace.

  2. client.fork(partial) / withContext(partial) — returns a child client with its own context, sharing transport and auth state. Use this when you have a multi-call sequence scoped to one workspace:

    const draft = client.withContext({ workspace: draftBranch });
    const [doc, meta] = await Promise.all([draft.queryByIdentifier(id), draft.getMeta(id)]);
  3. client.setContext() — only for the session default (e.g. on login, when switching projects). Avoid it inside request flows; the child-client pattern above is race-free.

If you're debugging a "wrong workspace" symptom, the server echoes the resolved context on every response:

X-XDB-Workspace: <normalized branch; empty string = root>
X-XDB-Date: <X-Date if set>
X-XDB-Prefix: <X-Prefix if set>
X-XDB-Unversioned: true (only when set)

Compare the echo to the value you expected — a mismatch confirms the request landed on a different workspace than you thought, which is almost always a defaultContext race that the per-call override would fix.

Forks are metadata-only

createUserWorkspace(name, { sourceBranch: 'main' }) (and getUserWorkspace) does not copy documents into the new branch. The server writes a branch record pointing at the fork date, and subsequent reads on the new branch walk back to the parent (LMDB MVCC). So:

  • A freshly-forked workspace has byte-identical reads to the parent at the fork date — including (and especially) any attributes the parent has or lacks.
  • If your editor mints structural ids on mount when nodes lack them, those mints will produce a real diff against the parent on the very first save. The chip flipping to "unpublished changes" before the user types is correct behavior, not a bug.
  • If you want forks to mount diff-clean, normalize once on the parent before forking (read each document, mint, publish back to main). After that, every fork inherits the populated ids.

There's no flag to make createUserWorkspace materialize documents into the child branch — the metadata-only model is what gives forks their O(1) cost and unbounded count.

_xcite_json_doc sentinel

JSON documents written via writeJsonDocument no longer leak the internal _xcite_json_doc: true marker into the read payload — the server strips it before serializing. Older clients that filtered it out can drop that workaround. Likewise, deleteDocument now correctly removes JSON documents (it routes JSON ids through deleteJSON automatically); you don't need to pick deleteJsonDocument based on the document's storage shape.

testAuth: 'bypass' requires the creator credential

When you create a test session, the issuing API key/Bearer is the creator credential. Calling endpoints under X-Test-Auth: bypass requires presenting that same credential alongside X-Test-Session; it isn't only a login restriction. If you can't present the creator credential (e.g. you're operating from a different identity), switch to X-Test-Auth: required and the session itself authenticates the request.

BFF/SPA-side concerns (tracked separately)

The following are app/BFF-repo concerns rather than SDK or server bugs: getOrCreateDraft check-then-act races (TOCTOU on per-(user, key) resource minting); coexisting legacy and modern last-drafts storage formats with different identifier encodings; and project-level workspace wiring of @xcitedbs/client for top-level diagnostic scripts. They're tracked in the BFF/SPA repo and aren't fixed here.

Build

npm run build

Outputs dist/ (CommonJS + .d.ts).