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

@prvt/integration-sdk

v0.8.0

Published

TypeScript SDK for building visible Node.js integrations for prvt rooms.

Readme

@prvt/integration-sdk

TypeScript SDK for building visible Node.js integrations for a self-hosted prvt room.

The SDK exchanges a room-scoped integration key for a short-lived participant token and can connect the integration through the official LiveKit Node SDK. It does not persist credentials, retry mutations, or hide integration participants.

Integration manifest

Approved providers use the same public manifest shape everywhere they appear:

import type { IntegrationManifest } from "@prvt/integration-sdk";

const manifest: IntegrationManifest = {
  title: "Music bot",
  description: "Play tracks from YouTube and SoundCloud, controlled from room chat.",
  imageUrl: "https://bot.example.com/icon.png",
  websiteUrl: "https://bot.example.com/",
  permissions: {
    subscribeMedia: false,
    publishMicrophone: true,
    publishData: true,
    publishTranscription: false,
    showAsParticipant: true,
  },
};

The server-side approved manifest is authoritative: the browser never trusts a provider-supplied permission claim. imageUrl may be null; non-null images and websites must use HTTPS without embedded credentials. The permission booleans are the exact grants used when a room moderator creates the provider's one-time integration key.

Install

npm install @prvt/integration-sdk @livekit/rtc-node

Node.js 18 or newer is required. @livekit/rtc-node is a peer dependency so the application controls the native realtime runtime version.

Coding agents can start from the reusable AGENT_PROMPT.md, which summarizes the supported workflow, security boundaries, permissions, and completion checks.

Create an integration key

Join a room in the browser that created it, open Room tools, and choose Copy integrations API token. The Personal integration key is shown only once. Move it directly into the Node application's secret manager before closing the dialog.

Never put the integration key in a URL, log, source file, or browser storage.

Connect

import { AudioStream, RoomEvent, VideoStream } from "@livekit/rtc-node";
import { createPrvtIntegration } from "@prvt/integration-sdk";

const integrationKey = process.env.PRVT_INTEGRATION_KEY;
if (!integrationKey) throw new Error("PRVT_INTEGRATION_KEY is required");

const integration = createPrvtIntegration({
  baseUrl: "https://call.example.com",
  integrationKey,
  displayName: "Transcript bot",
});

const session = await integration.connect({
  roomOptions: {
    autoSubscribe: true,
    dynacast: false,
  },
});

session.room.on(RoomEvent.ParticipantConnected, (participant) => {
  console.log(`${participant.name ?? participant.identity} joined`);
});

const stopAudio = session.onAudioTrack(async (track, publication, participant) => {
  const stream = new AudioStream(track, { sampleRate: 48_000, numChannels: 1 });
  const reader = stream.getReader();
  try {
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      // Send value.data (Int16 PCM) to your audio pipeline.
      console.log("audio", participant.identity, publication.source, value.data.length);
    }
  } finally {
    await reader.cancel().catch(() => undefined);
  }
});

const stopVideo = session.onVideoTrack(async (track, publication, participant) => {
  const stream = new VideoStream(track);
  const reader = stream.getReader();
  try {
    for (;;) {
      const { done, value } = await reader.read();
      if (done) break;
      // Process value.frame using your video pipeline.
      console.log("video", participant.identity, publication.source, value.frame.width, value.frame.height);
    }
  } finally {
    await reader.cancel().catch(() => undefined);
  }
});

process.once("SIGINT", async () => {
  stopAudio();
  stopVideo();
  await session.disconnect();
  process.exit(0);
});

session.room is the official @livekit/rtc-node Room. Use its track, participant, text-stream, and data APIs directly. The integration's stable identity, expiry, and effective permissions are also available on the session. onAudioTrack() and onVideoTrack() are permission-gated convenience listeners for subscribed remote tracks. They include the LiveKit publication and participant, replay tracks that were already subscribed during connection, and return an unsubscribe function. AudioStream yields PCM AudioFrame values; VideoStream yields VideoFrame events. Cancel each stream reader when the pipeline stops. Calling either listener without subscribeMedia throws PrvtMediaSubscriptionUnavailableError. prvt-specific profile, pin, and reaction helpers are described below.

Exchange without connecting

Use exchangeToken() when the application needs to configure the LiveKit room itself:

const token = await integration.exchangeToken();

// token.participantToken  short-lived bearer JWT
// token.serverUrl         public LiveKit WebSocket URL
// token.participantIdentity
// token.expiresAt
// token.permissions

The participant JWT is valid for five minutes and exactly one managed room. Fetch a fresh token before a full reconnect instead of persisting it as a replacement for the integration key. displayName is optional; when omitted, the name chosen when the integration key was created is used. When supplied, the server normalizes and validates it for that exchange without changing the owner-facing integration record. The effective name is available on the token and on session.displayName.

Reconnect after disconnect or moderator resume

connect() performs one token exchange and one LiveKit connection; the SDK does not retry it automatically. A long-running integration should wait for the LiveKit Disconnected event, then call connect() again so the server issues a fresh participant JWT. This also handles a moderator resuming a paused integration: the next exchange succeeds, but only after the moderator has cleared the pause.

import { RoomEvent } from "@livekit/rtc-node";
import {
  PrvtApiError,
  PrvtConnectionError,
  PrvtNetworkError,
} from "@prvt/integration-sdk";

const wait = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const waitForDisconnect = (session: Awaited<ReturnType<typeof integration.connect>>) => (
  new Promise<void>((resolve) => {
    const onDisconnected = () => {
      session.room.off(RoomEvent.Disconnected, onDisconnected);
      resolve();
    };
    session.room.on(RoomEvent.Disconnected, onDisconnected);
  })
);

let stopping = false;
let activeSession: Awaited<ReturnType<typeof integration.connect>> | undefined;
const stop = () => {
  stopping = true;
  void activeSession?.disconnect();
};
process.once("SIGINT", stop);
process.once("SIGTERM", stop);

while (!stopping) {
  try {
    activeSession = await integration.connect({ roomOptions: { autoSubscribe: true } });
    // Register listeners and reapply live-only profile state here.
    await waitForDisconnect(activeSession);
  } catch (error) {
    if (error instanceof PrvtApiError && error.code === "INTEGRATION_PAUSED") {
      await wait(5_000);
      continue;
    }
    if (error instanceof PrvtApiError && ["INTEGRATION_REVOKED", "ROOM_CLOSED", "UNAUTHORIZED"].includes(error.code)) {
      throw error;
    }
    if (!(error instanceof PrvtConnectionError) && !(error instanceof PrvtNetworkError)) throw error;
    await wait(5_000);
  } finally {
    await activeSession?.disconnect().catch(() => undefined);
    activeSession = undefined;
  }
}

Treat INTEGRATION_PAUSED as an expected operator-controlled state rather than an invalid key. Use bounded backoff so a paused integration does not busy-loop; once the moderator resumes it, the next fresh exchange reconnects it.

Register an integration-owned V2 lifecycle webhook

An integration can own one active V2 webhook subscription. Generate a separate webhook key and secret, store the secret encrypted on the receiving server, and only then register the endpoint. generatePrvtWebhookCredentials() returns the raw secret once to your process. The registration response deliberately omits it, so losing the secret requires rotation with the current revision.

import {
  createPrvtIntegration,
  generatePrvtWebhookCredentials,
} from "@prvt/integration-sdk";

const integration = createPrvtIntegration({
  baseUrl: "https://call.example.com",
  integrationKey: process.env.PRVT_INTEGRATION_KEY!,
});
const credentials = generatePrvtWebhookCredentials();

// This must be encrypted server storage. It must be available before PUT,
// because prvt challenges the endpoint before activating the subscription.
await encryptedWebhookSecrets.store(credentials.keyId, credentials.secret);

const subscription = await integration.registerWebhookSubscription({
  endpointUrl: "https://bot.example.com/prvt-webhook",
  events: [
    "integration.paused",
    "integration.resumed",
    "integration.revoked",
    "integration.permissions_changed",
    "room.closed",
  ],
  credentials,
});

// subscription contains metadata and a revision, never the raw secret.
await webhookMetadata.store(subscription);

The generated secret uses the prvt_webhook_v1. credential-format prefix. That credential version is independent of the V2 event and v2= signature protocol. Fixed-provider V1 secrets have the same textual format, but they are separate credentials and must never be reused or copied between configurations.

Registration sends the integration key only as the private Bearer credential. The webhook secret appears only in the HTTPS JSON body of registration or rotation. getWebhookSubscription(), deleteWebhookSubscription(), normal token/profile calls, metadata responses, and webhook deliveries never carry the raw secret. Do not log either credential or keep the webhook secret in plaintext storage.

Rotate credentials with an optimistic revision check. Keep both encrypted keys available while an older signed delivery may still be in flight:

const current = await integration.getWebhookSubscription();
const nextCredentials = generatePrvtWebhookCredentials();
await encryptedWebhookSecrets.store(nextCredentials.keyId, nextCredentials.secret);

const rotated = await integration.registerWebhookSubscription({
  endpointUrl: current.endpointUrl,
  events: current.events,
  credentials: nextCredentials,
  expectedRevision: current.revision,
});
await webhookMetadata.store(rotated);

Keep the previous secret available for at least 24 hours so already-enqueued deliveries pinned to the older revision can still verify. To disable future delivery entirely, fetch the current revision and delete it explicitly:

const current = await integration.getWebhookSubscription();
await integration.deleteWebhookSubscription({ expectedRevision: current.revision });

The receiving endpoint must preserve the exact raw bytes and select the secret by the signed key ID. A V2 verification challenge requires an exact proof response:

import {
  PrvtWebhookVerificationError,
  createPrvtWebhookV2ChallengeResponse,
  verifyPrvtWebhookV2,
  type PrvtWebhookReplayAdapter,
} from "@prvt/integration-sdk";

const replay: PrvtWebhookReplayAdapter = {
  async claim(eventId, expiresAt) {
    return webhookEvents.claimOnce(eventId, expiresAt);
  },
};

export async function handleWebhook(request: Request): Promise<Response> {
  const rawBody = await request.arrayBuffer();
  try {
    let selectedSecret: string | undefined;
    const event = await verifyPrvtWebhookV2(
      request.headers,
      rawBody,
      async (keyId) => {
        selectedSecret = await encryptedWebhookSecrets.load(keyId);
        return selectedSecret;
      },
      { replay },
    );

    if (event.type === "verification.challenge") {
      if (!selectedSecret) return new Response("Invalid webhook", { status: 400 });
      return Response.json(
        await createPrvtWebhookV2ChallengeResponse(event, selectedSecret),
      );
    }

    // Persist both IDs from a verified challenge. For later lifecycle events,
    // pass them as verifier options to bind this receiver before replay claim.
    await wakeIntegrationReconciler(event.integration.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof PrvtWebhookVerificationError) {
      if (error.code === "replay") return new Response(null, { status: 204 });
      return new Response("Invalid webhook", { status: 400 });
    }
    throw error;
  }
}

V2 events have no provider field. Verification requires the canonical key ID, the v2= raw-body HMAC, an exact challenge or lifecycle schema, and a timestamp within five minutes by default. Use an atomic replay adapter with the default 24-hour retention. Lifecycle deliveries are unordered, at-least-once advisory wake-ups: coalesce them and reconcile with a fresh token exchange rather than treating event status or permissions as current authority.

Receive deployment-managed V1 lifecycle webhooks

An approved provider can receive deployment-managed V1 signed lifecycle events for integrations that were created from its manifest. Verify the exact raw request body before JSON parsing. Body parsers that decode and then reserialize JSON change the signed bytes and must not run before verifyPrvtWebhook().

import {
  PrvtWebhookVerificationError,
  verifyPrvtWebhook,
  type PrvtWebhookReplayAdapter,
} from "@prvt/integration-sdk";

const webhookSecret = process.env.PRVT_WEBHOOK_SECRET;
if (!webhookSecret) throw new Error("PRVT_WEBHOOK_SECRET is required");

// Back this with an atomic INSERT-if-absent in shared, durable storage.
const replay: PrvtWebhookReplayAdapter = {
  async claim(eventId, expiresAt) {
    return webhookEvents.claimOnce(eventId, expiresAt);
  },
};

export async function handleWebhook(request: Request): Promise<Response> {
  const rawBody = await request.arrayBuffer();

  try {
    const event = await verifyPrvtWebhook(
      request.headers,
      rawBody,
      webhookSecret,
      {
        keyId: "whk_primary",
        providerId: "music-bot",
        replay,
      },
    );

    // Every event type follows the same path. This durable, coalescing wake-up
    // exchanges the integration key again; the fresh API result decides whether
    // the integration is active, paused, revoked, or belongs to a closed room.
    await wakeIntegrationReconciler(event.integration.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof PrvtWebhookVerificationError) {
      // A valid duplicate has already caused (or queued) reconciliation. Ack it
      // without running the handler's effects again.
      if (error.code === "replay") return new Response(null, { status: 204 });
      return new Response("Invalid webhook", { status: 400 });
    }
    throw error;
  }
}

The V1 verifyPrvtWebhook() API and event schema remain unchanged. verifyPrvtWebhook() accepts Headers or a case-insensitive header record and a raw string, Uint8Array, or ArrayBuffer. It requires all four prvt-webhook-id, prvt-webhook-timestamp, prvt-webhook-key-id, and prvt-webhook-signature headers, checks the v1= HMAC-SHA256 signature with Web Crypto, enforces a five-minute timestamp tolerance by default, rejects bodies over 16 KiB, and then parses the exact V1 event shape. Pass the expected keyId during secret rotation, the endpoint's expected approved providerId, and an atomic replay adapter in production. The provider binding is checked before replay state is claimed. Replay claims expire 24 hours after receipt by default, independently of the five-minute signature tolerance, because a sender retry uses the same event ID with a fresh delivery timestamp. replayRetentionSeconds accepts positive whole seconds up to 30 days. Verification errors expose only the sanitized signature, timestamp, replay, or protocol code.

Webhook delivery is unordered and at least once. Treat every event as a prompt to reconcile, not permission to reuse an old participant JWT or apply the event's possibly stale status directly. A reconciler should coalesce wake-ups, exchange the long-lived integration key again, and treat the fresh API result and connected session permissions as authoritative. A handler that keeps an ordered event cursor may ignore an event whose createdAt is older than the last applied mutation; reconciling current API state is safer when ordering is uncertain. A valid duplicate should normally receive a successful response without repeating side effects. Keep bounded reconnect backoff as a fallback for delayed or missed webhook delivery.

Permissions

The server returns the effective permissions with every exchange:

  • subscribeMedia allows room-wide media subscription, including both audio and video.
  • publishMicrophone allows publication restricted to microphone-source tracks.
  • publishData allows room-wide data publication. Topics such as prvt.chat route messages but are not authorization boundaries.
  • publishTranscription allows the integration to stream bounded subtitle segments attached to a participant and audio track. It does not grant the chat helpers.
  • showAsParticipant controls whether the web client renders the integration as a participant with a profile card. It does not change LiveKit membership, the integration count, or the integration's permitted realtime streams.

PRVT_CHAT_TOPIC exports the exact prvt.chat topic name.

Participant subtitles

Use createTranscriptionStream() for live transcription or translation. The stream is broadcast to the room on prvt.transcription; the SDK adds the target participant, audio track, segment ID, language, and partial/final state as validated attributes:

const subtitle = await session.createTranscriptionStream({
  participantIdentity: "ada-lovelace-12345678",
  trackSid: "TR_audio_123",
  segmentId: "segment_42",
  language: "ru",
  final: false,
});
await subtitle.write("Привет, ");
await subtitle.write("как дела?");
await subtitle.close();

Create another stream with the same segmentId to replace a partial segment with its final text. The browser displays the newest active segment for each target participant as ephemeral subtitles. This method requires publishTranscription; missing permission throws PrvtTranscriptionPublishingUnavailableError.

When receiving media, pass roomOptions.autoSubscribe: true to connect(). Use onAudioTrack() for microphone audio and onVideoTrack() for camera or screen-share video; inspect the supplied publication's source when the pipeline needs to distinguish them. A track callback may run more than once over a reconnect, so media pipelines should be idempotent and release their stream readers when they finish.

Message language context

Human messages from the prvt web app carry the sender's selected UI language (en or ru) in the text-stream attribute prvt.chat.language. Read it from the LiveKit stream metadata:

import { parseChatMessageLanguage } from "@prvt/integration-sdk";

// Inside your LiveKit text-stream handler:
const language = parseChatMessageLanguage(reader.info.attributes);

PRVT_CHAT_LANGUAGE_ATTRIBUTE also exports the attribute name. The helper returns null for missing or unknown values, including messages from older clients. Choose your integration's fallback when no preference is available. The value describes the sender's UI at send time, not the detected language of the text. It can guide localized replies; it does not translate messages or change a participant's interface automatically.

Markdown and callback keyboards

Integrations can send bounded Markdown, native callback buttons, HTTPS link buttons, and embedded web-app buttons. Callback buttons may include an optional toast value that is returned with the button event and shown to the clicking participant as a temporary chat alert. Raw HTML, images, unsafe links, oversized labels, and oversized keyboards are rejected by the SDK or ignored by the web client.

const message = await session.sendMessage("**Choose a track**", {
  format: "markdown",
  keyboard: {
    v: 1,
    rows: [[
      { id: "play", label: "Play", toast: "play now", style: "primary" },
      { id: "pause", label: "Pause" },
      { app: "https://music.example/queue", label: "Queue" },
    ]],
  },
});

const stopListening = session.onChatCallback(async (callback) => {
  if (callback.buttonId !== "play") {
    await session.acknowledgeChatCallback(callback, { status: "rejected", text: "Unavailable", alert: true });
    return;
  }
  console.log(callback.toast);
  await session.acknowledgeChatCallback(callback, { status: "accepted", text: "Starting" });
  await session.sendMessage("Started playing.");
});

The browser sends callback packets on PRVT_CHAT_CALLBACK_TOPIC directly to the integration that authored the message. The SDK derives senderIdentity from LiveKit's authenticated DataReceived event. Acknowledgements use PRVT_CHAT_CALLBACK_ACK_TOPIC and are targeted to that participant. Call the unsubscribe function during shutdown. The transport is realtime and best-effort, so actions should be idempotent and no callback should be treated as persisted or replayable.

Embedded web apps

Use the app button field for a path-only HTTPS app URL owned by the connected integration:

const message = await session.sendMessage("Choose a track", {
  keyboard: { v: 1, rows: [[{ app: "https://music.example/queue", label: "Queue" }]] },
});

const stopListening = session.onWebAppRequest(async (request) => {
  if (request.action !== "queue.add") return;
  await queue.add(request.payload);
  await session.respondToWebAppRequest(request, { ok: true, result: { queued: true } });
});

The browser app uses the separate @prvt/mini-app-sdk package:

import { createPrvtMiniApp } from "@prvt/mini-app-sdk";

const app = createPrvtMiniApp({ hostOrigin: "https://call.example.com" });
await app.ready();
await app.request("queue.add", { trackId: "track_123" });

The app receives no room credentials or integration key. senderIdentity is derived from LiveKit by the Node SDK, not accepted from the browser payload. App actions are bounded, targeted, realtime-only, and should be validated and idempotent by the integration.

Editing chat messages

editMessage() replaces an integration-authored chat message, including its Markdown format and keyboard, for up to 60 minutes after sendMessage() returns. Omit keyboard to preserve it, provide a new keyboard to replace it, or pass keyboard: null to remove it. The SDK remembers sent message IDs for the current live session and publishes edits on PRVT_CHAT_EDITS_TOPIC:

const message = await session.sendMessage("Preparing the results…");
await session.editMessage(message.id, "Results are ready", {
  format: "markdown",
  keyboard: {
    v: 1,
    rows: [[{ id: "details", label: "Show details" }]],
  },
});

Edits are realtime-only and update the pinned preview when that message is pinned. A new SDK connection cannot edit messages sent by an earlier session.

Profile picture and status

Update the connected bot's live profile attributes through the authenticated prvt API. A status can hold changing context such as a music bot's current song:

await session.updateProfile({
  profilePicture: "data:image/webp;base64,<base64-webp-bytes>",
  statusMessage: "Playing Midnight City",
  websiteUrl: "https://music.example.com/",
  webAppUrl: "https://music.example.com/room-app",
});

await session.updateProfile({ statusMessage: "Playing Intro" });
await session.updateProfile({ statusMessage: null });
await session.updateProfile({ websiteUrl: null });
await session.updateProfile({ webAppUrl: null });

Pictures must be PNG, JPEG, or WebP data URLs no longer than 12,000 characters. Remote URLs and SVG are rejected so participant browsers never contact a bot-selected image host. Status messages are normalized to one line and limited to 128 characters. null clears a field. Updates affect only the active LiveKit participant and are not stored in the managed-room database, so reapply the profile after reconnecting. Updating while disconnected returns INTEGRATION_NOT_CONNECTED. The API accepts 60 profile updates per integration per minute and returns the ordinary RATE_LIMITED error plus Retry-After after that bound.

The optional websiteUrl must be HTTPS and contain no embedded credentials. It links the integration's displayed name in chat and on its media card. The optional webAppUrl must be HTTPS, contain no embedded credentials, query, or fragment, and be no longer than 512 characters. It adds a small room-app button before the integration's name on its participant card and opens the embedded room-app modal used by chat app buttons. Omitting either field preserves its current value; null clears it. Both fields are live-only, like the other profile fields, so reapply them after reconnecting. A profile website or app does not imply marketplace approval. Never put integration keys, room credentials, or participant tokens in either URL.

Chat pins

With publishData, an integration can pin and unpin a message by the LiveKit chat message ID:

const message = await session.sendMessage("Voting closes in two minutes");
await session.pinMessage(message.id);
await session.unpinMessage(message.id);

sendMessage() sends the normalized text on PRVT_CHAT_TOPIC and returns its stream ID. Pins use versioned reliable packets on PRVT_CHAT_PINS_TOPIC. An unpin clears only the named current pin, so a delayed unpin cannot remove a newer one. Pins are ephemeral like chat: there is no persistence, replay, or late-join history.

Bot reactions and rate limit

With publishData, send any emoji in PRVT_REACTION_EMOJI:

await session.sendReaction("👏");

The catalog includes 😶 and 👎. It retains 🎉 for version-1 packet compatibility with existing integrations, though 🎉 is no longer offered in the human picker.

Each SDK session accepts at most three reactions per rolling ten seconds. A fourth throws PrvtReactionRateLimitError and exposes retryAfterMs. The web client enforces the same per-integration display limit even when a bot publishes directly through LiveKit. Reactions use lossy packets on PRVT_REACTIONS_TOPIC.

Personal integrations created from Room tools currently receive all five permissions. The owner API can create integrations with selected permissions.

Errors

import {
  PrvtApiError,
  PrvtConnectionError,
  PrvtDataPublishingUnavailableError,
  PrvtTranscriptionPublishingUnavailableError,
  PrvtNetworkError,
  PrvtProtocolError,
  PrvtReactionRateLimitError,
} from "@prvt/integration-sdk";

try {
  await integration.connect();
} catch (error) {
  if (error instanceof PrvtApiError) {
    console.error(error.code, error.status, error.retryAfterSeconds);
  } else if (error instanceof PrvtNetworkError) {
    console.error("The prvt API is unreachable");
  } else if (error instanceof PrvtProtocolError) {
    console.error("The API response did not match the SDK contract");
  } else if (error instanceof PrvtConnectionError) {
    console.error("LiveKit connection failed");
  } else if (error instanceof PrvtReactionRateLimitError) {
    console.error(`Retry the reaction in ${error.retryAfterMs} ms`);
  } else if (error instanceof PrvtDataPublishingUnavailableError) {
    console.error("This integration cannot publish room data");
  }
}

The SDK does not include integration keys, participant JWTs, request headers, or response bodies in its own error messages. It does not retry automatically. Respect retryAfterSeconds for RATE_LIMITED; use bounded backoff for temporary STORAGE_UNAVAILABLE or LIVEKIT_UNAVAILABLE failures, and for the operator-controlled INTEGRATION_PAUSED state; require operator action for revoked, closed, or unauthorized credentials.

Visibility and lifecycle

Integrations are ordinary LiveKit room participants. The server-signed showAsParticipant permission controls whether prvt renders their participant card; it does not change room membership or the separate integration count. Visible integrations use server-signed metadata to drive their badge, roster entry, join announcement, and chat attribution. A new connection with the same stable integration identity replaces the older connection.

Revoking an integration blocks future token exchanges and attempts to remove the connected bot. An already issued JWT cannot be invalidated and may reconnect for the remainder of its five-minute lifetime. Always disconnect the session cleanly during shutdown.

This package intentionally does not wrap the owner API or browser moderator flow. It is the runtime SDK for a Node.js integration after an integration key has been created.