Repair multiline PEM inside private_key before Firebase init; add unit test; use normalized JSON for credentials. Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
2.3 KiB
Go
96 lines
2.3 KiB
Go
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:]
|
|
}
|