(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

Introducing margin.cafe PDS

scanash00 2c4e898c 7b9d4d88

+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",