@uploadista/flow-security-clamscan 0.0.18-beta.17 → 0.0.18-beta.2

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.
@@ -1,22 +1,21 @@
1
-
2
- 
3
- > @uploadista/flow-security-clamscan@0.0.18-beta.16 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan
4
- > tsdown
5
-
6
- ℹ tsdown v0.16.8 powered by rolldown v1.0.0-beta.52
7
- ℹ Using tsdown config: /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan/tsdown.config.ts
8
- ℹ entry: src/index.ts
9
- ℹ tsconfig: tsconfig.json
10
- ℹ Build start
11
- ℹ Cleaning 7 files
12
- ℹ [CJS] dist/index.cjs 2.89 kB │ gzip: 1.22 kB
13
- ℹ [CJS] 1 files, total: 2.89 kB
14
- ℹ [ESM] dist/index.mjs 2.27 kB │ gzip: 0.98 kB
15
- ℹ [ESM] dist/index.mjs.map 9.40 kB │ gzip: 2.93 kB
16
- ℹ [ESM] dist/index.d.mts.map 0.25 kB │ gzip: 0.17 kB
17
- ℹ [ESM] dist/index.d.mts 1.65 kB │ gzip: 0.73 kB
18
- ℹ [ESM] 4 files, total: 13.57 kB
19
- ℹ [CJS] dist/index.d.cts.map 0.25 kB │ gzip: 0.17 kB
20
- ℹ [CJS] dist/index.d.cts 1.65 kB │ gzip: 0.73 kB
21
- ℹ [CJS] 2 files, total: 1.90 kB
22
- ✔ Build complete in 3927ms
1
+
2
+ > @uploadista/flow-security-clamscan@0.0.18-beta.1 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan
3
+ > tsdown
4
+
5
+ ℹ tsdown v0.16.6 powered by rolldown v1.0.0-beta.51
6
+ ℹ Using tsdown config: /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan/tsdown.config.ts
7
+ ℹ entry: src/index.ts
8
+ ℹ tsconfig: tsconfig.json
9
+ ℹ Build start
10
+ ℹ Cleaning 7 files
11
+ ℹ [CJS] dist/index.cjs 2.84 kB │ gzip: 1.17 kB
12
+ ℹ [CJS] 1 files, total: 2.84 kB
13
+ ℹ [ESM] dist/index.mjs 2.25 kB │ gzip: 0.92 kB
14
+ ℹ [ESM] dist/index.mjs.map 9.32 kB │ gzip: 2.88 kB
15
+ ℹ [ESM] dist/index.d.mts.map 0.25 kB │ gzip: 0.17 kB
16
+ ℹ [ESM] dist/index.d.mts 1.65 kB │ gzip: 0.73 kB
17
+ ℹ [ESM] 4 files, total: 13.46 kB
18
+ ℹ [CJS] dist/index.d.cts.map 0.25 kB │ gzip: 0.17 kB
19
+ ℹ [CJS] dist/index.d.cts 1.65 kB │ gzip: 0.73 kB
20
+ ℹ [CJS] 2 files, total: 1.90 kB
21
+ ✔ Build complete in 5398ms
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`node:crypto`),l=require(`node:fs/promises`);l=s(l);let u=require(`node:os`);u=s(u);let d=require(`node:path`);d=s(d);let f=require(`@uploadista/core/errors`),p=require(`@uploadista/core/flow`),m=require(`@uploadista/observability`),h=require(`clamscan`);h=s(h);let g=require(`effect`);var _=class{clamscan=null;constructor(e={}){this.config=e}initScanner(){return g.Effect.gen(function*(){if(this.clamscan)return this.clamscan;let e=yield*g.Effect.tryPromise({try:async()=>await new h.default().init({preference:this.config.preference??`clamdscan`,remove_infected:this.config.remove_infected??!1,debug_mode:this.config.debug_mode??!1,clamdscan:{socket:this.config.clamdscan_socket,host:this.config.clamdscan_host,port:this.config.clamdscan_port??3310,timeout:6e4,local_fallback:!0},clamscan:{path:`/usr/bin/clamscan`,scan_archives:!0,active:!0}}),catch:e=>f.UploadistaError.fromCode(`CLAMAV_NOT_INSTALLED`,{body:`ClamAV initialization failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})});return this.clamscan=e,e}.bind(this)).pipe((0,m.withOperationSpan)(`virus-scan`,`init`,{"scan.preference":this.config.preference??`clamdscan`}))}scan(e){return g.Effect.gen(function*(){let t=yield*this.initScanner(),n=u.tmpdir(),r=`uploadista-scan-${(0,c.randomUUID)()}`,i=d.join(n,r);return yield*g.Effect.tryPromise({try:()=>l.writeFile(i,e),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to create temporary file for scanning`,details:{error:e}})}),yield*g.Effect.tryPromise({try:()=>t.isInfected(i),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Virus scan failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})}).pipe(g.Effect.map(e=>({isClean:!e.isInfected,detectedViruses:e.viruses||[]})),g.Effect.ensuring(g.Effect.tryPromise({try:()=>l.unlink(i),catch:()=>void 0}).pipe(g.Effect.ignore)))}.bind(this)).pipe((0,m.withOperationSpan)(`virus-scan`,`scan`,{"scan.file_size":e.byteLength}))}getVersion(){return g.Effect.gen(function*(){let e=yield*this.initScanner();return(yield*g.Effect.tryPromise({try:()=>e.getVersion(),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to get ClamAV version`,details:{error:e}})})).version||`Unknown`}.bind(this)).pipe((0,m.withOperationSpan)(`virus-scan`,`get-version`,{}))}};function v(e={}){return g.Layer.succeed(p.VirusScanPlugin,new _(e))}exports.ClamScanPluginLayer=v;
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`node:crypto`),l=require(`node:fs/promises`);l=s(l);let u=require(`node:os`);u=s(u);let d=require(`node:path`);d=s(d);let f=require(`@uploadista/core/errors`),p=require(`@uploadista/core/flow`),m=require(`clamscan`);m=s(m);let h=require(`effect`);var g=class{clamscan=null;constructor(e={}){this.config=e}async initScanner(){if(this.clamscan)return this.clamscan;try{let e=await new m.default().init({preference:this.config.preference??`clamdscan`,remove_infected:this.config.remove_infected??!1,debug_mode:this.config.debug_mode??!1,clamdscan:{socket:this.config.clamdscan_socket,host:this.config.clamdscan_host,port:this.config.clamdscan_port??3310,timeout:6e4,local_fallback:!0},clamscan:{path:`/usr/bin/clamscan`,scan_archives:!0,active:!0}});return this.clamscan=e,e}catch(e){throw Error(`ClamAV initialization failed: ${e instanceof Error?e.message:String(e)}`)}}scan(e){return h.Effect.gen(function*(){let t=yield*h.Effect.tryPromise({try:()=>this.initScanner(),catch:e=>f.UploadistaError.fromCode(`CLAMAV_NOT_INSTALLED`,{body:e instanceof Error?e.message:`ClamAV is not installed or not available`,details:{error:e}})}),n=u.tmpdir(),r=`uploadista-scan-${(0,c.randomUUID)()}`,i=d.join(n,r);return yield*h.Effect.tryPromise({try:()=>l.writeFile(i,e),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to create temporary file for scanning`,details:{error:e}})}),yield*h.Effect.tryPromise({try:()=>t.isInfected(i),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Virus scan failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})}).pipe(h.Effect.map(e=>({isClean:!e.isInfected,detectedViruses:e.viruses||[]})),h.Effect.ensuring(h.Effect.tryPromise({try:()=>l.unlink(i),catch:()=>void 0}).pipe(h.Effect.ignore)))}.bind(this))}getVersion(){return h.Effect.gen(function*(){let e=yield*h.Effect.tryPromise({try:()=>this.initScanner(),catch:e=>f.UploadistaError.fromCode(`CLAMAV_NOT_INSTALLED`,{body:e instanceof Error?e.message:`ClamAV is not installed or not available`,details:{error:e}})});return(yield*h.Effect.tryPromise({try:()=>e.getVersion(),catch:e=>f.UploadistaError.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to get ClamAV version`,details:{error:e}})})).version||`Unknown`}.bind(this))}};function _(e={}){return h.Layer.succeed(p.VirusScanPlugin,new g(e))}exports.ClamScanPluginLayer=_;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAqNgB,UArNC,cAAA,CAqNkB;EACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBADM,mBAAA,UACN,iBACP,KAAA,CAAM,MAAM"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;AAaA;AA2NgB,UA3NC,cAAA,CA2NkB;EACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBADM,mBAAA,UACN,iBACP,KAAA,CAAM,MAAM"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;AAcA;AAqNgB,UArNC,cAAA,CAqNkB;EACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBADM,mBAAA,UACN,iBACP,KAAA,CAAM,MAAM"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;AAaA;AA2NgB,UA3NC,cAAA,CA2NkB;EACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBADM,mBAAA,UACN,iBACP,KAAA,CAAM,MAAM"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{randomUUID as e}from"node:crypto";import*as t from"node:fs/promises";import*as n from"node:os";import*as r from"node:path";import{UploadistaError as i}from"@uploadista/core/errors";import{VirusScanPlugin as a}from"@uploadista/core/flow";import{withOperationSpan as o}from"@uploadista/observability";import s from"clamscan";import{Effect as c,Layer as l}from"effect";var u=class{clamscan=null;constructor(e={}){this.config=e}initScanner(){return c.gen(function*(){if(this.clamscan)return this.clamscan;let e=yield*c.tryPromise({try:async()=>await new s().init({preference:this.config.preference??`clamdscan`,remove_infected:this.config.remove_infected??!1,debug_mode:this.config.debug_mode??!1,clamdscan:{socket:this.config.clamdscan_socket,host:this.config.clamdscan_host,port:this.config.clamdscan_port??3310,timeout:6e4,local_fallback:!0},clamscan:{path:`/usr/bin/clamscan`,scan_archives:!0,active:!0}}),catch:e=>i.fromCode(`CLAMAV_NOT_INSTALLED`,{body:`ClamAV initialization failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})});return this.clamscan=e,e}.bind(this)).pipe(o(`virus-scan`,`init`,{"scan.preference":this.config.preference??`clamdscan`}))}scan(a){return c.gen(function*(){let o=yield*this.initScanner(),s=n.tmpdir(),l=`uploadista-scan-${e()}`,u=r.join(s,l);return yield*c.tryPromise({try:()=>t.writeFile(u,a),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to create temporary file for scanning`,details:{error:e}})}),yield*c.tryPromise({try:()=>o.isInfected(u),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Virus scan failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})}).pipe(c.map(e=>({isClean:!e.isInfected,detectedViruses:e.viruses||[]})),c.ensuring(c.tryPromise({try:()=>t.unlink(u),catch:()=>void 0}).pipe(c.ignore)))}.bind(this)).pipe(o(`virus-scan`,`scan`,{"scan.file_size":a.byteLength}))}getVersion(){return c.gen(function*(){let e=yield*this.initScanner();return(yield*c.tryPromise({try:()=>e.getVersion(),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to get ClamAV version`,details:{error:e}})})).version||`Unknown`}.bind(this)).pipe(o(`virus-scan`,`get-version`,{}))}};function d(e={}){return l.succeed(a,new u(e))}export{d as ClamScanPluginLayer};
1
+ import{randomUUID as e}from"node:crypto";import*as t from"node:fs/promises";import*as n from"node:os";import*as r from"node:path";import{UploadistaError as i}from"@uploadista/core/errors";import{VirusScanPlugin as a}from"@uploadista/core/flow";import o from"clamscan";import{Effect as s,Layer as c}from"effect";var l=class{clamscan=null;constructor(e={}){this.config=e}async initScanner(){if(this.clamscan)return this.clamscan;try{let e=await new o().init({preference:this.config.preference??`clamdscan`,remove_infected:this.config.remove_infected??!1,debug_mode:this.config.debug_mode??!1,clamdscan:{socket:this.config.clamdscan_socket,host:this.config.clamdscan_host,port:this.config.clamdscan_port??3310,timeout:6e4,local_fallback:!0},clamscan:{path:`/usr/bin/clamscan`,scan_archives:!0,active:!0}});return this.clamscan=e,e}catch(e){throw Error(`ClamAV initialization failed: ${e instanceof Error?e.message:String(e)}`)}}scan(a){return s.gen(function*(){let o=yield*s.tryPromise({try:()=>this.initScanner(),catch:e=>i.fromCode(`CLAMAV_NOT_INSTALLED`,{body:e instanceof Error?e.message:`ClamAV is not installed or not available`,details:{error:e}})}),c=n.tmpdir(),l=`uploadista-scan-${e()}`,u=r.join(c,l);return yield*s.tryPromise({try:()=>t.writeFile(u,a),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to create temporary file for scanning`,details:{error:e}})}),yield*s.tryPromise({try:()=>o.isInfected(u),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Virus scan failed: ${e instanceof Error?e.message:String(e)}`,details:{error:e}})}).pipe(s.map(e=>({isClean:!e.isInfected,detectedViruses:e.viruses||[]})),s.ensuring(s.tryPromise({try:()=>t.unlink(u),catch:()=>void 0}).pipe(s.ignore)))}.bind(this))}getVersion(){return s.gen(function*(){let e=yield*s.tryPromise({try:()=>this.initScanner(),catch:e=>i.fromCode(`CLAMAV_NOT_INSTALLED`,{body:e instanceof Error?e.message:`ClamAV is not installed or not available`,details:{error:e}})});return(yield*s.tryPromise({try:()=>e.getVersion(),catch:e=>i.fromCode(`VIRUS_SCAN_FAILED`,{body:`Failed to get ClamAV version`,details:{error:e}})})).version||`Unknown`}.bind(this))}};function u(e={}){return c.succeed(a,new l(e))}export{u as ClamScanPluginLayer};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["config: ClamScanConfig"],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { UploadistaError } from \"@uploadista/core/errors\";\nimport type { ScanResult, VirusScanPluginShape } from \"@uploadista/core/flow\";\nimport { VirusScanPlugin } from \"@uploadista/core/flow\";\nimport { withOperationSpan } from \"@uploadista/observability\";\nimport NodeClam from \"clamscan\";\nimport { Effect, Layer } from \"effect\";\n\n/**\n * Configuration options for the ClamAV plugin\n */\nexport interface ClamScanConfig {\n /**\n * Preference for scanning method\n * - \"clamdscan\": Use clamd daemon (faster, recommended)\n * - \"clamscan\": Use command-line binary\n */\n preference?: \"clamdscan\" | \"clamscan\";\n\n /**\n * Path to clamd socket (for daemon mode)\n * Default: /var/run/clamd.scan/clamd.sock\n */\n clamdscan_socket?: string;\n\n /**\n * TCP host for clamd (alternative to socket)\n */\n clamdscan_host?: string;\n\n /**\n * TCP port for clamd\n * Default: 3310\n */\n clamdscan_port?: number;\n\n /**\n * Whether to remove infected files automatically\n * Default: false (not recommended in flow context)\n */\n remove_infected?: boolean;\n\n /**\n * Debug mode for clamscan library\n * Default: false\n */\n debug_mode?: boolean;\n}\n\n/**\n * ClamAV implementation of the VirusScanPlugin\n *\n * This plugin uses the `clamscan` npm package to scan files for viruses\n * using ClamAV antivirus engine. It supports both clamd daemon mode (fast)\n * and binary mode (slower but more portable).\n *\n * @example\n * ```typescript\n * import { ClamScanPluginLayer } from \"@uploadista/flow-security-clamscan\";\n *\n * const program = Effect.gen(function* () {\n * const virusScan = yield* VirusScanPlugin;\n * const result = yield* virusScan.scan(fileBytes);\n * console.log(result.isClean ? \"Clean\" : \"Infected\");\n * }).pipe(Effect.provide(ClamScanPluginLayer));\n * ```\n */\nclass ClamScanPluginImpl implements VirusScanPluginShape {\n private clamscan: NodeClam | null = null;\n\n constructor(private config: ClamScanConfig = {}) {}\n\n /**\n * Initialize the ClamAV scanner\n * This is called lazily on first use\n */\n private initScanner(): Effect.Effect<NodeClam, UploadistaError> {\n return Effect.gen(\n function* (this: ClamScanPluginImpl) {\n if (this.clamscan) {\n return this.clamscan;\n }\n\n const scanner = yield* Effect.tryPromise({\n try: async () => {\n // Initialize clamscan with configuration\n return await new NodeClam().init({\n preference: this.config.preference ?? \"clamdscan\",\n remove_infected: this.config.remove_infected ?? false,\n debug_mode: this.config.debug_mode ?? false,\n clamdscan: {\n socket: this.config.clamdscan_socket,\n host: this.config.clamdscan_host,\n port: this.config.clamdscan_port ?? 3310,\n timeout: 60000,\n local_fallback: true, // Fall back to binary if daemon unavailable\n },\n clamscan: {\n path: \"/usr/bin/clamscan\", // Standard path\n scan_archives: true,\n active: true,\n },\n });\n },\n catch: (error) =>\n UploadistaError.fromCode(\"CLAMAV_NOT_INSTALLED\", {\n body: `ClamAV initialization failed: ${error instanceof Error ? error.message : String(error)}`,\n details: { error },\n }),\n });\n\n this.clamscan = scanner;\n return scanner;\n }.bind(this),\n ).pipe(\n withOperationSpan(\"virus-scan\", \"init\", {\n \"scan.preference\": this.config.preference ?? \"clamdscan\",\n }),\n );\n }\n\n /**\n * Scans a file for viruses using ClamAV\n *\n * @param input - File contents as Uint8Array\n * @returns Effect with scan results\n */\n scan(input: Uint8Array): Effect.Effect<ScanResult, UploadistaError> {\n return Effect.gen(\n function* (this: ClamScanPluginImpl) {\n // Initialize scanner (lazy initialization)\n const scanner = yield* this.initScanner();\n\n // Create temporary file path for scanning\n const tmpDir = os.tmpdir();\n const fileName = `uploadista-scan-${randomUUID()}`;\n const tempFilePath = path.join(tmpDir, fileName);\n\n // Write file data to temp file\n yield* Effect.tryPromise({\n try: () => fs.writeFile(tempFilePath, input),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: \"Failed to create temporary file for scanning\",\n details: { error },\n }),\n });\n\n // Scan the file and ensure cleanup\n const result = yield* Effect.tryPromise({\n try: () => scanner.isInfected(tempFilePath),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: `Virus scan failed: ${error instanceof Error ? error.message : String(error)}`,\n details: { error },\n }),\n }).pipe(\n Effect.map((scanResult) => ({\n isClean: !scanResult.isInfected,\n detectedViruses: scanResult.viruses || [],\n })),\n Effect.ensuring(\n // Clean up temporary file (ignore errors)\n Effect.tryPromise({\n try: () => fs.unlink(tempFilePath),\n catch: () => undefined,\n }).pipe(Effect.ignore),\n ),\n );\n\n return result;\n }.bind(this),\n ).pipe(\n withOperationSpan(\"virus-scan\", \"scan\", {\n \"scan.file_size\": input.byteLength,\n }),\n );\n }\n\n /**\n * Gets the ClamAV engine version\n *\n * @returns Effect with version string\n */\n getVersion(): Effect.Effect<string, UploadistaError> {\n return Effect.gen(\n function* (this: ClamScanPluginImpl) {\n // Initialize scanner (lazy initialization)\n const scanner = yield* this.initScanner();\n\n // Get version from ClamAV\n const versionResult = yield* Effect.tryPromise({\n try: () => scanner.getVersion(),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: \"Failed to get ClamAV version\",\n details: { error },\n }),\n });\n\n return versionResult.version || \"Unknown\";\n }.bind(this),\n ).pipe(withOperationSpan(\"virus-scan\", \"get-version\", {}));\n }\n}\n\n/**\n * Creates a VirusScanPlugin layer using ClamAV\n *\n * @param config - Optional ClamAV configuration\n * @returns Layer providing VirusScanPlugin\n *\n * @example\n * ```typescript\n * // Use with default configuration\n * const layer = ClamScanPluginLayer();\n *\n * // Use with custom configuration\n * const customLayer = ClamScanPluginLayer({\n * preference: \"clamdscan\",\n * clamdscan_socket: \"/var/run/clamav/clamd.ctl\"\n * });\n * ```\n */\nexport function ClamScanPluginLayer(\n config: ClamScanConfig = {},\n): Layer.Layer<VirusScanPlugin, never, never> {\n return Layer.succeed(VirusScanPlugin, new ClamScanPluginImpl(config));\n}\n"],"mappings":"qXAsEA,IAAM,EAAN,KAAyD,CACvD,SAAoC,KAEpC,YAAY,EAAiC,EAAE,CAAE,CAA7B,KAAA,OAAA,EAMpB,aAAgE,CAC9D,OAAO,EAAO,IACZ,WAAqC,CACnC,GAAI,KAAK,SACP,OAAO,KAAK,SAGd,IAAM,EAAU,MAAO,EAAO,WAAW,CACvC,IAAK,SAEI,MAAM,IAAI,GAAU,CAAC,KAAK,CAC/B,WAAY,KAAK,OAAO,YAAc,YACtC,gBAAiB,KAAK,OAAO,iBAAmB,GAChD,WAAY,KAAK,OAAO,YAAc,GACtC,UAAW,CACT,OAAQ,KAAK,OAAO,iBACpB,KAAM,KAAK,OAAO,eAClB,KAAM,KAAK,OAAO,gBAAkB,KACpC,QAAS,IACT,eAAgB,GACjB,CACD,SAAU,CACR,KAAM,oBACN,cAAe,GACf,OAAQ,GACT,CACF,CAAC,CAEJ,MAAQ,GACN,EAAgB,SAAS,uBAAwB,CAC/C,KAAM,iCAAiC,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GAC7F,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAGF,MADA,MAAK,SAAW,EACT,GACP,KAAK,KAAK,CACb,CAAC,KACA,EAAkB,aAAc,OAAQ,CACtC,kBAAmB,KAAK,OAAO,YAAc,YAC9C,CAAC,CACH,CASH,KAAK,EAA+D,CAClE,OAAO,EAAO,IACZ,WAAqC,CAEnC,IAAM,EAAU,MAAO,KAAK,aAAa,CAGnC,EAAS,EAAG,QAAQ,CACpB,EAAW,mBAAmB,GAAY,GAC1C,EAAe,EAAK,KAAK,EAAQ,EAAS,CAkChD,OA/BA,MAAO,EAAO,WAAW,CACvB,QAAW,EAAG,UAAU,EAAc,EAAM,CAC5C,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,+CACN,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAGa,MAAO,EAAO,WAAW,CACtC,QAAW,EAAQ,WAAW,EAAa,CAC3C,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,sBAAsB,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GAClF,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAAC,KACD,EAAO,IAAK,IAAgB,CAC1B,QAAS,CAAC,EAAW,WACrB,gBAAiB,EAAW,SAAW,EAAE,CAC1C,EAAE,CACH,EAAO,SAEL,EAAO,WAAW,CAChB,QAAW,EAAG,OAAO,EAAa,CAClC,UAAa,IAAA,GACd,CAAC,CAAC,KAAK,EAAO,OAAO,CACvB,CACF,EAGD,KAAK,KAAK,CACb,CAAC,KACA,EAAkB,aAAc,OAAQ,CACtC,iBAAkB,EAAM,WACzB,CAAC,CACH,CAQH,YAAqD,CACnD,OAAO,EAAO,IACZ,WAAqC,CAEnC,IAAM,EAAU,MAAO,KAAK,aAAa,CAYzC,OATsB,MAAO,EAAO,WAAW,CAC7C,QAAW,EAAQ,YAAY,CAC/B,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,+BACN,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,EAEmB,SAAW,WAChC,KAAK,KAAK,CACb,CAAC,KAAK,EAAkB,aAAc,cAAe,EAAE,CAAC,CAAC,GAsB9D,SAAgB,EACd,EAAyB,EAAE,CACiB,CAC5C,OAAO,EAAM,QAAQ,EAAiB,IAAI,EAAmB,EAAO,CAAC"}
1
+ {"version":3,"file":"index.mjs","names":["config: ClamScanConfig"],"sources":["../src/clamscan-plugin.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { UploadistaError } from \"@uploadista/core/errors\";\nimport type { ScanResult, VirusScanPluginShape } from \"@uploadista/core/flow\";\nimport { VirusScanPlugin } from \"@uploadista/core/flow\";\nimport NodeClam from \"clamscan\";\nimport { Effect, Layer } from \"effect\";\n\n/**\n * Configuration options for the ClamAV plugin\n */\nexport interface ClamScanConfig {\n /**\n * Preference for scanning method\n * - \"clamdscan\": Use clamd daemon (faster, recommended)\n * - \"clamscan\": Use command-line binary\n */\n preference?: \"clamdscan\" | \"clamscan\";\n\n /**\n * Path to clamd socket (for daemon mode)\n * Default: /var/run/clamd.scan/clamd.sock\n */\n clamdscan_socket?: string;\n\n /**\n * TCP host for clamd (alternative to socket)\n */\n clamdscan_host?: string;\n\n /**\n * TCP port for clamd\n * Default: 3310\n */\n clamdscan_port?: number;\n\n /**\n * Whether to remove infected files automatically\n * Default: false (not recommended in flow context)\n */\n remove_infected?: boolean;\n\n /**\n * Debug mode for clamscan library\n * Default: false\n */\n debug_mode?: boolean;\n}\n\n/**\n * ClamAV implementation of the VirusScanPlugin\n *\n * This plugin uses the `clamscan` npm package to scan files for viruses\n * using ClamAV antivirus engine. It supports both clamd daemon mode (fast)\n * and binary mode (slower but more portable).\n *\n * @example\n * ```typescript\n * import { ClamScanPluginLayer } from \"@uploadista/flow-security-clamscan\";\n *\n * const program = Effect.gen(function* () {\n * const virusScan = yield* VirusScanPlugin;\n * const result = yield* virusScan.scan(fileBytes);\n * console.log(result.isClean ? \"Clean\" : \"Infected\");\n * }).pipe(Effect.provide(ClamScanPluginLayer));\n * ```\n */\nclass ClamScanPluginImpl implements VirusScanPluginShape {\n private clamscan: NodeClam | null = null;\n\n constructor(private config: ClamScanConfig = {}) {}\n\n /**\n * Initialize the ClamAV scanner\n * This is called lazily on first use\n */\n private async initScanner(): Promise<NodeClam> {\n if (this.clamscan) {\n return this.clamscan;\n }\n\n try {\n // Initialize clamscan with configuration\n const scanner = await new NodeClam().init({\n preference: this.config.preference ?? \"clamdscan\",\n remove_infected: this.config.remove_infected ?? false,\n debug_mode: this.config.debug_mode ?? false,\n clamdscan: {\n socket: this.config.clamdscan_socket,\n host: this.config.clamdscan_host,\n port: this.config.clamdscan_port ?? 3310,\n timeout: 60000,\n local_fallback: true, // Fall back to binary if daemon unavailable\n },\n clamscan: {\n path: \"/usr/bin/clamscan\", // Standard path\n scan_archives: true,\n active: true,\n },\n });\n\n this.clamscan = scanner;\n return scanner;\n } catch (error) {\n // ClamAV not installed or not available\n throw new Error(\n `ClamAV initialization failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n /**\n * Scans a file for viruses using ClamAV\n *\n * @param input - File contents as Uint8Array\n * @returns Effect with scan results\n */\n scan(input: Uint8Array): Effect.Effect<ScanResult, UploadistaError> {\n return Effect.gen(\n function* (this: ClamScanPluginImpl) {\n // Initialize scanner (lazy initialization)\n const scanner = yield* Effect.tryPromise({\n try: () => this.initScanner(),\n catch: (error) =>\n UploadistaError.fromCode(\"CLAMAV_NOT_INSTALLED\", {\n body:\n error instanceof Error\n ? error.message\n : \"ClamAV is not installed or not available\",\n details: { error },\n }),\n });\n\n // Create temporary file path for scanning\n const tmpDir = os.tmpdir();\n const fileName = `uploadista-scan-${randomUUID()}`;\n const tempFilePath = path.join(tmpDir, fileName);\n\n // Write file data to temp file\n yield* Effect.tryPromise({\n try: () => fs.writeFile(tempFilePath, input),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: \"Failed to create temporary file for scanning\",\n details: { error },\n }),\n });\n\n // Scan the file and ensure cleanup\n const result = yield* Effect.tryPromise({\n try: () => scanner.isInfected(tempFilePath),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: `Virus scan failed: ${error instanceof Error ? error.message : String(error)}`,\n details: { error },\n }),\n }).pipe(\n Effect.map((scanResult) => ({\n isClean: !scanResult.isInfected,\n detectedViruses: scanResult.viruses || [],\n })),\n Effect.ensuring(\n // Clean up temporary file (ignore errors)\n Effect.tryPromise({\n try: () => fs.unlink(tempFilePath),\n catch: () => undefined,\n }).pipe(Effect.ignore),\n ),\n );\n\n return result;\n }.bind(this),\n );\n }\n\n /**\n * Gets the ClamAV engine version\n *\n * @returns Effect with version string\n */\n getVersion(): Effect.Effect<string, UploadistaError> {\n return Effect.gen(\n function* (this: ClamScanPluginImpl) {\n // Initialize scanner (lazy initialization)\n const scanner = yield* Effect.tryPromise({\n try: () => this.initScanner(),\n catch: (error) =>\n UploadistaError.fromCode(\"CLAMAV_NOT_INSTALLED\", {\n body:\n error instanceof Error\n ? error.message\n : \"ClamAV is not installed or not available\",\n details: { error },\n }),\n });\n\n // Get version from ClamAV\n const versionResult = yield* Effect.tryPromise({\n try: () => scanner.getVersion(),\n catch: (error) =>\n UploadistaError.fromCode(\"VIRUS_SCAN_FAILED\", {\n body: \"Failed to get ClamAV version\",\n details: { error },\n }),\n });\n\n return versionResult.version || \"Unknown\";\n }.bind(this),\n );\n }\n}\n\n/**\n * Creates a VirusScanPlugin layer using ClamAV\n *\n * @param config - Optional ClamAV configuration\n * @returns Layer providing VirusScanPlugin\n *\n * @example\n * ```typescript\n * // Use with default configuration\n * const layer = ClamScanPluginLayer();\n *\n * // Use with custom configuration\n * const customLayer = ClamScanPluginLayer({\n * preference: \"clamdscan\",\n * clamdscan_socket: \"/var/run/clamav/clamd.ctl\"\n * });\n * ```\n */\nexport function ClamScanPluginLayer(\n config: ClamScanConfig = {},\n): Layer.Layer<VirusScanPlugin, never, never> {\n return Layer.succeed(VirusScanPlugin, new ClamScanPluginImpl(config));\n}\n"],"mappings":"uTAqEA,IAAM,EAAN,KAAyD,CACvD,SAAoC,KAEpC,YAAY,EAAiC,EAAE,CAAE,CAA7B,KAAA,OAAA,EAMpB,MAAc,aAAiC,CAC7C,GAAI,KAAK,SACP,OAAO,KAAK,SAGd,GAAI,CAEF,IAAM,EAAU,MAAM,IAAI,GAAU,CAAC,KAAK,CACxC,WAAY,KAAK,OAAO,YAAc,YACtC,gBAAiB,KAAK,OAAO,iBAAmB,GAChD,WAAY,KAAK,OAAO,YAAc,GACtC,UAAW,CACT,OAAQ,KAAK,OAAO,iBACpB,KAAM,KAAK,OAAO,eAClB,KAAM,KAAK,OAAO,gBAAkB,KACpC,QAAS,IACT,eAAgB,GACjB,CACD,SAAU,CACR,KAAM,oBACN,cAAe,GACf,OAAQ,GACT,CACF,CAAC,CAGF,MADA,MAAK,SAAW,EACT,QACA,EAAO,CAEd,MAAU,MACR,iCAAiC,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GACxF,EAUL,KAAK,EAA+D,CAClE,OAAO,EAAO,IACZ,WAAqC,CAEnC,IAAM,EAAU,MAAO,EAAO,WAAW,CACvC,QAAW,KAAK,aAAa,CAC7B,MAAQ,GACN,EAAgB,SAAS,uBAAwB,CAC/C,KACE,aAAiB,MACb,EAAM,QACN,2CACN,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAGI,EAAS,EAAG,QAAQ,CACpB,EAAW,mBAAmB,GAAY,GAC1C,EAAe,EAAK,KAAK,EAAQ,EAAS,CAkChD,OA/BA,MAAO,EAAO,WAAW,CACvB,QAAW,EAAG,UAAU,EAAc,EAAM,CAC5C,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,+CACN,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAGa,MAAO,EAAO,WAAW,CACtC,QAAW,EAAQ,WAAW,EAAa,CAC3C,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,sBAAsB,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,GAClF,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAAC,KACD,EAAO,IAAK,IAAgB,CAC1B,QAAS,CAAC,EAAW,WACrB,gBAAiB,EAAW,SAAW,EAAE,CAC1C,EAAE,CACH,EAAO,SAEL,EAAO,WAAW,CAChB,QAAW,EAAG,OAAO,EAAa,CAClC,UAAa,IAAA,GACd,CAAC,CAAC,KAAK,EAAO,OAAO,CACvB,CACF,EAGD,KAAK,KAAK,CACb,CAQH,YAAqD,CACnD,OAAO,EAAO,IACZ,WAAqC,CAEnC,IAAM,EAAU,MAAO,EAAO,WAAW,CACvC,QAAW,KAAK,aAAa,CAC7B,MAAQ,GACN,EAAgB,SAAS,uBAAwB,CAC/C,KACE,aAAiB,MACb,EAAM,QACN,2CACN,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,CAYF,OATsB,MAAO,EAAO,WAAW,CAC7C,QAAW,EAAQ,YAAY,CAC/B,MAAQ,GACN,EAAgB,SAAS,oBAAqB,CAC5C,KAAM,+BACN,QAAS,CAAE,QAAO,CACnB,CAAC,CACL,CAAC,EAEmB,SAAW,WAChC,KAAK,KAAK,CACb,GAsBL,SAAgB,EACd,EAAyB,EAAE,CACiB,CAC5C,OAAO,EAAM,QAAQ,EAAiB,IAAI,EAAmB,EAAO,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uploadista/flow-security-clamscan",
3
3
  "type": "module",
4
- "version": "0.0.18-beta.17",
4
+ "version": "0.0.18-beta.2",
5
5
  "description": "ClamAV virus scanning plugin for Uploadista Flow",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -15,8 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "clamscan": "^2.3.3",
18
- "@uploadista/core": "0.0.18-beta.17",
19
- "@uploadista/observability": "0.0.18-beta.17"
18
+ "@uploadista/core": "0.0.18-beta.2"
20
19
  },
21
20
  "peerDependencies": {
22
21
  "effect": "^3.0.0",
@@ -25,11 +24,11 @@
25
24
  "devDependencies": {
26
25
  "@effect/vitest": "0.27.0",
27
26
  "@types/node": "24.10.1",
28
- "effect": "3.19.8",
29
- "tsdown": "0.16.8",
30
- "vitest": "4.0.15",
27
+ "effect": "3.19.6",
28
+ "tsdown": "0.16.6",
29
+ "vitest": "4.0.13",
31
30
  "zod": "4.1.13",
32
- "@uploadista/typescript-config": "0.0.18-beta.17"
31
+ "@uploadista/typescript-config": "0.0.18-beta.2"
33
32
  },
34
33
  "scripts": {
35
34
  "build": "tsdown",
@@ -5,7 +5,6 @@ import * as path from "node:path";
5
5
  import { UploadistaError } from "@uploadista/core/errors";
6
6
  import type { ScanResult, VirusScanPluginShape } from "@uploadista/core/flow";
7
7
  import { VirusScanPlugin } from "@uploadista/core/flow";
8
- import { withOperationSpan } from "@uploadista/observability";
9
8
  import NodeClam from "clamscan";
10
9
  import { Effect, Layer } from "effect";
11
10
 
@@ -77,49 +76,39 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
77
76
  * Initialize the ClamAV scanner
78
77
  * This is called lazily on first use
79
78
  */
80
- private initScanner(): Effect.Effect<NodeClam, UploadistaError> {
81
- return Effect.gen(
82
- function* (this: ClamScanPluginImpl) {
83
- if (this.clamscan) {
84
- return this.clamscan;
85
- }
86
-
87
- const scanner = yield* Effect.tryPromise({
88
- try: async () => {
89
- // Initialize clamscan with configuration
90
- return await new NodeClam().init({
91
- preference: this.config.preference ?? "clamdscan",
92
- remove_infected: this.config.remove_infected ?? false,
93
- debug_mode: this.config.debug_mode ?? false,
94
- clamdscan: {
95
- socket: this.config.clamdscan_socket,
96
- host: this.config.clamdscan_host,
97
- port: this.config.clamdscan_port ?? 3310,
98
- timeout: 60000,
99
- local_fallback: true, // Fall back to binary if daemon unavailable
100
- },
101
- clamscan: {
102
- path: "/usr/bin/clamscan", // Standard path
103
- scan_archives: true,
104
- active: true,
105
- },
106
- });
107
- },
108
- catch: (error) =>
109
- UploadistaError.fromCode("CLAMAV_NOT_INSTALLED", {
110
- body: `ClamAV initialization failed: ${error instanceof Error ? error.message : String(error)}`,
111
- details: { error },
112
- }),
113
- });
114
-
115
- this.clamscan = scanner;
116
- return scanner;
117
- }.bind(this),
118
- ).pipe(
119
- withOperationSpan("virus-scan", "init", {
120
- "scan.preference": this.config.preference ?? "clamdscan",
121
- }),
122
- );
79
+ private async initScanner(): Promise<NodeClam> {
80
+ if (this.clamscan) {
81
+ return this.clamscan;
82
+ }
83
+
84
+ try {
85
+ // Initialize clamscan with configuration
86
+ const scanner = await new NodeClam().init({
87
+ preference: this.config.preference ?? "clamdscan",
88
+ remove_infected: this.config.remove_infected ?? false,
89
+ debug_mode: this.config.debug_mode ?? false,
90
+ clamdscan: {
91
+ socket: this.config.clamdscan_socket,
92
+ host: this.config.clamdscan_host,
93
+ port: this.config.clamdscan_port ?? 3310,
94
+ timeout: 60000,
95
+ local_fallback: true, // Fall back to binary if daemon unavailable
96
+ },
97
+ clamscan: {
98
+ path: "/usr/bin/clamscan", // Standard path
99
+ scan_archives: true,
100
+ active: true,
101
+ },
102
+ });
103
+
104
+ this.clamscan = scanner;
105
+ return scanner;
106
+ } catch (error) {
107
+ // ClamAV not installed or not available
108
+ throw new Error(
109
+ `ClamAV initialization failed: ${error instanceof Error ? error.message : String(error)}`,
110
+ );
111
+ }
123
112
  }
124
113
 
125
114
  /**
@@ -132,7 +121,17 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
132
121
  return Effect.gen(
133
122
  function* (this: ClamScanPluginImpl) {
134
123
  // Initialize scanner (lazy initialization)
135
- const scanner = yield* this.initScanner();
124
+ const scanner = yield* Effect.tryPromise({
125
+ try: () => this.initScanner(),
126
+ catch: (error) =>
127
+ UploadistaError.fromCode("CLAMAV_NOT_INSTALLED", {
128
+ body:
129
+ error instanceof Error
130
+ ? error.message
131
+ : "ClamAV is not installed or not available",
132
+ details: { error },
133
+ }),
134
+ });
136
135
 
137
136
  // Create temporary file path for scanning
138
137
  const tmpDir = os.tmpdir();
@@ -173,10 +172,6 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
173
172
 
174
173
  return result;
175
174
  }.bind(this),
176
- ).pipe(
177
- withOperationSpan("virus-scan", "scan", {
178
- "scan.file_size": input.byteLength,
179
- }),
180
175
  );
181
176
  }
182
177
 
@@ -189,7 +184,17 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
189
184
  return Effect.gen(
190
185
  function* (this: ClamScanPluginImpl) {
191
186
  // Initialize scanner (lazy initialization)
192
- const scanner = yield* this.initScanner();
187
+ const scanner = yield* Effect.tryPromise({
188
+ try: () => this.initScanner(),
189
+ catch: (error) =>
190
+ UploadistaError.fromCode("CLAMAV_NOT_INSTALLED", {
191
+ body:
192
+ error instanceof Error
193
+ ? error.message
194
+ : "ClamAV is not installed or not available",
195
+ details: { error },
196
+ }),
197
+ });
193
198
 
194
199
  // Get version from ClamAV
195
200
  const versionResult = yield* Effect.tryPromise({
@@ -203,7 +208,7 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
203
208
 
204
209
  return versionResult.version || "Unknown";
205
210
  }.bind(this),
206
- ).pipe(withOperationSpan("virus-scan", "get-version", {}));
211
+ );
207
212
  }
208
213
  }
209
214