@@ -103,6 +103,14 @@ async function verifyHuggingFaceKey(apiKey) {
103103 if ( ! res . ok ) throw new Error ( `Hugging Face token invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
104104}
105105
106+ async function verifyCohereKey ( apiKey ) {
107+ const { res, text } = await fetchTextWithTimeout ( 'https://api.cohere.com/v1/models' , {
108+ method : 'GET' ,
109+ headers : { Authorization : `Bearer ${ apiKey } ` }
110+ } , 30000 , 'Cohere auth' ) ;
111+ if ( ! res . ok ) throw new Error ( `Cohere key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
112+ }
113+
106114async function verifyFirecrawlKey ( apiKey ) {
107115 const { res, text } = await fetchTextWithTimeout ( 'https://api.firecrawl.dev/v1/search' , {
108116 method : 'POST' ,
@@ -139,6 +147,64 @@ async function verifyDriftbotKey(apiKey) {
139147 if ( ! res . ok ) throw new Error ( `Driftbot key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
140148}
141149
150+ async function verifyBraveSearchKey ( apiKey ) {
151+ const { res, text } = await fetchTextWithTimeout ( 'https://api.search.brave.com/res/v1/web/search?q=example.com&count=1' , {
152+ method : 'GET' ,
153+ headers : {
154+ 'X-Subscription-Token' : apiKey ,
155+ Accept : 'application/json'
156+ }
157+ } , 45000 , 'Brave auth' ) ;
158+ if ( ! res . ok ) throw new Error ( `Brave key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
159+ }
160+
161+ async function verifyTavilyKey ( apiKey ) {
162+ const { res, text } = await fetchTextWithTimeout ( 'https://api.tavily.com/search' , {
163+ method : 'POST' ,
164+ headers : { 'Content-Type' : 'application/json' } ,
165+ body : JSON . stringify ( { api_key : apiKey , query : 'example.com' , max_results : 1 } )
166+ } , 45000 , 'Tavily auth' ) ;
167+ if ( ! res . ok ) throw new Error ( `Tavily key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
168+ }
169+
170+ async function verifyExaKey ( apiKey ) {
171+ const { res, text } = await fetchTextWithTimeout ( 'https://api.exa.ai/search' , {
172+ method : 'POST' ,
173+ headers : {
174+ 'Content-Type' : 'application/json' ,
175+ 'x-api-key' : apiKey
176+ } ,
177+ body : JSON . stringify ( { query : 'example.com' , numResults : 1 } )
178+ } , 45000 , 'Exa auth' ) ;
179+ if ( ! res . ok ) throw new Error ( `Exa key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
180+ }
181+
182+ async function verifySerperKey ( apiKey ) {
183+ const { res, text } = await fetchTextWithTimeout ( 'https://google.serper.dev/search' , {
184+ method : 'POST' ,
185+ headers : {
186+ 'Content-Type' : 'application/json' ,
187+ 'X-API-KEY' : apiKey
188+ } ,
189+ body : JSON . stringify ( { q : 'example.com' , num : 1 } )
190+ } , 45000 , 'Serper auth' ) ;
191+ if ( ! res . ok ) throw new Error ( `Serper key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
192+ }
193+
194+ async function verifySerpApiKey ( apiKey ) {
195+ const { res, text } = await fetchTextWithTimeout ( `https://serpapi.com/search.json?q=example.com&num=1&api_key=${ encodeURIComponent ( apiKey ) } ` , {
196+ method : 'GET'
197+ } , 45000 , 'SerpAPI auth' ) ;
198+ if ( ! res . ok ) throw new Error ( `SerpAPI key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
199+ }
200+
201+ async function verifyGoogleKgKey ( apiKey ) {
202+ const { res, text } = await fetchTextWithTimeout ( `https://kgsearch.googleapis.com/v1/entities:search?query=Tokyo&limit=1&key=${ encodeURIComponent ( apiKey ) } ` , {
203+ method : 'GET'
204+ } , 45000 , 'Google KG auth' ) ;
205+ if ( ! res . ok ) throw new Error ( `Google KG key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
206+ }
207+
142208async function verifyLlamaCloudKey ( apiKey ) {
143209 if ( ! String ( apiKey || '' ) . trim ( ) . startsWith ( 'llx-' ) ) {
144210 throw new Error ( 'LlamaCloud key format invalid (expected llx-)' ) ;
@@ -169,6 +235,20 @@ async function verifyAssemblyAiKey(apiKey) {
169235 }
170236}
171237
238+ async function verifyUnstructuredKey ( apiKey ) {
239+ const { res, text } = await fetchTextWithTimeout ( 'https://api.unstructuredapp.io/general/v0/general' , {
240+ method : 'POST' ,
241+ headers : {
242+ Accept : 'application/json' ,
243+ 'unstructured-api-key' : apiKey
244+ } ,
245+ body : new FormData ( )
246+ } , 30000 , 'Unstructured auth' ) ;
247+ if ( res . status === 401 || res . status === 403 ) {
248+ throw new Error ( `Unstructured key invalid (${ res . status } ): ${ text . slice ( 0 , 300 ) } ` ) ;
249+ }
250+ }
251+
172252async function verifyAllProviderCredentials ( providerEnv , options = { } ) {
173253 const strictAll = Boolean ( options . strictAll ) ;
174254 const checks = [ ] ;
@@ -199,6 +279,11 @@ async function verifyAllProviderCredentials(providerEnv, options = {}) {
199279 enabled : Boolean ( key ( 'HUGGINGFACE_INFERENCE_API_TOKEN' ) ) ,
200280 run : ( ) => verifyHuggingFaceKey ( key ( 'HUGGINGFACE_INFERENCE_API_TOKEN' ) )
201281 } ) ;
282+ checks . push ( {
283+ name : 'cohere' ,
284+ enabled : Boolean ( key ( 'COHERE_API_KEY' ) ) ,
285+ run : ( ) => verifyCohereKey ( key ( 'COHERE_API_KEY' ) )
286+ } ) ;
202287 checks . push ( {
203288 name : 'firecrawl' ,
204289 enabled : Boolean ( key ( 'FIRECRAWL_API_KEY' ) ) ,
@@ -219,11 +304,47 @@ async function verifyAllProviderCredentials(providerEnv, options = {}) {
219304 enabled : Boolean ( key ( 'LLAMA_CLOUD_API_KEY' ) ) ,
220305 run : ( ) => verifyLlamaCloudKey ( key ( 'LLAMA_CLOUD_API_KEY' ) )
221306 } ) ;
307+ checks . push ( {
308+ name : 'unstructured' ,
309+ enabled : Boolean ( key ( 'UNSTRUCTURED_API_KEY' ) ) ,
310+ run : ( ) => verifyUnstructuredKey ( key ( 'UNSTRUCTURED_API_KEY' ) )
311+ } ) ;
222312 checks . push ( {
223313 name : 'assemblyai' ,
224314 enabled : Boolean ( key ( 'ASSEMBLYAI_API_KEY' ) ) ,
225315 run : ( ) => verifyAssemblyAiKey ( key ( 'ASSEMBLYAI_API_KEY' ) )
226316 } ) ;
317+ checks . push ( {
318+ name : 'brave' ,
319+ enabled : Boolean ( key ( 'BRAVE_SEARCH_API_KEY' ) ) ,
320+ run : ( ) => verifyBraveSearchKey ( key ( 'BRAVE_SEARCH_API_KEY' ) )
321+ } ) ;
322+ checks . push ( {
323+ name : 'tavily' ,
324+ enabled : Boolean ( key ( 'TAVILY_API_KEY' ) ) ,
325+ run : ( ) => verifyTavilyKey ( key ( 'TAVILY_API_KEY' ) )
326+ } ) ;
327+ checks . push ( {
328+ name : 'exa' ,
329+ enabled : Boolean ( key ( 'EXA_API_KEY' ) ) ,
330+ optional : true ,
331+ run : ( ) => verifyExaKey ( key ( 'EXA_API_KEY' ) )
332+ } ) ;
333+ checks . push ( {
334+ name : 'serper' ,
335+ enabled : Boolean ( key ( 'SERPER_API_KEY' ) ) ,
336+ run : ( ) => verifySerperKey ( key ( 'SERPER_API_KEY' ) )
337+ } ) ;
338+ checks . push ( {
339+ name : 'serpapi' ,
340+ enabled : Boolean ( key ( 'SERPAPI_API_KEY' ) ) ,
341+ run : ( ) => verifySerpApiKey ( key ( 'SERPAPI_API_KEY' ) )
342+ } ) ;
343+ checks . push ( {
344+ name : 'googlekg' ,
345+ enabled : Boolean ( key ( 'GOOGLE_KG_API_KEY' ) ) ,
346+ run : ( ) => verifyGoogleKgKey ( key ( 'GOOGLE_KG_API_KEY' ) )
347+ } ) ;
227348
228349 const failures = [ ] ;
229350 for ( const check of checks ) {
@@ -235,7 +356,12 @@ async function verifyAllProviderCredentials(providerEnv, options = {}) {
235356 await check . run ( ) ;
236357 console . log ( `[deploy] provider credential check ok: ${ check . name } ` ) ;
237358 } catch ( error ) {
238- failures . push ( `${ check . name } : ${ String ( error ?. message || error ) } ` ) ;
359+ const message = `${ check . name } : ${ String ( error ?. message || error ) } ` ;
360+ if ( check . optional && ! strictAll ) {
361+ console . warn ( `[deploy] provider credential check warning: ${ message } ` ) ;
362+ continue ;
363+ }
364+ failures . push ( message ) ;
239365 }
240366 }
241367 if ( failures . length ) {
@@ -305,7 +431,7 @@ function pickCloudflareAccountTokenCandidates(cfYaml) {
305431 return out ;
306432}
307433
308- function resolveR2Config ( args , cfYaml , awsYaml ) {
434+ function resolveR2Config ( args , cfYaml , awsYaml , serviceName = '' ) {
309435 const cfR2 = ( cfYaml && cfYaml . r2 ) || { } ;
310436 const s3Clients = ( cfR2 && cfR2 . s3_clients ) || { } ;
311437 const key2 = s3Clients . keypair_2 || { } ;
@@ -316,7 +442,11 @@ function resolveR2Config(args, cfYaml, awsYaml) {
316442 process . env . CLOUDFLARE_ACCOUNT_ID ,
317443 cfYaml && cfYaml . account_id
318444 ) ;
319- const bucket = firstNonEmpty ( args [ 'r2-bucket' ] , process . env . R2_BUCKET , cfR2 . bucket , 'web2comics-bot-data' ) ;
445+ const explicitBucket = firstNonEmpty ( args [ 'r2-bucket' ] ) ;
446+ const isStageService = / s t a g e / i. test ( String ( serviceName || '' ) ) ;
447+ const stageBucketDefault = firstNonEmpty ( args [ 'r2-bucket-stage' ] , process . env . R2_BUCKET_STAGE , 'web2comics-bot-data-stage' ) ;
448+ const nonStageBucketDefault = firstNonEmpty ( process . env . R2_BUCKET , cfR2 . bucket , 'web2comics-bot-data' ) ;
449+ const bucket = explicitBucket || ( isStageService ? stageBucketDefault : nonStageBucketDefault ) ;
320450 const endpointRaw = firstNonEmpty (
321451 args [ 'r2-endpoint' ] ,
322452 process . env . R2_S3_ENDPOINT ,
@@ -544,6 +674,7 @@ async function main() {
544674 }
545675 const envOnly = parseBool ( preArgs [ 'env-only' ] || process . env . BOT_SECRETS_ENV_ONLY ) ;
546676 loadEnvFiles ( [
677+ path . join ( repoRoot , '.env.all' ) ,
547678 path . join ( repoRoot , '.env.local' ) ,
548679 path . join ( repoRoot , '.env.e2e.local' ) ,
549680 path . join ( repoRoot , '.crawler' ) ,
@@ -602,13 +733,22 @@ async function main() {
602733 CLOUDFLARE_ACCOUNT_ID : cloudflareAccountId ,
603734 CLOUDFLARE_API_TOKEN : cloudflareAiToken ,
604735 HUGGINGFACE_INFERENCE_API_TOKEN : firstNonEmpty ( args [ 'huggingface-token' ] , process . env . HUGGINGFACE_INFERENCE_API_TOKEN ) ,
736+ COHERE_API_KEY : firstNonEmpty ( args [ 'cohere-key' ] , process . env . COHERE_API_KEY ) ,
605737 FIRECRAWL_API_KEY : firstNonEmpty ( args [ 'firecrawl-key' ] , process . env . FIRECRAWL_API_KEY ) ,
606738 JINA_API_KEY : firstNonEmpty ( args [ 'jina-key' ] , process . env . JINA_API_KEY ) ,
607- DRIFTBOT_API_KEY : firstNonEmpty ( args [ 'driftbot-key' ] , process . env . DRIFTBOT_API_KEY ) ,
739+ DRIFTBOT_API_KEY : firstNonEmpty ( args [ 'driftbot-key' ] , process . env . DRIFTBOT_API_KEY , process . env . DIFFBOT_API_KEY ) ,
740+ DIFFBOT_API_KEY : firstNonEmpty ( args [ 'diffbot-key' ] , process . env . DIFFBOT_API_KEY ) ,
741+ BRAVE_SEARCH_API_KEY : firstNonEmpty ( args [ 'brave-key' ] , process . env . BRAVE_SEARCH_API_KEY ) ,
742+ TAVILY_API_KEY : firstNonEmpty ( args [ 'tavily-key' ] , process . env . TAVILY_API_KEY ) ,
743+ EXA_API_KEY : firstNonEmpty ( args [ 'exa-key' ] , process . env . EXA_API_KEY ) ,
744+ SERPER_API_KEY : firstNonEmpty ( args [ 'serper-key' ] , process . env . SERPER_API_KEY ) ,
745+ SERPAPI_API_KEY : firstNonEmpty ( args [ 'serpapi-key' ] , process . env . SERPAPI_API_KEY ) ,
746+ GOOGLE_KG_API_KEY : firstNonEmpty ( args [ 'googlekg-key' ] , process . env . GOOGLE_KG_API_KEY ) ,
608747 LLAMA_CLOUD_API_KEY : firstNonEmpty ( args [ 'llama-cloud-key' ] , process . env . LLAMA_CLOUD_API_KEY , process . env . LLAMAPARSE_API_KEY ) ,
748+ UNSTRUCTURED_API_KEY : firstNonEmpty ( args [ 'unstructured-key' ] , process . env . UNSTRUCTURED_API_KEY ) ,
609749 ASSEMBLYAI_API_KEY : firstNonEmpty ( args [ 'assemblyai-key' ] , process . env . ASSEMBLYAI_API_KEY )
610750 } ;
611- const resolvedR2 = resolveR2Config ( args , cfYaml , awsYaml ) ;
751+ const resolvedR2 = resolveR2Config ( args , cfYaml , awsYaml , serviceName ) ;
612752 const r2Env = {
613753 R2_S3_ENDPOINT : resolvedR2 . endpoint ,
614754 R2_BUCKET : resolvedR2 . bucket ,
@@ -653,11 +793,14 @@ async function main() {
653793 throw new Error ( 'Missing Telegram bot token. Provide TELEGRAM_BOT_TOKEN (GitHub Secret) or --telegram-token' ) ;
654794 }
655795 const existingWebhookSecret = await getExistingWebhookSecret ( telegramToken ) ;
796+ const defaultWebhookSecret = / s t a g e / i. test ( String ( serviceName || '' ) )
797+ ? 'web2comics-render-webhook-secret-stage-v1'
798+ : 'web2comics-render-webhook-secret-v1' ;
656799 const webhookSecret = firstNonEmpty (
657800 args [ 'webhook-secret' ] ,
658801 process . env . TELEGRAM_WEBHOOK_SECRET ,
659802 existingWebhookSecret ,
660- 'web2comics-render-webhook-secret-v1'
803+ defaultWebhookSecret
661804 ) ;
662805 const keyCheck = validateProviderEnv ( providerEnv , requireAllKeys ) ;
663806 if ( ! keyCheck . ok ) {
@@ -856,6 +999,7 @@ async function main() {
856999 publicUrl,
8571000 webhookUrl,
8581001 webhookSecret,
1002+ r2Bucket : r2Env . R2_BUCKET ,
8591003 telegramTestChatId,
8601004 notifyChatId
8611005 } , null , 2 ) , 'utf8' ) ;
0 commit comments