HubMix

Documentation

JavaScript et TypeScript

Le SDK npm de Gaspard pour Node.js, Next.js, les navigateurs, Deno et Bun : sans dépendance, ESM et CommonJS.

SDK JavaScript / TypeScript officiel du hub HubMix (API publique /v1/public) : Gaspard, l'assistant IA, la recherche dans ses connaissances et, depuis la 1.1.0, le tuteur, la confiance (signaux d'arnaque des annonces), les embeddings et le vérificateur des webhooks de l'ancien portail développeurs (retiré le 28 octobre 2026). Aucune dépendance : il s'appuie sur fetch, ReadableStream, TextDecoder et AbortController, et fonctionne avec Node 18+, les navigateurs, Deno et Bun. Paquet ESM + CommonJS, types inclus.

Installation #

Terminal
npm install @valais-connect/gaspard
TypeScript
import { Gaspard, GaspardError } from '@valais-connect/gaspard'; // ESM / TypeScript
const { Gaspard } = require('@valais-connect/gaspard');         // CommonJS

Deux sortes de clés #

Clé Où Option En-tête envoyé
Secrète vck_live_… Serveur uniquement apiKey Authorization: Bearer …
Publiable vcp_live_… Page web, application publishableKey X-Gaspard-Key: … (+ jeton visiteur)

Il faut exactement l'une des deux. Une clé secrète utilisée dans un navigateur lève une TypeError : gardez-la sur votre serveur.

Option Défaut Rôle
baseUrl https://hubmix.dealmood-solutions.com Hôte du service (le SDK ajoute /v1/public)
origin — Clé publiable hors navigateur : envoyée comme Origin (ignorée dans un navigateur)
endUser — Clé secrète : identifiant de votre utilisateur (X-End-User, 1–128 caractères ASCII imprimables)
timeout 60000 Délai par requête, en millisecondes (flux : jusqu'au début de la réponse)
maxRetries 2 Nouvelles tentatives (voir Erreurs et nouvelles tentatives), 0 pour les désactiver
userAgent gaspard-js/1.1.0 User-Agent (jamais envoyé depuis un navigateur)
fetch globalThis.fetch Une autre implémentation de fetch (tests, environnements particuliers)

Côté serveur #

Express #

TypeScript
import express from 'express';
import { Gaspard, GaspardError } from '@valais-connect/gaspard';

const gaspard = new Gaspard({ apiKey: process.env.GASPARD_API_KEY! });
const app = express();
app.use(express.json());

app.post('/api/gaspard/conversations', async (req, res) => {
  const conversation = await gaspard.forEndUser(req.user.id).startConversation('fr', req.body.page);
  res.status(201).json(conversation);
});

app.post('/api/gaspard/conversations/:id/messages', async (req, res) => {
  try {
    const answer = await gaspard.forEndUser(req.user.id).ask(req.params.id, req.body.text);
    res.json(answer);
  } catch (error) {
    if (error instanceof GaspardError) {
      res.status(error.status || 502).json({ code: error.code, retryAfter: error.retryAfter });
    } else throw error;
  }
});

Next.js (route handler, réponse en flux) #

TypeScript
// app/api/gaspard/[id]/route.ts
import { Gaspard } from '@valais-connect/gaspard';

const gaspard = new Gaspard({ apiKey: process.env.GASPARD_API_KEY! });

export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const { text } = await request.json();
  const encoder = new TextEncoder();
  const body = new ReadableStream({
    async start(controller) {
      try {
        for await (const event of gaspard.askStream(id, text, undefined, { signal: request.signal })) {
          controller.enqueue(encoder.encode(JSON.stringify(event) + '\n'));
        }
        controller.close();
      } catch (error) {
        controller.error(error);
      }
    },
  });
  return new Response(body, { headers: { 'Content-Type': 'application/x-ndjson' } });
}

Vos utilisateurs (X-End-User) #

Avec une clé secrète, indiquez qui pose la question pour que chaque personne ne voie que ses conversations et puisse exercer ses droits (seul un hachage à clé de l'identifiant est conservé) :

TypeScript
const forLea = gaspard.forEndUser('user-42');   // client dérivé, même configuration
await forLea.giveConsent(notice.version);       // après avoir montré la notice
await forLea.exportData();                      // droit d'accès
await forLea.eraseData();                       // droit à l'effacement
await gaspard.consent({ endUser: 'user-42' });  // ou appel par appel

Dans le navigateur (clé publiable et consentement) #

La clé publiable ne fonctionne que depuis les origines déclarées pour votre client. Le visiteur doit d'abord accepter la notice de confidentialité ; openSession ouvre alors une session visiteur et garde son jeton (vst_…, valable 30 jours) pour tous les appels suivants.

TypeScript
import { Gaspard } from '@valais-connect/gaspard';

const gaspard = new Gaspard({ publishableKey: 'vcp_live_…' });

const saved = localStorage.getItem('gaspard-token');
if (saved) {
  gaspard.setVisitorToken(saved);
} else {
  const notice = await gaspard.privacyNotice('fr');
  if (!(await showNoticeAndAsk(notice.text))) throw new Error('Pas de consentement');
  const session = await gaspard.openSession({ consent: true, locale: 'fr', allowTraining: false });
  localStorage.setItem('gaspard-token', session.token);
}

const conversation = await gaspard.startConversation('fr', location.pathname.slice(0, 80));
const answer = await gaspard.ask(conversation.id, 'Que faire à Lyon ce week-end ?');

openSession({ consent: false }) est refusé localement (TypeError). Le visiteur peut retirer son consentement (withdrawConsent()), consulter ses données (exportData()) ou les effacer (eraseData()) ; setVisitorToken(null) oublie le jeton.

Hors navigateur (application de bureau, React Native…), passez aussi origin : il doit faire partie des origines déclarées.

Réponses en flux #

askStream renvoie un itérable asynchrone : des événements delta (morceaux de texte), puis une seule réponse answer complète. La requête part au début de l'itération ; sortir de la boucle (break) ferme le flux.

TypeScript
let text = '';
for await (const event of gaspard.askStream(conversation.id, 'Comment trouver un logement étudiant à Montréal ?')) {
  if (event.type === 'delta') {
    text += event.text;
    render(text);
  } else {
    showSources(event.answer.sources, event.answer.listings);
  }
}

Un flux qui se termine par une erreur lève une GaspardError (assistant_busy → 503, conversation_full → 409…) ; un flux coupé avant la réponse lève stream_incomplete. Un flux déjà commencé n'est jamais relancé automatiquement.

Méthodes #

Méthode Appel Renvoie
privacyNotice(locale = 'fr') GET /privacy-notice PrivacyNotice
openSession({ consent: true, locale?, allowTraining? }) POST /sessions Session
setVisitorToken(token | null), visitorToken — —
startConversation(locale = 'fr', page?) POST /gaspard/conversations Conversation
ask(conversationId, text, firstName?) POST /gaspard/conversations/{id}/messages Answer
askStream(conversationId, text, firstName?) idem, en flux AsyncIterable<StreamEvent>
conversation(conversationId) GET /gaspard/conversations/{id} ConversationThread
feedback(messageId, 'up' | 'down', comment?) POST /gaspard/messages/{id}/feedback Feedback
searchKnowledge(query, limit = 5, topics?) POST /knowledge/search { passages }
consent() / giveConsent(version, allowTraining = false) / withdrawConsent() /visitors/me/consent ConsentStatus
exportData() GET /visitors/me/data VisitorData
eraseData() DELETE /visitors/me Erasure
forEndUser(id) — client dérivé
products() GET /products Product[]
tutor.explain({ lesson, messages, mode?, learner?, locale? }) POST /tutor/explanations TutorReply
tutor.quiz({ lesson, count?, difficulty?, kinds?, learner?, locale? }) POST /tutor/quizzes Quiz
tutor.flashcards({ lesson, count?, learner?, locale? }) POST /tutor/flashcards Flashcards
tutor.grade({ lesson, question, answer, countryCode?, locale? }) POST /tutor/grades Grade
trust.assess(listings) POST /trust/assessments TrustResult
embeddings.create(texts) POST /embeddings Embeddings

Chaque méthode accepte en dernier argument { signal?, requestId?, endUser? } :

TypeScript
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await gaspard.ask(id, 'Bonjour', undefined, { signal: controller.signal, requestId: 'commande-1234' });

Les champs sont en camelCase (remainingMessages, sourceName…), les dates restent des chaînes ISO 8601, et les champs inconnus renvoyés par le serveur sont ignorés.

Produits du hub (clé serveur) #

Le tuteur, la confiance et les embeddings sont activés par application par l'équipe HubMix (sur demande) ; sans le produit, l'API répond 403 insufficient_scope. Ils demandent une clé serveur : avec une clé publiable, l'appel est refusé (TypeError) avant toute requête.

TypeScript
const gaspard = new Gaspard({ apiKey: process.env.VC_API_KEY! });

// Confiance : une annonce suspecte avant de la publier
const { assessments } = await gaspard.trust.assess([
  { kind: 'housing', title: 'Studio centre-ville', description: 'Paiement par Western Union avant visite',
    priceAmount: 250, currency: 'EUR', countryCode: 'FR', city: 'Lyon', housingType: 'studio' },
]);
if (assessments[0]!.withheld) {
  // à faire vérifier par un modérateur ; assessments[0].findings dit pourquoi
}

// Tuteur : des cartes de révision
const { cards } = await gaspard.tutor.flashcards({
  lesson: { title: 'La photosynthèse', content: texteDuCours },
  count: 8,
  learner: { level: 'bachelor', countryCode: 'SN' },
  locale: 'fr',
});

// Embeddings : un vecteur par texte, dans le même ordre
const { embeddings, dimensions } = await gaspard.embeddings.create(['chambre à louer', 'room for rent']);

Erreurs et nouvelles tentatives #

Toute erreur d'appel est une GaspardError :

Propriété Contenu
status Statut HTTP, 0 sans réponse
code error.code de l'API (not_found, rate_limited, token_quota_exceeded, assistant_busy…), sinon http_<statut>, network_error, timeout, invalid_response, stream_incomplete
message Message lisible
requestId Identifiant de la requête (à communiquer au support), ou null
retryAfter Secondes à attendre (Retry-After), ou null
TypeScript
try {
  await gaspard.ask(id, question);
} catch (error) {
  if (error instanceof GaspardError && error.code === 'token_quota_exceeded') {
    console.warn(`Quota atteint, réessayer dans ${error.retryAfter} s (requête ${error.requestId})`);
  } else throw error;
}

Les erreurs de configuration (clé manquante, clé secrète dans un navigateur, consent: false, requestId invalide…) sont des TypeError, levées avant toute requête. Une annulation par AbortSignal rejette avec la raison du signal (AbortError), pas une GaspardError.

Le SDK réessaie seul, jusqu'à maxRetries fois : 503 assistant_busy, 429 rate_limited et, pour GET et DELETE seulement, les erreurs réseau et les délais dépassés. Il attend Retry-After quand il est présent (au-delà de 30 s, il abandonne et lève l'erreur), sinon 0,5 s × 2^tentative. token_quota_exceeded et les autres erreurs 4xx ne sont jamais réessayées. Réessayer une question est sans risque : le serveur n'enregistre un échange qu'avec sa réponse.