work-license-server 1.0.11 → 1.0.13

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.
Files changed (2) hide show
  1. package/package.json +4 -1
  2. package/src/server.js +83 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "work-license-server",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Лицензионный сервер, биллинг ЮKassa, боты поддержки (MAX, Telegram, VK) и лендинг ПАК WORK (work.odant.org)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -28,5 +28,8 @@
28
28
  "license": "SEE LICENSE IN README.md",
29
29
  "engines": {
30
30
  "node": ">=22.0.0"
31
+ },
32
+ "dependencies": {
33
+ "nodemailer": "^8.0.4"
31
34
  }
32
35
  }
package/src/server.js CHANGED
@@ -157,28 +157,90 @@ function loadSmtpConfig() {
157
157
  }
158
158
 
159
159
  /**
160
- * Отправка реального письма с 6-значным кодом подтверждения по SMTPS (TLS 465, без внешних зависимостей).
160
+ * Отправка реального письма с 6-значным кодом подтверждения (через nodemailer — точно как в sources/host/auth-methods.js,
161
+ * с полноценными RFC 5322 заголовками Date, Message-ID и text+html, чтобы Gmail доставлял письмо мгновенно).
161
162
  */
163
+ let _cachedTransporter = null;
162
164
  async function sendOtpEmail({ to, code }) {
163
165
  const cfg = loadSmtpConfig();
164
- const subjectB64 = Buffer.from(`Код подтверждения ODANT-WORK: ${code}`, 'utf8').toString('base64');
165
- const htmlBody = [
166
- '<div style="font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:480px;margin:0 auto;padding:24px;border:1px solid #e2e8f0;border-radius:12px;">',
167
- '<h2 style="margin:0 0 12px;color:#0f172a;">Вход в Личный кабинет ODANT-WORK</h2>',
168
- '<p style="color:#475569;font-size:15px;">Ваш одноразовый код подтверждения:</p>',
169
- `<div style="font-size:36px;font-weight:800;letter-spacing:8px;text-align:center;padding:16px;background:#f8fafc;border-radius:10px;color:#0284c7;">${code}</div>`,
170
- '<p style="color:#64748b;font-size:13px;margin-top:16px;">Код действителен 10 минут. Если вы не запрашивали вход на work.odant.org, просто проигнорируйте это письмо.</p>',
171
- '</div>',
172
- ].join('\r\n');
166
+ const subject = 'Код подтверждения ODANT-WORK';
167
+ const textBody = `Ваш одноразовый код: ${code}\n\nДействует 10 минут.`;
168
+ const htmlBody = `
169
+ <h2>Код подтверждения</h2>
170
+ <p>Ваш код для входа/регистрации на work.odant.org:</p>
171
+ <h1 style="font-size: 48px; letter-spacing: 10px; font-weight: bold; text-align: center;">
172
+ ${code}
173
+ </h1>
174
+ <p>Код действителен <strong>10 минут</strong>. Ни с кем не делитесь.</p>
175
+ <p>Если это не вы — просто проигнорируйте письмо.</p>
176
+ <p>С уважением,<br>ODANT-WORK</p>
177
+ `;
178
+
179
+ try {
180
+ let nm;
181
+ try {
182
+ nm = await import('nodemailer');
183
+ }
184
+ catch {
185
+ nm = await import(new URL('../../node_modules/nodemailer/lib/nodemailer.js', import.meta.url).href);
186
+ }
187
+ const nodemailer = nm.default || nm;
188
+ if (!_cachedTransporter) {
189
+ _cachedTransporter = nodemailer.createTransport({
190
+ host: cfg.host,
191
+ port: cfg.port,
192
+ secure: cfg.port === 465,
193
+ auth: {
194
+ user: cfg.user,
195
+ pass: cfg.pass,
196
+ },
197
+ });
198
+ }
199
+ const info = await _cachedTransporter.sendMail({
200
+ from: `"ODANT-WORK" <${cfg.user}>`,
201
+ to,
202
+ subject,
203
+ text: textBody,
204
+ html: htmlBody,
205
+ });
206
+ console.log('[work.odant.org] SMTP delivered to', to, 'messageId:', info?.messageId, 'response:', info?.response);
207
+ return { ok: true, messageId: info?.messageId };
208
+ }
209
+ catch (nmErr) {
210
+ if (nmErr?.code !== 'ERR_MODULE_NOT_FOUND') {
211
+ console.error('[work.odant.org] SMTP error via nodemailer:', nmErr?.message || nmErr);
212
+ throw nmErr;
213
+ }
214
+ }
215
+
216
+ // Резервный нативный SMTPS-клиент с полными RFC 5322 заголовками (Date + Message-ID + multipart/alternative text+html)
217
+ const domain = (cfg.user.split('@')[1] || 'yandex.ru').trim();
218
+ const msgId = `<${crypto.randomUUID()}@${domain}>`;
219
+ const dateHdr = new Date().toUTCString();
220
+ const boundary = `----=_Part_${crypto.randomBytes(8).toString('hex')}`;
221
+ const subjectB64 = Buffer.from(subject, 'utf8').toString('base64');
222
+ const b64Text = (Buffer.from(textBody, 'utf8').toString('base64').match(/.{1,76}/g) || []).join('\r\n');
223
+ const b64Html = (Buffer.from(htmlBody, 'utf8').toString('base64').match(/.{1,76}/g) || []).join('\r\n');
173
224
  const mimeMsg = [
225
+ `Date: ${dateHdr}`,
226
+ `Message-ID: ${msgId}`,
174
227
  `From: "ODANT-WORK" <${cfg.user}>`,
175
228
  `To: <${to}>`,
176
229
  `Subject: =?UTF-8?B?${subjectB64}?=`,
177
230
  'MIME-Version: 1.0',
231
+ `Content-Type: multipart/alternative; boundary="${boundary}"`,
232
+ '',
233
+ `--${boundary}`,
234
+ 'Content-Type: text/plain; charset=utf-8',
235
+ 'Content-Transfer-Encoding: base64',
236
+ '',
237
+ b64Text,
238
+ `--${boundary}`,
178
239
  'Content-Type: text/html; charset=utf-8',
179
240
  'Content-Transfer-Encoding: base64',
180
241
  '',
181
- Buffer.from(htmlBody, 'utf8').toString('base64'),
242
+ b64Html,
243
+ `--${boundary}--`,
182
244
  '.',
183
245
  '',
184
246
  ].join('\r\n');
@@ -202,7 +264,7 @@ async function sendOtpEmail({ to, code }) {
202
264
  reject(new Error('Ошибка SMTP: ' + line));
203
265
  return;
204
266
  }
205
- if (step === 0) { step = 1; sendCmd('EHLO work.odant.org'); }
267
+ if (step === 0) { step = 1; sendCmd(`EHLO ${domain}`); }
206
268
  else if (step === 1) { step = 2; sendCmd('AUTH LOGIN'); }
207
269
  else if (step === 2) { step = 3; sendCmd(Buffer.from(cfg.user, 'utf8').toString('base64')); }
208
270
  else if (step === 3) { step = 4; sendCmd(Buffer.from(cfg.pass, 'utf8').toString('base64')); }
@@ -210,7 +272,7 @@ async function sendOtpEmail({ to, code }) {
210
272
  else if (step === 5) { step = 6; sendCmd(`RCPT TO:<${to}>`); }
211
273
  else if (step === 6) { step = 7; sendCmd('DATA'); }
212
274
  else if (step === 7) { step = 8; sock.write(mimeMsg); }
213
- else if (step === 8) { step = 9; sendCmd('QUIT'); sock.end(); resolve({ ok: true }); }
275
+ else if (step === 8) { step = 9; sendCmd('QUIT'); sock.end(); resolve({ ok: true, messageId: msgId }); }
214
276
  }
215
277
  });
216
278
  });
@@ -330,7 +392,14 @@ export function createLicenseServerApp(options = {}) {
330
392
 
331
393
  // 2. Каталог ПАК, отдельной базовой лицензии (4 млн ₽), тарифов SLA, прочих товаров, партнёрской франшизы и оферты
332
394
  if (req.method === 'GET' && pathname === '/api/catalog') {
395
+ let serverVersion = '1.0.13';
396
+ try {
397
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
398
+ serverVersion = pkg.version || serverVersion;
399
+ }
400
+ catch { /* ignore */ }
333
401
  return sendJson(res, 200, {
402
+ serverVersion,
334
403
  product: PAK_PRODUCT,
335
404
  baseLicenseProduct: BASE_LICENSE_PRODUCT,
336
405
  additionalProducts: ADDITIONAL_PRODUCTS,
@@ -360,6 +429,7 @@ export function createLicenseServerApp(options = {}) {
360
429
  expiresAt: Date.now() + 10 * 60_000,
361
430
  attempts: 0,
362
431
  });
432
+ console.log('[work.odant.org] Registration/Login code for', cleanEmail, ':', code);
363
433
  if (!options.skipEmailSend) {
364
434
  await sendOtpEmail({ to: cleanEmail, code });
365
435
  }