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

@datanimbus/dnio-sdk

v0.1.0

Published

DNIO SDK — modular JavaScript client for the DataNimbus platform.

Downloads

122

Readme

DNIO SDK

Modular JavaScript client libraries for the DataNimbus (DNIO) platform. Designed for dashboards and apps that consume DNIO — read app/service metadata, run records CRUD, trigger lifecycle ops (deploy / publish / start / stop / sync), inspect runtime interactions.

Authoring is OUT of scope. Creating data-service definitions, designing data pipes (flows), installing plugins, and creating / editing deployment groups are intentionally not exposed. Use the MCP server (@datanimbus/dnio-mcp) or the platform UI for those. The SDK gives you everything you need to consume the platform — read state, run records CRUD, and trigger lifecycle ops on existing artifacts.

JS + JSDoc only, no TypeScript build step. Use the umbrella package for convenience, or import only the feature you need to keep your bundle slim.

Install

npm install @datanimbus/dnio-sdk
# or
yarn add @datanimbus/dnio-sdk
# or
pnpm add @datanimbus/dnio-sdk

That single install brings in the entire SDK. Node 18+ required (native fetch). Modern browsers via your bundler (Vite, Next, webpack, esbuild — all CJS-friendly).

// Whole umbrella — most apps:
const { DNIO } = require('@datanimbus/dnio-sdk');

// Cherry-pick — keep your bundle slim:
const { HttpClient } = require('@datanimbus/dnio-sdk/core');
const { AuthClient } = require('@datanimbus/dnio-sdk/auth');
const { RecordsClient } = require('@datanimbus/dnio-sdk/records');

ES modules work too via the same subpath exports:

import { DNIO } from '@datanimbus/dnio-sdk';
import { RecordsClient } from '@datanimbus/dnio-sdk/records';

Layout

dnio-sdk/
├── package.json
└── src/
    ├── index.js                 # umbrella — DNIO class
    ├── core/                    # HttpClient, storage adapters, errors
    ├── auth/                    # login, logout, session, refresh
    ├── apps/                    # AppsClient + AppContext + AppHandle + ServiceHandle
    ├── services/                # data-service read + lifecycle (no authoring)
    ├── records/                 # full Swagger surface on /api/c/{app}/{path}
    ├── connectors/              # instances + marketplace types
    ├── plugins/                 # read-only catalog browse
    ├── flows/                   # list / get / publish (no authoring)
    ├── deployments/             # list / get / start / stop / sync (no authoring)
    ├── data-formats/            # list + get
    ├── api-keys/                # full lifecycle
    ├── interactions/            # runtime observability
    ├── agents/                  # stub — future
    └── workflows/               # stub — future

Usage

Handle chain (recommended)

The handle chain mirrors how you think about the platform: app → service → action. Args are pre-bound, so the LLM (or your IDE) can dot-walk the model and never has to thread appName / servicePath through every call.

const { DNIO } = require('@datanimbus/dnio-sdk');

const dnio = new DNIO({
  baseUrl: 'https://your.dnioinstance.io',
  // persistCredentials: true,                  // silent re-login on token expiry
  // storage: new CookieStorageAdapter(),       // override storage backend
  // onTokenExpired: (err) => routeToLogin(),   // consumer-handled expiry
});

// 1. Auth
if (!(await dnio.initialize())) {
  await dnio.login({ username: '[email protected]', password: '...' });
}

// 2. Bind to an app — synchronous, no network yet.
const app = dnio.app('MCP');

// 3. Bind to a service — auto-loads the app's service registry on first call.
const svc = await app.service('customerProfile');
//   svc.info.serviceId   // 'SRVC13363'
//   svc.info.servicePath // 'customerProfile'
//   svc.info.definition  // field schema array
//   svc.info.status      // 'Active' | 'Draft' | ...

// 4. Records CRUD via the bound handle — full Swagger surface.
const list   = await svc.records.list({ count: 10, sort: '-_metadata.createdAt' });
const count  = await svc.records.count();
const agg    = await svc.records.aggregate([{ $count: 'total' }]);
const record = await svc.records.create({ firstName: 'Ada' });
await svc.records.update(record._id, { firstName: 'Ada Lovelace' });
await svc.records.math(record._id, [{ $inc: { visitCount: 1 } }]);
await svc.records.delete(record._id);

// 5. Service lifecycle (pre-bound to svc.serviceId).
await svc.deploy();
await svc.start();
await svc.stop();

// 6. App-scoped sub-clients (no service registry needed).
await app.connectors.list({ category: 'DB' });
await app.flows.list();
await app.flows.publish('FLOW6156');
await app.deployments.start('DG123');
await app.apiKeys.create({ name: 'Test', expiryAfter: 30 });
await app.interactions.list('FLOW6156', { count: 5 });

await dnio.logout();

Flat clients (multi-app loops, advanced)

The flat namespaces remain available — every domain client takes (appName, servicePath, ...) explicitly. Use these when you need to operate on multiple apps in one function.

const apps = await dnio.apps.list();                               // admin-only
await dnio.records.list('appA', 'studentRecord', { count: 10 });
await dnio.records.list('appB', 'studentRecord', { count: 10 });

Token refresh

By default, an expired token causes the next call to throw with 401. Two ways to handle:

  1. Silent re-login (recommended for kiosks / long sessions): construct with persistCredentials: true. The SDK stores {username, password} alongside the session in your StorageAdapter and silently calls login() again before expiry (and on any 401). Trade-off: credentials sit in browser storage; do NOT enable on shared/untrusted devices.
  2. Consumer-handled (recommended for normal apps): pass onTokenExpired: (err) => routeToLogin(). The SDK fires this callback when it can't transparently refresh. Caller redirects the user to a login screen.

Both are optional. Without either, calls just fail with HttpError(status: 401) after expiry — handle in your own retry logic.

Per-feature (lean imports)

Every feature class is importable through a subpath export so bundlers can tree-shake / drop unused code.

const { HttpClient } = require('@datanimbus/dnio-sdk/core');
const { AuthClient } = require('@datanimbus/dnio-sdk/auth');
const { RecordsClient } = require('@datanimbus/dnio-sdk/records');

const http = new HttpClient({ baseUrl: 'https://your.dnioinstance.io' });
const auth = new AuthClient(http);
await auth.login({ username, password });

const records = new RecordsClient(http);
await records.list('myApp', 'studentRecord');

Subpaths available: /core, /auth, /apps, /services, /records, /connectors, /plugins, /flows, /deployments, /data-formats, /api-keys, /interactions, /agents, /workflows.

Capabilities at a glance

| Domain | Entry point | What you can do | What you can't (use MCP / UI) | |---|---|---|---| | Authentication | dnio.auth | login, logout, restore session, silent / hooked refresh | — | | Apps & service registry | dnio.apps, dnio.app(name) | list apps, sticky select-app, list services in app, resolve service by name | — | | Data Services | app.services, await app.service(name) | list, get, getSchema, deploy, start, stop | create / update definition | | Records | svc.records.* | full CRUD + count + math + aggregate + bulk + export + file upload/download + hook + simulate | — | | Connectors | app.connectors | list instances, list marketplace types, create instance | — | | Plugins | app.plugins | listMarketplace, listInstalled (read-only) | install / update / uninstall | | Flows (data pipes) | app.flows | list, get, publish, build invocation URL | create / update flow doc / add or remove nodes | | Deployment groups | app.deployments | list, get, listAvailableFlows, getYamls, start, stop, sync | create / update / delete group, add or remove flows | | Data formats | app.dataFormats | list, get | create / update / delete | | API keys | app.apiKeys | list, get, create, update, delete | — | | Flow Interactions | app.interactions | list runs, get one, per-node state, raw payload at any node | — |

Domain reference

Everything below uses the recommended handle chain. Each section names what comes back so you know what to consume.

1. Authentication

const dnio = new DNIO({
  baseUrl: 'https://your.dnioinstance.io',
  // session persistence:
  storage:    new CookieStorageAdapter({ maxAgeSec: 86400 }),  // OR LocalStorage / Memory / your own
  sessionKey: 'session',                                       // storage key

  // expiry handling — pick at most one:
  persistCredentials: true,                                    // (1) silent re-login
  // onTokenExpired: (err) => routeToLogin(),                  // (2) consumer-handled
  refreshLeadMs: 2 * 60 * 1000                                 // refresh this many ms before expiry (0 disables)
});

// Restore from storage if present, else login fresh.
if (!(await dnio.initialize())) {
  await dnio.login({ username: '[email protected]', password: '...' });
}

dnio.isAuthenticated();    // → boolean
dnio.getToken();           // → JWT string | null
await dnio.logout();       // clears memory + storage + cancels scheduled refresh

AuthClient subscribes to the shared HttpClient's tokenExpired event so a 401 anywhere transparently triggers refresh(). If persistCredentials is off and no onTokenExpired is set, the original 401 surfaces as an HttpError.

2. Apps & service registry

// Admin-only: list every app the current user can access.
const apps = await dnio.apps.list({ count: -1, select: '_id,description' });
// → Array<{_id, description, ...}>

// Bind to one app — sync, no network yet.
const app = dnio.app('MCP');
app.name;       // 'MCP'
app.loaded;     // false until something triggers a registry load

// Lazy-load + read the service registry.
const services = await app.listServices();
// → Array<ServiceEntry>: { serviceId, name, servicePath, toolPrefix, description, definition, status }

// Pre-load explicitly if you'll be calling app.service(...) repeatedly:
await app.load();
app.servicesList;   // sync getter — same array as above
await app.refresh(); // drop the cache + reload

3. Data Services (definitions)

Read + lifecycle only. Authoring belongs to the MCP / UI.

// At the app level — operate on any service by id.
const all = await app.services.list({ filter: { status: 'Active' } });
// → Array<service document>
const def = await app.services.get('SRVC13363', { draft: false });
const full = await app.services.getSchema('SRVC13363');     // full document incl. definition
await app.services.deploy('SRVC13363');                     // PUT /api/a/sm/{app}/service/utils/{id}/deploy
await app.services.start('SRVC13363');
await app.services.stop('SRVC13363');

// At the service level — bound to one service, no id arg.
const svc = await app.service('customerProfile');
svc.serviceId;   // 'SRVC13363'
svc.servicePath; // 'customerProfile'
svc.status;      // 'Active'
svc.info.definition;          // field schema array (length === field count)

await svc.deploy();
await svc.start();
await svc.stop();
const fresh = await svc.getSchema();   // re-fetch full document

4. Records (full Swagger surface)

The most-used surface. Every RecordsClient method is exposed on svc.records.* with appName + servicePath pre-bound.

const svc = await app.service('customerProfile');

// ── Read ─────────────────────────────────────────────────────────────────
const list  = await svc.records.list({ filter: { status: 'Active' }, sort: '-_metadata.createdAt', count: 20, page: 1, expand: false });
// → Array<record document>
const one   = await svc.records.get('REC123', { expand: true, select: 'firstName,email' });
// → record document
const total = await svc.records.count();                          // number
const some  = await svc.records.count({ status: 'Active' });      // number

// Mongo aggregation pipeline — ad-hoc analytics.
const agg = await svc.records.aggregate([
  { $match: { status: 'Active' } },
  { $group: { _id: '$country', count: { $sum: 1 } } },
  { $sort:  { count: -1 } }
]);
// → Array<aggregation row>

// ── Write ────────────────────────────────────────────────────────────────
const created = await svc.records.create({ firstName: 'Ada' }, { expireAfter: '7d' });
// → created record (server-enriched: _id, _metadata, ...)

await svc.records.update(created._id, { firstName: 'Ada Lovelace' }, { upsert: false });
// → updated record

await svc.records.delete(created._id);                            // empty response

// Atomic numeric mutations on a single record.
await svc.records.math(created._id, [
  { $inc: { visitCount: 1, score: 0.5 } },
  { $mul: { score: 1.1 } }
]);
// → mutated record

// ── Bulk ─────────────────────────────────────────────────────────────────
await svc.records.bulkUpdate(['REC1', 'REC2'], { tag: 'archived' });        // → Array<updated record>
await svc.records.bulkUpsert(
  { keys: ['email'], docs: [{ email: 'a@x', name: 'A' }] },
  { update: true, insert: true }
);                                                                          // → Array<updated + inserted>
await svc.records.bulkDelete(['REC1', 'REC2']);                             // → empty

// ── Files ────────────────────────────────────────────────────────────────
// fileUpload accepts Blob / File / Buffer / Uint8Array.
const meta = await svc.records.fileUpload(blobOrBuffer, { filename: 'invoice.pdf', encryptionKey: '<optional>' });
// → file metadata: { _id, filename, contentType, length, ... }

// fileDownload returns the raw `Response` so the caller can blob() / arrayBuffer() / pipe to disk.
const res = await svc.records.fileDownload(meta._id, { encryptionKey: '<optional>' });
const buf = await res.arrayBuffer();

// fileMapperCount — count of records associated with a previously-imported mapper file.
await svc.records.fileMapperCount(fileId);                                  // → number

// ── Export ───────────────────────────────────────────────────────────────
const job = await svc.records.exportRecords(
  { filter: '...', select: 'firstName,email', sort: '-_metadata.createdAt', skip: 0, batchSize: 1000 },
  { expand: false }
);
// → export job descriptor with the export file _id

const dl  = await svc.records.exportDownload(job._id, { filename: 'customers.csv' });
const text = await dl.text();

// ── Hooks ────────────────────────────────────────────────────────────────
// Manually re-fire the configured webhook for this service.
await svc.records.hook({ event: 'manual' }, { url: '<override URL — optional>' });

// ── Validation / dry-run ─────────────────────────────────────────────────
// Server validates + enriches the doc but does NOT persist. Useful for forms,
// schema discovery, and debugging "why does my create fail?".
const sim = await svc.records.simulate(
  { firstName: 'Ada' },
  { generateId: true, operation: 'POST', select: '...' }
);
// → enriched (un-persisted) document; OR the platform throws HttpError(400) with the field validation list

5. Connectors

// Browse marketplace types (each item carries a `fields` schema describing required credentials).
const types = await app.connectors.listMarketTypes({ count: 1000 });
// → Array<{ _id, type, label, category, fields: [{key, label, type, required, encrypted}, ...] }>

// List existing instances (filter by category for picker UIs).
const dbs  = await app.connectors.list({ category: 'DB' });
const stgs = await app.connectors.list({ category: 'STORAGE' });
// → Array<{ _id, name, category, type, options, _metadata }>

// Register a new instance.
const inst = await app.connectors.create({
  name: 'My MongoDB Prod',
  marketItemId: types[0]._id,
  values: { connectionString: 'mongodb://...' }
});
// → persisted connector instance

6. Data Pipes (flows)

Read + publish only.

const flows = await app.flows.list();
// → Array<{ _id, name, status, inputNode, ... }>

const flow = await app.flows.get('FLOW6156');
// → full flow document

// Public invocation URL for HTTP-triggered flows. Returns null for Timer / non-HTTP triggers.
const inv = app.flows.invocationUrl(flow);
// → { method: 'POST', url: 'https://your.dnioinstance.io/b2b/pipes/MCP/ingest' } | null

await app.flows.publish('FLOW6156');
// → updated flow document (Draft → Active). Required before adding to a deployment group.

7. Deployment Groups (Kubernetes)

Read + lifecycle only. Group authoring (create / update / delete / add or remove flows / rename) is out of scope — use the MCP server or platform UI.

// Inventory.
const groups = await app.deployments.list();
// → Array<{ _id, name, status, deployments: [{_id, name, type:'FLOW', version, ...}], ... }>

const grp = await app.deployments.get('DG3017');
// → full group document

// Discover what could be added to a NEW group (published flows not currently bound).
const avail = await app.deployments.listAvailableFlows();
// → Array<flow>

// Actual K8s manifests the platform generated for this group.
const yamls = await app.deployments.getYamls('DG3017');
// → Array<manifest> OR object keyed by manifest kind — depends on platform version

// Lifecycle. K8s ops are async on the platform side (~10s settle time).
await app.deployments.start('DG3017');   // PUT /utils/{id}/start  → spin up pods
await app.deployments.stop('DG3017');    // PUT /utils/{id}/stop   → tear down pods (definition preserved)
await app.deployments.sync('DG3017');    // PUT /utils/{id}/sync   → re-pull latest published flows

8. Plugins (workflow nodes)

Read-only catalog browse. Install / update / uninstall is out of scope.

const market = await app.plugins.listMarketplace({ page: 1, count: 50, sort: 'label' });
// → Array<{ _id, marketId, label, type, version, ... }>

const installed = await app.plugins.listInstalled({ count: 200 });
// → Array<{ _id, name, type, version, ... }>

Note: the marketplace _id is NOT the same as the installed _id. Install returns a different id for the per-app copy.

9. Data formats

Read-only.

const formats = await app.dataFormats.list({ count: 50 });
// → Array<format summary>

const full = await app.dataFormats.get('DF1234');
// → full format document with attribute / HRSF section definitions

10. API keys

Full lifecycle. The JWT comes back ONLY ONCE in the create response — surface it to the user immediately.

const keys = await app.apiKeys.list();
// → Array<{ _id, name, status, expiryAfterDate, roles: [...] }> — JWT NOT included

const meta = await app.apiKeys.get('API1202');
// → key document — JWT NOT included

const created = await app.apiKeys.create({ name: 'dashboard-test', expiryAfter: 30 });
// → { ..., apiKey: '<JWT — shown only once>' } — SAVE IT NOW

// Update is a full-document PUT — round-trip server-managed fields like
// tokenHash, expiryAfterDate. Use the `redactApiKey` helper to strip the JWT
// from a fetched doc before mutating.
const { redactApiKey } = require('dnio');
const doc = await app.apiKeys.get('API1202');
await app.apiKeys.update('API1202', { ...redactApiKey(doc), status: 'Disabled' });

// Permanent. Prefer status='Disabled' if you may need to re-enable.
await app.apiKeys.delete('API1202');

11. Flow Interactions (runtime observability)

Read-only debug / audit surface. Each interaction is one flow run; each run has one state per node that executed; each state has actual incoming + outgoing payload accessible separately.

// 1. List runs for a flow.
const runs = await app.interactions.list('FLOW6272', {
  page: 1,
  count: 30,
  sort: '-_metadata.createdAt',
  filter: { status: 'ERROR' }    // or { '_metadata.createdAt': { $gte: '2026-01-01' } }
});
// → Array<{ _id, status:'SUCCESS'|'ERROR', txnId, _metadata: {createdAt, ...} }>

// 2. Full metadata for one run (headers, params, query, txn ids, parent linkage).
const det = await app.interactions.get('FLOW6272', runs[0]._id);

// 3. Per-node trace — schema summaries only (NOT actual payloads).
const states = await app.interactions.state('FLOW6272', runs[0]._id);
// → Array<NodeState>: { nodeId, status, statusCode, incoming: {schema}, outgoing: {schema}, _metadata: {...} }
//   Ordered by execution. The flow path is `states.map(s => s.nodeId)`.

// 4. Actual payload at one specific node.
const data = await app.interactions.nodeData('FLOW6272', runs[0]._id, 'route_request');
// → { nodeId, status, statusCode, incoming, outgoing, _metadata: {...} }
//   incoming + outgoing carry the real data the runtime saw at that step.

UI integration

The SDK is browser-first. Drop it into any framework — single shared instance per app, exposed via your DI / context primitive of choice.

Plain HTML / vanilla JS

<script type="module">
  import { DNIO } from 'https://esm.sh/@datanimbus/dnio-sdk';

  const dnio = new DNIO({ baseUrl: 'https://your.dnioinstance.io' });

  document.querySelector('#login').addEventListener('submit', async (e) => {
    e.preventDefault();
    await dnio.login({
      username: e.target.username.value,
      password: e.target.password.value
    });
    const app = dnio.app('MCP');
    const svc = await app.service('customerProfile');
    const rows = await svc.records.list({ count: 20 });
    document.querySelector('#rows').textContent = JSON.stringify(rows, null, 2);
  });
</script>

React

One DNIO per app. Share via context.

// dnio-context.js
import { createContext, useContext, useMemo } from 'react';
import { DNIO } from '@datanimbus/dnio-sdk';

const DnioContext = createContext(null);

export function DnioProvider({ baseUrl, children }) {
  const dnio = useMemo(() => new DNIO({
    baseUrl,
    persistCredentials: true,
    onTokenExpired: () => { window.location.href = '/login'; }
  }), [baseUrl]);
  return <DnioContext.Provider value={dnio}>{children}</DnioContext.Provider>;
}

export const useDnio = () => useContext(DnioContext);
// CustomerTable.jsx
import { useEffect, useState } from 'react';
import { useDnio } from './dnio-context';

export function CustomerTable() {
  const dnio = useDnio();
  const [rows, setRows] = useState([]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!dnio.isAuthenticated()) await dnio.initialize();
      const app = dnio.app('MCP');
      const svc = await app.service('customerProfile');
      const list = await svc.records.list({ count: 50, sort: '-_metadata.createdAt' });
      if (!cancelled) setRows(list);
    })();
    return () => { cancelled = true; };
  }, [dnio]);

  return (
    <table>
      <tbody>
        {rows.map((r) => (
          <tr key={r._id}>
            <td>{r.firstName}</td>
            <td>{r.email}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
// App.jsx — wire it all up
import { DnioProvider } from './dnio-context';
import { CustomerTable } from './CustomerTable';

export default function App() {
  return (
    <DnioProvider baseUrl="https://your.dnioinstance.io">
      <CustomerTable />
    </DnioProvider>
  );
}

Vue 3

Inject the SDK at app boot, consume from any component.

// main.js
import { createApp } from 'vue';
import { DNIO } from '@datanimbus/dnio-sdk';
import App from './App.vue';

const dnio = new DNIO({ baseUrl: 'https://your.dnioinstance.io', persistCredentials: true });
const app = createApp(App);
app.provide('dnio', dnio);
app.mount('#app');
<!-- CustomerTable.vue -->
<script setup>
import { inject, ref, onMounted } from 'vue';
const dnio = inject('dnio');
const rows = ref([]);

onMounted(async () => {
  if (!dnio.isAuthenticated()) await dnio.initialize();
  const app = dnio.app('MCP');
  const svc = await app.service('customerProfile');
  rows.value = await svc.records.list({ count: 50 });
});
</script>

<template>
  <ul><li v-for="r in rows" :key="r._id">{{ r.firstName }} — {{ r.email }}</li></ul>
</template>

Next.js / SSR

The SDK is browser-only — DO NOT instantiate it in server components. Wrap it in a client-side context provider:

// app/dnio-provider.tsx
'use client';
import { DnioProvider } from '@/lib/dnio-context';
export function DnioRoot({ children }) {
  return <DnioProvider baseUrl={process.env.NEXT_PUBLIC_DNIO_BASE_URL}>{children}</DnioProvider>;
}

// app/layout.tsx (server component)
import { DnioRoot } from './dnio-provider';
export default function RootLayout({ children }) {
  return <html><body><DnioRoot>{children}</DnioRoot></body></html>;
}

For SSR session restore, use CookieStorageAdapter so the server can read the same JWT cookie:

new DNIO({
  baseUrl: '...',
  storage: new CookieStorageAdapter({ maxAgeSec: 86400, sameSite: 'Lax' })
});

Login form pattern

function LoginForm() {
  const dnio = useDnio();
  const [err, setErr] = useState(null);

  async function onSubmit(e) {
    e.preventDefault();
    setErr(null);
    try {
      await dnio.login({
        username: e.target.username.value,
        password: e.target.password.value
      });
      window.location.href = '/dashboard';
    } catch (ex) {
      setErr(ex.body?.message || ex.message);
    }
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="username" required />
      <input name="password" type="password" required />
      <button type="submit">Sign in</button>
      {err && <p style={{color: 'red'}}>{err}</p>}
    </form>
  );
}

App / service picker pattern

function AppPicker({ onSelect }) {
  const dnio = useDnio();
  const [apps, setApps] = useState([]);

  useEffect(() => {
    dnio.apps.list()                                // admin-only
      .then(setApps)
      .catch((e) => e.status === 403 ? setApps([{ _id: 'MCP', description: 'enter manually' }]) : null);
  }, [dnio]);

  return (
    <select onChange={(e) => onSelect(e.target.value)}>
      {apps.map((a) => <option key={a._id} value={a._id}>{a._id}</option>)}
    </select>
  );
}

function ServicePicker({ appName, onPick }) {
  const dnio = useDnio();
  const [services, setServices] = useState([]);

  useEffect(() => {
    if (!appName) return;
    dnio.app(appName).listServices().then(setServices);
  }, [dnio, appName]);

  return (
    <select onChange={(e) => onPick(e.target.value)}>
      {services.map((s) => (
        <option key={s.serviceId} value={s.name}>{s.name} ({s.status})</option>
      ))}
    </select>
  );
}

Storage adapters

The auth module persists the session via a pluggable adapter. Pass one to new DNIO({ storage }) or new AuthClient(http, { storage }):

| Adapter | Use when | |---|---| | LocalStorageAdapter | Browser, default. Survives reloads. | | CookieStorageAdapter | Browser SSR / cross-tab. Tunable Path/Domain/SameSite/Secure/Max-Age. | | MemoryStorage | Node, tests, ephemeral. The auto-default outside browsers. | | Your own | Implement { get(key), set(key, value), remove(key) } (sync or async). |

If no adapter is passed, defaultStorage() picks LocalStorageAdapter in browsers and MemoryStorage everywhere else.

Error handling

Every error thrown by the SDK extends DnioError. The most useful subclasses:

| Class | When | |---|---| | HttpError | Non-2xx response, network failure, or timeout. Carries status, body, path. | | AuthError | Login failures, invalid credentials, missing tokens. | | NotInitializedError | Calling an authenticated method before .initialize() / .login() has set a token. |

const { HttpError, AuthError } = require('dnio');

try {
  await svc.records.create({ firstName: 'Ada' });
} catch (err) {
  if (err instanceof HttpError) {
    // err.status   — HTTP status code (0 for network failure)
    // err.path     — request path
    // err.body     — parsed response body (often {message: '...'})
    if (err.status === 400) {
      // Platform validation — show err.body.message to the user.
    } else if (err.status === 403) {
      // Permission denied — surface "you don't have access" UI.
    } else if (err.status === 404) {
      // Not found — record / service / app missing.
    } else {
      // 5xx — retry or alert.
    }
  } else if (err instanceof AuthError) {
    // Login failed, missing token, refresh failed.
    routeToLogin();
  }
}

Tip: call svc.records.simulate(doc, { generateId: true, operation: 'POST' }) before a create() to discover what fields are mandatory — the platform returns a 400 with field : Mandatory Field for every missing required field, no record persisted.

Adding a new feature package

  1. mkdir packages/<name> and add a package.json (copy any sibling).
  2. mkdir packages/<name>/src and write index.js exporting a Client class that takes the shared http.
  3. Add @dnio/<name> to packages/sdk/package.json dependencies and import + mount it in packages/sdk/src/index.js.
  4. Run npm install at the repo root to wire workspaces.
  5. Add JSDoc on every public method — type signatures + parameter docs are how consumers discover the surface.

Contracts

  • JS only. No TypeScript, no build step. Source is shipped as-is via the files: ["src"] whitelist.
  • CommonJS. All packages use require / module.exports. Modern bundlers (Vite, Next, webpack) handle CJS fine.
  • Node 18+ required for native fetch.
  • Single HttpClient shared across feature clients — auth lives there, every call inherits it.
  • No per-package side effects. Importing a feature does not perform network I/O until you call a method on its class.

License

ISC.