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

@doswiftly/storefront-sdk

v21.0.1

Published

Storefront runtime SDK for DoSwiftly Commerce — layered transport, middleware pipeline, React providers, Zustand stores, cache strategies. 0 runtime dependencies in core.

Downloads

3,368

Readme

@doswiftly/storefront-sdk

Layered runtime SDK for DoSwiftly Commerce storefronts. Framework-agnostic core + React adapter, 0 runtime dependencies in core.

Architecture

@doswiftly/storefront-sdk
├── core (.)              — Framework-agnostic: transport + middleware pipeline,
│                           CartClient / AuthClient, cart recovery runner,
│                           cart capability cookie (id + secret), auth route helpers,
│                           bot-protection managers, errors, format utilities,
│                           sanitizeHtml, normalizeConnection, schema enums
├── react (./react)       — React adapter: StorefrontProvider, CartManagerProvider,
│                           Zustand stores (Context-based), auth/cart/session hooks,
│                           pre-built headless components
├── react/server          — Server-side: client factory, SDK-BFF auth route
│                           (createStorefrontAuthRoute), getInitialAuth,
│                           first-party cookie readers, server cart-secret middleware
└── cache (./cache)       — Cache strategy functions

Core works everywhere: Node.js, Edge Workers, Deno, Bun, CLI scripts — without React. React adapter requires react ^18 || ^19 and zustand ^5 as peer dependencies.

Installation

pnpm add @doswiftly/storefront-sdk

Configuration

createStorefrontClient and <StorefrontProvider> take apiUrl and shopSlug as explicit config — the SDK does not read environment variables, sniff hostnames, or inspect request headers. The storefront supplies the values; the SDK uses them verbatim.

Scaffolded storefronts (doswiftly init) ship a graphqlConfig helper (lib/graphql/config.ts) that resolves both values from three sources in order:

| Source | When | What it gives you | | --- | --- | --- | | doswiftly.config.ts (preferred) | doswiftly init storefronts | A committed config file with both values. No env wiring needed in normal use. | | NEXT_PUBLIC_API_URL + NEXT_PUBLIC_SHOP_SLUG (fallback) | Local development; storefronts scaffolded outside doswiftly init | Standard Next.js public env vars. doswiftly dev rewrites NEXT_PUBLIC_API_URL to a local CORS proxy at runtime so client-side calls don't hit production CORS headers. | | http://localhost:8000 + demo-shop (defaults) | Empty smoke test | Last-resort placeholders so the project boots before any config is written. |

If you go the env-var route, use exactly these namesdoswiftly dev keys off them when overriding. Inventing API_URL, STOREFRONT_URL, or TENANT_SLUG means the dev proxy starts, but your storefront still calls the production API directly and you only learn about it on the first client-side mutation (build and SSR pass silently).

Scratch-built storefronts can read process.env.NEXT_PUBLIC_* directly and pass the values into config={} — the resolution helper is a convenience, not a requirement.

Quick start — Next.js App Router

Four files give you a production-grade storefront runtime: first-party auth cookies with automatic session refresh, a shared cart with stale-cart auto-recovery, and a typed GraphQL client with the full middleware pipeline.

1. app/api/auth/[action]/route.ts — one file mounts the whole auth surface (POST /api/auth/login | refresh | logout, GET /api/auth/whoami). The handlers run on the storefront's own domain, call the backend server-to-server, and own the first-party httpOnly cookies — the refresh token never reaches browser JavaScript:

import {
  createStorefrontAuthRoute,
  trustedForwardedHostValidator,
} from '@doswiftly/storefront-sdk/react/server';

export const { GET, POST } = createStorefrontAuthRoute({
  apiUrl: process.env.NEXT_PUBLIC_API_URL!,
  shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
  // Pass when the storefront runs behind a reverse proxy that rewrites Host
  // (DoSwiftly hosting, Vercel). Omit for bare deployments / local dev.
  isTrustedOrigin: trustedForwardedHostValidator,
});

2. app/layout.tsx — seed the first render from the first-party cookies via getInitialAuth() (no signed-out flash, no whoami round-trip) and wrap the tree in StorefrontProvider:

import { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
import { getStorefrontClient, getInitialAuth } from '@doswiftly/storefront-sdk/react/server';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const serverClient = getStorefrontClient({
    apiUrl: process.env.NEXT_PUBLIC_API_URL!,
    shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
  });

  // `SHOP_CONFIG_QUERY` is your own operation — generated by graphql-codegen from
  // `@doswiftly/storefront-operations/schema.graphql`. It must request the fields
  // of the `ShopConfig` shape: `currencyCode`, `supportedCurrencies`,
  // `defaultLanguage`, `supportedLanguages`, `botProtection`.
  const [{ shop }, initialAuth] = await Promise.all([
    serverClient.query(SHOP_CONFIG_QUERY),
    getInitialAuth(),
  ]);

  return (
    <html lang="pl">
      <body>
        <StorefrontProvider
          config={{
            apiUrl: process.env.NEXT_PUBLIC_API_URL!,
            shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
          }}
          shopData={shop}
          initialIsAuthenticated={initialAuth.isAuthenticated}
          initialAccessToken={initialAuth.accessToken}
          initialExpiresAt={initialAuth.expiresAt}
        >
          {children}
        </StorefrontProvider>
      </body>
    </html>
  );
}

Session refresh is automatic — the provider defaults to autoRefresh in the browser: a scheduler renews the access token shortly before it expires, and a 401 on a read query triggers a single deduped refresh + replay. Pass autoRefresh={false} to drive refreshing yourself.

3. Cart — wrap the shopping subtree in CartManagerProvider (one shared cart manager: one loading state, one recovery queue) and read it with useCartManagerContext():

'use client';
import { CartManagerProvider, useCartManagerContext } from '@doswiftly/storefront-sdk/react';
import { toast } from 'sonner';

export function ShopProviders({ children }: { children: React.ReactNode }) {
  return (
    <CartManagerProvider
      onMutationError={(operation, error) => toast.error(error.message)}
    >
      {children}
    </CartManagerProvider>
  );
}

export function AddToCart({ variantId }: { variantId: string }) {
  const { addItem, status } = useCartManagerContext();
  return (
    <button
      onClick={() => addItem([{ variantId, quantity: 1 }])}
      disabled={status.type === 'loading'}
    >
      Add to cart
    </button>
  );
}

The cart cookie, the cart access secret, creation on first add, and stale-cart recovery are all handled for you — see Cart.

4. Global session + cart expiry handling — mount once near the root:

'use client';
import { useEffect } from 'react';
import { useSessionExpired, useCartManagerContext } from '@doswiftly/storefront-sdk/react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';

export function GlobalGuards() {
  const router = useRouter();
  const { onExpired } = useCartManagerContext();

  // Fired when the SDK can no longer keep the customer session alive.
  useSessionExpired(() => router.replace('/auth/login?reason=session_expired'));

  // Fired when a stale cart cannot be transparently recovered.
  useEffect(
    () => onExpired(() => toast.error('Your cart expired — please add the items again')),
    [onExpired],
  );
  return null;
}

Quick start — Core (framework-agnostic)

import {
  createStorefrontClient,
  cartSecretMiddleware,
  retryMiddleware,
  timeoutMiddleware,
  errorMiddleware,
  CartClient,
  formatCartCookieValue,
} from '@doswiftly/storefront-sdk';

let cartSecret: string | null = null;

const client = createStorefrontClient({
  apiUrl: 'https://api.doswiftly.pl',
  shopSlug: 'my-shop',
  middleware: [
    cartSecretMiddleware(() => cartSecret), // lazy getter — picks up rotation
    retryMiddleware({ maxRetries: 2 }),
    timeoutMiddleware({ timeout: 5000 }),
    errorMiddleware(), // ALWAYS LAST
  ],
});

const cartClient = new CartClient(client);

// `create` reveals a one-time cart access secret — store it immediately.
// Possession of the secret is what authorizes cart reads and writes.
const { cart, secret } = await cartClient.create();
cartSecret = secret;
if (secret) {
  persistCookie('cart-id', formatCartCookieValue({ cartId: cart.id, cartSecret: secret }));
}

const { cart: updated, warnings } = await cartClient.addItems(cart.id, [
  { variantId: 'variant-123', quantity: 1 },
]);

Queries are deduplicated and cacheable; mutations are never cached and never retried.

Export paths

| Path | Description | Dependencies | |------|-------------|-------------| | @doswiftly/storefront-sdk | Core: transport, middleware, clients, recovery, errors, format, enums, cookie contracts | 0 | | @doswiftly/storefront-sdk/react | Providers, hooks, stores, pre-built UI components | react, zustand | | @doswiftly/storefront-sdk/react/server | Server client factory, SDK-BFF auth route, cookie readers | react (peer; getInitialAuth additionally requires Next.js) | | @doswiftly/storefront-sdk/cache | Cache strategy functions | 0 |

Authentication

Session model

Auth runs through BFF route handlers on the storefront's own domain (createStorefrontAuthRoute). The browser never talks to the backend directly for auth — the route handlers do, server-to-server. First-party cookies work identically on a platform subdomain, a custom domain, and off-platform hosting.

| Cookie | httpOnly | Path | Purpose | |--------|----------|------|---------| | customerAccessToken | yes | / | Access token — read server-side to seed SSR and the in-memory store | | customerRefreshToken | yes | /api/auth | Refresh token — read exclusively server-side by the BFF route | | session-expiry | no | / | Readable absolute expiry (ISO 8601) for the refresh scheduler |

Renewal is automatic (<StorefrontProvider autoRefresh> — default ON in the browser):

  • Proactive — a scheduler calls POST {authBasePath}/refresh shortly before expiresAt; the route rotates the refresh cookie and returns a fresh access token.
  • Reactive — a 401 on a read query triggers one deduped refresh and replays the query. A 401 on a mutation never retries — it fires the session-expired signal instead (replaying a mutation, e.g. a payment, would be unsafe).

When the session cannot be kept alive, subscribe globally:

'use client';
import { useSessionExpired } from '@doswiftly/storefront-sdk/react';

useSessionExpired((event) => router.replace('/auth/login'));

Sign-in form (BFF login)

POST {authBasePath}/login is the only flow that sets the full cookie set (access + refresh + expiry) on the storefront domain — required for automatic session refresh. Post the credentials, then seed the client-side store with the returned token:

'use client';
import { useAuthStore } from '@doswiftly/storefront-sdk/react';

export function LoginForm() {
  const setAuth = useAuthStore((s) => s.setAuth);

  async function onSubmit(email: string, password: string) {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    if (!res.ok) {
      // Backend errors are passed through verbatim (already localized).
      const body = await res.json().catch(() => null);
      showError(body);
      return;
    }
    const { accessToken, expiresAt, customer } = await res.json();
    setAuth(customer ?? null, accessToken, expiresAt);
  }
  // ...
}

After a successful sign-in, merge the guest cart into the customer's cart with cartClient.merge(guestCartId) — see Auth ↔ cart lifecycle.

GraphQL auth hooks — useLogin / useLogout / useAuth

The focused hooks drive auth over the GraphQL transport (customerLogin / customerLogout mutations) and keep the token in the in-memory store. The backend sets/clears its own httpOnly cookie on these mutations, on the API domain — fine for same-site setups and non-browser clients. For first-party cookies on the storefront domain (and the refresh cookie required by autoRefresh) prefer the BFF login above; the optional onSetToken / onClearToken callbacks let you sync a cookie through your own route during migration from older setups.

import { useLogin, useLogout } from '@doswiftly/storefront-sdk/react';

const { login, isLoggingIn, error } = useLogin();
const { logout, isLoggingOut } = useLogout();

const result = await login(email, password);
if (!result.success) showErrors(result.userErrors); // backend-translated messages

useLogout additionally downgrades the active cart to guest before logging out (clears the customer's contact details, addresses and saved-payment selection so they do not linger on a shared device) — best-effort, never blocks sign-out.

useAuth(options?) is a convenience facade aggregating useLogin, useLogout and useRefreshToken:

const {
  login, logout, refreshToken,
  isLoggingIn, isLoggingOut, isRefreshingToken, isLoading,
  error,
} = useAuth();

Auth state (customer, flags) lives in the store, not in the hooks:

import { useAuthStore, useAuthHydrated } from '@doswiftly/storefront-sdk/react';

const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const customer = useAuthStore((s) => s.customer);
const authHydrated = useAuthHydrated(); // true after persist rehydration

AuthClient (no React)

import { AuthClient } from '@doswiftly/storefront-sdk';

const authClient = new AuthClient(client, { authBasePath: '/api/auth' });

| Method | Returns | Notes | |---|---|---| | login(email, password) | Promise<AuthResult> | GraphQL mutation; throws StorefrontError with backend-translated userErrors | | register(input) | Promise<AuthResult> | CustomerCreateInput; result carries customer | | logout() | Promise<void> | Never throws (token may already be expired) | | refreshSession() | Promise<SessionRefreshResult> | Same-origin POST {authBasePath}/refresh (BFF) — works with an expired access token; throws SESSION_EXPIRED on failure | | getCustomer() | Promise<Customer \| null> | null when unauthenticated | | getAddresses() | Promise<MailingAddress[] \| null> | Saved address book incl. B2B fields (taxId, vatNumber) and isDefault; null when unauthenticated | | refreshToken() | Promise<AuthResult> | Deprecated — GraphQL refresh requires a still-valid access token; use refreshSession() |

Low-level route handlers (escape hatch)

createStorefrontAuthRoute is the recommended mount. The standalone Web API factories remain available for custom setups and migrations:

import {
  createSetTokenHandler,   // POST — sets the httpOnly access-token cookie
  createClearTokenHandler, // POST — clears it
  createWhoamiHandler,     // GET  — hydrates { isAuthenticated, customer } from the cookie
} from '@doswiftly/storefront-sdk';

export const POST = createSetTokenHandler();

All handlers are pure Web API (Request/Response) — they run in Next.js Route Handlers, Cloudflare Workers, Deno, etc. Security baked in: strict origin validation, Content-Type check, SameSite=Lax, httpOnly cookies.

Behind a reverse proxy

When a proxy rewrites/strips Host (DoSwiftly hosting, Vercel, NGINX), strict Origin host === Host validation would 403 every auth call. Every handler (and createStorefrontAuthRoute) accepts an isTrustedOrigin predicate:

import {
  trustedForwardedHostValidator, // trust X-Forwarded-Host (set by the proxy)
  originAllowlistValidator,      // static allowlist of origins
} from '@doswiftly/storefront-sdk';

createStorefrontAuthRoute({ apiUrl, shopSlug, isTrustedOrigin: trustedForwardedHostValidator });
// or
createSetTokenHandler({
  isTrustedOrigin: originAllowlistValidator(['https://shop.example.com']),
});

Custom predicates get ({ origin, originHost, request }) and may be async. A throwing predicate fails closed (falls back to strict matching).

Auth token client (client-side helper)

import { createAuthTokenClient } from '@doswiftly/storefront-sdk';

const { setToken, clearToken } = createAuthTokenClient();
await setToken(accessToken);  // POST /api/auth/set-token
await clearToken();           // POST /api/auth/clear-token

Cart

Capability model — cart id + secret

Cart access is authorized by possession of a secret, not by the customer session. The cart-id cookie stores a composite value "<cartId>.<secret>" (30 days, SSR/edge-visible, not httpOnly — the cart carries no payment data). CartClient.create() and recoveryRedeem() reveal the secret once; the SDK persists it into the cookie for you. Every request then carries the secret in the x-cart-secret header via middleware:

  • Browser: StorefrontProvider wires cartSecretMiddleware automatically — the secret is read lazily from the cookie on every request, so a rotated secret is picked up without rebuilding the client.
  • Server (SSR/edge): prepend serverCartSecretMiddleware(await readCartCredentials()) to your server client — see Server-side.
  • Custom runtimes: cartSecretMiddleware(() => secret) + the parseCartCookieValue / formatCartCookieValue helpers.

A cookie without the secret half (or a stale capability) makes the cart unreachable — mutations reject with CART_NOT_FOUND and the standard recovery flow recreates a fresh cart.

useCartManager — cookie-driven cart + checkout lifecycle

The primary React cart API. Owns the cart-id cookie, auto-creates the cart on first add, persists the secret, and recovers from stale carts per operation:

'use client';
import { useCartManager } from '@doswiftly/storefront-sdk/react';

const {
  // Read
  getCart, getCartId,
  // Mutations (all return Promise<CartMutationOutcome> = { cart, warnings })
  addItem, updateItem, removeItem,
  updateBuyerIdentity, setShippingAddress, setBillingAddress,
  updateDiscountCodes, updateNote, updateAttributes,
  selectShippingMethod, selectPaymentMethod, clearPaymentSelection,
  applyGiftCard, removeGiftCard, updateGiftCardRecipient,
  // Completion
  complete,      // Promise<CartCompleteOutcome> = { order, warnings }
  createPayment, // Promise<PaymentSession> — post-completion, works on orderId
  // Lifecycle
  clearCart, onExpired,
  // Reactive state
  status,        // tagged union — see below
  isLoading, error, // derived selectors over `status`
} = useCartManager(options?);

Per-operation recovery strategy — when a write hits a stale cart (userErrors[].codeCART_NOT_FOUND / ALREADY_COMPLETED):

| Strategy | Operations | Behaviour | |---|---|---| | Auto-replay | addItem, updateBuyerIdentity, setShippingAddress, updateDiscountCodes, updateNote, updateAttributes | Transparently recreates the cart via an atomic cartCreate(input) and resolves as success | | Bail + event | updateItem, removeItem, setBillingAddress, selectShippingMethod, selectPaymentMethod, clearPaymentSelection, applyGiftCard, removeGiftCard, updateGiftCardRecipient, complete | Clears the cookie, throws CartRecoveryNotPossibleError, and fires every onExpired listener — subscribe once globally instead of try/catching every call site | | Out of scope | createPayment | Operates on orderId (post-completion) — no cart to recover |

Status is a tagged union for exhaustive rendering:

const { status } = useCartManager();
if (status.type === 'loading') return <Spinner label={status.operation} />;
if (status.type === 'error') return <ErrorBanner error={status.error} />;
// status.type ∈ { 'idle', 'success' }

Options (UseCartManagerOptions) — all additive:

| Option | Purpose | |---|---| | initialCartId | Server-known cart-id seed used when the cookie is empty on mount (cookie wins). Accepts a bare id or the composite "<cartId>.<secret>" value — pass the composite when the secret is known server-side. Use cases: SSR checkout, magic-link, embedded iframe, customer-service "view this cart", multi-cart B2B. | | onMutationStart / onMutationSuccess / onMutationError | Lifecycle callbacks around every operation — centralize toasts / router refresh / loading indicators. Cart expiry goes to onExpired, not onMutationError. | | cookieDebug | Debug sink for cart-id cookie writes — see Debug logging. |

<CartManagerProvider> — one shared instance

useCartManager keeps per-mount state, so calling it in several components creates independent managers. Wrap the subtree (inside StorefrontProvider) and read the shared instance with useCartManagerContext():

'use client';
import { CartManagerProvider, useCartManagerContext } from '@doswiftly/storefront-sdk/react';

<CartManagerProvider
  initialCartId={initialCartId}
  onMutationSuccess={() => router.refresh()}
  onMutationError={(operation, error) => toast.error(error.message)}
>
  <CheckoutForm />
</CartManagerProvider>;

// CheckoutForm.tsx
const { addItem, complete, status } = useCartManagerContext();

For deliberately independent managers (multi-cart B2B, an admin "view this cart" panel) call useCartManager() directly instead.

Checkout completion + payment

const { complete, createPayment } = useCartManagerContext();

// 1. Finalize the cart into an Order. On success the cart-id cookie is cleared
//    and status resets to idle — a follow-up addItem creates a fresh cart.
const { order } = await complete();

// 2. Decide the payment flow from the Order itself — no hardcoded brand checks.
if (order.canCreatePayment) {
  const session = await createPayment({
    orderId: order.id,
    returnUrl: `${origin}/checkout/return`, // optional — must point to a verified shop domain
  });
  // Branch on session.flow: redirectUrl (redirect), clientSecret (embedded
  // widget), status (instant settlement). Failures throw with
  // `err.userErrors[0].code` of the PAYMENT_* family.
}

getBrowserDataForPayment() (from /react) is a standalone helper collecting the browser context fields used by strong-customer-authentication flows (user agent, screen, timezone, language); it throws BrowserDataNotAvailableError outside the browser — call it in an event handler, never during SSR.

The completed order also carries order.accessToken — an opaque token for guest order lookup via cartClient.getOrderByToken(token, email?).

Discovery queries (CartClient)

const cartClient = new CartClient(client);

// Cart-aware shipping preview for an address. Returns null when the cart is gone.
const payload = await cartClient.getAvailableShippingMethods(cartId, address);
if (payload) {
  if (payload.userErrors.length > 0) {
    // Backend business condition with a translated message
    // (e.g. a digital-only cart needs no shipping).
    show(payload.userErrors[0].message);
  } else {
    render(payload.methods, payload.freeShippingProgress);
    // each method carries deliveryType: HOME | PICKUP_POINT | LOCKER
  }
}

// Shop-level payment methods + the default pre-selection signal.
const { methods, defaultMethod } = await cartClient.getAvailablePaymentMethods();

// Validate a discount code before applying it.
const result = await cartClient.validateDiscountCode(cartId, 'SAVE10');
if (!result.isValid) show(result.error?.message); // backend-translated

// Guest order lookup by the opaque token from complete().
const order = await cartClient.getOrderByToken(token, email);

Error-handling contract across the SDK (messages are always backend-translated per Accept-Language — the SDK never synthesizes copy):

| Backend shape | SDK behaviour | You handle | |---|---|---| | Mutation with userErrors[] | Throws StorefrontError; err.userErrors[0].message is translated | try/catch, branch on err.userErrors[0].code | | Nullable query root | Returns T \| null | if (!result) … | | Structured payload (errors inside) | Returns the raw payload | Branch on payload.userErrors[].code / payload.error.code |

Auth ↔ cart lifecycle

// After a successful sign-in: merge the guest cart into the customer context.
// The secret is preserved — the same cookie keeps working.
await cartClient.merge(guestCartId);

// On sign-out: strip customer PII from the cart (contact details, addresses,
// saved-payment selection). `useLogout` calls this automatically.
await cartClient.downgradeOnLogout(cartId);

// Cart recovery links (e.g. from an abandoned-cart email): redeem the token.
// Rotates the secret — persist the new composite cookie value.
const { cart, secret } = await cartClient.recoveryRedeem(token);

Cart recovery without React

The same recovery semantics ship in core for Vue/Svelte/CLI/mobile consumers:

import {
  CartClient,
  createCartRecoveryRunner,
  recreateWithInput,
  CartRecoveryNotPossibleError,
  type CartCookieStore,
} from '@doswiftly/storefront-sdk';

// Implement the cookie port for your runtime
// (browsers can use createBrowserCartCookieStore from /react).
const cookieStore: CartCookieStore = {
  get: () => readCartCookie(),                    // returns the cart id
  set: (cartId, opts) => writeCartCookie(cartId, opts?.secret),
  clear: () => deleteCartCookie(),
};

const runner = createCartRecoveryRunner({ cartClient, cookieStore });

runner.onExpired((event) => {
  console.warn(`Cart expired (${event.reason}) — reset local state.`);
});

// Auto-replay: the caller never thinks about stale carts.
const { cart, warnings } = await runner.execute({
  name: 'addItems',
  run: (cartId) => cartClient.addItems(cartId, [{ variantId: 'v-123', quantity: 1 }]),
  recreateAndRun: recreateWithInput({ lines: [{ variantId: 'v-123', quantity: 1 }] }),
});

// Bail-on-stale: no recreateAndRun — throws CartRecoveryNotPossibleError instead.

Detection inspects err.userErrors[].code (CART_NOT_FOUND / ALREADY_COMPLETED) — locale-independent. The runner also creates the cart on first use (deduped across concurrent calls) and persists the revealed secret through the cookie store.

useCart(cartId) — server-driven cart

Sister of useCartManager bound to an explicit cartId prop — never touches the cookie, no auto-recovery. For SSR-rendered checkout, deep-link order recovery, and admin "view this cart" UIs:

'use client';
import { useCart } from '@doswiftly/storefront-sdk/react';

const {
  cart, isLoading, error, operation,
  refetch,
  addItems, updateItems, removeItems,
  updateBuyerIdentity, setShippingAddress,
  updateDiscountCodes, updateNote, updateAttributes,
} = useCart(cartId, {
  autoFetch: false,   // skip the mount fetch when the server already rendered the cart
  initialCart,        // SSR seed — combine with autoFetch: false
});

Pre-built React components

Headless, accessibility-aware, zero styling — pass className to integrate with your CSS approach. Available from @doswiftly/storefront-sdk/react:

| Component | Purpose | |-----------|---------| | <Money amount currency> | Locale-formatted price string from minor units | | <Image data sizes priority> | <img> with thumbhash blur placeholder + sane defaults | | <CartCount count label> | Aria-live cart item count | | <AddToCartButton variantId quantity> | Button wired to useCartManager().addItem (loading state + a11y error surfacing) | | <PriceDisplay price compareAtPrice currency> | Price + optional strikethrough sale price | | <CartTotals subtotal tax shipping discount total currency> | Cart financial breakdown <dl> | | <PaymentInstrumentTile instrument> | One selectable payment instrument (card brand, wallet, bank) | | <PaymentInstrumentSection method> | Instrument group for a payment method (renders tiles) |

import { Money, PriceDisplay, CartCount } from '@doswiftly/storefront-sdk/react';

<Money amount={9990} currency="PLN" />          {/* "99,90 zł" */}
<PriceDisplay price={7990} compareAtPrice={9990} currency="PLN" />
<CartCount count={3} label="items" />

Middleware pipeline

Default order (wired automatically by StorefrontProvider):

auth → cart-secret → currency → language → bot-protection → [custom] → retry → timeout → errors (ALWAYS LAST)

When session refresh is active, a reactive-401 middleware wraps the whole pipeline: a 401 on a read query triggers one deduped refreshSession() and a replay; a 401 on a mutation fires the session-expired signal instead.

import {
  authMiddleware,           // Authorization: Bearer <token> (lazy getter)
  cartSecretMiddleware,     // x-cart-secret header (lazy getter)
  currencyMiddleware,       // X-Preferred-Currency header
  languageMiddleware,       // X-Language header (skipped when null — intentional)
  botProtectionMiddleware,  // challenge token for protected mutations only
  retryMiddleware,          // exponential backoff + jitter — queries only, never mutations
  timeoutMiddleware,        // AbortController, edge-safe (default 5s)
  errorMiddleware,          // normalizes all errors → StorefrontError (ALWAYS LAST)
  sessionRetryMiddleware,   // reactive-401 refresh + replay (queries only)
} from '@doswiftly/storefront-sdk';

// Custom middleware
const logMiddleware: Middleware = async (request, next) => {
  console.log('Request:', request.operationName);
  const response = await next(request);
  return response;
};

Middleware that reads mutable state takes a lazy getter (authMiddleware(() => store.getState().accessToken)) so rotated values are picked up without rebuilding the client.

Core API

createStorefrontClient

const client = createStorefrontClient({
  apiUrl: string,
  shopSlug: string,
  middleware?: Middleware[],
  defaultHeaders?: Record<string, string>,
  fetch?: typeof globalThis.fetch,        // custom fetch (polyfill, test mocks)
  debug?: boolean | 'verbose' | DebugOptions,
});

client.query<T, V>(document, variables?, cache?): Promise<T>
client.mutate<T, V>(document, variables?): Promise<T>
client.use(middleware): void // imperative middleware add

Features: lazy pipeline compilation, same-tick request deduplication (queries only), TypedDocumentString support from graphql-codegen.

Debug logging

createStorefrontClient({ apiUrl, shopSlug, debug: 'verbose' });
  • debug: true — minimal: operation name + variables on request, status + userErrors on response.
  • debug: 'verbose' — everything: full query, variables, headers, response body, timing, userErrors.
  • debug: { request?, response?, headers?, timing?, userErrors?, log? } — granular per-dimension opt-in, plus a custom sink (log: (event: DebugEvent) => void) for routing into your logger.
  • Env fallback: DOSWIFTLY_SDK_DEBUG=verbose|true|minimal when the option is omitted — disabled in NODE_ENV=production (PII safety).
  • Authorization: Bearer … and auth-cookie values are unconditionally redacted to ***<last4> whenever headers are logged.

createRemoteDebugTransport builds a shared remote sink — pass it as debug: { remote: transport } on the client and as cookieDebug on the providers so GraphQL operations and cookie writes land on one timeline with a single session id.

StorefrontError

import { StorefrontError, ErrorCodes } from '@doswiftly/storefront-sdk';

try {
  await client.query(ProductQuery, { handle: 'missing' });
} catch (err) {
  if (err instanceof StorefrontError) {
    err.code;           // 'GRAPHQL_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'USER_ERROR' | 'SESSION_EXPIRED' | ...
    err.status;         // HTTP status (0 for network errors)
    err.graphqlErrors;  // GraphQL-level errors
    err.userErrors;     // field-level validation errors (backend-translated messages)
    err.hasUserErrors;  // boolean
    err.isNetworkError; // boolean
    err.isTimeout;      // boolean
  }
}

assertNoUserErrors(payload) — the helper the clients use internally — is also exported for custom operations: it throws a StorefrontError carrying the first backend-translated userErrors[].message.

Schema enums — runtime constants

Every schema enum is exported as a runtime const + type alias pair, so both import type { DeliveryType } and Object.values(DeliveryType) work:

import { DeliveryType, PaymentMethodType, CountryCode } from '@doswiftly/storefront-sdk';

const schema = z.enum(Object.values(PaymentMethodType)); // runtime validation
type T = DeliveryType;                                   // 'HOME' | 'PICKUP_POINT' | 'LOCKER'

Available: DeliveryType, PaymentMethodType, PaymentInitiationFlow, CurrencyCode, CountryCode, LanguageCode, ProductTypeEnum, WeightUnit, CartWarningCode, AttributeType, AttributeFillingMode, AttributeBillingMode, AttributeOptionSurchargeType, StorefrontOrderStatus, OrderPaymentStatus, OrderFulfillmentStatus, DiscountErrorCode, DiscountApplicationType, PaymentProvider, PaymentInstrumentType, PaymentInstrumentDisplayHint, PaymentMethodUnavailableReason.

Format utilities

import {
  formatPrice, formatPriceRange, formatAmount,
  formatDate, formatDateTime, formatNumber, formatPercentage,
  getCurrencySymbol,
} from '@doswiftly/storefront-sdk';

formatPrice({ amount: '99.99', currencyCode: 'USD' }); // "$99.99"
formatAmount('115.20', 'EUR');                          // "115,20 €"
formatPercentage(0.15);                                 // "15%"

Locale-bound versions that follow the active storefront language are available as React hooks — see Format hooks.

HTML sanitizer + connection normalizer

import { sanitizeHtml, normalizeConnection } from '@doswiftly/storefront-sdk';

const safe = sanitizeHtml(userHtml); // strips <script>, event handlers, javascript: URLs

const { items, pageInfo, totalCount } = normalizeConnection(data.products); // Relay → flat array

Cookie contracts (platform constants)

All first-party cookie names/defaults the platform relies on are exported — never hardcode the strings:

| Constant | Cookie | Notes | |---|---|---| | AUTH_COOKIE_NAME / AUTH_COOKIE_DEFAULTS | customerAccessToken | httpOnly access token | | REFRESH_COOKIE_NAME / REFRESH_COOKIE_DEFAULTS | customerRefreshToken | httpOnly, path-scoped to the auth route | | SESSION_EXPIRY_COOKIE_NAME / SESSION_EXPIRY_COOKIE_DEFAULTS | session-expiry | readable expiry hint for the scheduler | | CART_COOKIE_NAME / CART_COOKIE_MAX_AGE | cart-id | composite "<cartId>.<secret>", 30 days | | CURRENCY_COOKIE_NAME / CURRENCY_COOKIE_MAX_AGE / CURRENCY_HEADER_NAME | preferred-currency | | | LANGUAGE_COOKIE_NAME / LANGUAGE_COOKIE_MAX_AGE / LANGUAGE_HEADER_NAME | preferred-language | |

import { parseCartCookieValue, formatCartCookieValue } from '@doswiftly/storefront-sdk';

parseCartCookieValue('abc.s3cret'); // { cartId: 'abc', cartSecret: 's3cret' }
parseCartCookieValue('abc');        // { cartId: 'abc', cartSecret: null } (legacy)
formatCartCookieValue({ cartId: 'abc', cartSecret: 's3cret' }); // 'abc.s3cret'

Route matching

import { matchesRoute } from '@doswiftly/storefront-sdk';

matchesRoute('/account/orders', ['/account']); // true (exact + prefix matching)

React adapter

<StorefrontProvider> props

| Prop | Type | Purpose | |---|---|---| | config | { apiUrl, shopSlug, … } | Client config (full StorefrontClientConfig, incl. debug) | | shopData | ShopConfig | currencyCode, supportedCurrencies, localeToCurrencyMap?, defaultLanguage?, supportedLanguages?, botProtection? | | middleware | Middleware[] | Extra middleware, inserted after the built-in header middleware | | initialIsAuthenticated | boolean | Server-side auth hint — no "Sign in" flash. Defaults to !!initialAccessToken | | initialAccessToken | string \| null | Server-side raw JWT seed (kept in memory, never persisted) | | initialExpiresAt | string \| null | Session expiry seed (ISO 8601) — arms the refresh scheduler on cold start | | initialLanguage | string | Server-side locale hint — no language flash | | autoRefresh | boolean | Proactive session refresh — default ON in the browser | | authBasePath | string | Where the BFF auth route is mounted (default /api/auth) | | cookieDebug | (event: DebugEvent) => void | Debug sink for currency/language cookie writes |

The provider creates all store instances per mount (no module-level singletons — safe under bundler module duplication), wires the default middleware pipeline, mounts the bot-protection widget when the shop has it configured, and runs the session-refresh scheduler.

StorefrontClientProvider, CurrencyProvider, LanguageProvider are exported separately for custom composition.

Stores (Context-based)

All store hooks require the StorefrontProvider wrapper and accept an optional selector:

import {
  useAuthStore, useAuthStoreApi, useAuthHydrated,
  useCurrencyStore, useCurrencyStoreApi,
  useLanguageStore, useLanguageStoreApi,
} from '@doswiftly/storefront-sdk/react';

// Auth
const { isAuthenticated, customer, accessToken, expiresAt, setAuth, clearAuth } = useAuthStore();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated); // with selector
const authHydrated = useAuthHydrated(); // true after localStorage rehydration

// Currency
const { currency, baseCurrency, supportedCurrencies, setCurrency, isLoaded } = useCurrencyStore();

// Language
const { language, setLanguage } = useLanguageStore();

// `*StoreApi` variants return the raw store for .getState() in callbacks
const token = useAuthStoreApi().getState().accessToken;

Pre-built selectors: selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded, selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded.

Security note: the access token is never persisted to localStorage/sessionStorage — it lives in memory (and in the httpOnly cookie). Persisted auth state covers only customer + isAuthenticated.

useCurrency() is a convenience aggregator over the currency store.

Format hooks

Locale-aware formatters bound to the active storefront language:

import {
  useFormatPrice, useFormatAmount, useFormatPriceRange,
  useFormatDate, useFormatDateTime, useFormatNumber, useGetCurrencySymbol,
} from '@doswiftly/storefront-sdk/react';

const formatPrice = useFormatPrice();
formatPrice({ amount: '99.99', currencyCode: 'PLN' }); // honors the active locale

Generic hooks

import { useHydrated, useDebouncedValue, useStorefrontClient } from '@doswiftly/storefront-sdk/react';

const isHydrated = useHydrated();        // false during SSR + first client render
const debounced = useDebouncedValue(query, 300);
const client = useStorefrontClient();    // the StorefrontClient from context

createStoreContext

Build your own Context-based Zustand stores (the same pattern the SDK uses — no module-level singletons):

import { createStoreContext } from '@doswiftly/storefront-sdk/react';

const { Provider: WishlistProvider, useStore: useWishlistStore, useApi: useWishlistStoreApi } =
  createStoreContext<WishlistState>('WishlistStore');

Bot protection

When the shop has bot protection configured (shopData.botProtection), the provider loads the challenge widget and botProtectionMiddleware attaches tokens to protected mutations automatically. useBotProtection() exposes execute() for manual token acquisition; createBotProtectionManager / FallbackBotProtectionManager are exported from core for custom wiring. Fail-open by default — a challenge outage never blocks checkout.

Server-side (/react/server)

import {
  getStorefrontClient,          // server client factory (10s timeout, retry, errors)
  createStorefrontAuthRoute,    // SDK-BFF auth route — see Authentication
  getInitialAuth,               // cold-start auth seed from first-party cookies (Next.js)
  readCartIdCookie,             // cart id (string | null)
  readCartCredentials,          // { cartId, cartSecret } | null — composite cookie
  readCurrencyCookie,           // preferred currency (string | null)
  serverCartSecretMiddleware,   // attach x-cart-secret on SSR/edge cart reads
  trustedForwardedHostValidator,
  originAllowlistValidator,
} from '@doswiftly/storefront-sdk/react/server';

SSR cart read with the capability secret:

// Server Component / Route Handler
const credentials = await readCartCredentials();

const client = getStorefrontClient({
  apiUrl: process.env.NEXT_PUBLIC_API_URL!,
  shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
  middleware: [serverCartSecretMiddleware(credentials)],
});

const cart = credentials ? await new CartClient(client).get(credentials.cartId) : null;

The server factory ships without auth/currency middleware (there are no stores on the server) — add request-scoped headers via custom middleware. For authenticated SSR reads, forward the access token from the httpOnly cookie:

middleware: [
  async (request, next) => {
    const token = (await cookies()).get(AUTH_COOKIE_NAME)?.value;
    if (token) request.headers['Authorization'] = `Bearer ${token}`;
    return next(request);
  },
],

Caching

import { cacheLong, cacheShort, cacheNone, cachePrivate, cacheCustom } from '@doswiftly/storefront-sdk/cache';

cacheLong()                              // 1h + 23h stale-while-revalidate
cacheLong({ tags: ['product', slug] })   // with Next.js revalidation tags
cacheShort()                             // 1s + 9s swr
cacheNone()                              // no-store
cachePrivate()                           // private, 1s + 9s swr
cacheCustom({ maxAge: 300, swr: 600 })   // 5min + 10min swr

const data = await client.query(ProductQuery, { handle }, cacheLong());

GraphQL schema for codegen

The GraphQL SDL ships in the linked @doswiftly/storefront-operations package (installed alongside the SDK) — point your codegen / IDE plugin at it directly. No live backend required:

// codegen.ts — uses the raw .graphql file from the operations package
const config = {
  schema: 'node_modules/@doswiftly/storefront-operations/schema.graphql',
  documents: ['./app/**/*.{ts,tsx,graphql}'],
  generates: { './generated/graphql.ts': { /* … */ } },
};
export default config;

For the VS Code GraphQL extension, point graphql-config at the same path. client.query/client.mutate accept the generated TypedDocumentString documents directly.

Deprecated

| Symbol | Replacement | |---|---| | createCartStore, CartProvider, useCartStore, useCartStoreApi | useCartManager / <CartManagerProvider> — the legacy store predates capability carts and does not carry the cart secret | | AuthClient.refreshToken(), useRefreshToken | Automatic refresh (autoRefresh) / AuthClient.refreshSession() — the GraphQL refresh requires a still-valid access token | | ShopCurrencyData | ShopConfig |

Deprecated symbols remain exported for backward compatibility and will be removed in a future major release.

License

MIT