@helix3/helix-sdk
v0.1.1-helix3.27
Published
HELIX Instant SDK — identity (and later multiplayer/voice/wallet) for worlds running on HELIX Instant
Readme
@hypersoniclabs/helix-sdk
The HELIX Instant browser SDK — the runtime a world embeds to talk to the HELIX shell (the play page that iframes it). v0.1 provides identity; later versions add multiplayer, voice, wallet, and inventory.
A world is a static bundle that runs inside a sandboxed iframe. The shell hands it a
short-lived, world-scoped session token (minted by the backend at
POST /api/v1/instant-worlds/:worldId/session) over postMessage. The SDK wraps that handshake.
Install
npm install @hypersoniclabs/helix-sdkUsage
import { Helix } from '@hypersoniclabs/helix-sdk';
const { embedded, world, user } = await Helix.init(); // call once, at startup
if (Helix.auth.isAuthenticated()) {
const me = await Helix.auth.getUser(); // { id, username, displayName }
}
// Raise the shell's login overlay (no-op reload — world state is preserved):
loginButton.onclick = async () => {
try {
const user = await Helix.auth.requestLogin();
console.log('signed in as', user.username);
} catch {
/* dismissed or unavailable */
}
};
Helix.auth.onAuthChanged((user) => updateUi(user)); // login + logout
// Ask the HELIX shell to render native status notifications.
Helix.notify.success('Prize claimed', 'Welcome Chip added to inventory');
Helix.notify.failure('Purchase failed', 'Not enough LIX');
Helix.notify.notification('Quest updated', 'Return to the VIP lounge');
Helix.notify.message('Nova', 'Meet me by the arcade machines');
// Publish a current interaction prompt without tying your world to a renderer.
Helix.prompts.set({
id: 'fortune-arcade.vip',
title: 'VIP Room',
description: 'Buy a keycard to enter.',
input: { key: 'E', label: 'Interact' },
actionLabel: 'Buy keycard',
state: 'available',
});
Helix.prompts.clear('fortune-arcade.vip');When the world is opened directly (e.g. a local vite dev server with no shell), init() resolves
with embedded: false and all identity APIs return null/false — so the same build runs locally
and embedded.
API
Helix.init(): Promise<{ embedded, world, user }>— handshake; call once before anything else.Helix.auth.getUser(): Promise<HelixUser | null>Helix.auth.isAuthenticated(): booleanHelix.auth.requestLogin(): Promise<HelixUser>— resolves on login, rejects on dismiss/unavailable.Helix.auth.onAuthChanged(fn): () => void— fires on login and logout; returns an unsubscribe fn.Helix.notify.show({ kind, title, message?, timeoutMs?, actionLabel?, actionHref? }): booleanHelix.notify.success|failure|notification|message(title, message?): boolean— returnsfalsewhen the world is running standalone without a shell.Helix.prompts.set({ id, title, description?, input?, actionLabel?, state?, priority? }): booleanHelix.prompts.clear(promptId?): booleanHelix.getSessionToken(): string | null— the world-scoped token, for advanced direct API calls.
Economy & items (v2, shell-mediated)
All of these are server-authoritative: the world requests, the shell (which holds the player's
session) settles with the backend and renders the purchase popup. Reads degrade to empty defaults
when not embedded; a purchase returns Unauthorized (never a fake success).
const { lix, coins } = await Helix.wallet.getBalance();
Helix.wallet.onBalanceChanged((b) => updateHud(b));
// Buy a marketplace item or a world-registered product. completed === player owns it now.
const res = await Helix.marketplace.purchaseItem('premium_sword_001');
if (res.completed) equip('premium_sword_001'); // Granted | Claimed | AlreadyOwned
else showToast(res.status); // InsufficientFunds | Cancelled | …
await Helix.marketplace.purchaseProduct('golden_key'); // a world product
const items = await Helix.marketplace.getListings({ kind: 'home_item' });
// Ownership gating — works across worlds & creators (VIP / season passes).
if (await Helix.inventory.hasItem('vip_pass_001')) openBackstage();
const owned = await Helix.inventory.getMyItems();
await Helix.inventory.equipItem('vip_hat_001');
Helix.inventory.onInventoryChanged(refreshInventory);Helix.wallet.getBalance()/onBalanceChanged(fn)Helix.marketplace.purchaseItem(itemId)/purchaseProduct(productId)/getListings(query?)/getPurchaseContext(ref)Helix.inventory.hasItem(id)/getQuantity(id)/getMyItems()/equipItem(id)/onInventoryChanged(fn)
The SDK mints a stable idempotency key per purchase (reused across the popup's retries) so a dropped
network can never double-charge. See the backend /api/v1/iwp/* endpoints for the settlement layer.
Interaction prompts
Helix.prompts is intentionally engine-agnostic. A Three.js, Phaser, Unity WebGL, or plain DOM world
can all publish the same contract: a stable prompt id, display text, optional keyboard/gamepad labels,
and a coarse state (default, available, locked, or busy). The shell decides how to render it.
Rich presence
Push a live activity for the local player that enriches the baseline "In [World]" their friends
already see (in the friends sidebar and on their profile) — e.g. Racing — lap 3/5. It's dynamic:
call it whenever the moment changes, no registration, no per-activity setup.
// Enrich presence with whatever the player is doing right now.
await Helix.presence.setActivity({
state: 'Racing — lap 3/5', // required, ≤128 chars — the line friends see
details: 'Sunset Circuit', // optional second line, ≤128 chars
party: { current: 2, max: 8 }, // optional party sizing
// image: 'my-mode-icon', // optional; omitted → the world's thumbnail (zero-config)
// joinable: true, // hint only; joining an instance ships with the MP runtime
});
// e.g. drive it off game state
raceEvents.on('lap', (n, total) =>
Helix.presence.setActivity({ state: `Racing — lap ${n}/${total}` }),
);
// Back to plain "In [World]". Also happens automatically when the player leaves.
await Helix.presence.clearActivity();Helix.presence.setActivity(activity)→Promise<boolean>—trueon success;falsefor guests, outside a shell, when not in a world, or when rejected. Never throws into your game loop.Helix.presence.clearActivity()→Promise<boolean>.
Server-authoritative (by design). setActivity posts to the authenticated presence endpoint using
the player's own session. The server sets activity only for that player and only for the world
they're currently in — a world can never write another player's status, or a world they aren't in.
This is the interim of the "the world's server sets it" model: when the multiplayer room runtime lands
it will set activity server-to-server after authenticating the player into its instance; the API and
validation are already shaped for that. Backend: POST / DELETE /api/v1/users/me/presence/activity.
Privacy & safety. Activity is governed by the player's presence privacy (visibility
Everyone / Friends only / Nobody, plus a hide-activity toggle) — if they've opted out, the server
never emits it, to anyone. Text is length-capped, rate-limited, and screened by the shared text
moderation layer server-side (a bad-actor world can't push junk into a friend's status card).
Protocol
src/protocol.ts is the wire contract (postMessage messages between world and
shell) and is imported by both sides. Any breaking change must bump PROTOCOL_VERSION. The
HelixSession shape matches the backend's InstantWorldSessionResponseDto, so the shell forwards
the backend response verbatim.
Develop
npm install
npm test # jest + jsdom
npm run build # tsc → dist/ (ESM)
npm run lint