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

@r3pos/ecom-storefront

v0.1.0

Published

Browser-side SDK for the R3 Ecom Platform API. Used with a PUBLISHABLE key (r3_pk_…) in a customer-facing shop: catalog and inventory reads, order creation, and checkout. Exposes nothing a publishable key cannot do. Built on @r3pos/ecom-core.

Readme

@r3pos/ecom-storefront

The browser-side SDK for the R3 Ecom Platform API — the commerce API behind R3 POS. Use it in a customer-facing shop with a publishable key to read the catalog and inventory, place an order, run checkout, and follow that order to pickup.

For it: anyone building the shop itself — a merchant's own site, an agency's custom storefront, a React/Vue/Svelte front end, or the client half of a Next.js/Remix/Astro app.

Server code needs the sibling package, @r3pos/ecom, which takes a secret key and adds order listing, webhook management and webhook verification.

Install

npm install @r3pos/ecom-storefront

Ships ESM and CommonJS with TypeScript declarations and source maps — CJS included because a shop is routinely server-rendered before it is hydrated, and the server half of a framework may still be loading CommonJS.

Quickstart

import { createStorefrontClient } from '@r3pos/ecom-storefront';

const client = createStorefrontClient({
  apiKey: 'r3_pk_live_x8Kq2mBvR7nT4wYzC1aH6dLpF9sJgE3uN5oM0iX2rQw', // publishable: safe in a bundle
  baseUrl: 'https://api.r3pos.com', // origin only — /ecom/v1 is added for you
});

// Shop front
const merchant = await client.merchant.get();
const { data: products } = await client.catalog.products.list({ q: 'latte', limit: 20 });
const { data: categories } = await client.catalog.categories.list();

// Cart → order. All money is computed server-side from the merchant's own
// prices, so a price the page sends is ignored and cannot be tampered with.
const order = await client.orders.create(
  { items: [{ productId: products[0]!.id, quantity: 2 }] },
  { idempotencyKey: cart.idempotencyKey },
);

// ⚠️ Keep the token — it is the only way this client can read the order again
sessionStorage.setItem(`order:${order.id}`, order.orderToken);

// Pay
const session = await client.orders.checkout.create(order.id);
// session.mode is 'stripe' | 'helcim' | 'mock' | 'free' — branch on it, not on your config
// in 'stripe' mode: session.clientSecret + session.publishableKey feed Stripe.js

// Confirmation page
const fresh = await client.orders.get(order.id, { orderToken: order.orderToken });
console.log(fresh.number, fresh.status); // "1042" "preparing"

Build one client per page and share it. It is stateless apart from its configuration.

What is on the client

| Namespace | Methods | | --------------------------- | --------------------------------------------------------- | | client.merchant | get() | | client.catalog.products | list(), get(id), listAll() | | client.catalog.categories | list(), listAll() | | client.inventory | list(), listAll() | | client.orders | create(), get(id, { orderToken }) | | client.orders.checkout | create(orderId), confirm(orderId, body) | | client.key | the parsed key — kind, mode (live/test), prefix |

client.key.mode is how you render a "test mode" badge without guessing from the hostname.

Two things are deliberately absent. There is no way to list orders, and no webhook management or verification. GET /ecom/v1/orders needs orders:read, which a publishable key can never hold — exposing it would put every customer's order one devtools call away. Webhook verification needs the signing secret, which is a server secret by definition. Neither is missing from the docs; the methods do not exist on the object. Both live in @r3pos/ecom.

Reading an order back: the guest / orderToken flow

A shopper who never signs in still needs a confirmation page and a "your order is ready" screen. That is what orderToken is for.

orders.create() returns the order plus a one-time orderToken that grants read access to that single order and nothing else. It is the only order this client can read at all:

const order = await client.orders.create({ items: cart.lines });

// 1. Persist the token with the customer's session, keyed by order id.
sessionStorage.setItem(`order:${order.id}`, order.orderToken);

// 2. Read it back — the token is required, and required by the TYPE, because
//    there is no fallback: a publishable key holds no `orders:read` scope.
const status = await client.orders.get(order.id, { orderToken: order.orderToken });

// 3. Poll it on the confirmation page as the order moves
//    received → accepted → preparing → ready → picked_up
const token = sessionStorage.getItem(`order:${orderId}`)!;
const latest = await client.orders.get(orderId, { orderToken: token });

Treat the token as a bearer credential scoped to one order: hand it to the customer's own session (sessionStorage, a signed cookie, a URL you email only to them), never log it, and never render it anywhere another customer could see it. Lose it and the order is unreadable from the browser — recovering it means asking your server, which holds a secret key and can read any order with @r3pos/ecom.

In Stripe mode the payment settles on a payment_intent.succeeded webhook rather than in the browser, so do not call checkout.confirm(); poll orders.get() with the token until paymentStatus reads succeeded. checkout.confirm() answers 409 for Stripe-mode checkouts.

Key classes: publishable here, secret on the server

An R3 Ecom API key is r3_<sk|pk>_<live|test>_<43 URL-safe base64 chars>.

  • r3_pk_… — publishable. Browser-safe by design. Limited to catalog:read, inventory:read and orders:write, plus a per-key origin allowlist the API enforces. This package requires one, and it is fine in your bundle.
  • r3_sk_… — secret. Every scope, including orders:read and webhooks:manage. Server only, in @r3pos/ecom.

createStorefrontClient refuses a secret key at construction:

createStorefrontClient({ apiKey: 'r3_sk_live_…', baseUrl });
// R3EcomInvalidKeyError: @r3pos/ecom-storefront needs a PUBLISHABLE key
// (r3_pk_live_… / r3_pk_test_…) but was given a SECRET key (r3_sk_…). …
// If this key has ever been served to a browser, ROTATE IT NOW.

If you mix them up: this is the failure worth caring about. A secret key works from a browser — the shop functions perfectly, nothing errors, nothing looks wrong — while the key sits in the JS bundle, in devtools, and in whatever page source a crawler cached. It carries orders:read and webhooks:manage: the merchant's entire order history, and the ability to re-point their webhooks. So the check happens at construction, on the developer's machine, before any of that can ship. A secret key that has ever reached a browser is compromised — rotate it. Only a hash is stored server-side, so keys are rotated, never recovered.

The other direction is harmless here: a publishable key is what this package wants. If it 403s, the cause is usually the origin allowlist rather than the key.

Error handling

Every failure is an R3EcomError subclass carrying a literal kind, the HTTP status (0 when no response arrived), the server's code, and the X-Request-Id when there was one. Each has a type guard, so a catch is one check rather than a status comparison.

import {
  isValidationError,
  isConflictError,
  isForbiddenError,
  isNetworkError,
  isRateLimitError,
  isR3EcomError,
} from '@r3pos/ecom-storefront';

try {
  const order = await client.orders.create({ items: cart.lines });
  goToCheckout(order);
} catch (err) {
  if (isValidationError(err)) {
    // 422 — e.g. an empty cart, or a product id that no longer exists
    return showCartError('Something in your cart is no longer available.');
  }
  if (isConflictError(err)) {
    // 409 — the order already moved on (already paid, already cancelled)
    return refreshOrder();
  }
  if (isForbiddenError(err)) {
    // 403 — missing scope, or this origin is not on the key's allowlist
    return showFatal('This shop is not configured correctly.');
  }
  if (isNetworkError(err) || isRateLimitError(err)) {
    return showRetry('Connection problem — try again.');
  }
  if (isR3EcomError(err)) {
    reportToSentry({ kind: err.kind, code: err.code, requestId: err.requestId });
  }
  throw err;
}

Note that a CORS rejection surfaces as R3EcomNetworkError, not R3EcomForbiddenError — the browser blocks the response before the SDK can read the status. If every call fails with a network error from one origin but works from another, check the key's origin allowlist first.

The full family: R3EcomAuthError (401), R3EcomForbiddenError (403), R3EcomNotFoundError (404), R3EcomValidationError (400/422), R3EcomConflictError (409), R3EcomRateLimitError (429, with retryAfterSeconds), R3EcomServerError (5xx), R3EcomTimeoutError, R3EcomNetworkError, R3EcomInvalidKeyError and R3EcomPaginationError — each re-exported here with its guard, plus the catch-alls isR3EcomError and isRetryableError.

Idempotency: the double-tapped "Place order" button

orders.create always sends an Idempotency-Key; without one you supply, the SDK mints a UUID for that call. That covers the transport's own retries — which matters on a phone, where a dropped connection otherwise surfaces as an error on a request that may well have created the order.

It does not cover the customer tapping the button again, because a new call mints a new key. Mint one key when the cart is created and pass it every time:

import { generateIdempotencyKey } from '@r3pos/ecom-storefront';

// once, when the cart is created
cart.idempotencyKey = generateIdempotencyKey();

// on every "Place order" attempt
const order = await client.orders.create(
  { items: cart.lines },
  { idempotencyKey: cart.idempotencyKey },
);

Replaying the same key with the same cart returns the same order. Replaying it with a different cart is a 409 (R3EcomConflictError) — mint a new key when the cart changes.

Pagination

Every list method returns a Page<T> — { data, nextCursor, hasMore } — and has a listAll() twin that walks the cursor lazily, one round trip at a time:

const first = await client.catalog.products.list({ limit: 24 });
const next = await client.catalog.products.list({ limit: 24, cursor: first.nextCursor! });

for await (const product of client.catalog.products.listAll({ categoryId })) {
  render(product);
  if (enoughForNow()) break; // breaking out stops the walk — no wasted requests
}

Configuration

createStorefrontClient accepts everything the transport does: apiKey, baseUrl, fetch, timeoutMs (default 30000), maxRetries (default 2), defaultHeaders, and the backoff knobs retryBaseDelayMs / retryMaxDelayMs / maxRetryAfterMs. userAgent is accepted but inert in a browser — browsers forbid setting that header and drop it silently.

Nothing is read from the environment; your app decides where the key comes from.

Reference

The wire contract — routes, scopes, error codes, pagination, idempotency and the webhook event catalogue — is documented in docs/ecom/CONTRACT.md.

License

MIT © R3 Lab