@varnir/signing 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License (MIT)
2
+ Copyright (c) 2026 Adam Walker
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @varnir/signing
2
+
3
+ Key management for Varnir wallets: generate and validate BIP39 recovery
4
+ phrases, derive a secp256k1 key pair from one, and import an already-known
5
+ private key. This package derives and validates key material — it never
6
+ sends a key anywhere. All signing and derivation happen locally, in the
7
+ caller's own process; nothing here talks to a network.
8
+
9
+ ## Custody model
10
+
11
+ Everything in this package operates on key material the caller already
12
+ holds in memory. There is no server component, no remote call, and no
13
+ place a private key or recovery phrase is transmitted. A recovery phrase
14
+ generated here, or a private key derived or imported through it, exists
15
+ only for as long as the calling process keeps it — what you do with it
16
+ afterwards (store it, hand it to a signer, discard it) is entirely up to
17
+ the caller. Treat both recovery phrases and private keys as bearer secrets:
18
+ anyone who has one can spend from the wallet it derives.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @varnir/signing
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import {
30
+ generateRecoveryPhrase,
31
+ isValidRecoveryPhrase,
32
+ derivePrivateKeyFromPhrase,
33
+ keyPairFromPrivateKeyHex,
34
+ } from '@varnir/signing';
35
+
36
+ const phrase = generateRecoveryPhrase(); // 12-word BIP39 phrase
37
+ isValidRecoveryPhrase(phrase); // true
38
+
39
+ const {privateKeyHex, publicKeyHex} = derivePrivateKeyFromPhrase(phrase);
40
+
41
+ // Or start from an already-known private key instead of a phrase:
42
+ const keyPair = keyPairFromPrivateKeyHex('0x...');
43
+ ```
44
+
45
+ - **`generateRecoveryPhrase()`** — a fresh 12-word (128-bit entropy) BIP39
46
+ mnemonic, from the standard English wordlist.
47
+ - **`isValidRecoveryPhrase(phrase)`** / **`normalizeRecoveryPhrase(phrase)`**
48
+ — validate or normalize (trim, lower-case, collapse whitespace) a phrase
49
+ before using it.
50
+ - **`derivePrivateKeyFromPhrase(phrase)`** — BIP32/BIP44 HD derivation
51
+ (`m/44'/1'/0'/0/0`) from the phrase's BIP39 seed, returning a secp256k1
52
+ key pair as `0x`-prefixed hex.
53
+ - **`recoverySeedFromPhrase(phrase)`** — the raw 64-byte BIP39 seed itself,
54
+ for callers (such as `@varnir/chain-client`) that derive other key types
55
+ from the same seed.
56
+ - **`keyPairFromPrivateKeyHex(hex)`** / **`isPrivateKeyHex(value)`** —
57
+ derive the matching public key from an already-known private key, or
58
+ check that a string looks like one, without generating anything new.
59
+
60
+ Built on audited, widely-used primitives: [`@noble/curves`](https://www.npmjs.com/package/@noble/curves)
61
+ for secp256k1, [`@scure/bip32`](https://www.npmjs.com/package/@scure/bip32)
62
+ for HD derivation, and [`bip39`](https://www.npmjs.com/package/bip39) for
63
+ mnemonic generation/validation. Ships as compiled ESM with type
64
+ declarations.
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,24 @@
1
+ export declare const RECOVERY_PHRASE_LENGTH = 12;
2
+ export type KeyPair = {
3
+ privateKeyHex: string;
4
+ publicKeyHex: string;
5
+ };
6
+ export declare function normalizeRecoveryPhrase(phrase: string): string;
7
+ export declare function generateRecoveryPhrase(): string;
8
+ export declare function isValidRecoveryPhrase(phrase: string): boolean;
9
+ export declare function isPrivateKeyHex(value: string): boolean;
10
+ /**
11
+ * The raw 64-byte BIP39 seed for a recovery phrase: `mnemonicToSeedSync` of the
12
+ * normalized phrase, validated first. This is the ONE seed step every key type
13
+ * derives from - the secp256k1 HD key below, and the post-quantum keys derived
14
+ * in `@varnir/chain-client`'s `deriveKeyPairFromPhrase` (which domain-separates
15
+ * by key type on top of this same seed). It is exported so that PQ derivation
16
+ * reuses this exact step rather than duplicating `bip39.mnemonicToSeedSync`.
17
+ *
18
+ * Returned as a plain `Uint8Array` (not a Node `Buffer`) so a consumer without
19
+ * Node builtins - a browser, a Worker - can use it.
20
+ */
21
+ export declare function recoverySeedFromPhrase(phrase: string): Uint8Array;
22
+ export declare function derivePrivateKeyFromPhrase(phrase: string): KeyPair;
23
+ export declare function keyPairFromPrivateKeyHex(value: string): KeyPair;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,MAAM,MAAM,OAAO,GAAG;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAmBF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMjE;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAWlE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAa/D"}
package/dist/index.js ADDED
@@ -0,0 +1,93 @@
1
+ // Secure signing tools — key management and transaction signing used by the wallet apps and SDK.
2
+ //
3
+ // BIP39/HD derivation ported from apps/wallet-mobile/src/wallet/keys.ts,
4
+ // where it's already proven to run inside the Expo/React Native runtime
5
+ // (a strictly harder environment than a browser - no Node builtins either
6
+ // way) - not reimplemented, just given a shared home so scanner-web's
7
+ // mini-wallet doesn't duplicate it a third time. wallet-mobile's own copy
8
+ // is untouched; nothing here is wired up to import from this package yet.
9
+ import { secp256k1 } from '@noble/curves/secp256k1';
10
+ import { HDKey } from '@scure/bip32';
11
+ import * as bip39 from 'bip39';
12
+ /**
13
+ * BIP44: purpose 44' / coin type 1' / account 0' / external chain / first address.
14
+ * Coin type 1' is the registered "all testnets" slot; the ledger engine has no
15
+ * registered coin type of its own and this build talks to no chain, so 1' keeps
16
+ * the keys off any production coin's derivation space.
17
+ */
18
+ const DERIVATION_PATH = "m/44'/1'/0'/0/0";
19
+ /** 128 bits of entropy — a 12 word phrase. */
20
+ const ENTROPY_BITS = 128;
21
+ const WORDLIST = bip39.wordlists.english;
22
+ export const RECOVERY_PHRASE_LENGTH = 12;
23
+ function toPrefixedHex(bytes) {
24
+ let out = '';
25
+ for (const byte of bytes) {
26
+ out += byte.toString(16).padStart(2, '0');
27
+ }
28
+ return `0x${out}`;
29
+ }
30
+ function fromPrefixedHex(value) {
31
+ const body = value.slice(2);
32
+ const bytes = new Uint8Array(body.length / 2);
33
+ for (let i = 0; i < bytes.length; i += 1) {
34
+ bytes[i] = Number.parseInt(body.slice(i * 2, i * 2 + 2), 16);
35
+ }
36
+ return bytes;
37
+ }
38
+ export function normalizeRecoveryPhrase(phrase) {
39
+ return phrase.trim().toLowerCase().split(/\s+/).join(' ');
40
+ }
41
+ export function generateRecoveryPhrase() {
42
+ return bip39.generateMnemonic(ENTROPY_BITS, undefined, WORDLIST);
43
+ }
44
+ export function isValidRecoveryPhrase(phrase) {
45
+ return bip39.validateMnemonic(normalizeRecoveryPhrase(phrase), WORDLIST);
46
+ }
47
+ export function isPrivateKeyHex(value) {
48
+ return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
49
+ }
50
+ /**
51
+ * The raw 64-byte BIP39 seed for a recovery phrase: `mnemonicToSeedSync` of the
52
+ * normalized phrase, validated first. This is the ONE seed step every key type
53
+ * derives from - the secp256k1 HD key below, and the post-quantum keys derived
54
+ * in `@varnir/chain-client`'s `deriveKeyPairFromPhrase` (which domain-separates
55
+ * by key type on top of this same seed). It is exported so that PQ derivation
56
+ * reuses this exact step rather than duplicating `bip39.mnemonicToSeedSync`.
57
+ *
58
+ * Returned as a plain `Uint8Array` (not a Node `Buffer`) so a consumer without
59
+ * Node builtins - a browser, a Worker - can use it.
60
+ */
61
+ export function recoverySeedFromPhrase(phrase) {
62
+ const normalized = normalizeRecoveryPhrase(phrase);
63
+ if (!bip39.validateMnemonic(normalized, WORDLIST)) {
64
+ throw new Error('That recovery phrase is not valid.');
65
+ }
66
+ return Uint8Array.from(bip39.mnemonicToSeedSync(normalized));
67
+ }
68
+ export function derivePrivateKeyFromPhrase(phrase) {
69
+ const seed = recoverySeedFromPhrase(phrase);
70
+ const child = HDKey.fromMasterSeed(seed).derive(DERIVATION_PATH);
71
+ if (!child.privateKey || !child.publicKey) {
72
+ throw new Error('Could not derive a key from that recovery phrase.');
73
+ }
74
+ return {
75
+ privateKeyHex: toPrefixedHex(child.privateKey),
76
+ publicKeyHex: toPrefixedHex(child.publicKey),
77
+ };
78
+ }
79
+ export function keyPairFromPrivateKeyHex(value) {
80
+ const trimmed = value.trim();
81
+ if (!isPrivateKeyHex(trimmed)) {
82
+ throw new Error('Enter a 64-character hex key prefixed with 0x.');
83
+ }
84
+ const privateKeyHex = trimmed.toLowerCase();
85
+ try {
86
+ const publicKey = secp256k1.getPublicKey(fromPrefixedHex(privateKeyHex), true);
87
+ return { privateKeyHex, publicKeyHex: toPrefixedHex(publicKey) };
88
+ }
89
+ catch {
90
+ throw new Error('That key is not a valid secp256k1 private key.');
91
+ }
92
+ }
93
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@varnir/signing",
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "description": "Secure signing tools — key management and transaction signing used by the wallet apps and SDK.",
6
+ "$comment": "Ships COMPILED output (dist) for the same reason @varnir/chain-client does, and as a precondition of it: chain-client's built dist/index.js imports this package by name, so if this one still resolved to raw .ts, chain-client's build would be unloadable outside a TS-aware bundler - exactly the case shipping dist exists to serve. ESM to match chain-client; src/ has no relative imports, so nothing here needed .js extensions.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Varnir/varnir.git",
10
+ "directory": "packages/signing"
11
+ },
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "engines": {
22
+ "node": ">=20.19.4"
23
+ },
24
+ "dependencies": {
25
+ "@noble/curves": "^1.9.0",
26
+ "@scure/bip32": "^1.7.0",
27
+ "bip39": "^3.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.7.2",
31
+ "@varnir/tsconfig": "0.1.0"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit",
37
+ "lint": "echo \"no lint configured yet\"",
38
+ "clean": "rm -rf dist"
39
+ }
40
+ }