@30k/starcomm-client
v0.6.0
Published
TypeScript API client for Star Comms shard Owner API
Maintainers
Readme
@30k/starcomm-client
TypeScript client for the Star Comms shard Owner API. Zero dependencies, uses native fetch (Node 18+, Bun, Deno, browsers).
Install
pnpm add @30k/starcomm-clientUsage
import { StarCommsClient } from "@30k/starcomm-client";
const client = new StarCommsClient({
baseUrl: "http://your-shard:25588",
ownerApiKey: "scok_your_key_here",
});
const status = await client.status.get();
await client.assignments.assign("discord_user_id", 1);
await client.comms.sendAcars("Fleet departing!");Docs
Full API documentation: 30k-inc.github.io/starcomm-client
Config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| baseUrl | string | — | Shard URL (no trailing slash) |
| ownerApiKey | string | — | Owner key (scok_...) |
| serviceKey | string? | — | Service key (scsk_...) for audio listening |
| shardToken | string? | — | Shard token for debug endpoint |
| timeoutMs | number? | 10000 | Request timeout |
| connectTimeoutMs | number? | 30000 | Connection timeout for streaming endpoints |
| fetch | typeof fetch? | globalThis.fetch | Custom fetch impl |
Audio Listener
Listen-only voice connection that receives Opus audio frames and PTT events from shard nets. Requires a service key (scsk_...) obtained from the shard API key management.
const client = new StarCommsClient({
baseUrl: "http://your-shard:25588",
ownerApiKey: "scok_...",
serviceKey: "scsk_...",
});
// Event handlers
client.audio.on("ptt.start", (evt) => {
console.log(`${evt.displayName} keyed up on net ${evt.netId}`);
});
client.audio.on("ptt.stop", (evt) => {
console.log(`${evt.displayName} stopped on net ${evt.netId}`);
});
client.audio.on("audio", (frame) => {
// frame.netId — which net
// frame.userId — who's transmitting
// frame.opus — raw Opus data (Uint8Array)
});
// Lifecycle events
client.audio.on("connected", ({ guildId }) => console.log("Connected to", guildId));
client.audio.on("disconnected", ({ reason }) => console.warn("Lost:", reason));
client.audio.on("reconnected", ({ attempt }) => console.log("Back after", attempt, "tries"));
client.audio.on("error", ({ reason }) => console.error(reason));
// Connect and listen on specific nets
await client.audio.listen({
guildId: "your_guild_id",
nets: [1, 2, 3],
});
// Dynamic net management (no reconnect needed)
await client.audio.addNet(4);
await client.audio.removeNet(1);
client.audio.nets; // [2, 3, 4]
// Stop and cleanup
await client.audio.stop();Audio Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| guildId | string | — | Discord guild ID |
| nets | number[]? | [] | Initial nets to listen on |
| displayName | string? | "" | Name shown in shard roster |
| autoReconnect | boolean? | true | Reconnect on connection loss |
| initialDelayMs | number? | 2000 | First reconnect delay |
| maxDelayMs | number? | 30000 | Max reconnect delay |
| maxAttempts | number? | Infinity | Max reconnect attempts |
Event Stream & Reconnection
The client includes a built-in SSE event stream with automatic reconnection:
// Listen for events
client.on("user.joined", (event) => {
console.log(event.data.displayName, "connected");
});
client.on("ready-check.started", (event) => {
console.log(`Ready check initiated by ${event.data.initiatorName}, ${event.data.participantCount} participants`);
});
client.on("ready-check.completed", (event) => {
const { summary } = event.data;
console.log(`Ready check done: ${summary.ready}/${summary.total} ready`);
});
// Lifecycle hooks
client.onLifecycle("stream.disconnected", ({ reason }) => {
console.warn("Stream lost:", reason);
});
client.onLifecycle("stream.reconnected", ({ attempt }) => {
console.log("Back online after", attempt, "attempts");
});
// Connect with reconnection options (all optional)
await client.connect({
autoReconnect: true, // default: true
initialDelayMs: 1000, // default: 1000
maxDelayMs: 30000, // default: 30000
maxAttempts: Infinity, // default: Infinity
});
// Intentional disconnect (won't trigger reconnection)
client.disconnect();Ready Checks
20-second attendance polls targeting connected operators. Templates define reusable configs; sessions are live instances.
// List templates
const { readyChecks } = await client.readyChecks.list();
// Create a template
const { readyCheck } = await client.readyChecks.upsert({
message: "Fleet departing in 2 minutes — confirm ready",
color: "#34CD84",
target: { everyone: true },
});
// Start a session
const { session, summary } = await client.readyChecks.start(readyCheck.id, "Fleet Commander");
console.log(`${summary.total} participants, expires at ${session.expiresAt}`);
// Poll session status
const status = await client.readyChecks.getSession(session.id);
console.log(`${status.summary.ready}/${status.summary.total} ready`);
// Remove a template
await client.readyChecks.remove(readyCheck.id);ACARS Alerts
Broadcast short text alerts to all connected operators:
await client.comms.sendAcars("Fleet departing in 60 seconds", {
senderName: "Fleet Commander",
durationMs: 8500,
alertType: "emergency", // "critical" | "emergency" | "non-emergency"
});Error Handling
All methods throw StarCommsError on failure:
import { StarCommsError } from "@30k/starcomm-client";
try {
await client.assignments.assign("bad_id", 99);
} catch (err) {
if (err instanceof StarCommsError) {
console.error(err.statusCode, err.message);
}
}SSE Event Types
| Event | Data Fields |
|-------|-------------|
| user.joined | userId, displayName, transport, nets |
| user.left | userId, displayName, transport |
| ptt.start | userId, displayName, netId |
| ptt.stop | userId, displayName, netId, reason? |
| operation.opened | open |
| operation.closed | open |
| assignments.changed | source?, action?, userId?, netId?, keyId? |
| config.changed | keyId?, action?, uid?, preset? |
| client.disconnected | userId, keyId, disconnected |
| acars.sent | id, text, durationMs, alertType, senderId, senderName, source, routed |
| ready-check.configured | templateId, keyId? |
| ready-check.removed | templateId, keyId? |
| ready-check.started | sessionId, templateId, initiatorId, initiatorName, participantCount, expiresAt |
| ready-check.response | sessionId, userId, name, status, respondedAt |
| ready-check.completed | sessionId, templateId, summary |
Local Client (Desktop Companion)
A standalone WebSocket client for communicating with the StarComm desktop application's Mobile Remote interface. Completely independent from the shard Owner API client — no API keys required, just the pairing token from StarComm's settings.
Setup
In StarComm desktop: Settings → Mobile Remote → Enable APK companion link. Note the LAN control port and pairing token.
Usage
import { StarCommsLocalClient } from "@30k/starcomm-client";
const local = new StarCommsLocalClient({
token: "your_pairing_token", // from StarComm settings
// host: "127.0.0.1", // default; use LAN IP for remote access
// port: 8798, // default; matches StarComm's "LAN control port"
debug: true, // enable console logging (optional)
});
// Listen for state updates
local.on("snapshot", (snapshot) => {
console.log(`Connected as ${snapshot.displayName}`);
console.log(`Guild: ${snapshot.guildName}`);
console.log(`Channels: ${snapshot.channels.length}`);
});
local.on("error", (err) => {
console.error("StarComm error:", err.message);
});
// Lifecycle events
local.onLifecycle("connected", () => console.log("Connected"));
local.onLifecycle("disconnected", ({ reason }) => console.warn("Disconnected:", reason));
local.onLifecycle("reconnecting", ({ attempt }) => console.log("Reconnecting...", attempt));
// Connect (returns a Promise — resolves when socket is open)
await local.connect();
// Access the latest snapshot at any time
console.log(local.snapshot?.channels);PTT (Push-to-Talk)
Three modes available:
// ptt.start — momentary (like holding a physical PTT button)
// Releases automatically if no keepalive is sent.
local.pttStart(netId);
// ptt.hold — toggle/latch mode (tap to lock on, tap again or stop to release)
// Use this for programmatic PTT that should stay active.
local.pttHold(netId);
// ptt.stop — release PTT on a net
local.pttStop(netId);For sustained transmission, use pttHold() + pttStop():
local.pttHold(255); // latch transmit on net 255
// ... do things ...
local.pttStop(255); // releaseAdmin Commands
Only work when the connected user has admin access (snapshot.access.isAdmin === true):
// Assign/unassign a user to a net
local.adminAssign("userId", netId);
local.adminUnassign("userId", netId);
// Disconnect a user
local.adminDisconnect("userId");
// Broadcast an ACARS alert
local.adminAcars("Fleet departing in 60 seconds");Config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| token | string | — | Pairing token (required) |
| host | string? | "127.0.0.1" | StarComm host IP |
| port | number? | 8798 | LAN control port |
| autoReconnect | boolean? | true | Auto-reconnect on disconnect |
| initialDelayMs | number? | 1000 | First reconnect delay |
| maxDelayMs | number? | 15000 | Max reconnect delay |
| maxAttempts | number? | Infinity | Max reconnect attempts |
| debug | boolean? | false | Log all messages to console |
Snapshot Structure
The snapshot is sent immediately on connection and again whenever state changes:
interface LocalSnapshot {
type: "snapshot";
protocol: string; // "starcomms-client-companion"
protocolVersion: number; // 1
app: string; // "star-comms-client"
version: string; // e.g. "1.50.3"
connected: boolean;
pairedDevices: number;
userId: string; // Discord user ID
displayName: string;
guildId: string;
guildName: string;
operationOpen: boolean;
socketStatus: string; // "connected" | "disconnected" | ...
activeNetId: number; // -1 = none
activeGuildId: string;
relay: { guildId, guildName, voiceEndpoint, udpVoiceEndpoint, shardName, shardActive, operationOpen };
access: { isAdmin, isFullAdmin, canCustomizeTheme };
ptt: { active, netId, channel, color };
overlay: { transmitting, transmitChannel, transmitColor, tag, language, labels, receiving[] };
receiving: []; // users currently transmitting
channels: Channel[]; // all visible nets with members, volume, mute state
admin?: { canAssign, canManageNets, canSendAcars, acarsEnabled, acarsDurationMs, streamerMode, users[], channels[] };
}Health Check
Before connecting via WebSocket, you can verify reachability with a plain HTTP call:
GET http://<host>:<port>/health
→ {"ok":true,"app":"star-comms-local-control","port":8798,"clients":1}License
MIT
