diff --git a/backend/go.mod b/backend/go.mod index beed649..ab21d0d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -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 diff --git a/backend/go.sum b/backend/go.sum index bb49e05..dbccde0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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= diff --git a/backend/internal/provider/grok/client.go b/backend/internal/provider/grok/client.go index 8fa695a..55f8e4d 100644 --- a/backend/internal/provider/grok/client.go +++ b/backend/internal/provider/grok/client.go @@ -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 , -// 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 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) diff --git a/backend/internal/provider/grok/statsig_engine.go b/backend/internal/provider/grok/statsig_engine.go new file mode 100644 index 0000000..f7af694 --- /dev/null +++ b/backend/internal/provider/grok/statsig_engine.go @@ -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().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 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()`; a loader chunk registers that +// 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: ,,=>{ ... 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 +} diff --git a/backend/internal/provider/grok/statsig_selfheal_test.go b/backend/internal/provider/grok/statsig_selfheal_test.go index fe036ec..be27b28 100644 --- a/backend/internal/provider/grok/statsig_selfheal_test.go +++ b/backend/internal/provider/grok/statsig_selfheal_test.go @@ -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) { diff --git a/backend/internal/provider/grok/statsig_shim.js b/backend/internal/provider/grok/statsig_shim.js new file mode 100644 index 0000000..96cdc49 --- /dev/null +++ b/backend/internal/provider/grok/statsig_shim.js @@ -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 , 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 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 + }; + + // 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); } + }; +})();