@bitspark/archon 0.6.1
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/dist/src/crypto.d.ts +44 -0
- package/dist/src/crypto.js +109 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/hexbytes.d.ts +11 -0
- package/dist/src/hexbytes.js +57 -0
- package/dist/src/hexbytes.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +10 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/keycodec.d.ts +8 -0
- package/dist/src/keycodec.js +126 -0
- package/dist/src/keycodec.js.map +1 -0
- package/dist/src/keytext.d.ts +9 -0
- package/dist/src/keytext.js +59 -0
- package/dist/src/keytext.js.map +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Length of an Ed25519 public key, in bytes. Mirrors rs `PUBLIC_KEY_SIZE` / go `PublicKeySize`. */
|
|
2
|
+
export declare const PUBLIC_KEY_SIZE = 32;
|
|
3
|
+
/** Length of an Ed25519 signature, in bytes. Mirrors rs `SIGNATURE_SIZE` / go `SignatureSize`. */
|
|
4
|
+
export declare const SIGNATURE_SIZE = 64;
|
|
5
|
+
/** Length of an Ed25519 seed (private key), in bytes. Mirrors rs `SEED_SIZE` / go `SeedSize`. */
|
|
6
|
+
export declare const SEED_SIZE = 32;
|
|
7
|
+
/** The longest domain (RFC 8032 context) a signature can be made in, in bytes. */
|
|
8
|
+
export declare const MAX_DOMAIN_SIZE = 255;
|
|
9
|
+
/** Derive the 32-byte Ed25519 public key for a 32-byte seed. */
|
|
10
|
+
export declare function getPublicKey(seed: Uint8Array): Uint8Array;
|
|
11
|
+
/** Produce the 64-byte Ed25519 signature over `message` with the key from `seed`. */
|
|
12
|
+
export declare function sign(message: Uint8Array, seed: Uint8Array): Uint8Array;
|
|
13
|
+
/**
|
|
14
|
+
* Verify a 64-byte Ed25519 `signature` over `message` against public key `pub`.
|
|
15
|
+
*
|
|
16
|
+
* Total: every shape failure (a signature that is not 64 bytes, a public key that is
|
|
17
|
+
* not 32 bytes, or a malformed point) collapses to `false` rather than throwing — so
|
|
18
|
+
* callers can treat all "bad signature" cases uniformly. This mirrors the Rust core's
|
|
19
|
+
* `crypto::verify` (and Go's `crypto.Verify`), which shape-guard before delegating;
|
|
20
|
+
* @noble's `verify` instead throws on a wrong-length input, so we guard the lengths and
|
|
21
|
+
* wrap the call. (Issue #90: found by fast-check fuzzing.)
|
|
22
|
+
*
|
|
23
|
+
* `{ zip215: false }` selects RFC 8032 semantics: @noble defaults to ZIP-215
|
|
24
|
+
* (cofactored) verification, which *accepts* small-order public keys, whereas Go's
|
|
25
|
+
* `crypto/ed25519` and Rust's `ed25519-dalek` v2 *reject* them. Passing `zip215: false`
|
|
26
|
+
* makes the TS core reject small-order keys identically, restoring tri-core parity so
|
|
27
|
+
* the canonical Rust core and the TS core admit exactly the same signed-fact / Head
|
|
28
|
+
* evidence. (Issue #106.)
|
|
29
|
+
*/
|
|
30
|
+
export declare function verify(signature: Uint8Array, message: Uint8Array, pub: Uint8Array): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Sign `message` with the key from `seed` IN `domain` (Ed25519ph with the domain as the
|
|
33
|
+
* RFC 8032 context). Throws — never silently signs raw — when the domain is empty or
|
|
34
|
+
* longer than `MAX_DOMAIN_SIZE` bytes: an empty domain is "sign in no domain", which is
|
|
35
|
+
* exactly what this function exists to make impossible.
|
|
36
|
+
*/
|
|
37
|
+
export declare function signInDomain(seed: Uint8Array, domain: string, message: Uint8Array): Uint8Array;
|
|
38
|
+
/**
|
|
39
|
+
* Verify `signature` over `message` IN `domain` under `pub`. Total, like `verify`: every
|
|
40
|
+
* shape failure — including an empty or over-long domain — is `false`. A raw signature
|
|
41
|
+
* over the same message is `false` here; a signature from any other domain is `false`
|
|
42
|
+
* here.
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyInDomain(pub: Uint8Array, domain: string, message: Uint8Array, signature: Uint8Array): boolean;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Ed25519 sign / verify primitives (RFC 8032), generic — no authority vocabulary.
|
|
2
|
+
//
|
|
3
|
+
// Two ways to sign, deliberately:
|
|
4
|
+
//
|
|
5
|
+
// - `sign` / `verify` — RAW: pure Ed25519 over exactly the bytes given. The caller owns
|
|
6
|
+
// separation; two protocols signing overlapping byte layouts with one key can replay
|
|
7
|
+
// each other's signatures. This is what every consumer's on-disk signatures are today,
|
|
8
|
+
// and it stays.
|
|
9
|
+
// - `signInDomain` / `verifyInDomain` — DOMAIN-SEPARATED: Ed25519ph with a context
|
|
10
|
+
// string (RFC 8032 §5.1), the domain, mixed into the hash. A signature made in one
|
|
11
|
+
// domain verifies in no other and never as a raw signature, and a raw signature
|
|
12
|
+
// verifies in no domain — cryptographically, whatever the bytes. One key, many
|
|
13
|
+
// protocols, no cross-talk: the default for any protocol that has no bytes on disk yet.
|
|
14
|
+
// The domain is the caller's (`<repo>/<purpose>/v<n>` by convention); archon neither
|
|
15
|
+
// knows nor registers domains.
|
|
16
|
+
//
|
|
17
|
+
// Built on @noble/curves (which replaced @noble/ed25519 when domain separation landed:
|
|
18
|
+
// only the former exposes the Ed25519ph-with-context variant). Both are synchronous, so
|
|
19
|
+
// archon's LeafVerifier.verify stays synchronous, matching logos's sync SPI.
|
|
20
|
+
import { ed25519, ed25519ph } from "@noble/curves/ed25519.js";
|
|
21
|
+
/** Length of an Ed25519 public key, in bytes. Mirrors rs `PUBLIC_KEY_SIZE` / go `PublicKeySize`. */
|
|
22
|
+
export const PUBLIC_KEY_SIZE = 32;
|
|
23
|
+
/** Length of an Ed25519 signature, in bytes. Mirrors rs `SIGNATURE_SIZE` / go `SignatureSize`. */
|
|
24
|
+
export const SIGNATURE_SIZE = 64;
|
|
25
|
+
/** Length of an Ed25519 seed (private key), in bytes. Mirrors rs `SEED_SIZE` / go `SeedSize`. */
|
|
26
|
+
export const SEED_SIZE = 32;
|
|
27
|
+
/** The longest domain (RFC 8032 context) a signature can be made in, in bytes. */
|
|
28
|
+
export const MAX_DOMAIN_SIZE = 255;
|
|
29
|
+
/** Derive the 32-byte Ed25519 public key for a 32-byte seed. */
|
|
30
|
+
export function getPublicKey(seed) {
|
|
31
|
+
return ed25519.getPublicKey(seed);
|
|
32
|
+
}
|
|
33
|
+
/** Produce the 64-byte Ed25519 signature over `message` with the key from `seed`. */
|
|
34
|
+
export function sign(message, seed) {
|
|
35
|
+
return ed25519.sign(message, seed);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Verify a 64-byte Ed25519 `signature` over `message` against public key `pub`.
|
|
39
|
+
*
|
|
40
|
+
* Total: every shape failure (a signature that is not 64 bytes, a public key that is
|
|
41
|
+
* not 32 bytes, or a malformed point) collapses to `false` rather than throwing — so
|
|
42
|
+
* callers can treat all "bad signature" cases uniformly. This mirrors the Rust core's
|
|
43
|
+
* `crypto::verify` (and Go's `crypto.Verify`), which shape-guard before delegating;
|
|
44
|
+
* @noble's `verify` instead throws on a wrong-length input, so we guard the lengths and
|
|
45
|
+
* wrap the call. (Issue #90: found by fast-check fuzzing.)
|
|
46
|
+
*
|
|
47
|
+
* `{ zip215: false }` selects RFC 8032 semantics: @noble defaults to ZIP-215
|
|
48
|
+
* (cofactored) verification, which *accepts* small-order public keys, whereas Go's
|
|
49
|
+
* `crypto/ed25519` and Rust's `ed25519-dalek` v2 *reject* them. Passing `zip215: false`
|
|
50
|
+
* makes the TS core reject small-order keys identically, restoring tri-core parity so
|
|
51
|
+
* the canonical Rust core and the TS core admit exactly the same signed-fact / Head
|
|
52
|
+
* evidence. (Issue #106.)
|
|
53
|
+
*/
|
|
54
|
+
export function verify(signature, message, pub) {
|
|
55
|
+
if (signature.length !== SIGNATURE_SIZE || pub.length !== PUBLIC_KEY_SIZE)
|
|
56
|
+
return false;
|
|
57
|
+
try {
|
|
58
|
+
return ed25519.verify(signature, message, pub, { zip215: false });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sign `message` with the key from `seed` IN `domain` (Ed25519ph with the domain as the
|
|
66
|
+
* RFC 8032 context). Throws — never silently signs raw — when the domain is empty or
|
|
67
|
+
* longer than `MAX_DOMAIN_SIZE` bytes: an empty domain is "sign in no domain", which is
|
|
68
|
+
* exactly what this function exists to make impossible.
|
|
69
|
+
*/
|
|
70
|
+
export function signInDomain(seed, domain, message) {
|
|
71
|
+
return ed25519ph.sign(message, seed, { context: domainBytes(domain) });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Verify `signature` over `message` IN `domain` under `pub`. Total, like `verify`: every
|
|
75
|
+
* shape failure — including an empty or over-long domain — is `false`. A raw signature
|
|
76
|
+
* over the same message is `false` here; a signature from any other domain is `false`
|
|
77
|
+
* here.
|
|
78
|
+
*/
|
|
79
|
+
export function verifyInDomain(pub, domain, message, signature) {
|
|
80
|
+
if (signature.length !== SIGNATURE_SIZE || pub.length !== PUBLIC_KEY_SIZE)
|
|
81
|
+
return false;
|
|
82
|
+
let context;
|
|
83
|
+
try {
|
|
84
|
+
context = domainBytes(domain);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return ed25519ph.verify(signature, message, pub, { context, zip215: false });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A domain is 1..=255 bytes of UTF-8 — the RFC 8032 context bound, with the empty
|
|
98
|
+
* context excluded on purpose (see `signInDomain`).
|
|
99
|
+
*/
|
|
100
|
+
function domainBytes(domain) {
|
|
101
|
+
const bytes = new TextEncoder().encode(domain);
|
|
102
|
+
if (bytes.length === 0)
|
|
103
|
+
throw new Error("domain is empty");
|
|
104
|
+
if (bytes.length > MAX_DOMAIN_SIZE) {
|
|
105
|
+
throw new Error(`domain is ${bytes.length} bytes, max ${MAX_DOMAIN_SIZE}`);
|
|
106
|
+
}
|
|
107
|
+
return bytes;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,EAAE;AACF,kCAAkC;AAClC,EAAE;AACF,wFAAwF;AACxF,uFAAuF;AACvF,yFAAyF;AACzF,kBAAkB;AAClB,mFAAmF;AACnF,qFAAqF;AACrF,kFAAkF;AAClF,iFAAiF;AACjF,0FAA0F;AAC1F,uFAAuF;AACvF,iCAAiC;AACjC,EAAE;AACF,uFAAuF;AACvF,wFAAwF;AACxF,6EAA6E;AAC7E,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE9D,oGAAoG;AACpG,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAClC,kGAAkG;AAClG,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AACjC,iGAAiG;AACjG,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAC5B,kFAAkF;AAClF,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC,gEAAgE;AAChE,MAAM,UAAU,YAAY,CAAC,IAAgB;IAC3C,OAAO,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,IAAI,CAAC,OAAmB,EAAE,IAAgB;IACxD,OAAO,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,MAAM,CAAC,SAAqB,EAAE,OAAmB,EAAE,GAAe;IAChF,IAAI,SAAS,CAAC,MAAM,KAAK,cAAc,IAAI,GAAG,CAAC,MAAM,KAAK,eAAe;QAAE,OAAO,KAAK,CAAC;IACxF,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,IAAgB,EAAE,MAAc,EAAE,OAAmB;IAChF,OAAO,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACzE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAe,EACf,MAAc,EACd,OAAmB,EACnB,SAAqB;IAErB,IAAI,SAAS,CAAC,MAAM,KAAK,cAAc,IAAI,GAAG,CAAC,MAAM,KAAK,eAAe;QAAE,OAAO,KAAK,CAAC;IACxF,IAAI,OAAmB,CAAC;IACxB,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,OAAO,SAAS,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,MAAc;IACjC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,aAAa,KAAK,CAAC,MAAM,eAAe,eAAe,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Render bytes as lowercase hex, two digits per byte. */
|
|
2
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
3
|
+
/**
|
|
4
|
+
* Decode the hex spelling of a 32-byte Ed25519 seed. Throws on odd length, any non-hex
|
|
5
|
+
* character, or a decoded length other than 32.
|
|
6
|
+
*/
|
|
7
|
+
export declare function seedFromHex(text: string): Uint8Array;
|
|
8
|
+
/** Decode the hex spelling of a 32-byte Ed25519 public key. Same failure rules as `seedFromHex`. */
|
|
9
|
+
export declare function pubkeyFromHex(text: string): Uint8Array;
|
|
10
|
+
/** Decode the hex spelling of a 64-byte Ed25519 signature. Same failure rules as `seedFromHex`. */
|
|
11
|
+
export declare function signatureFromHex(text: string): Uint8Array;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Typed, fail-closed hex spellings for the three fixed-size values the floor deals in:
|
|
2
|
+
// a 32-byte seed, a 32-byte public key, a 64-byte signature.
|
|
3
|
+
//
|
|
4
|
+
// Every consumer hand-wrote these — `hexToBytes`, `bytesToHex`, `seed32` — around its
|
|
5
|
+
// archon calls, ~50 definitions in the ts lane alone, each one a place for a length
|
|
6
|
+
// bug. The value here is not hex (every language has hex) but the FIXED SIZE: a decoder
|
|
7
|
+
// that returns exactly 32 or 64 bytes or throws, never an array the caller must
|
|
8
|
+
// re-check. Encoding is always lowercase; decoding accepts either case. No `0x` prefix,
|
|
9
|
+
// no whitespace — a spelling is a spelling.
|
|
10
|
+
import { PUBLIC_KEY_SIZE, SEED_SIZE, SIGNATURE_SIZE } from "./crypto.js";
|
|
11
|
+
/** Render bytes as lowercase hex, two digits per byte. */
|
|
12
|
+
export function toHex(bytes) {
|
|
13
|
+
let s = "";
|
|
14
|
+
for (const b of bytes)
|
|
15
|
+
s += b.toString(16).padStart(2, "0");
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Decode the hex spelling of a 32-byte Ed25519 seed. Throws on odd length, any non-hex
|
|
20
|
+
* character, or a decoded length other than 32.
|
|
21
|
+
*/
|
|
22
|
+
export function seedFromHex(text) {
|
|
23
|
+
return fixed(text, SEED_SIZE, "seed");
|
|
24
|
+
}
|
|
25
|
+
/** Decode the hex spelling of a 32-byte Ed25519 public key. Same failure rules as `seedFromHex`. */
|
|
26
|
+
export function pubkeyFromHex(text) {
|
|
27
|
+
return fixed(text, PUBLIC_KEY_SIZE, "public key");
|
|
28
|
+
}
|
|
29
|
+
/** Decode the hex spelling of a 64-byte Ed25519 signature. Same failure rules as `seedFromHex`. */
|
|
30
|
+
export function signatureFromHex(text) {
|
|
31
|
+
return fixed(text, SIGNATURE_SIZE, "signature");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Decode hex into exactly `n` bytes or throw. The length is checked on the text before
|
|
35
|
+
* any byte is decoded, so a wrong-sized input never allocates.
|
|
36
|
+
*/
|
|
37
|
+
function fixed(text, n, what) {
|
|
38
|
+
if (text.length !== n * 2) {
|
|
39
|
+
throw new Error(`${what} hex is ${text.length} characters, expected ${n * 2}`);
|
|
40
|
+
}
|
|
41
|
+
const out = new Uint8Array(n);
|
|
42
|
+
for (let i = 0; i < n; i++) {
|
|
43
|
+
out[i] = (hexDigit(text.charCodeAt(2 * i)) << 4) | hexDigit(text.charCodeAt(2 * i + 1));
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Map one ASCII hex-digit char code (either case) to its nibble value. */
|
|
48
|
+
function hexDigit(code) {
|
|
49
|
+
if (code >= 0x30 && code <= 0x39)
|
|
50
|
+
return code - 0x30; // 0-9
|
|
51
|
+
if (code >= 0x61 && code <= 0x66)
|
|
52
|
+
return code - 0x61 + 10; // a-f
|
|
53
|
+
if (code >= 0x41 && code <= 0x46)
|
|
54
|
+
return code - 0x41 + 10; // A-F
|
|
55
|
+
throw new Error(`non-hex character ${JSON.stringify(String.fromCharCode(code))}`);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=hexbytes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hexbytes.js","sourceRoot":"","sources":["../../src/hexbytes.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,6DAA6D;AAC7D,EAAE;AACF,sFAAsF;AACtF,oFAAoF;AACpF,wFAAwF;AACxF,gFAAgF;AAChF,wFAAwF;AACxF,4CAA4C;AAC5C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEzE,0DAA0D;AAC1D,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,IAAI,CAAC,GAAG,EAAE,CAAC;IACX,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC5D,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;AACxC,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,KAAK,CAAC,IAAI,EAAE,eAAe,EAAE,YAAY,CAAC,CAAC;AACpD,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,KAAK,CAAC,IAAI,EAAE,cAAc,EAAE,WAAW,CAAC,CAAC;AAClD,CAAC;AAED;;;GAGG;AACH,SAAS,KAAK,CAAC,IAAY,EAAE,CAAS,EAAE,IAAY;IAClD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,WAAW,IAAI,CAAC,MAAM,yBAAyB,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,2EAA2E;AAC3E,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,GAAG,IAAI,CAAC,CAAC,MAAM;IAC5D,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,MAAM;IACjE,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,MAAM;IACjE,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;AACpF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// archon — the constellation's identity and crypto layer.
|
|
2
|
+
//
|
|
3
|
+
// archon is who bears the office; thesmos is the law that binds it. This package
|
|
4
|
+
// answers "is this really them?" — keys, their canonical spellings, and signatures
|
|
5
|
+
// over canonical bytes. It has no opinion about what any identity may *do*.
|
|
6
|
+
export * from "./crypto.js";
|
|
7
|
+
export * from "./hexbytes.js";
|
|
8
|
+
export * from "./keytext.js";
|
|
9
|
+
export * from "./keycodec.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,EAAE;AACF,iFAAiF;AACjF,mFAAmF;AACnF,4EAA4E;AAC5E,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Encode a 32-byte Ed25519 public key as SPKI PEM (`-----BEGIN PUBLIC KEY-----`). */
|
|
2
|
+
export declare function pubkeyToSpkiPem(pubkey: Uint8Array): string;
|
|
3
|
+
/** Encode a 32-byte Ed25519 seed as PKCS#8 v1 PEM (`-----BEGIN PRIVATE KEY-----`). */
|
|
4
|
+
export declare function seedToPkcs8Pem(seed: Uint8Array): string;
|
|
5
|
+
/** Decode SPKI PEM back to the raw 32-byte Ed25519 public key. Throws on malformed input. */
|
|
6
|
+
export declare function spkiPemToPubkey(pem: string): Uint8Array;
|
|
7
|
+
/** Decode PKCS#8 v1 PEM back to the raw 32-byte Ed25519 seed. Throws on malformed input. */
|
|
8
|
+
export declare function pkcs8PemToSeed(pem: string): Uint8Array;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// PKCS#8/SPKI PEM key codec (ed25519-key-codec-v1, spec Appendix A.8). Pure, IO-free
|
|
2
|
+
// conversions between raw 32-byte Ed25519 keys/seeds and the two standard PEM containers —
|
|
3
|
+
// PKCS#8 v1 (private) and SPKI (public). A byte codec, NOT key custody (archon ADR 0002): no key is
|
|
4
|
+
// held, no file is read, no randomness is drawn. The DER is fixed-size, so encode is a
|
|
5
|
+
// constant prefix followed by the 32 key bytes and decode is a bounded template match. Decode
|
|
6
|
+
// is PEM-only and total — it throws on any shape it does not recognize, never returns garbage.
|
|
7
|
+
// PKCS#8 is v1-only; v2 is rejected. Byte-pinned across the three cores by the `keycodec`
|
|
8
|
+
// conformance family.
|
|
9
|
+
//
|
|
10
|
+
// base64 is hand-rolled (not Node's lenient `Buffer.from(_, "base64")`, which silently strips
|
|
11
|
+
// invalid characters) so the strict accept/reject set matches the Go/Rust cores exactly. The
|
|
12
|
+
// codec is synchronous, like the rest of the core (WebCrypto's PKCS#8 import is async).
|
|
13
|
+
const KEY_LEN = 32;
|
|
14
|
+
// SubjectPublicKeyInfo header for an Ed25519 public key (the first 12 bytes of the 44-byte
|
|
15
|
+
// DER): SEQUENCE { SEQUENCE { OID 1.3.101.112 }, BIT STRING(0 unused) { pubkey } }. The
|
|
16
|
+
// trailing 0x00 is the unused-bits octet.
|
|
17
|
+
const SPKI_PREFIX = Uint8Array.of(0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00);
|
|
18
|
+
// PKCS#8 v1 PrivateKeyInfo header for an Ed25519 seed (the first 16 bytes of the 48-byte DER):
|
|
19
|
+
// SEQUENCE { INTEGER 0, SEQUENCE { OID 1.3.101.112 }, OCTET STRING { OCTET STRING { seed } } }.
|
|
20
|
+
// Version 0 = v1 (no embedded public key).
|
|
21
|
+
const PKCS8_PREFIX = Uint8Array.of(0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20);
|
|
22
|
+
const PEM_PUBLIC = "PUBLIC KEY";
|
|
23
|
+
const PEM_PRIVATE = "PRIVATE KEY";
|
|
24
|
+
/** Encode a 32-byte Ed25519 public key as SPKI PEM (`-----BEGIN PUBLIC KEY-----`). */
|
|
25
|
+
export function pubkeyToSpkiPem(pubkey) {
|
|
26
|
+
return encode(pubkey, SPKI_PREFIX, PEM_PUBLIC);
|
|
27
|
+
}
|
|
28
|
+
/** Encode a 32-byte Ed25519 seed as PKCS#8 v1 PEM (`-----BEGIN PRIVATE KEY-----`). */
|
|
29
|
+
export function seedToPkcs8Pem(seed) {
|
|
30
|
+
return encode(seed, PKCS8_PREFIX, PEM_PRIVATE);
|
|
31
|
+
}
|
|
32
|
+
/** Decode SPKI PEM back to the raw 32-byte Ed25519 public key. Throws on malformed input. */
|
|
33
|
+
export function spkiPemToPubkey(pem) {
|
|
34
|
+
return decode(pem, SPKI_PREFIX, PEM_PUBLIC);
|
|
35
|
+
}
|
|
36
|
+
/** Decode PKCS#8 v1 PEM back to the raw 32-byte Ed25519 seed. Throws on malformed input. */
|
|
37
|
+
export function pkcs8PemToSeed(pem) {
|
|
38
|
+
return decode(pem, PKCS8_PREFIX, PEM_PRIVATE);
|
|
39
|
+
}
|
|
40
|
+
function encode(key, prefix, pemType) {
|
|
41
|
+
if (key.length !== KEY_LEN)
|
|
42
|
+
throw new Error(`keycodec: key must be ${KEY_LEN} bytes, got ${key.length}`);
|
|
43
|
+
const der = new Uint8Array(prefix.length + KEY_LEN);
|
|
44
|
+
der.set(prefix, 0);
|
|
45
|
+
der.set(key, prefix.length);
|
|
46
|
+
return `-----BEGIN ${pemType}-----\n${base64Encode(der)}\n-----END ${pemType}-----\n`;
|
|
47
|
+
}
|
|
48
|
+
function decode(pem, prefix, pemType) {
|
|
49
|
+
const trimmed = pem.replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
50
|
+
const begin = `-----BEGIN ${pemType}-----`;
|
|
51
|
+
const end = `-----END ${pemType}-----`;
|
|
52
|
+
const lines = trimmed.split("\n");
|
|
53
|
+
if (lines.length < 3 || lines[0] !== begin || lines[lines.length - 1] !== end) {
|
|
54
|
+
throw new Error(`keycodec: not a "${pemType}" PEM block`);
|
|
55
|
+
}
|
|
56
|
+
const der = base64Decode(lines.slice(1, -1).join(""));
|
|
57
|
+
if (der.length !== prefix.length + KEY_LEN || !startsWith(der, prefix)) {
|
|
58
|
+
throw new Error(`keycodec: DER does not match the ${pemType} ed25519-key-codec-v1 template`);
|
|
59
|
+
}
|
|
60
|
+
return der.slice(prefix.length);
|
|
61
|
+
}
|
|
62
|
+
function startsWith(a, prefix) {
|
|
63
|
+
if (a.length < prefix.length)
|
|
64
|
+
return false;
|
|
65
|
+
for (let i = 0; i < prefix.length; i += 1)
|
|
66
|
+
if (a[i] !== prefix[i])
|
|
67
|
+
return false;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
// ----- base64 (RFC 4648 standard alphabet, with padding) — matches Go/Rust strictly -----
|
|
71
|
+
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
72
|
+
function base64Encode(data) {
|
|
73
|
+
let out = "";
|
|
74
|
+
for (let i = 0; i < data.length; i += 3) {
|
|
75
|
+
const b0 = data[i] ?? 0;
|
|
76
|
+
const b1 = data[i + 1] ?? 0; // 0 past the end; the `has*` guards below control padding
|
|
77
|
+
const b2 = data[i + 2] ?? 0;
|
|
78
|
+
out += B64[b0 >> 2];
|
|
79
|
+
out += B64[((b0 & 0x03) << 4) | (b1 >> 4)];
|
|
80
|
+
out += i + 1 < data.length ? B64[((b1 & 0x0f) << 2) | (b2 >> 6)] : "=";
|
|
81
|
+
out += i + 2 < data.length ? B64[b2 & 0x3f] : "=";
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function b64Val(c) {
|
|
86
|
+
if (c >= 65 && c <= 90)
|
|
87
|
+
return c - 65; // A-Z
|
|
88
|
+
if (c >= 97 && c <= 122)
|
|
89
|
+
return c - 97 + 26; // a-z
|
|
90
|
+
if (c >= 48 && c <= 57)
|
|
91
|
+
return c - 48 + 52; // 0-9
|
|
92
|
+
if (c === 43)
|
|
93
|
+
return 62; // +
|
|
94
|
+
if (c === 47)
|
|
95
|
+
return 63; // /
|
|
96
|
+
throw new Error(`keycodec: invalid base64 character ${JSON.stringify(String.fromCharCode(c))}`);
|
|
97
|
+
}
|
|
98
|
+
function base64Decode(s) {
|
|
99
|
+
if (s.length % 4 !== 0)
|
|
100
|
+
throw new Error("keycodec: base64 length is not a multiple of 4");
|
|
101
|
+
const out = [];
|
|
102
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
103
|
+
const c2 = s.charCodeAt(i + 2);
|
|
104
|
+
const c3 = s.charCodeAt(i + 3);
|
|
105
|
+
const v0 = b64Val(s.charCodeAt(i));
|
|
106
|
+
const v1 = b64Val(s.charCodeAt(i + 1));
|
|
107
|
+
out.push((v0 << 2) | (v1 >> 4));
|
|
108
|
+
if (c2 === 61 /* = */) {
|
|
109
|
+
if (c3 !== 61 || i + 4 !== s.length)
|
|
110
|
+
throw new Error("keycodec: malformed base64 padding");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const v2 = b64Val(c2);
|
|
114
|
+
out.push(((v1 & 0x0f) << 4) | (v2 >> 2));
|
|
115
|
+
if (c3 === 61 /* = */) {
|
|
116
|
+
if (i + 4 !== s.length)
|
|
117
|
+
throw new Error("keycodec: malformed base64 padding");
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
out.push(((v2 & 0x03) << 6) | b64Val(c3));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return Uint8Array.from(out);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=keycodec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keycodec.js","sourceRoot":"","sources":["../../src/keycodec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Encode public-key bytes as the canonical key text `ed25519:<lowercase-hex>`. */
|
|
2
|
+
export declare function encodeKey(pubkey: Uint8Array): string;
|
|
3
|
+
/**
|
|
4
|
+
* Decode canonical key text back to its raw public-key bytes. Strips the `ed25519:`
|
|
5
|
+
* prefix and hex-decodes the body. Throws (never returns a partial result) when the
|
|
6
|
+
* prefix is missing, the body is not even-length hex, or the decoded length is not 32
|
|
7
|
+
* bytes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function decodeKey(text: string): Uint8Array;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Canonical key text — the v1 wire form for a public key.
|
|
2
|
+
//
|
|
3
|
+
// A public key is rendered as `"ed25519:" + lowercaseHex(pubkeyBytes)` and parsed by
|
|
4
|
+
// stripping that prefix and hex-decoding the body. This is the one human/CLI-facing
|
|
5
|
+
// spelling of a key; the three cores agree on it byte-for-byte so a key printed by one
|
|
6
|
+
// core round-trips through any other.
|
|
7
|
+
/** The v1 key-text scheme prefix. Exactly one scheme exists today (Ed25519). */
|
|
8
|
+
const ED25519_PREFIX = "ed25519:";
|
|
9
|
+
/** The fixed length, in bytes, of an Ed25519 public key. */
|
|
10
|
+
const PUBLIC_KEY_LEN = 32;
|
|
11
|
+
/** Encode public-key bytes as the canonical key text `ed25519:<lowercase-hex>`. */
|
|
12
|
+
export function encodeKey(pubkey) {
|
|
13
|
+
let hex = "";
|
|
14
|
+
for (const b of pubkey) {
|
|
15
|
+
hex += b.toString(16).padStart(2, "0");
|
|
16
|
+
}
|
|
17
|
+
return ED25519_PREFIX + hex;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Decode canonical key text back to its raw public-key bytes. Strips the `ed25519:`
|
|
21
|
+
* prefix and hex-decodes the body. Throws (never returns a partial result) when the
|
|
22
|
+
* prefix is missing, the body is not even-length hex, or the decoded length is not 32
|
|
23
|
+
* bytes.
|
|
24
|
+
*/
|
|
25
|
+
export function decodeKey(text) {
|
|
26
|
+
if (!text.startsWith(ED25519_PREFIX)) {
|
|
27
|
+
throw new Error(`missing '${ED25519_PREFIX}' prefix`);
|
|
28
|
+
}
|
|
29
|
+
const body = text.slice(ED25519_PREFIX.length);
|
|
30
|
+
if (body.length % 2 !== 0) {
|
|
31
|
+
throw new Error("key body has an odd number of hex digits");
|
|
32
|
+
}
|
|
33
|
+
const out = new Uint8Array(body.length / 2);
|
|
34
|
+
for (let i = 0; i < out.length; i++) {
|
|
35
|
+
const byte = parseHexByte(body, i * 2);
|
|
36
|
+
out[i] = byte;
|
|
37
|
+
}
|
|
38
|
+
if (out.length !== PUBLIC_KEY_LEN) {
|
|
39
|
+
throw new Error(`decoded key is ${out.length} bytes, expected ${PUBLIC_KEY_LEN}`);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Parse the two hex digits at `pos` into a byte, throwing on any non-hex character. */
|
|
44
|
+
function parseHexByte(body, pos) {
|
|
45
|
+
const hi = hexDigit(body.charCodeAt(pos));
|
|
46
|
+
const lo = hexDigit(body.charCodeAt(pos + 1));
|
|
47
|
+
return (hi << 4) | lo;
|
|
48
|
+
}
|
|
49
|
+
/** Map one ASCII hex-digit char code (either case) to its nibble value. */
|
|
50
|
+
function hexDigit(code) {
|
|
51
|
+
if (code >= 0x30 && code <= 0x39)
|
|
52
|
+
return code - 0x30; // 0-9
|
|
53
|
+
if (code >= 0x61 && code <= 0x66)
|
|
54
|
+
return code - 0x61 + 10; // a-f
|
|
55
|
+
if (code >= 0x41 && code <= 0x46)
|
|
56
|
+
return code - 0x41 + 10; // A-F
|
|
57
|
+
throw new Error(`non-hex character ${JSON.stringify(String.fromCharCode(code))} in key body`);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=keytext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keytext.js","sourceRoot":"","sources":["../../src/keytext.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,EAAE;AACF,qFAAqF;AACrF,oFAAoF;AACpF,uFAAuF;AACvF,sCAAsC;AAEtC,gFAAgF;AAChF,MAAM,cAAc,GAAG,UAAU,CAAC;AAElC,4DAA4D;AAC5D,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B,mFAAmF;AACnF,MAAM,UAAU,SAAS,CAAC,MAAkB;IAC1C,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,cAAc,GAAG,GAAG,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,YAAY,cAAc,UAAU,CAAC,CAAC;IACxD,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAC/C,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACvC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChB,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,cAAc,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,kBAAkB,GAAG,CAAC,MAAM,oBAAoB,cAAc,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wFAAwF;AACxF,SAAS,YAAY,CAAC,IAAY,EAAE,GAAW;IAC7C,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1C,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9C,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;AACxB,CAAC;AAED,2EAA2E;AAC3E,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,GAAG,IAAI,CAAC,CAAC,MAAM;IAC5D,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,MAAM;IACjE,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,MAAM;IACjE,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC;AAChG,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bitspark/archon",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "archon: Ed25519 primitives, the canonical key spelling, and the SPKI/PKCS-8 key codec. The identity floor beneath the law.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Bitspark/archon"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "dist/src/index.js",
|
|
12
|
+
"types": "dist/src/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/src"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"registry": "https://registry.npmjs.org"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "tsc -p tsconfig.json && node --test dist/test/*.test.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@noble/curves": "^2.4.0",
|
|
25
|
+
"@noble/hashes": "^2.2.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22",
|
|
29
|
+
"typescript": "^5.7"
|
|
30
|
+
}
|
|
31
|
+
}
|