@komozu/node (serveur)
SDK serveur de Komozu, le chat des utilisateurs connectés de votre site ou de votre application.
Utilisez-le uniquement depuis votre serveur pour :
- identifier vos utilisateurs connectés dans le chat (un jeton par membre, créé une fois et conservé) ;
- créer, modifier et supprimer des membres ;
- créer des salons pour vos groupes ou vos événements, et gérer les membres des salons privés ;
- modérer : supprimer et épingler des messages, bannir des membres, traiter les messages signalés depuis le chat.
Votre clé secrète (
kz_sec_…) ne va jamais dans un navigateur ni dans une application mobile. L’API refuse tout appel serveur qui porte un en-têteOrigin(403 SECRET_KEY_IN_BROWSER).
Node.js 20 ou plus récent, ESM, fetch natif, aucune dépendance JWT (les jetons sont signés avec node:crypto).
Installation
Section intitulée « Installation »npm install @komozu/nodeimport { Komozu, KomozuApiError } from '@komozu/node';
const komozu = new Komozu({ secretKey: process.env.KOMOZU_SECRET_KEY });// API auto-hébergée ou locale : new Komozu({ secretKey, apiUrl: 'http://localhost:4000' })Identifier vos utilisateurs
Section intitulée « Identifier vos utilisateurs »- Quand un utilisateur s’inscrit, votre serveur crée le membre et obtient son jeton :
const { member, token } = await komozu.members.upsert({ id, name, avatarUrl }).idest l’identifiant de l’utilisateur dans votre propre base de données. Le jeton n’expire pas : conservez-le avec l’utilisateur. - Votre serveur écrit le jeton conservé dans la page :
data-komozu-token="…"sur le script d’intégration, la proptokendeKomozuChat(@komozu/react), oumemberTokendans@komozu/core. Vide pour un visiteur non connecté : le chat l’invite à se connecter, sans afficher aucun message.
Un jeton est obligatoire pour lire ou écrire : il n’existe pas de mode anonyme ni « identifiant seul ». Un jeton valide pour un
membre qui n’a jamais été créé est refusé (401 MEMBER_NOT_FOUND). Le nom affiché et l’avatar viennent toujours
d’upsert / update, jamais du jeton.
import { createServer } from 'node:http';import { Komozu } from '@komozu/node';
const komozu = new Komozu({ secretKey: process.env.KOMOZU_SECRET_KEY });
// 1. Quand un utilisateur s'inscrit : créer le membre une fois, conserver le jeton avec l'utilisateurexport async function onSignUp(user) { const { token } = await komozu.members.upsert({ id: user.id, name: user.name, avatarUrl: user.avatarUrl ?? null }); await saveUser({ ...user, komozuToken: token });}
// Quand le nom ou l'avatar change (le jeton reste le même)export async function onProfileChange(user) { await komozu.members.update(user.id, { name: user.name, avatarUrl: user.avatarUrl ?? null });}
// 2. Sur chaque page : le jeton de l'utilisateur connecté, vide pour un visiteurcreateServer(async (req, res) => { const user = await getSignedInUser(req); // votre propre session res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.end(`<h1>Club news</h1><script src="https://embed.komozu.com/komozu-embed.js" data-komozu-integration="int_…" data-komozu-token="${user?.komozuToken ?? ''}" defer></script>`);}).listen(3000);Révoquer un jeton qui a fuité : const token = await komozu.members.revokeTokens(id) renvoie un nouveau jeton à
conserver. Tous les jetons émis auparavant sont refusés à partir de ce moment (401 TOKEN_REVOKED) et les chats
ouverts du membre sont déconnectés.
Signer un jeton localement (avancé, sans appel réseau) : komozu.members.createToken(id) signe le même type de
jeton avec votre clé secrète (HS256, sub et iat). Passez { expiresIn: seconds } pour ajouter une expiration. Le
membre doit déjà exister.
Un salon par événement
Section intitulée « Un salon par événement »Un salon est une discussion. Créez-les depuis votre serveur, avec votre propre identifiant comme externalId, pour ne
jamais avoir à conserver le slug généré :
const channel = await komozu.channels.create({ name: 'Tournament – Nov 2', externalId: `evt_${event.id}`, // facultatif, unique dans le projet, non modifiable attachmentsEnabled: true, // facultatif : images et vidéos (true par défaut)});channel.slug; // "tournament-nov-2" (généré à partir du nom, -2, -3… s'il est pris, jamais modifiable)
await komozu.channels.get({ externalId: `evt_${event.id}` });await komozu.channels.update({ externalId: `evt_${event.id}` }, { name: 'Tournament – Final' });await komozu.channels.delete({ externalId: `evt_${event.id}` }); // les messages, intégrations et fichiers sont supprimés avec luiAffichez ensuite le salon de chaque événement sur sa page avec @komozu/react, par son
externalId :
<KomozuChat clientId="kz_live_…" channel={{ externalId: 'evt_1102' }} />Sans React, créez une intégration par salon d’événement dans le tableau de bord (une intégration affiche toujours un seul salon).
Un ChannelRef est un slug, l’UUID d’un salon ou { externalId } (envoyé sous la forme ?by=externalId : un
identifiant externe n’est jamais deviné à partir d’un slug).
Salons de groupe privés
Section intitulée « Salons de groupe privés »const team = await komozu.channels.create({ name: 'U15 team', access: 'private' });
await komozu.channels.addMembers(team.slug, ['user_42', 'user_43']); // tout ou rien sur les identifiants inconnus, idempotentawait komozu.channels.removeMembers(team.slug, ['user_43']); // ses flux ouverts sont fermés par le serveurconst members = await komozu.channels.listMembers(team.slug); // chacun avec son rôle ('user' | 'admin')Niveaux d’accès : members (par défaut, tout membre du projet) ou private (uniquement les membres que vous
ajoutez).
Admins de salon et messages épinglés
Section intitulée « Admins de salon et messages épinglés »// Un membre devient admin d'un salon : dans le chat, il supprime, épingle et retire des membres d'un salon privé.// Sur un salon members, le rôle n'ajoute que des droits, jamais d'accès.await komozu.channels.setRole({ externalId: 'evt_1102' }, 'user_42', 'admin');await komozu.channels.setRole({ externalId: 'evt_1102' }, 'user_42', 'user');const members = await komozu.channels.listMembers('general'); // [{ externalId, displayName, …, role }]
// Messages épinglés : le dernier s'affiche en haut du chat, la liste s'ouvre depuis lui (50 par salon au maximum)await komozu.messages.pin(messageId);await komozu.messages.unpin(messageId);const pinned = await komozu.messages.listPinned({ externalId: 'evt_1102' }); // le dernier épinglé en premierLes chats ouverts appliquent tout de suite un changement de rôle. Épingler et désépingler sont idempotents.
Modération
Section intitulée « Modération »// Supprimer un message : il disparaît en direct pour tout le monde, ses fichiers sont purgésawait komozu.messages.delete(messageId);
// Bannir un membre de tout le projet (idempotent) ; ses flux ouverts sont fermésawait komozu.members.ban('user_42', { reason: 'Spam' });await komozu.members.unban('user_42');
// Supprimer un membre : ses messages sont conservés et affichés comme « Deleted member », ou supprimés avec deleteMessagesawait komozu.members.delete('user_42', { deleteMessages: true });
// Lire l'historique d'un salon (quel que soit son niveau d'accès)const { messages, nextCursor } = await komozu.messages.list({ externalId: 'evt_1102' }, { limit: 50 });
// Messages signalés depuis le chat (bouton Signaler), regroupés par message, le plus récemment signalé en premierconst reports = await komozu.reports.list(); // [{ message, channel, reportCount, reasons, firstReportedAt, lastReportedAt }]await komozu.messages.delete(reports[0].message.id); // supprime le message et ses signalementsawait komozu.reports.dismiss(messageId); // garde le message, classe ses signalementsRéférence
Section intitulée « Référence »komozu.members.upsert({ id, name, avatarUrl? }): Promise<{ member: MemberAdminDTO; token: string }>;komozu.members.revokeTokens(id): Promise<string>; // le nouveau jetonkomozu.members.createToken(id, { expiresIn? }): string; // synchrone, localkomozu.members.update(id, { name?, avatarUrl? }): Promise<MemberAdminDTO>;komozu.members.get(id): Promise<MemberAdminDTO>;komozu.members.list({ limit?, cursor?, search? }): Promise<{ members; nextCursor }>;komozu.members.delete(id, { deleteMessages? }): Promise<void>;komozu.members.ban(id, { reason? }): Promise<MemberBanDTO>;komozu.members.unban(id): Promise<void>;
komozu.channels.create({ name, access?, externalId?, attachmentsEnabled? }): Promise<ChannelDTO>;komozu.channels.get(ref): Promise<ChannelDTO>;komozu.channels.list(): Promise<ChannelDTO[]>;komozu.channels.update(ref, { name?, access?, attachmentsEnabled? }): Promise<ChannelDTO>;komozu.channels.delete(ref): Promise<void>;komozu.channels.addMembers(ref, memberIds): Promise<void>;komozu.channels.removeMembers(ref, memberIds): Promise<void>;komozu.channels.listMembers(ref): Promise<ChannelMemberDTO[]>; // MemberAdminDTO + rolekomozu.channels.setRole(ref, memberId, 'admin' | 'user'): Promise<ChannelMemberDTO>;
komozu.messages.list(ref, { limit?, cursor? }): Promise<{ messages; nextCursor }>;komozu.messages.delete(messageId): Promise<void>;komozu.messages.pin(messageId): Promise<MessageDTO>;komozu.messages.unpin(messageId): Promise<MessageDTO>;komozu.messages.listPinned(ref): Promise<MessageDTO[]>;
komozu.reports.list(): Promise<MessageReportDTO[]>;komozu.reports.dismiss(messageId): Promise<number>; // nombre de signalements classésChaque appel en échec lève une KomozuApiError avec un code stable lisible par une machine, le status HTTP et un
message en anglais :
try { await komozu.channels.create({ name: 'Finals', externalId: 'evt_9' });} catch (error) { if (error instanceof KomozuApiError && error.code === 'CHANNEL_EXTERNAL_ID_TAKEN') { // déjà créé }}| Code | Statut | Quand |
|---|---|---|
INVALID_SECRET_KEY | 401 | Clé secrète absente ou inconnue |
SECRET_KEY_IN_BROWSER | 403 | La requête portait un en-tête Origin |
RATE_LIMIT_EXCEEDED | 429 | Plus de 600 requêtes par minute pour le projet (Retry-After) |
VALIDATION_ERROR | 400 | Entrée invalide (par exemple un avatar qui n’est pas une URL https://) |
MEMBER_NOT_FOUND | 404 | Membre inconnu (addMembers liste tous les identifiants inconnus et ne modifie rien) |
CHANNEL_NOT_FOUND | 404 | Salon inconnu dans ce projet |
CHANNEL_EXTERNAL_ID_TAKEN | 409 | Un autre salon utilise déjà cet externalId |
CHANNEL_LIMIT_EXCEEDED | 403 | Quota de salons du plan atteint |
MESSAGE_NOT_FOUND | 404 | Message inconnu dans ce projet |
PIN_LIMIT_EXCEEDED | 409 | Le salon a déjà 50 messages épinglés |
Référence HTTP complète : API serveur.