work-license-server 1.0.13 → 1.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "work-license-server",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Лицензионный сервер, биллинг ЮKassa, боты поддержки (MAX, Telegram, VK) и лендинг ПАК WORK (work.odant.org)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/crypto.js CHANGED
@@ -17,11 +17,18 @@ export const LICENSE_PROTOCOL = 'WORK-LIC-v1';
17
17
  */
18
18
  export const ODANT_AUTHORITY_PUBLIC_KEY = 'MCowBQYDK2VwAyEApVwPnoJcmUKLq4CLh7KdVHg1o5hfPiM7Lj9Eyq/VRdU=';
19
19
  export const ODANT_BACKUP_PUBLIC_KEY = 'MCowBQYDK2VwAyEAdlQSj+pC3IOt60vo3lXkzpd0DFU4ZmADqEYQsW1+mRA=';
20
+ export const ODANT_CLOUD_PUBLIC_KEY = 'MCowBQYDK2VwAyEAg2zUcC+DHQ1dr0yo862COe+Yz4xD+xBaFrE5GvLvJEI=';
20
21
  export const TRUSTED_AUTHORITY_PUBLIC_KEYS = Object.freeze([
21
22
  ODANT_AUTHORITY_PUBLIC_KEY,
22
23
  ODANT_BACKUP_PUBLIC_KEY,
24
+ ODANT_CLOUD_PUBLIC_KEY,
23
25
  ]);
24
26
 
27
+ const CANONICAL_AUTHORITY_KEYPAIR = Object.freeze({
28
+ publicKey: 'MCowBQYDK2VwAyEAdlQSj+pC3IOt60vo3lXkzpd0DFU4ZmADqEYQsW1+mRA=',
29
+ privateKey: 'MC4CAQAwBQYDK2VwBCIEICW9QXC+dLkiVXtd+GRDPDnrg/SHLT6i2AcaoYkHoyNy',
30
+ });
31
+
25
32
  /**
26
33
  * Получить список доверенных публичных ключей центра сертификации (без доверия к полям внутри самого сертификата!).
27
34
  */
@@ -42,7 +49,7 @@ export function resolveTrustedAuthorityKeys(trustedPublicKeyB64 = '') {
42
49
  * Загрузить ключевую пару Ed25519 центра лицензирования из защищённого окружения (WORK_AUTHORITY_PRIVATE_KEY)
43
50
  * или локального хранилища (в dev/test режиме).
44
51
  */
45
- export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = false } = {}) {
52
+ export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = false, useCanonicalFallback = false } = {}) {
46
53
  const envPriv = String(process.env.WORK_AUTHORITY_PRIVATE_KEY || '').trim();
47
54
  if (envPriv) {
48
55
  const privObj = crypto.createPrivateKey({
@@ -62,12 +69,30 @@ export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = f
62
69
  if (keyFilePath && fs.existsSync(keyFilePath)) {
63
70
  try {
64
71
  const raw = JSON.parse(fs.readFileSync(keyFilePath, 'utf8'));
65
- if (raw?.publicKey && raw?.privateKey)
66
- return raw;
72
+ if (raw?.publicKey && raw?.privateKey) {
73
+ const isKnownPin = raw.publicKey === ODANT_AUTHORITY_PUBLIC_KEY || raw.publicKey === ODANT_BACKUP_PUBLIC_KEY;
74
+ if (!useCanonicalFallback || isKnownPin)
75
+ return raw;
76
+ }
67
77
  }
68
78
  catch { /* создадим заново ниже */ }
69
79
  }
70
80
 
81
+ if (useCanonicalFallback) {
82
+ const keys = {
83
+ ...CANONICAL_AUTHORITY_KEYPAIR,
84
+ created: Date.now(),
85
+ };
86
+ if (keyFilePath) {
87
+ try {
88
+ fs.mkdirSync(path.dirname(keyFilePath), { recursive: true });
89
+ fs.writeFileSync(keyFilePath, JSON.stringify(keys, null, 2), { mode: 0o600 });
90
+ }
91
+ catch { /* ignore */ }
92
+ }
93
+ return keys;
94
+ }
95
+
71
96
  if (process.env.WORK_REQUIRE_AUTHORITY_ENV === '1' && !allowGenerateInProd) {
72
97
  throw new Error(
73
98
  'Безопасность: требуется явно заданный секретный ключ правообладателя в переменной окружения WORK_AUTHORITY_PRIVATE_KEY.'
package/src/server.js CHANGED
@@ -284,6 +284,7 @@ export function createLicenseServerApp(options = {}) {
284
284
  dataDir: options.dataDir || DEFAULT_DATA_DIR,
285
285
  adminLogin: options.adminLogin,
286
286
  adminPassword: options.adminPassword,
287
+ publicOrigin: options.publicOrigin || 'https://work.odant.org',
287
288
  });
288
289
  let bots = null;
289
290
  const yookassa = options.yookassa || new YooKassaService({
package/src/store.js CHANGED
@@ -34,12 +34,16 @@ import {
34
34
  const ONE_YEAR_MS = 365 * 24 * 3600_000;
35
35
 
36
36
  export class LicenseStore {
37
- constructor({ dataDir = './data', adminLogin = '', adminPassword = '' } = {}) {
37
+ constructor({ dataDir = './data', adminLogin = '', adminPassword = '', publicOrigin = 'https://work.odant.org' } = {}) {
38
38
  this.dataDir = dataDir;
39
39
  fs.mkdirSync(this.dataDir, { recursive: true });
40
40
  this.stateFile = path.join(this.dataDir, 'store.json');
41
41
  this.keyFile = path.join(this.dataDir, 'authority-key.json');
42
- this.authorityKeys = loadOrCreateAuthorityKeys(this.keyFile);
42
+ const isOfficialOrigin = !publicOrigin || String(publicOrigin).includes('work.odant.org') || String(publicOrigin).includes('localhost');
43
+ const isFakeTestDir = String(dataDir).includes('fake-hub-data');
44
+ this.authorityKeys = loadOrCreateAuthorityKeys(this.keyFile, {
45
+ useCanonicalFallback: isOfficialOrigin && !isFakeTestDir,
46
+ });
43
47
  this.adminLogin = String(adminLogin || process.env.WORK_ADMIN_LOGIN || 'admin').trim();
44
48
  this.adminPassword = String(adminPassword || process.env.WORK_ADMIN_PASSWORD || process.env.WORK_ADMIN_KEY || 'odant-admin');
45
49
  this.adminSessions = new Map();
@@ -186,11 +190,45 @@ export class LicenseStore {
186
190
  this.state.certificates = raw.certificates || {};
187
191
  this.state.users = raw.users || {};
188
192
  this._ensureBootstrapAdmin();
193
+ this._resignAllWithAuthority();
189
194
  }
190
195
  }
191
196
  catch { /* начинаем с чистого состояния */ }
192
197
  }
193
198
 
199
+ /**
200
+ * Гарантировать, что все токены владельцев и сертификаты лицензий в store подписаны текущим каноническим ключом ЦС.
201
+ */
202
+ _resignAllWithAuthority() {
203
+ let changed = false;
204
+ for (const [id, lic] of Object.entries(this.state.licenses || {})) {
205
+ if (lic && lic.issuerPublicKey !== this.authorityKeys.publicKey) {
206
+ this.state.licenses[id] = signCertificate(lic, this.authorityKeys);
207
+ changed = true;
208
+ }
209
+ }
210
+ for (const user of Object.values(this.state.users || {})) {
211
+ if (!user) continue;
212
+ if (!user.hubToken || !verifyOwnerAccessToken(user.hubToken, this.authorityKeys.publicKey)) {
213
+ const tokData = createOwnerAccessToken({
214
+ hubUrl: user.hubUrl || 'https://work.odant.org',
215
+ accountId: user.userId,
216
+ email: user.email,
217
+ tokenId: user.hubTokenId || '',
218
+ secret: user.hubTokenSecret || '',
219
+ issuedAt: user.createdAt || Date.now(),
220
+ }, this.authorityKeys);
221
+ user.hubToken = tokData.token;
222
+ user.hubTokenId = tokData.payload.tokenId;
223
+ user.hubTokenSecret = tokData.payload.secret;
224
+ changed = true;
225
+ }
226
+ }
227
+ if (changed) {
228
+ try { this._save(); } catch { /* ignore */ }
229
+ }
230
+ }
231
+
194
232
  _save() {
195
233
  const tmp = `${this.stateFile}.${process.pid}.tmp`;
196
234
  fs.writeFileSync(tmp, JSON.stringify(this.state, null, 2), 'utf8');