{"record":{"id":"01ffc412947cd298","repo":"router-for-me/CLIProxyAPI","slug":"home-jwt-target-address-is-invalid","errorCode":null,"errorMessage":"home jwt target address is invalid","messagePattern":"home jwt target address is invalid","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/home/certificate.go","lineNumber":107,"sourceCode":"\t\treturn claims, errDecode\n\t}\n\tif errUnmarshal := json.Unmarshal(payload, &claims); errUnmarshal != nil {\n\t\treturn claims, errUnmarshal\n\t}\n\tif strings.TrimSpace(claims.CertificateID) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt certificate_id is required\")\n\t}\n\tif strings.TrimSpace(claims.ClusterID) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt cluster_id is required\")\n\t}\n\tif normalizeFingerprint(claims.CAFingerprint) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt ca_fingerprint is required\")\n\t}\n\tif strings.TrimSpace(claims.EnrollmentSecret) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt enrollment_secret is required\")\n\t}\n\tif strings.TrimSpace(claims.IP) == \"\" || claims.Port <= 0 {\n\t\treturn claims, fmt.Errorf(\"home jwt target address is invalid\")\n\t}\n\treturn claims, nil\n}\n\nfunc decodeJWTPart(part string) ([]byte, error) {\n\tif decoded, errDecode := base64.RawURLEncoding.DecodeString(part); errDecode == nil {\n\t\treturn decoded, nil\n\t}\n\treturn base64.URLEncoding.DecodeString(part)\n}\n\nfunc defaultCertificatePaths() (certificatePaths, error) {\n\thomeDir, errHome := os.UserHomeDir()\n\tif errHome != nil {\n\t\treturn certificatePaths{}, errHome\n\t}\n\tdir := filepath.Join(homeDir, \".cli-proxy-api\")\n\treturn certificatePaths{","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/home/certificate.go#L89-L125","documentation":"Final claim check in parseHomeJWTClaims: the target address is invalid because the ip claim is empty/whitespace OR the port claim is <= 0. Together these tell the enrolled client where to reach the home coordinator for mTLS.","triggerScenarios":"JWT payload has ip: \"\" (or missing, which decodes to empty) or port: 0 / negative — e.g. a token minted with a hostname in the wrong claim, or port defaulted to zero because the field was omitted.","commonSituations":"Coordinator config misbinding (address split incorrectly into ip/port), token built from a template missing the port, or port passed as a string that failed to unmarshal into the numeric claim.","solutions":["Use a token whose payload has a non-empty ip and a positive port (e.g. \"ip\":\"10.0.0.5\",\"port\":8443)","If you control minting, log the claims (never the secret) at mint time to catch this early","Check for a coordinator version/config regression if tokens previously worked"],"exampleFix":"// before (JWT payload)\n{\"ip\":\"\",\"port\":0, ...}\n\n// after (JWT payload)\n{\"ip\":\"203.0.113.10\",\"port\":8443, ...}","handlingStrategy":"validation","validationCode":"func targetAddressOK(payload []byte) bool {\n    var c struct {\n        IP   string `json:\"ip\"`\n        Port int   `json:\"port\"`\n    }\n    return json.Unmarshal(payload, &c) == nil &&\n        strings.TrimSpace(c.IP) != \"\" && c.Port > 0\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Confirm ip and port claims are populated when minting tokens","Watch for coordinator config regressions where address parsing drops one field","Prefer validating the full claim set in one pre-flight check"],"tags":["jwt","authentication","enrollment","home","network"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}