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-container

v0.7.8-2

Published

Host container for hosting and managing products within the Polkadot ecosystem.

Readme

@novasamatech/host-container

A robust solution for hosting and managing decentralized applications (dapps) within the Polkadot ecosystem.

Overview

Host container provides the infrastructure layer for securely embedding and communicating with third-party dapps. It handles the isolation boundary, message routing, lifecycle management, and security concerns inherent in hosting untrusted web content.

Installation

npm install @novasamatech/host-container --save -E

Basic Container Setup

iframe

import { createContainer, createIframeProvider } from '@novasamatech/host-container';

const iframe = document.createElement('iframe');

const provider = createIframeProvider({
  iframe,
  url: 'https://dapp.example.com'
});
const container = createContainer(provider);

document.body.appendChild(iframe);

webview

import { createContainer, createWebviewProvider } from '@novasamatech/host-container';

const webview = document.createElement('webview');

const provider = createWebviewProvider({
  webview,
  openDevTools: false,
});
const container = createContainer(provider);

document.body.appendChild(webview);

API reference

handleFeatureSupported

container.handleFeatureSupported((params, { ok, err }) => {
  if (params.tag === 'Chat') {
    return ok(supportedChains.has(params.value));
  }
  return ok(false);
});

handleDevicePermission

The request parameter is one of: 'Notifications', 'Camera', 'Microphone', 'Bluetooth', 'NFC', 'Location', 'Clipboard', 'OpenUrl', 'Biometrics'.

container.handleDevicePermission(async (request, { ok, err }) => {
  // request is a string literal: 'Notifications' | 'Camera' | 'Microphone' | ...
  const granted = await promptDevicePermission(request);
  return ok(granted);
});

handlePermission

The request parameter is a single RemotePermission item. Return ok(true) when the permission is granted, ok(false) when denied.

The item has one of these shapes:

  • { tag: 'Remote', value: string[] } — HTTP/WS domain patterns (exact or *.wildcard)
  • { tag: 'WebRTC', value: undefined } — WebRTC access (may expose user IP)
  • { tag: 'ChainSubmit', value: undefined } — broadcast transactions via remote_chain_transaction_broadcast
  • { tag: 'PreimageSubmit', value: undefined } — submit preimages via remote_preimage_submit
  • { tag: 'StatementSubmit', value: undefined } — submit statements via remote_statement_store_submit
container.handlePermission(async (permission, { ok, err }) => {
  switch (permission.tag) {
    case 'Remote':
      return ok(await checkDomainPermissions(permission.value));
    case 'WebRTC':
      return ok(await promptWebRTCPermission());
    case 'ChainSubmit':
      return ok(await promptChainSubmitPermission());
    case 'PreimageSubmit':
      return ok(await promptPreimageSubmitPermission());
    case 'StatementSubmit':
      return ok(await promptStatementSubmitPermission());
  }
});

handlePushNotification

Gated by the Notifications device permission: the container consults handleDevicePermission with 'Notifications' before invoking this handler. If the device permission is denied or errors, the handler is skipped and the request fails.

container.handlePushNotification(async (notification, { ok, err }) => {
  await showNotification(notification);
  return ok(undefined);
});

handleNavigateTo

container.handleNavigateTo(async (url, { ok, err }) => {
  await navigate(url);
  return ok(undefined);
});

handleDeriveEntropy

container.handleDeriveEntropy(async (key, { ok, err }) => {
  const entropy = await deriveEntropy(key);
  return ok(entropy);
});

handleLocalStorageRead

container.handleLocalStorageRead(async (key, { ok, err }) => {
  const value = await storage.get(key);
  return ok(value ?? null);
});

handleLocalStorageWrite

container.handleLocalStorageWrite(async ([key, value], { ok, err }) => {
  try {
    await storage.set(key, value);
    return ok(undefined);
  } catch (e) {
    return err({ tag: 'Full' });
  }
});

handleLocalStorageClear

container.handleLocalStorageClear(async (key, { ok, err }) => {
  await storage.delete(key);
  return ok(undefined);
});

handleAccountConnectionStatusSubscribe

container.handleAccountConnectionStatusSubscribe((_, send, interrupt) => {
  const listener = (status) => send(status);
  accountService.on('connectionStatusChange', listener);
  return () => accountService.off('connectionStatusChange', listener);
});

handleThemeSubscribe

container.handleThemeSubscribe((_, send, interrupt) => {
  const listener = (theme: 'light' | 'dark') => send(theme);
  themeService.on('change', listener);
  send(themeService.getCurrentTheme());
  return () => themeService.off('change', listener);
});

handleGetUserId

Called when a product requests the user's primary DotNS username (RFC-0014). Show a disclosure prompt on first call; the host decides what counts as "primary" for the calling product. Return NotConnected without prompting if no user is connected; return PermissionDenied if the user denies disclosure.

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

container.handleGetUserId(async (_, { ok, err }) => {
  const username = await pickPrimaryUsernameForCallingProduct();
  if (!username) {
    return err(new GetUserIdErr.NotConnected());
  }

  const granted = await promptUserForUsernameDisclosure();
  if (!granted) {
    return err(new GetUserIdErr.PermissionDenied());
  }

  return ok({ primaryUsername: username });
});

handleRequestLogin

Called when a product requests the host login UI. Present the sign-in flow and return the outcome. reason is an optional human-readable string the product provides to explain why login is needed.

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

container.handleRequestLogin(async (reason, { ok, err }) => {
  const alreadyConnected = await checkIfConnected();
  if (alreadyConnected) return ok('alreadyConnected');

  const result = await presentLoginUI(reason);
  if (!result.success) return ok('rejected');

  return ok('success');
});

handleAccountGet

container.handleAccountGet(async ([dotnsId, derivationIndex], { ok, err }) => {
  const account = await getProductAccount(dotnsId, derivationIndex);
  if (account) {
    return ok({ publicKey: account.publicKey });
  }
  return err({ tag: 'NotConnected' });
});

handleAccountGetAlias

container.handleAccountGetAlias(async ([dotnsId, derivationIndex], { ok, err }) => {
  const alias = await getAccountAlias(dotnsId, derivationIndex);
  if (alias) {
    return ok({ context: alias.context, alias: alias.alias });
  }
  return err(new RequestCredentialsErr.NotConnected());
});

handleAccountCreateProof

container.handleAccountCreateProof(async ([[dotnsId, derivationIndex], ringLocation, message], { ok, err }) => {
  try {
    const proof = await createRingProof(dotnsId, derivationIndex, ringLocation, message);
    return ok(proof);
  } catch (e) {
    return err({ tag: 'RingNotFound' });
  }
});

handleGetLegacyAccounts

container.handleGetLegacyAccounts(async (_, { ok, err }) => {
  const accounts = await getLegacyAccounts();
  return ok(accounts);
});

handleCreateTransaction

container.handleCreateTransaction(async ([productAccountId, payload], { ok, err }) => {
  try {
    const signedTx = await createTransaction(productAccountId, payload);
    return ok(signedTx);
  } catch (e) {
    return err({ tag: 'Rejected' });
  }
});

handleCreateTransactionWithLegacyAccount

container.handleCreateTransactionWithLegacyAccount(async (payload, { ok, err }) => {
  try {
    const signedTx = await createTransactionWithLegacyAccount(payload);
    return ok(signedTx);
  } catch (e) {
    return err({ tag: 'Rejected' });
  }
});

handleSignRaw

container.handleSignRaw(async (payload, { ok, err }) => {
  try {
    const result = await signRaw(payload);
    return ok({ signature: result.signature, signedTransaction: result.signedTransaction });
  } catch (e) {
    return err({ tag: 'Rejected' });
  }
});

handleSignPayload

container.handleSignPayload(async (payload, { ok, err }) => {
  try {
    const result = await signPayload(payload);
    return ok({ signature: result.signature, signedTransaction: result.signedTransaction ?? null });
  } catch (e) {
    return err({ tag: 'Rejected' });
  }
});

handleChatCreateRoom

container.handleChatCreateRoom(async (room, { ok, err }) => {
  await chatService.registerRoom(room);
  return ok(undefined);
});

handleChatBotRegistration

container.handleChatBotRegistration(async (bot, { ok, err }) => {
  await chatService.registerBot(bot);
  return ok(undefined);
});

handleChatListSubscribe

container.handleChatListSubscribe((_, send, interrupt) => {
  const listener = (rooms) => send(rooms);
  chatService.on('roomsUpdate', listener);
  return () => chatService.off('roomsUpdate', listener);
});

handleChatPostMessage

container.handleChatPostMessage(async (message, { ok, err }) => {
  const messageId = await chatService.postMessage(message);
  return ok({ messageId });
});

handleChatActionSubscribe

container.handleChatActionSubscribe((_, send, interrupt) => {
  const listener = (action) => send(action);
  chatService.on('action', listener);
  return () => chatService.off('action', listener);
});

renderChatCustomMessage

const subscription = container.renderChatCustomMessage('my-custom-type', payload, (node) => {
  // node is a CustomRendererNode tree describing the UI to render
  console.log('Render custom message:', node);
});

// Unsubscribe when done
subscription.unsubscribe();

handleStatementStoreSubscribe

container.handleStatementStoreSubscribe((filter, send, interrupt) => {
  // filter is { tag: 'MatchAll', value: Uint8Array[] } | { tag: 'MatchAny', value: Uint8Array[] }
  const listener = (page) => send(page);
  statementStore.subscribe(filter, listener);
  return () => statementStore.unsubscribe(filter, listener);
});

handleStatementStoreCreateProof

container.handleStatementStoreCreateProof(async ([[dotnsId, derivationIndex], statement], { ok, err }) => {
  try {
    const proof = await createStatementProof(dotnsId, derivationIndex, statement);
    return ok(proof);
  } catch (e) {
    return err({ tag: 'UnableToSign' });
  }
});

handleStatementStoreSubmit

container.handleStatementStoreSubmit(async (statement, { ok, err }) => {
  try {
    await statementStore.submit(statement);
    return ok(undefined);
  } catch (e) {
    return err({ tag: 'Unknown', value: { reason: e.message } });
  }
});

handlePreimageLookupSubscribe

container.handlePreimageLookupSubscribe((key, send, interrupt) => {
  const listener = (value) => send(value);
  preimageService.subscribe(key, listener);
  return () => preimageService.unsubscribe(key, listener);
});

handlePreimageSubmit

container.handlePreimageSubmit(async (preimage, { ok, err }) => {
  try {
    const key = await preimageService.submit(preimage);
    return ok(key);
  } catch (e) {
    return err({ tag: 'Unknown', value: { reason: e.message } });
  }
});

handlePaymentBalanceSubscribe

Called when a product subscribes to balance updates. Host should prompt for user consent on the first call; interrupt the subscription to communicate denial.

container.handlePaymentBalanceSubscribe((_params, send, interrupt) => {
  const unsubscribe = balanceService.subscribe(balance => {
    send({ available: balance.available, pending: balance.pending });
  });

  return () => unsubscribe();
});

handlePaymentTopUp

Called when a product requests a balance top-up from a product-controlled source. Does not require user consent.

container.handlePaymentTopUp(async ({ amount, source }, { ok, err }) => {
  if (source.tag === 'ProductAccount') {
    const [dotNsIdentifier, derivationIndex] = source.value;
    await transferFromProductAccount(dotNsIdentifier, derivationIndex, amount);
    return ok(undefined);
  }
  if (source.tag === 'PrivateKey') {
    await transferFromPrivateKey(source.value, amount);
    return ok(undefined);
  }
  return err(new PaymentTopUpErr.InvalidSource());
});

handlePaymentRequest

Called when a product requests a payment from the user's balance. Host MUST show a confirmation UI. Returns a receipt immediately; settlement is asynchronous.

container.handlePaymentRequest(async ({ amount, destination }, { ok, err }) => {
  const approved = await showPaymentConfirmation({ amount, destination });
  if (!approved) return err(new PaymentRequestErr.Denied());

  const paymentId = await paymentService.submit(amount, destination);
  return ok({ id: paymentId });
});

handlePaymentStatusSubscribe

Called when a product subscribes to the status of a previously requested payment.

container.handlePaymentStatusSubscribe((paymentId, send, interrupt) => {
  const unsubscribe = paymentService.trackStatus(paymentId, status => {
    if (status === 'processing') send({ tag: 'Processing', value: undefined });
    if (status === 'completed') send({ tag: 'Completed', value: undefined });
    if (status === 'failed') send({ tag: 'Failed', value: 'settlement failed' });
  });

  return () => unsubscribe();
});

handleChainConnection

import { getWsProvider } from 'polkadot-api/ws-provider';

const chains = new Map([
  ['0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', 'wss://rpc.polkadot.io'],
  ['0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe', 'wss://kusama-rpc.polkadot.io'],
]);

container.handleChainConnection({
  factory(genesisHash) {
    const endpoint = chains.get(genesisHash);
    if (!endpoint) return null;
    return getWsProvider(endpoint);
  }
});

isReady

const ready = await container.isReady();
if (ready) {
  console.log('Container is ready');
}

dispose

container.dispose();

subscribeProductConnectionStatus

const unsubscribe = container.subscribeProductConnectionStatus((status) => {
  console.log('Connection status:', status);
});

Known pitfalls

CSP error on iframe loading

If a dapp is hosted on a different domain than the container and uses HTTPS, you should add this meta tag to your host application HTML:

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">