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

koro-sdk

v0.2.0

Published

Official TypeScript SDK for the Koro API — end-to-end encrypted messaging, spaces, meetings, tasks, calendar and webhooks.

Readme

koro-sdk

Official TypeScript SDK for the Koro API — end-to-end encrypted messaging, spaces, meetings, tasks, calendar and webhooks.

Works in Node 18+ (ESM and CommonJS). Fully typed.

npm install koro-sdk

Quickstart

import { Koro } from 'koro-sdk';

const koro = new Koro(); // reads KORO_TOKEN from the environment

// Who am I?
const me = await koro.me();

// Send an end-to-end encrypted message
await koro.messages.send('<conversationId>', 'Hello from a bot 👋');

// Create a space and schedule a meeting
const space = await koro.spaces.create({ name: 'My Team' });
const { url } = await koro.meetings.create({
  title: 'Standup',
  scheduled_at: '2026-07-01T09:00:00Z',
});
console.log('Join at', url);

Authentication

Every request is authenticated with a Koro API key of the form koro_live_<prefix>_<secret>, sent as Authorization: Bearer <token>.

The SDK reads the token from the KORO_TOKEN environment variable, or you can pass it explicitly:

const koro = new Koro({ token: 'koro_live_abcd_…' });

Create a .env in your project:

KORO_TOKEN=koro_live_abcd_yoursecret
# the bot device secret, shown once when you created the bot in the portal:
KORO_BOT_SECRET=base64secretkey…
# optional — defaults to https://api.koro.chat:3001
KORO_API_URL=https://api.koro.chat:3001

The easiest way to get both values is to create a named bot in the Koro Developer PortalBotsBot erstellen. The portal generates the bot's keypair in your browser and shows you KORO_TOKEN and KORO_BOT_SECRET once — paste both into your .env. The bot then appears in chats with the name + avatar you chose.

If you have dotenv installed, the SDK auto-loads .env for you. It is not a dependency — if it is absent the SDK simply reads process.env directly. (Or call import 'dotenv/config' yourself.)

API keys are workspace-scoped. Scopes include messages:read|write, conversations:read|write, tasks:read|write, users:read, webhooks:manage. A missing scope returns HTTP 403 — see Error handling.

End-to-end encryption (read this before sending messages)

Koro messages are end-to-end encrypted per recipient device using NaCl box (Curve25519 + XSalsa20-Poly1305, via tweetnacl). The server only ever stores opaque ciphertext — there is no plaintext path. A bot is just another device: it owns a Curve25519 box keypair, and peers seal ciphertext to its public key.

Bot identity (two ways)

The bot is a device with a Curve25519 box keypair. There are two ways to provide it, in order of preference:

1. Portal-issued KORO_BOT_SECRET (recommended). When you create a bot in the Koro Developer Portal, its keypair is generated in your browser and the secret is shown once. Set it in .env (or pass new Koro({ botSecret })). The matching public key is already registered server-side, so the bot can send and receive immediately — and shows up in chats with your chosen name + avatar.

const koro = new Koro(); // reads KORO_TOKEN + KORO_BOT_SECRET from .env

2. Self-managed identity file (fallback). With no KORO_BOT_SECRET, the SDK generates a keypair on first use and persists it to ./.koro/bot-identity.json (override with new Koro({ identityFile })). You must then enroll the printed public key on the server yourself.

const koro = new Koro();
console.log(koro.bot.identityPublicKey()); // base64 — enroll this on the server

Either way, the bot must be a member of the conversation to send/receive. Add it via the portal (Bot → in Konversation einladen) or POST /conversations/:id/members. Until the bot's public key is registered AND it's a conversation member:

  • messages.send(...) is rejected (the bot device isn't a valid recipient), and
  • messages.list(...) returns envelopes with text: null.

Keep KORO_BOT_SECRET / ./.koro/bot-identity.json private and out of version control — they contain the bot's secret key.

How messages.send works

1. GET /conversations/:id/devices         → [{ device_id, identity_public_key }]
2. for each device: nacl.box(plaintext, nonce, devicePubKey, botSecretKey)
3. POST /messages  { conversation_id, kind: 'text',
                     recipients: [{ device_id, ciphertext, nonce }] }

Receiving messages

const messages = await koro.messages.list('<conversationId>', { limit: 50 });
for (const m of messages) {
  if (m.text) console.log(m.sender_user_id, '→', m.text);
}

list fetches the recipient copy addressed to the bot device and decrypts it with nacl.box.open. Rows that aren't text, aren't addressed to the bot, or fail to open come back with text: null but full envelope metadata.

Koro has no long-poll endpoint, so bots poll messages.list. See examples/echo-bot.ts.

Message envelope format (assumption to reconcile)

The verified Koro mobile client seals the raw UTF-8 message string directly — there is no JSON wrapper for kind: 'text'. The SDK matches this byte-for-byte (EnvelopeFormat.Raw, the default), so messages it sends are readable by the mobile app and vice-versa.

A structured { t: 'text', body } envelope is also implemented (EnvelopeFormat.JsonTextBody) for forward-compatibility, and list will transparently unwrap either form. If Koro later standardizes on the structured envelope, switch with:

import { Koro, EnvelopeFormat } from 'koro-sdk';
const koro = new Koro({ envelopeFormat: EnvelopeFormat.JsonTextBody });

API reference

Construct once and reuse:

const koro = new Koro({
  token?: string,          // default: process.env.KORO_TOKEN
  apiUrl?: string,         // default: process.env.KORO_API_URL ?? https://api.koro.chat:3001
  identityFile?: string,   // default: ./.koro/bot-identity.json
  envelopeFormat?: EnvelopeFormat, // default: Raw
});

koro.me()

GET /users/me → the identity behind the API key.

koro.messages

| Method | Description | |---|---| | send(conversationId, text, { reply_to_message_id? }) | E2E-encrypt + send a text message. Returns { message }. | | list(conversationId, { limit?, before? }) | List + best-effort decrypt. Returns DecryptedMessage[]. | | devices(conversationId) | The conversation's recipient devices. |

koro.conversations

| Method | Description | |---|---| | list() | Your conversations. | | create({ kind, member_user_ids, title?, workspace_id? }) | Create a conversation. | | devices(id) | Recipient devices of a conversation. |

koro.spaces (workspaces)

| Method | Description | |---|---| | list() | Spaces you belong to. | | create({ name, description? }) | Create a space. | | invite(spaceId, { role?, max_uses?, expires_at? }) | Create an invite code. | | addChannel(spaceId, name) | Add a channel. |

koro.meetings

| Method | Description | |---|---| | create({ title, scheduled_at?, ... }) | Create a meeting. Returns { meeting, room_id, url }. | | list() | Your meetings. |

koro.tasks

| Method | Description | |---|---| | list() | Your tasks. | | create({ title, due_at?, priority?, workspace_id? }) | Create a task. | | update(id, patch) | Patch a task. | | complete(id) | Mark a task done. |

koro.calendar

| Method | Description | |---|---| | list() | Calendar events. | | create({ title, starts_at, ends_at?, workspace_id? }) | Create an event. |

koro.webhooks (requires webhooks:manage)

| Method | Description | |---|---| | list() | Registered webhooks. | | create({ workspace_id, url, events }) | Register a webhook. The returned secret is shown once. | | delete(id) | Delete a webhook. |

koro.bot

| Method | Description | |---|---| | identityPublicKey() | Base64 public key of the bot device (enroll this). | | keyPair() | The raw bot device keypair. |

koro.request(method, path, body?)

Low-level escape hatch for any endpoint not yet wrapped:

const calls = await koro.request('GET', '/calls');

Error handling

Any non-2xx response throws a KoroError:

import { KoroError } from 'koro-sdk';

try {
  await koro.messages.send(convId, 'hi');
} catch (err) {
  if (err instanceof KoroError) {
    console.error(err.status, err.message); // e.g. 403 "Missing scope: messages:write"
    if (err.isScopeError) {
      // 403 — your API key is missing a required scope
    }
  }
}

KoroError carries status, message (from the API's { error } body), body (raw parsed response), and request ({ method, path }).

Examples

Run with tsx:

KORO_TOKEN=koro_live_… npx tsx examples/echo-bot.ts <conversationId>

License

MIT