11#!/usr/bin/env node
22
33import { readFile , writeFile } from "node:fs/promises" ;
4+ import { createSign } from "node:crypto" ;
45import path from "node:path" ;
56import process from "node:process" ;
67
78const apiRoot = "https://chromewebstore.googleapis.com" ;
89const oauthTokenUrl = "https://oauth2.googleapis.com/token" ;
10+ const chromeWebStoreScope = "https://www.googleapis.com/auth/chromewebstore" ;
911
1012function readFlag ( name , fallback = null ) {
1113 const prefix = `--${ name } =` ;
@@ -24,6 +26,10 @@ function hasFlag(name) {
2426 return process . argv . includes ( `--${ name } ` ) ;
2527}
2628
29+ function optionalEnv ( name ) {
30+ return process . env [ name ] ?? "" ;
31+ }
32+
2733function env ( name ) {
2834 const value = process . env [ name ] ;
2935 if ( ! value ) {
@@ -54,7 +60,44 @@ async function requestJson(url, options = {}) {
5460 return body ;
5561}
5662
57- async function getAccessToken ( ) {
63+ function base64UrlEncode ( input ) {
64+ return Buffer . from ( input )
65+ . toString ( "base64" )
66+ . replaceAll ( "+" , "-" )
67+ . replaceAll ( "/" , "_" )
68+ . replace ( / = + $ / , "" ) ;
69+ }
70+
71+ function signJwt ( payload , privateKey ) {
72+ const header = {
73+ alg : "RS256" ,
74+ typ : "JWT"
75+ } ;
76+ const encodedHeader = base64UrlEncode ( JSON . stringify ( header ) ) ;
77+ const encodedPayload = base64UrlEncode ( JSON . stringify ( payload ) ) ;
78+ const signingInput = `${ encodedHeader } .${ encodedPayload } ` ;
79+ const signer = createSign ( "RSA-SHA256" ) ;
80+ signer . update ( signingInput ) ;
81+ signer . end ( ) ;
82+ const signature = signer . sign ( privateKey ) ;
83+ return `${ signingInput } .${ base64UrlEncode ( signature ) } ` ;
84+ }
85+
86+ function readServiceAccountJson ( ) {
87+ const raw = optionalEnv ( "CWS_SERVICE_ACCOUNT_JSON" ) ;
88+ if ( ! raw ) {
89+ return null ;
90+ }
91+ try {
92+ return JSON . parse ( raw ) ;
93+ } catch ( error ) {
94+ throw new Error (
95+ `CWS_SERVICE_ACCOUNT_JSON is not valid JSON: ${ error instanceof Error ? error . message : String ( error ) } `
96+ ) ;
97+ }
98+ }
99+
100+ async function getOauthAccessToken ( ) {
58101 const body = new URLSearchParams ( {
59102 client_id : env ( "CWS_CLIENT_ID" ) ,
60103 client_secret : env ( "CWS_CLIENT_SECRET" ) ,
@@ -74,6 +117,64 @@ async function getAccessToken() {
74117 return token . access_token ;
75118}
76119
120+ async function getServiceAccountAccessToken ( serviceAccount ) {
121+ const clientEmail = serviceAccount . client_email ;
122+ const privateKey = serviceAccount . private_key ;
123+ const tokenUri = serviceAccount . token_uri ?? oauthTokenUrl ;
124+ if ( typeof clientEmail !== "string" || clientEmail === "" ) {
125+ throw new Error ( "CWS_SERVICE_ACCOUNT_JSON must include client_email" ) ;
126+ }
127+ if ( typeof privateKey !== "string" || privateKey === "" ) {
128+ throw new Error ( "CWS_SERVICE_ACCOUNT_JSON must include private_key" ) ;
129+ }
130+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
131+ const assertion = signJwt (
132+ {
133+ iss : clientEmail ,
134+ scope : chromeWebStoreScope ,
135+ aud : tokenUri ,
136+ exp : now + 3600 ,
137+ iat : now
138+ } ,
139+ privateKey
140+ ) ;
141+ const token = await requestJson ( tokenUri , {
142+ method : "POST" ,
143+ headers : {
144+ "Content-Type" : "application/x-www-form-urlencoded"
145+ } ,
146+ body : new URLSearchParams ( {
147+ grant_type : "urn:ietf:params:oauth:grant-type:jwt-bearer" ,
148+ assertion
149+ } )
150+ } ) ;
151+ if ( ! token . access_token ) {
152+ throw new Error ( "Service account token response did not include access_token" ) ;
153+ }
154+ return token . access_token ;
155+ }
156+
157+ async function getAccessToken ( ) {
158+ const directAccessToken = optionalEnv ( "CWS_ACCESS_TOKEN" ) ;
159+ if ( directAccessToken ) {
160+ return directAccessToken ;
161+ }
162+ const serviceAccount = readServiceAccountJson ( ) ;
163+ if ( serviceAccount ) {
164+ return await getServiceAccountAccessToken ( serviceAccount ) ;
165+ }
166+ if (
167+ ! optionalEnv ( "CWS_CLIENT_ID" ) ||
168+ ! optionalEnv ( "CWS_CLIENT_SECRET" ) ||
169+ ! optionalEnv ( "CWS_REFRESH_TOKEN" )
170+ ) {
171+ throw new Error (
172+ "Configure one Chrome Web Store auth method: CWS_ACCESS_TOKEN, CWS_SERVICE_ACCOUNT_JSON, or CWS_CLIENT_ID+CWS_CLIENT_SECRET+CWS_REFRESH_TOKEN"
173+ ) ;
174+ }
175+ return await getOauthAccessToken ( ) ;
176+ }
177+
77178function itemName ( ) {
78179 return `publishers/${ env ( "CWS_PUBLISHER_ID" ) } /items/${ env ( "CWS_EXTENSION_ID" ) } ` ;
79180}
0 commit comments