chore: license server updates and tests
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -34,11 +34,21 @@ MC4CAQAwBQYDK2VwBCIEIDNxA9U1VSG9zoOvcJ5uB+JUe25UD5m9UwMi6slXzW44
|
||||
|
||||
## Запуск
|
||||
|
||||
На Windows перед запуском задайте переменную окружения `LICENSE_PRIVATE_KEY_PEM` (многострочное значение в кавычках или через `.env` и загрузчик по желанию).
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
На Windows перед запуском задайте переменную окружения `LICENSE_PRIVATE_KEY_PEM` (многострочное значение в кавычках или через `.env` и загрузчик по желанию).
|
||||
## Тесты
|
||||
|
||||
Из корня репозитория:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Запускает модульные тесты Node (`node --test`, см. `package.json` → `test`).
|
||||
|
||||
## API
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createPrivateKey, sign } from 'node:crypto';
|
||||
|
||||
import { canonicalJson } from './canonicalJson.mjs';
|
||||
|
||||
export function b64url(buf) {
|
||||
return Buffer.from(buf)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/u, '');
|
||||
}
|
||||
|
||||
export function signPayload(payload, privateKeyPem) {
|
||||
const key = createPrivateKey(privateKeyPem);
|
||||
const body = canonicalJson(payload);
|
||||
const sig = sign(null, Buffer.from(body, 'utf8'), key);
|
||||
return `${b64url(Buffer.from(body, 'utf8'))}.${b64url(sig)}`;
|
||||
}
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
"type": "module",
|
||||
"description": "Сервис выдачи и отзыва лицензий DNDGamePlayer (Ed25519)",
|
||||
"scripts": {
|
||||
"start": "node src/server.mjs"
|
||||
"start": "node src/server.mjs",
|
||||
"test": "node --test test/*.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
||||
+2
-16
@@ -1,10 +1,11 @@
|
||||
import { createPrivateKey, randomUUID, sign } from 'node:crypto';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { canonicalJson } from '../lib/canonicalJson.mjs';
|
||||
import { signPayload } from '../lib/signPayload.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
@@ -20,21 +21,6 @@ function writeData(data) {
|
||||
fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function b64url(buf) {
|
||||
return Buffer.from(buf)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/u, '');
|
||||
}
|
||||
|
||||
function signPayload(payload, privateKeyPem) {
|
||||
const key = createPrivateKey(privateKeyPem);
|
||||
const body = canonicalJson(payload);
|
||||
const sig = sign(null, Buffer.from(body, 'utf8'), key);
|
||||
return `${b64url(Buffer.from(body, 'utf8'))}.${b64url(sig)}`;
|
||||
}
|
||||
|
||||
function json(res, code, obj) {
|
||||
const body = JSON.stringify(obj);
|
||||
res.writeHead(code, {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { canonicalJson } from '../lib/canonicalJson.mjs';
|
||||
|
||||
void test('canonicalJson: stable key order', () => {
|
||||
const a = canonicalJson({ b: 2, a: 1 });
|
||||
const b = canonicalJson({ a: 1, b: 2 });
|
||||
assert.equal(a, b);
|
||||
assert.equal(a, '{"a":1,"b":2}');
|
||||
});
|
||||
|
||||
void test('canonicalJson: rejects non-finite number', () => {
|
||||
assert.throws(() => canonicalJson({ x: NaN }), TypeError);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { generateKeyPairSync, verify } from 'node:crypto';
|
||||
import test from 'node:test';
|
||||
|
||||
import { signPayload } from '../lib/signPayload.mjs';
|
||||
|
||||
void test('signPayload produces verifiable Ed25519 token', () => {
|
||||
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
||||
const pem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
||||
const payload = { v: 1, sub: 's1', pid: 'p', iat: 1, exp: 2, did: 'd1' };
|
||||
const token = signPayload(payload, pem);
|
||||
const [bodyB64, sigB64] = token.split('.');
|
||||
assert.ok(bodyB64 && sigB64);
|
||||
const body = Buffer.from(bodyB64, 'base64url');
|
||||
const sig = Buffer.from(sigB64, 'base64url');
|
||||
const ok = verify(null, body, publicKey, sig);
|
||||
assert.equal(ok, true);
|
||||
});
|
||||
Reference in New Issue
Block a user