@a5it/storefront-sdk
v0.1.4
Published
Type-safe storefront authentication SDK with core, React, and Next.js clients
Maintainers
Readme
@a5it/storefront-sdk
Type-safe client for the A5IT storefront API. The auth, products, categories,
brands, orders, and addresses modules work through framework-independent
promises, React Query hooks, and a Next.js server client. useCart is a separate,
localStorage-backed cart hook exported alongside them. Payment provider
integrations (card tokenization, ACH/Plaid, 3-D Secure) are intentionally not part
of the SDK; applications call those directly.
Install
pnpm add @a5it/storefront-sdk react @tanstack/react-queryReact 18/19 and React Query 5 are peer dependencies. Next.js 14/15 is an optional
peer dependency, required when using the /server entry.
Core client
Use the framework-independent entry in scripts, tests, or custom integrations:
import { createStorefrontClient, memoryTokenStore } from "@a5it/storefront-sdk/core";
const tokens = memoryTokenStore();
const client = createStorefrontClient({
baseUrl: "https://api.example.com/api/v2",
cookieName: "storefront_session",
tenantDomain: "store.example.com",
advanced: { tokenStore: tokens },
});
const challenge = await client.auth.sendCode(
{ email: "[email protected]" },
{ captchaToken: "captcha-response" },
);
await client.auth.confirmEmail(
{ code: "123456", token: challenge.token },
{ captchaToken: "new-captcha-response" },
);
const session = await client.auth.getSession();
await client.auth.logout();Successful confirmation and registration store the returned session token.
Successful logout clears it. Every core endpoint returns a promise. Request
options accept captchaToken and an AbortSignal as signal.
React integration
Mount a provider inside your application's client boundary:
"use client";
import { StorefrontProvider, storefront } from "@a5it/storefront-sdk";
function Account() {
const { data, isLoading, error } = storefront.auth.getSession();
const { mutate: logout, isPending } = storefront.auth.logout();
if (isLoading) return <p>Loading account…</p>;
if (error) return <p>{error.message}</p>;
return (
<div>
<p>{data?.user.email}</p>
<button disabled={isPending} onClick={() => logout(undefined)}>Sign out</button>
</div>
);
}
export function AccountApp() {
return (
<StorefrontProvider config={{ baseUrl: "/api/v2", cookieName: "storefront_session" }}>
<Account />
</StorefrontProvider>
);
}Call storefront.auth.* hooks at the top level of client components or custom
hooks. Pass query options after the input; for example,
storefront.auth.getSession(undefined, { enabled: signedIn }). Caller options
such as staleTime override endpoint defaults. The provider creates its runtime
once per mount; remount it to change configuration.
Mutation callbacks receive the original input. Captcha tokens are supplied per mutation call:
const { mutate: confirmEmail } = storefront.auth.confirmEmail();
confirmEmail({ code, token }, { captchaToken, onSuccess: () => router.refresh() });Cache invalidation
Mutations declare which queries they make stale on the endpoint definition itself,
via invalidates, so callers never have to remember to do it. addresses.add,
for example:
add: mutation<IAddressWriteInput, IAddress>({
method: "POST",
path: ADDRESSES,
auth: "required",
invalidates: [queryKey("addresses", "list")],
}),Every successful call to storefront.addresses.add() (or .update() /
.remove()) invalidates addresses.list automatically, refetching it wherever
it's mounted — no manual queryClient.invalidateQueries at the call site. Add
more keys to invalidates if a mutation should also bust another module's cache
(e.g. an order mutation that also changes the customer's default address), or use
ALL_QUERIES (as auth.confirmEmail/register/logout do) to clear everything
on login/logout: the whole cache is reset, so results fetched under the previous
session are dropped and mounted queries refetch.
You are not limited to what a definition declares. Cache helpers use the mounted provider and can run in event handlers or anywhere else in the app:
storefront.auth.getSession.key();
storefront.auth.getSession.getData(undefined);
await storefront.auth.getSession.fetch(undefined);
await storefront.auth.getSession.prefetch(undefined);
await storefront.auth.getSession.cancel();
await storefront.auth.getSession.invalidate(); // one endpoint
await storefront.auth.invalidate(); // every endpoint in a module
await storefront.invalidateAll(); // everythingQuery helpers also expose setData(input, valueOrUpdater). Omitting the input to
key, invalidate, or cancel targets every cached input for that endpoint.
Imperative helpers use a single active provider; use one provider per application.
For a one-off case a declared invalidates doesn't cover, useStorefrontContext()
returns the real QueryClient, so queryClient.invalidateQueries(...) still works
exactly as it would outside the SDK.
Next.js server integration
import { configureStorefront, storefront } from "@a5it/storefront-sdk/server";
configureStorefront({
baseUrl: process.env.STOREFRONT_API_URL!,
cookieName: "storefront_session",
});
// Inside a server component, route handler, or server action:
const session = await storefront.auth.getSession();The server entry uses Next.js's native fetch transport. Every endpoint is a
plain promise, and public GET queries accept Next's Data Cache options after the
input:
import { cache } from "react";
import { storefront } from "@a5it/storefront-sdk/server";
const getCategory = cache((slug: string) =>
storefront.categories.getBySlug(
{ slug },
{
next: {
revalidate: 60,
tags: [`category:${slug}`],
},
},
),
);next.revalidate and next.tags persist public responses in Next's Data Cache
across requests. cache: "force-cache" is also supported when time-based
revalidation is not needed. React's cache() serves a different purpose: it
deduplicates calls within one server render, such as a request shared by
generateMetadata and the page. Pass server data to a client component as a prop
or as initialData on the matching hook.
Server caching is intentionally limited to endpoint definitions with
kind: "query", method: "GET", and auth: "none". Authenticated and
optionally authenticated queries, along with every mutation, are always sent
with cache: "no-store"; attempting to enable caching for one of them throws.
This prevents user-specific responses from entering a shared cache.
Configuration is read lazily. Each call reads the current request's host,
protocol, and authentication cookie from next/headers, then creates a fresh
transport. This makes routes using the server singleton dynamically rendered,
even when a public endpoint's response uses the Data Cache. Configure shared
deployment settings, not per-user tokens. Server token stores are read-only:
login/logout calls do not write response cookies; handle cookie changes in your
route handler or server action.
Without explicit configuration, the server reads STOREFRONT_API_URL or
NEXT_PUBLIC_BASE_API_ENDPOINT, plus NEXT_PUBLIC_COOKIE_NAME. Relative API
paths use NEXT_PUBLIC_BACKEND_URL as the origin, falling back to
http://localhost:8000. Outside a Next.js request scope, calls have no request
cookie or tenant headers; use the core client for scripts needing explicit tokens.
Authentication endpoints
| Method | Input | Result |
| --- | --- | --- |
| auth.sendCode | ISendCode | ISendCodeResult |
| auth.confirmEmail | IConfirmEmail | IAuthResult |
| auth.register | IRegisterUser | IAuthResult |
| auth.checkEmailExistence | ICheckEmail | IEmailExistence |
| auth.getSession | none | ISession |
| auth.logout | none | undefined |
| auth.makeOwner | IMakeOwner | undefined |
getSession and makeOwner require a token and reject with NO_TOKEN before
sending a request when none is available. Session models preserve the backend's
nested customer, organization, permissions, and benefit fields. These are part
of the authentication response contract, not separate API modules.
Product endpoints
| Method | Input | Result |
| --- | --- | --- |
| products.search | IProductSearch | IProductSearchResult |
| products.filter | IProductFilter | IProductFilterResult |
| products.conditionFacets | IConditionFacetFilter | IConditionFacet[] |
| products.related | IRelatedProducts | IRelatedProduct[] |
| products.getBySlug | IProductSlug | IProductDetails \| null |
| products.getBasicBySlug | IProductSlug | IProductBasic \| null |
| products.conditionSiblings | IProductSlug | IProductConditionSibling[] |
| pricing.priceDetails | IProductIds | IProductPrice[] |
| pricing.livePrices | IProductIds | IProductPrice[] |
| pricing.validateCartPrices (mutation) | IProductIds | IProductPrice[] |
| products.liveStock (mutation, captcha) | IProductId | IProductLiveStock |
Listing endpoints (search, filter, related) return products without prices;
call priceDetails with the listed ids and merge. priceDetails is cached for an
hour to match the backend; livePrices bypasses that cache (and is never cached by the hook) and
validateCartPrices always returns fresh prices. getBySlug returns the raw
catalog record with its brand and category, never a price; getBasicBySlug
is the small first-render subset. Both resolve to null and conditionSiblings
to [] on 404. These three catalog queries are public, tenant-scoped GETs and
may use the server entry's Next.js cache options. Pricing queries remain
optionally authenticated because their results can vary by customer organization
and cannot use the shared server cache. liveStock is a mutation because each
call needs a fresh captcha token.
Category and brand endpoints
| Method | Input | Result |
| --- | --- | --- |
| categories.list | ICategoryFilter | ICategoryListResult |
| categories.minimal | none | ICategoryMenuItem[] |
| categories.getBySlug | ICategorySlug | ICategoryDetails \| null |
| categories.hierarchy | ICategorySlug | ICategoryHierarchy \| null |
| brands.list | IBrandFilter | IBrandListResult |
categories.list returns top-level categories with nested children; minimal
returns the featured tree used for navigation. Unknown slugs resolve to null.
brands.list paginates only when both page and limit are sent, otherwise it
returns every brand.
Order and address endpoints
| Method | Input | Result |
| --- | --- | --- |
| orders.place (mutation, captcha) | IPlaceOrderInput | IPlaceOrderResult |
| orders.placeGuest (mutation, captcha) | IPlaceGuestOrderInput | IPlaceOrderResult |
| orders.calculateCharges (mutation) | ICalculateChargesInput | IOrderCharges |
| orders.calculateGuestCharges (mutation) | ICalculateGuestChargesInput | IOrderCharges |
| orders.applyCoupon (mutation, guest-friendly) | IApplyCouponInput | ICoupon |
| addresses.list | none | IAddress[] |
| addresses.add (mutation) | IAddressWriteInput | IAddress |
| addresses.update (mutation) | IUpdateAddressInput | undefined |
| addresses.remove (mutation) | IAddressId | undefined |
orders.place/placeGuest return requires3DS/id/teamId/appId instead of
order when Authorize.Net asks for a 3-D Secure challenge; run the challenge and
retry with threeDsSessionId set. orders.applyCoupon requires reseller/tenant
resolution but not a customer token, so guest checkouts can call it too.
addresses.add takes IAddressWriteInput, while .update takes a partial write
input — the backend's create/update schemas are strict and reject id/name, so
those don't appear on write inputs the way they do on IAddress/IAddressInput
(the broader shape used for an already-known or checkout-selected address).
.update responds with an empty envelope, not the updated address — re-read
addresses.list for fresh data. addresses.add,
.update, and .remove all invalidate addresses.list (see
Cache invalidation).
These modules cover order placement, charge calculation, coupons, and the address
book only — card/ACH tokenization, payment credentials, and provider-specific
flows (Authorize.Net, Finix, Plaid) stay in application code.
Configuration and errors
baseUrl is required. Optional configuration includes cookieName, timeoutMs
(default: 100,000), and advanced: { tokenStore, headers, onUnauthorized,
queryClient }. Core clients also accept tenantDomain and getToken.
Token precedence is getToken, then advanced.tokenStore, then browser cookies
(or an in-memory store outside the browser). Set cookieName to the backend's
customer cookie name. If omitted, the SDK reads NEXT_PUBLIC_COOKIE_NAME and
otherwise warns before using a5sync_auth_token.
Errors are normalized to StorefrontError with status, code, message, and
messages. Use isStorefrontError to identify them. Only HTTP 401 responses
with SESSION_SUPERSEDED, TOKEN_INVALID, ACCOUNT_BLOCKED, or
POLICY_REQUIRES_2FA clear the token and call advanced.onUnauthorized.
Other client errors do not end the session. React queries do not retry 4xx
responses. Applications control how errors are displayed.
Development
Storefront modules live in src/core/modules/, with each module defining its own
endpoints (auth.ts, products.ts, categories.ts, brands.ts, orders.ts,
addresses.ts). Register new modules in modules/index.ts and add their names to
MODULE_NAMES in modules/names.ts. The shared bindings expose these modules
through the core client, React hooks, and server client.
Use Node.js 22.15 or newer for the test runner's module mocking hooks.
pnpm install --frozen-lockfile
pnpm typecheck
pnpm testpnpm test builds the ESM/CommonJS bundles and declarations, then runs the
authentication, cache, server-context, and package-surface checks. The root entry
has a "use client" directive; /core is framework-independent and /server
uses the server-only boundary.
