{"record":{"id":"c25a9689cdc0a33e","repo":"can1357/oh-my-pi","slug":"destination-option-protocol-must-be-ftp-ftps-or","errorCode":null,"errorMessage":"Destination option protocol must be ftp, ftps, or sftp","messagePattern":"Destination option protocol must be ftp, ftps, or sftp","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts","lineNumber":178,"sourceCode":"function errorCode(error: unknown): unknown {\n\tif (typeof error !== \"object\" || error === null || !(\"code\" in error)) return undefined;\n\treturn error.code;\n}\n\nfunction ftpUploadUrl(protocol: \"ftp\" | \"ftps\", host: string, port: number, destinationPath: string): string {\n\tconst implicitTls = protocol === \"ftps\" && port === 990;\n\tconst scheme = implicitTls ? \"ftps\" : \"ftp\";\n\tconst bracketedHost = host.includes(\":\") && !host.startsWith(\"[\") ? `[${host}]` : host;\n\tconst url = new URL(`${scheme}://${bracketedHost}`);\n\turl.port = String(port);\n\turl.pathname = `/${encodedPath(pathParts(destinationPath))}`;\n\treturn url.toString();\n}\n\nfunction createFtpUploader(config: DestinationRuntimeConfig): BlobUploader {\n\tconst protocol = optionString(config, \"protocol\", \"sftp\");\n\tif (protocol !== \"ftp\" && protocol !== \"ftps\" && protocol !== \"sftp\") {\n\t\tthrow new Error(\"Destination option protocol must be ftp, ftps, or sftp\");\n\t}\n\tconst host = requiredStringOption(config, \"host\");\n\tconst username = requireCredential(config, \"username\");\n\tconst directory = optionString(config, \"path\");\n\tconst publicBase = requiredStringOption(config, \"publicBaseUrl\");\n\thttpBase(publicBase, \"publicBaseUrl\");\n\n\tif (protocol === \"sftp\") {\n\t\tconst port = optionNumber(config, \"port\", 22) ?? 22;\n\t\tconst keyPath = credentialString(config, \"privateKey\");\n\t\tconst password = credentialString(config, \"password\");\n\t\tif (password && !keyPath) {\n\t\t\tthrow new DestinationUnavailableError(\n\t\t\t\t\"ftp\",\n\t\t\t\t\"SFTP password injection is unsupported by the shared SSH transport; configure a private-key path or SSH agent\",\n\t\t\t);\n\t\t}\n\t\tif (keyPath?.includes(\"-----BEGIN\")) {","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/uploaders-self-hosted.ts#L160-L196","documentation":"Thrown by createFtpUploader when the ftp destination's options.protocol is set to a value other than \"ftp\", \"ftps\", or \"sftp\". The broker supports exactly three transfer protocols for the ftp destination id; anything else (including case variants like \"FTP\" or \"SFTP\") is rejected before any upload attempt.","triggerScenarios":"Configuring a destination with destination \"ftp\" and options.protocol set to e.g. \"scp\", \"SFTP\", \"sftps\", \"ftpes\", or an empty string.","commonSituations":"Users conflating scp with sftp; typing the protocol in uppercase because the error message shows uppercased names; inventing ftpes (explicit FTPS) which is not a supported value here; leaving protocol set from a previous template.","solutions":["Set options.protocol to exactly one of \"ftp\", \"ftps\", or \"sftp\" (lowercase).","If you meant SCP over SSH, use \"sftp\" — the shared SSH transport (writeRemoteFile) is used for it.","For explicit FTPS (AUTH TLS on port 21), use \"ftps\" with a port other than 990; the uploader sends curl --ssl-reqd in that case.","Remove the protocol option entirely to get the default (\"sftp\") if that is what you want."],"exampleFix":"// before\n{ \"destination\": \"ftp\", \"options\": { \"protocol\": \"SFTP\", \"host\": \"box.example.com\", \"publicBaseUrl\": \"https://f.example.com\" } }\n// after\n{ \"destination\": \"ftp\", \"options\": { \"protocol\": \"sftp\", \"host\": \"box.example.com\", \"publicBaseUrl\": \"https://f.example.com\" } }","handlingStrategy":"validation","validationCode":"const ALLOWED = new Set(['ftp', 'ftps', 'sftp']);\nconst protocol = config.options?.protocol ?? 'sftp';\nif (!ALLOWED.has(protocol)) throw new Error(`protocol must be one of ftp, ftps, sftp (got ${protocol})`);","typeGuard":"const isFtpProtocol = (v) => v === 'ftp' || v === 'ftps' || v === 'sftp';","tryCatchPattern":"try {\n  const uploader = createSelfHostedUploader('ftp', config);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('must be ftp, ftps, or sftp')) {\n    // fix options.protocol before retrying\n  } else throw err;\n}","preventionTips":["Use a lowercase string-literal union type (e.g. type Protocol = 'ftp'|'ftps'|'sftp') for config parsing.","Normalize case with toLowerCase() when reading user-supplied protocol values.","Document that scp is not supported; sftp covers SSH transfers.","Validate destination options centrally at config-load time rather than at upload time."],"tags":["configuration","ftp","sftp","protocol"],"backgroundTag":"invalid-enum-value","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}