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

biznexus-store-sdk

v0.9.0

Published

Browser SDK for embedding a BizNexus tenant's online store catalog + checkout on any website. Public per-store API key + Firebase Anonymous Auth (via direct REST calls, no firebase package) - no secrets shipped to the client, and zero runtime dependencies

Readme

biznexus-store-sdk

Embed a BizNexus tenant's online store (catalog, variants, reviews, wishlist, and M-Pesa checkout) on any website - not just genetech-website - with no server-side code of your own and no secrets in the page. Published to the public npm registry (nothing in this package is a secret - see "How auth works" below).

Zero runtime dependencies - Firebase Auth (anonymous sessions and real email/password accounts) is done via direct REST calls (see src/authSession.js), not the firebase package, so nothing else ships to the page either.

Get a key

In the BizNexus app: Online Store → Stores → New Store. Copy the store's public key (pk_store_...) - that's the only credential you need. It's publishable, not secret: safe to put directly in your page's JS. It only ever lets a caller read that one store's visible catalog and place orders against it - nothing else, and it can be rotated (instantly revoking the old one) any time from the same screen.

By default a brand-new store accepts requests from any origin. Once you know your site's real domain, add it under that store's Allowed Origins to lock it down.

Install

npm install biznexus-store-sdk
import { OnlineStoreClient, StoreCart, formatCurrency } from "biznexus-store-sdk";

const store = new OnlineStoreClient({ publicKey: "pk_store_xxxxxxxx" });

Quick start (plain <script> tag, no build step, no npm at all)

For a site with no bundler (WordPress, a static HTML page, etc.), copy dist/biznexus-store-sdk.iife.js wherever that site serves static assets from:

<script src="/assets/biznexus-store-sdk.iife.js"></script>
<script>
  const store = new BiznexusStoreSDK.OnlineStoreClient({ publicKey: "pk_store_xxxxxxxx" });

  store.getCatalog().then((listings) => {
    console.log(listings); // [{ id, type, name, price, quantity, availability, image_url, ... }]
  });
</script>

API

Catalog

await store.getCatalog({ type: "product", minPrice: 500, maxPrice: 5000, limit: 50, offset: 0 }); // all optional
await store.getListing(12);        // single product/service, for a detail page
await store.getCategories();       // ["Electronics", "Home", ...] - for a filter sidebar
await store.search({ q: "laptop", type: "product", minPrice: 500, maxPrice: 5000 }); // FULLTEXT, prefix-matching search
await store.getBranding();         // { name, tagline, logo_url, primary_color, accent_color }
await store.getVatRate();          // number, e.g. 0.16

Every catalog/search row also carries avg_rating/review_count (see Reviews below), and size/color if the listing is part of a variant group. getListing(id) additionally returns:

const listing = await store.getListing(12);
listing.gallery_images; // string[] - extra photos beyond the cover image_url
listing.variants;       // this listing's size/color siblings, itself included (see Variants below)
listing.rating_breakdown; // { 5: 12, 4: 3, 3: 0, 2: 0, 1: 1 } - for a star bar chart

Every read above is cached in-memory for a short TTL (30s for catalog/search/listing/categories, 5min for branding/VAT) with in-flight de-duping - two components asking for the same thing at once share one request. Call store.clearCache() if your own app knows something changed and you want the next read to hit the network regardless. This is a small built-in cache, not RTK Query/Redux - deliberately, so the SDK works the same whether or not your app uses Redux. If you do use RTK Query, wrap this client's methods in your own baseQuery - it's a plain class, nothing here fights that.

Variants (size/color)

A "variant" (e.g. "Red T-Shirt, size M") is just another listing row with size/color set, grouped with its siblings under a shared parent. There's no separate variant object to fetch - getListing(id) on any row in the group returns the whole family via variants:

const listing = await store.getListing(12);
listing.variants;
// [
//   { id: 12, name: "T-Shirt", size: "S", color: "Red", price: 1200, quantity: 4, availability: "in_stock", image_url: "..." },
//   { id: 13, name: "T-Shirt", size: "M", color: "Red", price: 1200, quantity: 0, availability: "out_of_stock", image_url: "..." },
//   { id: 14, name: "T-Shirt", size: "M", color: "Blue", price: 1200, quantity: 9, availability: "in_stock", image_url: "..." },
// ]

A listing with no variant group just gets itself back (variants.length === 1) - check that before deciding whether to render a size/color picker at all. Picking a different variant is just getListing(otherVariant.id) (or use the row already in hand from variants) - each has its own independent price/stock/checkout, same as any other listing.

size is a plain string with no fixed vocabulary - "S"/"M"/"L", a shoe size ("42", "9.5"), or anything else a tenant's staff typed when creating the listing. variants comes back pre-sorted server-side so you don't have to: numeric-looking sizes ("6", "7", "8", "9", "10") sort in actual numeric order rather than insertion order (where "10" would otherwise land before "9"), and non-numeric sizes fall back to alphabetical - no client-side sorting needed. A variant's own availability/quantity still tell you whether that specific size/color is in stock; a picker built from variants should render every size (so a shopper can see what exists at all) but visually distinguish the sold-out ones rather than only showing what's currently purchasable - see templates/sokoni-store-react's Product screen for a reference implementation (grays out sold-out size/color chips instead of hiding them).

Reviews & ratings

Reading reviews needs no account; writing one does (see Accounts below).

await store.getListingReviews(12, { limit: 20, offset: 0 }); // newest first
// [{ id, rating, title, body, is_verified_purchase, reviewer_name, created_at }, ...]

await store.submitReview(12, { rating: 5, title: "Great fit", body: "Runs true to size." });
// re-submitting for the same listing edits your existing review, not a duplicate
await store.deleteReview(12); // deletes your own review only

is_verified_purchase is set automatically (from whether you have a paid order containing that listing) - there's no input for it.

Wishlist (requires an account)

await store.getWishlist();             // full listing objects, newest-saved first
await store.addToWishlist(12);         // idempotent - adding twice is a no-op
await store.removeFromWishlist(12);

Delivery addresses (requires an account)

A saved address is a map pin, not a typed street address - riders/drivers navigate to a pin far more reliably than they parse free text. Bring your own map picker (any library that gives you a { lat, lng } on click/drag works); this SDK just stores/retrieves the result.

await store.getMyAddresses();
// [{ id, label, latitude, longitude, notes, is_default, created_at, updated_at }, ...]
// default pin (if any) first

const home = await store.addAddress({
  label: "Home",
  latitude: -1.2921,
  longitude: 36.8219,
  notes: "Blue gate, call on arrival", // optional
  isDefault: true,                     // optional - exclusive, unsets any other default
});
// a shopper's very first address becomes default automatically even without isDefault

await store.updateAddress(home.id, { label: "Home (new)", isDefault: true });
await store.removeAddress(home.id);

Pass one of these straight to createOrder as deliveryAddressId at checkout - see below.

Cart (local, no server round-trip)

There's no server-side "cart" - an order is created directly from a line-item list at checkout. StoreCart is a small localStorage-backed helper so you don't have to build that part yourself:

import { OnlineStoreClient, StoreCart } from "biznexus-store-sdk";

const cart = new StoreCart();
const listing = await store.getListing(12);

cart.add(listing, 2);              // survives a page reload
cart.setQuantity(listing.id, 3);
cart.remove(listing.id);
cart.itemCount;                    // 3
cart.subtotal;                     // sum of price * quantity
cart.toOrderItems();               // [{ listing_id, quantity }] - ready for createOrder below

Checkout

await store.checkAvailability(cart.toOrderItems()); // pre-flight - surfaces "only 2 left" before payment
// { items: [{ listing_id, available, reason?, quantity_available? }], all_available }

const order = await store.createOrder({
  items: cart.toOrderItems(),
  customerName: "Jane Doe",
  customerPhone: "254712345678",
  customerEmail: "[email protected]", // optional
  notes: "Leave at the gate",        // optional

  // Delivery is entirely optional - omit deliveryMethod for the same
  // behavior as before (no delivery info recorded, business handles
  // fulfillment out of band).
  deliveryMethod: "delivery",        // 'pickup' | 'delivery'
  deliveryAddressId: address.id,     // one of getMyAddresses()'s saved pins (requires an account) ...
  // ...OR, for a guest or a one-off location, skip deliveryAddressId and pass a pin directly:
  // deliveryLatitude: -1.2921,
  // deliveryLongitude: 36.8219,
  // deliveryLabel: "Office - 4th floor",
  // deliveryNotes: "Blue gate, call on arrival",
});
// order.status === 'processing' - an M-Pesa prompt has been sent to customerPhone

const finalStatus = await store.waitForOrder(order.public_token, {
  onUpdate: (status) => console.log("still", status.status),
});
// resolves once status leaves 'pending' (paid/failed/cancelled), or throws after ~90s of no answer

deliveryMethod: "delivery" requires either deliveryAddressId or both deliveryLatitude/deliveryLongitude - the server rejects the order otherwise. deliveryMethod: "pickup" (or omitting it) needs none of the above. Whichever pin is used is snapshotted onto the order at checkout time, so editing or deleting a saved address later never changes a past order's delivery location.

formatCurrency(order.total_amount)"KES 4,500".

Event tracking

getListing(), search() (when q is given) and createOrder() already fire product_viewed / product_searched / checkout_started + order_created on their own - nothing to wire up. For anything else (most notably adding to cart, since StoreCart is a deliberately network-free local class with no reference to the client):

const lines = cart.add(listing, quantity);
store.track("product_added_to_cart", { listingId: listing.id, properties: { quantity } });

track() is fire-and-forget and never throws - a tracking failure never breaks or slows down the storefront. This is the raw event log a future BizNexus dashboard reads conversion analytics/abandoned-cart detection/recommendations from - nothing computes those yet, this just makes sure the data exists.

Accounts (optional - guest checkout always works)

Every method above already works with no account at all - createOrder works from a plain anonymous session. Accounts are additive: a shopper who signs up gets order history and doesn't have to retype their details next time, but nothing requires it. Each account is scoped to this one store - the same email/password used at a different BizNexus-powered store is a completely separate account there, with its own separate order history.

// { name, phone } are optional - saved to the shopper's profile right after signup.
const customer = await store.signUp({
  email: "[email protected]",
  password: "correct horse battery staple",
  name: "Jane Doe",
  phone: "254712345678",
});

const customer2 = await store.logIn({ email: "[email protected]", password: "..." });

store.getLocalUser();
// { uid, email, isAnonymous } for whichever session is currently active - synchronous, no
// network call, safe to check on every render for "logged in as ..." UI.

await store.getCurrentCustomer();          // { id, email, name, phone, created_at }
await store.updateProfile({ name: "Jane K. Doe" });
await store.getMyOrders({ limit: 20 });    // this store's orders placed while logged in

await store.sendPasswordReset("[email protected]"); // emails a reset link if that account exists
await store.sendEmailVerification();               // emails a 6-digit code to the current account
await store.confirmEmailVerification("123456");    // marks it verified once jane enters that code

store.logOut(); // falls back to a fresh anonymous session - browsing/guest checkout keeps working

createOrder automatically links the order to the logged-in shopper when there's an active account - no extra step needed at checkout for that to show up in getMyOrders() afterward. A guest order placed before signing up is never retroactively linked.

How auth works (and why there's no "secret key")

Nothing shipped to the browser in this SDK is a secret - a secret embedded in client-side JS isn't actually secret (anyone can read it from dev tools). Instead:

  • Public key identifies which store, and is checked against that store's registered origin allowlist server-side.
  • A Firebase session token - anonymous by default (handled automatically by this SDK on first use, via direct REST calls to Identity Toolkit - see src/authSession.js - not the firebase package), or a real one once signUp/logIn has been called - proves the request came from a real browser session, not a fabricated request. The backend only ever uses this to tell "real account" from "anonymous" (for the account-only endpoints above) - the public key + origin above are what actually scope what it can access.
  • Firebase App Check (optional, recommended once you've set it up for your own site) is the real bot/scraping deterrent - pass a getAppCheckToken function to the client if you have it.

See api/middleware/storefrontAuth.js in the main repo for exactly what the backend checks on every request.

Prerequisite: both the Anonymous and Email/Password sign-in providers need to be enabled for the genetech-hub Firebase project (Console → Authentication → Sign-in method) - signUp/logIn fail with auth/operation-not-allowed otherwise.

Building from source

npm install
npm run build

Outputs dist/biznexus-store-sdk.esm.js, dist/biznexus-store-sdk.iife.js (global BiznexusStoreSDK), and dist/index.d.ts.

Not yet included

  • Faceted filtering beyond type/category/price (brand, rating threshold, etc.) - search/getCatalog cover type + price range today; anything richer needs client-side post-filtering over a page of results for now.
  • Coupons/discount codes, flash sales, and "customers also bought" recommendations.
  • True multi-vendor marketplace semantics - every store still belongs to exactly one BizNexus business (see db-init/genetech_db.sql's online_stores table).