@uploadista/adapters-express 0.0.20-beta.9 → 0.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- let e=require(`effect`),t=require(`@uploadista/core`),n=require(`@uploadista/server`);const r=async e=>{if(e.body&&typeof e.body==`object`)return e.body;let t=[];for await(let n of e)t.push(n);let n=Buffer.concat(t).toString();return JSON.parse(n)},i=(t,{baseUrl:n})=>e.Effect.promise(async()=>{let e=new URL(t.request.url,`http://${t.request.get(`host`)}`),i=t.request.get(`Accept`),a=`/${n}/`;if(e.pathname.startsWith(a)&&t.request.method===`GET`){let t=e.pathname.slice(a.length);if(t===`health`||t===`healthz`)return{type:`health`,acceptHeader:i};if(t===`ready`||t===`readyz`)return{type:`health-ready`,acceptHeader:i};if(t===`health/components`)return{type:`health-components`,acceptHeader:i}}let o=`/${n}/api/`;if(!e.pathname.includes(o))return{type:`not-found`};let s=e.pathname.replace(`${n}/api/`,``).split(`/`).filter(Boolean);if(s[0]===`upload`||s.includes(`upload`))switch(t.request.method){case`POST`:return{type:`create-upload`,data:await r(t.request)};case`GET`:if(s[s.length-1]===`capabilities`){let t=e.searchParams.get(`storageId`),n=s[s.length-2],r=t||(n===`upload`?null:n);return r?{type:`get-capabilities`,storageId:r}:{type:`bad-request`,message:`Storage ID is required`}}return s.length<2?{type:`bad-request`,message:`Upload ID is required`}:{type:`get-upload`,uploadId:s[1]};case`PATCH`:{if(s.length<2)return{type:`bad-request`,message:`Upload ID is required`};let e=new ReadableStream({start(e){t.request.on(`data`,t=>{e.enqueue(t)}),t.request.on(`end`,()=>{e.close()}),t.request.on(`error`,t=>{e.error(t)})}});return{type:`upload-chunk`,uploadId:s[1],data:e}}default:return{type:`method-not-allowed`}}else if(s[0]===`flow`||s.includes(`flow`))switch(t.request.method){case`GET`:return{type:`get-flow`,flowId:s[1]};case`POST`:{let e=await r(t.request);return!e||typeof e!=`object`||!(`inputs`in e)?{type:`bad-request`,message:`Inputs are required`}:{type:`run-flow`,flowId:s[1],storageId:s[2],inputs:e.inputs}}default:return{type:`method-not-allowed`}}else if(s[0]===`dlq`||s.includes(`dlq`))switch(t.request.method){case`GET`:return s.length===1?{type:`dlq-list`,options:{status:e.searchParams.get(`status`),flowId:e.searchParams.get(`flowId`),clientId:e.searchParams.get(`clientId`),limit:e.searchParams.get(`limit`)?Number.parseInt(e.searchParams.get(`limit`)):void 0,offset:e.searchParams.get(`offset`)?Number.parseInt(e.searchParams.get(`offset`)):void 0}}:s[1]===`stats`?{type:`dlq-stats`}:{type:`dlq-get`,itemId:s[1]};case`POST`:return s[1]===`cleanup`?{type:`dlq-cleanup`,options:await r(t.request).catch(()=>({}))}:s[1]===`retry-all`?{type:`dlq-retry-all`,options:await r(t.request).catch(()=>({}))}:s[2]===`retry`?{type:`dlq-retry`,itemId:s[1]}:s[2]===`resolve`?{type:`dlq-resolve`,itemId:s[1]}:{type:`method-not-allowed`};case`DELETE`:return s.length<2?{type:`bad-request`,message:`Item ID is required`}:{type:`dlq-delete`,itemId:s[1]};default:return{type:`method-not-allowed`}}else if(s[0]===`jobs`||s.includes(`jobs`)){if(t.request.method===`GET`&&e.pathname.endsWith(`/status`))return s.length<3?{type:`bad-request`,message:`Job ID is required`}:{type:`job-status`,jobId:s[1]};if(t.request.method===`PATCH`&&s.includes(`resume`)){let e=s[1];if(!e)return{type:`bad-request`,message:`Job ID is required`};let n=s[3];if(!n)return{type:`bad-request`,message:`Node ID is required`};let i=t.request.get(`content-type`),a;if(i?.includes(`application/octet-stream`))a=t.request;else if(i?.includes(`application/json`)){let e=await r(t.request);if(!e||typeof e!=`object`||!(`newData`in e))return{type:`bad-request`,message:`Missing newData`};a=e.newData}else return{type:`unsupported-content-type`};return{type:`resume-flow`,jobId:e,nodeId:n,newData:a}}else if(t.request.method===`POST`&&e.pathname.endsWith(`/pause`))return{type:`pause-flow`,jobId:s[1]};else if(t.request.method===`POST`&&e.pathname.endsWith(`/cancel`))return{type:`cancel-flow`,jobId:s[1]};return{type:`method-not-allowed`}}else return{type:`not-found`}}),a=(t,n)=>e.Effect.sync(()=>{let e=t.headers||{};e[`Content-Type`]||=`application/json`;for(let[t,r]of Object.entries(e))n.response.set(t,r);return n.response.status(t.status).send(t.body)});function o(e,t){let n=RegExp(`[?&]${t}=([^&]*)`),r=e.match(n);return r?.[1]?decodeURIComponent(r[1]):void 0}const s=new Map,c=(e,t)=>{let n=new URL(e.url||``,`http://${e.headers.host}`),r=`${t}/ws/`;if(!n.pathname.includes(r))return{type:`invalid-path`,expectedPrefix:r};let i=n.pathname.replace(r,``).split(`/`).filter(Boolean),a=i.includes(`upload`),s=i.includes(`flow`),c=o(e.url||``,`jobId`),l=o(e.url||``,`uploadId`);if(!c&&!l&&i.length>=2){let e=i[0],t=i[1];e===`flow`?c=t:e===`upload`&&(l=t)}let u=c||l;return{baseUrl:t,pathname:n.pathname,routeSegments:i,isUploadRoute:a,isFlowRoute:s,jobId:c,uploadId:l,eventId:u,connection:null}},l=async(e,t)=>{try{let n=new URL(e.url||``,`http://${e.headers.host}`).searchParams.get(`token`),r=null;if(r=n?await t({request:{...e,header:t=>t.toLowerCase()===`authorization`?`Bearer ${n}`:e.headers[t.toLowerCase()]},response:{}}):await t({request:e,response:{}}),!r){let e=n?`token`:`cookies`;return{success:!1,error:{message:`Authentication failed: invalid or expired ${e}`,code:4001,authMethod:e}}}return console.log(`WebSocket authenticated for user: ${r.clientId}`),{success:!0,authResult:r}}catch(e){return console.error(`WebSocket auth error:`,e),{success:!1,error:{message:`Authentication error`,code:4001,authMethod:`unknown`}}}},u=(r,i)=>e.Effect.gen(function*(){let a=yield*t.UploadEngine,o=yield*t.FlowEngine;return(t,u)=>{let d=c(u,r);if(console.log(`🔍 WebSocket request details:`,d),`type`in d&&d.type===`invalid-path`){t.send(JSON.stringify({type:`invalid-path`,message:`WebSocket path must start with ${d.expectedPrefix}`,expectedPrefix:d.expectedPrefix})),t.close(1e3,`Invalid path`);return}let f=d,p={id:`conn_${Date.now()}_${Math.random().toString(36).substring(2,11)}`,send:e=>{t.readyState===t.OPEN?(console.log(`📤 Sending WebSocket message to connection ${p.id}:`,e.substring(0,100)),t.send(e)):console.warn(`⚠️ Cannot send message, WebSocket not open. State: ${t.readyState}`)},close:(e,n)=>t.close(e,n),get readyState(){return t.readyState}};f.connection=p,(async()=>{if(i){let e=await l(u,i);if(!e.success){t.send(JSON.stringify({type:`auth-failed`,message:e.error?.message,code:`AUTH_FAILED`,authMethod:e.error?.authMethod})),t.close(e.error?.code||4001,e.error?.message);return}e.authResult&&s.set(p.id,e.authResult)}console.log(`🔍 WebSocket open for eventId:`,f.eventId,`with connection id:`,p.id);let r=(0,n.handleWebSocketOpen)(f,a,o);e.Effect.runFork(r)})(),t.on(`message`,t=>{let r=(0,n.handleWebSocketMessage)(t,p);e.Effect.runFork(r)}),t.on(`close`,()=>{f.connection?.id&&(s.delete(f.connection.id),console.log(`Cleared auth cache for WebSocket connection: ${f.connection.id}`));let t=(0,n.handleWebSocketClose)(f,a,o);e.Effect.runFork(t)}),t.on(`error`,(...t)=>{let r=t[0],i=(0,n.handleWebSocketError)(r,f.eventId);e.Effect.runFork(i)})}}),d=(t={})=>{let{authMiddleware:n}=t;return{extractRequest:i,sendResponse:a,webSocketHandler:({baseUrl:e})=>u(e,n),runAuthMiddleware:n?t=>e.Effect.tryPromise(()=>n(t)).pipe(e.Effect.catchAll(t=>(console.error(`Express auth middleware failed:`,t),e.Effect.succeed(null)))):void 0}};exports.expressAdapter=d;
1
+ let e=require(`effect`),t=require(`@uploadista/core`),n=require(`@uploadista/server`);const r=async e=>{if(e.body&&typeof e.body==`object`)return e.body;let t=[];for await(let n of e)t.push(n);let n=Buffer.concat(t).toString();return JSON.parse(n)},i=(t,{baseUrl:n})=>e.Effect.promise(async()=>{let e=new URL(t.request.url,`http://${t.request.get(`host`)}`),i=t.request.get(`Accept`),a=`/${n}/`;if(e.pathname.startsWith(a)&&t.request.method===`GET`){let t=e.pathname.slice(a.length);if(t===`health`||t===`healthz`)return{type:`health`,acceptHeader:i};if(t===`ready`||t===`readyz`)return{type:`health-ready`,acceptHeader:i};if(t===`health/components`)return{type:`health-components`,acceptHeader:i}}let o=`/${n}/api/`;if(!e.pathname.includes(o))return{type:`not-found`};let s=e.pathname.replace(`${n}/api/`,``).split(`/`).filter(Boolean);if(s[0]===`upload`||s.includes(`upload`))switch(t.request.method){case`POST`:return{type:`create-upload`,data:await r(t.request)};case`GET`:if(s[s.length-1]===`capabilities`){let t=e.searchParams.get(`storageId`),n=s[s.length-2],r=t||(n===`upload`?null:n);return r?{type:`get-capabilities`,storageId:r}:{type:`bad-request`,message:`Storage ID is required`}}return s.length<2?{type:`bad-request`,message:`Upload ID is required`}:{type:`get-upload`,uploadId:s[1]};case`PATCH`:{if(s.length<2)return{type:`bad-request`,message:`Upload ID is required`};let e=new ReadableStream({start(e){t.request.on(`data`,t=>{e.enqueue(t)}),t.request.on(`end`,()=>{e.close()}),t.request.on(`error`,t=>{e.error(t)})}});return{type:`upload-chunk`,uploadId:s[1],data:e}}default:return{type:`method-not-allowed`}}else if(s[0]===`flow`||s.includes(`flow`))switch(t.request.method){case`GET`:return{type:`get-flow`,flowId:s[1]};case`POST`:{let e=await r(t.request);return!e||typeof e!=`object`||!(`inputs`in e)?{type:`bad-request`,message:`Inputs are required`}:{type:`run-flow`,flowId:s[1],storageId:s[2],inputs:e.inputs}}default:return{type:`method-not-allowed`}}else if(s[0]===`dlq`||s.includes(`dlq`))switch(t.request.method){case`GET`:return s.length===1?{type:`dlq-list`,options:{status:e.searchParams.get(`status`),flowId:e.searchParams.get(`flowId`),clientId:e.searchParams.get(`clientId`),limit:e.searchParams.get(`limit`)?Number.parseInt(e.searchParams.get(`limit`),10):void 0,offset:e.searchParams.get(`offset`)?Number.parseInt(e.searchParams.get(`offset`),10):void 0}}:s[1]===`stats`?{type:`dlq-stats`}:{type:`dlq-get`,itemId:s[1]};case`POST`:return s[1]===`cleanup`?{type:`dlq-cleanup`,options:await r(t.request).catch(()=>({}))}:s[1]===`retry-all`?{type:`dlq-retry-all`,options:await r(t.request).catch(()=>({}))}:s[2]===`retry`?{type:`dlq-retry`,itemId:s[1]}:s[2]===`resolve`?{type:`dlq-resolve`,itemId:s[1]}:{type:`method-not-allowed`};case`DELETE`:return s.length<2?{type:`bad-request`,message:`Item ID is required`}:{type:`dlq-delete`,itemId:s[1]};default:return{type:`method-not-allowed`}}else if(s[0]===`jobs`||s.includes(`jobs`)){if(t.request.method===`GET`&&e.pathname.endsWith(`/status`))return s.length<3?{type:`bad-request`,message:`Job ID is required`}:{type:`job-status`,jobId:s[1]};if(t.request.method===`PATCH`&&s.includes(`resume`)){let e=s[1];if(!e)return{type:`bad-request`,message:`Job ID is required`};let n=s[3];if(!n)return{type:`bad-request`,message:`Node ID is required`};let i=t.request.get(`content-type`),a;if(i?.includes(`application/octet-stream`))a=t.request;else if(i?.includes(`application/json`)){let e=await r(t.request);if(!e||typeof e!=`object`||!(`newData`in e))return{type:`bad-request`,message:`Missing newData`};a=e.newData}else return{type:`unsupported-content-type`};return{type:`resume-flow`,jobId:e,nodeId:n,newData:a}}else if(t.request.method===`POST`&&e.pathname.endsWith(`/pause`))return{type:`pause-flow`,jobId:s[1]};else if(t.request.method===`POST`&&e.pathname.endsWith(`/cancel`))return{type:`cancel-flow`,jobId:s[1]};return{type:`method-not-allowed`}}else return{type:`not-found`}}),a=(t,n)=>e.Effect.sync(()=>{let e=t.headers||{};e[`Content-Type`]||=`application/json`;for(let[t,r]of Object.entries(e))n.response.set(t,r);return n.response.status(t.status).send(t.body)});function o(e,t){let n=RegExp(`[?&]${t}=([^&]*)`),r=e.match(n);return r?.[1]?decodeURIComponent(r[1]):void 0}const s=new Map,c=(e,t)=>{let n=new URL(e.url||``,`http://${e.headers.host}`),r=`${t}/ws/`;if(!n.pathname.includes(r))return{type:`invalid-path`,expectedPrefix:r};let i=n.pathname.replace(r,``).split(`/`).filter(Boolean),a=i.includes(`upload`),s=i.includes(`flow`),c=o(e.url||``,`jobId`),l=o(e.url||``,`uploadId`);if(!c&&!l&&i.length>=2){let e=i[0],t=i[1];e===`flow`?c=t:e===`upload`&&(l=t)}let u=c||l;return{baseUrl:t,pathname:n.pathname,routeSegments:i,isUploadRoute:a,isFlowRoute:s,jobId:c,uploadId:l,eventId:u,connection:null}},l=async(e,t)=>{try{let n=new URL(e.url||``,`http://${e.headers.host}`).searchParams.get(`token`),r=null;if(r=n?await t({request:{...e,header:t=>t.toLowerCase()===`authorization`?`Bearer ${n}`:e.headers[t.toLowerCase()]},response:{}}):await t({request:e,response:{}}),!r){let e=n?`token`:`cookies`;return{success:!1,error:{message:`Authentication failed: invalid or expired ${e}`,code:4001,authMethod:e}}}return console.log(`WebSocket authenticated for user: ${r.clientId}`),{success:!0,authResult:r}}catch(e){return console.error(`WebSocket auth error:`,e),{success:!1,error:{message:`Authentication error`,code:4001,authMethod:`unknown`}}}},u=(r,i)=>e.Effect.gen(function*(){let a=yield*t.UploadEngine,o=yield*t.FlowEngine;return(t,u)=>{let d=c(u,r);if(console.log(`🔍 WebSocket request details:`,d),`type`in d&&d.type===`invalid-path`){t.send(JSON.stringify({type:`invalid-path`,message:`WebSocket path must start with ${d.expectedPrefix}`,expectedPrefix:d.expectedPrefix})),t.close(1e3,`Invalid path`);return}let f=d,p={id:`conn_${Date.now()}_${Math.random().toString(36).substring(2,11)}`,send:e=>{t.readyState===t.OPEN?(console.log(`📤 Sending WebSocket message to connection ${p.id}:`,e.substring(0,100)),t.send(e)):console.warn(`⚠️ Cannot send message, WebSocket not open. State: ${t.readyState}`)},close:(e,n)=>t.close(e,n),get readyState(){return t.readyState}};f.connection=p,(async()=>{if(i){let e=await l(u,i);if(!e.success){t.send(JSON.stringify({type:`auth-failed`,message:e.error?.message,code:`AUTH_FAILED`,authMethod:e.error?.authMethod})),t.close(e.error?.code||4001,e.error?.message);return}e.authResult&&s.set(p.id,e.authResult)}console.log(`🔍 WebSocket open for eventId:`,f.eventId,`with connection id:`,p.id);let r=(0,n.handleWebSocketOpen)(f,a,o);e.Effect.runFork(r)})(),t.on(`message`,t=>{let r=(0,n.handleWebSocketMessage)(t,p);e.Effect.runFork(r)}),t.on(`close`,()=>{f.connection?.id&&(s.delete(f.connection.id),console.log(`Cleared auth cache for WebSocket connection: ${f.connection.id}`));let t=(0,n.handleWebSocketClose)(f,a,o);e.Effect.runFork(t)}),t.on(`error`,(...t)=>{let r=t[0],i=(0,n.handleWebSocketError)(r,f.eventId);e.Effect.runFork(i)})}}),d=(t={})=>{let{authMiddleware:n}=t;return{extractRequest:i,sendResponse:a,webSocketHandler:({baseUrl:e})=>u(e,n),runAuthMiddleware:n?t=>e.Effect.tryPromise(()=>n(t)).pipe(e.Effect.catchAll(t=>(console.error(`Express auth middleware failed:`,t),e.Effect.succeed(null)))):void 0}};exports.expressAdapter=d;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{Effect as e}from"effect";import{FlowEngine as t,UploadEngine as n}from"@uploadista/core";import{handleWebSocketClose as r,handleWebSocketError as i,handleWebSocketMessage as a,handleWebSocketOpen as o}from"@uploadista/server";const s=async e=>{if(e.body&&typeof e.body==`object`)return e.body;let t=[];for await(let n of e)t.push(n);let n=Buffer.concat(t).toString();return JSON.parse(n)},c=(t,{baseUrl:n})=>e.promise(async()=>{let e=new URL(t.request.url,`http://${t.request.get(`host`)}`),r=t.request.get(`Accept`),i=`/${n}/`;if(e.pathname.startsWith(i)&&t.request.method===`GET`){let t=e.pathname.slice(i.length);if(t===`health`||t===`healthz`)return{type:`health`,acceptHeader:r};if(t===`ready`||t===`readyz`)return{type:`health-ready`,acceptHeader:r};if(t===`health/components`)return{type:`health-components`,acceptHeader:r}}let a=`/${n}/api/`;if(!e.pathname.includes(a))return{type:`not-found`};let o=e.pathname.replace(`${n}/api/`,``).split(`/`).filter(Boolean);if(o[0]===`upload`||o.includes(`upload`))switch(t.request.method){case`POST`:return{type:`create-upload`,data:await s(t.request)};case`GET`:if(o[o.length-1]===`capabilities`){let t=e.searchParams.get(`storageId`),n=o[o.length-2],r=t||(n===`upload`?null:n);return r?{type:`get-capabilities`,storageId:r}:{type:`bad-request`,message:`Storage ID is required`}}return o.length<2?{type:`bad-request`,message:`Upload ID is required`}:{type:`get-upload`,uploadId:o[1]};case`PATCH`:{if(o.length<2)return{type:`bad-request`,message:`Upload ID is required`};let e=new ReadableStream({start(e){t.request.on(`data`,t=>{e.enqueue(t)}),t.request.on(`end`,()=>{e.close()}),t.request.on(`error`,t=>{e.error(t)})}});return{type:`upload-chunk`,uploadId:o[1],data:e}}default:return{type:`method-not-allowed`}}else if(o[0]===`flow`||o.includes(`flow`))switch(t.request.method){case`GET`:return{type:`get-flow`,flowId:o[1]};case`POST`:{let e=await s(t.request);return!e||typeof e!=`object`||!(`inputs`in e)?{type:`bad-request`,message:`Inputs are required`}:{type:`run-flow`,flowId:o[1],storageId:o[2],inputs:e.inputs}}default:return{type:`method-not-allowed`}}else if(o[0]===`dlq`||o.includes(`dlq`))switch(t.request.method){case`GET`:return o.length===1?{type:`dlq-list`,options:{status:e.searchParams.get(`status`),flowId:e.searchParams.get(`flowId`),clientId:e.searchParams.get(`clientId`),limit:e.searchParams.get(`limit`)?Number.parseInt(e.searchParams.get(`limit`)):void 0,offset:e.searchParams.get(`offset`)?Number.parseInt(e.searchParams.get(`offset`)):void 0}}:o[1]===`stats`?{type:`dlq-stats`}:{type:`dlq-get`,itemId:o[1]};case`POST`:return o[1]===`cleanup`?{type:`dlq-cleanup`,options:await s(t.request).catch(()=>({}))}:o[1]===`retry-all`?{type:`dlq-retry-all`,options:await s(t.request).catch(()=>({}))}:o[2]===`retry`?{type:`dlq-retry`,itemId:o[1]}:o[2]===`resolve`?{type:`dlq-resolve`,itemId:o[1]}:{type:`method-not-allowed`};case`DELETE`:return o.length<2?{type:`bad-request`,message:`Item ID is required`}:{type:`dlq-delete`,itemId:o[1]};default:return{type:`method-not-allowed`}}else if(o[0]===`jobs`||o.includes(`jobs`)){if(t.request.method===`GET`&&e.pathname.endsWith(`/status`))return o.length<3?{type:`bad-request`,message:`Job ID is required`}:{type:`job-status`,jobId:o[1]};if(t.request.method===`PATCH`&&o.includes(`resume`)){let e=o[1];if(!e)return{type:`bad-request`,message:`Job ID is required`};let n=o[3];if(!n)return{type:`bad-request`,message:`Node ID is required`};let r=t.request.get(`content-type`),i;if(r?.includes(`application/octet-stream`))i=t.request;else if(r?.includes(`application/json`)){let e=await s(t.request);if(!e||typeof e!=`object`||!(`newData`in e))return{type:`bad-request`,message:`Missing newData`};i=e.newData}else return{type:`unsupported-content-type`};return{type:`resume-flow`,jobId:e,nodeId:n,newData:i}}else if(t.request.method===`POST`&&e.pathname.endsWith(`/pause`))return{type:`pause-flow`,jobId:o[1]};else if(t.request.method===`POST`&&e.pathname.endsWith(`/cancel`))return{type:`cancel-flow`,jobId:o[1]};return{type:`method-not-allowed`}}else return{type:`not-found`}}),l=(t,n)=>e.sync(()=>{let e=t.headers||{};e[`Content-Type`]||=`application/json`;for(let[t,r]of Object.entries(e))n.response.set(t,r);return n.response.status(t.status).send(t.body)});function u(e,t){let n=RegExp(`[?&]${t}=([^&]*)`),r=e.match(n);return r?.[1]?decodeURIComponent(r[1]):void 0}const d=new Map,f=(e,t)=>{let n=new URL(e.url||``,`http://${e.headers.host}`),r=`${t}/ws/`;if(!n.pathname.includes(r))return{type:`invalid-path`,expectedPrefix:r};let i=n.pathname.replace(r,``).split(`/`).filter(Boolean),a=i.includes(`upload`),o=i.includes(`flow`),s=u(e.url||``,`jobId`),c=u(e.url||``,`uploadId`);if(!s&&!c&&i.length>=2){let e=i[0],t=i[1];e===`flow`?s=t:e===`upload`&&(c=t)}let l=s||c;return{baseUrl:t,pathname:n.pathname,routeSegments:i,isUploadRoute:a,isFlowRoute:o,jobId:s,uploadId:c,eventId:l,connection:null}},p=async(e,t)=>{try{let n=new URL(e.url||``,`http://${e.headers.host}`).searchParams.get(`token`),r=null;if(r=n?await t({request:{...e,header:t=>t.toLowerCase()===`authorization`?`Bearer ${n}`:e.headers[t.toLowerCase()]},response:{}}):await t({request:e,response:{}}),!r){let e=n?`token`:`cookies`;return{success:!1,error:{message:`Authentication failed: invalid or expired ${e}`,code:4001,authMethod:e}}}return console.log(`WebSocket authenticated for user: ${r.clientId}`),{success:!0,authResult:r}}catch(e){return console.error(`WebSocket auth error:`,e),{success:!1,error:{message:`Authentication error`,code:4001,authMethod:`unknown`}}}},m=(s,c)=>e.gen(function*(){let l=yield*n,u=yield*t;return(t,n)=>{let m=f(n,s);if(console.log(`🔍 WebSocket request details:`,m),`type`in m&&m.type===`invalid-path`){t.send(JSON.stringify({type:`invalid-path`,message:`WebSocket path must start with ${m.expectedPrefix}`,expectedPrefix:m.expectedPrefix})),t.close(1e3,`Invalid path`);return}let h=m,g={id:`conn_${Date.now()}_${Math.random().toString(36).substring(2,11)}`,send:e=>{t.readyState===t.OPEN?(console.log(`📤 Sending WebSocket message to connection ${g.id}:`,e.substring(0,100)),t.send(e)):console.warn(`⚠️ Cannot send message, WebSocket not open. State: ${t.readyState}`)},close:(e,n)=>t.close(e,n),get readyState(){return t.readyState}};h.connection=g,(async()=>{if(c){let e=await p(n,c);if(!e.success){t.send(JSON.stringify({type:`auth-failed`,message:e.error?.message,code:`AUTH_FAILED`,authMethod:e.error?.authMethod})),t.close(e.error?.code||4001,e.error?.message);return}e.authResult&&d.set(g.id,e.authResult)}console.log(`🔍 WebSocket open for eventId:`,h.eventId,`with connection id:`,g.id);let r=o(h,l,u);e.runFork(r)})(),t.on(`message`,t=>{let n=a(t,g);e.runFork(n)}),t.on(`close`,()=>{h.connection?.id&&(d.delete(h.connection.id),console.log(`Cleared auth cache for WebSocket connection: ${h.connection.id}`));let t=r(h,l,u);e.runFork(t)}),t.on(`error`,(...t)=>{let n=t[0],r=i(n,h.eventId);e.runFork(r)})}}),h=(t={})=>{let{authMiddleware:n}=t;return{extractRequest:c,sendResponse:l,webSocketHandler:({baseUrl:e})=>m(e,n),runAuthMiddleware:n?t=>e.tryPromise(()=>n(t)).pipe(e.catchAll(t=>(console.error(`Express auth middleware failed:`,t),e.succeed(null)))):void 0}};export{h as expressAdapter};
1
+ import{Effect as e}from"effect";import{FlowEngine as t,UploadEngine as n}from"@uploadista/core";import{handleWebSocketClose as r,handleWebSocketError as i,handleWebSocketMessage as a,handleWebSocketOpen as o}from"@uploadista/server";const s=async e=>{if(e.body&&typeof e.body==`object`)return e.body;let t=[];for await(let n of e)t.push(n);let n=Buffer.concat(t).toString();return JSON.parse(n)},c=(t,{baseUrl:n})=>e.promise(async()=>{let e=new URL(t.request.url,`http://${t.request.get(`host`)}`),r=t.request.get(`Accept`),i=`/${n}/`;if(e.pathname.startsWith(i)&&t.request.method===`GET`){let t=e.pathname.slice(i.length);if(t===`health`||t===`healthz`)return{type:`health`,acceptHeader:r};if(t===`ready`||t===`readyz`)return{type:`health-ready`,acceptHeader:r};if(t===`health/components`)return{type:`health-components`,acceptHeader:r}}let a=`/${n}/api/`;if(!e.pathname.includes(a))return{type:`not-found`};let o=e.pathname.replace(`${n}/api/`,``).split(`/`).filter(Boolean);if(o[0]===`upload`||o.includes(`upload`))switch(t.request.method){case`POST`:return{type:`create-upload`,data:await s(t.request)};case`GET`:if(o[o.length-1]===`capabilities`){let t=e.searchParams.get(`storageId`),n=o[o.length-2],r=t||(n===`upload`?null:n);return r?{type:`get-capabilities`,storageId:r}:{type:`bad-request`,message:`Storage ID is required`}}return o.length<2?{type:`bad-request`,message:`Upload ID is required`}:{type:`get-upload`,uploadId:o[1]};case`PATCH`:{if(o.length<2)return{type:`bad-request`,message:`Upload ID is required`};let e=new ReadableStream({start(e){t.request.on(`data`,t=>{e.enqueue(t)}),t.request.on(`end`,()=>{e.close()}),t.request.on(`error`,t=>{e.error(t)})}});return{type:`upload-chunk`,uploadId:o[1],data:e}}default:return{type:`method-not-allowed`}}else if(o[0]===`flow`||o.includes(`flow`))switch(t.request.method){case`GET`:return{type:`get-flow`,flowId:o[1]};case`POST`:{let e=await s(t.request);return!e||typeof e!=`object`||!(`inputs`in e)?{type:`bad-request`,message:`Inputs are required`}:{type:`run-flow`,flowId:o[1],storageId:o[2],inputs:e.inputs}}default:return{type:`method-not-allowed`}}else if(o[0]===`dlq`||o.includes(`dlq`))switch(t.request.method){case`GET`:return o.length===1?{type:`dlq-list`,options:{status:e.searchParams.get(`status`),flowId:e.searchParams.get(`flowId`),clientId:e.searchParams.get(`clientId`),limit:e.searchParams.get(`limit`)?Number.parseInt(e.searchParams.get(`limit`),10):void 0,offset:e.searchParams.get(`offset`)?Number.parseInt(e.searchParams.get(`offset`),10):void 0}}:o[1]===`stats`?{type:`dlq-stats`}:{type:`dlq-get`,itemId:o[1]};case`POST`:return o[1]===`cleanup`?{type:`dlq-cleanup`,options:await s(t.request).catch(()=>({}))}:o[1]===`retry-all`?{type:`dlq-retry-all`,options:await s(t.request).catch(()=>({}))}:o[2]===`retry`?{type:`dlq-retry`,itemId:o[1]}:o[2]===`resolve`?{type:`dlq-resolve`,itemId:o[1]}:{type:`method-not-allowed`};case`DELETE`:return o.length<2?{type:`bad-request`,message:`Item ID is required`}:{type:`dlq-delete`,itemId:o[1]};default:return{type:`method-not-allowed`}}else if(o[0]===`jobs`||o.includes(`jobs`)){if(t.request.method===`GET`&&e.pathname.endsWith(`/status`))return o.length<3?{type:`bad-request`,message:`Job ID is required`}:{type:`job-status`,jobId:o[1]};if(t.request.method===`PATCH`&&o.includes(`resume`)){let e=o[1];if(!e)return{type:`bad-request`,message:`Job ID is required`};let n=o[3];if(!n)return{type:`bad-request`,message:`Node ID is required`};let r=t.request.get(`content-type`),i;if(r?.includes(`application/octet-stream`))i=t.request;else if(r?.includes(`application/json`)){let e=await s(t.request);if(!e||typeof e!=`object`||!(`newData`in e))return{type:`bad-request`,message:`Missing newData`};i=e.newData}else return{type:`unsupported-content-type`};return{type:`resume-flow`,jobId:e,nodeId:n,newData:i}}else if(t.request.method===`POST`&&e.pathname.endsWith(`/pause`))return{type:`pause-flow`,jobId:o[1]};else if(t.request.method===`POST`&&e.pathname.endsWith(`/cancel`))return{type:`cancel-flow`,jobId:o[1]};return{type:`method-not-allowed`}}else return{type:`not-found`}}),l=(t,n)=>e.sync(()=>{let e=t.headers||{};e[`Content-Type`]||=`application/json`;for(let[t,r]of Object.entries(e))n.response.set(t,r);return n.response.status(t.status).send(t.body)});function u(e,t){let n=RegExp(`[?&]${t}=([^&]*)`),r=e.match(n);return r?.[1]?decodeURIComponent(r[1]):void 0}const d=new Map,f=(e,t)=>{let n=new URL(e.url||``,`http://${e.headers.host}`),r=`${t}/ws/`;if(!n.pathname.includes(r))return{type:`invalid-path`,expectedPrefix:r};let i=n.pathname.replace(r,``).split(`/`).filter(Boolean),a=i.includes(`upload`),o=i.includes(`flow`),s=u(e.url||``,`jobId`),c=u(e.url||``,`uploadId`);if(!s&&!c&&i.length>=2){let e=i[0],t=i[1];e===`flow`?s=t:e===`upload`&&(c=t)}let l=s||c;return{baseUrl:t,pathname:n.pathname,routeSegments:i,isUploadRoute:a,isFlowRoute:o,jobId:s,uploadId:c,eventId:l,connection:null}},p=async(e,t)=>{try{let n=new URL(e.url||``,`http://${e.headers.host}`).searchParams.get(`token`),r=null;if(r=n?await t({request:{...e,header:t=>t.toLowerCase()===`authorization`?`Bearer ${n}`:e.headers[t.toLowerCase()]},response:{}}):await t({request:e,response:{}}),!r){let e=n?`token`:`cookies`;return{success:!1,error:{message:`Authentication failed: invalid or expired ${e}`,code:4001,authMethod:e}}}return console.log(`WebSocket authenticated for user: ${r.clientId}`),{success:!0,authResult:r}}catch(e){return console.error(`WebSocket auth error:`,e),{success:!1,error:{message:`Authentication error`,code:4001,authMethod:`unknown`}}}},m=(s,c)=>e.gen(function*(){let l=yield*n,u=yield*t;return(t,n)=>{let m=f(n,s);if(console.log(`🔍 WebSocket request details:`,m),`type`in m&&m.type===`invalid-path`){t.send(JSON.stringify({type:`invalid-path`,message:`WebSocket path must start with ${m.expectedPrefix}`,expectedPrefix:m.expectedPrefix})),t.close(1e3,`Invalid path`);return}let h=m,g={id:`conn_${Date.now()}_${Math.random().toString(36).substring(2,11)}`,send:e=>{t.readyState===t.OPEN?(console.log(`📤 Sending WebSocket message to connection ${g.id}:`,e.substring(0,100)),t.send(e)):console.warn(`⚠️ Cannot send message, WebSocket not open. State: ${t.readyState}`)},close:(e,n)=>t.close(e,n),get readyState(){return t.readyState}};h.connection=g,(async()=>{if(c){let e=await p(n,c);if(!e.success){t.send(JSON.stringify({type:`auth-failed`,message:e.error?.message,code:`AUTH_FAILED`,authMethod:e.error?.authMethod})),t.close(e.error?.code||4001,e.error?.message);return}e.authResult&&d.set(g.id,e.authResult)}console.log(`🔍 WebSocket open for eventId:`,h.eventId,`with connection id:`,g.id);let r=o(h,l,u);e.runFork(r)})(),t.on(`message`,t=>{let n=a(t,g);e.runFork(n)}),t.on(`close`,()=>{h.connection?.id&&(d.delete(h.connection.id),console.log(`Cleared auth cache for WebSocket connection: ${h.connection.id}`));let t=r(h,l,u);e.runFork(t)}),t.on(`error`,(...t)=>{let n=t[0],r=i(n,h.eventId);e.runFork(r)})}}),h=(t={})=>{let{authMiddleware:n}=t;return{extractRequest:c,sendResponse:l,webSocketHandler:({baseUrl:e})=>m(e,n),runAuthMiddleware:n?t=>e.tryPromise(()=>n(t)).pipe(e.catchAll(t=>(console.error(`Express auth middleware failed:`,t),e.succeed(null)))):void 0}};export{h as expressAdapter};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["chunks: Buffer[]","newData: unknown","authResult: AuthResult | null","connection: WebSocketConnection"],"sources":["../src/express-http-handler.ts","../src/express-websocket-handler.ts","../src/express-adapter.ts"],"sourcesContent":["import type { UploadistaRequest, UploadistaResponse } from \"@uploadista/server\";\nimport { Effect } from \"effect\";\nimport type { ExpressContext } from \"./express-adapter\";\n\n/**\n * Helper to parse JSON body if not already parsed\n */\nconst parseJsonBody = async (\n req: ExpressContext[\"request\"],\n): Promise<unknown> => {\n // If body is already parsed, return it\n if (req.body && typeof req.body === \"object\") {\n return req.body;\n }\n\n // Manually parse JSON body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk as Buffer);\n }\n const body = Buffer.concat(chunks).toString();\n return JSON.parse(body);\n};\n\nexport const extractExpressRequest = (\n ctx: ExpressContext,\n { baseUrl }: { baseUrl: string },\n) => {\n // Run the routing logic as an Effect program\n return Effect.promise(async () => {\n // Get request details\n const url = new URL(ctx.request.url, `http://${ctx.request.get(\"host\")}`);\n const acceptHeader = ctx.request.get(\"Accept\");\n\n // Check for health check endpoints first (at /{baseUrl}/health, not under /api/)\n const healthPrefix = `/${baseUrl}/`;\n if (url.pathname.startsWith(healthPrefix) && ctx.request.method === \"GET\") {\n const healthPath = url.pathname.slice(healthPrefix.length);\n\n // /health or /healthz - Liveness probe\n if (healthPath === \"health\" || healthPath === \"healthz\") {\n return {\n type: \"health\",\n acceptHeader,\n } as UploadistaRequest;\n }\n\n // /ready or /readyz - Readiness probe\n if (healthPath === \"ready\" || healthPath === \"readyz\") {\n return {\n type: \"health-ready\",\n acceptHeader,\n } as UploadistaRequest;\n }\n\n // /health/components - Detailed component status\n if (healthPath === \"health/components\") {\n return {\n type: \"health-components\",\n acceptHeader,\n } as UploadistaRequest;\n }\n }\n\n // Check for baseUrl/api/ prefix for other routes\n const expectedPrefix = `/${baseUrl}/api/`;\n if (!url.pathname.includes(expectedPrefix)) {\n return {\n type: \"not-found\",\n } as UploadistaRequest;\n }\n\n // Remove the prefix and get the actual route segments\n const routeSegments = url.pathname\n .replace(`${baseUrl}/api/`, \"\")\n .split(\"/\")\n .filter(Boolean);\n\n // Route based on first segment\n if (routeSegments[0] === \"upload\" || routeSegments.includes(\"upload\")) {\n switch (ctx.request.method) {\n case \"POST\": {\n // Parse JSON body if not already parsed\n const data = await parseJsonBody(ctx.request);\n return {\n type: \"create-upload\",\n data,\n } as UploadistaRequest;\n }\n case \"GET\": {\n const lastSegment = routeSegments[routeSegments.length - 1];\n\n if (lastSegment === \"capabilities\") {\n const storageId = url.searchParams.get(\"storageId\");\n const storageIdFromPath = routeSegments[routeSegments.length - 2];\n\n // Only use path segment if it's not \"upload\"\n const finalStorageId =\n storageId || (storageIdFromPath !== \"upload\" ? storageIdFromPath : null);\n\n if (!finalStorageId) {\n return {\n type: \"bad-request\",\n message: \"Storage ID is required\",\n } as UploadistaRequest;\n }\n return {\n type: \"get-capabilities\",\n storageId: finalStorageId,\n } as UploadistaRequest;\n }\n if (routeSegments.length < 2) {\n return {\n type: \"bad-request\",\n message: \"Upload ID is required\",\n } as UploadistaRequest;\n }\n return {\n type: \"get-upload\",\n uploadId: routeSegments[1],\n } as UploadistaRequest;\n }\n case \"PATCH\": {\n if (routeSegments.length < 2) {\n return {\n type: \"bad-request\",\n message: \"Upload ID is required\",\n } as UploadistaRequest;\n }\n // Convert Node.js Readable stream to web ReadableStream\n const body = new ReadableStream({\n start(controller) {\n ctx.request.on(\"data\", (chunk: Buffer) => {\n controller.enqueue(chunk);\n });\n ctx.request.on(\"end\", () => {\n controller.close();\n });\n ctx.request.on(\"error\", (error: Error) => {\n controller.error(error);\n });\n },\n });\n\n return {\n type: \"upload-chunk\",\n uploadId: routeSegments[1],\n data: body,\n } as UploadistaRequest;\n }\n default:\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n }\n } else if (routeSegments[0] === \"flow\" || routeSegments.includes(\"flow\")) {\n switch (ctx.request.method) {\n case \"GET\":\n return {\n type: \"get-flow\",\n flowId: routeSegments[1],\n } as UploadistaRequest;\n case \"POST\": {\n // Parse JSON body if not already parsed\n const params = await parseJsonBody(ctx.request);\n if (!params || typeof params !== \"object\" || !(\"inputs\" in params)) {\n return {\n type: \"bad-request\",\n message: \"Inputs are required\",\n } as UploadistaRequest;\n }\n return {\n type: \"run-flow\",\n flowId: routeSegments[1],\n storageId: routeSegments[2],\n inputs: (params as { inputs: unknown }).inputs,\n } as UploadistaRequest;\n }\n default:\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n }\n } else if (routeSegments[0] === \"dlq\" || routeSegments.includes(\"dlq\")) {\n // DLQ Admin routes: /api/dlq, /api/dlq/:itemId, /api/dlq/:itemId/retry, etc.\n switch (ctx.request.method) {\n case \"GET\": {\n if (routeSegments.length === 1) {\n // GET /api/dlq - List DLQ items\n const status = url.searchParams.get(\"status\") as string | undefined;\n const flowId = url.searchParams.get(\"flowId\") as string | undefined;\n const clientId = url.searchParams.get(\"clientId\") as\n | string\n | undefined;\n const limit = url.searchParams.get(\"limit\")\n ? Number.parseInt(url.searchParams.get(\"limit\") as string)\n : undefined;\n const offset = url.searchParams.get(\"offset\")\n ? Number.parseInt(url.searchParams.get(\"offset\") as string)\n : undefined;\n return {\n type: \"dlq-list\",\n options: { status, flowId, clientId, limit, offset },\n } as UploadistaRequest;\n }\n if (routeSegments[1] === \"stats\") {\n // GET /api/dlq/stats - Get DLQ statistics\n return {\n type: \"dlq-stats\",\n } as UploadistaRequest;\n }\n // GET /api/dlq/:itemId - Get specific DLQ item\n return {\n type: \"dlq-get\",\n itemId: routeSegments[1],\n } as UploadistaRequest;\n }\n case \"POST\": {\n if (routeSegments[1] === \"cleanup\") {\n // POST /api/dlq/cleanup - Cleanup old items\n const body = await parseJsonBody(ctx.request).catch(() => ({}));\n return {\n type: \"dlq-cleanup\",\n options: body,\n } as UploadistaRequest;\n }\n if (routeSegments[1] === \"retry-all\") {\n // POST /api/dlq/retry-all - Retry all matching items\n const body = await parseJsonBody(ctx.request).catch(() => ({}));\n return {\n type: \"dlq-retry-all\",\n options: body,\n } as UploadistaRequest;\n }\n if (routeSegments[2] === \"retry\") {\n // POST /api/dlq/:itemId/retry - Retry specific item\n return {\n type: \"dlq-retry\",\n itemId: routeSegments[1],\n } as UploadistaRequest;\n }\n if (routeSegments[2] === \"resolve\") {\n // POST /api/dlq/:itemId/resolve - Manually resolve item\n return {\n type: \"dlq-resolve\",\n itemId: routeSegments[1],\n } as UploadistaRequest;\n }\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n }\n case \"DELETE\": {\n // DELETE /api/dlq/:itemId - Delete a DLQ item\n if (routeSegments.length < 2) {\n return {\n type: \"bad-request\",\n message: \"Item ID is required\",\n } as UploadistaRequest;\n }\n return {\n type: \"dlq-delete\",\n itemId: routeSegments[1],\n } as UploadistaRequest;\n }\n default:\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n }\n } else if (routeSegments[0] === \"jobs\" || routeSegments.includes(\"jobs\")) {\n if (ctx.request.method === \"GET\" && url.pathname.endsWith(\"/status\")) {\n // Need at least 3 segments: jobs, jobId, status\n if (routeSegments.length < 3) {\n return {\n type: \"bad-request\",\n message: \"Job ID is required\",\n } as UploadistaRequest;\n }\n const jobId = routeSegments[1];\n return {\n type: \"job-status\",\n jobId,\n } as UploadistaRequest;\n } else if (\n ctx.request.method === \"PATCH\" &&\n routeSegments.includes(\"resume\")\n ) {\n const jobId = routeSegments[1];\n if (!jobId) {\n return {\n type: \"bad-request\",\n message: \"Job ID is required\",\n } as UploadistaRequest;\n }\n const nodeId = routeSegments[3];\n if (!nodeId) {\n return {\n type: \"bad-request\",\n message: \"Node ID is required\",\n } as UploadistaRequest;\n }\n\n const contentType = ctx.request.get(\"content-type\");\n let newData: unknown;\n\n // Handle different content types\n if (contentType?.includes(\"application/octet-stream\")) {\n // For streaming data, pass the req object (Express handles streams)\n // Express doesn't expose ReadableStream like Hono, use req itself\n newData = ctx.request;\n } else if (contentType?.includes(\"application/json\")) {\n // Parse JSON body if not already parsed\n const body = await parseJsonBody(ctx.request);\n\n if (!body || typeof body !== \"object\" || !(\"newData\" in body)) {\n return {\n type: \"bad-request\",\n message: \"Missing newData\",\n } as UploadistaRequest;\n }\n\n newData = (body as { newData: unknown }).newData;\n } else {\n return {\n type: \"unsupported-content-type\",\n } as UploadistaRequest;\n }\n\n return {\n type: \"resume-flow\",\n jobId,\n nodeId,\n newData,\n } as UploadistaRequest;\n } else if (\n ctx.request.method === \"POST\" &&\n url.pathname.endsWith(\"/pause\")\n ) {\n return {\n type: \"pause-flow\",\n jobId: routeSegments[1],\n } as UploadistaRequest;\n } else if (\n ctx.request.method === \"POST\" &&\n url.pathname.endsWith(\"/cancel\")\n ) {\n return {\n type: \"cancel-flow\",\n jobId: routeSegments[1],\n } as UploadistaRequest;\n }\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n } else {\n return {\n type: \"not-found\",\n } as UploadistaRequest;\n }\n });\n};\n\nexport const sendExpressResponse = (\n response: UploadistaResponse,\n ctx: ExpressContext,\n) =>\n Effect.sync(() => {\n // Set default Content-Type header if not provided\n const headers = response.headers || {};\n if (!headers[\"Content-Type\"]) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n // Set headers\n for (const [key, value] of Object.entries(headers)) {\n ctx.response.set(key, value);\n }\n\n return ctx.response.status(response.status).send(response.body);\n });\n","import type { IncomingMessage } from \"node:http\";\nimport { FlowEngine, UploadEngine } from \"@uploadista/core\";\nimport type { AuthResult } from \"@uploadista/server\";\nimport {\n handleWebSocketClose,\n handleWebSocketError,\n handleWebSocketMessage,\n handleWebSocketOpen,\n type WebSocketConnection,\n type WebSocketConnectionRequest,\n} from \"@uploadista/server\";\nimport { Effect } from \"effect\";\nimport type { Request, Response } from \"express\";\nimport type { WebSocket } from \"ws\";\nimport type { ExpressContext } from \"./express-adapter\";\n\nexport type ExpressWebSocketHandler = (\n ws: WebSocket,\n req: IncomingMessage,\n) => void;\n\nfunction extractQueryParam(url: string, param: string): string | undefined {\n const regex = new RegExp(`[?&]${param}=([^&]*)`);\n const match = url.match(regex);\n return match?.[1] ? decodeURIComponent(match[1]) : undefined;\n}\n\n/**\n * Cache for storing auth context per WebSocket connection\n */\nconst wsAuthCache = new Map<string, AuthResult>();\n\n/**\n * Extracts WebSocket connection request details from Express/Node.js request\n */\nconst extractWebSocketRequest = (\n req: IncomingMessage,\n baseUrl: string,\n):\n | WebSocketConnectionRequest\n | { type: \"invalid-path\"; expectedPrefix: string } => {\n const url = new URL(req.url || \"\", `http://${req.headers.host}`);\n const expectedPrefix = `${baseUrl}/ws/`;\n\n // Check for ws/uploadista prefix\n if (!url.pathname.includes(expectedPrefix)) {\n return {\n type: \"invalid-path\",\n expectedPrefix,\n };\n }\n\n // Remove the prefix and get the actual route segments\n const routeSegments = url.pathname\n .replace(expectedPrefix, \"\")\n .split(\"/\")\n .filter(Boolean);\n\n const isUploadRoute = routeSegments.includes(\"upload\");\n const isFlowRoute = routeSegments.includes(\"flow\");\n\n // Extract jobId and uploadId from URL path or query parameters\n // Path format: /uploadista/ws/flow/{jobId} or /uploadista/ws/upload/{uploadId}\n let jobId = extractQueryParam(req.url || \"\", \"jobId\");\n let uploadId = extractQueryParam(req.url || \"\", \"uploadId\");\n\n // If not in query params, extract from path segments\n if (!jobId && !uploadId && routeSegments.length >= 2) {\n const routeType = routeSegments[0]; // 'flow' or 'upload'\n const id = routeSegments[1]; // the actual ID\n\n if (routeType === \"flow\") {\n jobId = id;\n } else if (routeType === \"upload\") {\n uploadId = id;\n }\n }\n\n // Use jobId if available, otherwise use uploadId\n const eventId = jobId || uploadId;\n\n return {\n baseUrl,\n pathname: url.pathname,\n routeSegments,\n isUploadRoute,\n isFlowRoute,\n jobId,\n uploadId,\n eventId,\n // Connection will be set when WebSocket opens\n connection: null as unknown as WebSocketConnection,\n };\n};\n\n/**\n * Authenticates a WebSocket connection using the provided auth middleware\n */\nconst authenticateWebSocket = async (\n req: IncomingMessage,\n authMiddleware: (ctx: ExpressContext) => Promise<AuthResult>,\n): Promise<{\n success: boolean;\n authResult?: AuthResult;\n error?: { message: string; code: number; authMethod: string };\n}> => {\n try {\n // Extract token from query parameter\n const url = new URL(req.url || \"\", `http://${req.headers.host}`);\n const token = url.searchParams.get(\"token\");\n\n let authResult: AuthResult | null = null;\n\n if (token) {\n // Token-based authentication\n // Create a mock request with Authorization header\n const mockReq = {\n ...req,\n header: (name: string) => {\n if (name.toLowerCase() === \"authorization\") {\n return `Bearer ${token}`;\n }\n return req.headers[name.toLowerCase()];\n },\n } as unknown as Request;\n\n authResult = await authMiddleware({\n request: mockReq as unknown as Request,\n response: {} as Response,\n });\n } else {\n // Cookie-based authentication\n // Pass the original request so auth middleware can read cookies\n authResult = await authMiddleware({\n request: req as unknown as Request,\n response: {} as Response,\n });\n }\n\n if (!authResult) {\n const authMethod = token ? \"token\" : \"cookies\";\n return {\n success: false,\n error: {\n message: `Authentication failed: invalid or expired ${authMethod}`,\n code: 4001,\n authMethod,\n },\n };\n }\n\n console.log(`WebSocket authenticated for user: ${authResult.clientId}`);\n return { success: true, authResult };\n } catch (error) {\n console.error(\"WebSocket auth error:\", error);\n return {\n success: false,\n error: {\n message: \"Authentication error\",\n code: 4001,\n authMethod: \"unknown\",\n },\n };\n }\n};\n\n/**\n * Creates an Express WebSocket handler that delegates to core WebSocket handlers\n */\nexport const expressWebSocketHandler = (\n baseUrl: string,\n authMiddleware?: (ctx: ExpressContext) => Promise<AuthResult>,\n): Effect.Effect<ExpressWebSocketHandler, never, UploadEngine | FlowEngine> => {\n return Effect.gen(function* () {\n // Get the server instances from the Effect context\n const uploadEngine = yield* UploadEngine;\n const flowEngine = yield* FlowEngine;\n\n return (ws: WebSocket, req: IncomingMessage) => {\n // Extract request details (adapter's responsibility)\n const requestOrError = extractWebSocketRequest(req, baseUrl);\n\n console.log(\"🔍 WebSocket request details:\", requestOrError);\n\n // Handle invalid path\n if (\"type\" in requestOrError && requestOrError.type === \"invalid-path\") {\n ws.send(\n JSON.stringify({\n type: \"invalid-path\",\n message: `WebSocket path must start with ${requestOrError.expectedPrefix}`,\n expectedPrefix: requestOrError.expectedPrefix,\n }),\n );\n ws.close(1000, \"Invalid path\");\n return;\n }\n\n // Type narrowing: at this point, requestOrError is WebSocketConnectionRequest\n const request = requestOrError as WebSocketConnectionRequest;\n\n // Create framework-agnostic connection wrapper\n // Use a getter for readyState so it always reflects the current state\n const connection: WebSocketConnection = {\n id: `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,\n send: (data: string) => {\n if (ws.readyState === ws.OPEN) {\n console.log(\n `📤 Sending WebSocket message to connection ${connection.id}:`,\n data.substring(0, 100),\n );\n ws.send(data);\n } else {\n console.warn(\n `⚠️ Cannot send message, WebSocket not open. State: ${ws.readyState}`,\n );\n }\n },\n close: (code?: number, reason?: string) => ws.close(code, reason),\n get readyState() {\n return ws.readyState;\n },\n };\n\n // Update request with connection\n request.connection = connection;\n\n // Handle WebSocket open\n (async () => {\n // Validate authentication if auth middleware is configured\n if (authMiddleware) {\n const authResult = await authenticateWebSocket(req, authMiddleware);\n\n if (!authResult.success) {\n ws.send(\n JSON.stringify({\n type: \"auth-failed\",\n message: authResult.error?.message,\n code: \"AUTH_FAILED\",\n authMethod: authResult.error?.authMethod,\n }),\n );\n ws.close(authResult.error?.code || 4001, authResult.error?.message);\n return;\n }\n\n // Cache auth context for this connection\n if (authResult.authResult) {\n wsAuthCache.set(connection.id, authResult.authResult);\n }\n }\n\n console.log(\n \"🔍 WebSocket open for eventId:\",\n request.eventId,\n \"with connection id:\",\n connection.id,\n );\n\n // Delegate to core handler for business logic\n const openEffect = handleWebSocketOpen(\n request,\n uploadEngine,\n flowEngine,\n );\n Effect.runFork(openEffect);\n })();\n\n // Handle WebSocket message\n ws.on(\"message\", (data: unknown) => {\n const messageEffect = handleWebSocketMessage(\n data as string,\n connection,\n );\n Effect.runFork(messageEffect);\n });\n\n // Handle WebSocket close\n ws.on(\"close\", () => {\n // Clear cached auth context for this connection\n if (request.connection?.id) {\n wsAuthCache.delete(request.connection.id);\n console.log(\n `Cleared auth cache for WebSocket connection: ${request.connection.id}`,\n );\n }\n\n // Delegate to core handler for cleanup\n const closeEffect = handleWebSocketClose(\n request,\n uploadEngine,\n flowEngine,\n );\n Effect.runFork(closeEffect);\n });\n\n // Handle WebSocket error\n ws.on(\"error\", (...args: unknown[]) => {\n const error = args[0] as Error;\n const errorEffect = handleWebSocketError(error, request.eventId);\n Effect.runFork(errorEffect);\n });\n };\n });\n};\n","import type { AuthResult, ServerAdapter } from \"@uploadista/server\";\nimport { Effect } from \"effect\";\nimport type { Request, Response } from \"express\";\nimport {\n extractExpressRequest,\n sendExpressResponse,\n} from \"./express-http-handler\";\nimport {\n type ExpressWebSocketHandler,\n expressWebSocketHandler,\n} from \"./express-websocket-handler\";\n\nexport type ExpressContext = {\n request: Request;\n response: Response;\n next?: (error?: Error) => void;\n};\n\n/**\n * Options for creating an Express server adapter.\n */\nexport interface ExpressAdapterOptions {\n /**\n * Optional authentication middleware function.\n * Called for each request to authenticate the user.\n *\n * @param ctx - Express context\n * @returns Promise resolving to AuthResult (AuthContext or null)\n */\n authMiddleware?: (ctx: ExpressContext) => Promise<AuthResult>;\n}\n\n// WebSocket interface from ws package\nexport interface WebSocket {\n readyState: number;\n OPEN: number;\n send: (data: string) => void;\n close: (code?: number, reason?: string) => void;\n on: (event: string, listener: (...args: unknown[]) => void) => void;\n off: (event: string, listener: (...args: unknown[]) => void) => void;\n}\n\n/**\n * Creates an Express server adapter that implements the ServerAdapter interface.\n *\n * This adapter translates between Express's Request/Response API and the core server's\n * standard request/response model. It supports:\n * - Request extraction from Express Request\n * - Response sending via Express Response\n * - Optional authentication middleware\n * - WebSocket handling\n *\n * @param options - Adapter configuration options\n * @returns ServerAdapter implementation for Express\n *\n * @example\n * ```typescript\n * import { expressAdapter } from \"@uploadista/adapters-express\";\n * import { createUploadistaServer } from \"@uploadista/server\";\n *\n * const adapter = expressAdapter({\n * authMiddleware: async (req, res) => {\n * const userId = req.header(\"x-user-id\");\n * return userId ? { clientId: userId } : null;\n * }\n * });\n *\n * const server = await createUploadistaServer({\n * flows: getFlows,\n * dataStore: { type: \"s3\", config: { bucket: \"uploads\" } },\n * kvStore: redisKvStore,\n * eventEmitter: webSocketEventEmitter(),\n * adapter\n * });\n * ```\n */\nexport const expressAdapter = (\n options: ExpressAdapterOptions = {},\n): ServerAdapter<ExpressContext, Response, ExpressWebSocketHandler> => {\n const { authMiddleware } = options;\n\n return {\n /**\n * Extract standard request details from Express Request.\n *\n * Converts Express's Request into a framework-agnostic UploadistaRequest\n * by parsing the URL, headers, and body.\n */\n extractRequest: extractExpressRequest,\n\n /**\n * Send standard response using Express Response format.\n *\n * Converts a UploadistaResponse into an object with status, headers, and body\n * that the Express handler can send.\n */\n sendResponse: sendExpressResponse,\n\n /**\n * WebSocket handler for Express with ws package.\n */\n webSocketHandler: ({ baseUrl }: { baseUrl: string }) =>\n expressWebSocketHandler(baseUrl, authMiddleware),\n\n /**\n * Run framework-specific auth middleware.\n *\n * If provided, executes the Express-specific authentication middleware\n * with access to the full Express Request and Response objects.\n */\n runAuthMiddleware: authMiddleware\n ? (ctx: ExpressContext) =>\n Effect.tryPromise(() => authMiddleware(ctx)).pipe(\n Effect.catchAll((error) => {\n console.error(\"Express auth middleware failed:\", error);\n // Return null to indicate auth failure (not an error in the Effect sense)\n return Effect.succeed(null);\n }),\n )\n : undefined,\n };\n};\n"],"mappings":"yOAOA,MAAM,EAAgB,KACpB,IACqB,CAErB,GAAI,EAAI,MAAQ,OAAO,EAAI,MAAS,SAClC,OAAO,EAAI,KAIb,IAAMA,EAAmB,EAAE,CAC3B,UAAW,IAAM,KAAS,EACxB,EAAO,KAAK,EAAgB,CAE9B,IAAM,EAAO,OAAO,OAAO,EAAO,CAAC,UAAU,CAC7C,OAAO,KAAK,MAAM,EAAK,EAGZ,GACX,EACA,CAAE,aAGK,EAAO,QAAQ,SAAY,CAEhC,IAAM,EAAM,IAAI,IAAI,EAAI,QAAQ,IAAK,UAAU,EAAI,QAAQ,IAAI,OAAO,GAAG,CACnE,EAAe,EAAI,QAAQ,IAAI,SAAS,CAGxC,EAAe,IAAI,EAAQ,GACjC,GAAI,EAAI,SAAS,WAAW,EAAa,EAAI,EAAI,QAAQ,SAAW,MAAO,CACzE,IAAM,EAAa,EAAI,SAAS,MAAM,EAAa,OAAO,CAG1D,GAAI,IAAe,UAAY,IAAe,UAC5C,MAAO,CACL,KAAM,SACN,eACD,CAIH,GAAI,IAAe,SAAW,IAAe,SAC3C,MAAO,CACL,KAAM,eACN,eACD,CAIH,GAAI,IAAe,oBACjB,MAAO,CACL,KAAM,oBACN,eACD,CAKL,IAAM,EAAiB,IAAI,EAAQ,OACnC,GAAI,CAAC,EAAI,SAAS,SAAS,EAAe,CACxC,MAAO,CACL,KAAM,YACP,CAIH,IAAM,EAAgB,EAAI,SACvB,QAAQ,GAAG,EAAQ,OAAQ,GAAG,CAC9B,MAAM,IAAI,CACV,OAAO,QAAQ,CAGlB,GAAI,EAAc,KAAO,UAAY,EAAc,SAAS,SAAS,CACnE,OAAQ,EAAI,QAAQ,OAApB,CACE,IAAK,OAGH,MAAO,CACL,KAAM,gBACN,KAHW,MAAM,EAAc,EAAI,QAAQ,CAI5C,CAEH,IAAK,MAGH,GAFoB,EAAc,EAAc,OAAS,KAErC,eAAgB,CAClC,IAAM,EAAY,EAAI,aAAa,IAAI,YAAY,CAC7C,EAAoB,EAAc,EAAc,OAAS,GAGzD,EACJ,IAAc,IAAsB,SAA+B,KAApB,GAQjD,OANK,EAME,CACL,KAAM,mBACN,UAAW,EACZ,CARQ,CACL,KAAM,cACN,QAAS,yBACV,CAaL,OANI,EAAc,OAAS,EAClB,CACL,KAAM,cACN,QAAS,wBACV,CAEI,CACL,KAAM,aACN,SAAU,EAAc,GACzB,CAEH,IAAK,QAAS,CACZ,GAAI,EAAc,OAAS,EACzB,MAAO,CACL,KAAM,cACN,QAAS,wBACV,CAGH,IAAM,EAAO,IAAI,eAAe,CAC9B,MAAM,EAAY,CAChB,EAAI,QAAQ,GAAG,OAAS,GAAkB,CACxC,EAAW,QAAQ,EAAM,EACzB,CACF,EAAI,QAAQ,GAAG,UAAa,CAC1B,EAAW,OAAO,EAClB,CACF,EAAI,QAAQ,GAAG,QAAU,GAAiB,CACxC,EAAW,MAAM,EAAM,EACvB,EAEL,CAAC,CAEF,MAAO,CACL,KAAM,eACN,SAAU,EAAc,GACxB,KAAM,EACP,CAEH,QACE,MAAO,CACL,KAAM,qBACP,SAEI,EAAc,KAAO,QAAU,EAAc,SAAS,OAAO,CACtE,OAAQ,EAAI,QAAQ,OAApB,CACE,IAAK,MACH,MAAO,CACL,KAAM,WACN,OAAQ,EAAc,GACvB,CACH,IAAK,OAAQ,CAEX,IAAM,EAAS,MAAM,EAAc,EAAI,QAAQ,CAO/C,MANI,CAAC,GAAU,OAAO,GAAW,UAAY,EAAE,WAAY,GAClD,CACL,KAAM,cACN,QAAS,sBACV,CAEI,CACL,KAAM,WACN,OAAQ,EAAc,GACtB,UAAW,EAAc,GACzB,OAAS,EAA+B,OACzC,CAEH,QACE,MAAO,CACL,KAAM,qBACP,SAEI,EAAc,KAAO,OAAS,EAAc,SAAS,MAAM,CAEpE,OAAQ,EAAI,QAAQ,OAApB,CACE,IAAK,MA0BH,OAzBI,EAAc,SAAW,EAapB,CACL,KAAM,WACN,QAAS,CAAE,OAbE,EAAI,aAAa,IAAI,SAAS,CAaxB,OAZN,EAAI,aAAa,IAAI,SAAS,CAYhB,SAXZ,EAAI,aAAa,IAAI,WAAW,CAWV,MARzB,EAAI,aAAa,IAAI,QAAQ,CACvC,OAAO,SAAS,EAAI,aAAa,IAAI,QAAQ,CAAW,CACxD,IAAA,GAM0C,OAL/B,EAAI,aAAa,IAAI,SAAS,CACzC,OAAO,SAAS,EAAI,aAAa,IAAI,SAAS,CAAW,CACzD,IAAA,GAGkD,CACrD,CAEC,EAAc,KAAO,QAEhB,CACL,KAAM,YACP,CAGI,CACL,KAAM,UACN,OAAQ,EAAc,GACvB,CAEH,IAAK,OA+BH,OA9BI,EAAc,KAAO,UAGhB,CACL,KAAM,cACN,QAHW,MAAM,EAAc,EAAI,QAAQ,CAAC,WAAa,EAAE,EAAE,CAI9D,CAEC,EAAc,KAAO,YAGhB,CACL,KAAM,gBACN,QAHW,MAAM,EAAc,EAAI,QAAQ,CAAC,WAAa,EAAE,EAAE,CAI9D,CAEC,EAAc,KAAO,QAEhB,CACL,KAAM,YACN,OAAQ,EAAc,GACvB,CAEC,EAAc,KAAO,UAEhB,CACL,KAAM,cACN,OAAQ,EAAc,GACvB,CAEI,CACL,KAAM,qBACP,CAEH,IAAK,SAQH,OANI,EAAc,OAAS,EAClB,CACL,KAAM,cACN,QAAS,sBACV,CAEI,CACL,KAAM,aACN,OAAQ,EAAc,GACvB,CAEH,QACE,MAAO,CACL,KAAM,qBACP,SAEI,EAAc,KAAO,QAAU,EAAc,SAAS,OAAO,CAAE,CACxE,GAAI,EAAI,QAAQ,SAAW,OAAS,EAAI,SAAS,SAAS,UAAU,CASlE,OAPI,EAAc,OAAS,EAClB,CACL,KAAM,cACN,QAAS,qBACV,CAGI,CACL,KAAM,aACN,MAHY,EAAc,GAI3B,IAED,EAAI,QAAQ,SAAW,SACvB,EAAc,SAAS,SAAS,CAChC,CACA,IAAM,EAAQ,EAAc,GAC5B,GAAI,CAAC,EACH,MAAO,CACL,KAAM,cACN,QAAS,qBACV,CAEH,IAAM,EAAS,EAAc,GAC7B,GAAI,CAAC,EACH,MAAO,CACL,KAAM,cACN,QAAS,sBACV,CAGH,IAAM,EAAc,EAAI,QAAQ,IAAI,eAAe,CAC/CC,EAGJ,GAAI,GAAa,SAAS,2BAA2B,CAGnD,EAAU,EAAI,gBACL,GAAa,SAAS,mBAAmB,CAAE,CAEpD,IAAM,EAAO,MAAM,EAAc,EAAI,QAAQ,CAE7C,GAAI,CAAC,GAAQ,OAAO,GAAS,UAAY,EAAE,YAAa,GACtD,MAAO,CACL,KAAM,cACN,QAAS,kBACV,CAGH,EAAW,EAA8B,aAEzC,MAAO,CACL,KAAM,2BACP,CAGH,MAAO,CACL,KAAM,cACN,QACA,SACA,UACD,SAED,EAAI,QAAQ,SAAW,QACvB,EAAI,SAAS,SAAS,SAAS,CAE/B,MAAO,CACL,KAAM,aACN,MAAO,EAAc,GACtB,SAED,EAAI,QAAQ,SAAW,QACvB,EAAI,SAAS,SAAS,UAAU,CAEhC,MAAO,CACL,KAAM,cACN,MAAO,EAAc,GACtB,CAEH,MAAO,CACL,KAAM,qBACP,MAED,MAAO,CACL,KAAM,YACP,EAEH,CAGS,GACX,EACA,IAEA,EAAO,SAAW,CAEhB,IAAM,EAAU,EAAS,SAAW,EAAE,CACtC,AACE,EAAQ,kBAAkB,mBAI5B,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,EAAQ,CAChD,EAAI,SAAS,IAAI,EAAK,EAAM,CAG9B,OAAO,EAAI,SAAS,OAAO,EAAS,OAAO,CAAC,KAAK,EAAS,KAAK,EAC/D,CCvWJ,SAAS,EAAkB,EAAa,EAAmC,CACzE,IAAM,EAAY,OAAO,OAAO,EAAM,UAAU,CAC1C,EAAQ,EAAI,MAAM,EAAM,CAC9B,OAAO,IAAQ,GAAK,mBAAmB,EAAM,GAAG,CAAG,IAAA,GAMrD,MAAM,EAAc,IAAI,IAKlB,GACJ,EACA,IAGsD,CACtD,IAAM,EAAM,IAAI,IAAI,EAAI,KAAO,GAAI,UAAU,EAAI,QAAQ,OAAO,CAC1D,EAAiB,GAAG,EAAQ,MAGlC,GAAI,CAAC,EAAI,SAAS,SAAS,EAAe,CACxC,MAAO,CACL,KAAM,eACN,iBACD,CAIH,IAAM,EAAgB,EAAI,SACvB,QAAQ,EAAgB,GAAG,CAC3B,MAAM,IAAI,CACV,OAAO,QAAQ,CAEZ,EAAgB,EAAc,SAAS,SAAS,CAChD,EAAc,EAAc,SAAS,OAAO,CAI9C,EAAQ,EAAkB,EAAI,KAAO,GAAI,QAAQ,CACjD,EAAW,EAAkB,EAAI,KAAO,GAAI,WAAW,CAG3D,GAAI,CAAC,GAAS,CAAC,GAAY,EAAc,QAAU,EAAG,CACpD,IAAM,EAAY,EAAc,GAC1B,EAAK,EAAc,GAErB,IAAc,OAChB,EAAQ,EACC,IAAc,WACvB,EAAW,GAKf,IAAM,EAAU,GAAS,EAEzB,MAAO,CACL,UACA,SAAU,EAAI,SACd,gBACA,gBACA,cACA,QACA,WACA,UAEA,WAAY,KACb,EAMG,EAAwB,MAC5B,EACA,IAKI,CACJ,GAAI,CAGF,IAAM,EADM,IAAI,IAAI,EAAI,KAAO,GAAI,UAAU,EAAI,QAAQ,OAAO,CAC9C,aAAa,IAAI,QAAQ,CAEvCC,EAAgC,KA4BpC,GA1BA,AAoBE,EApBE,EAaW,MAAM,EAAe,CAChC,QAXc,CACd,GAAG,EACH,OAAS,GACH,EAAK,aAAa,GAAK,gBAClB,UAAU,IAEZ,EAAI,QAAQ,EAAK,aAAa,EAExC,CAIC,SAAU,EAAE,CACb,CAAC,CAIW,MAAM,EAAe,CAChC,QAAS,EACT,SAAU,EAAE,CACb,CAAC,CAGA,CAAC,EAAY,CACf,IAAM,EAAa,EAAQ,QAAU,UACrC,MAAO,CACL,QAAS,GACT,MAAO,CACL,QAAS,6CAA6C,IACtD,KAAM,KACN,aACD,CACF,CAIH,OADA,QAAQ,IAAI,qCAAqC,EAAW,WAAW,CAChE,CAAE,QAAS,GAAM,aAAY,OAC7B,EAAO,CAEd,OADA,QAAQ,MAAM,wBAAyB,EAAM,CACtC,CACL,QAAS,GACT,MAAO,CACL,QAAS,uBACT,KAAM,KACN,WAAY,UACb,CACF,GAOQ,GACX,EACA,IAEO,EAAO,IAAI,WAAa,CAE7B,IAAM,EAAe,MAAO,EACtB,EAAa,MAAO,EAE1B,OAAQ,EAAe,IAAyB,CAE9C,IAAM,EAAiB,EAAwB,EAAK,EAAQ,CAK5D,GAHA,QAAQ,IAAI,gCAAiC,EAAe,CAGxD,SAAU,GAAkB,EAAe,OAAS,eAAgB,CACtE,EAAG,KACD,KAAK,UAAU,CACb,KAAM,eACN,QAAS,kCAAkC,EAAe,iBAC1D,eAAgB,EAAe,eAChC,CAAC,CACH,CACD,EAAG,MAAM,IAAM,eAAe,CAC9B,OAIF,IAAM,EAAU,EAIVC,EAAkC,CACtC,GAAI,QAAQ,KAAK,KAAK,CAAC,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,UAAU,EAAG,GAAG,GACrE,KAAO,GAAiB,CAClB,EAAG,aAAe,EAAG,MACvB,QAAQ,IACN,8CAA8C,EAAW,GAAG,GAC5D,EAAK,UAAU,EAAG,IAAI,CACvB,CACD,EAAG,KAAK,EAAK,EAEb,QAAQ,KACN,sDAAsD,EAAG,aAC1D,EAGL,OAAQ,EAAe,IAAoB,EAAG,MAAM,EAAM,EAAO,CACjE,IAAI,YAAa,CACf,OAAO,EAAG,YAEb,CAGD,EAAQ,WAAa,GAGpB,SAAY,CAEX,GAAI,EAAgB,CAClB,IAAM,EAAa,MAAM,EAAsB,EAAK,EAAe,CAEnE,GAAI,CAAC,EAAW,QAAS,CACvB,EAAG,KACD,KAAK,UAAU,CACb,KAAM,cACN,QAAS,EAAW,OAAO,QAC3B,KAAM,cACN,WAAY,EAAW,OAAO,WAC/B,CAAC,CACH,CACD,EAAG,MAAM,EAAW,OAAO,MAAQ,KAAM,EAAW,OAAO,QAAQ,CACnE,OAIE,EAAW,YACb,EAAY,IAAI,EAAW,GAAI,EAAW,WAAW,CAIzD,QAAQ,IACN,iCACA,EAAQ,QACR,sBACA,EAAW,GACZ,CAGD,IAAM,EAAa,EACjB,EACA,EACA,EACD,CACD,EAAO,QAAQ,EAAW,IACxB,CAGJ,EAAG,GAAG,UAAY,GAAkB,CAClC,IAAM,EAAgB,EACpB,EACA,EACD,CACD,EAAO,QAAQ,EAAc,EAC7B,CAGF,EAAG,GAAG,YAAe,CAEf,EAAQ,YAAY,KACtB,EAAY,OAAO,EAAQ,WAAW,GAAG,CACzC,QAAQ,IACN,gDAAgD,EAAQ,WAAW,KACpE,EAIH,IAAM,EAAc,EAClB,EACA,EACA,EACD,CACD,EAAO,QAAQ,EAAY,EAC3B,CAGF,EAAG,GAAG,SAAU,GAAG,IAAoB,CACrC,IAAM,EAAQ,EAAK,GACb,EAAc,EAAqB,EAAO,EAAQ,QAAQ,CAChE,EAAO,QAAQ,EAAY,EAC3B,GAEJ,CClOS,GACX,EAAiC,EAAE,GACkC,CACrE,GAAM,CAAE,kBAAmB,EAE3B,MAAO,CAOL,eAAgB,EAQhB,aAAc,EAKd,kBAAmB,CAAE,aACnB,EAAwB,EAAS,EAAe,CAQlD,kBAAmB,EACd,GACC,EAAO,eAAiB,EAAe,EAAI,CAAC,CAAC,KAC3C,EAAO,SAAU,IACf,QAAQ,MAAM,kCAAmC,EAAM,CAEhD,EAAO,QAAQ,KAAK,EAC3B,CACH,CACH,IAAA,GACL"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/express-http-handler.ts","../src/express-websocket-handler.ts","../src/express-adapter.ts"],"sourcesContent":["import type { UploadistaRequest, UploadistaResponse } from \"@uploadista/server\";\nimport { Effect } from \"effect\";\nimport type { ExpressContext } from \"./express-adapter\";\n\n/**\n * Helper to parse JSON body if not already parsed\n */\nconst parseJsonBody = async (\n req: ExpressContext[\"request\"],\n): Promise<unknown> => {\n // If body is already parsed, return it\n if (req.body && typeof req.body === \"object\") {\n return req.body;\n }\n\n // Manually parse JSON body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk as Buffer);\n }\n const body = Buffer.concat(chunks).toString();\n return JSON.parse(body);\n};\n\nexport const extractExpressRequest = (\n ctx: ExpressContext,\n { baseUrl }: { baseUrl: string },\n) => {\n // Run the routing logic as an Effect program\n return Effect.promise(async () => {\n // Get request details\n const url = new URL(ctx.request.url, `http://${ctx.request.get(\"host\")}`);\n const acceptHeader = ctx.request.get(\"Accept\");\n\n // Check for health check endpoints first (at /{baseUrl}/health, not under /api/)\n const healthPrefix = `/${baseUrl}/`;\n if (url.pathname.startsWith(healthPrefix) && ctx.request.method === \"GET\") {\n const healthPath = url.pathname.slice(healthPrefix.length);\n\n // /health or /healthz - Liveness probe\n if (healthPath === \"health\" || healthPath === \"healthz\") {\n return {\n type: \"health\",\n acceptHeader,\n } as UploadistaRequest;\n }\n\n // /ready or /readyz - Readiness probe\n if (healthPath === \"ready\" || healthPath === \"readyz\") {\n return {\n type: \"health-ready\",\n acceptHeader,\n } as UploadistaRequest;\n }\n\n // /health/components - Detailed component status\n if (healthPath === \"health/components\") {\n return {\n type: \"health-components\",\n acceptHeader,\n } as UploadistaRequest;\n }\n }\n\n // Check for baseUrl/api/ prefix for other routes\n const expectedPrefix = `/${baseUrl}/api/`;\n if (!url.pathname.includes(expectedPrefix)) {\n return {\n type: \"not-found\",\n } as UploadistaRequest;\n }\n\n // Remove the prefix and get the actual route segments\n const routeSegments = url.pathname\n .replace(`${baseUrl}/api/`, \"\")\n .split(\"/\")\n .filter(Boolean);\n\n // Route based on first segment\n if (routeSegments[0] === \"upload\" || routeSegments.includes(\"upload\")) {\n switch (ctx.request.method) {\n case \"POST\": {\n // Parse JSON body if not already parsed\n const data = await parseJsonBody(ctx.request);\n return {\n type: \"create-upload\",\n data,\n } as UploadistaRequest;\n }\n case \"GET\": {\n const lastSegment = routeSegments[routeSegments.length - 1];\n\n if (lastSegment === \"capabilities\") {\n const storageId = url.searchParams.get(\"storageId\");\n const storageIdFromPath = routeSegments[routeSegments.length - 2];\n\n // Only use path segment if it's not \"upload\"\n const finalStorageId =\n storageId ||\n (storageIdFromPath !== \"upload\" ? storageIdFromPath : null);\n\n if (!finalStorageId) {\n return {\n type: \"bad-request\",\n message: \"Storage ID is required\",\n } as UploadistaRequest;\n }\n return {\n type: \"get-capabilities\",\n storageId: finalStorageId,\n } as UploadistaRequest;\n }\n if (routeSegments.length < 2) {\n return {\n type: \"bad-request\",\n message: \"Upload ID is required\",\n } as UploadistaRequest;\n }\n return {\n type: \"get-upload\",\n uploadId: routeSegments[1],\n } as UploadistaRequest;\n }\n case \"PATCH\": {\n if (routeSegments.length < 2) {\n return {\n type: \"bad-request\",\n message: \"Upload ID is required\",\n } as UploadistaRequest;\n }\n // Convert Node.js Readable stream to web ReadableStream\n const body = new ReadableStream({\n start(controller) {\n ctx.request.on(\"data\", (chunk: Buffer) => {\n controller.enqueue(chunk);\n });\n ctx.request.on(\"end\", () => {\n controller.close();\n });\n ctx.request.on(\"error\", (error: Error) => {\n controller.error(error);\n });\n },\n });\n\n return {\n type: \"upload-chunk\",\n uploadId: routeSegments[1],\n data: body,\n } as UploadistaRequest;\n }\n default:\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n }\n } else if (routeSegments[0] === \"flow\" || routeSegments.includes(\"flow\")) {\n switch (ctx.request.method) {\n case \"GET\":\n return {\n type: \"get-flow\",\n flowId: routeSegments[1],\n } as UploadistaRequest;\n case \"POST\": {\n // Parse JSON body if not already parsed\n const params = await parseJsonBody(ctx.request);\n if (!params || typeof params !== \"object\" || !(\"inputs\" in params)) {\n return {\n type: \"bad-request\",\n message: \"Inputs are required\",\n } as UploadistaRequest;\n }\n return {\n type: \"run-flow\",\n flowId: routeSegments[1],\n storageId: routeSegments[2],\n inputs: (params as { inputs: unknown }).inputs,\n } as UploadistaRequest;\n }\n default:\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n }\n } else if (routeSegments[0] === \"dlq\" || routeSegments.includes(\"dlq\")) {\n // DLQ Admin routes: /api/dlq, /api/dlq/:itemId, /api/dlq/:itemId/retry, etc.\n switch (ctx.request.method) {\n case \"GET\": {\n if (routeSegments.length === 1) {\n // GET /api/dlq - List DLQ items\n const status = url.searchParams.get(\"status\") as string | undefined;\n const flowId = url.searchParams.get(\"flowId\") as string | undefined;\n const clientId = url.searchParams.get(\"clientId\") as\n | string\n | undefined;\n const limit = url.searchParams.get(\"limit\")\n ? Number.parseInt(url.searchParams.get(\"limit\") as string, 10)\n : undefined;\n const offset = url.searchParams.get(\"offset\")\n ? Number.parseInt(url.searchParams.get(\"offset\") as string, 10)\n : undefined;\n return {\n type: \"dlq-list\",\n options: { status, flowId, clientId, limit, offset },\n } as UploadistaRequest;\n }\n if (routeSegments[1] === \"stats\") {\n // GET /api/dlq/stats - Get DLQ statistics\n return {\n type: \"dlq-stats\",\n } as UploadistaRequest;\n }\n // GET /api/dlq/:itemId - Get specific DLQ item\n return {\n type: \"dlq-get\",\n itemId: routeSegments[1],\n } as UploadistaRequest;\n }\n case \"POST\": {\n if (routeSegments[1] === \"cleanup\") {\n // POST /api/dlq/cleanup - Cleanup old items\n const body = await parseJsonBody(ctx.request).catch(() => ({}));\n return {\n type: \"dlq-cleanup\",\n options: body,\n } as UploadistaRequest;\n }\n if (routeSegments[1] === \"retry-all\") {\n // POST /api/dlq/retry-all - Retry all matching items\n const body = await parseJsonBody(ctx.request).catch(() => ({}));\n return {\n type: \"dlq-retry-all\",\n options: body,\n } as UploadistaRequest;\n }\n if (routeSegments[2] === \"retry\") {\n // POST /api/dlq/:itemId/retry - Retry specific item\n return {\n type: \"dlq-retry\",\n itemId: routeSegments[1],\n } as UploadistaRequest;\n }\n if (routeSegments[2] === \"resolve\") {\n // POST /api/dlq/:itemId/resolve - Manually resolve item\n return {\n type: \"dlq-resolve\",\n itemId: routeSegments[1],\n } as UploadistaRequest;\n }\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n }\n case \"DELETE\": {\n // DELETE /api/dlq/:itemId - Delete a DLQ item\n if (routeSegments.length < 2) {\n return {\n type: \"bad-request\",\n message: \"Item ID is required\",\n } as UploadistaRequest;\n }\n return {\n type: \"dlq-delete\",\n itemId: routeSegments[1],\n } as UploadistaRequest;\n }\n default:\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n }\n } else if (routeSegments[0] === \"jobs\" || routeSegments.includes(\"jobs\")) {\n if (ctx.request.method === \"GET\" && url.pathname.endsWith(\"/status\")) {\n // Need at least 3 segments: jobs, jobId, status\n if (routeSegments.length < 3) {\n return {\n type: \"bad-request\",\n message: \"Job ID is required\",\n } as UploadistaRequest;\n }\n const jobId = routeSegments[1];\n return {\n type: \"job-status\",\n jobId,\n } as UploadistaRequest;\n } else if (\n ctx.request.method === \"PATCH\" &&\n routeSegments.includes(\"resume\")\n ) {\n const jobId = routeSegments[1];\n if (!jobId) {\n return {\n type: \"bad-request\",\n message: \"Job ID is required\",\n } as UploadistaRequest;\n }\n const nodeId = routeSegments[3];\n if (!nodeId) {\n return {\n type: \"bad-request\",\n message: \"Node ID is required\",\n } as UploadistaRequest;\n }\n\n const contentType = ctx.request.get(\"content-type\");\n let newData: unknown;\n\n // Handle different content types\n if (contentType?.includes(\"application/octet-stream\")) {\n // For streaming data, pass the req object (Express handles streams)\n // Express doesn't expose ReadableStream like Hono, use req itself\n newData = ctx.request;\n } else if (contentType?.includes(\"application/json\")) {\n // Parse JSON body if not already parsed\n const body = await parseJsonBody(ctx.request);\n\n if (!body || typeof body !== \"object\" || !(\"newData\" in body)) {\n return {\n type: \"bad-request\",\n message: \"Missing newData\",\n } as UploadistaRequest;\n }\n\n newData = (body as { newData: unknown }).newData;\n } else {\n return {\n type: \"unsupported-content-type\",\n } as UploadistaRequest;\n }\n\n return {\n type: \"resume-flow\",\n jobId,\n nodeId,\n newData,\n } as UploadistaRequest;\n } else if (\n ctx.request.method === \"POST\" &&\n url.pathname.endsWith(\"/pause\")\n ) {\n return {\n type: \"pause-flow\",\n jobId: routeSegments[1],\n } as UploadistaRequest;\n } else if (\n ctx.request.method === \"POST\" &&\n url.pathname.endsWith(\"/cancel\")\n ) {\n return {\n type: \"cancel-flow\",\n jobId: routeSegments[1],\n } as UploadistaRequest;\n }\n return {\n type: \"method-not-allowed\",\n } as UploadistaRequest;\n } else {\n return {\n type: \"not-found\",\n } as UploadistaRequest;\n }\n });\n};\n\nexport const sendExpressResponse = (\n response: UploadistaResponse,\n ctx: ExpressContext,\n) =>\n Effect.sync(() => {\n // Set default Content-Type header if not provided\n const headers = response.headers || {};\n if (!headers[\"Content-Type\"]) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n // Set headers\n for (const [key, value] of Object.entries(headers)) {\n ctx.response.set(key, value);\n }\n\n return ctx.response.status(response.status).send(response.body);\n });\n","import type { IncomingMessage } from \"node:http\";\nimport { FlowEngine, UploadEngine } from \"@uploadista/core\";\nimport type { AuthResult } from \"@uploadista/server\";\nimport {\n handleWebSocketClose,\n handleWebSocketError,\n handleWebSocketMessage,\n handleWebSocketOpen,\n type WebSocketConnection,\n type WebSocketConnectionRequest,\n} from \"@uploadista/server\";\nimport { Effect } from \"effect\";\nimport type { Request, Response } from \"express\";\nimport type { WebSocket } from \"ws\";\nimport type { ExpressContext } from \"./express-adapter\";\n\nexport type ExpressWebSocketHandler = (\n ws: WebSocket,\n req: IncomingMessage,\n) => void;\n\nfunction extractQueryParam(url: string, param: string): string | undefined {\n const regex = new RegExp(`[?&]${param}=([^&]*)`);\n const match = url.match(regex);\n return match?.[1] ? decodeURIComponent(match[1]) : undefined;\n}\n\n/**\n * Cache for storing auth context per WebSocket connection\n */\nconst wsAuthCache = new Map<string, AuthResult>();\n\n/**\n * Extracts WebSocket connection request details from Express/Node.js request\n */\nconst extractWebSocketRequest = (\n req: IncomingMessage,\n baseUrl: string,\n):\n | WebSocketConnectionRequest\n | { type: \"invalid-path\"; expectedPrefix: string } => {\n const url = new URL(req.url || \"\", `http://${req.headers.host}`);\n const expectedPrefix = `${baseUrl}/ws/`;\n\n // Check for ws/uploadista prefix\n if (!url.pathname.includes(expectedPrefix)) {\n return {\n type: \"invalid-path\",\n expectedPrefix,\n };\n }\n\n // Remove the prefix and get the actual route segments\n const routeSegments = url.pathname\n .replace(expectedPrefix, \"\")\n .split(\"/\")\n .filter(Boolean);\n\n const isUploadRoute = routeSegments.includes(\"upload\");\n const isFlowRoute = routeSegments.includes(\"flow\");\n\n // Extract jobId and uploadId from URL path or query parameters\n // Path format: /uploadista/ws/flow/{jobId} or /uploadista/ws/upload/{uploadId}\n let jobId = extractQueryParam(req.url || \"\", \"jobId\");\n let uploadId = extractQueryParam(req.url || \"\", \"uploadId\");\n\n // If not in query params, extract from path segments\n if (!jobId && !uploadId && routeSegments.length >= 2) {\n const routeType = routeSegments[0]; // 'flow' or 'upload'\n const id = routeSegments[1]; // the actual ID\n\n if (routeType === \"flow\") {\n jobId = id;\n } else if (routeType === \"upload\") {\n uploadId = id;\n }\n }\n\n // Use jobId if available, otherwise use uploadId\n const eventId = jobId || uploadId;\n\n return {\n baseUrl,\n pathname: url.pathname,\n routeSegments,\n isUploadRoute,\n isFlowRoute,\n jobId,\n uploadId,\n eventId,\n // Connection will be set when WebSocket opens\n connection: null as unknown as WebSocketConnection,\n };\n};\n\n/**\n * Authenticates a WebSocket connection using the provided auth middleware\n */\nconst authenticateWebSocket = async (\n req: IncomingMessage,\n authMiddleware: (ctx: ExpressContext) => Promise<AuthResult>,\n): Promise<{\n success: boolean;\n authResult?: AuthResult;\n error?: { message: string; code: number; authMethod: string };\n}> => {\n try {\n // Extract token from query parameter\n const url = new URL(req.url || \"\", `http://${req.headers.host}`);\n const token = url.searchParams.get(\"token\");\n\n let authResult: AuthResult | null = null;\n\n if (token) {\n // Token-based authentication\n // Create a mock request with Authorization header\n const mockReq = {\n ...req,\n header: (name: string) => {\n if (name.toLowerCase() === \"authorization\") {\n return `Bearer ${token}`;\n }\n return req.headers[name.toLowerCase()];\n },\n } as unknown as Request;\n\n authResult = await authMiddleware({\n request: mockReq as unknown as Request,\n response: {} as Response,\n });\n } else {\n // Cookie-based authentication\n // Pass the original request so auth middleware can read cookies\n authResult = await authMiddleware({\n request: req as unknown as Request,\n response: {} as Response,\n });\n }\n\n if (!authResult) {\n const authMethod = token ? \"token\" : \"cookies\";\n return {\n success: false,\n error: {\n message: `Authentication failed: invalid or expired ${authMethod}`,\n code: 4001,\n authMethod,\n },\n };\n }\n\n console.log(`WebSocket authenticated for user: ${authResult.clientId}`);\n return { success: true, authResult };\n } catch (error) {\n console.error(\"WebSocket auth error:\", error);\n return {\n success: false,\n error: {\n message: \"Authentication error\",\n code: 4001,\n authMethod: \"unknown\",\n },\n };\n }\n};\n\n/**\n * Creates an Express WebSocket handler that delegates to core WebSocket handlers\n */\nexport const expressWebSocketHandler = (\n baseUrl: string,\n authMiddleware?: (ctx: ExpressContext) => Promise<AuthResult>,\n): Effect.Effect<ExpressWebSocketHandler, never, UploadEngine | FlowEngine> => {\n return Effect.gen(function* () {\n // Get the server instances from the Effect context\n const uploadEngine = yield* UploadEngine;\n const flowEngine = yield* FlowEngine;\n\n return (ws: WebSocket, req: IncomingMessage) => {\n // Extract request details (adapter's responsibility)\n const requestOrError = extractWebSocketRequest(req, baseUrl);\n\n console.log(\"🔍 WebSocket request details:\", requestOrError);\n\n // Handle invalid path\n if (\"type\" in requestOrError && requestOrError.type === \"invalid-path\") {\n ws.send(\n JSON.stringify({\n type: \"invalid-path\",\n message: `WebSocket path must start with ${requestOrError.expectedPrefix}`,\n expectedPrefix: requestOrError.expectedPrefix,\n }),\n );\n ws.close(1000, \"Invalid path\");\n return;\n }\n\n // Type narrowing: at this point, requestOrError is WebSocketConnectionRequest\n const request = requestOrError as WebSocketConnectionRequest;\n\n // Create framework-agnostic connection wrapper\n // Use a getter for readyState so it always reflects the current state\n const connection: WebSocketConnection = {\n id: `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,\n send: (data: string) => {\n if (ws.readyState === ws.OPEN) {\n console.log(\n `📤 Sending WebSocket message to connection ${connection.id}:`,\n data.substring(0, 100),\n );\n ws.send(data);\n } else {\n console.warn(\n `⚠️ Cannot send message, WebSocket not open. State: ${ws.readyState}`,\n );\n }\n },\n close: (code?: number, reason?: string) => ws.close(code, reason),\n get readyState() {\n return ws.readyState;\n },\n };\n\n // Update request with connection\n request.connection = connection;\n\n // Handle WebSocket open\n (async () => {\n // Validate authentication if auth middleware is configured\n if (authMiddleware) {\n const authResult = await authenticateWebSocket(req, authMiddleware);\n\n if (!authResult.success) {\n ws.send(\n JSON.stringify({\n type: \"auth-failed\",\n message: authResult.error?.message,\n code: \"AUTH_FAILED\",\n authMethod: authResult.error?.authMethod,\n }),\n );\n ws.close(authResult.error?.code || 4001, authResult.error?.message);\n return;\n }\n\n // Cache auth context for this connection\n if (authResult.authResult) {\n wsAuthCache.set(connection.id, authResult.authResult);\n }\n }\n\n console.log(\n \"🔍 WebSocket open for eventId:\",\n request.eventId,\n \"with connection id:\",\n connection.id,\n );\n\n // Delegate to core handler for business logic\n const openEffect = handleWebSocketOpen(\n request,\n uploadEngine,\n flowEngine,\n );\n Effect.runFork(openEffect);\n })();\n\n // Handle WebSocket message\n ws.on(\"message\", (data: unknown) => {\n const messageEffect = handleWebSocketMessage(\n data as string,\n connection,\n );\n Effect.runFork(messageEffect);\n });\n\n // Handle WebSocket close\n ws.on(\"close\", () => {\n // Clear cached auth context for this connection\n if (request.connection?.id) {\n wsAuthCache.delete(request.connection.id);\n console.log(\n `Cleared auth cache for WebSocket connection: ${request.connection.id}`,\n );\n }\n\n // Delegate to core handler for cleanup\n const closeEffect = handleWebSocketClose(\n request,\n uploadEngine,\n flowEngine,\n );\n Effect.runFork(closeEffect);\n });\n\n // Handle WebSocket error\n ws.on(\"error\", (...args: unknown[]) => {\n const error = args[0] as Error;\n const errorEffect = handleWebSocketError(error, request.eventId);\n Effect.runFork(errorEffect);\n });\n };\n });\n};\n","import type { AuthResult, ServerAdapter } from \"@uploadista/server\";\nimport { Effect } from \"effect\";\nimport type { Request, Response } from \"express\";\nimport {\n extractExpressRequest,\n sendExpressResponse,\n} from \"./express-http-handler\";\nimport {\n type ExpressWebSocketHandler,\n expressWebSocketHandler,\n} from \"./express-websocket-handler\";\n\nexport type ExpressContext = {\n request: Request;\n response: Response;\n next?: (error?: Error) => void;\n};\n\n/**\n * Options for creating an Express server adapter.\n */\nexport interface ExpressAdapterOptions {\n /**\n * Optional authentication middleware function.\n * Called for each request to authenticate the user.\n *\n * @param ctx - Express context\n * @returns Promise resolving to AuthResult (AuthContext or null)\n */\n authMiddleware?: (ctx: ExpressContext) => Promise<AuthResult>;\n}\n\n// WebSocket interface from ws package\nexport interface WebSocket {\n readyState: number;\n OPEN: number;\n send: (data: string) => void;\n close: (code?: number, reason?: string) => void;\n on: (event: string, listener: (...args: unknown[]) => void) => void;\n off: (event: string, listener: (...args: unknown[]) => void) => void;\n}\n\n/**\n * Creates an Express server adapter that implements the ServerAdapter interface.\n *\n * This adapter translates between Express's Request/Response API and the core server's\n * standard request/response model. It supports:\n * - Request extraction from Express Request\n * - Response sending via Express Response\n * - Optional authentication middleware\n * - WebSocket handling\n *\n * @param options - Adapter configuration options\n * @returns ServerAdapter implementation for Express\n *\n * @example\n * ```typescript\n * import { expressAdapter } from \"@uploadista/adapters-express\";\n * import { createUploadistaServer } from \"@uploadista/server\";\n *\n * const adapter = expressAdapter({\n * authMiddleware: async (req, res) => {\n * const userId = req.header(\"x-user-id\");\n * return userId ? { clientId: userId } : null;\n * }\n * });\n *\n * const server = await createUploadistaServer({\n * flows: getFlows,\n * dataStore: { type: \"s3\", config: { bucket: \"uploads\" } },\n * kvStore: redisKvStore,\n * eventEmitter: webSocketEventEmitter(),\n * adapter\n * });\n * ```\n */\nexport const expressAdapter = (\n options: ExpressAdapterOptions = {},\n): ServerAdapter<ExpressContext, Response, ExpressWebSocketHandler> => {\n const { authMiddleware } = options;\n\n return {\n /**\n * Extract standard request details from Express Request.\n *\n * Converts Express's Request into a framework-agnostic UploadistaRequest\n * by parsing the URL, headers, and body.\n */\n extractRequest: extractExpressRequest,\n\n /**\n * Send standard response using Express Response format.\n *\n * Converts a UploadistaResponse into an object with status, headers, and body\n * that the Express handler can send.\n */\n sendResponse: sendExpressResponse,\n\n /**\n * WebSocket handler for Express with ws package.\n */\n webSocketHandler: ({ baseUrl }: { baseUrl: string }) =>\n expressWebSocketHandler(baseUrl, authMiddleware),\n\n /**\n * Run framework-specific auth middleware.\n *\n * If provided, executes the Express-specific authentication middleware\n * with access to the full Express Request and Response objects.\n */\n runAuthMiddleware: authMiddleware\n ? (ctx: ExpressContext) =>\n Effect.tryPromise(() => authMiddleware(ctx)).pipe(\n Effect.catchAll((error) => {\n console.error(\"Express auth middleware failed:\", error);\n // Return null to indicate auth failure (not an error in the Effect sense)\n return Effect.succeed(null);\n }),\n )\n : undefined,\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uploadista/adapters-express",
3
3
  "type": "module",
4
- "version": "0.0.20-beta.9",
4
+ "version": "0.1.0-beta.5",
5
5
  "description": "Express adapter for Uploadista",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -14,12 +14,12 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "ws": "8.18.3",
18
- "@uploadista/core": "0.0.20-beta.9",
19
- "@uploadista/observability": "0.0.20-beta.9",
20
- "@uploadista/server": "0.0.20-beta.9",
21
- "@uploadista/event-broadcaster-memory": "0.0.20-beta.9",
22
- "@uploadista/event-emitter-websocket": "0.0.20-beta.9"
17
+ "ws": "8.19.0",
18
+ "@uploadista/observability": "0.1.0-beta.5",
19
+ "@uploadista/event-emitter-websocket": "0.1.0-beta.5",
20
+ "@uploadista/server": "0.1.0-beta.5",
21
+ "@uploadista/core": "0.1.0-beta.5",
22
+ "@uploadista/event-broadcaster-memory": "0.1.0-beta.5"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "effect": "^3.0.0",
@@ -29,14 +29,14 @@
29
29
  "devDependencies": {
30
30
  "@effect/vitest": "0.27.0",
31
31
  "@types/express": "^5.0.0",
32
- "@types/node": "24.10.4",
32
+ "@types/node": "24.10.8",
33
33
  "@types/ws": "8.18.1",
34
- "effect": "3.19.12",
35
- "tsdown": "0.18.0",
34
+ "effect": "3.19.14",
35
+ "tsdown": "0.19.0",
36
36
  "typescript": "5.9.3",
37
- "vitest": "4.0.15",
38
- "zod": "4.2.0",
39
- "@uploadista/typescript-config": "0.0.20-beta.9"
37
+ "vitest": "4.0.17",
38
+ "zod": "4.3.5",
39
+ "@uploadista/typescript-config": "0.1.0-beta.5"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc --noEmit && tsdown",
@@ -44,7 +44,7 @@
44
44
  "clean": "rimraf -rf dist && rimraf -rf .turbo && rimraf tsconfig.tsbuildinfo",
45
45
  "format": "biome format --write ./src",
46
46
  "lint": "biome lint --write ./src",
47
- "test": "vitest",
47
+ "test": "vitest run",
48
48
  "test:run": "vitest run",
49
49
  "test:watch": "vitest --watch"
50
50
  }
@@ -96,7 +96,8 @@ export const extractExpressRequest = (
96
96
 
97
97
  // Only use path segment if it's not "upload"
98
98
  const finalStorageId =
99
- storageId || (storageIdFromPath !== "upload" ? storageIdFromPath : null);
99
+ storageId ||
100
+ (storageIdFromPath !== "upload" ? storageIdFromPath : null);
100
101
 
101
102
  if (!finalStorageId) {
102
103
  return {
@@ -193,10 +194,10 @@ export const extractExpressRequest = (
193
194
  | string
194
195
  | undefined;
195
196
  const limit = url.searchParams.get("limit")
196
- ? Number.parseInt(url.searchParams.get("limit") as string)
197
+ ? Number.parseInt(url.searchParams.get("limit") as string, 10)
197
198
  : undefined;
198
199
  const offset = url.searchParams.get("offset")
199
- ? Number.parseInt(url.searchParams.get("offset") as string)
200
+ ? Number.parseInt(url.searchParams.get("offset") as string, 10)
200
201
  : undefined;
201
202
  return {
202
203
  type: "dlq-list",