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

@krovacloud/sdk

v0.4.14

Published

Official TypeScript SDK for Krova Cloud — a typed client for provisioning and managing Cubes (Firecracker microVMs) with dedicated resources.

Downloads

1,430

Readme

@krovacloud/sdk

npm version npm downloads license: MIT types: included

The official TypeScript SDK for the Krova Cloud API — a fully typed client for provisioning and managing Cubes (Firecracker microVMs).

Highlights

  • Fully typed — request bodies, responses, and path params are generated from the Krova Cloud OpenAPI spec via openapi-typescript + openapi-fetch.
  • Ergonomic helpers — client.cubes, domains, snapshots, tcpMappings, imports, backups, webhooks and catalog unwrap the response body and throw a typed KrovaError on failure.
  • Full escape hatch — client.raw exposes the underlying typed client for every operation in the bundled OpenAPI spec (Domains, TCP mappings, Snapshots, Backups, Imports, Webhooks, and more).
  • Zero-config resilience — automatic retries on 429 / 503, honoring Retry-After.
  • ESM + CJS — ships both, with bundled .d.ts declarations. No runtime dependencies beyond openapi-fetch.

Install

npm i @krovacloud/sdk
# or: pnpm add @krovacloud/sdk
# or: yarn add @krovacloud/sdk

Requires Node.js ≥ 18 (uses the global fetch).

Quickstart

import { KrovaClient, KrovaError } from "@krovacloud/sdk";

const krova = new KrovaClient({
  apiKey: process.env.KROVA_API_KEY!, // a "kro_..." token
});

// List Cubes in a Space
const cubes = await krova.cubes.list("space_123");
console.log(cubes);

// Create a Cube (sshPublicKey is required)
const cube = await krova.cubes.create("space_123", {
  name: "web-server",
  image: "ubuntu-24.04",
  resources: { vcpu: 2, ramGb: 4, diskGb: 40 },
  sshPublicKey: "ssh-ed25519 AAAA...your-key... you@host",
});

console.log(`Created cube ${cube.id} (${cube.state})`);
console.log(`  ${cube.resources.vcpu} vCPU / ${cube.resources.ramGb} GB RAM, image ${cube.image}`);

// Everything needed to SSH in — including WHICH USER to log in as.
// Read `user`; do not derive it from `image`. It is "ubuntu" or "debian" on
// Cubes from images that ship a default user (both with passwordless sudo)
// and "root" on Cubes created before that change, so the image id cannot
// tell you. `cube.sshUser` on the Cube object carries the same value.
//
// `ssh.host` is the Cube's own stable SSH hostname — it survives migration
// to another server, falling back to the server's public IPv4 only when the
// Cube has no DNS record yet. If you pin host keys in your own known_hosts
// (as the CLI does), expect a one-time re-pin for any Cube you'd previously
// connected to by IP: the host string changes from an IP to a name, so it's
// a new known_hosts entry, not a compromised host.
const ssh = await krova.cubes.ssh("space_123", cube.id);
console.log(`ssh ${ssh.user}@${ssh.host} -p ${ssh.port}`);

// Power it off, then start it again
await krova.cubes.powerOff("space_123", cube.id);
await krova.cubes.wake("space_123", cube.id);
await krova.cubes.restart("space_123", cube.id); // cold restart

Authentication

Get an API key at krova.cloud → your Space settings. Keys are scoped per Space, inherit the permissions of the membership that created them, and look like kro_....

By default the client sends the key as the X-API-KEY header (the API's security scheme). If your gateway expects a bearer token instead, pass authScheme: "bearer":

const krova = new KrovaClient({ apiKey: "kro_...", authScheme: "bearer" });

Keep keys secret. Never commit a key or embed it in a browser bundle. Load it from an environment variable or a secrets manager.

API reference

Every public export, with its real signature and a short example.

new KrovaClient(options)

new KrovaClient({
  apiKey: string,               // required — your "kro_..." token
  baseUrl?: string,             // default: "https://krova.cloud/api/v1"
  authScheme?: "x-api-key" | "bearer", // default: "x-api-key"
  maxRetries?: number,          // default: 2 — retries 429/503 (honors Retry-After); 0 disables
  fetch?: typeof fetch,         // optional fetch override (proxy, tests)
});

Throws if apiKey is missing. Exposes client.baseUrl (the resolved base URL), client.getSpace(), and the resource helpers client.cubes, client.domains, client.snapshots, client.tcpMappings, client.imports, client.backups, client.catalog (each documented in its section below), plus the raw typed client client.raw.

client.getSpace()

Resolve the Space your API key is scoped to — so you don't have to hardcode a spaceId:

const space = await krova.getSpace(); // { id, name, tier, createdAt }
const cubes = await krova.cubes.list(space.id);

client.cubes

Ergonomic helpers for the Cube lifecycle. Each unwraps the response body and throws KrovaError on a non-2xx status.

| Method | Signature | Returns | | --- | --- | --- | | list | (spaceId: string) | the Cube list body | | create | (spaceId, body, opts?) | the created Cube | | get | (spaceId, cubeId) | the Cube body | | update | (spaceId, cubeId, body) | changes the port sshd listens on inside the Cube | | setTerminationProtection | (spaceId, cubeId, enabled) | the updated Cube. While protection is on, delete throws TerminationProtectedError (a KrovaError for the API's 409) | | delete | (spaceId, cubeId) | enqueues deletion | | powerOff | (spaceId, cubeId) | enqueues a power-off | | wake | (spaceId, cubeId) | enqueues a start | | ssh | (spaceId, cubeId) | the Cube's SSH connection info — host is its stable SSH hostname (falls back to the server's public IPv4 with no DNS record yet), plus port, user, hostKeys | | restore | (spaceId, cubeId, snapshotId) | enqueues a restore — replaces the Cube's disk from a snapshot |

// create — sshPublicKey is required; region + userData (cloud-init) are optional.
// opts.idempotencyKey (≤255 chars, per-space) makes retries safe.
const cube = await krova.cubes.create(
  "space_123",
  {
    name: "web-server",
    image: "ubuntu-24.04",
    resources: { vcpu: 2, ramGb: 4, diskGb: 40 },
    sshPublicKey: "ssh-ed25519 AAAA... you@host",
    region: "us",                // optional — slug from catalog.regions()
    userData: "#cloud-config\n",  // optional — cloud-init (max 16 KB)
    terminationProtection: true, // optional — refuse deletes until turned off
  },
  { idempotencyKey: "deploy-2026-07-01" },
);

// get / list
const one = await krova.cubes.get("space_123", cube.id);
const all = await krova.cubes.list("space_123");

// update — the port sshd listens on inside the Cube
await krova.cubes.update("space_123", cube.id, { cubePort: 2222 });

// termination protection — delete is refused until it is turned off
await krova.cubes.setTerminationProtection("space_123", cube.id, false);

// lifecycle — powerOff, wake, delete are asynchronous (enqueued)
await krova.cubes.powerOff("space_123", cube.id);
await krova.cubes.wake("space_123", cube.id);
// Cold restart: reboots against the host's current kernel, preserving the disk.
// The only way to pick up a refreshed guest kernel — a `reboot` run INSIDE the
// Cube cannot, because the kernel is supplied by the host, and it reports no
// error. Requires the Cube to be `running`; a concurrent restart 409s.
await krova.cubes.restart("space_123", cube.id);
await krova.cubes.delete("space_123", cube.id);

The Cube type is exported for your own signatures:

import type { Cube } from "@krovacloud/sdk";
// {
//   id: string; name: string;
//   state: "pending" | "booting" | "running" | "stopped" | "stopping" | "resizing" | "error" | "deleted";
//   publicIpv4: string | null;
//   resources: { vcpu: number; ramGb: number; diskGb: number };
//   image: string; costPerHour: number;
//   terminationProtection: boolean;
//   createdAt: string; updatedAt: string;
// }

client.catalog

Public catalog endpoints (no auth required by the API; the client sends your key harmlessly).

const regions = await krova.catalog.regions(); // regions with available capacity
const images = await krova.catalog.images();   // available OS images
const pricing = await krova.catalog.pricing(); // per-resource hourly rates + volume tiers

Domains, snapshots & TCP mappings

Typed helpers for a Cube's attached resources — each unwraps the response and throws KrovaError on failure.

// Custom domains — create() resolves to { domain, records } (BREAKING in 0.4.0).
// The records are the DNS entries you must publish for the domain to work, so
// you can create them in the same run: a wildcard needs three, an exact host one.
const domains = await krova.domains.list("space_123", "cube_123");
const { domain, records } = await krova.domains.create("space_123", "cube_123", {
  domain: "app.example.com",
  port: 8080,
});
for (const r of records) console.log(`${r.type} ${r.host} → ${r.value}`);
await krova.domains.update("space_123", "cube_123", domain.id, { responseCompression: true });
await krova.domains.delete("space_123", "cube_123", domain.id);

// Snapshots + restore
const snap = await krova.snapshots.create("space_123", "cube_123", { name: "nightly" });
const snaps = await krova.snapshots.list("space_123", "cube_123");
await krova.cubes.restore("space_123", "cube_123", snap.id); // replace the disk from a snapshot
await krova.snapshots.delete("space_123", "cube_123", snap.id);

// TCP port mappings (expose a Cube port on the host)
const mapping = await krova.tcpMappings.create("space_123", "cube_123", {
  cubePort: 5432,
  whitelistedIps: ["203.0.113.4/32"],
});
// `mapping.host` is where to connect — the Cube's OWN stable hostname, which
// survives migration to another server. Use it rather than the Cube's
// `serverDomain`, which is the current physical host and changes when the
// Cube moves. Null only when the Cube has no server assigned.
console.log(`psql -h ${mapping.host} -p ${mapping.hostPort}`);
await krova.tcpMappings.list("space_123", "cube_123");
await krova.tcpMappings.delete("space_123", "cube_123", mapping.id);

Domain, Snapshot, and TcpMapping are exported for your own signatures.

domains.records — the DNS records a domain needs

The same records domains.create() returns, but each one checked against live DNS. Poll it after publishing them — summary.complete turns true only once every record is found. Each call performs real DNS lookups and is rate limited, so poll on an interval rather than in a tight loop.

const status = await krova.domains.records("space_123", "cube_123", domain.id);
// { domain, isWildcard, records: DnsRecordStatus[], summary: { found, total, complete }, checkedAt }
for (const r of status.records) console.log(`${r.host}: ${r.state}`); // found | missing | mismatch | unknown
if (status.summary.complete) console.log("all records resolve — the domain can go live");

Two record states are worth knowing before you alert on them: missing means NOT PUBLISHED YET — the expected state before the records are created, never an error — and unknown means the lookup itself could not complete, which says nothing about your DNS. Each record also carries mustBeGrey and proxyOk: the routing record may sit behind Cloudflare's proxy, the _acme-challenge record must not — automation needs both flags, one alone would let you orange-cloud the single record that has to stay grey.

DnsRecord (what create returns) and DnsRecordStatus (the checked variant, adding state, detail, observed) are exported for your own signatures.

Imports & backups

Move .cube archives in and out. imports.create returns a multipart upload target; upload the archive to the presigned parts, then call imports.complete.

const start = await krova.imports.create("space_123", { name: "restored", fileSizeBytes: 1_048_576 });
// ...upload the archive to start.parts (presigned URLs)...
await krova.imports.complete("space_123", start.importId, { parts, config });
const status = await krova.imports.get("space_123", start.importId);

// Export: get a time-limited download URL for a backup archive
const dl = await krova.backups.download("space_123", "backup_123");

The download URL is valid for 15 minutes and needs no further authentication. Anyone holding it can fetch the whole disk, so treat it as a secret.

Backups and sharing them with another space

A backup is a Cube's disk and configuration, kept after the Cube is gone. You can offer another space its own copy: nothing is copied until that space accepts, within 48 hours, and from then the copy is the destination's own and billed to it. Your backup and bill are unchanged, and deleting either copy leaves the other alone.

const backups = await krova.backups.list("space_123");
const backup = await krova.backups.get("space_123", backups[0]!.id);

// Offer a copy. Always a pending request through the API, even when your user owns both spaces.
const share = await krova.backups.share(
  "space_123",
  backup.id,
  { destinationSpaceId: "space_456" },
  { idempotencyKey: "share-nightly-2026-10-07" },
);

// In the destination space: list what is waiting, then accept or decline.
const { incoming, outgoing } = await krova.backupShares.list("space_456");
const { backup: copy } = await krova.backupShares.accept("space_456", share.id);
// copy.sharedFromBackupId === backup.id

// Or: krova.backupShares.decline("space_456", share.id)
// The source can withdraw it before then: krova.backupShares.cancel("space_123", share.id)

These calls need the Backups permissions on the key's member: View Backups to list and get, Manage Backups for everything else. Only the destination space's owner, or a member there with Manage Backups, can accept or decline. accept throws a KrovaError with status 422 when the destination cannot take the copy (no card or credit, owner not verified, an open abuse notice, or no backup allowance left on its plan).

Webhooks

client.webhooks manages a space's webhook endpoints. create is the only call that returns the signing secret, so store it straight away. To verify deliveries, use @krovacloud/webhook.

const endpoint = await krova.webhooks.create("space_123", {
  url: "https://example.com/hook",
  events: ["cube.running", "cube.stopped"],
  description: "deploy notifier", // optional
});
// endpoint.secret is returned only here: store it now

await krova.webhooks.list("space_123");
await krova.webhooks.get("space_123", endpoint.id);
await krova.webhooks.deliveries("space_123", endpoint.id, { limit: 20 }); // last 30 days
await krova.webhooks.delete("space_123", endpoint.id);

client.raw — every endpoint

The helpers cover Cubes, the catalog, domains, snapshots, TCP mappings, imports, backups, and webhooks. When you would rather handle the error yourself than catch an exception, use client.raw, the fully typed openapi-fetch client. It returns { data, error, response } and never throws:

const { data, error } = await krova.raw.GET("/spaces/{spaceId}/cubes/{cubeId}", {
  params: { path: { spaceId: "space_123", cubeId: "cube_123" } },
});

if (error) {
  console.error("Cube lookup failed:", error.error);
} else {
  console.log(data);
}

Path, method, params, and body are all type-checked against the spec. The generated paths and components types are also exported for advanced use:

import type { paths, components } from "@krovacloud/sdk";
type Domain = components["schemas"]["Domain"];

KrovaError

Thrown by every helper on any non-2xx response. (client.raw never throws — it returns the error in { error }.)

| Field | Type | Source | | --- | --- | --- | | status | number | HTTP status code | | message | string | the API's error string, else statusText | | code | string \| undefined | X-Error-Code response header | | requestId | string \| undefined | X-Request-Id response header | | body | object \| undefined | the parsed JSON error body | | response | Response \| undefined | the raw Response |

import { KrovaError } from "@krovacloud/sdk";

try {
  await krova.cubes.get("space_123", "cube_missing");
} catch (err) {
  if (err instanceof KrovaError) {
    console.error(`[${err.status}] ${err.message}`);
    if (err.code) console.error("code:", err.code);
    if (err.requestId) console.error("request id:", err.requestId); // quote this to support
  } else {
    throw err;
  }
}

Mutating POST / DELETE endpoints are rate-limited (10 requests / 60s per client IP). The client automatically retries 429 and 503 up to maxRetries times, honoring the Retry-After header.

Configuration

Point the client at a different base URL (self-hosted gateway, staging, a proxy):

const krova = new KrovaClient({
  apiKey: "kro_...",
  baseUrl: "https://gateway.internal/krova/api/v1",
});

TypeScript

The package ships its own type declarations — no @types/* install needed. Cube, Backup, BackupShare, KrovaError, KrovaClientOptions, AuthScheme, and the generated paths / components are all exported.

Related packages

| Package | What it is | | --- | --- | | @krovacloud/cli | Command-line interface for Krova Cloud | | @krovacloud/webhook | Verify and parse Krova Cloud webhook events | | @krovacloud/mcp | Model Context Protocol server for Krova Cloud | | n8n-nodes-krova | n8n community nodes for Krova Cloud |

Requirements

  • Node.js ≥ 18 (global fetch).
  • Works in any modern runtime with a WHATWG fetch (Deno, Bun, edge). Pass a custom fetch if the global isn't available.

Contributing

See CONTRIBUTING.md. Report security issues privately per SECURITY.md.

Custom domains: HTTPS to the Cube

By default the edge reaches your Cube over cleartext HTTP — visitors are on HTTPS either way, since TLS terminates at the edge. Set originScheme: "https" when the Cube terminates TLS itself: a control panel holding its own certificate, or an app listening on HTTPS, answers plain HTTP with a redirect and cannot be served over cleartext at all.

// When attaching the domain…
await krova.domains.create(spaceId, cubeId, {
  domain: "panel.example.com",
  port: 443,
  originScheme: "https",
});

// …or switch an already-attached domain over.
await krova.domains.update(spaceId, cubeId, mappingId, {
  originScheme: "https",
});

The dial port is derived: https on the default port 80 connects on 443, and a deliberate custom port is honored exactly. The setting applies as saved, with no check: if nothing in the Cube answers HTTPS for the domain, visitors get an error page until you set it back to http.

Custom domains: the visitor's address on the connection

The edge always passes the visitor's address in the X-Real-IP and X-Forwarded-For headers. For software that reads it from the TCP connection instead, set proxyProtocol to "v1" (a line of text) or "v2" (binary): the edge then sends a PROXY protocol header naming the visitor at the start of every connection to the Cube. null turns it off.

await krova.domains.update(spaceId, cubeId, mappingId, { proxyProtocol: "v2" });

The app must accept the PROXY protocol on that port before you turn it on, or every request to the domain fails. An app accepts it per port, so every domain mapped to the same Cube port needs the same setting: a create or update that would mix them is rejected with a 409 whose message names the other domains. Send confirmMixedProxyProtocol: true to save anyway, when the app handles both on that port.

License

MIT © 2026 Krova Inc.