package notificationservice import ( "encoding/json" "strconv" "strings" ) // normalizeFCMCredentialsJSON returns credential bytes valid for encoding/json and // Firebase's credentials parser. Repairs the common mistake of pasting PEM with // literal newlines inside the JSON "private_key" string (.env multiline breakage). func normalizeFCMCredentialsJSON(raw string) ([]byte, error) { s := strings.TrimSpace(strings.TrimPrefix(raw, "\ufeff")) if json.Valid([]byte(s)) { return []byte(s), nil } if fixed := tryRepairFirebasePrivateKeyNewlines(s); fixed != "" && fixed != s { b := []byte(fixed) if json.Valid(b) { return b, nil } } var m map[string]any err := json.Unmarshal([]byte(strings.TrimSpace(s)), &m) return nil, err } // tryRepairFirebasePrivateKeyNewlines rewrites `"private_key": "...(PEM with real newlines)..."` // using strconv.Quote for the inner PEM. Empty string means no rewrite applied. func tryRepairFirebasePrivateKeyNewlines(s string) string { const beginRSA = "-----BEGIN RSA PRIVATE KEY-----" const endRSA = "-----END RSA PRIVATE KEY-----" const beginPK = "-----BEGIN PRIVATE KEY-----" const endPK = "-----END PRIVATE KEY-----" beginIdx := strings.Index(s, beginRSA) endMarker := endRSA endIdx := strings.Index(s, endMarker) if beginIdx < 0 { beginIdx = strings.Index(s, beginPK) endMarker = endPK endIdx = strings.Index(s, endMarker) } if beginIdx < 0 || endIdx < beginIdx { return "" } endInclusive := endIdx + len(endMarker) const pkAttr = `"private_key"` pkIdx := strings.Index(s, pkAttr) if pkIdx < 0 || pkIdx > beginIdx { return "" } restAfterKey := s[pkIdx+len(pkAttr):] colonRel := strings.Index(restAfterKey, ":") if colonRel < 0 { return "" } searchFrom := pkIdx + len(pkAttr) + colonRel + 1 openQuote := -1 for i := searchFrom; i < beginIdx; i++ { c := s[i] if c == '"' { openQuote = i break } if c != ' ' && c != '\t' && c != '\n' && c != '\r' { return "" } } if openQuote < 0 { return "" } closeQuote := -1 for i := endInclusive; i < len(s); i++ { c := s[i] if c == '"' { closeQuote = i break } if c != ' ' && c != '\t' && c != '\n' && c != '\r' { return "" } } if closeQuote < 0 || closeQuote <= openQuote { return "" } inner := s[openQuote+1 : closeQuote] quoted := strconv.Quote(inner) return s[:openQuote] + quoted + s[closeQuote+1:] }