@elcrm/jwt
v0.1.1
Published
AES-GCM ssid tokens for elCRM: createJWT, multi-session, oneToken, validateAgent, refresh.
Readme
@elcrm/jwt
Зашифрованные ssid-токены для elCRM под Bun: AES-256-GCM, алиасы, TTL, validateAgent, oneToken, multi-session (sessions + _sid), refresh.
Установка
bun add @elcrm/jwtТребования: Bun ≥ 1.0. Пакет отдаёт TypeScript-исходники (src/lib) — отдельная сборка у потребителя не нужна.
Это зашифрованный payload (AES-GCM), а не классический подписанный JWT (JWS). Секрет: secret или process.env.TOKEN.
Разработка и публикация: dev.md, deploy.md.
Быстрый старт
import { createJWT } from "@elcrm/jwt";
const jwt = createJWT({
secret: "your-secret-key",
alias: [
["i", "id"],
["j", "job"],
],
expiresIn: 3600,
validateAgent: true,
oneToken: true,
tokenFilePath: "jwt.json",
excludeRoutes: ["login", "register"], // для сервера через getConfig()
});
const token = await jwt.create({ id: 123, job: "developer" });
const data = await jwt.parse(token);Создание экземпляра
const jwt = createJWT({
secret: "your-secret-key",
alias: [
["i", "id"],
["j", "job"],
],
expiresIn: 3600,
validateAgent: true,
oneToken: true,
tokenFilePath: "jwt.json",
});
// Или сразу создать токен: createJWT(options, data) → Promise<string>
const token = await createJWT({ secret: "key" }, { id: 1 });Создание токена
const token = await jwt.create({ id: 123, job: "developer" });
const headers = {
get: (name: string) => {
if (name === "x-forwarded-for") return "192.168.1.1";
if (name === "user-agent") return "Mozilla/5.0";
return null;
},
};
const token2 = await jwt.create(
{ id: 456 },
{
expiresIn: 7200,
headers,
},
);
// setHeaders — безопасная изоляция заголовков на запрос
const jwtWithHeaders = jwt.setHeaders(headers);
const token3 = await jwtWithHeaders.create({ id: 789 });Парсинг и init
const data = await jwt.parse(token);
if (data === false) {
// невалиден / истёк / IP/UA / oneToken
} else {
console.log(data);
}
const result = await jwt.init(headers);
if (result.error) {
// jwt_token_not_found | jwt_token_invalid (+ code: 401)
} else {
console.log(result);
}
// requireAdmin: user.admin === 1
const admin = await jwt.init(headers, undefined, true);Multi-session (sessions)
import { createJWT, createMemorySessionStore, createFileSessionStore } from "@elcrm/jwt";
const jwt = createJWT({
secret: "…",
sessions: true, // или createMemorySessionStore() / createFileSessionStore("sessions.json") / свой store
});
const token = await jwt.create({ id: 1 });
const user = await jwt.parse(token); // user.sid — id сессии
await jwt.listSessions("1");
await jwt.revokeSession(user.sid);
await jwt.revokeOthers("1", user.sid);
await jwt.revokeAll("1");oneToken: true + sessions = максимум одна активная сессия (при create остальные отзываются).
Без sessions поведение oneToken через файл _ct сохранено (legacy).
Параметры createJWT
| Опция | По умолчанию | Описание |
| --------------- | ------------ | ----------------------------------------------------- |
| secret | TOKEN env | Секрет AES (обязателен, если нет env) |
| alias | [] | [["short", "full"], …] — только поля с алиасами |
| expiresIn | null | TTL в секундах; null — без срока |
| headerName | "ssid" | Заголовок для init() |
| validateAgent | false | Сохранять/проверять IP и User-Agent |
| oneToken | false | Один активный токен на id (файл + поле _ct) |
| tokenFilePath | "jwt.json" | Файл для oneToken |
| excludeRoutes | [] | Список роутов без JWT — читается через getConfig() |
| wirePrefix | "" | Wire-формат {prefix}.{aes}; create/parse/init/refresh |
Опции algorithm / ivLength / saltLength / tagLength / keyLength — низкоуровневые; шифрование всегда через AES-GCM Web Crypto.
API экземпляра
| Метод | Назначение |
| ---------------- | ----------------------------------------------- |
| create | Создать токен |
| parse | Расшифровать; false при ошибке |
| init | Токен из заголовка headerName |
| refresh | Новый токен из (в т.ч. истёкшего) |
| setHeaders | Новый экземпляр с привязанными заголовками |
| deleteToken | Удалить запись пользователя из oneToken-файла |
| setSecret | Сменить секрет |
| setExpiresIn | Сменить TTL по умолчанию |
| setAlias / addAlias | Алиасы |
| getConfig | Текущая конфигурация (в т.ч. excludeRoutes, wirePrefix) |
| password | SHA-256 hex (Bun.CryptoHasher) |
| generateSalt | Случайная соль (crypto.getRandomValues) |
| hash | Hex по кодам символов (для agent) |
| agent | Отпечаток IP+UA |
Хелперы (multi-tenant auth — разные секреты на slug):
| Функция | Назначение |
| ------------------ | ----------------------------------------------- |
| packWireToken | prefix + aes → prefix.aes |
| unpackWireToken | разобрать wire; null если формат неверный |
Wire-префикс (tenant slug)
// Один тенант / один секрет — префикс на инстансе
const jwt = createJWT({
secret: tenantSecret,
wirePrefix: "wms-by9cqxaw",
});
const token = await jwt.create({ id: "u1" }); // "wms-by9cqxaw.<aes>"
await jwt.parse(token);
// Auth (много тенантов): префикс при create, секрет — после unpack
import { packWireToken, unpackWireToken } from "@elcrm/jwt";
const aes = await jwtBare.create({ id: "u1" });
const ssid = packWireToken(slug, aes);
const { prefix, token } = unpackWireToken(ssid)!;
const secret = await secretBySlug(prefix);
await jwtFor(secret).parse(token);create / parse / init / refresh
jwt.create(data, {
customSecret?: string,
expiresIn?: number | null,
headers?: RequestHeaders | RequestLike,
});
jwt.parse(token, customSecret?, allowExpired?, reqOrHeaders?);
jwt.init(headers, customSecret?, requireAdmin?, allowExpired?);
jwt.refresh(expiredToken, newExpiresIn?, customSecret?, reqOrHeaders?);Примеры
Алиасы
const jwt = createJWT({
secret: "key",
alias: [
["i", "id"],
["j", "job"],
],
});
const token = await jwt.create({ id: 123, job: "dev" });
const data = await jwt.parse(token); // { id: 123, job: "dev" }TTL и refresh
const jwt = createJWT({ secret: "key", expiresIn: 3600 });
const token = await jwt.create({ id: 123 });
const newToken = await jwt.refresh(token, 7200);validateAgent + setHeaders
const jwt = createJWT({ secret: "key", validateAgent: true });
const jwtReq = jwt.setHeaders(headers);
const token = await jwtReq.create({ id: 123 });
const user = await jwtReq.init(headers);oneToken
При oneToken: true новый токен для того же id инвалидирует предыдущий. Нужно поле id (или алиас на id). Данные: кэш в памяти + файл tokenFilePath.
const jwt = createJWT({ secret: "key", oneToken: true });
const t1 = await jwt.create({ id: 123 });
const t2 = await jwt.create({ id: 123 });
await jwt.parse(t1); // false
await jwt.parse(t2); // { id: 123 }
await jwt.deleteToken("123"); // выход / сброс сессииexcludeRoutes на сервере
Конфиг хранится в JWT; проверка маршрута — на стороне приложения:
const { excludeRoutes } = config.jwt.getConfig();
const skipAuth = excludeRoutes.includes(path[0]);Список лучше кэшировать при старте (Set), не вызывать getConfig() на каждый запрос.
Лицензия
MIT
