1- import { AwsClient } from "aws4fetch" ;
21import { env } from "~/env.server" ;
32import { AuthenticatedEnvironment } from "~/services/apiAuth.server" ;
43import { logger } from "~/services/logger.server" ;
54import { singleton } from "~/utils/singleton" ;
65import { startActiveSpan } from "./tracer.server" ;
76import { IOPacket } from "@trigger.dev/core/v3" ;
7+ import { ObjectStoreClient , type ObjectStoreClientConfig } from "./objectStoreClient.server" ;
88
99/**
1010 * Parsed storage URI with optional protocol prefix
@@ -49,106 +49,78 @@ export function formatStorageUri(path: string, protocol?: string): string {
4949}
5050
5151/**
52- * Object storage client configuration
52+ * Get object storage configuration for a given protocol.
53+ * Returns a config if baseUrl is set, even without explicit credentials —
54+ * in that case the AWS credential chain (ECS task role, EC2 IMDS, etc.) is used,
55+ * and OBJECT_STORE_BUCKET must also be set.
5356 */
54- type ObjectStoreConfig = {
55- baseUrl : string ;
56- accessKeyId : string ;
57- secretAccessKey : string ;
58- region ?: string ;
59- service ?: string ;
60- } ;
61-
62- /**
63- * Get object storage configuration for a given protocol
64- * @param protocol Protocol name (e.g., "s3", "r2"), or undefined for default
65- * @returns Configuration object or undefined if not configured
66- */
67- function getObjectStoreConfig ( protocol ?: string ) : ObjectStoreConfig | undefined {
57+ function getObjectStoreConfig ( protocol ?: string ) : ObjectStoreClientConfig | undefined {
6858 if ( protocol ) {
6959 // Named provider (e.g., OBJECT_STORE_S3_*)
7060 const prefix = `OBJECT_STORE_${ protocol . toUpperCase ( ) } _` ;
7161 const baseUrl = process . env [ `${ prefix } BASE_URL` ] ;
72- const accessKeyId = process . env [ `${ prefix } ACCESS_KEY_ID` ] ;
73- const secretAccessKey = process . env [ `${ prefix } SECRET_ACCESS_KEY` ] ;
74- const region = process . env [ `${ prefix } REGION` ] ;
75- const service = process . env [ `${ prefix } SERVICE` ] ;
76-
77- if ( ! baseUrl || ! accessKeyId || ! secretAccessKey ) {
78- return undefined ;
79- }
62+ if ( ! baseUrl ) return undefined ;
8063
8164 return {
8265 baseUrl,
83- accessKeyId,
84- secretAccessKey,
85- region,
86- service,
66+ bucket : process . env [ `${ prefix } BUCKET` ] || undefined ,
67+ accessKeyId : process . env [ `${ prefix } ACCESS_KEY_ID` ] || undefined ,
68+ secretAccessKey : process . env [ `${ prefix } SECRET_ACCESS_KEY` ] || undefined ,
69+ region : process . env [ `${ prefix } REGION` ] || undefined ,
70+ service : process . env [ `${ prefix } SERVICE` ] || undefined ,
8771 } ;
8872 }
8973
9074 // Default provider (backward compatible)
91- if (
92- ! env . OBJECT_STORE_BASE_URL ||
93- ! env . OBJECT_STORE_ACCESS_KEY_ID ||
94- ! env . OBJECT_STORE_SECRET_ACCESS_KEY
95- ) {
75+ if ( ! env . OBJECT_STORE_BASE_URL ) {
9676 return undefined ;
9777 }
9878
9979 return {
10080 baseUrl : env . OBJECT_STORE_BASE_URL ,
101- accessKeyId : env . OBJECT_STORE_ACCESS_KEY_ID ,
102- secretAccessKey : env . OBJECT_STORE_SECRET_ACCESS_KEY ,
103- region : env . OBJECT_STORE_REGION ,
104- service : env . OBJECT_STORE_SERVICE ,
81+ bucket : env . OBJECT_STORE_BUCKET || undefined ,
82+ accessKeyId : env . OBJECT_STORE_ACCESS_KEY_ID || undefined ,
83+ secretAccessKey : env . OBJECT_STORE_SECRET_ACCESS_KEY || undefined ,
84+ region : env . OBJECT_STORE_REGION || undefined ,
85+ service : env . OBJECT_STORE_SERVICE || undefined ,
10586 } ;
10687}
10788
10889/**
109- * Object storage clients registry
110- * Maps protocol name to AwsClient instance
90+ * Object storage client registry. Maps protocol name to ObjectStoreClient singleton.
91+ * ObjectStoreClient internally uses either aws4fetch (static credentials) or the
92+ * AWS SDK S3Client (IAM credential chain), selected at creation time.
11193 */
11294const objectStoreClients = singleton (
11395 "objectStoreClients" ,
114- ( ) => new Map < string | undefined , AwsClient > ( )
96+ ( ) => new Map < string , ObjectStoreClient > ( )
11597) ;
11698
117- /**
118- * Get or create an object storage client for a given protocol
119- * @param protocol Protocol name (e.g., "s3", "r2"), or undefined for default
120- * @returns AwsClient instance or undefined if not configured
121- */
122- function getObjectStoreClient ( protocol ?: string ) : AwsClient | undefined {
123- const key = protocol ;
124-
125- if ( objectStoreClients . has ( key ) ) {
126- return objectStoreClients . get ( key ) ;
127- }
128-
99+ function getObjectStoreClient ( protocol ?: string ) : ObjectStoreClient | undefined {
129100 const config = getObjectStoreConfig ( protocol ) ;
130- if ( ! config ) {
131- return undefined ;
101+ if ( ! config ) return undefined ;
102+
103+ // Key includes baseUrl so that config changes (e.g. different containers in tests)
104+ // always produce a fresh client while production usage (stable env) is effectively
105+ // a per-protocol singleton.
106+ const cacheKey = `${ protocol ?? "default" } :${ config . baseUrl } ` ;
107+ if ( objectStoreClients . has ( cacheKey ) ) {
108+ return objectStoreClients . get ( cacheKey ) ;
132109 }
133110
134- const client = new AwsClient ( {
135- accessKeyId : config . accessKeyId ,
136- secretAccessKey : config . secretAccessKey ,
137- region : config . region ,
138- // We now set the default value to "s3" in the schema to enhance interoperability with various S3-compatible services.
139- // Setting this env var to an empty string will restore the previous behavior of not setting a service.
140- service : config . service ? config . service : undefined ,
141- } ) ;
142-
143- objectStoreClients . set ( key , client ) ;
111+ const client = ObjectStoreClient . create ( config ) ;
112+ objectStoreClients . set ( cacheKey , client ) ;
144113 return client ;
145114}
146115
147116export function hasObjectStoreClient ( ) : boolean {
148- return getObjectStoreClient ( ) !== undefined || getObjectStoreClient ( env . OBJECT_STORE_DEFAULT_PROTOCOL ) !== undefined ;
117+ const defaultConfig = getObjectStoreConfig ( ) ;
118+ const protocolConfig = env . OBJECT_STORE_DEFAULT_PROTOCOL
119+ ? getObjectStoreConfig ( env . OBJECT_STORE_DEFAULT_PROTOCOL )
120+ : undefined ;
121+ return ! ! ( defaultConfig || protocolConfig ) ;
149122}
150123
151-
152124export async function uploadPacketToObjectStore (
153125 filename : string ,
154126 data : ReadableStream | string ,
@@ -161,39 +133,21 @@ export async function uploadPacketToObjectStore(
161133 const client = getObjectStoreClient ( protocol ) ;
162134
163135 if ( ! client ) {
164- throw new Error (
165- `Object store credentials are not set for protocol: ${ protocol || "default" } `
166- ) ;
167- }
168-
169- const config = getObjectStoreConfig ( protocol ) ;
170- if ( ! config ?. baseUrl ) {
171- throw new Error ( `Object store base URL is not set for protocol: ${ protocol || "default" } ` ) ;
136+ throw new Error ( `Object store is not configured for protocol: ${ protocol || "default" } ` ) ;
172137 }
173138
174139 span . setAttributes ( {
175140 projectRef : environment . project . externalRef ,
176141 environmentSlug : environment . slug ,
177- filename : filename ,
142+ filename,
178143 protocol : protocol || "default" ,
179144 } ) ;
180145
181- const url = new URL ( config . baseUrl ) ;
182- url . pathname = `/packets/${ environment . project . externalRef } /${ environment . slug } /${ filename } ` ;
146+ const key = `packets/${ environment . project . externalRef } /${ environment . slug } /${ filename } ` ;
183147
184- logger . debug ( "Uploading to object store" , { url : url . href , protocol : protocol || "default" } ) ;
148+ logger . debug ( "Uploading to object store" , { key , protocol : protocol || "default" } ) ;
185149
186- const response = await client . fetch ( url . toString ( ) , {
187- method : "PUT" ,
188- headers : {
189- "Content-Type" : contentType ,
190- } ,
191- body : data ,
192- } ) ;
193-
194- if ( ! response . ok ) {
195- throw new Error ( `Failed to upload output to ${ url } : ${ response . statusText } ` ) ;
196- }
150+ await client . putObject ( key , data , contentType ) ;
197151
198152 // Return filename with protocol prefix if specified
199153 return formatStorageUri ( filename , protocol ) ;
@@ -222,14 +176,7 @@ export async function downloadPacketFromObjectStore(
222176 const client = getObjectStoreClient ( protocol ) ;
223177
224178 if ( ! client ) {
225- throw new Error (
226- `Object store credentials are not set for protocol: ${ protocol || "default" } `
227- ) ;
228- }
229-
230- const config = getObjectStoreConfig ( protocol ) ;
231- if ( ! config ?. baseUrl ) {
232- throw new Error ( `Object store base URL is not set for protocol: ${ protocol || "default" } ` ) ;
179+ throw new Error ( `Object store is not configured for protocol: ${ protocol || "default" } ` ) ;
233180 }
234181
235182 span . setAttributes ( {
@@ -239,28 +186,13 @@ export async function downloadPacketFromObjectStore(
239186 protocol : protocol || "default" ,
240187 } ) ;
241188
242- const url = new URL ( config . baseUrl ) ;
243- url . pathname = `/packets/${ environment . project . externalRef } /${ environment . slug } /${ path } ` ;
189+ const key = `packets/${ environment . project . externalRef } /${ environment . slug } /${ path } ` ;
244190
245- logger . debug ( "Downloading from object store" , {
246- url : url . href ,
247- protocol : protocol || "default" ,
248- } ) ;
249-
250- const response = await client . fetch ( url . toString ( ) ) ;
251-
252- if ( ! response . ok ) {
253- throw new Error ( `Failed to download input from ${ url } : ${ response . statusText } ` ) ;
254- }
191+ logger . debug ( "Downloading from object store" , { key, protocol : protocol || "default" } ) ;
255192
256- const data = await response . text ( ) ;
193+ const data = await client . getObject ( key ) ;
257194
258- const rawPacket = {
259- data,
260- dataType : "application/json" ,
261- } ;
262-
263- return rawPacket ;
195+ return { data, dataType : "application/json" } ;
264196 } ) ;
265197}
266198
@@ -276,39 +208,26 @@ export async function uploadDataToObjectStore(
276208 const client = getObjectStoreClient ( protocol ) ;
277209
278210 if ( ! client ) {
279- throw new Error (
280- `Object store credentials are not set for protocol: ${ protocol || "default" } `
281- ) ;
211+ throw new Error ( `Object store is not configured for protocol: ${ protocol || "default" } ` ) ;
282212 }
283213
284214 const config = getObjectStoreConfig ( protocol ) ;
285- if ( ! config ?. baseUrl ) {
286- throw new Error ( `Object store base URL is not set for protocol: ${ protocol || "default" } ` ) ;
287- }
288215
289216 span . setAttributes ( {
290217 prefix,
291218 filename,
292219 protocol : protocol || "default" ,
293220 } ) ;
294221
295- const url = new URL ( config . baseUrl ) ;
296- url . pathname = prefix ? `/${ prefix } /${ filename } ` : `/${ filename } ` ;
222+ const key = prefix ? `${ prefix } /${ filename } ` : filename ;
297223
298- logger . debug ( "Uploading to object store" , { url : url . href , protocol : protocol || "default" } ) ;
224+ logger . debug ( "Uploading to object store" , { key , protocol : protocol || "default" } ) ;
299225
300- const response = await client . fetch ( url . toString ( ) , {
301- method : "PUT" ,
302- headers : {
303- "Content-Type" : contentType ,
304- } ,
305- body : data ,
306- } ) ;
307-
308- if ( ! response . ok ) {
309- throw new Error ( `Failed to upload data to ${ url } : ${ response . statusText } ` ) ;
310- }
226+ await client . putObject ( key , data , contentType ) ;
311227
228+ // Return a full URL for the caller (reconstruct from baseUrl + key)
229+ const url = new URL ( config ! . baseUrl ) ;
230+ url . pathname = `/${ key } ` ;
312231 return url . href ;
313232 } ) ;
314233}
@@ -334,44 +253,41 @@ export async function generatePresignedRequest(
334253 if ( ! config ?. baseUrl ) {
335254 return {
336255 success : false ,
337- error : `Object store base URL is not set for protocol: ${ protocol || "default" } ` ,
256+ error : `Object store is not configured for protocol: ${ protocol || "default" } ` ,
338257 } ;
339258 }
340259
341260 const client = getObjectStoreClient ( protocol ) ;
342261 if ( ! client ) {
343262 return {
344263 success : false ,
345- error : `Object store client is not initialized for protocol: ${ protocol || "default" } ` ,
264+ error : `Object store is not configured for protocol: ${ protocol || "default" } ` ,
346265 } ;
347266 }
348267
349- const url = new URL ( config . baseUrl ) ;
350- url . pathname = `/packets/${ projectRef } /${ envSlug } /${ path } ` ;
351- url . searchParams . set ( "X-Amz-Expires" , "300" ) ; // 5 minutes
268+ const key = `packets/${ projectRef } /${ envSlug } /${ path } ` ;
352269
353- const signed = await client . sign (
354- new Request ( url , {
355- method,
356- } ) ,
357- {
358- aws : { signQuery : true } ,
359- }
360- ) ;
361-
362- logger . debug ( "Generated presigned URL" , {
363- url : signed . url ,
364- headers : Object . fromEntries ( signed . headers ) ,
365- projectRef,
366- envSlug,
367- filename,
368- protocol : protocol || "default" ,
369- } ) ;
270+ try {
271+ const url = await client . presign ( key , method , 300 ) ; // 5 minutes
370272
371- return {
372- success : true ,
373- request : signed ,
374- } ;
273+ logger . debug ( "Generated presigned URL" , {
274+ url,
275+ projectRef,
276+ envSlug,
277+ filename,
278+ protocol : protocol || "default" ,
279+ } ) ;
280+
281+ return {
282+ success : true ,
283+ request : new Request ( url , { method } ) ,
284+ } ;
285+ } catch ( error ) {
286+ return {
287+ success : false ,
288+ error : `Failed to generate presigned URL: ${ error instanceof Error ? error . message : String ( error ) } ` ,
289+ } ;
290+ }
375291}
376292
377293export async function generatePresignedUrl (
0 commit comments