@uploadista/flow-security-clamscan 0.0.20-beta.8 → 0.0.20-beta.9

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,6 +1,6 @@
1
1
 
2
2
  
3
- > @uploadista/flow-security-clamscan@0.0.20-beta.7 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan
3
+ > @uploadista/flow-security-clamscan@0.0.20-beta.8 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/clamscan
4
4
  > tsc --noEmit && tsdown
5
5
 
6
6
  ℹ tsdown v0.18.0 powered by rolldown v1.0.0-beta.53
@@ -12,12 +12,12 @@
12
12
  ℹ [CJS] dist/index.cjs 2.89 kB │ gzip: 1.22 kB
13
13
  ℹ [CJS] 1 files, total: 2.89 kB
14
14
  ℹ [CJS] dist/index.d.cts.map 0.25 kB │ gzip: 0.17 kB
15
- ℹ [CJS] dist/index.d.cts 1.65 kB │ gzip: 0.73 kB
16
- ℹ [CJS] 2 files, total: 1.90 kB
17
- ✔ Build complete in 1859ms
18
- ℹ [ESM] dist/index.mjs 2.27 kB │ gzip: 0.98 kB
19
- ℹ [ESM] dist/index.mjs.map 9.40 kB │ gzip: 2.93 kB
15
+ ℹ [CJS] dist/index.d.cts 1.68 kB │ gzip: 0.73 kB
16
+ ℹ [CJS] 2 files, total: 1.92 kB
17
+ ℹ [ESM] dist/index.mjs 2.27 kB │ gzip: 0.97 kB
18
+ ℹ [ESM] dist/index.mjs.map 9.43 kB │ gzip: 2.94 kB
20
19
  ℹ [ESM] dist/index.d.mts.map 0.25 kB │ gzip: 0.17 kB
21
- ℹ [ESM] dist/index.d.mts 1.65 kB │ gzip: 0.73 kB
22
- ℹ [ESM] 4 files, total: 13.57 kB
23
- ✔ Build complete in 1910ms
20
+ ℹ [ESM] dist/index.d.mts 1.68 kB │ gzip: 0.73 kB
21
+ ℹ [ESM] 4 files, total: 13.62 kB
22
+ ✔ Build complete in 7004ms
23
+ ✔ Build complete in 7005ms
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(`@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.virusScanPlugin=v;
package/dist/index.d.cts CHANGED
@@ -4,9 +4,9 @@ import { Layer } from "effect";
4
4
  //#region src/clamscan-plugin.d.ts
5
5
 
6
6
  /**
7
- * Configuration options for the ClamAV plugin
7
+ * Configuration options for the ClamAV Virus Scan plugin
8
8
  */
9
- interface ClamScanConfig {
9
+ interface VirusScanPluginConfig {
10
10
  /**
11
11
  * Preference for scanning method
12
12
  * - "clamdscan": Use clamd daemon (faster, recommended)
@@ -39,7 +39,7 @@ interface ClamScanConfig {
39
39
  debug_mode?: boolean;
40
40
  }
41
41
  /**
42
- * Creates a VirusScanPlugin layer using ClamAV
42
+ * Creates a Virus Scan Plugin layer using ClamAV
43
43
  *
44
44
  * @param config - Optional ClamAV configuration
45
45
  * @returns Layer providing VirusScanPlugin
@@ -56,7 +56,7 @@ interface ClamScanConfig {
56
56
  * });
57
57
  * ```
58
58
  */
59
- declare function ClamScanPluginLayer(config?: ClamScanConfig): Layer.Layer<VirusScanPlugin, never, never>;
59
+ declare function virusScanPlugin(config?: VirusScanPluginConfig): Layer.Layer<VirusScanPlugin, never, never>;
60
60
  //#endregion
61
- export { type ClamScanConfig, ClamScanPluginLayer, type ScanMetadata, type ScanResult };
61
+ export { type ScanMetadata, type ScanResult, type VirusScanPluginConfig, virusScanPlugin };
62
62
  //# sourceMappingURL=index.d.cts.map
@@ -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":";;;;;;;AAcA;AAqNgB,UArNC,qBAAA,CAqNc;EACrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBADM,eAAA,UACN,wBACP,KAAA,CAAM,MAAM"}
package/dist/index.d.mts CHANGED
@@ -4,9 +4,9 @@ import { Layer } from "effect";
4
4
  //#region src/clamscan-plugin.d.ts
5
5
 
6
6
  /**
7
- * Configuration options for the ClamAV plugin
7
+ * Configuration options for the ClamAV Virus Scan plugin
8
8
  */
9
- interface ClamScanConfig {
9
+ interface VirusScanPluginConfig {
10
10
  /**
11
11
  * Preference for scanning method
12
12
  * - "clamdscan": Use clamd daemon (faster, recommended)
@@ -39,7 +39,7 @@ interface ClamScanConfig {
39
39
  debug_mode?: boolean;
40
40
  }
41
41
  /**
42
- * Creates a VirusScanPlugin layer using ClamAV
42
+ * Creates a Virus Scan Plugin layer using ClamAV
43
43
  *
44
44
  * @param config - Optional ClamAV configuration
45
45
  * @returns Layer providing VirusScanPlugin
@@ -56,7 +56,7 @@ interface ClamScanConfig {
56
56
  * });
57
57
  * ```
58
58
  */
59
- declare function ClamScanPluginLayer(config?: ClamScanConfig): Layer.Layer<VirusScanPlugin, never, never>;
59
+ declare function virusScanPlugin(config?: VirusScanPluginConfig): Layer.Layer<VirusScanPlugin, never, never>;
60
60
  //#endregion
61
- export { type ClamScanConfig, ClamScanPluginLayer, type ScanMetadata, type ScanResult };
61
+ export { type ScanMetadata, type ScanResult, type VirusScanPluginConfig, virusScanPlugin };
62
62
  //# sourceMappingURL=index.d.mts.map
@@ -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":";;;;;;;AAcA;AAqNgB,UArNC,qBAAA,CAqNc;EACrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBADM,eAAA,UACN,wBACP,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{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 virusScanPlugin};
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: VirusScanPluginConfig"],"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 Virus Scan plugin\n */\nexport interface VirusScanPluginConfig {\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: VirusScanPluginConfig = {}) {}\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 Virus Scan Plugin 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 virusScanPlugin(\n config: VirusScanPluginConfig = {},\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,EAAwC,EAAE,CAAE,CAApC,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,EAAgC,EAAE,CACU,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.20-beta.8",
4
+ "version": "0.0.20-beta.9",
5
5
  "description": "ClamAV virus scanning plugin for Uploadista Flow",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -15,8 +15,8 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "clamscan": "^2.3.3",
18
- "@uploadista/core": "0.0.20-beta.8",
19
- "@uploadista/observability": "0.0.20-beta.8"
18
+ "@uploadista/core": "0.0.20-beta.9",
19
+ "@uploadista/observability": "0.0.20-beta.9"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "effect": "^3.0.0",
@@ -29,7 +29,7 @@
29
29
  "tsdown": "0.18.0",
30
30
  "vitest": "4.0.15",
31
31
  "zod": "4.2.0",
32
- "@uploadista/typescript-config": "0.0.20-beta.8"
32
+ "@uploadista/typescript-config": "0.0.20-beta.9"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsc --noEmit && tsdown",
@@ -10,9 +10,9 @@ import NodeClam from "clamscan";
10
10
  import { Effect, Layer } from "effect";
11
11
 
12
12
  /**
13
- * Configuration options for the ClamAV plugin
13
+ * Configuration options for the ClamAV Virus Scan plugin
14
14
  */
15
- export interface ClamScanConfig {
15
+ export interface VirusScanPluginConfig {
16
16
  /**
17
17
  * Preference for scanning method
18
18
  * - "clamdscan": Use clamd daemon (faster, recommended)
@@ -71,7 +71,7 @@ export interface ClamScanConfig {
71
71
  class ClamScanPluginImpl implements VirusScanPluginShape {
72
72
  private clamscan: NodeClam | null = null;
73
73
 
74
- constructor(private config: ClamScanConfig = {}) {}
74
+ constructor(private config: VirusScanPluginConfig = {}) {}
75
75
 
76
76
  /**
77
77
  * Initialize the ClamAV scanner
@@ -208,7 +208,7 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
208
208
  }
209
209
 
210
210
  /**
211
- * Creates a VirusScanPlugin layer using ClamAV
211
+ * Creates a Virus Scan Plugin layer using ClamAV
212
212
  *
213
213
  * @param config - Optional ClamAV configuration
214
214
  * @returns Layer providing VirusScanPlugin
@@ -225,8 +225,8 @@ class ClamScanPluginImpl implements VirusScanPluginShape {
225
225
  * });
226
226
  * ```
227
227
  */
228
- export function ClamScanPluginLayer(
229
- config: ClamScanConfig = {},
228
+ export function virusScanPlugin(
229
+ config: VirusScanPluginConfig = {},
230
230
  ): Layer.Layer<VirusScanPlugin, never, never> {
231
231
  return Layer.succeed(VirusScanPlugin, new ClamScanPluginImpl(config));
232
232
  }
package/src/index.ts CHANGED
@@ -8,4 +8,4 @@ import type {} from "@uploadista/core/upload";
8
8
  export type { ScanMetadata, ScanResult } from "@uploadista/core/flow";
9
9
 
10
10
  // Export plugin implementation
11
- export { type ClamScanConfig, ClamScanPluginLayer } from "./clamscan-plugin";
11
+ export { type VirusScanPluginConfig, virusScanPlugin } from "./clamscan-plugin";