evm-kms-signer 1.0.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 +21 -0
- package/README.md +305 -0
- package/dist/account.d.ts +25 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +36 -0
- package/dist/account.js.map +1 -0
- package/dist/errors/index.d.ts +17 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +32 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/kms/client.d.ts +60 -0
- package/dist/kms/client.d.ts.map +1 -0
- package/dist/kms/client.js +98 -0
- package/dist/kms/client.js.map +1 -0
- package/dist/kms/signer.d.ts +192 -0
- package/dist/kms/signer.d.ts.map +1 -0
- package/dist/kms/signer.js +271 -0
- package/dist/kms/signer.js.map +1 -0
- package/dist/types/index.d.ts +56 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/address.d.ts +29 -0
- package/dist/utils/address.d.ts.map +1 -0
- package/dist/utils/address.js +60 -0
- package/dist/utils/address.js.map +1 -0
- package/dist/utils/der.d.ts +15 -0
- package/dist/utils/der.d.ts.map +1 -0
- package/dist/utils/der.js +100 -0
- package/dist/utils/der.js.map +1 -0
- package/dist/utils/signature.d.ts +95 -0
- package/dist/utils/signature.d.ts.map +1 -0
- package/dist/utils/signature.js +153 -0
- package/dist/utils/signature.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DerParsingError } from '../errors';
|
|
2
|
+
/**
|
|
3
|
+
* Parse DER-encoded ECDSA signature into r and s components
|
|
4
|
+
*
|
|
5
|
+
* DER structure:
|
|
6
|
+
* SEQUENCE (0x30) [total_length]
|
|
7
|
+
* INTEGER (0x02) [r_length] [r_bytes]
|
|
8
|
+
* INTEGER (0x02) [s_length] [s_bytes]
|
|
9
|
+
*
|
|
10
|
+
* @param der - DER-encoded signature from AWS KMS
|
|
11
|
+
* @returns Object with r and s as 32-byte Uint8Arrays
|
|
12
|
+
* @throws DerParsingError if signature format is invalid
|
|
13
|
+
*/
|
|
14
|
+
export function parseDerSignature(der) {
|
|
15
|
+
// Validate minimum length
|
|
16
|
+
if (der.length === 0) {
|
|
17
|
+
throw new DerParsingError('Invalid DER signature: empty buffer');
|
|
18
|
+
}
|
|
19
|
+
if (der.length < 8) {
|
|
20
|
+
throw new DerParsingError('Invalid DER signature: buffer too short');
|
|
21
|
+
}
|
|
22
|
+
// Validate SEQUENCE tag
|
|
23
|
+
if (der[0] !== 0x30) {
|
|
24
|
+
throw new DerParsingError('Invalid DER signature: expected SEQUENCE tag (0x30)');
|
|
25
|
+
}
|
|
26
|
+
const sequenceLength = der[1];
|
|
27
|
+
if (der.length < sequenceLength + 2) {
|
|
28
|
+
throw new DerParsingError('Invalid DER signature: SEQUENCE length exceeds buffer size');
|
|
29
|
+
}
|
|
30
|
+
let offset = 2; // Skip SEQUENCE tag and length
|
|
31
|
+
// Parse r INTEGER
|
|
32
|
+
if (offset >= der.length) {
|
|
33
|
+
throw new DerParsingError('Invalid DER signature: missing r INTEGER tag');
|
|
34
|
+
}
|
|
35
|
+
if (der[offset] !== 0x02) {
|
|
36
|
+
throw new DerParsingError('Invalid DER signature: expected INTEGER tag (0x02) for r');
|
|
37
|
+
}
|
|
38
|
+
offset++;
|
|
39
|
+
if (offset >= der.length) {
|
|
40
|
+
throw new DerParsingError('Invalid DER signature: missing r length');
|
|
41
|
+
}
|
|
42
|
+
let rLength = der[offset];
|
|
43
|
+
offset++;
|
|
44
|
+
// Validate r length
|
|
45
|
+
if (rLength === 0) {
|
|
46
|
+
throw new DerParsingError('Invalid DER signature: r length cannot be 0');
|
|
47
|
+
}
|
|
48
|
+
if (rLength > 33) {
|
|
49
|
+
throw new DerParsingError('Invalid DER signature: r length exceeds maximum (33 bytes)');
|
|
50
|
+
}
|
|
51
|
+
if (offset + rLength > der.length) {
|
|
52
|
+
throw new DerParsingError('Invalid DER signature: r value exceeds buffer length');
|
|
53
|
+
}
|
|
54
|
+
let r = der.slice(offset, offset + rLength);
|
|
55
|
+
// Remove leading 0x00 (negative number prevention padding)
|
|
56
|
+
if (rLength === 33 && r[0] === 0x00) {
|
|
57
|
+
r = r.slice(1);
|
|
58
|
+
}
|
|
59
|
+
// Left-pad with zeros to 32 bytes
|
|
60
|
+
if (r.length < 32) {
|
|
61
|
+
const padded = new Uint8Array(32);
|
|
62
|
+
padded.set(r, 32 - r.length);
|
|
63
|
+
r = padded;
|
|
64
|
+
}
|
|
65
|
+
offset += rLength;
|
|
66
|
+
// Parse s INTEGER
|
|
67
|
+
if (offset >= der.length) {
|
|
68
|
+
throw new DerParsingError('Invalid DER signature: missing s INTEGER tag');
|
|
69
|
+
}
|
|
70
|
+
if (der[offset] !== 0x02) {
|
|
71
|
+
throw new DerParsingError('Invalid DER signature: expected INTEGER tag (0x02) for s');
|
|
72
|
+
}
|
|
73
|
+
offset++;
|
|
74
|
+
if (offset >= der.length) {
|
|
75
|
+
throw new DerParsingError('Invalid DER signature: missing s length');
|
|
76
|
+
}
|
|
77
|
+
let sLength = der[offset];
|
|
78
|
+
offset++;
|
|
79
|
+
// Validate s length
|
|
80
|
+
if (sLength === 0) {
|
|
81
|
+
throw new DerParsingError('Invalid DER signature: s length cannot be 0');
|
|
82
|
+
}
|
|
83
|
+
if (sLength > 33) {
|
|
84
|
+
throw new DerParsingError('Invalid DER signature: s length exceeds maximum (33 bytes)');
|
|
85
|
+
}
|
|
86
|
+
if (offset + sLength > der.length) {
|
|
87
|
+
throw new DerParsingError('Invalid DER signature: s value exceeds buffer length');
|
|
88
|
+
}
|
|
89
|
+
let s = der.slice(offset, offset + sLength);
|
|
90
|
+
if (sLength === 33 && s[0] === 0x00) {
|
|
91
|
+
s = s.slice(1);
|
|
92
|
+
}
|
|
93
|
+
if (s.length < 32) {
|
|
94
|
+
const padded = new Uint8Array(32);
|
|
95
|
+
padded.set(s, 32 - s.length);
|
|
96
|
+
s = padded;
|
|
97
|
+
}
|
|
98
|
+
return { r, s };
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=der.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"der.js","sourceRoot":"","sources":["../../src/utils/der.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Hex, Address } from 'viem';
|
|
2
|
+
/**
|
|
3
|
+
* secp256k1 curve order (n)
|
|
4
|
+
* Maximum value for ECDSA signature components r and s
|
|
5
|
+
*/
|
|
6
|
+
export declare const SECP256K1_N = 115792089237316195423570985008687907852837564279074904382605163141518161494337n;
|
|
7
|
+
/**
|
|
8
|
+
* Half of secp256k1 curve order
|
|
9
|
+
* Used for EIP-2 signature normalization
|
|
10
|
+
*/
|
|
11
|
+
export declare const SECP256K1_N_HALF: bigint;
|
|
12
|
+
/**
|
|
13
|
+
* Normalizes the s value of an ECDSA signature according to EIP-2.
|
|
14
|
+
*
|
|
15
|
+
* EIP-2 requires that s must be in the lower half of the curve order
|
|
16
|
+
* to prevent signature malleability attacks. If s > n/2, it is converted
|
|
17
|
+
* to s' = n - s.
|
|
18
|
+
*
|
|
19
|
+
* @param s - The s component of the ECDSA signature
|
|
20
|
+
* @returns The normalized s value
|
|
21
|
+
* @throws {SignatureNormalizationError} If s is out of valid range
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const s = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000001n
|
|
26
|
+
* const normalizedS = normalizeS(s)
|
|
27
|
+
* // normalizedS will be less than SECP256K1_N_HALF
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeS(s: bigint): bigint;
|
|
31
|
+
/**
|
|
32
|
+
* Calculates the recovery ID (0-3) for an ECDSA signature.
|
|
33
|
+
*
|
|
34
|
+
* The recovery ID is needed to recover the public key from a signature.
|
|
35
|
+
* This function tries all 4 possible recovery IDs and returns the one
|
|
36
|
+
* that produces a public key matching the expected address.
|
|
37
|
+
*
|
|
38
|
+
* @param messageHash - The hash of the signed message (32 bytes)
|
|
39
|
+
* @param r - The r component of the signature as hex string
|
|
40
|
+
* @param s - The s component of the signature as hex string
|
|
41
|
+
* @param expectedAddress - The expected Ethereum address
|
|
42
|
+
* @returns The recovery ID (0, 1, 2, or 3)
|
|
43
|
+
* @throws {RecoveryIdCalculationError} If no valid recovery ID is found
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const recoveryId = await calculateRecoveryId(
|
|
48
|
+
* '0x1234...', // message hash
|
|
49
|
+
* '0xabcd...', // r value
|
|
50
|
+
* '0xef01...', // s value
|
|
51
|
+
* '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'
|
|
52
|
+
* )
|
|
53
|
+
* // recoveryId will be 0, 1, 2, or 3
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare function calculateRecoveryId(messageHash: Hex, r: Hex, s: Hex, expectedAddress: Address): Promise<number>;
|
|
57
|
+
/**
|
|
58
|
+
* Calculates the v value for an ECDSA signature.
|
|
59
|
+
*
|
|
60
|
+
* The v value is used in Ethereum signatures to enable public key recovery.
|
|
61
|
+
* - Legacy (no chainId): v = 27 + recoveryId
|
|
62
|
+
* - EIP-155 (with chainId): v = chainId * 2 + 35 + recoveryId
|
|
63
|
+
*
|
|
64
|
+
* @param recoveryId - The recovery ID (0-3)
|
|
65
|
+
* @param chainId - Optional chain ID for EIP-155 signatures
|
|
66
|
+
* @returns The v value as bigint
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* // Legacy signature (no chain ID)
|
|
71
|
+
* const vLegacy = calculateV(0) // returns 27n
|
|
72
|
+
*
|
|
73
|
+
* // EIP-155 signature (with chain ID 1 for Ethereum mainnet)
|
|
74
|
+
* const vEIP155 = calculateV(0, 1) // returns 37n (1 * 2 + 35 + 0)
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function calculateV(recoveryId: number, chainId?: number): bigint;
|
|
78
|
+
/**
|
|
79
|
+
* Converts a Uint8Array to a bigint.
|
|
80
|
+
*
|
|
81
|
+
* This is useful for converting DER-encoded signature components
|
|
82
|
+
* (which are returned as Uint8Array) to bigint for cryptographic operations.
|
|
83
|
+
*
|
|
84
|
+
* @param arr - The Uint8Array to convert
|
|
85
|
+
* @returns The bigint representation
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04])
|
|
90
|
+
* const value = uint8ArrayToBigInt(bytes)
|
|
91
|
+
* // value === 0x01020304n
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare function uint8ArrayToBigInt(arr: Uint8Array): bigint;
|
|
95
|
+
//# sourceMappingURL=signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../src/utils/signature.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAIxC;;;GAGG;AACH,eAAO,MAAM,WAAW,kFAAsE,CAAA;AAE9F;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAmB,CAAA;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAY5C;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,GAAG,EAChB,CAAC,EAAE,GAAG,EACN,CAAC,EAAE,GAAG,EACN,eAAe,EAAE,OAAO,GACvB,OAAO,CAAC,MAAM,CAAC,CAgCjB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAcvE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAY1D"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { recoverPublicKey, fromHex } from 'viem';
|
|
2
|
+
import { SignatureNormalizationError, RecoveryIdCalculationError } from '../errors';
|
|
3
|
+
import { publicKeyToAddress } from './address';
|
|
4
|
+
/**
|
|
5
|
+
* secp256k1 curve order (n)
|
|
6
|
+
* Maximum value for ECDSA signature components r and s
|
|
7
|
+
*/
|
|
8
|
+
export const SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
9
|
+
/**
|
|
10
|
+
* Half of secp256k1 curve order
|
|
11
|
+
* Used for EIP-2 signature normalization
|
|
12
|
+
*/
|
|
13
|
+
export const SECP256K1_N_HALF = SECP256K1_N / 2n;
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes the s value of an ECDSA signature according to EIP-2.
|
|
16
|
+
*
|
|
17
|
+
* EIP-2 requires that s must be in the lower half of the curve order
|
|
18
|
+
* to prevent signature malleability attacks. If s > n/2, it is converted
|
|
19
|
+
* to s' = n - s.
|
|
20
|
+
*
|
|
21
|
+
* @param s - The s component of the ECDSA signature
|
|
22
|
+
* @returns The normalized s value
|
|
23
|
+
* @throws {SignatureNormalizationError} If s is out of valid range
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const s = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000001n
|
|
28
|
+
* const normalizedS = normalizeS(s)
|
|
29
|
+
* // normalizedS will be less than SECP256K1_N_HALF
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeS(s) {
|
|
33
|
+
if (s <= 0n || s >= SECP256K1_N) {
|
|
34
|
+
throw new SignatureNormalizationError(`s value out of valid range (must be 0 < s < n): ${s.toString(16)}`);
|
|
35
|
+
}
|
|
36
|
+
if (s > SECP256K1_N_HALF) {
|
|
37
|
+
return SECP256K1_N - s;
|
|
38
|
+
}
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Calculates the recovery ID (0-3) for an ECDSA signature.
|
|
43
|
+
*
|
|
44
|
+
* The recovery ID is needed to recover the public key from a signature.
|
|
45
|
+
* This function tries all 4 possible recovery IDs and returns the one
|
|
46
|
+
* that produces a public key matching the expected address.
|
|
47
|
+
*
|
|
48
|
+
* @param messageHash - The hash of the signed message (32 bytes)
|
|
49
|
+
* @param r - The r component of the signature as hex string
|
|
50
|
+
* @param s - The s component of the signature as hex string
|
|
51
|
+
* @param expectedAddress - The expected Ethereum address
|
|
52
|
+
* @returns The recovery ID (0, 1, 2, or 3)
|
|
53
|
+
* @throws {RecoveryIdCalculationError} If no valid recovery ID is found
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const recoveryId = await calculateRecoveryId(
|
|
58
|
+
* '0x1234...', // message hash
|
|
59
|
+
* '0xabcd...', // r value
|
|
60
|
+
* '0xef01...', // s value
|
|
61
|
+
* '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'
|
|
62
|
+
* )
|
|
63
|
+
* // recoveryId will be 0, 1, 2, or 3
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export async function calculateRecoveryId(messageHash, r, s, expectedAddress) {
|
|
67
|
+
for (let recoveryId = 0; recoveryId < 4; recoveryId++) {
|
|
68
|
+
try {
|
|
69
|
+
// Attempt to recover public key using this recovery ID
|
|
70
|
+
// Use legacy v value (27 + recoveryId) for recovery
|
|
71
|
+
const publicKey = await recoverPublicKey({
|
|
72
|
+
hash: messageHash,
|
|
73
|
+
signature: {
|
|
74
|
+
r,
|
|
75
|
+
s,
|
|
76
|
+
v: BigInt(27 + recoveryId)
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Calculate address from recovered public key
|
|
80
|
+
// recoverPublicKey returns Hex, convert to Uint8Array for publicKeyToAddress
|
|
81
|
+
const publicKeyBytes = fromHex(publicKey, 'bytes');
|
|
82
|
+
const address = publicKeyToAddress(publicKeyBytes);
|
|
83
|
+
// Compare with expected address (case-insensitive)
|
|
84
|
+
if (address.toLowerCase() === expectedAddress.toLowerCase()) {
|
|
85
|
+
return recoveryId;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Recovery failed with this ID, try next one
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new RecoveryIdCalculationError(`Cannot find valid recovery ID for signature (r=${r}, s=${s}) and address ${expectedAddress}`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Calculates the v value for an ECDSA signature.
|
|
97
|
+
*
|
|
98
|
+
* The v value is used in Ethereum signatures to enable public key recovery.
|
|
99
|
+
* - Legacy (no chainId): v = 27 + recoveryId
|
|
100
|
+
* - EIP-155 (with chainId): v = chainId * 2 + 35 + recoveryId
|
|
101
|
+
*
|
|
102
|
+
* @param recoveryId - The recovery ID (0-3)
|
|
103
|
+
* @param chainId - Optional chain ID for EIP-155 signatures
|
|
104
|
+
* @returns The v value as bigint
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* // Legacy signature (no chain ID)
|
|
109
|
+
* const vLegacy = calculateV(0) // returns 27n
|
|
110
|
+
*
|
|
111
|
+
* // EIP-155 signature (with chain ID 1 for Ethereum mainnet)
|
|
112
|
+
* const vEIP155 = calculateV(0, 1) // returns 37n (1 * 2 + 35 + 0)
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function calculateV(recoveryId, chainId) {
|
|
116
|
+
if (recoveryId < 0 || recoveryId > 3) {
|
|
117
|
+
throw new RecoveryIdCalculationError(`Invalid recovery ID (must be 0-3): ${recoveryId}`);
|
|
118
|
+
}
|
|
119
|
+
if (chainId !== undefined) {
|
|
120
|
+
// EIP-155: v = chainId * 2 + 35 + recoveryId
|
|
121
|
+
return BigInt(chainId * 2 + 35 + recoveryId);
|
|
122
|
+
}
|
|
123
|
+
// Legacy: v = 27 + recoveryId
|
|
124
|
+
return BigInt(27 + recoveryId);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Converts a Uint8Array to a bigint.
|
|
128
|
+
*
|
|
129
|
+
* This is useful for converting DER-encoded signature components
|
|
130
|
+
* (which are returned as Uint8Array) to bigint for cryptographic operations.
|
|
131
|
+
*
|
|
132
|
+
* @param arr - The Uint8Array to convert
|
|
133
|
+
* @returns The bigint representation
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04])
|
|
138
|
+
* const value = uint8ArrayToBigInt(bytes)
|
|
139
|
+
* // value === 0x01020304n
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export function uint8ArrayToBigInt(arr) {
|
|
143
|
+
if (arr.length === 0) {
|
|
144
|
+
return 0n;
|
|
145
|
+
}
|
|
146
|
+
// Convert Uint8Array to hex string
|
|
147
|
+
const hex = Array.from(arr)
|
|
148
|
+
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
149
|
+
.join('');
|
|
150
|
+
// Parse as bigint
|
|
151
|
+
return BigInt('0x' + hex);
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=signature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.js","sourceRoot":"","sources":["../../src/utils/signature.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "evm-kms-signer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AWS KMS-based Ethereum signer for viem with enterprise-grade security. Sign transactions and messages using keys stored in AWS KMS without exposing private keys.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ethereum",
|
|
21
|
+
"aws",
|
|
22
|
+
"kms",
|
|
23
|
+
"viem",
|
|
24
|
+
"signer",
|
|
25
|
+
"evm",
|
|
26
|
+
"crypto",
|
|
27
|
+
"blockchain",
|
|
28
|
+
"wallet",
|
|
29
|
+
"security"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/gtg7784/evm-kms-signer.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/gtg7784/evm-kms-signer/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/gtg7784/evm-kms-signer#readme",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"type-check": "tsc --noEmit",
|
|
41
|
+
"start": "tsx src/index.ts",
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"test": "vitest",
|
|
44
|
+
"test:ui": "vitest --ui",
|
|
45
|
+
"test:run": "vitest run",
|
|
46
|
+
"example:sign": "tsx examples/sign-message.ts",
|
|
47
|
+
"example:tx": "tsx examples/send-transaction.ts"
|
|
48
|
+
},
|
|
49
|
+
"author": "Alan Go <tae.gun7784@gmail.com>",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"packageManager": "pnpm@9.0.1+sha1.0e0a9c2d140ddf9aab730067eb7bcfb9e18bdee7",
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"viem": "^2.0.0"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@aws-sdk/client-kms": "^3.0.0",
|
|
60
|
+
"abitype": "^1.1.1"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^24.10.0",
|
|
64
|
+
"dotenv": "^17.2.3",
|
|
65
|
+
"tsx": "^4.20.6",
|
|
66
|
+
"typescript": "^5.9.3",
|
|
67
|
+
"vitest": "^4.0.8",
|
|
68
|
+
"viem": "^2.0.0"
|
|
69
|
+
}
|
|
70
|
+
}
|