mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
fix(openrouter): nettoyer la clé API souillée par un copier-coller
OpenRouter répondait « 401 Missing Authentication header », ce qui laisse croire à un en-tête absent alors qu'il était bien envoyé. Vérification faite contre l'API : ce message n'apparaît pas quand la clé est invalide (l'API répond alors « User not found »), mais quand le jeton placé derrière « Bearer » n'a pas la forme d'une clé — préfixe « Bearer » recopié depuis un exemple, guillemets laissés autour de la valeur, ou caractère invisible ramassé lors d'un copier-coller. String.trim() ne retire pas ces derniers. La clé est désormais nettoyée à l'enregistrement et à la lecture ; ce second point remet en marche les clés déjà stockées sans ressaisie. En cas de 401, le message indique en plus la longueur de la clé et si elle commence par « sk-or- », sans jamais en révéler le contenu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
This commit is contained in:
2 files changed
+56
-6
No files matched your search
+8
-1
@@ -9,7 +9,7 @@ import multer from "multer";
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import passport from "./auth";
|
import passport from "./auth";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { openRouterService, describeGenerationError } from "./services/openrouter";
|
import { openRouterService, describeGenerationError, sanitizeApiKey } from "./services/openrouter";
|
||||||
import { minioService as cloudinaryService, buildMinioUrl } from "./services/minio";
|
import { minioService as cloudinaryService, buildMinioUrl } from "./services/minio";
|
||||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
||||||
import type { User, InsertUser, ScheduledPost } from "@shared/schema";
|
import type { User, InsertUser, ScheduledPost } from "@shared/schema";
|
||||||
@@ -1853,6 +1853,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Check if config already exists
|
// Check if config already exists
|
||||||
const existingConfig = await storage.getOpenrouterConfig(userId);
|
const existingConfig = await storage.getOpenrouterConfig(userId);
|
||||||
|
|
||||||
|
// Une clé collée depuis un navigateur traîne souvent un « Bearer », des
|
||||||
|
// guillemets ou un caractère invisible. Stockée telle quelle, elle fait
|
||||||
|
// répondre « Missing Authentication header » à OpenRouter.
|
||||||
|
if (typeof req.body?.apiKey === 'string') {
|
||||||
|
req.body.apiKey = sanitizeApiKey(req.body.apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
let config;
|
let config;
|
||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
// Pour les mises à jour, utiliser le schéma qui rend apiKey optionnel
|
// Pour les mises à jour, utiliser le schéma qui rend apiKey optionnel
|
||||||
|
|||||||
@@ -15,6 +15,36 @@ interface GeneratedText {
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { storage } from '../storage';
|
import { storage } from '../storage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie une clé API collée depuis un navigateur ou une documentation.
|
||||||
|
*
|
||||||
|
* OpenRouter répond « Missing Authentication header » — et non « User not
|
||||||
|
* found » — dès que le jeton qui suit « Bearer » n'a pas la forme d'une clé :
|
||||||
|
* préfixe « Bearer » recopié par mégarde, guillemets autour de la valeur, ou
|
||||||
|
* espace insécable/de largeur nulle ramassé lors d'un copier-coller. Le message
|
||||||
|
* laisse croire à un en-tête absent alors que la clé est simplement souillée.
|
||||||
|
*
|
||||||
|
* Appliqué à l'enregistrement comme à la lecture, pour rattraper les clés déjà
|
||||||
|
* stockées sans imposer une ressaisie.
|
||||||
|
*/
|
||||||
|
export function sanitizeApiKey(raw: string): string {
|
||||||
|
let key = (raw || '')
|
||||||
|
// Caractères invisibles que String.trim() ne retire pas
|
||||||
|
.replace(/[-]/g, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// « Bearer sk-… » recopié depuis un exemple de documentation
|
||||||
|
key = key.replace(/^bearer\s+/i, '').trim();
|
||||||
|
|
||||||
|
// Valeur entourée de guillemets, typique d'un copier-coller depuis un JSON
|
||||||
|
const quoted = key.match(/^(["'])(.*)\1$/);
|
||||||
|
if (quoted) {
|
||||||
|
key = quoted[2].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extrait le message d'erreur d'une réponse OpenRouter, qu'elle soit au format
|
* Extrait le message d'erreur d'une réponse OpenRouter, qu'elle soit au format
|
||||||
* JSON ({ error: { message } }) ou en texte brut. Tronqué pour éviter de
|
* JSON ({ error: { message } }) ou en texte brut. Tronqué pour éviter de
|
||||||
@@ -70,7 +100,12 @@ export class OpenRouterService {
|
|||||||
throw new Error('Configuration OpenRouter non trouvée. Veuillez demander à un administrateur de configurer OpenRouter dans les Paramètres.');
|
throw new Error('Configuration OpenRouter non trouvée. Veuillez demander à un administrateur de configurer OpenRouter dans les Paramètres.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.apiKey || !config.apiKey.trim()) {
|
// Rattrape les clés déjà stockées avec un « Bearer », des guillemets ou un
|
||||||
|
// caractère invisible : sans cela OpenRouter répond « Missing Authentication
|
||||||
|
// header », ce qui laisse croire à tort que la clé est absente.
|
||||||
|
const apiKey = sanitizeApiKey(config.apiKey);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
throw new Error('Clé API OpenRouter manquante ou vide en base de données. Veuillez la ressaisir dans les Paramètres.');
|
throw new Error('Clé API OpenRouter manquante ou vide en base de données. Veuillez la ressaisir dans les Paramètres.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,14 +114,14 @@ export class OpenRouterService {
|
|||||||
// Use provided model or fall back to config model
|
// Use provided model or fall back to config model
|
||||||
const modelToUse = modelOverride || config.model;
|
const modelToUse = modelOverride || config.model;
|
||||||
|
|
||||||
const keyFingerprint = crypto.createHash('sha256').update(config.apiKey).digest('hex').slice(0, 8);
|
const keyFingerprint = crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 8);
|
||||||
console.log(`[OpenRouter] Generating with model="${modelToUse}" keyFingerprint=${keyFingerprint} keyLength=${config.apiKey.length} configUserId=${config.userId}`);
|
console.log(`[OpenRouter] Generating with model="${modelToUse}" keyFingerprint=${keyFingerprint} keyLength=${apiKey.length} configUserId=${config.userId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.baseUrl, {
|
const response = await fetch(this.baseUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bearer ${config.apiKey.trim()}`,
|
"Authorization": `Bearer ${apiKey}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"HTTP-Referer": process.env.APP_URL || "http://localhost:5555",
|
"HTTP-Referer": process.env.APP_URL || "http://localhost:5555",
|
||||||
"X-Title": "Social Flow"
|
"X-Title": "Social Flow"
|
||||||
@@ -110,9 +145,17 @@ export class OpenRouterService {
|
|||||||
// Le motif renvoyé par OpenRouter (clé invalide, crédits épuisés, modèle
|
// Le motif renvoyé par OpenRouter (clé invalide, crédits épuisés, modèle
|
||||||
// inconnu…) est la seule information exploitable : on le fait remonter
|
// inconnu…) est la seule information exploitable : on le fait remonter
|
||||||
// jusqu'à l'utilisateur, qui n'a pas accès aux logs du serveur.
|
// jusqu'à l'utilisateur, qui n'a pas accès aux logs du serveur.
|
||||||
|
// Sur un refus d'authentification, la forme de la clé est le seul
|
||||||
|
// indice utile — sans jamais en révéler le contenu.
|
||||||
|
const hint = response.status === 401
|
||||||
|
? ` La clé enregistrée fait ${apiKey.length} caractères et ` +
|
||||||
|
`${apiKey.startsWith('sk-or-') ? 'commence' : 'ne commence pas'} par "sk-or-" : ` +
|
||||||
|
`vérifiez-la dans Paramètres → OpenRouter.`
|
||||||
|
: '';
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`OpenRouter a refusé la requête (${response.status} ${response.statusText}) ` +
|
`OpenRouter a refusé la requête (${response.status} ${response.statusText}) ` +
|
||||||
`avec le modèle "${modelToUse}" : ${extractUpstreamMessage(errorText)}`
|
`avec le modèle "${modelToUse}" : ${extractUpstreamMessage(errorText)}.${hint}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user