diff --git a/bskyweb/cmd/bskyweb/walletjwt.go b/bskyweb/cmd/bskyweb/walletjwt.go index 3f1a157f29..6651605084 100644 --- a/bskyweb/cmd/bskyweb/walletjwt.go +++ b/bskyweb/cmd/bskyweb/walletjwt.go @@ -7,6 +7,7 @@ import ( "encoding/pem" "errors" "net/url" + "strings" "github.com/golang-jwt/jwt/v5" ) @@ -63,7 +64,7 @@ func BuildSaveJWT(cfg *WalletConfig, did, handle, theme string) (string, error) heroURL := cfg.HeroBaseURL + "?" + heroQuery.Encode() obj := map[string]any{ - "id": cfg.IssuerID + ".bsky-" + did + "-" + theme, + "id": cfg.IssuerID + ".bsky-" + strings.ReplaceAll(did, ":", "_") + "-" + theme, "classId": cfg.IssuerID + ".bsky_invite_v1", "logo": map[string]any{ "sourceUri": map[string]any{"uri": cfg.LogoURL}, diff --git a/bskyweb/cmd/bskyweb/walletjwt_test.go b/bskyweb/cmd/bskyweb/walletjwt_test.go index 740b31fad4..d3da65de0d 100644 --- a/bskyweb/cmd/bskyweb/walletjwt_test.go +++ b/bskyweb/cmd/bskyweb/walletjwt_test.go @@ -46,7 +46,7 @@ func TestBuildSaveJWT_RoundTrip(t *testing.T) { t.Fatalf("want 1 object, got %d", len(objs)) } obj := objs[0].(map[string]any) - if !strings.HasSuffix(obj["id"].(string), ".bsky-did:plc:abc-dusk") { + if !strings.HasSuffix(obj["id"].(string), ".bsky-did_plc_abc-dusk") { t.Errorf("unexpected id: %v", obj["id"]) } if obj["hexBackgroundColor"] != "#b15aa2" { @@ -71,3 +71,45 @@ func TestBuildSaveJWT_ThemeCoerced(t *testing.T) { t.Errorf("expected coercion to day, got id %v", obj["id"]) } } + +func TestBuildSaveJWT_IdSanitizesDidColons(t *testing.T) { + key, _ := rsa.GenerateKey(rand.Reader, 2048) + cfg := &WalletConfig{ + IssuerEmail: "issuer@example.com", + IssuerID: "3388000000000000000", + PrivateKey: key, + HeroBaseURL: "https://bsky.app/invite/wallet/hero", + LogoURL: "https://web-cdn.bsky.app/passes/logo.png", + } + + tok, err := BuildSaveJWT(cfg, "did:plc:abc123", "alice.bsky.social", "day") + if err != nil { + t.Fatalf("build: %v", err) + } + + parsed, err := jwt.Parse(tok, func(t *jwt.Token) (any, error) { + return &key.PublicKey, nil + }) + if err != nil { + t.Fatalf("parse: %v", err) + } + + claims := parsed.Claims.(jwt.MapClaims) + payload := claims["payload"].(map[string]any) + objs := payload["genericObjects"].([]any) + if len(objs) != 1 { + t.Fatalf("want 1 object, got %d", len(objs)) + } + obj := objs[0].(map[string]any) + + // Assert that the id ends with the sanitized DID (colons replaced with underscores) + if !strings.HasSuffix(obj["id"].(string), ".bsky-did_plc_abc123-day") { + t.Errorf("expected id to end with .bsky-did_plc_abc123-day, got %v", obj["id"]) + } + + // Assert that the barcode value still contains the original profile URL + barcode := obj["barcode"].(map[string]any) + if barcode["value"] != "https://bsky.app/profile/alice.bsky.social" { + t.Errorf("expected barcode.value to be unchanged, got %v", barcode["value"]) + } +} diff --git a/bskyweb/static/.keep b/bskyweb/static/.keep new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/bskyweb/static/.keep @@ -0,0 +1 @@ + diff --git a/bskyweb/static/index.txt b/bskyweb/static/index.txt new file mode 100644 index 0000000000..cd3f201d27 --- /dev/null +++ b/bskyweb/static/index.txt @@ -0,0 +1 @@ +# Placeholder static files