1+ #!/usr/bin/env node
2+
3+ import fs from 'fs' ;
4+
5+ // Configuration from environment variables
6+ const config = {
7+ extensionId : process . env . CHROME_EXTENSION_ID ,
8+ zipPath : process . env . CHROME_ZIP_PATH ,
9+ serviceAccountKey : process . env . GOOGLE_SERVICE_ACCOUNT_KEY
10+ } ;
11+
12+ // Validate required environment variables
13+ const requiredVars = [ 'extensionId' , 'zipPath' , 'serviceAccountKey' ] ;
14+ const missingVars = requiredVars . filter ( varName => ! config [ varName ] ) ;
15+
16+ if ( missingVars . length > 0 ) {
17+ console . error ( '❌ Missing required environment variables:' , missingVars . join ( ', ' ) ) ;
18+ process . exit ( 1 ) ;
19+ }
20+
21+ console . log ( '🚀 Starting Chrome Web Store deployment with Service Account...' ) ;
22+ console . log ( `📦 Extension ID: ${ config . extensionId } ` ) ;
23+ console . log ( `📁 ZIP Path: ${ config . zipPath } ` ) ;
24+
25+ try {
26+ // Check if ZIP file exists
27+ if ( ! fs . existsSync ( config . zipPath ) ) {
28+ throw new Error ( `ZIP file not found: ${ config . zipPath } ` ) ;
29+ }
30+
31+ // Parse service account key
32+ let serviceAccountKey ;
33+ try {
34+ serviceAccountKey = JSON . parse ( config . serviceAccountKey ) ;
35+ } catch ( error ) {
36+ throw new Error ( 'Invalid service account key format' ) ;
37+ }
38+
39+ // Create JWT token for Chrome Web Store API
40+ const header = {
41+ alg : 'RS256' ,
42+ typ : 'JWT'
43+ } ;
44+
45+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
46+ const payload = {
47+ iss : serviceAccountKey . client_email ,
48+ scope : 'https://www.googleapis.com/auth/chromewebstore' ,
49+ aud : 'https://oauth2.googleapis.com/token' ,
50+ exp : now + 3600 ,
51+ iat : now
52+ } ;
53+
54+ // Import crypto for JWT signing
55+ const { createSign } = await import ( 'crypto' ) ;
56+
57+ // Base64url encoding function
58+ const base64urlEncode = ( str ) => {
59+ return str . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = / g, '' ) ;
60+ } ;
61+
62+ // Create JWT
63+ const encodedHeader = base64urlEncode ( Buffer . from ( JSON . stringify ( header ) ) . toString ( 'base64' ) ) ;
64+ const encodedPayload = base64urlEncode ( Buffer . from ( JSON . stringify ( payload ) ) . toString ( 'base64' ) ) ;
65+ const jwtInput = `${ encodedHeader } .${ encodedPayload } ` ;
66+
67+ // Sign JWT
68+ const sign = createSign ( serviceAccountKey . private_key ) ;
69+ const signature = sign . update ( jwtInput ) . sign ( 'base64' ) ;
70+ const encodedSignature = base64urlEncode ( signature ) ;
71+
72+ const jwt = `${ jwtInput } .${ encodedSignature } ` ;
73+
74+ // Exchange JWT for access token
75+ console . log ( '🔐 Getting access token...' ) ;
76+ const tokenResponse = await fetch ( 'https://oauth2.googleapis.com/token' , {
77+ method : 'POST' ,
78+ headers : {
79+ 'Content-Type' : 'application/x-www-form-urlencoded'
80+ } ,
81+ body : new URLSearchParams ( {
82+ grant_type : 'urn:ietf:params:oauth:grant-type:jwt-bearer' ,
83+ assertion : jwt
84+ } )
85+ } ) ;
86+
87+ if ( ! tokenResponse . ok ) {
88+ const errorText = await tokenResponse . text ( ) ;
89+ throw new Error ( `Token exchange failed: ${ tokenResponse . status } ${ errorText } ` ) ;
90+ }
91+
92+ const tokenData = await tokenResponse . json ( ) ;
93+ const accessToken = tokenData . access_token ;
94+
95+ console . log ( '✅ Access token obtained successfully' ) ;
96+
97+ // Read ZIP file
98+ const zipData = fs . readFileSync ( config . zipPath ) ;
99+
100+ // Upload extension using Chrome Web Store API V1 (works with service accounts)
101+ console . log ( '📤 Uploading extension...' ) ;
102+
103+ const uploadResponse = await fetch ( `https://www.googleapis.com/upload/chromewebstore/v1.1/items/${ config . extensionId } ` , {
104+ method : 'PUT' ,
105+ headers : {
106+ 'Authorization' : `Bearer ${ accessToken } ` ,
107+ 'Content-Type' : 'application/zip' ,
108+ 'x-goog-upload-protocol' : 'raw'
109+ } ,
110+ body : zipData
111+ } ) ;
112+
113+ if ( ! uploadResponse . ok ) {
114+ const errorText = await uploadResponse . text ( ) ;
115+ throw new Error ( `Upload failed: ${ uploadResponse . status } ${ uploadResponse . statusText } - ${ errorText } ` ) ;
116+ }
117+
118+ const uploadResult = await uploadResponse . json ( ) ;
119+ console . log ( '✅ Upload successful' ) ;
120+ console . log ( '📋 Upload response:' , JSON . stringify ( uploadResult , null , 2 ) ) ;
121+
122+ // Publish extension
123+ console . log ( '🚀 Publishing extension...' ) ;
124+
125+ const publishResponse = await fetch ( `https://chromewebstore.googleapis.com/chromewebstore/v1.1/items/${ config . extensionId } /publish` , {
126+ method : 'POST' ,
127+ headers : {
128+ 'Authorization' : `Bearer ${ accessToken } ` ,
129+ 'Content-Type' : 'application/json' ,
130+ 'x-goog-api-key' : process . env . GOOGLE_API_KEY || ''
131+ } ,
132+ body : JSON . stringify ( {
133+ target : 'default'
134+ } )
135+ } ) ;
136+
137+ if ( ! publishResponse . ok ) {
138+ const errorText = await publishResponse . text ( ) ;
139+ throw new Error ( `Publish failed: ${ publishResponse . status } ${ publishResponse . statusText } - ${ errorText } ` ) ;
140+ }
141+
142+ const publishResult = await publishResponse . json ( ) ;
143+ console . log ( '✅ Published successfully' ) ;
144+ console . log ( '📋 Publish response:' , JSON . stringify ( publishResult , null , 2 ) ) ;
145+
146+ console . log ( '🎉 Chrome Web Store deployment completed successfully!' ) ;
147+
148+ } catch ( error ) {
149+ console . error ( '❌ Deployment failed:' , error . message ) ;
150+
151+ // Provide more detailed error information
152+ if ( error . response ) {
153+ console . error ( '📋 Error response:' , JSON . stringify ( error . response . data , null , 2 ) ) ;
154+ }
155+
156+ process . exit ( 1 ) ;
157+ }
0 commit comments