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.9.4

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

The derivation index is an Enum (RFC 0022): Index carries a plain index, Raw a raw 32-byte index. Expand it with derivationIndexBytes — past this boundary only the 32-byte form exists.

import { derivationIndexBytes } from '@novasamatech/host-container';

container.handleAccountGet(async ([dotnsId, derivationIndex], { ok, err }) => {
  // `//product//{dotnsId}/{index}` — hard, hard, soft junctions.
  const account = await getProductAccount(dotnsId, derivationIndexBytes(derivationIndex));
  if (account) {
    return ok({ publicKey: account.publicKey });
  }
  return err({ tag: 'NotConnected' });
});

handleAccountRegisterRingVrfKey

A product registers a ring VRF key it owns against the ring it intends it for (RFC-0024). Ownership is the calling product id and is never a parameter, so this needs no capability gate and no prompt. Registration is idempotent — registering an already-registered index for an additional ring extends that entry rather than creating a second one — and it declares intent, not membership, so it must never be treated as a personhood oracle.

Registration always reaches the Account Holder, which is the authoritative registry, but need not block on it: with the product's ring VRF domain entropy the host can answer immediately and mirror the registration fire-and-forget.

A host MUST NOT derive a member secret for a (product, index) pair absent from its registry. Domain entropy makes derivation unconditional arithmetic — only registration brings a key into existence.

container.handleAccountRegisterRingVrfKey(async ([index, ring], { ok, err }) => {
  if (!isConnected()) {
    return err(new RegisterRingVrfKeyErr.NotConnected());
  }
  if (!(await isKnownRing(ring))) {
    return err(new RegisterRingVrfKeyErr.RingNotFound());
  }
  // `productId` comes from the container's own context — never from the caller.
  return ok(await registry.register(productId, index, ring));
});

On a successful registration, match ring against the well-known ring table (People, People-Lite) by structural equality and record the handle as the corresponding person key — that is what replaces a compiled-in key selection for the host's own personhood-dependent features. If two products register for the same well-known ring, do not pick silently: resolve to the product the user designated as their personhood provider (defaulting to the first registrar), so a second product cannot displace the first.

handleAccountListRingVrfKeys

Answer from the registry snapshot when it is current. Listing the caller's own keys is permissionless; a foreign owner needs a grant or a prompt, and 'PublicKey' disclosure is separately permissioned because a member public key is linkable across every ring it appears in. Omit publicKey under 'Anonymized'.

container.handleAccountListRingVrfKeys(async ([owner, disclosure], { ok, err }) => {
  if (owner !== productId && !(await hasGrantFor(productId, owner))) {
    return err(new ListRingVrfKeysErr.Rejected());
  }
  const entries = await registry.list(owner);
  return ok(
    entries.map(entry => ({
      handle: entry.handle,
      rings: entry.rings,
      publicKey: disclosure === 'PublicKey' ? entry.publicKey : undefined,
    })),
  );
});

handleAccountGetAlias

keyHandle names the ring VRF key explicitly (RFC-0024) — the host no longer defines a PoP collection, infers correspondence, or falls back to a compiled-in key. context is [productId, suffix], where suffix is the same selector as an account's derivation index and expands to the same 32-byte value (RFC 0022), so the alias ↔ account mapping is the identity on it.

ring stays a separate argument because a key may be registered for several; verify it appears among the handle's declared rings and return KeyNotInRing otherwise, and KeyNotRegistered when the handle has no entry at all.

container.handleAccountGetAlias(async ([keyHandle, context, ring], { ok, err }) => {
  const entry = await registry.lookup(keyHandle);
  if (!entry) {
    return err(new GetAliasErr.KeyNotRegistered());
  }
  if (!entry.rings.some(declared => sameRing(declared, ring))) {
    return err(new GetAliasErr.KeyNotInRing());
  }
  const alias = await getContextualAlias(entry, context, ring);
  if (alias) {
    return ok({ context: alias.context, alias: alias.alias });
  }
  return err(new GetAliasErr.RingNotFound());
});

Reading an alias authorizes nothing, so a foreign keyHandle here is governed by the ordinary grant-or-prompt model — unlike handleAccountCreateProof below.

handleAccountCreateProof

Same handle checks as handleAccountGetAlias, plus the allowlist gate. A proof is a bearer token for its context's alias, and message is opaque — for an extrinsic it is a hash of the inherited implication — so nothing at call time can tell what the result will authorize. A host MUST therefore reject a foreign keyHandle unless the key's owning product allowlisted the caller in its manifest, and MUST NOT offer a user prompt as a fallback: consenting to an opaque message is not meaningful consent, and only the key's owner is positioned to evaluate the risk.

container.handleAccountCreateProof(async ([keyHandle, context, ring, message], { ok, err }) => {
  const [owner] = keyHandle;
  const entry = await registry.lookup(keyHandle);
  if (!entry) {
    return err(new CreateProofErr.KeyNotRegistered());
  }
  // The owner's manifest allowlist is the ONLY authorization here — no prompt.
  if (owner !== productId && !(await ownerAllowlists(owner, productId))) {
    return err(new CreateProofErr.NotAllowlisted());
  }
  if (!entry.rings.some(declared => sameRing(declared, ring))) {
    return err(new CreateProofErr.KeyNotInRing());
  }
  if (!(await isMemberOfRing(entry, ring))) {
    return err(new CreateProofErr.NotMember());
  }
  const { proof, contextualAlias, ringIndex, ringRevision } = await createRingProof(entry, context, ring, message);
  return ok({ proof, contextualAlias, ringIndex, ringRevision });
});

handleAccountRingVrfSign

Signs with the member key itself instead of producing an anonymous ring proof (RFC-0024). It carries no context and no ring, so there is nothing to scope what the signature is good for — it is the wider version of the bearer-token problem above, gated by the same allowlist with the same no-prompt rule. The result is verified against the member public key and is linkable to every other use of that key.

container.handleAccountRingVrfSign(async ([keyHandle, message], { ok, err }) => {
  if (!isConnected()) {
    return err(new RingVrfSignErr.NotConnected());
  }
  const [owner] = keyHandle;
  if (!(await registry.lookup(keyHandle))) {
    return err(new RingVrfSignErr.KeyNotRegistered());
  }
  if (owner !== productId && !(await ownerAllowlists(owner, productId))) {
    return err(new RingVrfSignErr.NotAllowlisted());
  }
  return ok(await signWithMemberKey(keyHandle, message));
});

handleAccountSignVrf

Produces an sr25519 (schnorrkel) VRF signature over a transcript the product supplies as a recipe (RFC-0023). Replay it verbatim — no interpretation of labels or values — so one method serves any consuming runtime. Authorize it exactly like handleSignRaw: reject with NotConnected when there is no session (never auto-prompt login), sign locally when AutoSigning covers the account, otherwise ask the user and return Rejected on decline. Bound items.length and the total transcript size against a hostile caller.

container.handleAccountSignVrf(async ({ account, transcriptLabel, items }, { ok, err }) => {
  if (!isConnected()) {
    return err(new SignVrfErr.NotConnected());
  }
  if (!(await confirmVrfSigning(account))) {
    return err(new SignVrfErr.Rejected());
  }

  const transcript = newTranscript(transcriptLabel);
  for (const item of items) {
    transcript.appendMessage(item.label, item.value);
  }
  const { preOutput, proof } = await vrfSign(account, transcript);

  return ok({ preOutput, proof });
});

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, derivationIndexBytes(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') {
    // Account of the calling product, addressed by the RFC-0022 selector.
    await transferFromProductAccount(derivationIndexBytes(source.value), 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);
});

Derivation index helpers

Product accounts live at //product//{productId}/{index} (RFC 0022): two hard junctions and a soft one whose chain code is a 32-byte derivation index. The wire selector — ProductAccountId's index, ProductProofContext's suffix, the ProductAccount top-up source and SmartContractAllowance — carries either a plain u32 or those 32 bytes directly; these helpers expand it.

import { INDEX_MAGIC, derivationIndexBytes, indexBytes } from '@novasamatech/host-container';

// blake2b256("product-account-index")[..28] — keeps the plain-index space and
// the raw-index space disjoint.
INDEX_MAGIC;

// u32 little-endian ++ INDEX_MAGIC. A product's default account is index 0.
indexBytes(0);

// Wire selector → the 32 bytes used as the soft junction's chain code.
derivationIndexBytes({ tag: 'Index', value: 5 }); // === indexBytes(5)
derivationIndexBytes({ tag: 'Raw', value: raw32 }); // === raw32

Note that stock tooling (polkadot-js, subkey) cannot express this path: the 32-byte index is not a typeable path segment, so //product//browse.dot/5 there does not derive index 5.

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