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

@tesseraloyalty/sdk

v0.3.0

Published

Official TypeScript SDK for the Tessera loyalty platform operations API (/v1).

Readme

@tesseraloyalty/sdk

Official TypeScript / JavaScript SDK for the Tessera loyalty platform. It wraps the public operations API (/v1) and owns the error-prone plumbing — bearer auth, HMAC request signing, idempotency keys, cursor pagination, retries with backoff, and a typed error taxonomy — so your backend can drive the whole member journey (browse → identify → earn → read → redeem) in a handful of lines.

Runs anywhere the Web fetch standard exists: Node 20+, edge / Cloudflare Workers, Deno, and Bun. Ships ESM + CommonJS with bundled types, and has zero runtime dependencies.

Status — publishing pending (TER-95). @tesseraloyalty/sdk is 0.x; the release pipeline is prepared but the package is not on npm yet, so the install line below is forthcoming. SDK v0.x targets Tessera operations API v1 (exported as API_VERSION).

Install

npm i @tesseraloyalty/sdk
# or: pnpm add @tesseraloyalty/sdk  /  yarn add @tesseraloyalty/sdk  /  bun add @tesseraloyalty/sdk
import { TesseraClient } from '@tesseraloyalty/sdk';

Constructing the client

TesseraClient takes exactly one API key plus optional transport and channel settings. Secret-key operations must run server-side (backend / BFF / edge function) — never ship a secret key to a browser.

const tessera = new TesseraClient({
  secretKey: process.env.TESSERA_SECRET_KEY,      // unlocks every operation (server-side only)
  signingSecret: process.env.TESSERA_SIGNING_SECRET, // SEPARATE connector HMAC secret, for events.ingest
  channel: 'shopify',                             // the channel bound to every member operation
});

| Option | Type | Default | Purpose | | --------------- | -------------------------- | ------------------------------ | ------- | | secretKey | string | — | The bearer key that unlocks every operation. Provide exactly one of secretKey / publishableKey. | | publishableKey| string | — | A catalogue-only bearer key — the only call it may make is rewards.list(). Any secret-scope call fails fast, pre-network, with PublishableKeyForbiddenError. | | signingSecret | string | — | The per-tenant connector HMAC secret (whsec_…) used to sign events.ingest. It is a separate credential from the API key and is only needed to ingest events. | | channel | Ch | 'api' | The channel bound to every customer-scoped operation (see Channel binding). Must be a member of channels when that allowlist is set. | | channels | readonly Ch[] | — | An optional channel allowlist. Enforced at runtime (out-of-list → pre-network throw) and, via as const, at compile time (typos fail to compile). | | environment | 'production' \| 'sandbox' | 'production' | The hosted environment. Picks the base URL from a preset (see Environments). Ignored when baseUrl is set. | | baseUrl | string | environment preset | The /v1 base URL. Overrides environment — use for local docker / CI / self-hosted. | | timeoutMs | number | 30000 | Per-request timeout. A timeout is terminal (not retried). | | maxRetries | number | 2 | Retries on retriable failures (network / 5xx / 429). | | fetch | FetchLike | globalThis.fetch | A custom fetch implementation (tests, proxies, older runtimes). |

The keys map to the two resource scopes: a publishableKey client may only call rewards.list(); everything else (customers, events, member(...) reads/redeem, redemptions) requires a secretKey.

Environments

The SDK ships base-URL presets for the two hosted stages. Pick one with environment (default production); an explicit baseUrl overrides it for local / CI / self-hosted:

| environment | Base URL | | ------------- | --------------------------------- | | production | https://api.usetessera.io/v1 | | sandbox | https://sandbox.usetessera.io/v1 |

const tessera = new TesseraClient({ secretKey: process.env.TESSERA_SECRET_KEY, environment: 'sandbox' });
// local dev / CI — baseUrl wins, environment is ignored:
const local = new TesseraClient({ secretKey: 'sk_test_local', baseUrl: 'http://localhost:3100/v1' });

Key/host guardrail. When you use a preset environment (i.e. you did not pass baseUrl), the SDK checks your key's prefix against the target: sk_test_/pk_test_ keys belong to sandbox, sk_live_/pk_live_ keys to production. A mismatch — the classic "test key against the prod host" slip — throws a clear config_error before any network call. Keys without a recognized prefix are not checked, and passing an explicit baseUrl skips the check entirely (local keys need not follow the convention).

Quickstart — the full journey (server-side)

A complete earn → read → redeem flow with a secret key. rewards.list()customers.upsert()events.ingest() (auto-signed) → member.balance() / .status() / .history()member.redeem()finalize().

import { TesseraClient } from '@tesseraloyalty/sdk';

const tessera = new TesseraClient({
  secretKey: process.env.TESSERA_SECRET_KEY,
  signingSecret: process.env.TESSERA_SIGNING_SECRET, // required for events.ingest
  channel: 'shopify',
});

// 1. Browse the public reward catalogue (allowed with a publishable OR a secret key).
const { pointsLabel, data: rewards } = await tessera.rewards.list();

// 2. Identify / enroll the member — create-or-update, keyed on (channel, externalId).
const customer = await tessera.customers.upsert({
  externalId: 'cust_5512',
  email: '[email protected]',
  name: 'Ada Lovelace',
});
console.log(customer.created ? 'enrolled' : 'updated', customer.customerUuid);

// 3. Ingest a purchase. The SDK HMAC-signs the EXACT bytes it sends (needs signingSecret);
//    earn is processed asynchronously server-side, so `enqueued` means the earn job was queued.
const event = await tessera.events.ingest({
  type: 'purchase',
  channel: 'shopify',
  externalId: 'cust_5512',
  sourceRef: 'ord_9001',
  idempotencyKey: 'shopify:ord_9001-purchase', // namespace the channel into the key
  occurredAt: new Date(),
  trigger: 'orders/paid',
  amount: { netOfTaxShipping: 4200, currency: 'USD' }, // minor units
});
console.log(event.status); // 'accepted' (first sighting) | 'duplicate' (deduped redelivery)

// 4. Open a member handle on the bound channel and read their standing.
const member = tessera.member('cust_5512');

const balance = await member.balance();
console.log(`${balance.available} ${balance.pointsLabel} available`);

const status = await member.status();
console.log('tier:', status.currentTier?.name ?? 'none');

// 5. Redeem: place a hold (auto Idempotency-Key), then finalize it against the order that
//    consumed it — or release it if the cart is abandoned.
const redemption = await member.redeem({
  optionId: rewards[0].id,
  cartSubtotalCents: 4200,
});
// …order is placed…
await redemption.finalize({ sourceChannel: 'shopify', sourceRef: 'ord_9001' });
// …or, on an abandoned cart:  await redemption.release();

Reading history — the async iterator

member.history() returns a HistoryPager that is both awaitable (resolves the first HistoryPage.data + .page) and async-iterable (auto-follows the cursor across every page):

// Iterate every entry across all pages — the pager follows the cursor for you.
for await (const entry of member.history()) {
  console.log(entry.type, entry.currency, entry.amount, entry.sourceRef);
}

// …or just the first page, with filters:
const page = await member.history({ limit: 50, types: ['earn', 'burn'], since: '2026-01-01' });
console.log(page.data.length, 'entries; more?', page.page.hasMore);

The other member reads — member.get() (pseudonymous profile) and member.balance() / member.status() — resolve to a single object.

Channel binding

A member operation never takes a per-call channel — it is bound once on the client, so a 'shopify'-here / 'shoify'-there typo can't silently split a customer's identity and ledger. Three layers enforce this:

  • Bound channel — set once via channel (default 'api'); withChannel(x) derives a channel-scoped clone that shares the same transport, auth, and signing secret for the rare legitimate multi-channel case.
  • Runtime allowlist — when channels is provided, the bound channel, every withChannel target, and every resolved channel must be a member, or the SDK throws pre-network naming the known channels.
  • Compile-time union — pass channels: [...] as const and the client is generic over that literal union, so a typo is a type error, not a runtime surprise.
const tessera = new TesseraClient({
  secretKey: process.env.TESSERA_SECRET_KEY,
  channels: ['shopify', 'pos', 'api'] as const, // Ch = 'shopify' | 'pos' | 'api'
  channel: 'shopify',
});

const pos = tessera.withChannel('pos');   // ok — clone bound to 'pos'
tessera.withChannel('shoify');            // ✗ compile error AND a pre-network throw

// Per-handle override, still validated against the allowlist:
const m = tessera.member('cust_5512', { channel: 'pos' });

Errors

Every failure is a TesseraError (or a subclass) carrying the stable, snake_case code from the API envelope plus status, requestId (the X-Request-Id, for support), and structured details. Catch by type — never string-match code:

import {
  InsufficientBalanceError,
  TierGatedError,
  HoldAlreadyReleasedError,
  PublishableKeyForbiddenError,
  TesseraError,
} from '@tesseraloyalty/sdk';

try {
  const r = await member.redeem({ optionId, cartSubtotalCents: 4200 });
  await r.finalize({ sourceChannel: 'shopify', sourceRef: 'ord_9001' });
} catch (err) {
  if (err instanceof InsufficientBalanceError) {
    console.log('short by', err.shortfall); // number | undefined, from details.shortfall
  } else if (err instanceof TierGatedError) {
    // option requires a higher tier than the member holds
  } else if (err instanceof HoldAlreadyReleasedError) {
    // finalizing a hold that was already released (terminal)
  } else if (err instanceof TesseraError) {
    // any other code — including one this SDK version doesn't yet know (see below)
    console.log(err.code, err.status, err.requestId);
  }
}

A new server code never crashes an old SDK: an unrecognized code resolves to the base TesseraError (with its code preserved), never a throw. The full code → subclass map is exported as CODE_TO_ERROR, and errorFromEnvelope is the factory that builds them.

| Error class | Wire code | Extra field | | ------------------------------ | -------------------------------------------------- | ----------- | | ValidationError | invalid_request | | | UnauthorizedError | unauthorized | | | SignatureInvalidError | signature_invalid | | | ReplayDetectedError | replay_detected | | | ForbiddenError | forbidden | | | TierGatedError | tier_gated | | | PublishableKeyForbiddenError | publishable_key_forbidden | | | NotFoundError | not_found | | | CustomerNotFoundError | customer_not_found | | | RedemptionNotFoundError | redemption_not_found | | | ConflictError | conflict | | | IdempotencyConflictError | idempotency_key_conflict / idempotency_key_reuse | | | VersionConflictError | version_conflict | | | CapReachedError | cap_reached | | | NotStackableError | not_stackable | | | HoldAlreadyReleasedError | hold_already_released | | | CustomerErasedError | customer_erased | | | PayloadTooLargeError | payload_too_large | | | InsufficientBalanceError | insufficient_balance | .shortfall: number \| undefined | | OptionNotEligibleError | option_not_eligible | | | InvalidPayloadError | invalid_payload | | | UnsupportedEventTypeError | unsupported_event_type | | | ProfileValidationError | profile_validation_error | .field: string \| undefined | | RateLimitedError | rate_limited | | | InternalError | internal_error | | | NotImplementedError | not_implemented | |

Pre-network failures (bad config, an out-of-allowlist channel, a missing signing secret) throw a base TesseraError with an SDK-local code (config_error) and no status.

Idempotency & retries

  • Redemptions auto-key. POST /v1/redemptions requires an Idempotency-Key; when you don't pass one, member.redeem(...) / redemptions.place(...) auto-generate a UUID and hold it constant across that call's own retries, so a retried place never double-charges the hold. Pass { idempotencyKey } to make a place stable across your retries too.
  • customers.upsert accepts an optional idempotencyKey for a safe create-or-update retry.
  • events.ingest dedups on the idempotencyKey inside the signed body (namespace the channel in, e.g. shopify:ord_9001-purchase). The Idempotency-Key header is optional and, if sent, must echo that body value.
  • Retries. Network errors, 5xx, and 429 are retried up to maxRetries (default 2) with exponential backoff (200 ms, doubling, capped at 20 s), honoring a Retry-After header when present. A per-request timeout (timeoutMs, default 30 s) is terminal — it means the call blew its budget and is not retried.

Event signing (advanced)

events.ingest signs for you. If you need to sign a request body yourself (e.g. a custom transport), the low-level signer and its header constants are exported:

import { signEventBody, SIGNATURE_HEADER, TIMESTAMP_HEADER, NONCE_HEADER } from '@tesseraloyalty/sdk';

const rawBody = JSON.stringify(payload);            // sign and send the SAME bytes
const signed = await signEventBody(rawBody, signingSecret); // HMAC-SHA256, `v1=<hex>`
// signed.headers → { 'X-Loyalty-Signature', 'X-Loyalty-Timestamp', 'X-Loyalty-Nonce' }

Re-serializing the body after signing invalidates the signature — sign and transmit the exact same string.

What this is not

@tesseraloyalty/sdk is a loyalty-operations client only. It serves customers, events, balances/reads, and redemptions for an already-provisioned tenant under a per-tenant API key. It deliberately does not cover:

  • merchant signup or tenant provisioning;
  • program configuration — earn rules, tiers, reward options, points label, branding. Those are managed in the merchant console, not the SDK.

If you're looking for a method to change how points are earned or what rewards exist, it isn't here by design — configure the program in the console; use this SDK to run it.

License

MIT — see LICENSE.