{"record":{"id":"e3310161167ed0f9","repo":"router-for-me/CLIProxyAPI","slug":"home-jwt-cluster-id-is-required","errorCode":null,"errorMessage":"home jwt cluster_id is required","messagePattern":"home jwt cluster_id is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/home/certificate.go","lineNumber":98,"sourceCode":"\nfunc parseHomeJWTClaims(rawJWT string) (homeJWTClaims, error) {\n\tvar claims homeJWTClaims\n\tparts := strings.Split(strings.TrimSpace(rawJWT), \".\")\n\tif len(parts) != 3 {\n\t\treturn claims, fmt.Errorf(\"home jwt is invalid\")\n\t}\n\tpayload, errDecode := decodeJWTPart(parts[1])\n\tif errDecode != nil {\n\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)","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/home/certificate.go#L80-L116","documentation":"Claim-level validation in parseHomeJWTClaims: the JWT payload's cluster_id claim is empty or whitespace-only. The cluster_id identifies which home cluster the client enrolls into and is mandatory in the enrollment token contract.","triggerScenarios":"The enrollment JWT decodes fine but its payload has no cluster_id (or only whitespace). Fires during home enrollment startup before any network activity.","commonSituations":"Coordinator bug or version mismatch minting incomplete tokens; test tokens hand-built without the claim; payload template drift.","solutions":["Obtain a new enrollment JWT from the coordinator that includes cluster_id","If self-minting, add the claim: \"cluster_id\": \"<your-cluster>\"","Inspect part 2 of the token (base64url-decode) to verify all required claims: certificate_id, cluster_id, ca_fingerprint, enrollment_secret, ip, port"],"exampleFix":"// before (JWT payload)\n{\"certificate_id\":\"client-123\",\"ca_fingerprint\":\"AA:BB\"}\n\n// after (JWT payload)\n{\"certificate_id\":\"client-123\",\"cluster_id\":\"home-1\",\"ca_fingerprint\":\"AA:BB\"}","handlingStrategy":"validation","validationCode":"func requiredClaimsPresent(rawJWT string) error {\n    parts := strings.Split(strings.TrimSpace(rawJWT), \".\")\n    if len(parts) != 3 {\n        return fmt.Errorf(\"not a compact JWT\")\n    }\n    payload, _ := base64.RawURLEncoding.DecodeString(parts[1])\n    for _, claim := range []string{\"certificate_id\", \"cluster_id\", \"ca_fingerprint\", \"enrollment_secret\"} {\n        if !hasClaim(payload, claim) {\n            return fmt.Errorf(\"missing claim %s\", claim)\n        }\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate all required claims client-side before startup so failures surface early with a clear message","Regenerate tokens from the coordinator rather than editing payloads","Alert on coordinator minting regressions (token schema checks in its tests)"],"tags":["jwt","authentication","enrollment","home","validation"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}