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

@hylmo/sdk

v0.1.0

Published

SDK storefront Hylmo — client HTTP typé (catalogue, disponibilité, panier local, checkout) pour intégrer Hylmo depuis un site tiers.

Readme

@hylmo/sdk

SDK storefront Hylmo : un client HTTP typé, isomorphe (navigateur / Node ≥ 18) et sans dépendance runtime, pour intégrer la location Hylmo (catalogue, disponibilité, panier, checkout, paiement, espace client, devis) depuis n'importe quel site — sans passer par apps/web.

Licence propriétaire — l'usage du SDK est réservé aux clients Hylmo disposant d'un abonnement actif et d'une clé d'API valide (voir LICENSE). Le paquet est distribué publiquement pour faciliter l'intégration, mais reste inerte sans clé d'API délivrée par Hylmo.

Installation

npm install @hylmo/sdk
# ou
pnpm add @hylmo/sdk

Node ≥ 18 (fetch natif). Dans le navigateur, aucun polyfill requis.

Démarrage rapide

import { createHylmoClient, HylmoApiError } from '@hylmo/sdk';

const hylmo = createHylmoClient({
  apiKey: 'pk_live_…',      // clé publiable — résout le tenant côté serveur
  agencyCode: 'LYON',       // optionnel : injecté dans les lectures storefront
  // baseUrl: 'https://api.hylmo.com'  // par défaut
});

// 1. Résoudre l'agence (les actions attendent un UUID `agencyId`)
const agency = await hylmo.agencies.get('LYON');

// 2. Catalogue
const { data: items } = await hylmo.catalog.list({ search: 'enceinte', page: 1 });
const product = await hylmo.catalog.get(items[0].slug);

// 3. Panier local (pur — aucun réseau)
const cart = hylmo.cart.create({
  start: '2026-08-01',
  end: '2026-08-03',
  agencyId: agency.id,
});
cart.add({ catalogItemId: product.id, quantity: 2, label: product.name });

// 4. Disponibilité
const dispo = await hylmo.availability.check({
  catalogItemId: product.id,
  agencyId: agency.id,
  startDate: '2026-08-01',
  endDate: '2026-08-03',
  quantity: 2,
});

// 5. Tarification + totaux locaux
const pricing = await hylmo.pricing.calculate({
  items: cart.items.map((i) => ({
    catalogItemId: i.catalogItemId,
    catalogItemVariantId: i.catalogItemVariantId,
    quantity: i.quantity,
  })),
  agencyId: agency.id,
  startDate: '2026-08-01',
  endDate: '2026-08-03',
});
const totals = cart.computeTotals(pricing.items, { mode: 'ttc' });

// 6. Code promo (optionnel)
try {
  const promo = await hylmo.promoCodes.validate('ETE2026', { agencyId: agency.id });
  cart.setPromo(promo);
} catch (err) {
  if (err instanceof HylmoApiError) console.warn(err.message);
}

// 7. Checkout — le serveur RECALCULE tout ; le body vient du panier
const { orderNumber, orderId } = await hylmo.checkout.submit(
  cart.toCheckoutPayload(
    { name: 'Jean Dupont', email: '[email protected]', phone: '0600000000' },
    { isQuote: false },
  ),
);

Un parcours complet compilable est fourni dans examples/vanilla.ts.

Tableau des méthodes

Lectures storefront (/api/v1/storefront/*)

| Méthode | Description | |---|---| | catalog.list(params?) | Liste paginée (filtres catégorie/agence/recherche/tri) | | catalog.get(slug) | Fiche produit complète (variantes, packs, accessoires, similaires) | | categories.list() | Arborescence des catégories | | agencies.list() | Liste des agences | | agencies.get(code) | Profil public d'une agence (horaires, FAQ, note…) | | reviews.list(params?) | Avis publiés, paginés | | config.get(params?) | Réglages publics (mode commande/devis, paiement en ligne, devise…) |

Actions (/api/web/*)

| Méthode | Description | |---|---| | availability.check(params) | Stock disponible pour une période/quantité | | pricing.calculate(params) | Tarifie chaque item (ordre préservé) | | pricing.cartDiscount(params) | Remise multi-produits du panier | | promoCodes.validate(code, { agencyId }) | Valide un code promo | | delivery.listModes(params) | Modes de livraison disponibles | | delivery.simulate(params) | Prix simulé des modes de livraison | | checkout.submit(payload) | Checkout atomique (commande ou devis) | | contact.send(payload) | Formulaire de contact | | payment.createIntent(orderId) | Crée un PaymentIntent Stripe Connect pour une commande |

Espace client (/api/customers/auth/*)

| Méthode | Description | |---|---| | auth.requestMagicLink({ email, type? }) | Envoie un email de connexion (lien magique) | | auth.verifyMagicLink({ token }) | Échange le token contre une CustomerSession | | auth.requestOtp({ email }) | Envoie un code de connexion à 6 chiffres | | auth.verifyOtp({ email, code }) | Vérifie le code et retourne une CustomerSession |

Portail client (session requise — hylmo.withSession(session).portal)

| Méthode | Description | |---|---| | portal.getProfile() / portal.updateProfile(params) | Profil du client | | portal.orders.list(params?) / .get(id) | Commandes du client (paginé / détail) | | portal.orders.cancel(id, params?) | Annule une commande (si le statut le permet) | | portal.orders.acceptQuote(id) | Accepte un devis depuis le portail | | portal.quotes.list() | Devis du client (métadonnées, sans accessToken) | | portal.invoices.list() | Factures du client (métadonnées + URL PDF) | | portal.addresses.list() / .create() / .update(id, params) / .remove(id) | Carnet d'adresses |

Devis par token (/api/quotes/:accessToken* — public, sans clé requise)

| Méthode | Description | |---|---| | quotes.get(accessToken) | Détail du devis (lignes, validité, dispo temps réel) | | quotes.accept(accessToken, params?) | Accepte le devis (quote → confirmed) | | quotes.pdfUrl(accessToken) | Construit l'URL du PDF (aucun appel réseau) |

Panier local (pur — aucun réseau)

| Méthode | Description | |---|---| | hylmo.cart.create(options) / createCart(options) | Crée un panier local | | cart.add(item) | Ajoute une ligne (fusionne les lignes identiques) | | cart.remove(id, variantId?, selKey?) | Retire une ligne | | cart.updateQuantity(id, qty, variantId?, selKey?) | Change la quantité (0 = retrait) | | cart.setDates(start, end) | Change la période | | cart.setPromo(promo) / cart.clearPromo() | Applique/retire un code promo validé | | cart.items / cart.dates / cart.promoState / cart.agency | État courant (copies) | | cart.computeTotals(pricingResults, options?) | Totaux (pur) : sous-total, remises, TVA blended | | cart.toCheckoutPayload(customer, options?) | Body EXACT de POST /api/web/checkout |

Gestion des erreurs

Toutes les méthodes réseau rejettent une erreur typée :

import { HylmoApiError, HylmoNetworkError } from '@hylmo/sdk';

try {
  await hylmo.checkout.submit(payload);
} catch (err) {
  if (err instanceof HylmoApiError) {
    // L'API a répondu ≥ 400 : err.status (HTTP) + err.code métier
    // (INSUFFICIENT_STOCK, INVALID_INPUT, NOT_FOUND, INVALID_API_KEY…)
    console.error(err.status, err.code, err.message, err.details);
  } else if (err instanceof HylmoNetworkError) {
    // La requête n'a jamais abouti (offline, DNS, TLS…)
  }
}

Les GET sont retentés une fois (backoff court) sur erreur réseau ou 5xx. Les POST/PATCH/DELETE ne sont jamais retentés (non idempotents).

Espace client (auth + portail)

Flux type : magic link ou OTP → CustomerSession (sessionToken opaque, signé par l'API — le SDK ne détient jamais SESSION_SECRET) → client portail lié via hylmo.withSession(session).

import { createHylmoClient } from '@hylmo/sdk';

const hylmo = createHylmoClient({ apiKey: 'pk_live_…' });

// 1a. Magic link (email avec lien de connexion)
await hylmo.auth.requestMagicLink({ email: '[email protected]' });
// … le client clique le lien, votre page `/auth/verify?token=…` appelle :
const session = await hylmo.auth.verifyMagicLink({ token: '<depuis l’URL>' });

// 1b. OU code à 6 chiffres (connexion inline, ex. sans quitter le checkout)
const { customerExists } = await hylmo.auth.requestOtp({ email: '[email protected]' });
if (customerExists) {
  // const session = await hylmo.auth.verifyOtp({ email: '[email protected]', code: '123456' });
}

// 2. `session` = { customerId, email, firstName, lastName, sessionToken }.
//    À persister côté intégrateur (cookie, storage…) puis à repasser tel quel
//    (ou juste `session.sessionToken`) à `withSession` :
const { portal } = hylmo.withSession(session);

// 3. Portail — la clé d'API part TOUJOURS en `Authorization: Bearer`, la
//    session TOUJOURS en `x-customer-session` (jamais l'inverse).
const profile = await portal.getProfile();
await portal.updateProfile({ phone: '0600000000' });

const { data: orders } = await portal.orders.list({ page: 1, status: 'confirmed' });
const order = await portal.orders.get(orders[0].id);
await portal.orders.cancel(order.id, { reason: 'Changement de programme' });

const { data: quotes } = await portal.quotes.list();
const { data: invoices } = await portal.invoices.list();

const address = await portal.addresses.create({
  type: 'delivery',
  line1: '12 rue de la Paix',
  postalCode: '75002',
  city: 'Paris',
  isDefault: true,
});
await portal.addresses.update(address.id, { label: 'Domicile' });
await portal.addresses.remove(address.id);

hylmo.withSession(session) accepte le sessionToken brut (string) ou l'objet CustomerSession complet — les deux sont équivalents.

Paiement (Stripe)

payment.createIntent(orderId) crée un PaymentIntent Stripe Connect pour une commande website en attente de paiement (new/confirmed, non payée). Le checkout (ou le devis accepté) renvoie déjà orderId pour enchaîner. Montage de Stripe Elements avec le compte connecté du loueur :

import { loadStripe } from '@stripe/stripe-js';
import { createHylmoClient } from '@hylmo/sdk';

const hylmo = createHylmoClient({ apiKey: 'pk_live_…' });

const { orderId } = await hylmo.checkout.submit(payload); // ou depuis un devis accepté
const { clientSecret, connectedAccountId } = await hylmo.payment.createIntent(orderId);

// `stripeAccount` = compte Connect du loueur — indispensable, sinon le
// paiement part sur le compte plateforme au lieu du compte du loueur.
const stripe = await loadStripe(STRIPE_PUBLISHABLE_KEY, { stripeAccount: connectedAccountId });
const elements = stripe.elements({ clientSecret });
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');

// Au submit du formulaire :
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: { return_url: 'https://votre-site.example/merci' },
});

Devis

Une commande créée en mode devis (checkout.submit({ ..., isQuote: true })) renvoie quoteAccessToken : un token opaque, secret, qui donne accès au devis sans authentification (ni clé d'API, ni session client) — à transmettre au client par email/lien, jamais à afficher publiquement.

const quote = await hylmo.quotes.get(accessToken);
console.log(quote.canAccept, quote.total, quote.lines.length);

if (quote.canAccept) {
  await hylmo.quotes.accept(accessToken);
}

// URL du PDF — à ouvrir dans un nouvel onglet ou définir en `href`, aucun
// appel réseau n'est fait par le SDK.
const pdfUrl = hylmo.quotes.pdfUrl(accessToken);

Le portail authentifié (portal.orders.acceptQuote(id)) applique les mêmes garde-fous (validité, expiration, disponibilité temps réel) que ce lien public.

Invariants

  • Prix en centimes (entiers) partout — jamais de float.
  • Percent en basis points : 1000 = 10 %.
  • Remise en HT (discountAmount) : elle réduit la base de TVA (conforme FR).
  • Les totaux calculés localement par cart.computeTotals sont une estimation d'affichage : le serveur recalcule tout au checkout et ne fait jamais confiance au body envoyé.