Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

Introducing margin.cafe PDS

+70 -18
+3 -3
backend/go.mod
··· 3 3 go 1.24.0 4 4 5 5 require ( 6 + github.com/fxamacker/cbor/v2 v2.9.0 6 7 github.com/go-chi/chi/v5 v5.1.0 7 8 github.com/go-chi/cors v1.2.1 8 9 github.com/go-jose/go-jose/v4 v4.0.4 9 10 github.com/gorilla/websocket v1.5.3 11 + github.com/ipfs/go-cid v0.6.0 10 12 github.com/joho/godotenv v1.5.1 11 13 github.com/lib/pq v1.10.9 12 14 github.com/mattn/go-sqlite3 v1.14.22 15 + github.com/multiformats/go-multihash v0.2.3 13 16 golang.org/x/image v0.34.0 14 17 ) 15 18 16 19 require ( 17 20 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 18 - github.com/fxamacker/cbor/v2 v2.9.0 // indirect 19 - github.com/ipfs/go-cid v0.6.0 // indirect 20 21 github.com/klauspost/cpuid/v2 v2.0.9 // indirect 21 22 github.com/minio/sha256-simd v1.0.0 // indirect 22 23 github.com/mr-tron/base58 v1.2.0 // indirect 23 24 github.com/multiformats/go-base32 v0.0.3 // indirect 24 25 github.com/multiformats/go-base36 v0.1.0 // indirect 25 26 github.com/multiformats/go-multibase v0.2.0 // indirect 26 - github.com/multiformats/go-multihash v0.2.3 // indirect 27 27 github.com/multiformats/go-varint v0.1.0 // indirect 28 28 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 29 29 github.com/spaolacci/murmur3 v1.1.0 // indirect
+10 -3
backend/internal/oauth/client.go
··· 311 311 } 312 312 313 313 func (c *Client) SendPAR(meta *AuthServerMetadata, loginHint, scope string, dpopKey *ecdsa.PrivateKey, pkceChallenge string) (*PARResponse, string, string, error) { 314 + return c.SendPARWithPrompt(meta, loginHint, scope, dpopKey, pkceChallenge, "") 315 + } 316 + 317 + func (c *Client) SendPARWithPrompt(meta *AuthServerMetadata, loginHint, scope string, dpopKey *ecdsa.PrivateKey, pkceChallenge, prompt string) (*PARResponse, string, string, error) { 314 318 stateBytes := make([]byte, 16) 315 319 rand.Read(stateBytes) 316 320 state := base64.RawURLEncoding.EncodeToString(stateBytes) 317 321 318 - parResp, dpopNonce, err := c.sendPARRequest(meta, loginHint, scope, dpopKey, pkceChallenge, state, "") 322 + parResp, dpopNonce, err := c.sendPARRequest(meta, loginHint, scope, dpopKey, pkceChallenge, state, "", prompt) 319 323 if err != nil { 320 324 321 325 if strings.Contains(err.Error(), "use_dpop_nonce") && dpopNonce != "" { 322 326 323 - parResp, dpopNonce, err = c.sendPARRequest(meta, loginHint, scope, dpopKey, pkceChallenge, state, dpopNonce) 327 + parResp, dpopNonce, err = c.sendPARRequest(meta, loginHint, scope, dpopKey, pkceChallenge, state, dpopNonce, prompt) 324 328 if err != nil { 325 329 return nil, "", "", err 326 330 } ··· 332 336 return parResp, state, dpopNonce, nil 333 337 } 334 338 335 - func (c *Client) sendPARRequest(meta *AuthServerMetadata, loginHint, scope string, dpopKey *ecdsa.PrivateKey, pkceChallenge, state, dpopNonce string) (*PARResponse, string, error) { 339 + func (c *Client) sendPARRequest(meta *AuthServerMetadata, loginHint, scope string, dpopKey *ecdsa.PrivateKey, pkceChallenge, state, dpopNonce, prompt string) (*PARResponse, string, error) { 336 340 dpopProof, err := c.CreateDPoPProof(dpopKey, "POST", meta.PushedAuthorizationRequestEndpoint, dpopNonce, "") 337 341 if err != nil { 338 342 return nil, "", err ··· 355 359 data.Set("client_assertion", clientAssertion) 356 360 if loginHint != "" { 357 361 data.Set("login_hint", loginHint) 362 + } 363 + if prompt != "" { 364 + data.Set("prompt", prompt) 358 365 } 359 366 360 367 req, err := http.NewRequest("POST", meta.PushedAuthorizationRequestEndpoint, strings.NewReader(data.Encode()))
+14 -6
backend/internal/oauth/handler.go
··· 13 13 "net/http" 14 14 "net/url" 15 15 "os" 16 + "strings" 16 17 "sync" 17 18 "time" 18 19 ··· 325 326 pkceVerifier, pkceChallenge := client.GeneratePKCE() 326 327 scope := "atproto offline_access blob:* include:at.margin.authFull" 327 328 328 - parResp, state, dpopNonce, err := client.SendPAR(meta, "", scope, dpopKey, pkceChallenge) 329 + parResp, state, dpopNonce, err := client.SendPARWithPrompt(meta, "", scope, dpopKey, pkceChallenge, "create") 329 330 if err != nil { 330 - log.Printf("PAR request failed for signup: %v", err) 331 - w.Header().Set("Content-Type", "application/json") 332 - w.WriteHeader(http.StatusInternalServerError) 333 - json.NewEncoder(w).Encode(map[string]string{"error": "Failed to initiate signup"}) 334 - return 331 + if strings.Contains(err.Error(), "prompt") || strings.Contains(err.Error(), "invalid_request") { 332 + log.Printf("prompt=create not supported, falling back to standard flow") 333 + pkceVerifier, pkceChallenge = client.GeneratePKCE() 334 + parResp, state, dpopNonce, err = client.SendPAR(meta, "", scope, dpopKey, pkceChallenge) 335 + } 336 + if err != nil { 337 + log.Printf("PAR request failed for signup: %v", err) 338 + w.Header().Set("Content-Type", "application/json") 339 + w.WriteHeader(http.StatusInternalServerError) 340 + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to initiate signup"}) 341 + return 342 + } 335 343 } 336 344 337 345 pending := &PendingAuth{
+15
web/src/components/Icons.jsx
··· 282 282 ); 283 283 } 284 284 285 + export function MarginIcon({ size = 18 }) { 286 + return ( 287 + <svg 288 + width={size} 289 + height={size} 290 + viewBox="0 0 265 231" 291 + fill="currentColor" 292 + xmlns="http://www.w3.org/2000/svg" 293 + > 294 + <path d="M0 230 V0 H199 V65.7156 H149.5 V115.216 H182.5 L199 131.716 V230 Z" /> 295 + <path d="M215 214.224 V230 H264.5 V0 H215.07 V16.2242 H248.5 V214.224 H215 Z" /> 296 + </svg> 297 + ); 298 + } 299 + 285 300 export function LogoutIcon({ size = 18 }) { 286 301 return ( 287 302 <svg
+27 -6
web/src/components/SignUpModal.jsx
··· 1 1 import { useState, useEffect } from "react"; 2 2 import { X, ChevronRight, Loader2, AlertCircle } from "lucide-react"; 3 - import { BlackskyIcon, NorthskyIcon, BlueskyIcon, TophhieIcon } from "./Icons"; 3 + import { 4 + BlackskyIcon, 5 + NorthskyIcon, 6 + BlueskyIcon, 7 + TophhieIcon, 8 + MarginIcon, 9 + } from "./Icons"; 4 10 import { startSignup } from "../api/client"; 5 11 6 12 const RECOMMENDED_PROVIDER = { 7 - id: "bluesky", 8 - name: "Bluesky", 9 - service: "https://bsky.social", 10 - Icon: BlueskyIcon, 11 - description: "The most popular option, recommended for most people", 13 + id: "margin", 14 + name: "Margin", 15 + service: "https://margin.cafe", 16 + Icon: MarginIcon, 17 + description: "Hosted by Margin, the easiest way to get started", 12 18 }; 13 19 14 20 const OTHER_PROVIDERS = [ 21 + { 22 + id: "bluesky", 23 + name: "Bluesky", 24 + service: "https://bsky.social", 25 + Icon: BlueskyIcon, 26 + description: "The most popular option on the AT Protocol", 27 + }, 15 28 { 16 29 id: "blacksky", 17 30 name: "Blacksky", 18 31 service: "https://blacksky.app", 19 32 Icon: BlackskyIcon, 20 33 description: "For the Culture. A safe space for Black users and allies", 34 + }, 35 + { 36 + id: "selfhosted.social", 37 + name: "selfhosted.social", 38 + service: "https://selfhosted.social", 39 + Icon: null, 40 + description: 41 + "For hackers, designers, developers, ATProto enthusiasts, scrobblers, tinkerers, friends, and curious minds.", 21 42 }, 22 43 { 23 44 id: "northsky",
+1
web/src/pages/Login.jsx
··· 25 25 const [morphClass, setMorphClass] = useState("morph-in"); 26 26 const providers = [ 27 27 "AT Protocol", 28 + "Margin", 28 29 "Bluesky", 29 30 "Blacksky", 30 31 "Tangled",