@uploadista/flow-security-nodes 0.0.16-beta.1

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.
@@ -0,0 +1,22 @@
1
+
2
+ 
3
+ > @uploadista/flow-security-nodes@0.0.15 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/nodes
4
+ > tsdown
5
+
6
+ ℹ tsdown v0.16.5 powered by rolldown v1.0.0-beta.50
7
+ ℹ Using tsdown config: /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/security/nodes/tsdown.config.ts
8
+ ℹ entry: src/index.ts
9
+ ℹ tsconfig: tsconfig.json
10
+ ℹ Build start
11
+ ℹ Cleaning 7 files
12
+ ℹ [CJS] dist/index.cjs 1.05 kB │ gzip: 0.58 kB
13
+ ℹ [CJS] 1 files, total: 1.05 kB
14
+ ℹ [ESM] dist/index.mjs 1.08 kB │ gzip: 0.61 kB
15
+ ℹ [ESM] dist/index.mjs.map 5.11 kB │ gzip: 1.95 kB
16
+ ℹ [ESM] dist/index.d.mts.map 0.54 kB │ gzip: 0.28 kB
17
+ ℹ [ESM] dist/index.d.mts 3.18 kB │ gzip: 1.17 kB
18
+ ℹ [ESM] 4 files, total: 9.91 kB
19
+ ℹ [CJS] dist/index.d.cts.map 0.54 kB │ gzip: 0.28 kB
20
+ ℹ [CJS] dist/index.d.cts 3.18 kB │ gzip: 1.16 kB
21
+ ℹ [CJS] 2 files, total: 3.72 kB
22
+ ✔ Build complete in 5257ms
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 uploadista
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @uploadista/flow-security-nodes
2
+
3
+ Security processing nodes for Uploadista Flow, including virus scanning and malware detection.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @uploadista/flow-security-nodes
9
+ # or
10
+ pnpm add @uploadista/flow-security-nodes
11
+ # or
12
+ yarn add @uploadista/flow-security-nodes
13
+ ```
14
+
15
+ ## Features
16
+
17
+ - **Virus Scanning**: Scan files for viruses and malware using pluggable antivirus engines
18
+ - **Configurable Actions**: Choose to fail flow or continue with metadata on virus detection
19
+ - **Effect-based**: Built on Effect-TS for type-safe, composable error handling
20
+ - **Plugin Architecture**: Support for multiple antivirus engines (ClamAV, cloud services, etc.)
21
+
22
+ ## Available Nodes
23
+
24
+ ### Scan Virus Node
25
+
26
+ Scans files for viruses and malware. Requires a `VirusScanPlugin` implementation (e.g., `@uploadista/flow-security-clamscan`).
27
+
28
+ #### Usage
29
+
30
+ ```typescript
31
+ import { createScanVirusNode } from "@uploadista/flow-security-nodes";
32
+ import { Effect } from "effect";
33
+
34
+ const program = Effect.gen(function* () {
35
+ // Create a scan virus node that fails on detection
36
+ const scanNode = yield* createScanVirusNode("virus-scan-1", {
37
+ action: "fail", // Stop flow if virus detected
38
+ timeout: 60000, // 60 second timeout
39
+ });
40
+
41
+ // Or create a node that passes with metadata
42
+ const auditNode = yield* createScanVirusNode("virus-scan-2", {
43
+ action: "pass", // Continue flow even if virus detected
44
+ timeout: 120000, // 2 minute timeout for large files
45
+ });
46
+ });
47
+ ```
48
+
49
+ #### Parameters
50
+
51
+ - `id` (required): Unique node identifier
52
+ - `params` (optional): Configuration object
53
+ - `action`: `"fail"` | `"pass"` (default: `"fail"`)
54
+ - `"fail"`: Mark flow task as FAILED and stop processing when virus detected
55
+ - `"pass"`: Continue processing but add virus metadata to file
56
+ - `timeout`: Maximum scan time in milliseconds (default: 60000, max: 300000)
57
+
58
+ #### Scan Metadata
59
+
60
+ All scan results are stored in `file.metadata.virusScan`:
61
+
62
+ ```typescript
63
+ type VirusScanMetadata = {
64
+ scanned: boolean; // Whether file was scanned
65
+ isClean: boolean; // Whether file is clean (no viruses)
66
+ detectedViruses: string[]; // Array of detected virus names
67
+ scanDate: string; // ISO 8601 timestamp
68
+ engineVersion: string; // Antivirus engine version
69
+ definitionsDate: string; // Virus definitions date
70
+ };
71
+ ```
72
+
73
+ #### Example Flow
74
+
75
+ ```typescript
76
+ import { createFlow } from "@uploadista/core/flow";
77
+ import { createScanVirusNode } from "@uploadista/flow-security-nodes";
78
+ import { ClamScanPluginLayer } from "@uploadista/flow-security-clamscan";
79
+
80
+ const secureUploadFlow = createFlow({
81
+ nodes: [
82
+ // 1. Input node
83
+ createInputNode("input-1"),
84
+
85
+ // 2. Scan for viruses - fail if infected
86
+ createScanVirusNode("scan-1", {
87
+ action: "fail",
88
+ timeout: 60000,
89
+ }),
90
+
91
+ // 3. Process clean files
92
+ createImageResizeNode("resize-1", {
93
+ width: 1920,
94
+ height: 1080,
95
+ }),
96
+
97
+ // 4. Store to S3
98
+ createStorageNode("storage-1", {
99
+ storageId: "my-s3-bucket",
100
+ }),
101
+ ],
102
+ edges: [
103
+ { source: "input-1", target: "scan-1" },
104
+ { source: "scan-1", target: "resize-1" },
105
+ { source: "resize-1", target: "storage-1" },
106
+ ],
107
+ }).pipe(Effect.provide(ClamScanPluginLayer()));
108
+ ```
109
+
110
+ ## Error Codes
111
+
112
+ The scan virus node may return the following error codes:
113
+
114
+ - `VIRUS_DETECTED`: Virus or malware detected in file (when `action: "fail"`)
115
+ - `VIRUS_SCAN_FAILED`: Generic scanning operation failure
116
+ - `CLAMAV_NOT_INSTALLED`: ClamAV or configured antivirus not available
117
+ - `SCAN_TIMEOUT`: Scanning exceeded timeout limit
118
+
119
+ ## Requirements
120
+
121
+ This package requires a `VirusScanPlugin` implementation. See [@uploadista/flow-security-clamscan](../clamscan) for ClamAV support.
122
+
123
+ ## TypeScript
124
+
125
+ This package is written in TypeScript and includes full type definitions.
126
+
127
+ ## License
128
+
129
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ let e=require(`@uploadista/core/errors`),t=require(`@uploadista/core/flow`),n=require(`effect`),r=require(`zod`);const i=r.z.enum([`fail`,`pass`]),a=r.z.object({action:i.default(`fail`),timeout:r.z.number().min(1e3).max(3e5).optional().default(6e4)});function o(r,i={action:`fail`,timeout:6e4}){return n.Effect.gen(function*(){let o=yield*t.VirusScanPlugin,s=a.parse(i);return yield*(0,t.createTransformNode)({id:r,name:`Scan Virus`,description:`Scans files for viruses and malware using ClamAV`,transform:(t,r)=>n.Effect.gen(function*(){let n=yield*o.scan(t),i=yield*o.getVersion(),a={scanned:!0,isClean:n.isClean,detectedViruses:n.detectedViruses,scanDate:new Date().toISOString(),engineVersion:i,definitionsDate:new Date().toISOString()};if(!n.isClean){let t=`Virus detected: ${n.detectedViruses.join(`, `)}`;if(s.action===`fail`)return yield*(0,e.httpFailure)(`VIRUS_DETECTED`,{body:t,details:{scanMetadata:a}})}return{bytes:t,metadata:{...r.metadata,virusScan:a}}})})})}exports.ScanAction=i,exports.ScanVirusParams=a,exports.createScanVirusNode=o;
@@ -0,0 +1,86 @@
1
+ import * as _uploadista_core_flow0 from "@uploadista/core/flow";
2
+ import { ScanMetadata, ScanResult, VirusScanPlugin } from "@uploadista/core/flow";
3
+ import * as _uploadista_core_types0 from "@uploadista/core/types";
4
+ import * as _uploadista_core_errors0 from "@uploadista/core/errors";
5
+ import * as _uploadista_core_upload0 from "@uploadista/core/upload";
6
+ import { Effect } from "effect";
7
+ import { z } from "zod";
8
+
9
+ //#region src/scan-virus-node.d.ts
10
+ /**
11
+ * Scan action to take when a virus is detected
12
+ */
13
+ declare const ScanAction: z.ZodEnum<{
14
+ fail: "fail";
15
+ pass: "pass";
16
+ }>;
17
+ type ScanAction = z.infer<typeof ScanAction>;
18
+ /**
19
+ * Parameters for the Scan Virus node
20
+ */
21
+ declare const ScanVirusParams: z.ZodObject<{
22
+ action: z.ZodDefault<z.ZodEnum<{
23
+ fail: "fail";
24
+ pass: "pass";
25
+ }>>;
26
+ timeout: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
27
+ }, z.core.$strip>;
28
+ type ScanVirusParams = z.infer<typeof ScanVirusParams>;
29
+ /**
30
+ * Creates a Scan Virus node for malware detection
31
+ *
32
+ * Scans files for viruses and malware using the configured VirusScanPlugin
33
+ * (typically ClamAV). Supports configurable actions on detection:
34
+ * - "fail": Stop flow execution when virus detected
35
+ * - "pass": Continue flow with detection metadata
36
+ *
37
+ * All scan results are stored in file.metadata.virusScan for downstream nodes.
38
+ *
39
+ * @param id - Unique node identifier
40
+ * @param params - Configuration parameters for scan behavior
41
+ * @returns Effect that resolves to the configured node
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // Fail on virus detection (default)
46
+ * const failNode = yield* createScanVirusNode("scan-1", {
47
+ * action: "fail"
48
+ * });
49
+ *
50
+ * // Pass through with metadata
51
+ * const passNode = yield* createScanVirusNode("scan-2", {
52
+ * action: "pass",
53
+ * timeout: 120000 // 2 minutes
54
+ * });
55
+ * ```
56
+ */
57
+ declare function createScanVirusNode(id: string, params?: ScanVirusParams): Effect.Effect<_uploadista_core_flow0.FlowNodeData & {
58
+ inputSchema: z.ZodType<_uploadista_core_types0.UploadFile, unknown, z.core.$ZodTypeInternals<_uploadista_core_types0.UploadFile, unknown>>;
59
+ outputSchema: z.ZodType<_uploadista_core_types0.UploadFile, unknown, z.core.$ZodTypeInternals<_uploadista_core_types0.UploadFile, unknown>>;
60
+ run: (args: {
61
+ data: _uploadista_core_types0.UploadFile;
62
+ jobId: string;
63
+ storageId: string;
64
+ flowId: string;
65
+ inputs?: Record<string, unknown>;
66
+ clientId: string | null;
67
+ }) => Effect.Effect<_uploadista_core_flow0.NodeExecutionResult<_uploadista_core_types0.UploadFile>, _uploadista_core_errors0.UploadistaError, never>;
68
+ condition?: {
69
+ field: string;
70
+ operator: string;
71
+ value: unknown;
72
+ };
73
+ multiInput?: boolean;
74
+ multiOutput?: boolean;
75
+ pausable?: boolean;
76
+ retry?: {
77
+ maxRetries?: number;
78
+ retryDelay?: number;
79
+ exponentialBackoff?: boolean;
80
+ };
81
+ } & {
82
+ type: _uploadista_core_flow0.NodeType;
83
+ }, _uploadista_core_errors0.UploadistaError, VirusScanPlugin | _uploadista_core_upload0.UploadServer>;
84
+ //#endregion
85
+ export { ScanAction, type ScanAction as ScanActionType, type ScanMetadata, type ScanResult, ScanVirusParams, type ScanVirusParams as ScanVirusParamsType, createScanVirusNode };
86
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/scan-virus-node.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;cAYa,YAAU,CAAA,CAAA;;;AAAvB,CAAA,CAAA;AACY,KAAA,UAAA,GAAa,CAAA,CAAE,KAAa,CAAA,OAAA,UAAR,CAAA;AAKhC;;;cAAa,iBAAe,CAAA,CAAA;;;;EAAA,CAAA,CAAA,CAAA;EAAA,OAAA,cAAA,cAAA,YAAA,CAAA,CAAA;AAgB5B,CAAA,eAAY,CAAA;AA8BI,KA9BJ,eAAA,GAAkB,CAAA,CAAE,KA8BG,CAAA,OA9BU,eA8BV,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAnB,mBAAA,sBAEN,kBAAoD,MAAA,CAAA,OAAA,sBAAA,CAAA,YAAA;yBAAA,uBAAA,CAAA,UAAA;;;;;;;aA2DoplI;;iEAAA,uBAAA,CAAA,UAAA"}
@@ -0,0 +1,86 @@
1
+ import * as _uploadista_core_errors0 from "@uploadista/core/errors";
2
+ import * as _uploadista_core_flow0 from "@uploadista/core/flow";
3
+ import { ScanMetadata, ScanResult, VirusScanPlugin } from "@uploadista/core/flow";
4
+ import { Effect } from "effect";
5
+ import { z } from "zod";
6
+ import * as _uploadista_core_types0 from "@uploadista/core/types";
7
+ import * as _uploadista_core_upload0 from "@uploadista/core/upload";
8
+
9
+ //#region src/scan-virus-node.d.ts
10
+ /**
11
+ * Scan action to take when a virus is detected
12
+ */
13
+ declare const ScanAction: z.ZodEnum<{
14
+ fail: "fail";
15
+ pass: "pass";
16
+ }>;
17
+ type ScanAction = z.infer<typeof ScanAction>;
18
+ /**
19
+ * Parameters for the Scan Virus node
20
+ */
21
+ declare const ScanVirusParams: z.ZodObject<{
22
+ action: z.ZodDefault<z.ZodEnum<{
23
+ fail: "fail";
24
+ pass: "pass";
25
+ }>>;
26
+ timeout: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
27
+ }, z.core.$strip>;
28
+ type ScanVirusParams = z.infer<typeof ScanVirusParams>;
29
+ /**
30
+ * Creates a Scan Virus node for malware detection
31
+ *
32
+ * Scans files for viruses and malware using the configured VirusScanPlugin
33
+ * (typically ClamAV). Supports configurable actions on detection:
34
+ * - "fail": Stop flow execution when virus detected
35
+ * - "pass": Continue flow with detection metadata
36
+ *
37
+ * All scan results are stored in file.metadata.virusScan for downstream nodes.
38
+ *
39
+ * @param id - Unique node identifier
40
+ * @param params - Configuration parameters for scan behavior
41
+ * @returns Effect that resolves to the configured node
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // Fail on virus detection (default)
46
+ * const failNode = yield* createScanVirusNode("scan-1", {
47
+ * action: "fail"
48
+ * });
49
+ *
50
+ * // Pass through with metadata
51
+ * const passNode = yield* createScanVirusNode("scan-2", {
52
+ * action: "pass",
53
+ * timeout: 120000 // 2 minutes
54
+ * });
55
+ * ```
56
+ */
57
+ declare function createScanVirusNode(id: string, params?: ScanVirusParams): Effect.Effect<_uploadista_core_flow0.FlowNodeData & {
58
+ inputSchema: z.ZodType<_uploadista_core_types0.UploadFile, unknown, z.core.$ZodTypeInternals<_uploadista_core_types0.UploadFile, unknown>>;
59
+ outputSchema: z.ZodType<_uploadista_core_types0.UploadFile, unknown, z.core.$ZodTypeInternals<_uploadista_core_types0.UploadFile, unknown>>;
60
+ run: (args: {
61
+ data: _uploadista_core_types0.UploadFile;
62
+ jobId: string;
63
+ storageId: string;
64
+ flowId: string;
65
+ inputs?: Record<string, unknown>;
66
+ clientId: string | null;
67
+ }) => Effect.Effect<_uploadista_core_flow0.NodeExecutionResult<_uploadista_core_types0.UploadFile>, _uploadista_core_errors0.UploadistaError, never>;
68
+ condition?: {
69
+ field: string;
70
+ operator: string;
71
+ value: unknown;
72
+ };
73
+ multiInput?: boolean;
74
+ multiOutput?: boolean;
75
+ pausable?: boolean;
76
+ retry?: {
77
+ maxRetries?: number;
78
+ retryDelay?: number;
79
+ exponentialBackoff?: boolean;
80
+ };
81
+ } & {
82
+ type: _uploadista_core_flow0.NodeType;
83
+ }, _uploadista_core_errors0.UploadistaError, VirusScanPlugin | _uploadista_core_upload0.UploadServer>;
84
+ //#endregion
85
+ export { ScanAction, type ScanAction as ScanActionType, type ScanMetadata, type ScanResult, ScanVirusParams, type ScanVirusParams as ScanVirusParamsType, createScanVirusNode };
86
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/scan-virus-node.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;cAYa,YAAU,CAAA,CAAA;;;AAAvB,CAAA,CAAA;AACY,KAAA,UAAA,GAAa,CAAA,CAAE,KAAa,CAAA,OAAA,UAAR,CAAA;AAKhC;;;cAAa,iBAAe,CAAA,CAAA;;;;EAAA,CAAA,CAAA,CAAA;EAAA,OAAA,cAAA,cAAA,YAAA,CAAA,CAAA;AAgB5B,CAAA,eAAY,CAAA;AA8BI,KA9BJ,eAAA,GAAkB,CAAA,CAAE,KA8BG,CAAA,OA9BU,eA8BV,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAnB,mBAAA,sBAEN,kBAAoD,MAAA,CAAA,OAAA,sBAAA,CAAA,YAAA;yBAAA,uBAAA,CAAA,UAAA;;;;;;;aA2DoplI;;iEAAA,uBAAA,CAAA,UAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{httpFailure as e}from"@uploadista/core/errors";import{VirusScanPlugin as t,createTransformNode as n}from"@uploadista/core/flow";import{Effect as r}from"effect";import{z as i}from"zod";const a=i.enum([`fail`,`pass`]),o=i.object({action:a.default(`fail`),timeout:i.number().min(1e3).max(3e5).optional().default(6e4)});function s(i,a={action:`fail`,timeout:6e4}){return r.gen(function*(){let s=yield*t,c=o.parse(a);return yield*n({id:i,name:`Scan Virus`,description:`Scans files for viruses and malware using ClamAV`,transform:(t,n)=>r.gen(function*(){let r=yield*s.scan(t),i=yield*s.getVersion(),a={scanned:!0,isClean:r.isClean,detectedViruses:r.detectedViruses,scanDate:new Date().toISOString(),engineVersion:i,definitionsDate:new Date().toISOString()};if(!r.isClean){let t=`Virus detected: ${r.detectedViruses.join(`, `)}`;if(c.action===`fail`)return yield*e(`VIRUS_DETECTED`,{body:t,details:{scanMetadata:a}})}return{bytes:t,metadata:{...n.metadata,virusScan:a}}})})})}export{a as ScanAction,o as ScanVirusParams,s as createScanVirusNode};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["scanMetadata: ScanMetadata"],"sources":["../src/scan-virus-node.ts"],"sourcesContent":["import { httpFailure } from \"@uploadista/core/errors\";\nimport {\n createTransformNode,\n type ScanMetadata,\n VirusScanPlugin,\n} from \"@uploadista/core/flow\";\nimport { Effect } from \"effect\";\nimport { z } from \"zod\";\n\n/**\n * Scan action to take when a virus is detected\n */\nexport const ScanAction = z.enum([\"fail\", \"pass\"]);\nexport type ScanAction = z.infer<typeof ScanAction>;\n\n/**\n * Parameters for the Scan Virus node\n */\nexport const ScanVirusParams = z.object({\n /**\n * Action to take when a virus is detected:\n * - \"fail\": Mark flow task as FAILED and stop processing\n * - \"pass\": Continue processing but add virus metadata to file\n */\n action: ScanAction.default(\"fail\"),\n\n /**\n * Maximum time to wait for scan completion (in milliseconds)\n * Default: 60000ms (60 seconds)\n * Max: 300000ms (5 minutes)\n */\n timeout: z.number().min(1000).max(300000).optional().default(60000),\n});\n\nexport type ScanVirusParams = z.infer<typeof ScanVirusParams>;\n\n/**\n * Creates a Scan Virus node for malware detection\n *\n * Scans files for viruses and malware using the configured VirusScanPlugin\n * (typically ClamAV). Supports configurable actions on detection:\n * - \"fail\": Stop flow execution when virus detected\n * - \"pass\": Continue flow with detection metadata\n *\n * All scan results are stored in file.metadata.virusScan for downstream nodes.\n *\n * @param id - Unique node identifier\n * @param params - Configuration parameters for scan behavior\n * @returns Effect that resolves to the configured node\n *\n * @example\n * ```typescript\n * // Fail on virus detection (default)\n * const failNode = yield* createScanVirusNode(\"scan-1\", {\n * action: \"fail\"\n * });\n *\n * // Pass through with metadata\n * const passNode = yield* createScanVirusNode(\"scan-2\", {\n * action: \"pass\",\n * timeout: 120000 // 2 minutes\n * });\n * ```\n */\nexport function createScanVirusNode(\n id: string,\n params: ScanVirusParams = { action: \"fail\", timeout: 60000 },\n) {\n return Effect.gen(function* () {\n const virusScanService = yield* VirusScanPlugin;\n\n // Validate params\n const validatedParams = ScanVirusParams.parse(params);\n\n return yield* createTransformNode({\n id,\n name: \"Scan Virus\",\n description: \"Scans files for viruses and malware using ClamAV\",\n transform: (inputBytes, file) =>\n Effect.gen(function* () {\n // Perform virus scan\n const scanResult = yield* virusScanService.scan(inputBytes);\n\n // Get engine version for metadata\n const engineVersion = yield* virusScanService.getVersion();\n\n // Build comprehensive scan metadata\n const scanMetadata: ScanMetadata = {\n scanned: true,\n isClean: scanResult.isClean,\n detectedViruses: scanResult.detectedViruses,\n scanDate: new Date().toISOString(),\n engineVersion,\n definitionsDate: new Date().toISOString(), // TODO: Get actual definitions date from plugin\n };\n\n // Check if virus was detected\n if (!scanResult.isClean) {\n // Build error message with detected viruses\n const virusList = scanResult.detectedViruses.join(\", \");\n const message = `Virus detected: ${virusList}`;\n\n // Handle based on configured action\n if (validatedParams.action === \"fail\") {\n // Fail the flow task\n return yield* httpFailure(\"VIRUS_DETECTED\", {\n body: message,\n details: { scanMetadata },\n });\n }\n // action === \"pass\": Continue with metadata (handled below)\n }\n\n // Return file with scan metadata (clean or pass action)\n return {\n bytes: inputBytes, // Pass through original bytes unchanged\n metadata: {\n ...file.metadata,\n virusScan: scanMetadata,\n },\n };\n }),\n });\n });\n}\n"],"mappings":"+LAYA,MAAa,EAAa,EAAE,KAAK,CAAC,OAAQ,OAAO,CAAC,CAMrC,EAAkB,EAAE,OAAO,CAMtC,OAAQ,EAAW,QAAQ,OAAO,CAOlC,QAAS,EAAE,QAAQ,CAAC,IAAI,IAAK,CAAC,IAAI,IAAO,CAAC,UAAU,CAAC,QAAQ,IAAM,CACpE,CAAC,CAgCF,SAAgB,EACd,EACA,EAA0B,CAAE,OAAQ,OAAQ,QAAS,IAAO,CAC5D,CACA,OAAO,EAAO,IAAI,WAAa,CAC7B,IAAM,EAAmB,MAAO,EAG1B,EAAkB,EAAgB,MAAM,EAAO,CAErD,OAAO,MAAO,EAAoB,CAChC,KACA,KAAM,aACN,YAAa,mDACb,WAAY,EAAY,IACtB,EAAO,IAAI,WAAa,CAEtB,IAAM,EAAa,MAAO,EAAiB,KAAK,EAAW,CAGrD,EAAgB,MAAO,EAAiB,YAAY,CAGpDA,EAA6B,CACjC,QAAS,GACT,QAAS,EAAW,QACpB,gBAAiB,EAAW,gBAC5B,SAAU,IAAI,MAAM,CAAC,aAAa,CAClC,gBACA,gBAAiB,IAAI,MAAM,CAAC,aAAa,CAC1C,CAGD,GAAI,CAAC,EAAW,QAAS,CAGvB,IAAM,EAAU,mBADE,EAAW,gBAAgB,KAAK,KAAK,GAIvD,GAAI,EAAgB,SAAW,OAE7B,OAAO,MAAO,EAAY,iBAAkB,CAC1C,KAAM,EACN,QAAS,CAAE,eAAc,CAC1B,CAAC,CAMN,MAAO,CACL,MAAO,EACP,SAAU,CACR,GAAG,EAAK,SACR,UAAW,EACZ,CACF,EACD,CACL,CAAC,EACF"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@uploadista/flow-security-nodes",
3
+ "type": "module",
4
+ "version": "0.0.16-beta.1",
5
+ "description": "Security processing nodes for Uploadista Flow",
6
+ "license": "MIT",
7
+ "author": "Uploadista",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ },
16
+ "dependencies": {
17
+ "effect": "3.19.4",
18
+ "zod": "4.1.12",
19
+ "@uploadista/core": "0.0.16-beta.1"
20
+ },
21
+ "devDependencies": {
22
+ "@effect/vitest": "0.27.0",
23
+ "@types/node": "24.10.1",
24
+ "tsdown": "0.16.5",
25
+ "vitest": "4.0.8",
26
+ "@uploadista/typescript-config": "0.0.16-beta.1"
27
+ },
28
+ "scripts": {
29
+ "build": "tsdown",
30
+ "format": "biome format --write ./src",
31
+ "lint": "biome lint --write ./src",
32
+ "check": "biome check --write ./src",
33
+ "test": "vitest",
34
+ "test:run": "vitest run",
35
+ "test:watch": "vitest watch"
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ // Security processing nodes
2
+
3
+ // Import from core packages to ensure proper type resolution in generated declarations
4
+ // These imports force tsdown to create namespace aliases instead of inlining types
5
+ import type {} from "@uploadista/core/types";
6
+ import type {} from "@uploadista/core/upload";
7
+
8
+ // Re-export types from core for convenience
9
+ export type { ScanMetadata, ScanResult } from "@uploadista/core/flow";
10
+ export {
11
+ createScanVirusNode,
12
+ ScanAction,
13
+ type ScanAction as ScanActionType,
14
+ ScanVirusParams,
15
+ type ScanVirusParams as ScanVirusParamsType,
16
+ } from "./scan-virus-node";
@@ -0,0 +1,125 @@
1
+ import { httpFailure } from "@uploadista/core/errors";
2
+ import {
3
+ createTransformNode,
4
+ type ScanMetadata,
5
+ VirusScanPlugin,
6
+ } from "@uploadista/core/flow";
7
+ import { Effect } from "effect";
8
+ import { z } from "zod";
9
+
10
+ /**
11
+ * Scan action to take when a virus is detected
12
+ */
13
+ export const ScanAction = z.enum(["fail", "pass"]);
14
+ export type ScanAction = z.infer<typeof ScanAction>;
15
+
16
+ /**
17
+ * Parameters for the Scan Virus node
18
+ */
19
+ export const ScanVirusParams = z.object({
20
+ /**
21
+ * Action to take when a virus is detected:
22
+ * - "fail": Mark flow task as FAILED and stop processing
23
+ * - "pass": Continue processing but add virus metadata to file
24
+ */
25
+ action: ScanAction.default("fail"),
26
+
27
+ /**
28
+ * Maximum time to wait for scan completion (in milliseconds)
29
+ * Default: 60000ms (60 seconds)
30
+ * Max: 300000ms (5 minutes)
31
+ */
32
+ timeout: z.number().min(1000).max(300000).optional().default(60000),
33
+ });
34
+
35
+ export type ScanVirusParams = z.infer<typeof ScanVirusParams>;
36
+
37
+ /**
38
+ * Creates a Scan Virus node for malware detection
39
+ *
40
+ * Scans files for viruses and malware using the configured VirusScanPlugin
41
+ * (typically ClamAV). Supports configurable actions on detection:
42
+ * - "fail": Stop flow execution when virus detected
43
+ * - "pass": Continue flow with detection metadata
44
+ *
45
+ * All scan results are stored in file.metadata.virusScan for downstream nodes.
46
+ *
47
+ * @param id - Unique node identifier
48
+ * @param params - Configuration parameters for scan behavior
49
+ * @returns Effect that resolves to the configured node
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // Fail on virus detection (default)
54
+ * const failNode = yield* createScanVirusNode("scan-1", {
55
+ * action: "fail"
56
+ * });
57
+ *
58
+ * // Pass through with metadata
59
+ * const passNode = yield* createScanVirusNode("scan-2", {
60
+ * action: "pass",
61
+ * timeout: 120000 // 2 minutes
62
+ * });
63
+ * ```
64
+ */
65
+ export function createScanVirusNode(
66
+ id: string,
67
+ params: ScanVirusParams = { action: "fail", timeout: 60000 },
68
+ ) {
69
+ return Effect.gen(function* () {
70
+ const virusScanService = yield* VirusScanPlugin;
71
+
72
+ // Validate params
73
+ const validatedParams = ScanVirusParams.parse(params);
74
+
75
+ return yield* createTransformNode({
76
+ id,
77
+ name: "Scan Virus",
78
+ description: "Scans files for viruses and malware using ClamAV",
79
+ transform: (inputBytes, file) =>
80
+ Effect.gen(function* () {
81
+ // Perform virus scan
82
+ const scanResult = yield* virusScanService.scan(inputBytes);
83
+
84
+ // Get engine version for metadata
85
+ const engineVersion = yield* virusScanService.getVersion();
86
+
87
+ // Build comprehensive scan metadata
88
+ const scanMetadata: ScanMetadata = {
89
+ scanned: true,
90
+ isClean: scanResult.isClean,
91
+ detectedViruses: scanResult.detectedViruses,
92
+ scanDate: new Date().toISOString(),
93
+ engineVersion,
94
+ definitionsDate: new Date().toISOString(), // TODO: Get actual definitions date from plugin
95
+ };
96
+
97
+ // Check if virus was detected
98
+ if (!scanResult.isClean) {
99
+ // Build error message with detected viruses
100
+ const virusList = scanResult.detectedViruses.join(", ");
101
+ const message = `Virus detected: ${virusList}`;
102
+
103
+ // Handle based on configured action
104
+ if (validatedParams.action === "fail") {
105
+ // Fail the flow task
106
+ return yield* httpFailure("VIRUS_DETECTED", {
107
+ body: message,
108
+ details: { scanMetadata },
109
+ });
110
+ }
111
+ // action === "pass": Continue with metadata (handled below)
112
+ }
113
+
114
+ // Return file with scan metadata (clean or pass action)
115
+ return {
116
+ bytes: inputBytes, // Pass through original bytes unchanged
117
+ metadata: {
118
+ ...file.metadata,
119
+ virusScan: scanMetadata,
120
+ },
121
+ };
122
+ }),
123
+ });
124
+ });
125
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "@uploadista/typescript-config/server.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "./",
5
+ "paths": {
6
+ "@/*": ["./src/*"]
7
+ },
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
11
+ "types": []
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: {
5
+ index: "src/index.ts",
6
+ },
7
+ minify: true,
8
+ format: ["esm", "cjs"],
9
+ dts: true,
10
+ outDir: "dist",
11
+ });