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

@novasamatech/host-api-wrapper

v0.9.4

Published

Host API wrapper: integrate and run your product inside Polkadot browser.

Readme

@novasamatech/host-api-wrapper

An easy way to embed Polkadot host functionality into your dapp.

Overview

Product SDK provides a set of tools to integrate your application with any Polkadot host application. Core features:

  • Generic injectWeb3 provider similar to polkadot-js extension
  • Chat module integration
  • Statement store integration
  • Accounts provider for product accounts and signing
  • Redirect PAPI requests to host application
  • Receive additional information from host application - supported chains, theme, etc.
  • Local storage for persisting data in the host application
  • Preimage manager for looking up and submitting preimages

Installation

npm install @novasamatech/host-api-wrapper --save -E

Usage

Injecting account provider into injectedWeb3 interface

Product SDK can provide account information and signers with the same interface as any other Polkadot-compatible wallet.

import { injectSpektrExtension, SpektrExtensionName } from '@novasamatech/host-api-wrapper';
import { connectInjectedExtension, type InjectedPolkadotAccount } from '@polkadot-api/pjs-signer';

async function getSpektrExtension() {
  const ready = await injectSpektrExtension();

  if (ready) {
    return connectInjectedExtension(SpektrExtensionName)
  }

  return null;
}

async function getAccounts(): Promise<InjectedPolkadotAccount[]> {
  const extension = await getSpektrExtension();

  if (extension) {
    return extension.getAccounts()
  }

  // fallback to other providers
  return [];
}

Redirecting PAPI requests to host application

You can wrap your PAPI provider with Spektr provider to support redirecting requests to the host application.

import { createClient, type PolkadotClient } from 'polkadot-api';
import { getWsProvider } from 'polkadot-api/ws-provider';
import { createPapiProvider, WellKnownChain } from '@novasamatech/host-api-wrapper';

function createPapiClient(): PolkadotClient {
  const polkadotEndpoint = 'wss://...';

-  const provider = getWsProvider(polkadotEndpoint);
+  const provider = createPapiProvider({
+    chainId: WellKnownChain.polkadotRelay,
+    fallback: getWsProvider(polkadotEndpoint),
+  });

  return createClient(provider);
}

Subscribing host connection status

import { metaProvider } from '@novasamatech/host-api-wrapper';

const unsubscribe = metaProvider.subscribeConnectionStatus((status) => {
  console.log('connection status changed', status);
});

Chat Integration

import { createProductChatManager } from '@novasamatech/host-api-wrapper';

// Create manager instance
const chat = createProductChatManager();

// Register your product as a chat contact
const roomRegistrationStatus = await chat.registerRoom({
  roomId: 'my-product-room',
  name: 'My Product',
  icon: 'https://example.com/icon.png'
});

// Register your product as a chat bot
const botRegistrationStatus = await chat.registerBot({
  botId: 'my-product-bot',
  name: 'My Product',
  icon: 'https://example.com/icon.png'
});

// Send a message
const { messageId } = await chat.sendMessage('my-product-room', {
  tag: 'Text',
  value: 'Hello dear user!'
});

// Subscribing to chat actions (incoming messages, etc.)
const subscriber = chat.subscribeAction((action) => {
  console.log('Room:', action.roomId);
  console.log('Sender:', action.peer);

  const payload = action.payload;

  if (payload.tag === 'MessagePosted') {
    console.log('Received message:', action.value);
  }
  if (payload.tag === 'ActionTriggered') {
    console.log('User triggered action:', action.value)
  }
});

// Subscribing to chat room list updates
const chatListSubscriber = chat.subscribeChatList((rooms) => {
  console.log('Chat rooms updated:', rooms);
});

// Sending a custom message
await chat.sendMessage('my-product-room', {
  tag: 'Custom',
  value: { messageType: 'my-custom-type', payload: new Uint8Array([/* ... */]) }
});

// Handling custom message rendering requests from host
const unsubscribeRenderer = chat.onCustomMessageRenderingRequest((messageType, payload, render) => {
  // Build a CustomRendererNode tree and pass it to render()
  render({
    tag: 'Text',
    value: {
      modifiers: undefined,
      props: { style: undefined, color: undefined },
      children: [{ tag: 'String', value: 'Custom message content' }],
    },
  });

  return () => {
    // cleanup when subscription ends
  };
});

Note: Messages sent before registration will be queued and sent automatically after successful registration.

Statement Store

The Statement Store provides a decentralized way to store statements (messages). It can be used for various purposes like p2p communication, storing temp data, etc.

import { createStatementStore } from '@novasamatech/host-api-wrapper';
import type {
  Topic,
  Statement,
  SignedStatement,
  StatementTopicFilter,
  ProductAccountRef,
} from '@novasamatech/host-api-wrapper';

// Create statement store instance
const statementStore = createStatementStore();

// Define topics (32-byte identifiers) to categorize statements
const topic: Topic = new Uint8Array(32);

// Subscribe to statements matching ALL listed topics (AND semantics)
const filter: StatementTopicFilter = { matchAll: [topic] };
const subscription = statementStore.subscribe(filter, (page) => {
  // page.isComplete is true once the initial historical dump is done
  console.log('Received statements:', page.statements, 'synced:', page.isComplete);
});

// Create a proof for a new statement
// [DotNS identifier, account selector]. The selector is a plain index or a raw
// 32-byte index (RFC 0022).
const accountId: ProductAccountRef = ['product.dot', 0];
const statement: Statement = {
  proof: undefined,
  decryptionKey: undefined,
  priority: undefined,
  channel: undefined,
  topics: [topic],
  data: new Uint8Array([/* your data */]),
};

const proof = await statementStore.createProof(accountId, statement);

// Submit a signed statement
const signedStatement: SignedStatement = {
  ...statement,
  proof,
};

await statementStore.submit(signedStatement);

// Unsubscribe when done
subscription.unsubscribe();

Accounts Provider

The Accounts Provider allows you to access product accounts and create signers for signing transactions.

import { accounts } from '@novasamatech/host-api-wrapper';
import type { ProductAccount, ProofContext } from '@novasamatech/host-api-wrapper';

// Get the user's primary DotNS username (RFC-0014)
// — prompts for permission on first call
const userIdResult = await accounts.getUserId();

if (userIdResult.isOk()) {
  const { primaryUsername } = userIdResult.value;
  console.log('Primary username:', primaryUsername);
} else {
  const err = userIdResult.error;
  if (err.tag === 'NotConnected') {
    console.log('User is not logged in');
  } else if (err.tag === 'PermissionDenied') {
    console.log('User denied disclosure of their primary username');
  }
}

// Request login — triggers host sign-in UI; reason is shown to the user
const loginResult = await accounts.requestLogin('Sign in to access your account');

if (loginResult.isOk()) {
  const outcome = loginResult.value; // 'success' | 'alreadyConnected' | 'rejected'
  if (outcome === 'rejected') {
    console.log('User cancelled login');
  }
} else {
  console.error('Login error:', loginResult.error);
}

// Get a product account by DotNS identifier and account selector. The selector
// is a plain index (the primary, enumerable form) or a raw 32-byte index
// (RFC 0022); it defaults to index 0, the product's default account.
const accountResult = await accounts.getProductAccount('product.dot', 0);
// …or, for a byte-valued selector:
// const accountResult = await accounts.getProductAccount('product.dot', raw32);

if (accountResult.isOk()) {
  const account: ProductAccount = accountResult.value;
  console.log('Public key:', account.publicKey);
}

// Ring VRF: a contextual alias and a proof are addressed by an explicit member
// key handle, a product-scoped `context` (`[productId, suffix]`) and a `ring`
// location on a chain (RFC 0004, amended by RFC 0024). The suffix is the same
// selector as an account's derivation index and expands to the same 32-byte
// value (RFC 0022).
const context: ProofContext = ['product.dot', 0]; // [productId, selector]
const ring = {
  chainId: '0x…', // 32-byte chain genesis hash
  junctions: [{ tag: 'PalletInstance', value: 42 }],
};

// Register a key your own product owns for that ring. Permissionless and
// prompt-free — ownership is the calling product, never a parameter. Returns the
// member public key. Registering the same index for another ring extends the
// existing entry rather than adding a second one.
const registerResult = await accounts.registerRingVrfKey(0, ring);

// Discover another product's keys. Handles are opaque: select by the rings an
// entry declares, NEVER by index — the index is the owner's implementation
// detail and hardcoding it breaks the moment the owner rotates or adds a key.
const keysResult = await accounts.listRingVrfKeys('peopl.dot'); // 'Anonymized' by default
const personKey = keysResult.isOk()
  ? keysResult.value.find(entry => entry.rings.some(r => r.chainId === ring.chainId))
  : undefined;

// A key handle for one of your own keys — for a foreign key use the handle from
// `listRingVrfKeys` verbatim instead.
import { ringVrfKeyHandle } from '@novasamatech/host-api-wrapper';

const ownHandle = ringVrfKeyHandle('product.dot', 0);

// Get the contextual alias for that (handle, context, ring).
const aliasResult = await accounts.getContextualAlias(ownHandle, context, ring);

if (aliasResult.isOk()) {
  const { context: contextBytes, alias } = aliasResult.value;
  console.log('Alias:', alias);
}

// Create a ring VRF proof binding `message` with an explicit member key.
// A proof is a bearer token for its context's alias, so a *foreign* handle is
// admitted only when its owner allowlisted your product in its manifest — there
// is no user-prompt fallback, and you get `NotAllowlisted` otherwise.
const proofResult = await accounts.createRingVRFProof(ownHandle, context, ring, new Uint8Array([0x48, 0x69]));

if (proofResult.isOk()) {
  const { proof, contextualAlias, ringIndex, ringRevision } = proofResult.value;
  console.log('Proof:', proof, 'at ring index', ringIndex, 'revision', ringRevision);
}

// Sign with the member key itself rather than proving membership anonymously
// (RFC 0024). No context and no ring: it derives no alias and proves nothing, so
// there is nothing for either to scope. Verified against the member public key,
// which makes the signature linkable to every other use of that key.
const signatureResult = await accounts.ringVrfSign(ownHandle, new Uint8Array([0x48, 0x69]));

// sr25519 VRF signature over a product account (RFC-0023). The transcript is a
// recipe — a root label plus ordered `(label, value)` items — that the host
// replays verbatim (`Transcript::new(label)` then one `append_message` per item)
// and signs. It never injects a `signer` item; pass the account's public key
// yourself if the transcript needs one.
import type { VrfTranscriptItem } from '@novasamatech/host-api-wrapper';

const transcriptLabel = new TextEncoder().encode('my-product-lottery');
const items: VrfTranscriptItem[] = [{ label: new TextEncoder().encode('round'), value: new Uint8Array([7]) }];

const vrfResult = await accounts.signVrf('product.dot', 0, transcriptLabel, items);

if (vrfResult.isOk()) {
  const { preOutput, proof } = vrfResult.value; // 32-byte VRFPreOut, 64-byte VRFProof
  console.log('VRF pre-output:', preOutput, 'proof:', proof);
} else {
  // err.tag: 'NotConnected' | 'Rejected' | 'Unknown'
  console.error('signVrf failed:', vrfResult.error.tag);
}

// Get legacy accounts (external wallets)
const legacyAccountsResult = await accounts.getLegacyAccounts();

if (legacyAccountsResult.isOk()) {
  console.log('Legacy accounts:', legacyAccountsResult.value);
}

// Subscribe to account connection status changes
const unsubscribe = accounts.subscribeAccountConnectionStatus((status) => {
  // status: 'connected' | 'disconnected'
  console.log('Account connection status:', status);
});

// Create a signer for a product account (for use with PAPI).
// Resolve the account first, then hand it to the signer factory.
const productAccountResult = await accounts.getProductAccount('product.dot', 0);

if (productAccountResult.isOk()) {
  const productSigner = accounts.getProductAccountSigner(productAccountResult.value);
  const signedTx = await tx.signAndSubmit(productSigner);
}

// Create a signer for a legacy account.
// Fetch the legacy account list, pick one, then pass it to the signer factory.
const legacyAccountsResult = await accounts.getLegacyAccounts();

if (legacyAccountsResult.isOk()) {
  const [legacyAccount] = legacyAccountsResult.value;
  if (legacyAccount) {
    const legacySigner = accounts.getLegacyAccountSigner(legacyAccount);
    const signedTx = await tx.signAndSubmit(legacySigner);
  }
}

If you need a non-default transport (e.g. for tests or multi-host setups), use createAccountsProvider(transport) to build your own instance with the same API.

Local Storage

The Local Storage module provides a way to persist data in the host application's storage.

import { hostLocalStorage, createLocalStorage } from '@novasamatech/host-api-wrapper';

// Use the default instance
const storage = hostLocalStorage;

// Or create a custom instance with a different transport
// const storage = createLocalStorage(customTransport);

// Write and read raw bytes
await storage.writeBytes('key', new Uint8Array([1, 2, 3]));
const bytes = await storage.readBytes('key');

// Write and read strings
await storage.writeString('greeting', 'Hello, World!');
const greeting = await storage.readString('greeting');

// Write and read JSON
await storage.writeJSON('config', { theme: 'dark', fontSize: 14 });
const config = await storage.readJSON('config');

// Clear a key
await storage.clear('key');

Derive Entropy

The Derive Entropy function allows products to derive deterministic 32-byte entropy scoped to the product and a caller-chosen key.

import { deriveEntropy } from '@novasamatech/host-api-wrapper';

const result = await deriveEntropy(new Uint8Array([1, 2, 3]));

if (result.isOk()) {
  const entropy: Uint8Array = result.value;
  console.log('Derived entropy:', entropy);
}

Permissions

Products can request device and remote permissions from the host. Decisions are prompted once and persisted permanently — subsequent calls for the same permission resolve immediately without prompting.

import { requestDevicePermission, requestPermission } from '@novasamatech/host-api-wrapper';

// Request a single device permission
const deviceResult = await requestDevicePermission('Camera');
if (deviceResult.isOk()) {
  console.log('Camera granted:', deviceResult.value); // boolean
}

// Request remote permissions in a batch (single user prompt for all)
const remoteResult = await requestPermission([
  { tag: 'Remote', value: ['api.coingecko.com', '*.example.com'] },
  { tag: 'ChainSubmit', value: undefined },
]);
if (remoteResult.isOk()) {
  console.log('All remote permissions granted:', remoteResult.value); // boolean
}

Available device permission values: 'Notifications', 'Camera', 'Microphone', 'Bluetooth', 'NFC', 'Location', 'Clipboard', 'OpenUrl', 'Biometrics'.

Available remote permission tags: 'Remote' (HTTP/WS domain patterns), 'WebRTC', 'ChainSubmit', 'PreimageSubmit', 'StatementSubmit'.

Note: remote_chain_transaction_broadcast, remote_preimage_submit, and remote_statement_store_submit implicitly trigger a permission prompt if the relevant permission has not yet been resolved. Call requestPermission(...) proactively before entering those flows for a controlled UX.

Preimage Manager

The Preimage Manager allows you to lookup and submit preimages to the host application.

import { preimageManager, createPreimageManager } from '@novasamatech/host-api-wrapper';

// Use the default instance
const manager = preimageManager;

// Or create a custom instance with a different transport
// const manager = createPreimageManager(customTransport);

// Lookup a preimage by its hash key
const subscription = manager.lookup('0x1234...', (preimage) => {
  if (preimage) {
    console.log('Preimage found:', preimage);
  } else {
    console.log('Preimage not found');
  }
});

// Unsubscribe when done
subscription.unsubscribe();

// Submit a preimage
const preimageKey = await manager.submit(new Uint8Array([1, 2, 3, 4]));

Payment manager

import { createPaymentManager } from '@novasamatech/host-api-wrapper';

const payments = createPaymentManager();

// Subscribe to the user's payment balance (host will prompt for consent)
const balanceSub = payments.subscribeBalance(balance => {
  console.log('Available:', balance.available);
  console.log('Pending:', balance.pending);
});
balanceSub.onInterrupt(() => console.log('Balance access denied or lost'));

// Top up the user's balance from one of the calling product's accounts.
// `derivationIndex` is the same selector as `accounts.getProductAccount` takes:
// a plain index or a raw 32-byte index (RFC 0022).
await payments.topUp(1_000_000n, {
  type: 'productAccount',
  derivationIndex: 0,
});

// Request a payment from the user (host shows confirmation UI)
const destination = new Uint8Array(32); // 32-byte AccountId
const receipt = await payments.requestPayment(500_000n, destination);

// Track payment settlement
const statusSub = payments.subscribePaymentStatus(receipt.id, status => {
  if (status.type === 'completed') console.log('Payment settled');
  if (status.type === 'failed') console.log('Payment failed:', status.reason);
});