An in browser local PDS localpds.at
20

Configure Feed

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

Remove webhook for now

authored by

Niall Bunting and committed by
Niall Bunting
(May 29, 2026, 9:00 AM +0100) c917a10e d72c3f0c

+419 -80
+415 -76
backend/main.go
··· 1 1 package main 2 2 3 3 import ( 4 + "crypto/ecdsa" 5 + "crypto/elliptic" 4 6 "crypto/sha256" 7 + "encoding/base64" 5 8 "encoding/json" 6 9 "fmt" 7 10 "log" 11 + "math/big" 8 12 "net/http" 13 + "strings" 9 14 "sync" 15 + "sync/atomic" 10 16 "time" 11 17 12 18 "github.com/fxamacker/cbor/v2" ··· 14 20 "github.com/gorilla/websocket" 15 21 ) 16 22 17 - // AT Protocol Header for WebSocket frames 23 + // --------------------------------------------------------------------------- 24 + // ATProto firehose frame types 25 + // --------------------------------------------------------------------------- 26 + 18 27 type FrameHeader struct { 19 28 Op int `cbor:"op"` 20 29 T string `cbor:"t,omitempty"` ··· 25 34 Message *string `cbor:"message,omitempty"` 26 35 } 27 36 28 - // Expected payload from the client 29 - type ClientPayload struct { 30 - DID string `json:"did"` 31 - // Go automatically decodes Base64 JSON strings into []byte for these fields 32 - CommitBytes []byte `json:"commitBytes"` 33 - Signature []byte `json:"signature"` 37 + // CommitFrame is the body of a #commit firehose message. 38 + // Matches the com.atproto.sync.subscribeRepos#commit lexicon. 39 + type CommitFrame struct { 40 + // Monotonically increasing sequence number for this relay 41 + Seq int64 `cbor:"seq"` 42 + // Whether this is a rebase/migration (always false for new commits) 43 + Rebase bool `cbor:"rebase"` 44 + // Whether the commit is a tombstone (account deletion) 45 + TooBig bool `cbor:"tooBig"` 46 + // The repo DID 47 + Repo string `cbor:"repo"` 48 + // CID of the commit object (raw bytes, not string) 49 + Commit []byte `cbor:"commit"` 50 + // Previous commit CID bytes (nil if first commit) 51 + Prev []byte `cbor:"prev,omitempty"` 52 + // Revision TID string 53 + Rev string `cbor:"rev"` 54 + // Since TID — previous rev (empty string if first commit) 55 + Since string `cbor:"since,omitempty"` 56 + // CAR file containing all new/changed blocks 57 + Blocks []byte `cbor:"blocks"` 58 + // List of operations in this commit 59 + Ops []RepoOp `cbor:"ops"` 60 + // Blobs added in this commit (empty for most operations) 61 + Blobs [][]byte `cbor:"blobs"` 62 + // Wall-clock time of the commit 63 + Time string `cbor:"time"` 64 + } 65 + 66 + type RepoOp struct { 67 + // "create", "update", or "delete" 68 + Action string `cbor:"action"` 69 + // e.g. "app.bsky.graph.follow/3k..." 70 + Path string `cbor:"path"` 71 + // CID bytes of the record (nil for deletes) 72 + Cid []byte `cbor:"cid,omitempty"` 73 + } 74 + 75 + // --------------------------------------------------------------------------- 76 + // Ingest payload from the browser extension 77 + // --------------------------------------------------------------------------- 78 + 79 + // IngestPayload is what background.js POSTs to /ingest after every write. 80 + // All byte slices are base64-encoded in JSON (Go handles this automatically). 81 + type IngestPayload struct { 82 + DID string `json:"did"` 83 + 84 + // Raw DAG-CBOR bytes of the signed commit object 85 + CommitBytes []byte `json:"commitBytes"` 86 + 87 + // CID of the commit as a base32-lower string (e.g. "bafy...") 88 + CommitCID string `json:"commitCid"` 89 + 90 + // CID of the previous commit (empty string if first commit) 91 + PrevCID string `json:"prevCid"` 92 + 93 + // Revision TID 94 + Rev string `json:"rev"` 95 + 96 + // Previous revision TID (empty if first) 97 + Since string `json:"since"` 98 + 99 + // Full CAR v1 file containing commit + MST nodes + changed record blocks 100 + CarBytes []byte `json:"carBytes"` 101 + 102 + // Operations performed in this commit 103 + Ops []IngestOp `json:"ops"` 34 104 } 35 105 36 - // DID Document structure from plc.directory 37 - type DIDDocument struct { 38 - VerificationMethod []struct { 39 - ID string `json:"id"` 40 - Type string `json:"type"` 41 - PublicKeyMultibase string `json:"publicKeyMultibase"` 42 - } `json:"verificationMethod"` 106 + type IngestOp struct { 107 + Action string `json:"action"` // "create" | "update" | "delete" 108 + Collection string `json:"collection"` // e.g. "app.bsky.graph.follow" 109 + Rkey string `json:"rkey"` 110 + CID string `json:"cid"` // empty for deletes 43 111 } 112 + 113 + // --------------------------------------------------------------------------- 114 + // Firehose hub 115 + // --------------------------------------------------------------------------- 44 116 45 117 type Firehose struct { 46 118 mu sync.RWMutex ··· 51 123 clients: make(map[*websocket.Conn]bool), 52 124 } 53 125 126 + // Global monotonic sequence counter. 127 + // Using atomic so concurrent ingests get unique seq numbers. 128 + var globalSeq atomic.Int64 129 + 54 130 var upgrader = websocket.Upgrader{ 55 131 CheckOrigin: func(r *http.Request) bool { return true }, 56 132 } 133 + 134 + // --------------------------------------------------------------------------- 135 + // Main 136 + // --------------------------------------------------------------------------- 57 137 58 138 func main() { 59 139 r := gin.Default() ··· 61 141 r.GET("/xrpc/com.atproto.sync.subscribeRepos", handleSubscribeRepos) 62 142 r.POST("/ingest", handleIngest) 63 143 64 - log.Println("Verifying Relay running on :3000") 144 + log.Println("ds-pod relay running on :3000") 65 145 if err := r.Run(":3000"); err != nil { 66 - log.Fatalf("Server failed to start: %v", err) 146 + log.Fatalf("Server failed: %v", err) 67 147 } 68 148 } 149 + 150 + // --------------------------------------------------------------------------- 151 + // subscribeRepos — firehose WebSocket for relay consumers 152 + // --------------------------------------------------------------------------- 69 153 70 154 func handleSubscribeRepos(c *gin.Context) { 71 155 conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) ··· 74 158 return 75 159 } 76 160 77 - // CURSOR HANDLING: If they ask for any history, instantly reject them. 78 161 if cursor := c.Query("cursor"); cursor != "" { 79 - log.Printf("Rejecting connection due to cursor: %s", cursor) 80 - 81 - // 1. Send the AT Protocol compliant Header (op: -1 for error/info) 162 + msg := "Stateless relay: history not available" 82 163 headerBytes, _ := cbor.Marshal(FrameHeader{Op: -1}) 83 - 84 - // 2. Send the OutdatedCursor message 85 - msg := "Stateless server: history not available" 86 164 bodyBytes, _ := cbor.Marshal(InfoFrame{Name: "OutdatedCursor", Message: &msg}) 87 - 88 - // Write the frame and close 89 165 conn.WriteMessage(websocket.BinaryMessage, append(headerBytes, bodyBytes...)) 90 166 conn.Close() 91 167 return 92 168 } 93 169 94 - // Register client 95 170 firehose.mu.Lock() 96 171 firehose.clients[conn] = true 97 172 firehose.mu.Unlock() 98 - log.Println("New relay connected to firehose") 173 + log.Println("Subscriber connected") 99 174 100 - // Keep connection open and handle disconnects 101 175 defer func() { 102 176 firehose.mu.Lock() 103 177 delete(firehose.clients, conn) 104 178 firehose.mu.Unlock() 105 179 conn.Close() 106 - log.Println("Relay disconnected") 180 + log.Println("Subscriber disconnected") 107 181 }() 108 182 109 - // Block and read to detect client disconnects 110 183 for { 111 184 if _, _, err := conn.ReadMessage(); err != nil { 112 185 break ··· 114 187 } 115 188 } 116 189 190 + // --------------------------------------------------------------------------- 191 + // /ingest — called by background.js after every repo write 192 + // --------------------------------------------------------------------------- 193 + 117 194 func handleIngest(c *gin.Context) { 118 - var payload ClientPayload 195 + var payload IngestPayload 119 196 if err := c.BindJSON(&payload); err != nil { 120 - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON format"}) 197 + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON: " + err.Error()}) 121 198 return 122 199 } 123 200 124 - // 1. FETCH PUBLIC KEY & VALIDATE SIGNATURE 125 - isValid, err := verifyATProtoSignature(payload.DID, payload.CommitBytes, payload.Signature) 126 - if err != nil || !isValid { 127 - log.Printf("Failed signature validation for %s: %v", payload.DID, err) 128 - c.JSON(http.StatusUnauthorized, gin.H{"error": "Cryptographic signature validation failed"}) 201 + if payload.DID == "" || len(payload.CommitBytes) == 0 || len(payload.CarBytes) == 0 { 202 + c.JSON(http.StatusBadRequest, gin.H{"error": "did, commitBytes and carBytes are required"}) 129 203 return 130 204 } 131 205 132 - // 2. CONSTRUCT AT PROTOCOL FIREHOSE FRAME 133 - header, _ := cbor.Marshal(FrameHeader{Op: 1, T: "#commit"}) 134 - 135 - // We wrap their raw validated bytes into the standard #commit format 136 - mockCommitBody, _ := cbor.Marshal(map[string]interface{}{ 137 - "repo": payload.DID, 138 - "seq": time.Now().UnixMilli(), // Pseudo-sequence for the stateless firehose 139 - "blocks": payload.CommitBytes, // The DAG-CBOR / CAR data they constructed 140 - }) 206 + // 1. Verify the commit signature against the DID document 207 + if err := verifyCommitSignature(payload.DID, payload.CommitBytes); err != nil { 208 + log.Printf("Signature verification failed for %s: %v", payload.DID, err) 209 + c.JSON(http.StatusUnauthorized, gin.H{"error": "signature verification failed: " + err.Error()}) 210 + return 211 + } 212 + 213 + // 2. Assign a sequence number 214 + seq := globalSeq.Add(1) 215 + 216 + // 3. Build the ops array for the firehose frame 217 + ops := make([]RepoOp, 0, len(payload.Ops)) 218 + for _, op := range payload.Ops { 219 + ro := RepoOp{ 220 + Action: op.Action, 221 + Path: op.Collection + "/" + op.Rkey, 222 + } 223 + if op.CID != "" { 224 + cidBytes, err := base32Decode(strings.TrimPrefix(op.CID, "b")) 225 + if err == nil { 226 + ro.Cid = cidBytes 227 + } 228 + } 229 + ops = append(ops, ro) 230 + } 231 + 232 + // 4. Decode the commit CID string to raw bytes for the frame 233 + commitCIDBytes, err := base32Decode(strings.TrimPrefix(payload.CommitCID, "b")) 234 + if err != nil { 235 + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid commitCid"}) 236 + return 237 + } 238 + 239 + var prevCIDBytes []byte 240 + if payload.PrevCID != "" { 241 + prevCIDBytes, _ = base32Decode(strings.TrimPrefix(payload.PrevCID, "b")) 242 + } 243 + 244 + // 5. Build the spec-compliant #commit frame body 245 + frame := CommitFrame{ 246 + Seq: seq, 247 + Rebase: false, 248 + TooBig: false, 249 + Repo: payload.DID, 250 + Commit: commitCIDBytes, 251 + Prev: prevCIDBytes, 252 + Rev: payload.Rev, 253 + Since: payload.Since, 254 + Blocks: payload.CarBytes, 255 + Ops: ops, 256 + Blobs: [][]byte{}, 257 + Time: time.Now().UTC().Format(time.RFC3339), 258 + } 141 259 142 - fullFrame := append(header, mockCommitBody...) 260 + headerBytes, _ := cbor.Marshal(FrameHeader{Op: 1, T: "#commit"}) 261 + bodyBytes, err := cbor.Marshal(frame) 262 + if err != nil { 263 + c.JSON(http.StatusInternalServerError, gin.H{"error": "cbor marshal failed"}) 264 + return 265 + } 266 + fullFrame := append(headerBytes, bodyBytes...) 143 267 144 - // 3. BROADCAST TO ALL LISTENERS 268 + // 6. Broadcast to all subscribers 145 269 firehose.mu.RLock() 146 270 for client := range firehose.clients { 147 271 client.WriteMessage(websocket.BinaryMessage, fullFrame) 148 272 } 149 273 firehose.mu.RUnlock() 150 274 151 - c.JSON(http.StatusOK, gin.H{"status": "broadcasted", "bytes": len(fullFrame)}) 275 + log.Printf("Broadcast seq=%d did=%s ops=%d bytes=%d", seq, payload.DID, len(ops), len(fullFrame)) 276 + c.JSON(http.StatusOK, gin.H{"seq": seq, "bytes": len(fullFrame)}) 277 + } 278 + 279 + // --------------------------------------------------------------------------- 280 + // Signature verification 281 + // --------------------------------------------------------------------------- 282 + 283 + // verifyCommitSignature decodes the signed commit CBOR, extracts the sig field, 284 + // rebuilds the unsigned commit bytes, fetches the DID's public key, and verifies. 285 + func verifyCommitSignature(did string, commitBytes []byte) error { 286 + // Decode the signed commit (DAG-CBOR map) 287 + var commit map[string]interface{} 288 + if err := cbor.Unmarshal(commitBytes, &commit); err != nil { 289 + return fmt.Errorf("cbor unmarshal: %w", err) 290 + } 291 + 292 + // Extract and remove the sig field 293 + rawSig, ok := commit["sig"] 294 + if !ok { 295 + return fmt.Errorf("no sig field in commit") 296 + } 297 + sigBytes, ok := rawSig.([]byte) 298 + if !ok { 299 + return fmt.Errorf("sig is not bytes") 300 + } 301 + if len(sigBytes) != 64 { 302 + return fmt.Errorf("expected 64-byte P1363 sig, got %d", len(sigBytes)) 303 + } 304 + 305 + // Rebuild unsigned commit by removing sig and re-encoding 306 + delete(commit, "sig") 307 + unsignedBytes, err := cbor.Marshal(commit) 308 + if err != nil { 309 + return fmt.Errorf("re-encode unsigned commit: %w", err) 310 + } 311 + 312 + // Fetch the public key from the DID document 313 + pubKey, err := resolvePublicKey(did) 314 + if err != nil { 315 + return fmt.Errorf("resolve public key: %w", err) 316 + } 317 + 318 + // Verify: ECDSA P-256 over SHA-256(unsignedBytes) 319 + // WebCrypto signs the raw message (not a pre-hash), so SHA-256 is applied internally. 320 + // On the Go side we must hash explicitly before calling ecdsa.Verify. 321 + digest := sha256.Sum256(unsignedBytes) 322 + r := new(big.Int).SetBytes(sigBytes[:32]) 323 + s := new(big.Int).SetBytes(sigBytes[32:]) 324 + 325 + if !ecdsa.Verify(pubKey, digest[:], r, s) { 326 + return fmt.Errorf("signature mismatch") 327 + } 328 + return nil 152 329 } 153 330 154 - // verifyATProtoSignature resolves the DID and checks the crypto signature 155 - func verifyATProtoSignature(did string, data []byte, signature []byte) (bool, error) { 156 - // Step A: Resolve the DID to get the public key 157 - plcURL := fmt.Sprintf("https://plc.directory/%s", did) 158 - resp, err := http.Get(plcURL) 331 + // --------------------------------------------------------------------------- 332 + // DID resolution 333 + // --------------------------------------------------------------------------- 334 + 335 + type didDocument struct { 336 + VerificationMethod []struct { 337 + ID string `json:"id"` 338 + Type string `json:"type"` 339 + PublicKeyMultibase string `json:"publicKeyMultibase"` 340 + PublicKeyJwk *jwkPublicKey `json:"publicKeyJwk"` 341 + } `json:"verificationMethod"` 342 + } 343 + 344 + type jwkPublicKey struct { 345 + Kty string `json:"kty"` 346 + Crv string `json:"crv"` 347 + X string `json:"x"` 348 + Y string `json:"y"` 349 + } 350 + 351 + func resolvePublicKey(did string) (*ecdsa.PublicKey, error) { 352 + var docURL string 353 + if strings.HasPrefix(did, "did:web:") { 354 + host := strings.TrimPrefix(did, "did:web:") 355 + docURL = fmt.Sprintf("https://%s/.well-known/did.json", host) 356 + } else if strings.HasPrefix(did, "did:plc:") { 357 + docURL = fmt.Sprintf("https://plc.directory/%s", did) 358 + } else { 359 + return nil, fmt.Errorf("unsupported DID method: %s", did) 360 + } 361 + 362 + resp, err := http.Get(docURL) 159 363 if err != nil { 160 - return false, fmt.Errorf("failed to contact PLC directory: %w", err) 364 + return nil, fmt.Errorf("fetch DID doc: %w", err) 161 365 } 162 366 defer resp.Body.Close() 367 + if resp.StatusCode != http.StatusOK { 368 + return nil, fmt.Errorf("DID doc returned %d", resp.StatusCode) 369 + } 163 370 164 - if resp.StatusCode != http.StatusOK { 165 - return false, fmt.Errorf("DID not found in PLC directory") 371 + var doc didDocument 372 + if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { 373 + return nil, fmt.Errorf("decode DID doc: %w", err) 374 + } 375 + 376 + // Find the #atproto verification method 377 + for _, vm := range doc.VerificationMethod { 378 + if !strings.HasSuffix(vm.ID, "#atproto") { 379 + continue 380 + } 381 + 382 + // Prefer JWK if present (that's what we write in our DID docs) 383 + if vm.PublicKeyJwk != nil && vm.PublicKeyJwk.Crv == "P-256" { 384 + return jwkToECDSA(vm.PublicKeyJwk) 385 + } 386 + 387 + // Fall back to multibase-encoded compressed key 388 + if vm.PublicKeyMultibase != "" { 389 + return multibaseToECDSA(vm.PublicKeyMultibase) 390 + } 391 + } 392 + return nil, fmt.Errorf("no #atproto P-256 key found in DID document") 393 + } 394 + 395 + func jwkToECDSA(jwk *jwkPublicKey) (*ecdsa.PublicKey, error) { 396 + xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X) 397 + if err != nil { 398 + return nil, fmt.Errorf("bad JWK x: %w", err) 399 + } 400 + yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y) 401 + if err != nil { 402 + return nil, fmt.Errorf("bad JWK y: %w", err) 403 + } 404 + return &ecdsa.PublicKey{ 405 + Curve: elliptic.P256(), 406 + X: new(big.Int).SetBytes(xBytes), 407 + Y: new(big.Int).SetBytes(yBytes), 408 + }, nil 409 + } 410 + 411 + func multibaseToECDSA(multibase string) (*ecdsa.PublicKey, error) { 412 + // Only base58btc ('z' prefix) is used by ATProto 413 + if !strings.HasPrefix(multibase, "z") { 414 + return nil, fmt.Errorf("unsupported multibase prefix: %q", multibase[:1]) 415 + } 416 + keyBytes, err := base58Decode(multibase[1:]) 417 + if err != nil { 418 + return nil, fmt.Errorf("base58 decode: %w", err) 419 + } 420 + 421 + // Strip multicodec varint prefix. 422 + // P-256 (p256-pub) = 0x1200 encoded as varint = [0x80, 0x24] 423 + // secp256k1 (secp256k1-pub) = 0xe7 encoded as varint = [0xe7, 0x01] 424 + if len(keyBytes) < 35 { 425 + return nil, fmt.Errorf("key bytes too short: %d", len(keyBytes)) 426 + } 427 + 428 + var curve elliptic.Curve 429 + var compressed []byte 430 + 431 + switch { 432 + case keyBytes[0] == 0x80 && keyBytes[1] == 0x24: 433 + curve = elliptic.P256() 434 + compressed = keyBytes[2:] 435 + default: 436 + return nil, fmt.Errorf("unsupported multicodec prefix: %02x %02x", keyBytes[0], keyBytes[1]) 166 437 } 167 438 168 - var didDoc DIDDocument 169 - if err := json.NewDecoder(resp.Body).Decode(&didDoc); err != nil { 170 - return false, fmt.Errorf("failed to parse DID document") 439 + // Decompress the 33-byte compressed P-256 point 440 + x, y := decompressPoint(curve, compressed) 441 + if x == nil { 442 + return nil, fmt.Errorf("invalid compressed point") 171 443 } 444 + return &ecdsa.PublicKey{Curve: curve, X: x, Y: y}, nil 445 + } 172 446 173 - if len(didDoc.VerificationMethod) == 0 { 174 - return false, fmt.Errorf("no verification methods found for DID") 447 + // decompressPoint decompresses a 33-byte SEC1 compressed EC point. 448 + func decompressPoint(curve elliptic.Curve, compressed []byte) (*big.Int, *big.Int) { 449 + if len(compressed) != 33 { 450 + return nil, nil 451 + } 452 + prefix := compressed[0] 453 + if prefix != 0x02 && prefix != 0x03 { 454 + return nil, nil 175 455 } 176 456 177 - // The public key is encoded as a multibase string (usually starting with 'z') 178 - pubKeyMultibase := didDoc.VerificationMethod[0].PublicKeyMultibase 179 - log.Printf("Found public key for %s: %s", did, pubKeyMultibase) 457 + p := curve.Params().P 458 + x := new(big.Int).SetBytes(compressed[1:]) 180 459 181 - // Step B: Perform the Cryptographic Check 182 - hash := sha256.Sum256(data) 183 - 184 - /* NOTE: To complete the actual math here, you need to: 185 - 1. Decode the multibase string (using a library like github.com/multiformats/go-multibase) 186 - 2. Determine the key type from the multicodec prefix (secp256k1 or P-256) 187 - 3. Use Go's crypto/ecdsa to verify `signature` against `hash` using the extracted key. 188 - */ 460 + // y² = x³ - 3x + b (mod p) for NIST curves 461 + x3 := new(big.Int).Mul(x, x) 462 + x3.Mul(x3, x) 463 + threeX := new(big.Int).Mul(x, big.NewInt(3)) 464 + y2 := new(big.Int).Sub(x3, threeX) 465 + y2.Add(y2, curve.Params().B) 466 + y2.Mod(y2, p) 189 467 190 - // Assuming the math checks out for this structural demonstration: 191 - _ = hash 192 - return true, nil 468 + // y = sqrt(y²) mod p using Tonelli-Shanks (p ≡ 3 mod 4 shortcut) 469 + exp := new(big.Int).Add(p, big.NewInt(1)) 470 + exp.Rsh(exp, 2) 471 + y := new(big.Int).Exp(y2, exp, p) 472 + 473 + // Choose the correct root 474 + if (prefix == 0x02) != (y.Bit(0) == 0) { 475 + y.Sub(p, y) 476 + } 477 + return x, y 478 + } 479 + 480 + // --------------------------------------------------------------------------- 481 + // base58 decode (Bitcoin alphabet) 482 + // --------------------------------------------------------------------------- 483 + 484 + const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" 485 + 486 + func base58Decode(s string) ([]byte, error) { 487 + num := new(big.Int) 488 + for _, c := range s { 489 + idx := strings.IndexRune(base58Alphabet, c) 490 + if idx < 0 { 491 + return nil, fmt.Errorf("invalid base58 char: %q", c) 492 + } 493 + num.Mul(num, big.NewInt(58)) 494 + num.Add(num, big.NewInt(int64(idx))) 495 + } 496 + decoded := num.Bytes() 497 + // Count leading '1's → leading zero bytes 498 + nLeading := 0 499 + for _, c := range s { 500 + if c == '1' { 501 + nLeading++ 502 + } else { 503 + break 504 + } 505 + } 506 + return append(make([]byte, nLeading), decoded...), nil 507 + } 508 + 509 + // --------------------------------------------------------------------------- 510 + // base32 lower decode (used for CID strings with 'b' prefix) 511 + // --------------------------------------------------------------------------- 512 + 513 + const base32LowerAlphabet = "abcdefghijklmnopqrstuvwxyz234567" 514 + 515 + func base32Decode(s string) ([]byte, error) { 516 + var bits uint 517 + var val uint64 518 + var out []byte 519 + for _, c := range strings.ToLower(s) { 520 + idx := strings.IndexRune(base32LowerAlphabet, c) 521 + if idx < 0 { 522 + return nil, fmt.Errorf("invalid base32 char: %q", c) 523 + } 524 + val = (val << 5) | uint64(idx) 525 + bits += 5 526 + if bits >= 8 { 527 + bits -= 8 528 + out = append(out, byte(val>>bits)) 529 + } 530 + } 531 + return out, nil 193 532 }
+4 -4
background.js
··· 20 20 decodeDagCbor, 21 21 CidLink, 22 22 } from './atproto-repo.js'; 23 - import { 24 - connectToSignalingServer 25 - } from './webhook.js'; 23 + //import { 24 + // connectToSignalingServer 25 + //} from './webhook.js'; 26 26 27 27 const DOMAIN = "ds-pod.com"; 28 28 let attachedTabId = null; ··· 519 519 } 520 520 521 521 522 - connectToSignalingServer(getOrCreateKeypair(), publicKeyJwkToDidKey, getRepo); 522 + //connectToSignalingServer(getOrCreateKeypair(), publicKeyJwkToDidKey, getRepo);
webhook.js webhook.js.unused