SFTP Source
Poll an SFTP server for new files with support for password and private key authentication.
Overview
The SFTP source connects to a remote SFTP server and polls a specified directory for new files at a configurable interval. When files matching the pattern are found, their contents are downloaded and passed into the channel pipeline.
SFTP is widely used in healthcare for secure file transfers between organizations, particularly for batch lab results, insurance claims, pharmacy data feeds, and clinical document exchange. Unlike plain FTP, SFTP encrypts all data in transit over SSH.
Configuration
listener:
type: sftp
sftp:
host: sftp.lab-corp.example.com
port: 22
poll_interval: 5m
directory: /outbound/results
file_pattern: "*.csv"
move_to: /outbound/archive
error_dir: /outbound/errors
auth:
type: private_key
username: intu-service
private_key_path: ${SFTP_KEY_PATH}
sort_by: date
Properties
22.30s, 5m. Defaults to 1m.*.csv, RESULTS_*.txt. Defaults to *.password and private_key.name, date, or size. Defaults to name.Auth Sub-Properties
| Type | Properties | Description |
|---|---|---|
password |
username, password |
Standard username/password authentication over SSH. |
private_key |
username, private_key_path, passphrase (optional) |
SSH key-based authentication. The passphrase is only required if the key is encrypted. |
Complete Example
Poll a remote SFTP server for CSV lab result files every 5 minutes, archive processed files, and route failures to an error directory.
id: sftp-lab-results
enabled: true
listener:
type: sftp
sftp:
host: sftp.lab-corp.example.com
port: 22
poll_interval: 5m
directory: /outbound/results
file_pattern: "*.csv"
move_to: /outbound/archive
error_dir: /outbound/errors
auth:
type: private_key
username: intu-service
private_key_path: ${SFTP_KEY_PATH}
sort_by: date
validator:
runtime: node
entrypoint: validator.ts
transformer:
runtime: node
entrypoint: transformer.ts
destinations:
- lab-results-fhir
TypeScript Transformer Example
This transformer parses a CSV file containing lab results and converts each row into a FHIR DiagnosticReport resource.
import { Message, TransformResult } from "@intu/sdk";
interface LabResult {
patientMrn: string;
testCode: string;
testName: string;
value: string;
unit: string;
referenceRange: string;
status: string;
collectedDate: string;
}
function parseCsv(content: string): LabResult[] {
const lines = content.trim().split("\n");
const headers = lines[0].split(",").map((h) => h.trim());
const results: LabResult[] = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(",").map((v) => v.trim());
results.push({
patientMrn: values[headers.indexOf("patient_mrn")] || "",
testCode: values[headers.indexOf("test_code")] || "",
testName: values[headers.indexOf("test_name")] || "",
value: values[headers.indexOf("value")] || "",
unit: values[headers.indexOf("unit")] || "",
referenceRange: values[headers.indexOf("reference_range")] || "",
status: values[headers.indexOf("status")] || "",
collectedDate: values[headers.indexOf("collected_date")] || "",
});
}
return results;
}
export default function transform(msg: Message): TransformResult {
const csvContent = msg.body as string;
const results = parseCsv(csvContent);
if (results.length === 0) {
return { success: false, error: "No lab results found in CSV" };
}
const bundle = {
resourceType: "Bundle",
type: "batch",
entry: results.map((lab) => ({
resource: {
resourceType: "DiagnosticReport",
status: lab.status === "final" ? "final" : "preliminary",
code: {
coding: [
{
system: "http://loinc.org",
code: lab.testCode,
display: lab.testName,
},
],
},
subject: {
identifier: {
system: "urn:oid:2.16.840.1.113883.19.5",
value: lab.patientMrn,
},
},
effectiveDateTime: lab.collectedDate,
result: [
{
display: `${lab.testName}: ${lab.value} ${lab.unit}`,
},
],
},
})),
};
return {
success: true,
body: JSON.stringify(bundle),
contentType: "application/fhir+json",
};
}