slack status without the slack status.zzstoatzz.io/
quickslice

fix: remove fragment from getFollows scope completely

The error explicitly says it needs the scope WITHOUT the fragment.
Both metadata and request now use the same format without fragment.

+101 -1
+1 -1
src/main.rs
··· 95 95 "client_name": "Status Sphere", 96 96 "client_uri": public_url.clone(), 97 97 "redirect_uris": [format!("{}/oauth/callback", public_url)], 98 - "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview", 98 + "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app", 99 99 "grant_types": ["authorization_code", "refresh_token"], 100 100 "response_types": ["code"], 101 101 "token_endpoint_auth_method": "none",
+46
test_oauth.py
··· 1 + #!/usr/bin/env python3 2 + import requests 3 + import json 4 + 5 + # Test different scope combinations 6 + test_cases = [ 7 + { 8 + "name": "Both with fragment", 9 + "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview" 10 + }, 11 + { 12 + "name": "getFollows without fragment", 13 + "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 14 + }, 15 + { 16 + "name": "Both without fragment", 17 + "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 18 + }, 19 + { 20 + "name": "getProfile without fragment", 21 + "scope": "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview" 22 + } 23 + ] 24 + 25 + print("Testing OAuth scope combinations...") 26 + print("=" * 60) 27 + 28 + for test in test_cases: 29 + print(f"\nTest: {test['name']}") 30 + print(f"Scope: {test['scope']}") 31 + 32 + # Create mock client metadata 33 + metadata = { 34 + "client_id": "https://test.example.com/oauth-client-metadata.json", 35 + "client_name": "Test Status App", 36 + "client_uri": "https://test.example.com", 37 + "redirect_uris": ["https://test.example.com/oauth/callback"], 38 + "scope": test['scope'], 39 + "grant_types": ["authorization_code", "refresh_token"], 40 + "response_types": ["code"], 41 + "token_endpoint_auth_method": "none", 42 + "dpop_bound_access_tokens": True 43 + } 44 + 45 + print(f"Metadata valid: {json.dumps(metadata, indent=2)}") 46 + print("-" * 40)
+54
test_scopes.js
··· 1 + #!/usr/bin/env node 2 + 3 + // Test OAuth scope validation with Bluesky 4 + const testCases = [ 5 + { 6 + name: "Both with fragment", 7 + metadata_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview", 8 + request_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview" 9 + }, 10 + { 11 + name: "getFollows without fragment in both", 12 + metadata_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app", 13 + request_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 14 + }, 15 + { 16 + name: "getFollows without fragment in request only", 17 + metadata_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app#bsky_appview", 18 + request_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app#bsky_appview rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 19 + }, 20 + { 21 + name: "Both without fragment", 22 + metadata_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app", 23 + request_scope: "atproto repo:io.zzstoatzz.status.record rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app" 24 + } 25 + ]; 26 + 27 + console.log("OAuth Scope Test Results"); 28 + console.log("=" .repeat(60)); 29 + 30 + for (const test of testCases) { 31 + console.log(`\nTest: ${test.name}`); 32 + console.log(`Metadata scope: ${test.metadata_scope}`); 33 + console.log(`Request scope: ${test.request_scope}`); 34 + 35 + // Check if scopes match 36 + const matches = test.metadata_scope === test.request_scope; 37 + console.log(`Scopes match: ${matches ? "✓" : "✗"}`); 38 + 39 + // Check what error message would be 40 + if (test.request_scope.includes("getFollows?aud=did:web:api.bsky.app#bsky_appview")) { 41 + console.log("Expected error: Missing scope without fragment"); 42 + } else if (test.request_scope.includes("getFollows?aud=did:web:api.bsky.app")) { 43 + console.log("Expected: Should work if metadata declares it"); 44 + } 45 + 46 + console.log("-".repeat(40)); 47 + } 48 + 49 + console.log("\nCONCLUSION:"); 50 + console.log("The error message says it needs: rpc:app.bsky.graph.getFollows?aud=did:web:api.bsky.app"); 51 + console.log("This is WITHOUT the #bsky_appview fragment"); 52 + console.log("\nWe should try:"); 53 + console.log("1. Metadata AND request both WITHOUT fragment for getFollows"); 54 + console.log("2. Or check if there's a different issue entirely");