1+ import * as crypto from 'crypto' ;
2+
3+ interface CoinbaseCredentials {
4+ apiKey : string ;
5+ privateKey : string ;
6+ }
7+
8+ interface BuildCdpJwtParams {
9+ apiKeyId : string ;
10+ apiKeySecret : string ;
11+ method : string ;
12+ host : string ;
13+ path : string ;
14+ }
15+
16+ function createCoinbaseJWT ( credentials : CoinbaseCredentials , requestMethod : string , requestHost : string , requestPath : string ) : string {
17+ const { apiKey, privateKey } = credentials ;
18+
19+ const pemKey = privateKey
20+ . replace ( / \\ n / g, '\n' )
21+ . trim ( ) ;
22+
23+ const keyName = apiKey ;
24+
25+ const header = {
26+ alg : 'ES256' ,
27+ kid : keyName ,
28+ typ : 'JWT' ,
29+ nonce : crypto . randomBytes ( 16 ) . toString ( 'hex' )
30+ } ;
31+
32+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
33+ const payload = {
34+ sub : keyName ,
35+ iss : 'coinbase-cloud' ,
36+ nbf : now ,
37+ exp : now + 120 , // 2 minutes expiration
38+ aud : [ 'cdp_service' ] ,
39+ uri : `${ requestMethod } ${ requestHost } ${ requestPath } `
40+ } ;
41+
42+ const base64UrlEncode = ( obj : any ) : string => {
43+ return Buffer . from ( JSON . stringify ( obj ) )
44+ . toString ( 'base64' )
45+ . replace ( / \+ / g, '-' )
46+ . replace ( / \/ / g, '_' )
47+ . replace ( / = / g, '' ) ;
48+ } ;
49+
50+ const encodedHeader = base64UrlEncode ( header ) ;
51+ const encodedPayload = base64UrlEncode ( payload ) ;
52+
53+ const message = `${ encodedHeader } .${ encodedPayload } ` ;
54+
55+ const sign = crypto . createSign ( 'SHA256' ) ;
56+ sign . update ( message ) ;
57+ sign . end ( ) ;
58+
59+ const signature = sign . sign ( pemKey ) ;
60+
61+ const derSignature = signature ;
62+
63+ let offset = 0 ;
64+
65+ if ( derSignature [ offset ] === 0x30 ) {
66+ offset += 2 ;
67+ }
68+
69+ if ( derSignature [ offset ] === 0x02 ) {
70+ offset += 1 ;
71+ const rLength = derSignature [ offset ] ;
72+ offset += 1 ;
73+ const r = derSignature . slice ( offset , offset + rLength ) ;
74+ offset += rLength ;
75+
76+ if ( derSignature [ offset ] === 0x02 ) {
77+ offset += 1 ;
78+ const sLength = derSignature [ offset ] ;
79+ offset += 1 ;
80+ const s = derSignature . slice ( offset , offset + sLength ) ;
81+
82+ const rPadded = r . length > 32 ? r . slice ( r . length - 32 ) : Buffer . concat ( [ Buffer . alloc ( 32 - r . length ) , r ] ) ;
83+ const sPadded = s . length > 32 ? s . slice ( s . length - 32 ) : Buffer . concat ( [ Buffer . alloc ( 32 - s . length ) , s ] ) ;
84+
85+ const rawSignature = Buffer . concat ( [ rPadded , sPadded ] ) ;
86+
87+ const encodedSignature = rawSignature
88+ . toString ( 'base64' )
89+ . replace ( / \+ / g, '-' )
90+ . replace ( / \/ / g, '_' )
91+ . replace ( / = / g, '' ) ;
92+
93+ return `${ encodedHeader } .${ encodedPayload } .${ encodedSignature } ` ;
94+ }
95+ }
96+
97+ throw new Error ( 'Failed to parse signature' ) ;
98+ }
99+
100+ export async function buildCdpJwtAsync ( params : BuildCdpJwtParams ) : Promise < string > {
101+ const { apiKeyId, apiKeySecret, method, host, path } = params ;
102+
103+ return createCoinbaseJWT (
104+ { apiKey : apiKeyId , privateKey : apiKeySecret } ,
105+ method ,
106+ host ,
107+ path
108+ ) ;
109+ }
0 commit comments