A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

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

1# Accessing Hold Data Without AppView 2 3This document explains how to retrieve your data directly from a hold service without going through the ATCR AppView. This is useful for: 4- GDPR data export requests 5- Backup and migration 6- Debugging and development 7- Building alternative clients 8 9## Quick Start: App Passwords (Recommended) 10 11The simplest way to authenticate is using an ATProto app password. This avoids the complexity of OAuth + DPoP. 12 13### Step 1: Create an App Password 14 151. Go to your Bluesky settings: https://bsky.app/settings/app-passwords 162. Create a new app password 173. Save it securely (you'll only see it once) 18 19### Step 2: Get a Session Token 20 21```bash 22# Replace with your handle and app password 23HANDLE="yourhandle.bsky.social" 24APP_PASSWORD="xxxx-xxxx-xxxx-xxxx" 25 26# Create session with your PDS 27SESSION=$(curl -s -X POST "https://bsky.social/xrpc/com.atproto.server.createSession" \ 28 -H "Content-Type: application/json" \ 29 -d "{\"identifier\": \"$HANDLE\", \"password\": \"$APP_PASSWORD\"}") 30 31# Extract tokens 32ACCESS_JWT=$(echo "$SESSION" | jq -r '.accessJwt') 33DID=$(echo "$SESSION" | jq -r '.did') 34PDS=$(echo "$SESSION" | jq -r '.didDoc.service[0].serviceEndpoint') 35 36echo "DID: $DID" 37echo "PDS: $PDS" 38``` 39 40### Step 3: Get a Service Token for the Hold 41 42```bash 43# The hold DID you want to access (e.g., did:web:hold01.atcr.io) 44HOLD_DID="did:web:hold01.atcr.io" 45 46# Get a service token from your PDS 47SERVICE_TOKEN=$(curl -s -X GET "$PDS/xrpc/com.atproto.server.getServiceAuth?aud=$HOLD_DID" \ 48 -H "Authorization: Bearer $ACCESS_JWT" | jq -r '.token') 49 50echo "Service Token: $SERVICE_TOKEN" 51``` 52 53### Step 4: Call Hold Endpoints 54 55Now you can call any authenticated hold endpoint with the service token: 56 57```bash 58# Export your data from the hold 59curl -s "https://hold01.atcr.io/xrpc/io.atcr.hold.exportUserData" \ 60 -H "Authorization: Bearer $SERVICE_TOKEN" | jq . 61``` 62 63### Complete Script 64 65Here's a complete script that does all the above: 66 67```bash 68#!/bin/bash 69# export-hold-data.sh - Export your data from an ATCR hold 70 71set -e 72 73# Configuration 74HANDLE="${1:-yourhandle.bsky.social}" 75APP_PASSWORD="${2:-xxxx-xxxx-xxxx-xxxx}" 76HOLD_DID="${3:-did:web:hold01.atcr.io}" 77 78# Default PDS (Bluesky's main PDS) 79DEFAULT_PDS="https://bsky.social" 80 81echo "Authenticating as $HANDLE..." 82 83# Step 1: Create session 84SESSION=$(curl -s -X POST "$DEFAULT_PDS/xrpc/com.atproto.server.createSession" \ 85 -H "Content-Type: application/json" \ 86 -d "{\"identifier\": \"$HANDLE\", \"password\": \"$APP_PASSWORD\"}") 87 88# Check for errors 89if echo "$SESSION" | jq -e '.error' > /dev/null 2>&1; then 90 echo "Error: $(echo "$SESSION" | jq -r '.message')" 91 exit 1 92fi 93 94ACCESS_JWT=$(echo "$SESSION" | jq -r '.accessJwt') 95DID=$(echo "$SESSION" | jq -r '.did') 96 97# Try to get PDS from didDoc, fall back to default 98PDS=$(echo "$SESSION" | jq -r '.didDoc.service[] | select(.id == "#atproto_pds") | .serviceEndpoint' 2>/dev/null || echo "$DEFAULT_PDS") 99if [ "$PDS" = "null" ] || [ -z "$PDS" ]; then 100 PDS="$DEFAULT_PDS" 101fi 102 103echo "Authenticated as $DID" 104echo "PDS: $PDS" 105 106# Step 2: Get service token for the hold 107echo "Getting service token for $HOLD_DID..." 108SERVICE_RESPONSE=$(curl -s -X GET "$PDS/xrpc/com.atproto.server.getServiceAuth?aud=$HOLD_DID" \ 109 -H "Authorization: Bearer $ACCESS_JWT") 110 111if echo "$SERVICE_RESPONSE" | jq -e '.error' > /dev/null 2>&1; then 112 echo "Error getting service token: $(echo "$SERVICE_RESPONSE" | jq -r '.message')" 113 exit 1 114fi 115 116SERVICE_TOKEN=$(echo "$SERVICE_RESPONSE" | jq -r '.token') 117 118# Step 3: Resolve hold DID to URL 119if [[ "$HOLD_DID" == did:web:* ]]; then 120 # did:web:example.com -> https://example.com 121 HOLD_HOST="${HOLD_DID#did:web:}" 122 HOLD_URL="https://$HOLD_HOST" 123else 124 echo "Error: Only did:web holds are currently supported for direct resolution" 125 exit 1 126fi 127 128echo "Hold URL: $HOLD_URL" 129 130# Step 4: Export data 131echo "Exporting data from $HOLD_URL..." 132curl -s "$HOLD_URL/xrpc/io.atcr.hold.exportUserData" \ 133 -H "Authorization: Bearer $SERVICE_TOKEN" | jq . 134``` 135 136Usage: 137```bash 138chmod +x export-hold-data.sh 139./export-hold-data.sh yourhandle.bsky.social xxxx-xxxx-xxxx-xxxx did:web:hold01.atcr.io 140``` 141 142--- 143 144## Available Hold Endpoints 145 146Once you have a service token, you can call these endpoints: 147 148### Data Export (GDPR) 149```bash 150GET /xrpc/io.atcr.hold.exportUserData 151Authorization: Bearer {service_token} 152``` 153 154Returns all your data stored on that hold: 155- Layer records (blobs you've pushed) 156- Crew membership status 157- Usage statistics 158- Whether you're the hold captain 159 160### Quota Information 161```bash 162GET /xrpc/io.atcr.hold.getQuota?userDid={your_did} 163# No auth required - just needs your DID 164``` 165 166### Blob Download (if you have read access) 167```bash 168GET /xrpc/com.atproto.sync.getBlob?did={owner_did}&cid={blob_digest} 169Authorization: Bearer {service_token} 170``` 171 172Returns a presigned URL to download the blob directly from storage. 173 174--- 175 176## OAuth + DPoP (Advanced) 177 178App passwords are the simplest option, but OAuth with DPoP is the "proper" way to authenticate in ATProto. However, it's significantly more complex because: 179 1801. **DPoP (Demonstrating Proof of Possession)** - Every request requires a cryptographically signed JWT proving you control a specific key 1812. **PAR (Pushed Authorization Requests)** - Authorization parameters are sent server-to-server 1823. **PKCE (Proof Key for Code Exchange)** - Prevents authorization code interception 183 184### Why DPoP Makes Curl Impractical 185 186Each request requires a fresh DPoP proof JWT with: 187- Unique `jti` (request ID) 188- Current `iat` timestamp 189- HTTP method and URL bound to the request 190- Server-provided `nonce` 191- Signature using your P-256 private key 192 193Example DPoP proof structure: 194```json 195{ 196 "alg": "ES256", 197 "typ": "dpop+jwt", 198 "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." } 199} 200{ 201 "htm": "GET", 202 "htu": "https://bsky.social/xrpc/com.atproto.server.getServiceAuth", 203 "jti": "550e8400-e29b-41d4-a716-446655440000", 204 "iat": 1735689100, 205 "nonce": "server-provided-nonce" 206} 207``` 208 209### If You Need OAuth 210 211If you need OAuth (e.g., for a production application), you'll want to use a library: 212 213**Go:** 214```go 215import "github.com/bluesky-social/indigo/atproto/auth/oauth" 216``` 217 218**TypeScript/JavaScript:** 219```bash 220npm install @atproto/oauth-client-node 221``` 222 223**Python:** 224```bash 225pip install atproto 226``` 227 228These libraries handle all the DPoP complexity for you. 229 230### High-Level OAuth Flow 231 232For documentation purposes, here's what the flow looks like: 233 2341. **Resolve identity**: `handle``DID``PDS endpoint` 2352. **Discover OAuth server**: `GET {pds}/.well-known/oauth-authorization-server` 2363. **Generate DPoP key**: Create P-256 key pair 2374. **PAR request**: Send authorization parameters (with DPoP proof) 2385. **User authorization**: Browser-based login 2396. **Token exchange**: Exchange code for tokens (with DPoP proof) 2407. **Use tokens**: All subsequent requests include DPoP proofs 241 242Each step after #3 requires generating a fresh DPoP proof JWT, which is why libraries are essential. 243 244--- 245 246## Troubleshooting 247 248### "Invalid token" or "Token expired" 249 250Service tokens are only valid for ~60 seconds. Get a fresh one: 251```bash 252SERVICE_TOKEN=$(curl -s "$PDS/xrpc/com.atproto.server.getServiceAuth?aud=$HOLD_DID" \ 253 -H "Authorization: Bearer $ACCESS_JWT" | jq -r '.token') 254``` 255 256### "Session expired" 257 258Your access JWT from `createSession` has expired. Create a new session: 259```bash 260SESSION=$(curl -s -X POST "$PDS/xrpc/com.atproto.server.createSession" ...) 261ACCESS_JWT=$(echo "$SESSION" | jq -r '.accessJwt') 262``` 263 264### "Audience mismatch" 265 266The service token is scoped to a specific hold. Make sure `HOLD_DID` matches exactly what's in the `aud` claim of your token. 267 268### "Access denied: user is not a crew member" 269 270You don't have access to this hold. You need to either: 271- Be the hold captain (owner) 272- Be a crew member with appropriate permissions 273 274### Finding Your Hold DID 275 276Check your sailor profile to find your default hold: 277```bash 278curl -s "https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=$DID&collection=io.atcr.sailor.profile&rkey=self" \ 279 -H "Authorization: Bearer $ACCESS_JWT" | jq -r '.value.defaultHold' 280``` 281 282Or check your manifest records for the hold where your images are stored: 283```bash 284curl -s "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=$DID&collection=io.atcr.manifest&limit=1" \ 285 -H "Authorization: Bearer $ACCESS_JWT" | jq -r '.records[0].value.holdDid' 286``` 287 288--- 289 290## Security Notes 291 292- **App passwords** are scoped tokens that can be revoked without changing your main password 293- **Service tokens** are short-lived (60 seconds) and scoped to a specific hold 294- **Never share** your app password or access tokens 295- Service tokens can only be used for the specific hold they were requested for (`aud` claim) 296 297--- 298 299## References 300 301- [ATProto OAuth Specification](https://atproto.com/specs/oauth) 302- [DPoP RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) 303- [Bluesky OAuth Guide](https://docs.bsky.app/docs/advanced-guides/oauth-client) 304- [ATCR BYOS Documentation](./BYOS.md)