From e147fab647168561af09cf07f0f950a55b6b67d1 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Thu, 25 Jun 2026 18:14:31 -0400 Subject: [PATCH] fix(bskyweb): jpeg avatars, base64 wallet creds, require token secret --- bskyweb/cmd/bskyweb/invitepass.go | 25 ++++++++++++---- bskyweb/cmd/bskyweb/invitepass_test.go | 41 ++++++++++++++++++++++++++ bskyweb/cmd/bskyweb/server.go | 9 +++++- bskyweb/example.env | 2 +- 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/bskyweb/cmd/bskyweb/invitepass.go b/bskyweb/cmd/bskyweb/invitepass.go index 2c113143ff..7d146fb558 100644 --- a/bskyweb/cmd/bskyweb/invitepass.go +++ b/bskyweb/cmd/bskyweb/invitepass.go @@ -7,9 +7,12 @@ import ( "errors" "fmt" "image" + _ "image/gif" + _ "image/jpeg" _ "image/png" "io" "io/fs" + "log/slog" "net/http" "strings" "time" @@ -73,6 +76,9 @@ func (srv *Server) RegisterInvitePassRoutes() { } func (srv *Server) WebInvitePassURL(c echo.Context) error { + if len(srv.cfg.InvitePass.TokenSecret) == 0 { + return c.JSON(http.StatusServiceUnavailable, echo.Map{"error": "InvitePassDisabled"}) + } did, _, err := srv.authenticator.Authenticate(c) if err != nil { return c.JSON(http.StatusUnauthorized, echo.Map{"error": "AuthMissing"}) @@ -91,6 +97,9 @@ func (srv *Server) WebInvitePassURL(c echo.Context) error { } func (srv *Server) WebInvitePassPkpass(c echo.Context) error { + if len(srv.cfg.InvitePass.TokenSecret) == 0 { + return c.JSON(http.StatusServiceUnavailable, echo.Map{"error": "InvitePassDisabled"}) + } theme := CoerceTheme(c.QueryParam("theme")) tok := c.QueryParam("t") did, tokTheme, err := VerifyPassToken(srv.cfg.InvitePass.TokenSecret, tok, time.Now()) @@ -165,7 +174,7 @@ func (srv *Server) WebInviteWalletHero(c echo.Context) error { if err != nil { return c.JSON(http.StatusInternalServerError, echo.Map{"error": "StripLoadFailed"}) } - avatarImg, _ := decodePNG(avatarBytes) + avatarImg, _ := decodeImage(avatarBytes) out, err := CompositeStrip(base, avatarImg, handle, srv.cfg.InvitePass.FontFace) if err != nil { return c.JSON(http.StatusInternalServerError, echo.Map{"error": "StripBuildFailed"}) @@ -190,13 +199,19 @@ func (srv *Server) buildPassAssets(theme, handle string, avatarBytes []byte) ([] logo2, _ := readFS(srv.cfg.InvitePass.StripFS, "passes/logo@2x.png") logo3, _ := readFS(srv.cfg.InvitePass.StripFS, "passes/logo@3x.png") - avatarImg, _ := decodePNG(avatarBytes) + avatarImg, _ := decodeImage(avatarBytes) strip1Bytes, err := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 1, avatarImg, handle, srv.cfg.InvitePass.FontFace) if err != nil { return nil, fmt.Errorf("strip@1x: %w", err) } - strip2Bytes, _ := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 2, avatarImg, handle, srv.cfg.InvitePass.FontFace) - strip3Bytes, _ := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 3, avatarImg, handle, srv.cfg.InvitePass.FontFace) + strip2Bytes, err := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 2, avatarImg, handle, srv.cfg.InvitePass.FontFace) + if err != nil { + slog.Debug("invite pass: strip composite failed", "scale", 2, "err", err) + } + strip3Bytes, err := buildStripAtDensity(srv.cfg.InvitePass.StripFS, theme, 3, avatarImg, handle, srv.cfg.InvitePass.FontFace) + if err != nil { + slog.Debug("invite pass: strip composite failed", "scale", 3, "err", err) + } assets := []PassAsset{ {Name: "icon.png", Data: icon1}, @@ -241,7 +256,7 @@ func readFS(staticFS fs.FS, name string) ([]byte, error) { return io.ReadAll(f) } -func decodePNG(data []byte) (image.Image, error) { +func decodeImage(data []byte) (image.Image, error) { if len(data) == 0 { return nil, errors.New("empty") } diff --git a/bskyweb/cmd/bskyweb/invitepass_test.go b/bskyweb/cmd/bskyweb/invitepass_test.go index 60f7b61d22..a3f95c1809 100644 --- a/bskyweb/cmd/bskyweb/invitepass_test.go +++ b/bskyweb/cmd/bskyweb/invitepass_test.go @@ -91,3 +91,44 @@ func TestWebInvitePassPkpass_ThemeMismatchInToken(t *testing.T) { t.Fatalf("status = %d", rec.Code) } } + +func TestWebInvitePassURL_NoTokenSecret(t *testing.T) { + authStub := &stubAuth{did: "did:plc:abc", handle: "alice.bsky.social"} + srv := &Server{ + echo: echo.New(), + cfg: &Config{ + InvitePass: InvitePassConfig{ + TokenSecret: nil, + }, + }, + authenticator: authStub, + } + srv.RegisterInvitePassRoutes() + req := httptest.NewRequest(http.MethodPost, "/invite/pass.url", bytes.NewReader([]byte(`{"theme":"dusk"}`))) + req.Header.Set("Authorization", "Bearer fake") + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + srv.echo.ServeHTTP(rec, req) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503", rec.Code) + } +} + +func TestWebInvitePassPkpass_NoTokenSecret(t *testing.T) { + srv := &Server{ + echo: echo.New(), + cfg: &Config{ + InvitePass: InvitePassConfig{ + TokenSecret: nil, + }, + }, + authenticator: &stubAuth{}, + } + srv.RegisterInvitePassRoutes() + req := httptest.NewRequest(http.MethodGet, "/invite/pass.pkpass?theme=dusk&t=anytoken", nil) + rec := httptest.NewRecorder() + srv.echo.ServeHTTP(rec, req) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503", rec.Code) + } +} diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index afd5cd8d86..713f671d3d 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -138,7 +138,11 @@ func serve(cctx *cli.Context) error { } } if saJSON, issuer := os.Getenv("GOOGLE_WALLET_SA_JSON"), os.Getenv("GOOGLE_WALLET_ISSUER_ID"); saJSON != "" && issuer != "" { - wcfg, werr := LoadWalletConfig([]byte(saJSON), issuer, + saBytes := []byte(saJSON) + if decoded, err := base64.StdEncoding.DecodeString(saJSON); err == nil { + saBytes = decoded + } + wcfg, werr := LoadWalletConfig(saBytes, issuer, os.Getenv("GOOGLE_WALLET_HERO_BASE_URL"), os.Getenv("GOOGLE_WALLET_LOGO_URL")) if werr != nil { @@ -154,6 +158,9 @@ func serve(cctx *cli.Context) error { invitePass.StripFS = stripFS invitePass.FontFace = basicfont.Face7x13 invitePass.BaseURL = "https://bsky.app" + if invitePass.Signer != nil && len(invitePass.TokenSecret) == 0 { + slog.Warn("invite pass: Apple cert loaded but INVITE_PASS_TOKEN_SECRET is empty; pass endpoints disabled") + } // // server diff --git a/bskyweb/example.env b/bskyweb/example.env index d0360455d5..0fa8353f59 100644 --- a/bskyweb/example.env +++ b/bskyweb/example.env @@ -9,7 +9,7 @@ APPLE_PASS_KEY_PEM= APPLE_PASS_WWDR_PEM= # Invite passes (Google Wallet) GOOGLE_WALLET_ISSUER_ID= -GOOGLE_WALLET_SA_JSON= +GOOGLE_WALLET_SA_JSON= # raw JSON or base64-encoded JSON; server tries base64 first, falls back to raw GOOGLE_WALLET_HERO_BASE_URL=https://bsky.app/invite/wallet/hero GOOGLE_WALLET_LOGO_URL=https://web-cdn.bsky.app/passes/logo.png # HMAC key for the iOS download token