Skip to content

Commit a937375

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 51e22fb + 5a61430 commit a937375

170 files changed

Lines changed: 7683 additions & 343 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/cmd/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.166
1+
0.1.168

backend/cmd/server/wire_gen.go

Lines changed: 12 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/go.mod

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ require (
1616
github.com/coder/websocket v1.8.14
1717
github.com/dgraph-io/ristretto v0.2.0
1818
github.com/gin-gonic/gin v1.9.1
19-
github.com/golang-jwt/jwt/v5 v5.2.2
19+
github.com/go-webauthn/webauthn v0.17.4
20+
github.com/golang-jwt/jwt/v5 v5.3.1
2021
github.com/google/uuid v1.6.0
2122
github.com/google/wire v0.7.0
2223
github.com/gorilla/websocket v1.5.3
@@ -97,6 +98,7 @@ require (
9798
github.com/fatih/color v1.18.0 // indirect
9899
github.com/felixge/httpsnoop v1.0.4 // indirect
99100
github.com/fsnotify/fsnotify v1.7.0 // indirect
101+
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
100102
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
101103
github.com/gin-contrib/sse v0.1.0 // indirect
102104
github.com/go-logr/logr v1.4.3 // indirect
@@ -106,9 +108,12 @@ require (
106108
github.com/go-playground/locales v0.14.1 // indirect
107109
github.com/go-playground/universal-translator v0.18.1 // indirect
108110
github.com/go-playground/validator/v10 v10.14.0 // indirect
111+
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
112+
github.com/go-webauthn/x v0.2.6 // indirect
109113
github.com/goccy/go-json v0.10.2 // indirect
110114
github.com/google/go-cmp v0.7.0 // indirect
111115
github.com/google/go-querystring v1.1.0 // indirect
116+
github.com/google/go-tpm v0.9.8 // indirect
112117
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
113118
github.com/hashicorp/hcl v1.0.0 // indirect
114119
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
@@ -137,6 +142,7 @@ require (
137142
github.com/opencontainers/go-digest v1.0.0 // indirect
138143
github.com/opencontainers/image-spec v1.1.1 // indirect
139144
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
145+
github.com/philhofer/fwd v1.2.0 // indirect
140146
github.com/pkg/errors v0.9.1 // indirect
141147
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
142148
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
@@ -158,10 +164,12 @@ require (
158164
github.com/testcontainers/testcontainers-go v0.40.0 // indirect
159165
github.com/tidwall/match v1.1.1 // indirect
160166
github.com/tidwall/pretty v1.2.0 // indirect
167+
github.com/tinylib/msgp v1.6.4 // indirect
161168
github.com/tklauser/go-sysconf v0.3.12 // indirect
162169
github.com/tklauser/numcpus v0.6.1 // indirect
163170
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
164171
github.com/ugorji/go/codec v1.2.11 // indirect
172+
github.com/x448/float16 v0.8.4 // indirect
165173
github.com/yuin/gopher-lua v1.1.1 // indirect
166174
github.com/yusufpapurcu/wmi v1.2.4 // indirect
167175
github.com/zclconf/go-cty v1.14.4 // indirect

backend/go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
128128
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
129129
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
130130
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
131+
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
132+
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
131133
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
132134
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
133135
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -153,16 +155,26 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
153155
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
154156
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
155157
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
158+
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
159+
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
160+
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
161+
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
162+
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
163+
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
156164
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
157165
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
158166
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
159167
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
168+
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
169+
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
160170
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
161171
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
162172
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
163173
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
164174
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
165175
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
176+
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
177+
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
166178
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
167179
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
168180
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
@@ -261,6 +273,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
261273
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
262274
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
263275
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
276+
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
277+
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
264278
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
265279
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
266280
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -356,6 +370,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
356370
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
357371
github.com/tiktoken-go/tokenizer v0.8.0 h1:drHWno2Zx3eAm/hk/LmvBKXPpSImB7BRyh/ru4+3Q7Y=
358372
github.com/tiktoken-go/tokenizer v0.8.0/go.mod h1:pTmPz4r14MV3JkUGAmAcdLdYhSxN68MCjrP+EoxBdx0=
373+
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
374+
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
359375
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
360376
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
361377
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -366,6 +382,8 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
366382
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
367383
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
368384
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
385+
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
386+
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
369387
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
370388
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
371389
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=

backend/internal/config/config.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type Config struct {
7373
Ops OpsConfig `mapstructure:"ops"`
7474
JWT JWTConfig `mapstructure:"jwt"`
7575
Totp TotpConfig `mapstructure:"totp"`
76+
WebAuthn WebAuthnConfig `mapstructure:"webauthn"`
7677
LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
7778
WeChat WeChatConnectConfig `mapstructure:"wechat_connect"`
7879
OIDC OIDCConnectConfig `mapstructure:"oidc_connect"`
@@ -686,6 +687,16 @@ type CORSConfig struct {
686687
AllowCredentials bool `mapstructure:"allow_credentials"`
687688
}
688689

690+
// WebAuthnConfig configures this deployment as a WebAuthn relying party.
691+
// RPID and RPOrigins are security boundaries and must never be inferred from
692+
// untrusted request Host or Origin headers.
693+
type WebAuthnConfig struct {
694+
Enabled bool `mapstructure:"enabled"`
695+
RPDisplayName string `mapstructure:"rp_display_name"`
696+
RPID string `mapstructure:"rp_id"`
697+
RPOrigins []string `mapstructure:"rp_origins"`
698+
}
699+
689700
const MaxForwardedClientIPHeaders = 16
690701

691702
type ForwardedClientIPSettings struct {
@@ -1875,12 +1886,21 @@ func setDefaults() {
18751886
viper.SetDefault("cors.allowed_origins", []string{})
18761887
viper.SetDefault("cors.allow_credentials", true)
18771888

1889+
// WebAuthn / Passkeys are opt-in because every deployment must explicitly
1890+
// declare its relying-party domain and trusted browser origins.
1891+
viper.SetDefault("webauthn.enabled", false)
1892+
viper.SetDefault("webauthn.rp_display_name", "Sub2API")
1893+
viper.SetDefault("webauthn.rp_id", "")
1894+
viper.SetDefault("webauthn.rp_origins", []string{})
1895+
18781896
// Security
18791897
viper.SetDefault("security.url_allowlist.enabled", false)
18801898
viper.SetDefault("security.url_allowlist.upstream_hosts", []string{
18811899
"api.openai.com",
18821900
"api.anthropic.com",
18831901
"api.kimi.com",
1902+
"api.moonshot.ai",
1903+
"api.moonshot.cn",
18841904
"open.bigmodel.cn",
18851905
"api.minimaxi.com",
18861906
"generativelanguage.googleapis.com",
@@ -2591,6 +2611,43 @@ func (c *Config) Validate() error {
25912611
}
25922612
warnIfInsecureURL("server.frontend_url", c.Server.FrontendURL)
25932613
}
2614+
if c.WebAuthn.Enabled {
2615+
c.WebAuthn.RPDisplayName = strings.TrimSpace(c.WebAuthn.RPDisplayName)
2616+
c.WebAuthn.RPID = strings.ToLower(strings.TrimSpace(c.WebAuthn.RPID))
2617+
c.WebAuthn.RPOrigins = normalizeStringSlice(c.WebAuthn.RPOrigins)
2618+
if c.WebAuthn.RPDisplayName == "" {
2619+
return fmt.Errorf("webauthn.rp_display_name is required when passkeys are enabled")
2620+
}
2621+
if c.WebAuthn.RPID == "" {
2622+
return fmt.Errorf("webauthn.rp_id is required when passkeys are enabled")
2623+
}
2624+
if strings.Contains(c.WebAuthn.RPID, "://") || strings.ContainsAny(c.WebAuthn.RPID, "/:") {
2625+
return fmt.Errorf("webauthn.rp_id must be a domain without scheme, port, or path")
2626+
}
2627+
if len(c.WebAuthn.RPOrigins) == 0 {
2628+
return fmt.Errorf("webauthn.rp_origins must contain at least one origin when passkeys are enabled")
2629+
}
2630+
for i, origin := range c.WebAuthn.RPOrigins {
2631+
u, err := url.Parse(origin)
2632+
if err != nil || u.Scheme == "" || u.Host == "" {
2633+
return fmt.Errorf("webauthn.rp_origins contains invalid origin %q", origin)
2634+
}
2635+
if u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "" {
2636+
return fmt.Errorf("webauthn.rp_origins entry %q must not include userinfo, path, query, or fragment", origin)
2637+
}
2638+
u.Scheme = strings.ToLower(u.Scheme)
2639+
u.Host = strings.ToLower(u.Host)
2640+
host := strings.ToLower(u.Hostname())
2641+
localDevelopment := host == "localhost" || host == "127.0.0.1" || host == "::1"
2642+
if u.Scheme != "https" && (u.Scheme != "http" || !localDevelopment) {
2643+
return fmt.Errorf("webauthn.rp_origins entry %q must use HTTPS (HTTP is allowed only for localhost)", origin)
2644+
}
2645+
if host != c.WebAuthn.RPID && !strings.HasSuffix(host, "."+c.WebAuthn.RPID) {
2646+
return fmt.Errorf("webauthn.rp_origins entry %q is not within relying party ID %q", origin, c.WebAuthn.RPID)
2647+
}
2648+
c.WebAuthn.RPOrigins[i] = u.Scheme + "://" + u.Host
2649+
}
2650+
}
25942651
if c.JWT.ExpireHour <= 0 {
25952652
return fmt.Errorf("jwt.expire_hour must be positive")
25962653
}

backend/internal/config/config_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,21 @@ func TestLoadDefaultSecurityToggles(t *testing.T) {
755755
if !cfg.Security.ResponseHeaders.Enabled {
756756
t.Fatalf("ResponseHeaders.Enabled = false, want true")
757757
}
758+
759+
wantHosts := []string{
760+
"api.kimi.com",
761+
"api.moonshot.ai",
762+
"api.moonshot.cn",
763+
}
764+
hostSet := make(map[string]struct{}, len(cfg.Security.URLAllowlist.UpstreamHosts))
765+
for _, h := range cfg.Security.URLAllowlist.UpstreamHosts {
766+
hostSet[h] = struct{}{}
767+
}
768+
for _, want := range wantHosts {
769+
if _, ok := hostSet[want]; !ok {
770+
t.Fatalf("URLAllowlist.UpstreamHosts missing %q; got %v", want, cfg.Security.URLAllowlist.UpstreamHosts)
771+
}
772+
}
758773
}
759774

760775
func TestLoadDefaultServerMode(t *testing.T) {

0 commit comments

Comments
 (0)