Add prettier, formatted files. Use bytes/hex conversions from noble.

This commit is contained in:
Jay
2025-10-24 12:58:28 -04:00
parent b23e56b2e6
commit 268f411633
16 changed files with 299 additions and 94 deletions

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
testdata

245
package-lock.json generated
View File

@@ -12,12 +12,140 @@
"@noble/secp256k1": "^3.0.0"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^24.9.1",
"prettier": "^3.5.3",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vitest": "^4.0.2"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/generator": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@babel/types": "^7.28.5",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.5",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
@@ -460,6 +588,27 @@
"node": ">=18"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -467,6 +616,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
@@ -803,6 +963,41 @@
"dev": true,
"license": "MIT"
},
"node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz",
"integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@babel/generator": "^7.26.5",
"@babel/parser": "^7.26.7",
"@babel/traverse": "^7.26.7",
"@babel/types": "^7.26.7",
"javascript-natural-sort": "^0.7.1",
"lodash": "^4.17.21"
},
"engines": {
"node": ">18.12"
},
"peerDependencies": {
"@vue/compiler-sfc": "3.x",
"prettier": "2.x - 3.x",
"prettier-plugin-svelte": "3.x",
"svelte": "4.x || 5.x"
},
"peerDependenciesMeta": {
"@vue/compiler-sfc": {
"optional": true
},
"prettier-plugin-svelte": {
"optional": true
},
"svelte": {
"optional": true
}
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -1089,6 +1284,40 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
"dev": true,
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1181,6 +1410,22 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/rollup": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",

View File

@@ -15,6 +15,8 @@
],
"scripts": {
"build": "tsc",
"lint": "tsc --noEmit",
"format": "prettier -w src",
"test": "vitest",
"test:run": "vitest run"
},
@@ -23,9 +25,28 @@
"@noble/secp256k1": "^3.0.0"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^24.9.1",
"prettier": "^3.5.3",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vitest": "^4.0.2"
},
"prettier": {
"importOrder": [
"^react$",
"^react-dom(.*)$",
"^react(.*)$",
"^@(?!(/))(.*)$",
"^(?!@|[.])(.*)$",
"^@(/)(.*)$",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": [
"@trivago/prettier-plugin-sort-imports"
]
}
}

View File

@@ -1,11 +0,0 @@
/**
* Matches 64-character lowercase hexadecimal strings.
* Used for validating event IDs and cryptographic keys.
*/
export const HEX_64_PATTERN = /^[a-f0-9]{64}$/;
/**
* Matches 128-character lowercase hexadecimal strings.
* Used for validating signatures.
*/
export const HEX_128_PATTERN = /^[a-f0-9]{128}$/;

View File

@@ -1,43 +1,3 @@
/**
* Public key is not 64 lowercase hex characters.
*/
export class MalformedPubKeyError extends Error {
constructor() {
super("public key must be 64 lowercase hex characters");
this.name = "MalformedPubKeyError";
}
}
/**
* Private key is not 64 lowercase hex characters.
*/
export class MalformedPrivKeyError extends Error {
constructor() {
super("private key must be 64 lowercase hex characters");
this.name = "MalformedPrivKeyError";
}
}
/**
* Event ID is not 64 hex characters.
*/
export class MalformedIDError extends Error {
constructor() {
super("event id must be 64 hex characters");
this.name = "MalformedIDError";
}
}
/**
* Event signature is not 128 hex characters.
*/
export class MalformedSigError extends Error {
constructor() {
super("event signature must be 128 hex characters");
this.name = "MalformedSigError";
}
}
/**
* Event tag contains fewer than two elements.
*/

View File

@@ -1,2 +1,3 @@
import { test } from "vitest";
test("placeholder", () => {});

View File

@@ -1,2 +1,3 @@
import { test } from "vitest";
test("placeholder", () => {});

View File

@@ -1,7 +1,8 @@
import { describe, test, expect } from "vitest";
import { describe, expect, test } from "vitest";
import { EventID } from "./id";
import { testEvent, testPK } from "./util.test";
import type { EventData } from "./types";
import { testEvent, testPK } from "./util.test";
interface IDTestCase {
name: string;

View File

@@ -1,4 +1,6 @@
import { sha256 } from "@noble/hashes/sha2.js";
import { bytesToHex } from "@noble/hashes/utils.js";
import type { EventData } from "./types";
/**
@@ -27,7 +29,7 @@ function serialize(event: EventData): string {
function getID(event: EventData): string {
const serialized = serialize(event);
const hash = sha256(new TextEncoder().encode(serialized));
return Buffer.from(hash).toString("hex");
return bytesToHex(hash);
}
export const EventID = {

View File

@@ -1,5 +1,4 @@
export * from "./types";
export * from "./constants";
export * from "./errors";
export { Event } from "./event";
export { Keys } from "./keys";

View File

@@ -1,7 +1,9 @@
import { describe, test, expect } from "vitest";
import { describe, expect, test } from "vitest";
import { Keys } from "./keys";
import { HEX_64_PATTERN } from "./constants";
import { testSK, testPK } from "./util.test";
import { testPK, testSK } from "./util.test";
const HEX_64_PATTERN = /^[a-f0-9]{64}$/;
describe("Keys.generatePrivate", () => {
test("returns 64 hex characters", () => {
@@ -24,7 +26,7 @@ describe("Keys.getPublic", () => {
test("throws on invalid private key - too short", () => {
expect(() => Keys.getPublicKey("abc123")).toThrow(
"private key must be 64 lowercase hex characters",
/"secret key" expected.*/,
);
});
@@ -33,14 +35,6 @@ describe("Keys.getPublic", () => {
Keys.getPublicKey(
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
),
).toThrow("private key must be 64 lowercase hex characters");
});
test("throws on invalid private key - uppercase", () => {
expect(() =>
Keys.getPublicKey(
"F43A0435F69529F310BBD1D6263D2FBF0977F54BFE2310CC37AE5904B83BB167",
),
).toThrow("private key must be 64 lowercase hex characters");
).toThrow(/hex string expected,.*/);
});
});

View File

@@ -1,6 +1,5 @@
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
import { schnorr } from "@noble/secp256k1";
import { HEX_64_PATTERN } from "./constants";
import { MalformedPrivKeyError } from "./errors";
/**
* Generates a new random secp256k1 private key.
@@ -8,7 +7,7 @@ import { MalformedPrivKeyError } from "./errors";
*/
function generatePrivateKey(): string {
const { secretKey } = schnorr.keygen();
return Buffer.from(secretKey).toString("hex");
return bytesToHex(secretKey);
}
/**
@@ -18,14 +17,10 @@ function generatePrivateKey(): string {
* @throws {MalformedPrivKeyError} If private key is not 64 lowercase hex characters
*/
function getPublicKey(privateKey: string): string {
if (!HEX_64_PATTERN.test(privateKey)) {
throw new MalformedPrivKeyError();
}
const privateKeyBytes = Buffer.from(privateKey, "hex");
const privateKeyBytes = hexToBytes(privateKey);
const publicKeyBytes = schnorr.getPublicKey(privateKeyBytes);
return Buffer.from(publicKeyBytes).toString("hex");
return bytesToHex(publicKeyBytes);
}
export const Keys = {

View File

@@ -1,6 +1,7 @@
import { describe, test, expect } from "vitest";
import { describe, expect, test } from "vitest";
import { Sign } from "./sign";
import { testSK, testEvent } from "./util.test";
import { testEvent, testSK } from "./util.test";
describe("Sign.sign", () => {
test("produces correct signature", () => {
@@ -10,13 +11,13 @@ describe("Sign.sign", () => {
test("throws on invalid event ID", () => {
expect(() => Sign.sign("thisisabadeventid", testSK)).toThrow(
"event id must be 64 hex characters",
/hex string expected,.*/,
);
});
test("throws on invalid private key", () => {
expect(() => Sign.sign(testEvent.id, "thisisabadsecretkey")).toThrow(
"private key must be 64 lowercase hex characters",
/hex string expected,.*/,
);
});
});

View File

@@ -1,8 +1,8 @@
import { hashes as secp_hashes } from "@noble/secp256k1";
import { schnorr } from "@noble/secp256k1";
import { hmac } from "@noble/hashes/hmac.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { MalformedIDError, MalformedPrivKeyError } from "./errors";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
import { hashes as secp_hashes } from "@noble/secp256k1";
import { schnorr } from "@noble/secp256k1";
secp_hashes.hmacSha256 = (key, msg) => hmac(sha256, key, msg);
secp_hashes.sha256 = sha256;
@@ -16,19 +16,12 @@ secp_hashes.sha256 = sha256;
* @throws {MalformedPrivKeyError} If private key is not 64 lowercase hex characters
*/
function sign(eventID: string, privateKey: string): string {
const privateKeyBytes = Buffer.from(privateKey, "hex");
if (privateKeyBytes.length !== 32) {
throw new MalformedPrivKeyError();
}
const idBytes = Buffer.from(eventID, "hex");
if (idBytes.length !== 32) {
throw new MalformedIDError();
}
const privateKeyBytes = hexToBytes(privateKey);
const idBytes = hexToBytes(eventID);
const auxRand = sha256(privateKeyBytes);
const signature = schnorr.sign(idBytes, privateKeyBytes, auxRand);
return Buffer.from(signature).toString("hex");
return bytesToHex(signature);
}
export const Sign = {

View File

@@ -1,4 +1,5 @@
import { test } from "vitest";
import type { EventData } from "./types";
test("placeholder", () => {});

View File

@@ -1,2 +1,3 @@
import { test } from "vitest";
test("placeholder", () => {});