work-license-server 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/assets/admin.js +572 -0
- package/assets/landing.css +113 -1
- package/assets/landing.js +322 -290
- package/bin/server.mjs +5 -2
- package/package.json +1 -1
- package/src/admin.js +319 -0
- package/src/agent.js +152 -8
- package/src/bots.js +1 -1
- package/src/catalog.js +153 -17
- package/src/crypto.js +249 -17
- package/src/index.js +22 -1
- package/src/landing.js +113 -141
- package/src/server.js +229 -24
- package/src/store.js +696 -50
- package/src/yookassa.js +22 -8
package/src/catalog.js
CHANGED
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
export const PAK_PRODUCT = Object.freeze({
|
|
8
8
|
sku: 'WORK-PAK-495-192-4T',
|
|
9
|
+
baseLicenseSku: 'WORK-LIC-BASE-10',
|
|
9
10
|
name: 'ODANT-WORK',
|
|
10
11
|
domain: 'work.odant.org',
|
|
11
12
|
basePriceRub: 5_000_000,
|
|
12
|
-
|
|
13
|
+
baseLicensePriceRub: 4_000_000,
|
|
14
|
+
baseSeats: 10,
|
|
15
|
+
unlicensedBossSeats: 1,
|
|
13
16
|
perpetualBase: true,
|
|
14
17
|
includedSupportYears: 1,
|
|
15
|
-
includedSupportTier: '
|
|
18
|
+
includedSupportTier: 'enterprise',
|
|
19
|
+
includedSupportTierName: 'Enterprise',
|
|
20
|
+
postYearPolicy: 'Дальнейшее продление поддержки и докупка мест — по желанию клиента (10 вечных лицензий работают бессрочно)',
|
|
16
21
|
hardware: Object.freeze({
|
|
17
22
|
cpu: 'AMD Ryzen™ AI Max+ PRO 495 (Zen 5, 16 ядер / 32 потока, до 5.2 ГГц, кэш 80 МБ)',
|
|
18
23
|
gpu: 'AMD Radeon™ 8065S Graphics (40 ядер, 3000 МГц, до 61 TFLOPS FP16)',
|
|
@@ -25,16 +30,17 @@ export const PAK_PRODUCT = Object.freeze({
|
|
|
25
30
|
}),
|
|
26
31
|
software: Object.freeze({
|
|
27
32
|
accounting: 'Встроенная система учёта, документооборота, управления процессами и приватный мультимодальный ИИ (справочники, план счетов, двойная запись, оборотно-сальдовая ведомость, анализ счёта, первичные документы и сквозные процессы)',
|
|
28
|
-
seatModel: 'По одновременным активным сессиям (
|
|
33
|
+
seatModel: 'По одновременным активным сессиям (10 вечных лицензий в базовом комплекте + 1 год поддержки Enterprise; дальше — продление и докупка мест по желанию)',
|
|
29
34
|
hardwareBinding: 'Привязка вечной лицензии к Hardware ID материнской платы/CPU AMD Ryzen + криптографическому ключу узла (#system/node-key.json)',
|
|
30
35
|
}),
|
|
36
|
+
modelsPolicy: 'Интегрированные локальные модели предназначены для базовых ежедневных операций внутри контура. Если требуются более мощные модели ИИ, их можно подключить через стандартный API из любого источника (облачные провайдеры, собственный GPU-кластер или API регионального партнёра).',
|
|
31
37
|
preinstalledModels: Object.freeze([
|
|
32
38
|
{
|
|
33
39
|
id: 'llm-core',
|
|
34
40
|
type: 'Большая языковая модель (LLM)',
|
|
35
|
-
title: '
|
|
36
|
-
badge: '
|
|
37
|
-
description: '
|
|
41
|
+
title: 'Базовое текстовое и учётное ядро',
|
|
42
|
+
badge: 'Базовые операции · Учёт · Документы',
|
|
43
|
+
description: 'Выполнение базовых операций: живой диалог, типовые хозяйственные проводки, шаблоны документов и повседневные задачи. Для глубокой аналитики и тяжёлых задач подключаются более мощные модели через API из любого источника.',
|
|
38
44
|
ramGb: 18.5,
|
|
39
45
|
},
|
|
40
46
|
{
|
|
@@ -77,7 +83,7 @@ export const AI_PLATFORM = Object.freeze({
|
|
|
77
83
|
'Действует в рамках роли и прав пользователя (ADMIN · BOSS · USER)',
|
|
78
84
|
'Инструменты учётного ядра: описание класса, поиск объектов, проведение операций, отчёты',
|
|
79
85
|
'Каждая консультация сохраняется задачей в личном кабинете и в ленте пользователя',
|
|
80
|
-
'
|
|
86
|
+
'Базовые операции выполняются интегрированными локальными моделями, а при необходимости более мощных ИИ — подключение через API из любого источника',
|
|
81
87
|
]),
|
|
82
88
|
}),
|
|
83
89
|
orchestration: Object.freeze([
|
|
@@ -210,13 +216,60 @@ export function calculateSeatVolumeDiscount(seats = 1) {
|
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
/**
|
|
213
|
-
*
|
|
219
|
+
* Отдельная базовая постоянная лицензия на 10 вечных пользователей (без оборудования) —
|
|
220
|
+
* для клиентов, самостоятельно приобретших автономную машину AMD Ryzen™ AI Max+ PRO 495 (192 ГБ)
|
|
221
|
+
* и установивших систему через npm.
|
|
222
|
+
*/
|
|
223
|
+
export const BASE_LICENSE_PRODUCT = Object.freeze({
|
|
224
|
+
sku: 'WORK-LIC-BASE-10',
|
|
225
|
+
name: 'Базовая постоянная лицензия ODANT-WORK (10 вечных пользователей, без железа)',
|
|
226
|
+
priceRub: 4_000_000,
|
|
227
|
+
baseSeats: 10,
|
|
228
|
+
perpetualBase: true,
|
|
229
|
+
aiAgentsEnabled: true,
|
|
230
|
+
includedSupportYears: 1,
|
|
231
|
+
includedSupportTier: 'enterprise',
|
|
232
|
+
description: 'Снимает стартовые ограничения: активирует агентскую ИИ-систему, открывает 10 вечных конкурентных пользовательских мест и включает 1 год поддержки Enterprise.',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Дополнительные услуги и товары, доступные в личном кабинете владельца.
|
|
237
|
+
*/
|
|
238
|
+
export const ADDITIONAL_PRODUCTS = Object.freeze([
|
|
239
|
+
{
|
|
240
|
+
id: 'ai-finetune-pack',
|
|
241
|
+
sku: 'WORK-SRV-FINETUNE',
|
|
242
|
+
title: 'Пакет дообучения локальных моделей ИИ (LoRA) и разработки навыков под предприятие',
|
|
243
|
+
priceRub: 600_000,
|
|
244
|
+
category: 'service',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: 'nas-backup-kit',
|
|
248
|
+
sku: 'WORK-HW-NAS',
|
|
249
|
+
title: 'Комплект резервного копирования и архивации баз организаций (NAS)',
|
|
250
|
+
priceRub: 350_000,
|
|
251
|
+
category: 'hardware',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'audit-migration-pack',
|
|
255
|
+
sku: 'WORK-SRV-MIGRATION',
|
|
256
|
+
title: 'Пакет переноса исторических учётных баз, справочников и остатков',
|
|
257
|
+
priceRub: 450_000,
|
|
258
|
+
category: 'service',
|
|
259
|
+
},
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Рассчитать стоимость заказа (покупка базового комплекса с машиной 5 млн ₽, отдельно базовой лицензии 4 млн ₽,
|
|
264
|
+
* докупка срочных годовых лицензий со скидкой от количества и прочих товаров/услуг).
|
|
214
265
|
*/
|
|
215
266
|
export function calculateOrderQuote({
|
|
216
267
|
includeBasePak = false,
|
|
268
|
+
includeBaseLicenseOnly = false,
|
|
217
269
|
tier = 'business',
|
|
218
270
|
addonSeats = 0,
|
|
219
271
|
years = 1,
|
|
272
|
+
extraProductIds = [],
|
|
220
273
|
} = {}) {
|
|
221
274
|
const tierKey = String(tier || 'business').toLowerCase();
|
|
222
275
|
const tierSpec = SLA_TIERS[tierKey] || SLA_TIERS.business;
|
|
@@ -226,25 +279,42 @@ export function calculateOrderQuote({
|
|
|
226
279
|
const volumeDiscount = calculateSeatVolumeDiscount(seats);
|
|
227
280
|
const volumeDiscountPercent = volumeDiscount.discountPercent;
|
|
228
281
|
|
|
229
|
-
const
|
|
282
|
+
const hasFullPak = Boolean(includeBasePak);
|
|
283
|
+
const hasBaseLicOnly = !hasFullPak && Boolean(includeBaseLicenseOnly);
|
|
284
|
+
|
|
285
|
+
const baseAmountRub = hasFullPak
|
|
286
|
+
? PAK_PRODUCT.basePriceRub
|
|
287
|
+
: (hasBaseLicOnly ? BASE_LICENSE_PRODUCT.priceRub : 0);
|
|
288
|
+
|
|
230
289
|
const addonsGrossAmountRub = seats * tierSpec.pricePerUserYearRub * periodYears;
|
|
231
290
|
const addonsDiscountRub = Math.round(addonsGrossAmountRub * (volumeDiscountPercent / 100));
|
|
232
291
|
const addonsAmountRub = addonsGrossAmountRub - addonsDiscountRub;
|
|
233
292
|
const effectiveUnitPriceRub = seats > 0 ? Math.round(addonsAmountRub / seats) : tierSpec.pricePerUserYearRub * periodYears;
|
|
234
|
-
const totalAmountRub = baseAmountRub + addonsAmountRub;
|
|
235
293
|
|
|
236
294
|
const items = [];
|
|
237
|
-
if (
|
|
295
|
+
if (hasFullPak) {
|
|
238
296
|
items.push({
|
|
239
|
-
|
|
297
|
+
sku: PAK_PRODUCT.sku,
|
|
298
|
+
title: `${PAK_PRODUCT.name} (${PAK_PRODUCT.hardware.cpu}, ${PAK_PRODUCT.hardware.ramGb} ГБ RAM, SSD ${PAK_PRODUCT.hardware.ssdTb} ТБ, вечная лицензия на ${PAK_PRODUCT.baseSeats} мест + 1 год поддержки Enterprise)`,
|
|
240
299
|
quantity: 1,
|
|
241
300
|
unitPriceRub: PAK_PRODUCT.basePriceRub,
|
|
242
301
|
totalRub: PAK_PRODUCT.basePriceRub,
|
|
243
302
|
});
|
|
244
303
|
}
|
|
304
|
+
else if (hasBaseLicOnly) {
|
|
305
|
+
items.push({
|
|
306
|
+
sku: BASE_LICENSE_PRODUCT.sku,
|
|
307
|
+
title: BASE_LICENSE_PRODUCT.name,
|
|
308
|
+
quantity: 1,
|
|
309
|
+
unitPriceRub: BASE_LICENSE_PRODUCT.priceRub,
|
|
310
|
+
totalRub: BASE_LICENSE_PRODUCT.priceRub,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
245
314
|
if (seats > 0) {
|
|
246
315
|
const discSuffix = volumeDiscountPercent > 0 ? `, скидка −${volumeDiscountPercent}% за объём` : '';
|
|
247
316
|
items.push({
|
|
317
|
+
sku: `WORK-LIC-ADDON-${tierSpec.id.toUpperCase()}`,
|
|
248
318
|
title: `Годовая лицензия ODANT-WORK на доп. рабочее место (уровень ${tierSpec.name}, ${periodYears} г.${discSuffix})`,
|
|
249
319
|
quantity: seats,
|
|
250
320
|
unitPriceRub: effectiveUnitPriceRub,
|
|
@@ -252,11 +322,31 @@ export function calculateOrderQuote({
|
|
|
252
322
|
});
|
|
253
323
|
}
|
|
254
324
|
|
|
325
|
+
let extraAmountRub = 0;
|
|
326
|
+
const extraIds = Array.isArray(extraProductIds) ? extraProductIds : [];
|
|
327
|
+
for (const pid of extraIds) {
|
|
328
|
+
const prod = ADDITIONAL_PRODUCTS.find(p => p.id === pid || p.sku === pid);
|
|
329
|
+
if (prod) {
|
|
330
|
+
extraAmountRub += prod.priceRub;
|
|
331
|
+
items.push({
|
|
332
|
+
sku: prod.sku,
|
|
333
|
+
title: prod.title,
|
|
334
|
+
quantity: 1,
|
|
335
|
+
unitPriceRub: prod.priceRub,
|
|
336
|
+
totalRub: prod.priceRub,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const totalAmountRub = baseAmountRub + addonsAmountRub + extraAmountRub;
|
|
342
|
+
|
|
255
343
|
return {
|
|
256
|
-
includeBasePak:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
344
|
+
includeBasePak: hasFullPak,
|
|
345
|
+
includeBaseLicenseOnly: hasBaseLicOnly,
|
|
346
|
+
basePakPriceRub: hasFullPak ? PAK_PRODUCT.basePriceRub : 0,
|
|
347
|
+
baseLicenseOnlyPriceRub: hasBaseLicOnly ? BASE_LICENSE_PRODUCT.priceRub : 0,
|
|
348
|
+
baseSeats: (hasFullPak || hasBaseLicOnly) ? PAK_PRODUCT.baseSeats : 0,
|
|
349
|
+
includedSupportYears: (hasFullPak || hasBaseLicOnly) ? PAK_PRODUCT.includedSupportYears : 0,
|
|
260
350
|
tier: tierSpec.id,
|
|
261
351
|
tierName: tierSpec.name,
|
|
262
352
|
pricePerUserYearRub: tierSpec.pricePerUserYearRub,
|
|
@@ -267,6 +357,7 @@ export function calculateOrderQuote({
|
|
|
267
357
|
addonsGrossAmountRub,
|
|
268
358
|
addonsDiscountRub,
|
|
269
359
|
addonsAmountRub,
|
|
360
|
+
extraAmountRub,
|
|
270
361
|
totalAmountRub,
|
|
271
362
|
reactionMinutes: tierSpec.reactionMinutes,
|
|
272
363
|
reactionLabel: tierSpec.reactionLabel,
|
|
@@ -536,7 +627,7 @@ export const PARTNER_TRAINING_TRACKS = Object.freeze([
|
|
|
536
627
|
passScore: 75,
|
|
537
628
|
requiredForFranchise: true,
|
|
538
629
|
topics: [
|
|
539
|
-
'Экономическое обоснование внедрения ODANT-WORK (
|
|
630
|
+
'Экономическое обоснование внедрения ODANT-WORK (10 вечных лицензий + 1 год поддержки Enterprise + локальный ИИ в собственном контуре)',
|
|
540
631
|
'Лицензионная политика: расчёт конкурентных сессий со скидками от объёма и подбор тарифов SLA (Start / Business / Enterprise)',
|
|
541
632
|
'Регламент первой и второй линии поддержки через боты MAX, Telegram и VK',
|
|
542
633
|
],
|
|
@@ -675,3 +766,48 @@ export function calculatePartnerEconomics({
|
|
|
675
766
|
operationsModel: FRANCHISE_PROGRAM.partnerOperationsModel,
|
|
676
767
|
};
|
|
677
768
|
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Публичный договор-оферта ODANT-WORK (ст. 435, 437, 1286 ГК РФ и ФЗ № 63-ФЗ «Об электронной подписи»).
|
|
772
|
+
* Подписание оферты простой электронной подписью (ПЭП) в личном кабинете обязательно перед оформлением
|
|
773
|
+
* любого заказа комплекса, докупкой годовых лицензий, оплатой или резервированием региональной франшизы.
|
|
774
|
+
*/
|
|
775
|
+
export const PUBLIC_OFERTA = Object.freeze({
|
|
776
|
+
version: 'ODANT-WORK-OFERTA-2026.1',
|
|
777
|
+
updatedAt: '01.11.2026',
|
|
778
|
+
title: 'Публичный договор-оферта на поставку ПАК ODANT-WORK, предоставление неисключительных лицензий, техническую поддержку (SLA) и резервирование региональной франшизы',
|
|
779
|
+
vendor: VENDOR_CONTACTS,
|
|
780
|
+
sections: Object.freeze([
|
|
781
|
+
{
|
|
782
|
+
number: '1',
|
|
783
|
+
title: 'Общие положения и предмет Договора-оферты',
|
|
784
|
+
text: 'Настоящий документ является официальным публичным предложением (офертой) ООО «Бизнес Интер Софт» (ИНН 6234049193, г. Рязань, ул. Ленина, 47), именуемого далее «Правообладатель», заключить договор на поставку программно-аппаратного комплекса ODANT-WORK, предоставление права использования ПО (простой неисключительной лицензии), сопровождение по уровню SLA и/или резервирование региональной партнёрской франшизы с любым юридическим лицом или индивидуальным предпринимателем («Заказчик / Партнёр»).',
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
number: '2',
|
|
788
|
+
title: 'Комплектация ПАК ODANT-WORK и вечная базовая лицензия',
|
|
789
|
+
text: 'Базовый программно-аппаратный комплекс ODANT-WORK (AMD Ryzen™ AI Max+ PRO 495, Radeon™ 8065S, 192 ГБ Unified RAM, SSD 4 ТБ) стоимостью 5 000 000 ₽ включает бессрочную (вечную) лицензию на 10 одновременных активных пользовательских сессий (привязанную к HWID оборудования и криптографическому ключу узла #system/node-key.json) и 1-й год технической поддержки максимального уровня Enterprise (24/7, время реакции до 15 минут). По истечении первого года продление поддержки и докупка мест осуществляются по желанию Заказчика; 10 базовых вечных лицензий продолжают работать бессрочно.',
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
number: '3',
|
|
793
|
+
title: 'Годовые пользовательские лицензии и уровни поддержки (SLA)',
|
|
794
|
+
text: 'Дополнительные конкурентные рабочие места предоставляются сроком от 1 до 5 лет по выбранному тарифу SLA: Start (10 000 ₽/год за место, до 12 обращений, реакция до 8 раб. ч.), Business (15 000 ₽/год, до 48 обращений, реакция до 2 ч.), Enterprise (20 000 ₽/год, безлимит обращений, реакция до 15 мин 24/7). При единовременном заказе от 5 мест применяется объёмная скидка от 5% до 25%. Выпуск и продление лицензий удостоверяются цифровым сертификатом Ed25519 (протокол WORK-LIC-v1).',
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
number: '4',
|
|
798
|
+
title: 'Региональная франшиза и условия резервирования квоты',
|
|
799
|
+
text: 'Квота региональных партнёров-франчайзи в каждом субъекте РФ пропорциональна численности населения региона. Фиксированный паушальный взнос равен 500 000 ₽, умноженным на экономический индекс выбранного субъекта РФ. При оформлении заявки на резервирование франшизы Заказчик оплачивает невозвратный обеспечительный залог в размере 100 000 ₽, который закрепляет за ним региональную квоту и засчитывается в счёт паушального взноса. Оставшаяся часть паушального взноса подлежит полной оплате в течение 12 (двенадцати) месяцев с даты резервирования.',
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
number: '5',
|
|
803
|
+
title: 'Порядок расчётов и фискализация (ЮKassa · 54-ФЗ)',
|
|
804
|
+
text: 'Все расчёты по настоящему Договору производятся в рублях РФ через авторизованный платёжный шлюз ЮKassa (API v3) с формированием кассового чека в соответствии с 54-ФЗ либо по безналичному счёту для юридических лиц. Исполнение обязательств по выпуску/обновлению сертификата лицензии или закреплению квоты франшизы происходит автоматически в момент подтверждения оплаты.',
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
number: '6',
|
|
808
|
+
title: 'Акцепт оферты простой электронной подписью (ПЭП)',
|
|
809
|
+
text: 'В соответствии со ст. 438 ГК РФ и ст. 6 ФЗ № 63-ФЗ «Об электронной подписи», регистрация Заказчика в личном кабинете на сайте work.odant.org с указанием реквизитов организации (наименование, ИНН, корпоративный email, ФИО представителя) и установка отметки о согласии с условиями настоящей Оферты признаются полным и безоговорочным акцептом Договора-оферты простой электронной подписью (ПЭП), равнозначным договору, подписанному собственноручно.',
|
|
810
|
+
},
|
|
811
|
+
]),
|
|
812
|
+
});
|
|
813
|
+
|
package/src/crypto.js
CHANGED
|
@@ -11,17 +11,70 @@ import { PAK_PRODUCT } from './catalog.js';
|
|
|
11
11
|
export const LICENSE_PROTOCOL = 'WORK-LIC-v1';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Официальные публичные ключи центра сертификации Правообладателя ODANT (Ed25519 SPKI DER base64).
|
|
15
|
+
* Вшиты в клиентский код odant-work; закрытые ключи хранятся исключительно на боевом сервере work.odant.org
|
|
16
|
+
* и холодном резервном носителе Правообладателя (никогда не публикуются в npm и репозиториях).
|
|
15
17
|
*/
|
|
16
|
-
export
|
|
18
|
+
export const ODANT_AUTHORITY_PUBLIC_KEY = 'MCowBQYDK2VwAyEAdlQSj+pC3IOt60vo3lXkzpd0DFU4ZmADqEYQsW1+mRA=';
|
|
19
|
+
export const ODANT_BACKUP_PUBLIC_KEY = 'MCowBQYDK2VwAyEA8xJ9r7V4kM2pN6qL1wZ5tB3cF0yH8gD4sE7uA9vK2mQ=';
|
|
20
|
+
export const TRUSTED_AUTHORITY_PUBLIC_KEYS = Object.freeze([
|
|
21
|
+
ODANT_AUTHORITY_PUBLIC_KEY,
|
|
22
|
+
ODANT_BACKUP_PUBLIC_KEY,
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Получить список доверенных публичных ключей центра сертификации (без доверия к полям внутри самого сертификата!).
|
|
27
|
+
*/
|
|
28
|
+
export function resolveTrustedAuthorityKeys(trustedPublicKeyB64 = '') {
|
|
29
|
+
const keys = [];
|
|
30
|
+
if (trustedPublicKeyB64)
|
|
31
|
+
keys.push(String(trustedPublicKeyB64).trim());
|
|
32
|
+
if (process.env.WORK_LICENSE_PUBLIC_KEY)
|
|
33
|
+
keys.push(String(process.env.WORK_LICENSE_PUBLIC_KEY).trim());
|
|
34
|
+
for (const k of TRUSTED_AUTHORITY_PUBLIC_KEYS) {
|
|
35
|
+
if (!keys.includes(k))
|
|
36
|
+
keys.push(k);
|
|
37
|
+
}
|
|
38
|
+
return keys.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Загрузить ключевую пару Ed25519 центра лицензирования из защищённого окружения (WORK_AUTHORITY_PRIVATE_KEY)
|
|
43
|
+
* или локального хранилища (в dev/test режиме).
|
|
44
|
+
*/
|
|
45
|
+
export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = false } = {}) {
|
|
46
|
+
const envPriv = String(process.env.WORK_AUTHORITY_PRIVATE_KEY || '').trim();
|
|
47
|
+
if (envPriv) {
|
|
48
|
+
const privObj = crypto.createPrivateKey({
|
|
49
|
+
key: Buffer.from(envPriv, 'base64'),
|
|
50
|
+
format: 'der',
|
|
51
|
+
type: 'pkcs8',
|
|
52
|
+
});
|
|
53
|
+
const derivedPub = crypto.createPublicKey(privObj).export({ type: 'spki', format: 'der' }).toString('base64');
|
|
54
|
+
const pubB64 = String(process.env.WORK_AUTHORITY_PUBLIC_KEY || derivedPub).trim();
|
|
55
|
+
return {
|
|
56
|
+
publicKey: pubB64,
|
|
57
|
+
privateKey: envPriv,
|
|
58
|
+
source: 'env',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
17
62
|
if (keyFilePath && fs.existsSync(keyFilePath)) {
|
|
18
63
|
try {
|
|
19
64
|
const raw = JSON.parse(fs.readFileSync(keyFilePath, 'utf8'));
|
|
20
65
|
if (raw?.publicKey && raw?.privateKey)
|
|
21
66
|
return raw;
|
|
22
67
|
}
|
|
23
|
-
catch { /* создадим заново */ }
|
|
68
|
+
catch { /* создадим заново ниже */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (process.env.NODE_ENV === 'production' && !allowGenerateInProd) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Безопасность: в production-режиме запрещена автогенерация случайных ключей лицензирования. '
|
|
74
|
+
+ 'Задайте секретный ключ правообладателя в переменной окружения WORK_AUTHORITY_PRIVATE_KEY.'
|
|
75
|
+
);
|
|
24
76
|
}
|
|
77
|
+
|
|
25
78
|
const pair = crypto.generateKeyPairSync('ed25519');
|
|
26
79
|
const keys = {
|
|
27
80
|
publicKey: pair.publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),
|
|
@@ -92,27 +145,206 @@ export function signCertificate(lic, authorityKeys) {
|
|
|
92
145
|
}
|
|
93
146
|
|
|
94
147
|
/**
|
|
95
|
-
* Проверить подпись сертификата
|
|
148
|
+
* Проверить подпись сертификата лицензии строго по доверенным ключам центра сертификации ODANT
|
|
149
|
+
* (поле lic.issuerPublicKey внутри самого сертификата НЕ является источником доверия).
|
|
96
150
|
*/
|
|
97
151
|
export function verifyCertificate(lic, publicKeyB64 = '') {
|
|
98
152
|
if (!lic || typeof lic !== 'object' || !lic.signature)
|
|
99
153
|
return false;
|
|
100
|
-
const
|
|
101
|
-
|
|
154
|
+
const trustedKeys = resolveTrustedAuthorityKeys(publicKeyB64);
|
|
155
|
+
const payload = canonicalLicenseString(lic);
|
|
156
|
+
const sigBuf = Buffer.from(String(lic.signature), 'base64');
|
|
157
|
+
for (const keyB64 of trustedKeys) {
|
|
158
|
+
try {
|
|
159
|
+
const pub = crypto.createPublicKey({
|
|
160
|
+
key: Buffer.from(String(keyB64), 'base64'),
|
|
161
|
+
format: 'der',
|
|
162
|
+
type: 'spki',
|
|
163
|
+
});
|
|
164
|
+
if (crypto.verify(null, Buffer.from(payload, 'utf8'), pub, sigBuf))
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch { /* try next trusted key */ }
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const LICENSE_FILE_PROTOCOL = 'WORK-LIC-FILE-v1';
|
|
173
|
+
export const OWNER_TOKEN_PREFIX = 'owk1';
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Каноническая сериализация отдельного файла лицензии (для размещения в #system).
|
|
177
|
+
* kind:
|
|
178
|
+
* - 'base' — первичная постоянная лицензия (10 вечных пользователей + снятие блокировки агентской системы);
|
|
179
|
+
* - 'addon' — дополнительная временная лицензия (N конкурентных пользователей на определённый период).
|
|
180
|
+
*/
|
|
181
|
+
export function canonicalLicenseFileString(fileObj = {}) {
|
|
182
|
+
const clean = {
|
|
183
|
+
protocol: fileObj.protocol || LICENSE_FILE_PROTOCOL,
|
|
184
|
+
fileId: String(fileObj.fileId || ''),
|
|
185
|
+
kind: String(fileObj.kind || 'base'), // 'base' | 'addon'
|
|
186
|
+
licenseId: String(fileObj.licenseId || ''),
|
|
187
|
+
ownerAccountId: String(fileObj.ownerAccountId || ''),
|
|
188
|
+
ownerEmail: String(fileObj.ownerEmail || '').toLowerCase(),
|
|
189
|
+
issuer: String(fileObj.issuer || 'work.odant.org'),
|
|
190
|
+
issuedAt: Number(fileObj.issuedAt || 0),
|
|
191
|
+
expiresAt: fileObj.perpetual ? null : Number(fileObj.expiresAt || 0),
|
|
192
|
+
perpetual: Boolean(fileObj.perpetual),
|
|
193
|
+
seats: Number(fileObj.seats || 0),
|
|
194
|
+
years: Number(fileObj.years || (fileObj.perpetual ? 0 : 1)),
|
|
195
|
+
tier: String(fileObj.tier || 'enterprise').toLowerCase(),
|
|
196
|
+
aiAgentsEnabled: fileObj.aiAgentsEnabled !== false,
|
|
197
|
+
orderId: String(fileObj.orderId || ''),
|
|
198
|
+
hardware: {
|
|
199
|
+
hwid: String(fileObj.hardware?.hwid || ''),
|
|
200
|
+
nodeFingerprint: String(fileObj.hardware?.nodeFingerprint || ''),
|
|
201
|
+
sku: String(fileObj.hardware?.sku || PAK_PRODUCT.sku),
|
|
202
|
+
},
|
|
203
|
+
customer: {
|
|
204
|
+
name: String(fileObj.customer?.name || ''),
|
|
205
|
+
inn: String(fileObj.customer?.inn || ''),
|
|
206
|
+
email: String(fileObj.customer?.email || ''),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
return JSON.stringify(clean);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Подписать отдельный файл лицензии (base или addon) закрытым ключом Ed25519 центра сертификации.
|
|
214
|
+
*/
|
|
215
|
+
export function signLicenseFile(fileObj, authorityKeys) {
|
|
216
|
+
const priv = crypto.createPrivateKey({
|
|
217
|
+
key: Buffer.from(String(authorityKeys.privateKey), 'base64'),
|
|
218
|
+
format: 'der',
|
|
219
|
+
type: 'pkcs8',
|
|
220
|
+
});
|
|
221
|
+
const payload = canonicalLicenseFileString(fileObj);
|
|
222
|
+
const signature = crypto.sign(null, Buffer.from(payload, 'utf8'), priv).toString('base64');
|
|
223
|
+
return {
|
|
224
|
+
...fileObj,
|
|
225
|
+
protocol: fileObj.protocol || LICENSE_FILE_PROTOCOL,
|
|
226
|
+
issuer: fileObj.issuer || 'work.odant.org',
|
|
227
|
+
issuerPublicKey: authorityKeys.publicKey,
|
|
228
|
+
signature,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Проверить подпись отдельного файла лицензии Ed25519 строго по доверенным ключам центра сертификации ODANT
|
|
234
|
+
* (поле fileObj.issuerPublicKey внутри файла НЕ является источником доверия).
|
|
235
|
+
*/
|
|
236
|
+
export function verifyLicenseFile(fileObj, publicKeyB64 = '') {
|
|
237
|
+
if (!fileObj || typeof fileObj !== 'object' || !fileObj.signature)
|
|
102
238
|
return false;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
239
|
+
const trustedKeys = resolveTrustedAuthorityKeys(publicKeyB64);
|
|
240
|
+
const payload = canonicalLicenseFileString(fileObj);
|
|
241
|
+
const sigBuf = Buffer.from(String(fileObj.signature), 'base64');
|
|
242
|
+
for (const keyB64 of trustedKeys) {
|
|
243
|
+
try {
|
|
244
|
+
const pub = crypto.createPublicKey({
|
|
245
|
+
key: Buffer.from(String(keyB64), 'base64'),
|
|
246
|
+
format: 'der',
|
|
247
|
+
type: 'spki',
|
|
248
|
+
});
|
|
249
|
+
if (crypto.verify(null, Buffer.from(payload, 'utf8'), pub, sigBuf))
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch { /* try next trusted key */ }
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Сгенерировать подписанный токен владельца (owk1.<payload_b64url>.<sig_b64url>),
|
|
259
|
+
* содержащий URL сервера поддержки (hubUrl), ID аккаунта владельца и секретный ключ для доступа локальной системы к ЛК.
|
|
260
|
+
*/
|
|
261
|
+
export function createOwnerAccessToken({
|
|
262
|
+
hubUrl = 'https://work.odant.org',
|
|
263
|
+
accountId = '',
|
|
264
|
+
email = '',
|
|
265
|
+
tokenId = '',
|
|
266
|
+
secret = '',
|
|
267
|
+
issuedAt = Date.now(),
|
|
268
|
+
} = {}, authorityKeys) {
|
|
269
|
+
const payloadObj = {
|
|
270
|
+
v: 1,
|
|
271
|
+
hubUrl: String(hubUrl || 'https://work.odant.org').replace(/\/+$/, ''),
|
|
272
|
+
accountId: String(accountId || ''),
|
|
273
|
+
email: String(email || '').toLowerCase(),
|
|
274
|
+
tokenId: String(tokenId || ('TOK-' + crypto.randomBytes(4).toString('hex').toUpperCase())),
|
|
275
|
+
secret: String(secret || crypto.randomBytes(16).toString('hex')),
|
|
276
|
+
issuedAt: Number(issuedAt || Date.now()),
|
|
277
|
+
issuerPublicKey: authorityKeys?.publicKey || '',
|
|
278
|
+
};
|
|
279
|
+
const payloadB64 = Buffer.from(JSON.stringify(payloadObj), 'utf8').toString('base64url');
|
|
280
|
+
let sigB64 = '';
|
|
281
|
+
if (authorityKeys?.privateKey) {
|
|
282
|
+
const priv = crypto.createPrivateKey({
|
|
283
|
+
key: Buffer.from(String(authorityKeys.privateKey), 'base64'),
|
|
106
284
|
format: 'der',
|
|
107
|
-
type: '
|
|
285
|
+
type: 'pkcs8',
|
|
108
286
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
287
|
+
sigB64 = crypto.sign(null, Buffer.from(`${OWNER_TOKEN_PREFIX}.${payloadB64}`, 'utf8'), priv).toString('base64url');
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
token: `${OWNER_TOKEN_PREFIX}.${payloadB64}.${sigB64}`,
|
|
291
|
+
payload: payloadObj,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Распарсить токен владельца (owk1.<payload_b64url>.<sig_b64url>) и извлечь зашитый URL сервера поддержки и реквизиты.
|
|
297
|
+
*/
|
|
298
|
+
export function parseOwnerAccessToken(tokenStr = '') {
|
|
299
|
+
const raw = String(tokenStr || '').trim();
|
|
300
|
+
const parts = raw.split('.');
|
|
301
|
+
if (parts.length !== 3 || parts[0] !== OWNER_TOKEN_PREFIX) {
|
|
302
|
+
throw new Error('Некорректный формат токена владельца (ожидается owk1.<payload>.<signature>)');
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const json = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
306
|
+
const payload = JSON.parse(json);
|
|
307
|
+
if (!payload?.hubUrl || !payload?.accountId || !payload?.tokenId) {
|
|
308
|
+
throw new Error('В токене отсутствуют обязательные поля hubUrl / accountId / tokenId');
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
token: raw,
|
|
312
|
+
prefix: parts[0],
|
|
313
|
+
payloadB64: parts[1],
|
|
314
|
+
signatureB64: parts[2],
|
|
315
|
+
payload,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
throw new Error(`Не удалось декодировать токен владельца: ${err?.message || err}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Проверить криптографическую подпись Ed25519 токена владельца строго по доверенным ключам центра сертификации ODANT
|
|
325
|
+
* (поле payload.issuerPublicKey внутри токена НЕ является источником доверия).
|
|
326
|
+
*/
|
|
327
|
+
export function verifyOwnerAccessToken(tokenStr = '', trustedPublicKeyB64 = '') {
|
|
328
|
+
try {
|
|
329
|
+
const parsed = parseOwnerAccessToken(tokenStr);
|
|
330
|
+
if (!parsed.signatureB64)
|
|
331
|
+
return false;
|
|
332
|
+
const trustedKeys = resolveTrustedAuthorityKeys(trustedPublicKeyB64);
|
|
333
|
+
const signedData = Buffer.from(`${OWNER_TOKEN_PREFIX}.${parsed.payloadB64}`, 'utf8');
|
|
334
|
+
const sigBuf = Buffer.from(parsed.signatureB64, 'base64url');
|
|
335
|
+
for (const keyB64 of trustedKeys) {
|
|
336
|
+
try {
|
|
337
|
+
const pub = crypto.createPublicKey({
|
|
338
|
+
key: Buffer.from(String(keyB64), 'base64'),
|
|
339
|
+
format: 'der',
|
|
340
|
+
type: 'spki',
|
|
341
|
+
});
|
|
342
|
+
if (crypto.verify(null, signedData, pub, sigBuf))
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
catch { /* try next trusted key */ }
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
116
348
|
}
|
|
117
349
|
catch {
|
|
118
350
|
return false;
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export {
|
|
2
2
|
PAK_PRODUCT,
|
|
3
|
+
BASE_LICENSE_PRODUCT,
|
|
4
|
+
ADDITIONAL_PRODUCTS,
|
|
3
5
|
AI_PLATFORM,
|
|
4
6
|
VENDOR_CONTACTS,
|
|
5
7
|
FEDERAL_DISTRICTS,
|
|
@@ -14,8 +16,27 @@ export {
|
|
|
14
16
|
calculateFranchiseFee,
|
|
15
17
|
calculatePartnerDiscount,
|
|
16
18
|
calculatePartnerEconomics,
|
|
19
|
+
PUBLIC_OFERTA,
|
|
17
20
|
} from './catalog.js';
|
|
18
|
-
export {
|
|
21
|
+
export {
|
|
22
|
+
LICENSE_PROTOCOL,
|
|
23
|
+
LICENSE_FILE_PROTOCOL,
|
|
24
|
+
OWNER_TOKEN_PREFIX,
|
|
25
|
+
ODANT_AUTHORITY_PUBLIC_KEY,
|
|
26
|
+
ODANT_BACKUP_PUBLIC_KEY,
|
|
27
|
+
TRUSTED_AUTHORITY_PUBLIC_KEYS,
|
|
28
|
+
resolveTrustedAuthorityKeys,
|
|
29
|
+
loadOrCreateAuthorityKeys,
|
|
30
|
+
canonicalLicenseString,
|
|
31
|
+
signCertificate,
|
|
32
|
+
verifyCertificate,
|
|
33
|
+
canonicalLicenseFileString,
|
|
34
|
+
signLicenseFile,
|
|
35
|
+
verifyLicenseFile,
|
|
36
|
+
createOwnerAccessToken,
|
|
37
|
+
parseOwnerAccessToken,
|
|
38
|
+
verifyOwnerAccessToken,
|
|
39
|
+
} from './crypto.js';
|
|
19
40
|
export { LicenseStore } from './store.js';
|
|
20
41
|
export { YooKassaService } from './yookassa.js';
|
|
21
42
|
export { SupportBotsHub } from './bots.js';
|