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

@snapie/hangouts-core

v0.14.2

Published

Framework-agnostic core SDK for [Hive Hangouts](https://hangout.3speak.tv) — Twitter Spaces-style audio rooms for the Hive blockchain.

Readme

@snapie/hangouts-core

Framework-agnostic core SDK for Hive Hangouts — Twitter Spaces-style audio rooms for the Hive blockchain.

Install

npm install @snapie/hangouts-core

What's included

  • HangoutsApiClient — typed HTTP client for all Hangouts API endpoints
  • Auth helpersloginWithKeychain, loginWithAioha, loginWithSignFn
  • AiohaLike interface — plug in any signing/transfer backend (Aioha, custodial, etc.)
  • TypeScript types — all request/response shapes exported from the package root

When to use this package

  • React Native apps (pair with @livekit/react-native for audio)
  • Non-React web apps (Vue, Svelte, vanilla JS)
  • Server-side scripts that need to interact with the Hangouts API

For React web apps, use @snapie/hangouts-react instead — it includes this package plus React hooks and UI components.


Quick start

import { HangoutsApiClient, loginWithKeychain } from '@snapie/hangouts-core';

const client = new HangoutsApiClient({ baseUrl: 'https://hangout-api.3speak.tv' });

// Authenticate with Hive Keychain (browser only)
const session = await loginWithKeychain(client, 'your-hive-username');
// session = { token: string, username: string }

// List rooms
const rooms = await client.listRooms();

// Create a room
const { room, token } = await client.createRoom('My Hangout', 'A description', undefined, 'public', 'en', {
  enabled: true,
  minBoostUsd: 1,
  creatorPayoutAccount: 'alice',
});

// Join
const join = await client.joinRoom('room-name');
// join.isHost, join.isGuest, join.isPremium

// Guest listen (no Hive account required)
const guest = await client.listenAsGuest('room-name', 'Optional Display Name');

Authentication

Hive Keychain (browser extension)

import { loginWithKeychain, isKeychainAvailable } from '@snapie/hangouts-core';

if (isKeychainAvailable()) {
  const session = await loginWithKeychain(client, 'alice');
}

Aioha (any registered provider)

import { loginWithAioha } from '@snapie/hangouts-core';

// aioha must already have a logged-in session (call aioha.login() first)
const session = await loginWithAioha(client, aioha);
// or pass the username explicitly:
const session = await loginWithAioha(client, aioha, 'alice');

Custom sign function

For any other signing flow — HiveSigner redirect, server-held keys, test mocks:

import { loginWithSignFn } from '@snapie/hangouts-core';

const session = await loginWithSignFn(client, 'alice', async (message) => {
  // sign `message` with alice's posting key, return the hex signature
  return mySigningBackend.sign(message);
});

Custodial adapter (Google login / server-held keys)

If users authenticate via Google (or any non-Hive flow) and a backend holds their Hive keys, implement the AiohaLike interface:

import type { AiohaLike } from '@snapie/hangouts-core';

const custodialAdapter: AiohaLike = {
  getCurrentUser: () => username,
  isLoggedIn: () => true,

  signMessage: async (message) => {
    const res = await fetch('/api/custodial/sign', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, message }),
    });
    const { signature } = await res.json();
    return { success: true, result: signature };
  },

  transfer: async (to, amount, currency, memo) => {
    const res = await fetch('/api/custodial/transfer', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ from: username, to, amount, currency, memo }),
    });
    return res.ok
      ? { success: true }
      : { success: false, error: await res.text() };
  },
};

// In React: <HangoutsProvider aioha={custodialAdapter} ...>
// Standalone: await loginWithAioha(client, custodialAdapter, username);

The transfer method is only called when a user sends a boost — you can omit it if your app doesn't use boosts.


AiohaLike interface

interface AiohaLike {
  /** Sign a message with the user's posting key. */
  signMessage(
    message: string,
    keyType: string,
  ): Promise<{ success: boolean; result?: string; error?: string }>;

  /** Return the currently logged-in username (optional). */
  getCurrentUser?(): string | undefined | null;

  /** True when the user has an active session (optional). */
  isLoggedIn?(): boolean;

  /** Broadcast a Hive transfer (optional — only needed for boosts). */
  transfer?(
    to: string,
    amount: number,
    currency: string,
    memo: string,
  ): Promise<{ success: boolean; result?: unknown; error?: string }>;
}

Error handling

All HangoutsApiClient methods throw HangoutsApiError on non-2xx responses:

import { HangoutsApiError } from '@snapie/hangouts-core';

try {
  await client.joinRoom('some-room');
} catch (err) {
  if (err instanceof HangoutsApiError) {
    console.log(err.status);   // HTTP status code (400, 401, 403, 404, 429…)
    console.log(err.message);  // server error message
    console.log(err.body);     // raw response body (unknown shape)
  }
}

Common status codes:

| Status | Meaning | |--------|---------| | 401 | Missing or expired session token | | 403 | Action requires host privileges, or guest identity rejected | | 404 | Room not found | | 409 | Room name conflict on create | | 429 | Rate limit hit (guest token endpoint: 10 / 5 min per IP) |


TypeScript types

// Auth
type AuthSession         = { token: string; username: string }
type ChallengeResponse   = { challenge: string; expires: number }

// Rooms
type RoomVisibility      = 'public' | 'hive-internal' | 'unlisted'

interface Room {
  name: string
  title: string
  host: string
  description?: string
  backgroundImage?: string
  numParticipants?: number
  createdAt: string
  origin?: string          // hostname that created the room (e.g. "3speak.tv")
  visibility?: RoomVisibility
  language?: string        // BCP-47
  boost?: BoostConfig
}

interface BoostConfig {
  enabled: boolean
  minBoostUsd: number
  creatorPayoutAccount?: string
}

// Join responses
interface JoinRoomResponse {
  token: string
  roomName: string
  identity: string
  isHost: boolean
  isPremium?: boolean
  isGuest?: boolean        // true for guest-* identities — listen-only
}

// Participants
type ParticipantRole = 'host' | 'speaker' | 'listener'

// Recording
type RecordingMode   = 'audio' | 'video'
type RecordingLayout = 'speaker' | 'grid' | 'single'

interface RecordingFileResult {
  blob: Blob
  filename: string
  duration: number   // seconds
  size: number       // bytes
}

// Boosts
interface BoostEvent {
  type: 'boost'
  id: string
  room: string
  sender: string
  displayName?: string
  message: string
  amount: string         // e.g. "5.000"
  asset: 'HIVE' | 'HBD'
  usdAmount: number
  feeAmount: string
  payoutAmount: string
  recipient: string
  txId: string
  blockNum: number
  timestamp: number
  belowMinimum?: boolean // server flag: below host's minBoostUsd; overlay suppressed
}

type BoostRejectReason =
  | 'invalid_memo'
  | 'invalid_asset'
  | 'room_not_found'
  | 'below_minimum'
  | 'invalid_destination'
  | 'duplicate_transfer'
  | 'payout_failed'
  | 'internal_error'

// Streaming
type StreamPlatform = 'youtube' | 'twitch'

Game results

HangoutsRoom's onGameEnd prop (in @snapie/hangouts-react) fires once per finished game with a GameResultPayload. Cast result based on gameId:

interface GameResultPayload {
  gameId: string          // 'chess' | 'fast-draw' | 'word-guess'
  players: string[]
  startedAt: number
  endedAt: number
  duration: number         // seconds
  result: unknown          // cast based on gameId
}

interface ChessGameResult {
  fen: string
  players: { w: string; b: string }
  turn: 'w' | 'b' | null
  status: 'playing' | 'checkmate' | 'resigned' | 'draw' | 'stalemate'
  winner: string | null
  moveHistory: string[]    // SAN notation
}

interface FastDrawGameResult {
  phase: 'drawing' | 'reveal' | 'game_over'
  theme: string
  winners: string[]
  scores: Record<string, number>
  roundNumber: number
  drawer: string
  revealedWord: string | null
}

interface WordGuessGameResult {
  theme: string
  playerCount: number
  words: Record<string, string>                  // full reveal, every player
  leaderboard: WordGuessLeaderboardEntry[]        // finishers, in finish order
}

interface WordGuessLeaderboardEntry {
  identity: string
  place: number
  word: string
  solveTimeMs: number
  wrongAttempts: number
}

Three formatter helpers turn a result into ready-to-post text (e.g. for a Hive snap recapping the game — posting itself is up to the integrator, this package only formats):

buildLichessAnalysisUrl(moveHistory: string[]): string
// → "https://lichess.org/analysis/pgn/e4_e5_Nf3_..." — opens the full game
//   in Lichess's analysis board, no API call or auth required.

formatWordGuessRecap(result: WordGuessGameResult): string
// → a ranked leaderboard + full word reveal, e.g.:
//   "🏁 Animals Word Guess Race!\n🥇 alice — 8.2s\n🥈 bob — 14.5s (3 tries)\n...\n🔍 Reveal: alice=GIRAFFE, ..."

formatFastDrawRecap(result: FastDrawGameResult): string
// → final scoreboard + winner(s), e.g.:
//   "🎨 Animals Fast Draw — 9 rounds!\n🏆 alice wins with 5 points!\n🥈 bob — 3\n🥉 carol — 2"

API client methods

// Auth
client.requestChallenge(username)                            → ChallengeResponse
client.verifySignature(username, challenge, signature)       → AuthSession

// Rooms
client.listRooms()                                           → Room[]
client.getRoom(roomName)                                     → Room | null
client.createRoom(title, description?, bgImage?, visibility?, language?, boost?)  → CreateRoomResponse
client.joinRoom(roomName)                                    → JoinRoomResponse
client.listenAsGuest(roomName, displayName?)                 → JoinRoomResponse
client.joinAsObserver(roomName)                              → JoinRoomResponse  // obs-* identity, invisible
client.deleteRoom(roomName)                                  → void
client.transferHost(roomName, newHostUsername)               → { host: string }
client.setRoomLayout(roomName, layout)                       → { layout: string }

// Participants
client.setPermissions(roomName, identity, canPublish)        → { identity, canPublish }
client.kickParticipant(roomName, identity)                   → void
client.banGuest(roomName, identity)                          → void  // guest-* only, IP-scoped

// Recording
client.startRecording(roomName, { mode?, layout? })          → RecordingStartResponse
client.stopRecording(roomName)                               → RecordingStopResponse
client.getRecordingStatus(roomName)                          → RecordingStatusResponse
client.setRecordingLayout(roomName, layout)                  → RecordingLayoutResponse
client.fetchRecordingFile(roomName, downloadToken)           → RecordingFileResult

// Streaming
client.startStream(roomName, platform, streamKey, bgImageUrl?, videoEnabled?)  → StreamStartResponse
client.stopStream(roomName)                                  → StreamStopResponse
client.getStreamStatus(roomName)                             → StreamStatusResponse

// Boosts
client.getBoostConfig()                                      → { enabled, platformAccount, feePercent }
client.updateBoostConfig(roomName, { enabled?, minBoostUsd?, creatorPayoutAccount? })

Boost memo format

Transfers to the platform account must include a JSON memo:

{
  "version": 1,
  "room": "alice-my-openpod-abc123",
  "message": "Great show!",
  "sender": "bob",
  "nonce": "1717000000000-abc123",
  "displayName": "Bob"
}

version, room, message, sender, nonce are required. The server validates the transfer on-chain, deduplicates by nonce, splits the payout, and broadcasts a BoostEvent to the room's LiveKit data channel on topic boost.


Docs

Full documentation: hangout.3speak.tv/docs

License

MIT