Skip to content

Commit 6a39a39

Browse files
authored
Add html filtering (#1356)
* Add html content sanitizer * Add basic html sanitization * . * Correct licenses and scripts to ignore vendor directory * Use singleton * Simplify
1 parent 05e0f8f commit 6a39a39

File tree

15 files changed

+255
-14
lines changed

15 files changed

+255
-14
lines changed

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ require (
66
github.com/google/go-github/v76 v76.0.0
77
github.com/josephburnett/jd v1.9.2
88
github.com/mark3labs/mcp-go v0.36.0
9+
github.com/microcosm-cc/bluemonday v1.0.27
910
github.com/migueleliasweb/go-github-mock v1.3.0
1011
github.com/spf13/cobra v1.10.1
1112
github.com/spf13/viper v1.21.0
1213
github.com/stretchr/testify v1.11.1
1314
)
1415

1516
require (
17+
github.com/aymerick/douceur v0.2.0 // indirect
1618
github.com/bahlo/generic-list-go v0.2.0 // indirect
1719
github.com/buger/jsonparser v1.1.1 // indirect
1820
github.com/go-openapi/jsonpointer v0.19.5 // indirect
1921
github.com/go-openapi/swag v0.21.1 // indirect
2022
github.com/google/go-github/v71 v71.0.0 // indirect
23+
github.com/gorilla/css v1.0.1 // indirect
2124
github.com/gorilla/mux v1.8.0 // indirect
2225
github.com/invopop/jsonschema v0.13.0 // indirect
2326
github.com/josharian/intern v1.0.0 // indirect
@@ -26,6 +29,7 @@ require (
2629
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
2730
go.yaml.in/yaml/v3 v3.0.4 // indirect
2831
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
32+
golang.org/x/net v0.26.0 // indirect
2933
gopkg.in/yaml.v2 v2.4.0 // indirect
3034
)
3135

go.sum

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
2+
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
13
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
24
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
35
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
@@ -24,14 +26,14 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2426
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
2527
github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30=
2628
github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M=
27-
github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=
28-
github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak=
2929
github.com/google/go-github/v76 v76.0.0 h1:MCa9VQn+VG5GG7Y7BAkBvSRUN3o+QpaEOuZwFPJmdFA=
3030
github.com/google/go-github/v76 v76.0.0/go.mod h1:38+d/8pYDO4fBLYfBhXF5EKO0wA3UkXBjfmQapFsNCQ=
3131
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
3232
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
3333
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3434
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
35+
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
36+
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
3537
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
3638
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
3739
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -57,6 +59,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
5759
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
5860
github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis=
5961
github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
62+
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
63+
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
6064
github.com/migueleliasweb/go-github-mock v1.3.0 h1:2sVP9JEMB2ubQw1IKto3/fzF51oFC6eVWOOFDgQoq88=
6165
github.com/migueleliasweb/go-github-mock v1.3.0/go.mod h1:ipQhV8fTcj/G6m7BKzin08GaJ/3B5/SonRAkgrk0zCY=
6266
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -104,6 +108,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
104108
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
105109
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
106110
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
111+
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
112+
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
107113
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
108114
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
109115
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=

pkg/github/issues.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,15 @@ func fragmentToIssue(fragment IssueFragment) *github.Issue {
212212

213213
return &github.Issue{
214214
Number: github.Ptr(int(fragment.Number)),
215-
Title: github.Ptr(sanitize.FilterInvisibleCharacters(string(fragment.Title))),
215+
Title: github.Ptr(sanitize.Sanitize(string(fragment.Title))),
216216
CreatedAt: &github.Timestamp{Time: fragment.CreatedAt.Time},
217217
UpdatedAt: &github.Timestamp{Time: fragment.UpdatedAt.Time},
218218
User: &github.User{
219219
Login: github.Ptr(string(fragment.Author.Login)),
220220
},
221221
State: github.Ptr(string(fragment.State)),
222222
ID: github.Ptr(fragment.DatabaseID),
223-
Body: github.Ptr(sanitize.FilterInvisibleCharacters(string(fragment.Body))),
223+
Body: github.Ptr(sanitize.Sanitize(string(fragment.Body))),
224224
Labels: foundLabels,
225225
Comments: github.Ptr(int(fragment.Comments.TotalCount)),
226226
}
@@ -327,10 +327,10 @@ func GetIssue(ctx context.Context, client *github.Client, owner string, repo str
327327
// Sanitize title/body on response
328328
if issue != nil {
329329
if issue.Title != nil {
330-
issue.Title = github.Ptr(sanitize.FilterInvisibleCharacters(*issue.Title))
330+
issue.Title = github.Ptr(sanitize.Sanitize(*issue.Title))
331331
}
332332
if issue.Body != nil {
333-
issue.Body = github.Ptr(sanitize.FilterInvisibleCharacters(*issue.Body))
333+
issue.Body = github.Ptr(sanitize.Sanitize(*issue.Body))
334334
}
335335
}
336336

pkg/github/pullrequests.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ func GetPullRequest(ctx context.Context, client *github.Client, owner, repo stri
127127
// sanitize title/body on response
128128
if pr != nil {
129129
if pr.Title != nil {
130-
pr.Title = github.Ptr(sanitize.FilterInvisibleCharacters(*pr.Title))
130+
pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title))
131131
}
132132
if pr.Body != nil {
133-
pr.Body = github.Ptr(sanitize.FilterInvisibleCharacters(*pr.Body))
133+
pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body))
134134
}
135135
}
136136

@@ -821,10 +821,10 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun
821821
continue
822822
}
823823
if pr.Title != nil {
824-
pr.Title = github.Ptr(sanitize.FilterInvisibleCharacters(*pr.Title))
824+
pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title))
825825
}
826826
if pr.Body != nil {
827-
pr.Body = github.Ptr(sanitize.FilterInvisibleCharacters(*pr.Body))
827+
pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body))
828828
}
829829
}
830830

pkg/sanitize/sanitize.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
package sanitize
22

3+
import (
4+
"sync"
5+
6+
"github.com/microcosm-cc/bluemonday"
7+
)
8+
9+
var policy *bluemonday.Policy
10+
var policyOnce sync.Once
11+
12+
func Sanitize(input string) string {
13+
return FilterHTMLTags(FilterInvisibleCharacters(input))
14+
}
15+
316
// FilterInvisibleCharacters removes invisible or control characters that should not appear
417
// in user-facing titles or bodies. This includes:
518
// - Unicode tag characters: U+E0001, U+E0020–U+E007F
@@ -20,6 +33,41 @@ func FilterInvisibleCharacters(input string) string {
2033
return string(out)
2134
}
2235

36+
func FilterHTMLTags(input string) string {
37+
if input == "" {
38+
return input
39+
}
40+
return getPolicy().Sanitize(input)
41+
}
42+
43+
func getPolicy() *bluemonday.Policy {
44+
policyOnce.Do(func() {
45+
p := bluemonday.StrictPolicy()
46+
47+
p.AllowElements(
48+
"b", "blockquote", "br", "code", "em",
49+
"h1", "h2", "h3", "h4", "h5", "h6",
50+
"hr", "i", "li", "ol", "p", "pre",
51+
"strong", "sub", "sup", "table", "tbody",
52+
"td", "th", "thead", "tr", "ul",
53+
"a", "img",
54+
)
55+
56+
p.AllowAttrs("href").OnElements("a")
57+
p.AllowURLSchemes("https")
58+
p.RequireParseableURLs(true)
59+
p.RequireNoFollowOnLinks(true)
60+
p.RequireNoReferrerOnLinks(true)
61+
p.AddTargetBlankToFullyQualifiedLinks(true)
62+
63+
p.AllowImages()
64+
p.AllowAttrs("src", "alt", "title").OnElements("img")
65+
66+
policy = p
67+
})
68+
return policy
69+
}
70+
2371
func shouldRemoveRune(r rune) bool {
2472
switch r {
2573
case 0x200B, // ZERO WIDTH SPACE

pkg/sanitize/sanitize_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,69 @@ func TestShouldRemoveRune(t *testing.T) {
186186
})
187187
}
188188
}
189+
190+
func TestFilterHtmlTags(t *testing.T) {
191+
tests := []struct {
192+
name string
193+
input string
194+
expected string
195+
}{
196+
{
197+
name: "empty string",
198+
input: "",
199+
expected: "",
200+
},
201+
{
202+
name: "allowed simple tags preserved",
203+
input: "<b>bold</b>",
204+
expected: "<b>bold</b>",
205+
},
206+
{
207+
name: "multiple allowed tags",
208+
input: "<b>bold</b> and <em>italic</em>",
209+
expected: "<b>bold</b> and <em>italic</em>",
210+
},
211+
{
212+
name: "code tag preserved",
213+
input: "<code>fmt.Println(\"hi\")</code>",
214+
expected: "<code>fmt.Println(&#34;hi&#34;)</code>", // quotes are escaped by sanitizer
215+
},
216+
{
217+
name: "disallowed script removed entirely",
218+
input: "<script>alert(1)</script>",
219+
expected: "", // StrictPolicy should drop script element and contents
220+
},
221+
{
222+
name: "allow anchor with https href",
223+
input: "Click <a href=\"https://example.com\">here</a> now",
224+
expected: "Click <a href=\"https://example.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">here</a> now",
225+
},
226+
{
227+
name: "anchor removed but inner text kept",
228+
input: "before <a href='https://example.com' onclick='alert(1)' title='foo' alt='bar'>link</a> after",
229+
expected: "before <a href=\"https://example.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">link</a> after",
230+
},
231+
{
232+
name: "image removed (no textual fallback)",
233+
input: "<img src='x' alt='y'>",
234+
expected: "<img src=\"x\" alt=\"y\">", // images are allowed via AllowImages()
235+
},
236+
{
237+
name: "mixed allowed and disallowed",
238+
input: "<b>bold</b> <script>alert(1)</script> <em>italic</em>",
239+
expected: "<b>bold</b> <em>italic</em>",
240+
},
241+
{
242+
name: "idempotent sanitization",
243+
input: FilterHTMLTags("<b>bold</b> and <em>italic</em>"),
244+
expected: "<b>bold</b> and <em>italic</em>",
245+
},
246+
}
247+
248+
for _, tt := range tests {
249+
t.Run(tt.name, func(t *testing.T) {
250+
result := FilterHTMLTags(tt.input)
251+
assert.Equal(t, tt.expected, result)
252+
})
253+
}
254+
}

script/licenses

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ for goos in linux darwin windows ; do
1414
#
1515
# Normally these warnings are packages containing non go code, which may or may not require explicit attribution,
1616
# depending on the license.
17-
GOOS="${goos}" go-licenses save ./... --save_path="${TEMPDIR}/${goos}" --force || echo "Ignore warnings"
18-
GOOS="${goos}" go-licenses report ./... --template .github/licenses.tmpl > third-party-licenses.${goos}.md || echo "Ignore warnings"
17+
GOOS="${goos}" GOFLAGS=-mod=mod go-licenses save ./... --save_path="${TEMPDIR}/${goos}" --force || echo "Ignore warnings"
18+
GOOS="${goos}" GOFLAGS=-mod=mod go-licenses report ./... --template .github/licenses.tmpl > third-party-licenses.${goos}.md || echo "Ignore warnings"
1919
cp -fR "${TEMPDIR}/${goos}"/* third-party/
2020
done
2121

script/licenses-check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ for goos in linux darwin windows ; do
88
#
99
# Normally these warnings are packages containing non go code, which may or may not require explicit attribution,
1010
# depending on the license.
11-
GOOS="${goos}" go-licenses report ./... --template .github/licenses.tmpl > third-party-licenses.${goos}.copy.md || echo "Ignore warnings"
11+
GOOS="${goos}" GOFLAGS=-mod=mod go-licenses report ./... --template .github/licenses.tmpl > third-party-licenses.${goos}.copy.md || echo "Ignore warnings"
1212
if ! diff -s third-party-licenses.${goos}.copy.md third-party-licenses.${goos}.md; then
13-
echo "License check failed.\n\nPlease update the license file by running \`.script/licenses\` and committing the output."
13+
printf "License check failed.\n\nPlease update the license file by running \`.script/licenses\` and committing the output."
1414
rm -f third-party-licenses.${goos}.copy.md
1515
exit 1
1616
fi

third-party-licenses.darwin.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The following open source dependencies are used to build the [github/github-mcp-
77
Some packages may only be included on certain architectures or operating systems.
88

99

10+
- [github.com/aymerick/douceur](https://pkg.go.dev/github.com/aymerick/douceur) ([MIT](https://github.com/aymerick/douceur/blob/v0.2.0/LICENSE))
1011
- [github.com/bahlo/generic-list-go](https://pkg.go.dev/github.com/bahlo/generic-list-go) ([BSD-3-Clause](https://github.com/bahlo/generic-list-go/blob/v0.2.0/LICENSE))
1112
- [github.com/buger/jsonparser](https://pkg.go.dev/github.com/buger/jsonparser) ([MIT](https://github.com/buger/jsonparser/blob/v1.1.1/LICENSE))
1213
- [github.com/fsnotify/fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify) ([BSD-3-Clause](https://github.com/fsnotify/fsnotify/blob/v1.9.0/LICENSE))
@@ -18,12 +19,14 @@ Some packages may only be included on certain architectures or operating systems
1819
- [github.com/google/go-github/v76/github](https://pkg.go.dev/github.com/google/go-github/v76/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v76.0.0/LICENSE))
1920
- [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE))
2021
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
22+
- [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE))
2123
- [github.com/gorilla/mux](https://pkg.go.dev/github.com/gorilla/mux) ([BSD-3-Clause](https://github.com/gorilla/mux/blob/v1.8.0/LICENSE))
2224
- [github.com/invopop/jsonschema](https://pkg.go.dev/github.com/invopop/jsonschema) ([MIT](https://github.com/invopop/jsonschema/blob/v0.13.0/COPYING))
2325
- [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v1.9.2/LICENSE))
2426
- [github.com/josharian/intern](https://pkg.go.dev/github.com/josharian/intern) ([MIT](https://github.com/josharian/intern/blob/v1.0.0/license.md))
2527
- [github.com/mailru/easyjson](https://pkg.go.dev/github.com/mailru/easyjson) ([MIT](https://github.com/mailru/easyjson/blob/v0.7.7/LICENSE))
2628
- [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.36.0/LICENSE))
29+
- [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
2730
- [github.com/migueleliasweb/go-github-mock/src/mock](https://pkg.go.dev/github.com/migueleliasweb/go-github-mock/src/mock) ([MIT](https://github.com/migueleliasweb/go-github-mock/blob/v1.3.0/LICENSE))
2831
- [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.4/LICENSE))
2932
- [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.11.0/LICENSE))
@@ -41,6 +44,7 @@ Some packages may only be included on certain architectures or operating systems
4144
- [github.com/yudai/golcs](https://pkg.go.dev/github.com/yudai/golcs) ([MIT](https://github.com/yudai/golcs/blob/ecda9a501e82/LICENSE))
4245
- [go.yaml.in/yaml/v3](https://pkg.go.dev/go.yaml.in/yaml/v3) ([MIT](https://github.com/yaml/go-yaml/blob/v3.0.4/LICENSE))
4346
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/8a7402ab:LICENSE))
47+
- [golang.org/x/net/html](https://pkg.go.dev/golang.org/x/net/html) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.26.0:LICENSE))
4448
- [golang.org/x/sys/unix](https://pkg.go.dev/golang.org/x/sys/unix) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.31.0:LICENSE))
4549
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.28.0:LICENSE))
4650
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))

0 commit comments

Comments
 (0)