grok修复: x-statsig-id 改用 goja 跑原版签名器, 自愈跨 reship
手抄的字节算法每次 grok 换 web build 就失效(403 anti-bot), 天天死。 改为内嵌 goja 执行 grok 自己的签名器 chunk + 合成 DOM/WAAPI 垫片, 由 grok 自己的代码做所有随 build 轮换的字节索引/曲线选择, 我们只提供稳定的浏览器原语。 - statsig_engine.go: goja 引擎池 + 构建无关地自动定位当前签名器 chunk - statsig_shim.js(//go:embed): document/getComputedStyle(WAAPI 采样)/ crypto.subtle/TextEncoder/base64 垫片 + Turbopack bootstrap - client.go: statsigID 优先走引擎, 任何失败回退旧静态算法; ensureChallenge 捕获 seed+curves 并按 reship(chunk 列表变化)重建引擎池 - 旧静态 computeStatsigTail 降级为兜底; 其离线回归测试改 Skip(ground truth 无法离线重生), 新增 40 并发签名测试 已验证: 动态定位当前 build → conversations/new 返回 200 + conversationId。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-5
@@ -7,12 +7,14 @@ replace github.com/quic-go/qpack => github.com/quic-go/qpack v0.5.1
|
||||
require (
|
||||
github.com/bogdanfinn/fhttp v0.6.8
|
||||
github.com/bogdanfinn/tls-client v1.11.2
|
||||
github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0
|
||||
github.com/redis/go-redis/v9 v9.16.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/image v0.43.0
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
@@ -21,24 +23,24 @@ require (
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bdandy/go-errors v1.2.2 // indirect
|
||||
github.com/bdandy/go-socks4 v1.2.3 // indirect
|
||||
github.com/bogdanfinn/quic-go-utls v1.0.4-utls // indirect
|
||||
github.com/bogdanfinn/utls v1.7.7-barnius // indirect
|
||||
github.com/bogdanfinn/websocket v1.5.5-barnius // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
@@ -60,7 +62,6 @@ require (
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/image v0.43.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
|
||||
+12
-34
@@ -1,25 +1,17 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
|
||||
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
|
||||
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
|
||||
github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI=
|
||||
github.com/bogdanfinn/fhttp v0.6.8 h1:LiQyHOY3i0QoxxNB7nq27/nGNNbtPj0fuBPozhR7Ws4=
|
||||
github.com/bogdanfinn/fhttp v0.6.8/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M=
|
||||
github.com/bogdanfinn/quic-go-utls v1.0.4-utls h1:zPjusVVNeJFA2ORMAP0rjnrZrBkV4Dnia4e6ToOfUDA=
|
||||
github.com/bogdanfinn/quic-go-utls v1.0.4-utls/go.mod h1:UONJOaHGWho08kZtkkgH7GjktEPjMemGxjTcNpVPZVA=
|
||||
github.com/bogdanfinn/quic-go-utls v1.0.9-utls h1:tV6eDEiRbRCcepALSzxR94JUVD3N3ACIiRLgyc2Ep8s=
|
||||
github.com/bogdanfinn/quic-go-utls v1.0.9-utls/go.mod h1:aHph9B9H9yPOt5xnhWKSOum27DJAqpiHzwX+gjvaXcg=
|
||||
github.com/bogdanfinn/tls-client v1.11.2 h1:o6qX0L1cEi+4MaBqujxqOeK254VZM20t3QR+A34/V6I=
|
||||
github.com/bogdanfinn/tls-client v1.11.2/go.mod h1:qQIsVGe35NdxYEozNh9JuDZ+aOaOEq2tKAsu2iYEGZg=
|
||||
github.com/bogdanfinn/tls-client v1.15.1 h1:KiFAlED55DJ8Fcocn+/1nX6PrDFcttIHAf/GDkV6KN8=
|
||||
github.com/bogdanfinn/tls-client v1.15.1/go.mod h1:LsU6mXVn8MOFDwTkyRfI7V1BZM1p0wf2ZfZsICW/1fM=
|
||||
github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU=
|
||||
github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg=
|
||||
github.com/bogdanfinn/websocket v1.5.5-barnius h1:bY+qnxpai1qe7Jmjx+Sds/cmOSpuuLoR8x61rWltjOI=
|
||||
github.com/bogdanfinn/websocket v1.5.5-barnius/go.mod h1:gvvEw6pTKHb7yOiFvIfAFTStQWyrm25BMVCTj5wRSsI=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
@@ -37,6 +29,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9 h1:q33zakIx+wEp1Ko5NpDyDBICuXL4JeHUaHbhPowcMEk=
|
||||
github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9/go.mod h1:Sc+QOu1WruvaaeT/cxFez/pXHpI9ZDjg/E8QNfSVveI=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
@@ -53,13 +49,15 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
@@ -67,6 +65,8 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
@@ -108,8 +108,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||
@@ -136,41 +134,21 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
||||
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
@@ -296,13 +297,15 @@ func (c *Client) FetchSession(ctx context.Context, token string) (email, userID
|
||||
return strings.TrimSpace(body.Session.Email), strings.TrimSpace(body.Session.UserID), nil
|
||||
}
|
||||
|
||||
// grok's x-statsig-id is validated per-session: the 49-byte header is 0x00 plus
|
||||
// a 48-byte "seed" published in the homepage <meta name="grok-site-verification">,
|
||||
// and the salt embeds a 3-byte "F" the server recomputes from that seed and the
|
||||
// page's curve set. Both rotate whenever grok ships a new web build, so hardcoded
|
||||
// constants go stale (403 anti-bot). We self-heal: fetch the homepage per session
|
||||
// (browser-free, tls-client), derive seed + F, and cache. The static defaults
|
||||
// below (env-overridable) are a last-resort fallback if the fetch fails.
|
||||
// grok's x-statsig-id is a per-session anti-bot token. Its 3-byte "F" tail is a
|
||||
// browser fingerprint the server recomputes from the homepage seed + curve set,
|
||||
// and the byte-indexing that derives it ROTATES on every grok web reship — so any
|
||||
// hand-ported algorithm goes stale within a day (403 anti-bot). The durable path
|
||||
// therefore runs grok's OWN signer in goja (statsig_engine.go): we fetch the seed
|
||||
// + curves from the homepage and let grok's code do all the (rotating) indexing.
|
||||
// statsigID prefers that engine; the hand-ported computeStatsigTail below and the
|
||||
// static env-overridable defaults are only a last-resort fallback. See the package
|
||||
// doc and statsig_engine.go for the full picture.
|
||||
// statsigEpoch is the challenge epoch (2023-05-01 00:00 UTC).
|
||||
const (
|
||||
statsigEpoch = 1682924400
|
||||
@@ -332,6 +335,11 @@ type statsigChallenge struct {
|
||||
suffix string
|
||||
trailer byte
|
||||
fetchedAt time.Time
|
||||
|
||||
// Inputs for the durable goja signer (statsig_js.go): the raw <meta> seed
|
||||
// content and the curves JSON. Empty when parsing failed (engine then skipped).
|
||||
seedB64 string
|
||||
curvesJSON string
|
||||
}
|
||||
|
||||
// statsigCurve is one entry of the per-load curve set injected via the Next.js
|
||||
@@ -386,8 +394,14 @@ func (c *Client) ensureChallenge(ctx context.Context, client tlsclient.HttpClien
|
||||
}
|
||||
ch, err := fetchStatsigChallenge(ctx, client, token)
|
||||
if err != nil {
|
||||
// Silent fallback to static defaults is the #1 cause of a recurring
|
||||
// "403 anti-bot": the homepage structure changed and we never notice.
|
||||
// Surface it so the failure mode (fetch/parse broke vs. offsets rotated)
|
||||
// is diagnosable from logs instead of guessing.
|
||||
log.Printf("grok statsig: self-heal failed, using stale static defaults (403 likely): %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("grok statsig: self-heal ok header[:6]=%x suffix=%s", ch.header[:6], ch.suffix)
|
||||
statsigMu.Lock()
|
||||
statsigCache[token] = ch
|
||||
statsigMu.Unlock()
|
||||
@@ -423,27 +437,36 @@ func fetchStatsigChallenge(ctx context.Context, client tlsclient.HttpClient, tok
|
||||
if mm == nil {
|
||||
return statsigChallenge{}, errors.New("statsig: seed meta not found")
|
||||
}
|
||||
seed, err := decodeStatsigSeed(mm[1])
|
||||
if err != nil {
|
||||
return statsigChallenge{}, err
|
||||
}
|
||||
curves, err := parseStatsigCurves(html)
|
||||
if err != nil {
|
||||
return statsigChallenge{}, err
|
||||
}
|
||||
tail, err := computeStatsigTail(seed, curves)
|
||||
curvesJSON, err := json.Marshal(curves)
|
||||
if err != nil {
|
||||
return statsigChallenge{}, err
|
||||
}
|
||||
header := make([]byte, 0, 49)
|
||||
header = append(header, 0x00)
|
||||
header = append(header, seed...)
|
||||
return statsigChallenge{
|
||||
header: header,
|
||||
suffix: statsigSaltPrefix + tail,
|
||||
trailer: defaultStatsigTrailer,
|
||||
fetchedAt: time.Now(),
|
||||
}, nil
|
||||
|
||||
// Primary path is the goja signer (statsig_js.go); set it up / refresh it for
|
||||
// this build. Static (header, salt) below is only a last-resort fallback.
|
||||
ensureEngine(ctx, client, html)
|
||||
|
||||
ch := statsigChallenge{
|
||||
header: statsigHeader,
|
||||
suffix: statsigSuffix,
|
||||
trailer: statsigTrailer,
|
||||
fetchedAt: time.Now(),
|
||||
seedB64: mm[1],
|
||||
curvesJSON: string(curvesJSON),
|
||||
}
|
||||
// Best-effort static derivation (the old hand-ported algorithm) as fallback.
|
||||
if seed, err := decodeStatsigSeed(mm[1]); err == nil {
|
||||
if tail, err := computeStatsigTail(seed, curves); err == nil {
|
||||
ch.header = append([]byte{0x00}, seed...)
|
||||
ch.suffix = statsigSaltPrefix + tail
|
||||
ch.trailer = defaultStatsigTrailer
|
||||
}
|
||||
}
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func decodeStatsigSeed(s string) ([]byte, error) {
|
||||
@@ -635,10 +658,20 @@ func cubicBezierEase(x1, y1, x2, y2, p float64) float64 {
|
||||
func statsigID(path, method, token string) string {
|
||||
header, suffix, trailer := statsigHeader, statsigSuffix, statsigTrailer
|
||||
statsigMu.Lock()
|
||||
if ch, ok := statsigCache[token]; ok {
|
||||
header, suffix, trailer = ch.header, ch.suffix, ch.trailer
|
||||
}
|
||||
ch, ok := statsigCache[token]
|
||||
statsigMu.Unlock()
|
||||
if ok {
|
||||
header, suffix, trailer = ch.header, ch.suffix, ch.trailer
|
||||
// Primary: run grok's own signer in goja (durable across reships). Falls
|
||||
// through to the static computation below on any failure.
|
||||
if ch.seedB64 != "" && ch.curvesJSON != "" {
|
||||
if id, err := signWithEngine(ch.seedB64, ch.curvesJSON, path, method); err == nil {
|
||||
return id
|
||||
} else if !errors.Is(err, errEngineNotReady) {
|
||||
log.Printf("grok statsig: js signer failed, using static fallback: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
counter := uint32(time.Now().Unix() - statsigEpoch)
|
||||
sig := fmt.Sprintf("%s!%s!%d%s", method, path, counter, suffix)
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
package grok
|
||||
|
||||
// This file makes x-statsig-id durable across grok web reships by executing grok's
|
||||
// OWN obfuscated signer (a Turbopack chunk) inside an embedded JS engine (goja),
|
||||
// under a synthesized DOM + Web-Animations getComputedStyle shim (statsig_shim.js).
|
||||
// grok's code does all the per-build byte-indexing / curve-selection; we only supply
|
||||
// the stable browser primitives. This replaces the brittle hand-ported byte-offset
|
||||
// algorithm in computeStatsigTail (kept as a last-resort fallback). See the package
|
||||
// doc and the grok-statsig-signer memory for the reverse-engineering details.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
http "github.com/bogdanfinn/fhttp"
|
||||
tlsclient "github.com/bogdanfinn/tls-client"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
//go:embed statsig_shim.js
|
||||
var statsigShimJS string
|
||||
|
||||
const sigPoolSize = 4
|
||||
|
||||
// errEngineNotReady means the durable signer engine has not been built yet (no
|
||||
// homepage fetched, or chunk location failed). Callers fall back to the static path.
|
||||
var errEngineNotReady = errors.New("statsig engine not ready")
|
||||
|
||||
var (
|
||||
// signer-chunk location patterns (Turbopack). The caller chunk contains the
|
||||
// literal "x-statsig-id" and a lazy import `.A(<moduleId>).then(e=>t(e.default()))`.
|
||||
statsigCallerRe = regexp.MustCompile(`\.A\((\d+)\)\.then\(`)
|
||||
chunkPathRe = regexp.MustCompile(`/_next/static/chunks/[a-zA-Z0-9_.\-/]+\.js`)
|
||||
// goja's parser tries to fetch //# sourceMappingURL=... from disk and errors.
|
||||
sourceMapRe = regexp.MustCompile(`(?m)//[#@]\s*sourceMappingURL=\S*`)
|
||||
|
||||
sigMgrMu sync.Mutex
|
||||
sigBuildKey string // hash of the homepage chunk list; changes on reship
|
||||
sigChunkSrc string // current signer chunk source
|
||||
sigPool chan *sigEngine // pool of ready engines for sigChunkSrc
|
||||
)
|
||||
|
||||
// sigEngine wraps one goja runtime with grok's signer chunk loaded. A goja runtime
|
||||
// is not safe for concurrent use; the pool hands each engine to one goroutine at a
|
||||
// time so no per-engine locking is needed.
|
||||
type sigEngine struct {
|
||||
rt *goja.Runtime
|
||||
fire goja.Callable // __grokSignInto
|
||||
}
|
||||
|
||||
func newSigEngine(chunkSrc string) (*sigEngine, error) {
|
||||
rt := goja.New()
|
||||
// SHA-256 bridge for crypto.subtle.digest.
|
||||
if err := rt.Set("__goSha256", func(call goja.FunctionCall) goja.Value {
|
||||
data := jsBytes(rt, call.Argument(0))
|
||||
sum := sha256.Sum256(data)
|
||||
return rt.ToValue(rt.NewArrayBuffer(sum[:]))
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := rt.RunString(statsigShimJS); err != nil {
|
||||
return nil, fmt.Errorf("shim: %w", err)
|
||||
}
|
||||
if _, err := rt.RunString(sourceMapRe.ReplaceAllString(chunkSrc, "")); err != nil {
|
||||
return nil, fmt.Errorf("chunk eval: %w", err)
|
||||
}
|
||||
if _, err := rt.RunString("__grokBootstrap()"); err != nil {
|
||||
return nil, fmt.Errorf("bootstrap: %w", err)
|
||||
}
|
||||
fire, ok := goja.AssertFunction(rt.Get("__grokSignInto"))
|
||||
if !ok {
|
||||
return nil, errors.New("statsig js: __grokSignInto missing")
|
||||
}
|
||||
return &sigEngine{rt: rt, fire: fire}, nil
|
||||
}
|
||||
|
||||
// statsigID runs grok's signer for one request. seedB64 is the raw <meta> content;
|
||||
// curvesJSON is [[{color,deg,bezier}...]...].
|
||||
func (e *sigEngine) statsigID(seedB64, curvesJSON, path, method string) (string, error) {
|
||||
_ = e.rt.Set("__SEED", seedB64)
|
||||
_ = e.rt.Set("__CURVES", curvesJSON)
|
||||
_ = e.rt.Set("__PATH", path)
|
||||
_ = e.rt.Set("__METHOD", method)
|
||||
// RunString drains goja's microtask queue, settling the async signer's promise.
|
||||
if _, err := e.fire(goja.Undefined()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if errv := e.rt.Get("__grokErr"); errv != nil && !goja.IsNull(errv) && !goja.IsUndefined(errv) {
|
||||
return "", fmt.Errorf("statsig js: %s", errv.String())
|
||||
}
|
||||
res := e.rt.Get("__grokResult")
|
||||
if res == nil || goja.IsNull(res) || goja.IsUndefined(res) {
|
||||
return "", errors.New("statsig js: promise did not settle")
|
||||
}
|
||||
id := res.String()
|
||||
if id == "" {
|
||||
return "", errors.New("statsig js: empty id")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// jsBytes extracts the byte contents of a JS Uint8Array / ArrayBuffer value.
|
||||
func jsBytes(rt *goja.Runtime, v goja.Value) []byte {
|
||||
if ab, ok := v.Export().(goja.ArrayBuffer); ok {
|
||||
return ab.Bytes()
|
||||
}
|
||||
obj := v.ToObject(rt)
|
||||
if buf := obj.Get("buffer"); buf != nil {
|
||||
if ab, ok := buf.Export().(goja.ArrayBuffer); ok {
|
||||
return ab.Bytes()
|
||||
}
|
||||
}
|
||||
n := int(obj.Get("length").ToInteger())
|
||||
out := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
out[i] = byte(obj.Get(strconv.Itoa(i)).ToInteger())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// signWithEngine borrows an engine from the pool (building one on demand), signs,
|
||||
// and returns it. Returns an error if the engine subsystem is not ready.
|
||||
func signWithEngine(seedB64, curvesJSON, path, method string) (string, error) {
|
||||
sigMgrMu.Lock()
|
||||
src, pool := sigChunkSrc, sigPool
|
||||
sigMgrMu.Unlock()
|
||||
if src == "" || pool == nil {
|
||||
return "", errEngineNotReady
|
||||
}
|
||||
var eng *sigEngine
|
||||
select {
|
||||
case eng = <-pool:
|
||||
default:
|
||||
var err error
|
||||
if eng, err = newSigEngine(src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
id, err := eng.statsigID(seedB64, curvesJSON, path, method)
|
||||
select {
|
||||
case pool <- eng:
|
||||
default:
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
|
||||
// ensureEngine refreshes the global engine pool when the homepage's chunk set
|
||||
// changes (i.e. grok reshipped). It locates the signer chunk build-agnostically and
|
||||
// rebuilds the pool. Cheap no-op when the build is unchanged.
|
||||
func ensureEngine(ctx context.Context, client tlsclient.HttpClient, homeHTML string) {
|
||||
paths := chunkPathRe.FindAllString(homeHTML, -1)
|
||||
if len(paths) == 0 {
|
||||
return
|
||||
}
|
||||
key := hashStrings(paths)
|
||||
|
||||
sigMgrMu.Lock()
|
||||
unchanged := key == sigBuildKey && sigPool != nil
|
||||
sigMgrMu.Unlock()
|
||||
if unchanged {
|
||||
return
|
||||
}
|
||||
|
||||
src, err := locateSignerChunk(ctx, client, dedupe(paths))
|
||||
if err != nil {
|
||||
log.Printf("grok statsig: locate signer chunk failed (will use static fallback): %v", err)
|
||||
return
|
||||
}
|
||||
// smoke-test: a build must produce a loadable engine before we commit to it.
|
||||
eng, err := newSigEngine(src)
|
||||
if err != nil {
|
||||
log.Printf("grok statsig: signer chunk did not load in goja (static fallback): %v", err)
|
||||
return
|
||||
}
|
||||
pool := make(chan *sigEngine, sigPoolSize)
|
||||
pool <- eng // reuse the smoke-test engine instead of discarding it
|
||||
sigMgrMu.Lock()
|
||||
sigBuildKey = key
|
||||
sigChunkSrc = src
|
||||
sigPool = pool
|
||||
sigMgrMu.Unlock()
|
||||
log.Printf("grok statsig: self-heal engine ready (build %s..)", key[:8])
|
||||
}
|
||||
|
||||
// locateSignerChunk finds grok's signer chunk from the homepage chunk list:
|
||||
// the caller chunk holds "x-statsig-id" + `.A(<id>)`; a loader chunk registers that
|
||||
// <id> with `Promise.all(["static/chunks/XXX.js"]...)` — XXX is the signer.
|
||||
func locateSignerChunk(ctx context.Context, client tlsclient.HttpClient, paths []string) (string, error) {
|
||||
var callerID string
|
||||
loaderRe := (*regexp.Regexp)(nil)
|
||||
var signerPath string
|
||||
|
||||
// pass 1: find the caller chunk + its lazy module id.
|
||||
for _, p := range paths {
|
||||
body, err := fetchChunk(ctx, client, p)
|
||||
if err != nil || !strings.Contains(body, "x-statsig-id") {
|
||||
continue
|
||||
}
|
||||
if m := statsigCallerRe.FindStringSubmatch(body); m != nil {
|
||||
callerID = m[1]
|
||||
}
|
||||
break
|
||||
}
|
||||
if callerID == "" {
|
||||
return "", errors.New("statsig caller module id not found")
|
||||
}
|
||||
// loader registers: ,<callerID>,<param>=>{ ... Promise.all(["static/chunks/XXX.js"] ...
|
||||
loaderRe = regexp.MustCompile(`,` + callerID + `,\w+=>\{[^}]*?Promise\.all\(\["(static/chunks/[^"]+\.js)"`)
|
||||
|
||||
// pass 2: find the loader chunk that maps callerID -> signer chunk path.
|
||||
for _, p := range paths {
|
||||
body, err := fetchChunk(ctx, client, p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if m := loaderRe.FindStringSubmatch(body); m != nil {
|
||||
signerPath = m[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if signerPath == "" {
|
||||
return "", fmt.Errorf("signer chunk path for module %s not found", callerID)
|
||||
}
|
||||
src, err := fetchChunk(ctx, client, "/_next/"+signerPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch signer chunk: %w", err)
|
||||
}
|
||||
return src, nil
|
||||
}
|
||||
|
||||
func fetchChunk(ctx context.Context, client tlsclient.HttpClient, path string) (string, error) {
|
||||
if !strings.HasPrefix(path, "http") {
|
||||
path = apiBase + path
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header = http.Header{
|
||||
"accept": {"*/*"},
|
||||
"user-agent": {userAgent},
|
||||
http.HeaderOrderKey: {"accept", "user-agent"},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("chunk http %d", resp.StatusCode)
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
func hashStrings(ss []string) string {
|
||||
uniq := dedupe(ss)
|
||||
h := sha512.New()
|
||||
for _, s := range uniq {
|
||||
_, _ = io.WriteString(h, s)
|
||||
_, _ = io.WriteString(h, "\n")
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
func dedupe(ss []string) []string {
|
||||
seen := map[string]bool{}
|
||||
out := ss[:0:0]
|
||||
for _, s := range ss {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -34,8 +36,15 @@ var statsigTriples = []struct {
|
||||
|
||||
const statsigTestCurves = `[[{"color":[48,44,6,37,198,15],"deg":192,"bezier":[118,76,158,16]},{"color":[224,216,196,111,43,97],"deg":119,"bezier":[67,167,95,219]},{"color":[90,235,250,5,223,64],"deg":104,"bezier":[100,57,106,204]},{"color":[6,109,253,44,29,224],"deg":151,"bezier":[204,60,142,122]},{"color":[81,0,121,208,228,133],"deg":181,"bezier":[182,89,105,123]},{"color":[31,3,160,181,226,184],"deg":98,"bezier":[207,150,215,136]},{"color":[231,243,81,28,109,131],"deg":175,"bezier":[17,103,7,81]},{"color":[222,250,130,169,55,247],"deg":141,"bezier":[21,238,12,84]},{"color":[74,62,116,145,209,185],"deg":109,"bezier":[0,74,58,233]},{"color":[204,168,203,138,107,125],"deg":214,"bezier":[41,13,239,45]},{"color":[246,116,162,162,84,234],"deg":127,"bezier":[160,202,52,76]},{"color":[138,202,210,36,61,195],"deg":234,"bezier":[31,50,177,229]},{"color":[47,46,196,176,79,255],"deg":221,"bezier":[39,14,9,76]},{"color":[245,164,227,71,201,110],"deg":190,"bezier":[193,208,125,9]},{"color":[199,253,44,33,130,240],"deg":191,"bezier":[168,247,61,37]},{"color":[19,91,5,180,202,243],"deg":216,"bezier":[96,152,42,123]}],[{"color":[42,154,230,170,197,128],"deg":108,"bezier":[160,52,34,184]},{"color":[224,132,83,0,231,162],"deg":238,"bezier":[227,37,122,102]},{"color":[24,82,237,199,100,74],"deg":130,"bezier":[186,106,188,209]},{"color":[47,50,169,65,124,44],"deg":228,"bezier":[198,22,146,225]},{"color":[111,131,95,195,131,73],"deg":207,"bezier":[134,146,202,172]},{"color":[192,10,166,28,236,3],"deg":219,"bezier":[85,77,157,235]},{"color":[236,139,199,72,233,250],"deg":197,"bezier":[180,9,79,147]},{"color":[146,195,78,16,231,211],"deg":93,"bezier":[123,18,87,27]},{"color":[168,211,49,42,124,18],"deg":172,"bezier":[232,171,120,118]},{"color":[43,146,96,11,203,53],"deg":146,"bezier":[11,4,83,198]},{"color":[183,97,101,38,115,41],"deg":159,"bezier":[69,223,197,236]},{"color":[78,73,207,132,109,134],"deg":49,"bezier":[59,18,133,168]},{"color":[239,153,225,207,127,157],"deg":194,"bezier":[13,206,154,182]},{"color":[19,146,127,191,68,199],"deg":165,"bezier":[148,212,7,16]},{"color":[93,225,249,144,88,255],"deg":215,"bezier":[62,30,77,69]},{"color":[203,79,164,145,3,20],"deg":81,"bezier":[228,28,93,130]}],[{"color":[171,53,4,125,232,43],"deg":122,"bezier":[136,9,128,97]},{"color":[143,97,205,57,63,69],"deg":192,"bezier":[232,20,219,189]},{"color":[28,173,18,107,158,44],"deg":255,"bezier":[93,187,184,198]},{"color":[170,17,62,142,200,32],"deg":29,"bezier":[14,31,12,97]},{"color":[19,117,122,173,239,66],"deg":74,"bezier":[117,176,139,212]},{"color":[213,151,230,112,224,255],"deg":25,"bezier":[1,223,72,233]},{"color":[153,131,51,105,69,47],"deg":108,"bezier":[123,177,126,140]},{"color":[120,114,44,151,88,83],"deg":165,"bezier":[16,104,134,75]},{"color":[245,145,194,75,120,26],"deg":142,"bezier":[79,235,38,43]},{"color":[147,63,50,255,239,106],"deg":190,"bezier":[122,143,160,150]},{"color":[233,78,184,130,25,123],"deg":54,"bezier":[87,18,184,226]},{"color":[162,180,233,70,57,249],"deg":87,"bezier":[123,238,61,124]},{"color":[146,95,56,171,38,240],"deg":239,"bezier":[241,134,228,44]},{"color":[111,11,149,62,208,177],"deg":70,"bezier":[103,149,4,37]},{"color":[159,128,118,21,197,153],"deg":175,"bezier":[246,215,172,236]},{"color":[194,131,68,247,215,108],"deg":30,"bezier":[23,91,151,231]}],[{"color":[239,129,141,243,85,208],"deg":38,"bezier":[252,248,245,195]},{"color":[205,56,138,49,126,99],"deg":107,"bezier":[72,85,228,91]},{"color":[185,54,148,122,170,158],"deg":192,"bezier":[33,88,51,136]},{"color":[7,14,75,26,23,41],"deg":93,"bezier":[153,21,55,147]},{"color":[245,211,213,64,5,253],"deg":49,"bezier":[253,19,106,155]},{"color":[144,232,165,21,114,130],"deg":200,"bezier":[193,179,133,226]},{"color":[34,196,100,42,114,0],"deg":52,"bezier":[4,4,71,65]},{"color":[195,8,130,102,201,141],"deg":210,"bezier":[8,173,23,33]},{"color":[86,136,44,95,223,62],"deg":249,"bezier":[220,98,68,113]},{"color":[205,48,9,247,236,71],"deg":75,"bezier":[163,240,28,25]},{"color":[43,190,29,239,55,135],"deg":146,"bezier":[109,245,34,188]},{"color":[146,91,92,2,3,251],"deg":97,"bezier":[183,188,95,157]},{"color":[40,213,196,70,81,174],"deg":120,"bezier":[153,197,61,201]},{"color":[131,92,180,68,131,214],"deg":251,"bezier":[94,191,198,89]},{"color":[47,113,219,96,115,228],"deg":238,"bezier":[22,35,60,63]},{"color":[246,244,203,196,78,136],"deg":44,"bezier":[88,23,205,184]}]]`
|
||||
|
||||
// TestComputeStatsigTail is the offline regression test for the F derivation.
|
||||
// TestComputeStatsigTail was the offline regression for the hand-ported F
|
||||
// derivation (computeStatsigTail). That algorithm is now only a last-resort
|
||||
// fallback: the durable path runs grok's own signer in goja (statsig_engine.go),
|
||||
// which self-heals across web reships. grok rotates the F algorithm on every
|
||||
// reship, so these browser-captured (seed,F) triples go stale and CANNOT be
|
||||
// regenerated offline. TestSelfHealStatsigE2E (live, via goja) is the real
|
||||
// regression now. Kept skipped so the reference vectors aren't lost.
|
||||
func TestComputeStatsigTail(t *testing.T) {
|
||||
t.Skip("static F derivation is a deprecated fallback; see TestSelfHealStatsigE2E (goja)")
|
||||
var curves [][]statsigCurve
|
||||
if err := json.Unmarshal([]byte(statsigTestCurves), &curves); err != nil {
|
||||
t.Fatalf("curves: %v", err)
|
||||
@@ -92,6 +101,55 @@ func TestSelfHealStatsigE2E(t *testing.T) {
|
||||
t.Logf("OK bytes=%d head=%.80s", len(body), strings.ReplaceAll(body, "\n", " "))
|
||||
}
|
||||
|
||||
// TestStatsigEngineConcurrent fires many concurrent goja signs through the pool to
|
||||
// catch data races / engine cross-talk (run with -race). Requires a live GROK_TOK
|
||||
// so the engine is built and the challenge cached.
|
||||
func TestStatsigEngineConcurrent(t *testing.T) {
|
||||
token := strings.TrimSpace(os.Getenv("GROK_TOK"))
|
||||
if token == "" {
|
||||
t.Skip("no GROK_TOK")
|
||||
}
|
||||
c := NewClient("")
|
||||
client, err := c.newTLSClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
c.ensureChallenge(ctx, client, token)
|
||||
statsigMu.Lock()
|
||||
ch, ok := statsigCache[token]
|
||||
statsigMu.Unlock()
|
||||
if !ok || ch.seedB64 == "" || ch.curvesJSON == "" {
|
||||
t.Fatal("engine inputs not cached")
|
||||
}
|
||||
|
||||
const n = 40
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, n)
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
id, err := signWithEngine(ch.seedB64, ch.curvesJSON, "/rest/app-chat/conversations/new", "POST")
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if len(id) < 40 {
|
||||
errs <- errTooShort
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for e := range errs {
|
||||
t.Fatalf("concurrent sign: %v", e)
|
||||
}
|
||||
}
|
||||
|
||||
var errTooShort = errors.New("statsig id too short")
|
||||
|
||||
// TestGenerateVideoE2E generates a real grok video using only the dynamic
|
||||
// self-healed statsig (no env overrides). Requires a live GROK_TOK.
|
||||
func TestGenerateVideoE2E(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// statsig_shim.js — browser-environment shim that lets grok.com's own obfuscated
|
||||
// x-statsig-id signer run inside goja. grok's code does all the (per-build,
|
||||
// rotating) byte-indexing/curve-selection; we only provide the STABLE browser
|
||||
// primitives it reads from: the seed <meta>, the .r-aufz1o SVG curve DOM, and a
|
||||
// standard Web-Animations getComputedStyle sampler. Inputs arrive via globals set
|
||||
// by Go before each sign: __SEED (base64 str), __CURVES (JSON [[{color,deg,bezier}]]),
|
||||
// __PATH, __METHOD. Go also injects __goSha256(Uint8Array)->ArrayBuffer.
|
||||
(function () {
|
||||
'use strict';
|
||||
var g = globalThis;
|
||||
|
||||
// ---- base64 (goja has no atob/btoa) ----
|
||||
var B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
g.atob = function (s) {
|
||||
s = String(s).replace(/=+$/, '');
|
||||
var out = '', bits = 0, val = 0;
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
var c = B64.indexOf(s.charAt(i));
|
||||
if (c < 0) continue;
|
||||
val = (val << 6) | c; bits += 6;
|
||||
if (bits >= 8) { bits -= 8; out += String.fromCharCode((val >> bits) & 0xff); }
|
||||
}
|
||||
return out;
|
||||
};
|
||||
g.btoa = function (s) {
|
||||
s = String(s); var out = '';
|
||||
for (var i = 0; i < s.length; i += 3) {
|
||||
var b0 = s.charCodeAt(i), b1 = s.charCodeAt(i + 1), b2 = s.charCodeAt(i + 2);
|
||||
var h0 = b0 >> 2, h1 = ((b0 & 3) << 4) | (b1 >> 4);
|
||||
var h2 = ((b1 & 15) << 2) | (b2 >> 6), h3 = b2 & 63;
|
||||
out += B64[h0] + B64[h1];
|
||||
out += isNaN(b1) ? '=' : B64[h2];
|
||||
out += isNaN(b2) ? '=' : B64[h3];
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// ---- TextEncoder (goja has no TextEncoder) ----
|
||||
if (typeof g.TextEncoder === 'undefined') {
|
||||
g.TextEncoder = function () {};
|
||||
g.TextEncoder.prototype.encode = function (str) {
|
||||
str = String(str);
|
||||
var bytes = [];
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
var c = str.charCodeAt(i);
|
||||
if (c < 0x80) bytes.push(c);
|
||||
else if (c < 0x800) { bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); }
|
||||
else if (c >= 0xd800 && c <= 0xdbff) { // surrogate pair
|
||||
var c2 = str.charCodeAt(++i);
|
||||
var cp = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff);
|
||||
bytes.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
|
||||
} else { bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); }
|
||||
}
|
||||
return Uint8Array.from(bytes);
|
||||
};
|
||||
}
|
||||
|
||||
// ---- crypto.subtle.digest, backed by Go SHA-256 ----
|
||||
g.crypto = g.crypto || {};
|
||||
g.crypto.subtle = g.crypto.subtle || {};
|
||||
g.crypto.subtle.digest = function (algo, data) {
|
||||
// grok only ever asks for sha-256; g.__goSha256 returns an ArrayBuffer
|
||||
var bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||
return Promise.resolve(g.__goSha256(bytes));
|
||||
};
|
||||
|
||||
// ---- Web Animations getComputedStyle sampler (the only real math we own) ----
|
||||
var K = 4096;
|
||||
function cubicBezier(x1, y1, x2, y2, p) {
|
||||
if (p <= 0) return 0; if (p >= 1) return 1;
|
||||
function bez(t, a, b) { var mt = 1 - t; return 3 * a * mt * mt * t + 3 * b * mt * t * t + t * t * t; }
|
||||
var lo = 0, hi = 1;
|
||||
for (var i = 0; i < 100; i++) { var m = (lo + hi) / 2; if (bez(m, x1, x2) < p) lo = m; else hi = m; }
|
||||
return bez((lo + hi) / 2, y1, y2);
|
||||
}
|
||||
function hexToRgb(h) { h = h.replace('#', ''); return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; }
|
||||
function sample(anim) {
|
||||
var kf = anim.keyframes, dur = anim.duration || K;
|
||||
var frac = anim.currentTime / dur; if (frac < 0) frac = 0; if (frac > 1) frac = 1;
|
||||
var bm = /cubic-bezier\(([^)]+)\)/.exec(kf.easing || '');
|
||||
var eased = frac;
|
||||
if (bm) { var p = bm[1].split(',').map(Number); eased = cubicBezier(p[0], p[1], p[2], p[3], frac); }
|
||||
var c0 = hexToRgb(kf.color[0]), c1 = hexToRgb(kf.color[1]);
|
||||
var col = [0, 1, 2].map(function (i) { return Math.round(c0[i] + (c1[i] - c0[i]) * eased); });
|
||||
var d0 = parseFloat(/rotate\(([-\d.]+)deg\)/.exec(kf.transform[0])[1]);
|
||||
var d1 = parseFloat(/rotate\(([-\d.]+)deg\)/.exec(kf.transform[1])[1]);
|
||||
var ang = (d0 + (d1 - d0) * eased) * Math.PI / 180;
|
||||
var cos = Math.cos(ang), sin = Math.sin(ang);
|
||||
return { color: 'rgb(' + col[0] + ', ' + col[1] + ', ' + col[2] + ')',
|
||||
transform: 'matrix(' + cos + ', ' + sin + ', ' + (-sin) + ', ' + cos + ', 0, 0)' };
|
||||
}
|
||||
|
||||
// ---- minimal DOM ----
|
||||
function makeEl(props) {
|
||||
var e = {
|
||||
nodeName: 'DIV', style: {}, childNodes: [], attrs: {}, _anim: null, _parent: null,
|
||||
setAttribute: function (k, v) { e.attrs[k] = v; },
|
||||
getAttribute: function (k) { return (k in e.attrs) ? e.attrs[k] : (props && props.attrs && k in props.attrs ? props.attrs[k] : null); },
|
||||
appendChild: function (c) { e.childNodes.push(c); return c; },
|
||||
append: function (c) { e.childNodes.push(c); return c; },
|
||||
removeChild: function (c) { return c; },
|
||||
remove: function () {},
|
||||
animate: function (keyframes, opts) {
|
||||
var anim = {
|
||||
keyframes: keyframes,
|
||||
duration: (opts && opts.duration) || (typeof opts === 'number' ? opts : K),
|
||||
currentTime: 0, pause: function () {}, play: function () {}, cancel: function () {},
|
||||
effect: { getKeyframes: function () { return Array.isArray(keyframes) ? keyframes : [keyframes]; } },
|
||||
};
|
||||
e._anim = anim; return anim;
|
||||
},
|
||||
getAnimations: function () { return e._anim ? [e._anim] : []; },
|
||||
};
|
||||
if (props) for (var k in props) if (k !== 'attrs') e[k] = props[k];
|
||||
Object.defineProperty(e, 'parentElement', { get: function () { return e._parent; } });
|
||||
Object.defineProperty(e, 'innerHTML', { set: function (v) { e._html = v; }, get: function () { return e._html; } });
|
||||
Object.defineProperty(e, 'textContent', { set: function (v) { e._text = v; }, get: function () { return e._text; } });
|
||||
return e;
|
||||
}
|
||||
|
||||
// .r-aufz1o group: g.childNodes[0].childNodes[1].getAttribute('d') = svg path whose
|
||||
// numbers (after substring(9), split('C')) decode back to each curve [c0..c5,deg,b0..b3].
|
||||
function groupEl(flatCurves) {
|
||||
var d = '_________' + flatCurves.map(function (c) { return c.join(' '); }).join('C');
|
||||
var path = makeEl({ attrs: { d: d } });
|
||||
var inner = makeEl(); inner.childNodes = [makeEl(), path];
|
||||
var outer = makeEl(); outer.childNodes = [inner]; outer._parent = makeEl();
|
||||
return outer;
|
||||
}
|
||||
|
||||
var docBody = makeEl();
|
||||
g.document = {
|
||||
currentScript: null, body: docBody, head: makeEl(),
|
||||
createElement: function (tag) { return makeEl({ nodeName: String(tag || 'div').toUpperCase() }); },
|
||||
querySelectorAll: function (sel) {
|
||||
sel = String(sel);
|
||||
if (/aufz1o/.test(sel)) {
|
||||
var curves = JSON.parse(g.__CURVES);
|
||||
return curves.map(function (grp) {
|
||||
return groupEl(grp.map(function (cv) { return cv.color.concat([cv.deg], cv.bezier); }));
|
||||
});
|
||||
}
|
||||
if (/verification|name/i.test(sel)) {
|
||||
var seed = g.__SEED;
|
||||
return [{ nodeName: 'META', getAttribute: function (a) { return a === 'content' ? seed : null; },
|
||||
get content() { return seed; } }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
querySelector: function (sel) { var r = this.querySelectorAll(sel); return r[0] || null; },
|
||||
};
|
||||
g.window = g;
|
||||
g.self = g;
|
||||
g.getComputedStyle = function (el) { return el && el._anim ? sample(el._anim) : { color: 'rgb(0, 0, 0)', transform: 'none' }; };
|
||||
g.navigator = g.navigator || { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' };
|
||||
g.location = g.location || { href: 'https://grok.com/', origin: 'https://grok.com', pathname: '/' };
|
||||
|
||||
// ---- Turbopack capture + bootstrap ----
|
||||
var TP = []; TP.push = function (entry) { TP._entry = entry; return 0; };
|
||||
g.TURBOPACK = TP;
|
||||
|
||||
// Called by Go AFTER the signer chunk is eval'd: run the module factory, grab default.
|
||||
g.__grokBootstrap = function () {
|
||||
var entry = TP._entry;
|
||||
if (!entry) throw new Error('turbopack entry not registered');
|
||||
var factory = entry[2];
|
||||
var exports = {};
|
||||
var ctx = {
|
||||
s: function () {
|
||||
var flat = Array.prototype.slice.call(arguments).flat(Infinity);
|
||||
var name = null;
|
||||
for (var i = 0; i < flat.length; i++) {
|
||||
var x = flat[i];
|
||||
if (typeof x === 'string') name = x;
|
||||
else if (typeof x === 'function' && name != null) {
|
||||
(function (nm, getter) { Object.defineProperty(exports, nm, { get: getter, configurable: true, enumerable: true }); })(name, x);
|
||||
name = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
factory(ctx);
|
||||
// exports.default is a GETTER that invokes the module factory on every read,
|
||||
// returning a FRESH async signer (path,method)=>Promise<id> with a fresh internal
|
||||
// curve cache. Keep the exports object; read .default fresh per sign so different
|
||||
// sessions' curves never leak through the closure cache.
|
||||
g.__grokExports = exports;
|
||||
if (typeof exports.default !== 'function') throw new Error('no default export');
|
||||
};
|
||||
|
||||
// Called by Go per sign. Fresh signer each time via the re-invoking getter.
|
||||
g.__grokSign = function () {
|
||||
var signer = g.__grokExports.default; // fresh async signer
|
||||
return signer(g.__PATH, g.__METHOD); // returns Promise<string>
|
||||
};
|
||||
|
||||
// Synchronous bridge: Go calls this via RunString (which drains goja's job queue),
|
||||
// then reads __grokResult / __grokErr. Works because crypto.subtle.digest resolves
|
||||
// synchronously (Promise.resolve over a Go SHA-256), so the whole await chain settles
|
||||
// within the microtask drain.
|
||||
g.__grokResult = null;
|
||||
g.__grokErr = null;
|
||||
g.__grokSignInto = function () {
|
||||
g.__grokResult = null; g.__grokErr = null;
|
||||
try {
|
||||
g.__grokSign().then(
|
||||
function (r) { g.__grokResult = r; },
|
||||
function (e) { g.__grokErr = (e && e.stack) ? String(e.stack) : String(e); }
|
||||
);
|
||||
} catch (e) { g.__grokErr = (e && e.stack) ? String(e.stack) : String(e); }
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user