Aller au contenu

@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ête Origin (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).

Fenêtre de terminal
npm install @komozu/node
import { 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' })
  1. 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 }). id est l’identifiant de l’utilisateur dans votre propre base de données. Le jeton n’expire pas : conservez-le avec l’utilisateur.
  2. Votre serveur écrit le jeton conservé dans la page : data-komozu-token="…" sur le script d’intégration, la prop token de KomozuChat (@komozu/react), ou memberToken dans @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'utilisateur
export 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 visiteur
createServer(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 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 lui

Affichez 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).

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, idempotent
await komozu.channels.removeMembers(team.slug, ['user_43']); // ses flux ouverts sont fermés par le serveur
const 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).

// 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 premier

Les chats ouverts appliquent tout de suite un changement de rôle. Épingler et désépingler sont idempotents.

// Supprimer un message : il disparaît en direct pour tout le monde, ses fichiers sont purgés
await komozu.messages.delete(messageId);
// Bannir un membre de tout le projet (idempotent) ; ses flux ouverts sont fermés
await 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 deleteMessages
await 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 premier
const reports = await komozu.reports.list(); // [{ message, channel, reportCount, reasons, firstReportedAt, lastReportedAt }]
await komozu.messages.delete(reports[0].message.id); // supprime le message et ses signalements
await komozu.reports.dismiss(messageId); // garde le message, classe ses signalements
komozu.members.upsert({ id, name, avatarUrl? }): Promise<{ member: MemberAdminDTO; token: string }>;
komozu.members.revokeTokens(id): Promise<string>; // le nouveau jeton
komozu.members.createToken(id, { expiresIn? }): string; // synchrone, local
komozu.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 + role
komozu.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és

Chaque 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éé
}
}
CodeStatutQuand
INVALID_SECRET_KEY401Clé secrète absente ou inconnue
SECRET_KEY_IN_BROWSER403La requête portait un en-tête Origin
RATE_LIMIT_EXCEEDED429Plus de 600 requêtes par minute pour le projet (Retry-After)
VALIDATION_ERROR400Entrée invalide (par exemple un avatar qui n’est pas une URL https://)
MEMBER_NOT_FOUND404Membre inconnu (addMembers liste tous les identifiants inconnus et ne modifie rien)
CHANNEL_NOT_FOUND404Salon inconnu dans ce projet
CHANNEL_EXTERNAL_ID_TAKEN409Un autre salon utilise déjà cet externalId
CHANNEL_LIMIT_EXCEEDED403Quota de salons du plan atteint
MESSAGE_NOT_FOUND404Message inconnu dans ce projet
PIN_LIMIT_EXCEEDED409Le salon a déjà 50 messages épinglés

Référence HTTP complète : API serveur.