TokenHandler
createTokenHandler est le coeur de la librairie. Là où beaucoup de bibliothèques exposent surtout des fonctions utilitaires à assembler, ici l'idée est inverse : on commence par créer un handler qui porte déjà la politique de gestion du token.
Ce handler regroupe au même endroit :
- la signature ;
- le chiffrement ;
- la durée de vie du token ;
- la gestions des claims (header/payload) ;
Autrement dit, le but est de construire un contrat clair que l'on peut réutiliser.
Création au démarrage
createTokenHandler est pensé pour être appelé au démarrage d'une l'application. La configuration est validée immédiatement, et une configuration invalide provoque un throw dès la création du handler.
Exemple simple
import { asyncPipe, D, DPE, E } from "@duplojs/utils";
import { Signer, createTokenHandler } from "@duplojs/json-web-token";
const tokenHandler = createTokenHandler({
maxAge: D.createTime(15, "minute"),
signer: Signer.createHS256({ secret: "my-secret" }),
issuer: "my-app",
audience: ["web"],
customPayloadShape: {
userId: DPE.string(),
},
});
const token = await tokenHandler.createOrThrow({
userId: "1",
});
// send to client ...
const result = await asyncPipe(
"receive-token",
tokenHandler.verify,
E.whenIsRight(
({ payload }) => {
const userId = payload.userId;
},
),
);Ce qui se passe ici
Au moment du createOrThrow, le handler ajoute lui-même les claims standards comme iat et exp, puis signe le contenu.
Au moment du verify, il redécode le token, vérifie la signature, puis applique les contrôles de configuration comme l'expiration, l'issuer, le subject ou l'audience.
verify retourne un résultat either. En cas de succès, l'information est token-verified et la valeur contient le header et le payload décodés.
Pourquoi createOrThrow existe ?
En pratique, on recommande createOrThrow.
Si la création d'un token échoue, c'est généralement un problème d'implémentation ou de configuration côté serveur, pas un scénario métier attendu. Dans ce cas, lever une erreur immédiatement est un comportement sain.
À l'inverse, verify n'a pas de variante orThrow : un échec de vérification peut être parfaitement normal (signature invalide, token expiré, claims incohérents, etc.). Ce n'est pas forcément une erreur d'implémentation, donc ce cas reste géré comme un résultat métier.
Paramètres
interface TokenHandlerParams {
maxAge: D.TheTime;
signer: Signer<string> | CreateSigner<string, unknown>;
cipher?: Cipher<string> | CreateCipher<string, unknown>;
issuer?: string;
subject?: string;
audience?: string | string[];
now?: () => D.TheDate;
customPayloadShape: DP.DataParserObjectShape;
customHeaderShape?: DP.DataParserObjectShape;
};Le handler retourné expose ensuite quatre méthodes :
create: crée un token et retournetoken-createdou une erreur de création.createOrThrow: crée directement une chaîne de token, ou throw si la création échoue.verify: vérifie la signature et les claims configurés, puis retournetoken-verifiedou une erreur de vérification.decode: lit le header et le payload sans vérifier la signature ni les claims, puis retournetoken-decodedou une erreur de décodage.
Exemple avec custom shapes
import { D, DPE, E, unwrap } from "@duplojs/utils";
import { Signer, createTokenHandler } from "@duplojs/json-web-token";
const tokenHandler = createTokenHandler({
maxAge: D.createTime(1, "hour"),
signer: Signer.createHS256({ secret: "my-secret" }),
issuer: "admin-app",
customPayloadShape: {
userId: DPE.string(),
role: DPE.literal("admin"),
},
customHeaderShape: {
kid: DPE.string().optional(),
},
});
const token = await tokenHandler.createOrThrow(
{
userId: "42",
role: "admin",
},
{
header: {
kid: "main",
},
},
);
// send to client ...
const decodedTokenResult = await tokenHandler.decode("receive-token");
if (E.isRight(decodedTokenResult)) {
const decodedToken = unwrap(decodedTokenResult);
const userId = decodedToken.payload.userId;
}Ce qui se passe ici
customPayloadShape et customHeaderShape définissent ce que ton application a le droit de mettre dans le token.
Les clés réservées du JWT, comme exp, iat, iss, sub, aud, typ ou alg, restent gérées par le handler lui-même.
decode lit uniquement le contenu du token. C'est utile pour inspecter, mais ce n'est pas la méthode à utiliser pour faire confiance à un token reçu.
Exemple avec des "creators"
import { D, DPE, E, unwrap } from "@duplojs/utils";
import { Cipher, Signer, createTokenHandler } from "@duplojs/json-web-token";
const tokenHandler = createTokenHandler({
maxAge: D.createTime(10, "minute"),
signer: Signer.createHS256,
cipher: Cipher.createRSAOAEP,
customPayloadShape: {
userId: DPE.string(),
},
});
const token = await tokenHandler.createOrThrow(
{
userId: "1",
},
{
signer: {
secret: "my-secret",
},
cipher: {
privateKey: "private-key",
publicKey: "public-key",
},
},
);
// send to client ...
const verifiedTokenResult = await tokenHandler.verify("receive-token", {
signer: {
secret: "my-secret",
},
cipher: {
privateKey: "private-key",
publicKey: "public-key",
},
});
if (E.isRight(verifiedTokenResult)) {
const verifiedToken = unwrap(verifiedTokenResult);
const userId = verifiedToken.payload.userId;
}Ce qui se passe ici
Quand tu passes un CreateSigner ou un CreateCipher au lieu d'une instance déjà configurée, les paramètres sont déplacés vers create, createOrThrow, verify et decode.
Cela permet de créer le handler une seule fois, tout en injectant plus tard les secrets, les clés ou d'autres paramètres nécessaires.
Le résultat de succès reste le même : create retourne token-created, decode retourne token-decoded et verify retourne token-verified.
