The Go90 Scale of Doomed Streaming Services

init

+9
.editorconfig
··· 1 + root = true 2 + 3 + [*] 4 + indent_style = space 5 + indent_size = 2 6 + end_of_line = lf 7 + charset = utf-8 8 + trim_trailing_whitespace = true 9 + insert_final_newline = true
+1
.gitignore
··· 1 + data/
+9
.prettierrc
··· 1 + { 2 + "printWidth": 100, 3 + "tabWidth": 2, 4 + "useTabs": false, 5 + "semi": true, 6 + "singleQuote": false, 7 + "htmlWhitespaceSensitivity": "css", 8 + "bracketSameLine": false 9 + }
+60
AGENTS.md
··· 1 + # Agents 2 + 3 + This is a starter kit for building apps on the Atmosphere with Quickslice. 4 + 5 + ## Project Structure 6 + 7 + - `index.html` - Main frontend with OAuth login flow 8 + - `docker-compose.yml` - Quickslice server configuration 9 + - `lexicons/` - AT Protocol lexicon schemas 10 + - `favicon.svg` - Slices logo 11 + 12 + ## Key Technologies 13 + 14 + - **Quickslice** - Backend server for AT Protocol apps 15 + - **AT Protocol** - Decentralized social protocol (Bluesky) 16 + - **OAuth** - Authentication via Bluesky accounts 17 + - **Web Components** - `<qs-actor-autocomplete>` for handle input 18 + 19 + ## Configuration 20 + 21 + In `index.html`: 22 + - `SERVER_URL` - Quickslice server (default: `http://127.0.0.1:8080`) 23 + - `CLIENT_ID` - OAuth client ID from Quickslice settings 24 + 25 + ## Development 26 + 27 + ```bash 28 + docker compose up # Start Quickslice server 29 + make serve # Serve frontend 30 + make format # Format HTML with Prettier 31 + make zip # Create lexicons.zip 32 + ``` 33 + 34 + ## MCP Server 35 + 36 + Quickslice exposes an MCP server at `http://127.0.0.1:8080/mcp`. Add it to your AI assistant: 37 + 38 + ```bash 39 + claude mcp add --scope user quickslice http://127.0.0.1:8080/mcp 40 + ``` 41 + 42 + Use MCP tools to: 43 + - Query the GraphQL API 44 + - Explore available lexicon schemas 45 + - Generate GraphQL queries/mutations 46 + - Understand record structures 47 + 48 + ## Lexicons 49 + 50 + Custom lexicons go in `/lexicons` following AT Protocol naming conventions (e.g., `com/example/myrecord.json`). Run `make zip` and upload via the Quickslice settings page. 51 + 52 + See the [Lexicon Style Guide](https://github.com/bluesky-social/atproto/discussions/4245) for best practices. 53 + 54 + ## Common Tasks 55 + 56 + **Add a new feature**: Ask the Quickslice MCP about relevant lexicons, then update `index.html` with new GraphQL queries and UI. 57 + 58 + **Add custom lexicons**: Create JSON schemas in `/lexicons`, run `make zip`, upload via settings page. If the lexicon already has published records across multiple PDSes, click "Trigger Backfill" on the Quickslice dashboard to sync existing data. Note: Avoid backfilling well-known lexicons like `app.bsky.*` as this will take days and require large amounts of disk space. 59 + 60 + **Style the app**: CSS variables are defined in `:root` - modify colors, spacing as needed.
+1
CLAUDE.md
··· 1 + See AGENTS.md
+10
Makefile
··· 1 + .PHONY: format zip serve 2 + 3 + format: 4 + npx prettier --write "**/*.html" 5 + 6 + zip: 7 + zip -r lexicons.zip lexicons 8 + 9 + serve: 10 + npx serve .
+72
README.md
··· 1 + # Slice Kit 2 + 3 + A starter kit for building apps on the Atmosphere with [Quickslice](https://tangled.org/slices.network/quickslice). 4 + 5 + ## Prerequisites 6 + 7 + - [Docker](https://docs.docker.com/get-docker/) 8 + - [Node.js](https://nodejs.org/) 9 + 10 + ## Quick Start 11 + 12 + 1. Start the server: 13 + 14 + ```bash 15 + docker compose up 16 + ``` 17 + 18 + 2. Login at http://127.0.0.1:8080 to create an admin account 19 + 20 + 3. Enter your domain authority (e.g., `com.example`) - this is the namespace for your app's lexicons. Leave blank if unsure; `app.bsky.actor.profile` will be treated as an external collection. 21 + 22 + 4. Upload `lexicons.zip` in the Lexicons section 23 + 24 + 5. Register an OAuth client at http://127.0.0.1:8080/oauth/clients 25 + 26 + 6. Copy the client ID and set it in `index.html`: 27 + 28 + ```javascript 29 + const CLIENT_ID = "your_client_id_here"; 30 + ``` 31 + 32 + 7. Serve the frontend: 33 + 34 + ```bash 35 + make serve 36 + ``` 37 + 38 + 8. Open http://localhost:3000 and login with your Bluesky handle. You should see your Bluesky avatar and profile info after logging in, it's synced automatically after logging in. 39 + 40 + 9. Configure the Quickslice MCP server for your AI assistant. Example: 41 + 42 + ```bash 43 + claude mcp add --scope user quickslice http://127.0.0.1:8080/mcp 44 + ``` 45 + 46 + 10. Add your own custom lexicons to the `/lexicons` folder, run `make zip`, and upload on the settings page. Ask the Quickslice MCP about your lexicons and use it to help build your app. 47 + 48 + ## Configuration 49 + 50 + Environment variables in `docker-compose.yml`: 51 + 52 + | Variable | Description | 53 + |----------|-------------| 54 + | `DATABASE_URL` | SQLite database path | 55 + | `SECRET_KEY_BASE` | Session signing key (generate your own for production) | 56 + | `OAUTH_SIGNING_KEY` | OAuth token signing key (generate your own for production) | 57 + | `OAUTH_LOOPBACK_MODE` | Enables localhost OAuth redirects for development | 58 + | `EXTERNAL_BASE_URL` | Public URL of your server | 59 + 60 + ## Make Commands 61 + 62 + | Command | Description | 63 + |---------|-------------| 64 + | `make serve` | Serve frontend locally | 65 + | `make format` | Format HTML files with Prettier | 66 + | `make zip` | Create `lexicons.zip` from lexicons directory | 67 + 68 + ## Production 69 + 70 + **Backend:** See the [deployment guide](https://quickslice.slices.network/guides/deployment) for deploying your Quickslice instance. 71 + 72 + **Frontend:** Deploy to a CDN or [wisp.place](https://wisp.place).
+20
docker-compose.yml
··· 1 + services: 2 + quickslice: 3 + image: ghcr.io/bigmoves/quickslice:latest 4 + ports: 5 + - "8080:8080" 6 + volumes: 7 + - ./data:/data 8 + environment: 9 + - HOST=0.0.0.0 10 + - PORT=8080 11 + - DATABASE_URL=sqlite:/data/quickslice.db 12 + # NOTE: Do NOT use in production - generate your own secure key 13 + - SECRET_KEY_BASE=Xdb/9oovpIzYRKPjfTm45QSWYyYJi35GY3n4475SBVmcyxHS9tMoFJcOwPGfA0xW 14 + # This disables cursor tracking in development so you're not always backfilling between server boots (for chattier lexicons) 15 + - JETSTREAM_DISABLE_CURSOR=true 16 + - EXTERNAL_BASE_URL=http://127.0.0.1:8080 17 + # NOTE: Do NOT use in production - generate your own secure key 18 + - OAUTH_SIGNING_KEY=z42tsNCXT8jZHj37qRd1D1vBE4qns8rp43DZsm1uez3cr8h6 19 + - OAUTH_LOOPBACK_MODE=true 20 + restart: on-failure:5
+7
favicon.svg
··· 1 + <svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"> 2 + <g transform="translate(64, 64)"> 3 + <ellipse cx="0" cy="-28" rx="50" ry="20" fill="#FF5722"/> 4 + <ellipse cx="0" cy="0" rx="60" ry="20" fill="#00ACC1"/> 5 + <ellipse cx="0" cy="28" rx="40" ry="20" fill="#32CD32"/> 6 + </g> 7 + </svg>
+471
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Slice Kit</title> 7 + <link rel="icon" type="image/svg+xml" href="favicon.svg" /> 8 + <style> 9 + *, 10 + *::before, 11 + *::after { 12 + box-sizing: border-box; 13 + } 14 + * { 15 + margin: 0; 16 + } 17 + body { 18 + line-height: 1.5; 19 + -webkit-font-smoothing: antialiased; 20 + } 21 + input, 22 + button { 23 + font: inherit; 24 + } 25 + 26 + :root { 27 + --primary-500: #0078ff; 28 + --primary-600: #0060cc; 29 + --gray-100: #f5f5f5; 30 + --gray-200: #e5e5e5; 31 + --gray-500: #737373; 32 + --gray-700: #404040; 33 + --gray-900: #171717; 34 + --border-color: #e5e5e5; 35 + --error-bg: #fef2f2; 36 + --error-border: #fecaca; 37 + --error-text: #dc2626; 38 + } 39 + 40 + body { 41 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 42 + background: var(--gray-100); 43 + color: var(--gray-900); 44 + min-height: 100vh; 45 + padding: 2rem 1rem; 46 + } 47 + 48 + #app { 49 + max-width: 500px; 50 + margin: 0 auto; 51 + } 52 + 53 + header { 54 + text-align: center; 55 + margin-bottom: 2rem; 56 + } 57 + 58 + .logo { 59 + width: 64px; 60 + height: 64px; 61 + margin-bottom: 0.5rem; 62 + } 63 + 64 + header h1 { 65 + font-size: 2rem; 66 + color: var(--primary-500); 67 + margin-bottom: 0.25rem; 68 + } 69 + 70 + .tagline { 71 + color: var(--gray-500); 72 + font-size: 1rem; 73 + } 74 + 75 + .card { 76 + background: white; 77 + border-radius: 0.5rem; 78 + padding: 1.5rem; 79 + margin-bottom: 1rem; 80 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 81 + } 82 + 83 + .login-form { 84 + display: flex; 85 + flex-direction: column; 86 + gap: 1rem; 87 + } 88 + 89 + .form-group { 90 + display: flex; 91 + flex-direction: column; 92 + gap: 0.25rem; 93 + } 94 + 95 + .form-group label { 96 + font-size: 0.875rem; 97 + font-weight: 500; 98 + color: var(--gray-700); 99 + } 100 + 101 + .form-group input { 102 + padding: 0.75rem; 103 + border: 1px solid var(--border-color); 104 + border-radius: 0.375rem; 105 + font-size: 1rem; 106 + } 107 + 108 + .form-group input:focus { 109 + outline: none; 110 + border-color: var(--primary-500); 111 + box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.1); 112 + } 113 + 114 + qs-actor-autocomplete { 115 + --qs-input-border: var(--border-color); 116 + --qs-input-border-focus: var(--primary-500); 117 + --qs-input-padding: 0.75rem; 118 + --qs-radius: 0.375rem; 119 + } 120 + 121 + .btn { 122 + padding: 0.75rem 1.5rem; 123 + border: none; 124 + border-radius: 0.375rem; 125 + font-size: 1rem; 126 + font-weight: 500; 127 + cursor: pointer; 128 + transition: background-color 0.15s; 129 + } 130 + 131 + .btn-primary { 132 + background: var(--primary-500); 133 + color: white; 134 + } 135 + 136 + .btn-primary:hover { 137 + background: var(--primary-600); 138 + } 139 + 140 + .btn-secondary { 141 + background: var(--gray-200); 142 + color: var(--gray-700); 143 + } 144 + 145 + .btn-secondary:hover { 146 + background: var(--border-color); 147 + } 148 + 149 + .user-card { 150 + display: flex; 151 + align-items: center; 152 + justify-content: space-between; 153 + } 154 + 155 + .user-info { 156 + display: flex; 157 + align-items: center; 158 + gap: 0.75rem; 159 + } 160 + 161 + .user-avatar { 162 + width: 48px; 163 + height: 48px; 164 + border-radius: 50%; 165 + background: var(--gray-200); 166 + display: flex; 167 + align-items: center; 168 + justify-content: center; 169 + font-size: 1.5rem; 170 + } 171 + 172 + .user-avatar img { 173 + width: 100%; 174 + height: 100%; 175 + border-radius: 50%; 176 + object-fit: cover; 177 + } 178 + 179 + .user-name { 180 + font-weight: 600; 181 + } 182 + 183 + .user-handle { 184 + font-size: 0.875rem; 185 + color: var(--gray-500); 186 + } 187 + 188 + #error-banner { 189 + position: fixed; 190 + top: 1rem; 191 + left: 50%; 192 + transform: translateX(-50%); 193 + background: var(--error-bg); 194 + border: 1px solid var(--error-border); 195 + color: var(--error-text); 196 + padding: 0.75rem 1rem; 197 + border-radius: 0.375rem; 198 + display: flex; 199 + align-items: center; 200 + gap: 0.75rem; 201 + max-width: 90%; 202 + z-index: 100; 203 + } 204 + 205 + #error-banner.hidden { 206 + display: none; 207 + } 208 + 209 + #error-banner button { 210 + background: none; 211 + border: none; 212 + color: var(--error-text); 213 + cursor: pointer; 214 + font-size: 1.25rem; 215 + line-height: 1; 216 + } 217 + 218 + .hidden { 219 + display: none !important; 220 + } 221 + </style> 222 + </head> 223 + <body> 224 + <div id="app"> 225 + <header> 226 + <svg class="logo" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"> 227 + <g transform="translate(64, 64)"> 228 + <ellipse cx="0" cy="-28" rx="50" ry="20" fill="#FF5722" /> 229 + <ellipse cx="0" cy="0" rx="60" ry="20" fill="#00ACC1" /> 230 + <ellipse cx="0" cy="28" rx="40" ry="20" fill="#32CD32" /> 231 + </g> 232 + </svg> 233 + <h1>Slice Kit</h1> 234 + <p class="tagline">Build your slice of Atmosphere</p> 235 + </header> 236 + <main> 237 + <div id="auth-section"></div> 238 + <div id="content"></div> 239 + </main> 240 + <div id="error-banner" class="hidden"></div> 241 + </div> 242 + 243 + <!-- Quickslice Client SDK --> 244 + <script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@main/quickslice-client-js/dist/quickslice-client.min.js"></script> 245 + <!-- Web Components --> 246 + <script src="https://cdn.jsdelivr.net/gh/bigmoves/elements@v0.1.0/dist/elements.min.js"></script> 247 + 248 + <script> 249 + // ============================================================================= 250 + // CONFIGURATION 251 + // ============================================================================= 252 + 253 + const SERVER_URL = "http://127.0.0.1:8080"; 254 + const CLIENT_ID = ""; // Set your OAuth client ID here after registering 255 + 256 + let client; 257 + 258 + // ============================================================================= 259 + // INITIALIZATION 260 + // ============================================================================= 261 + 262 + async function main() { 263 + // Check for OAuth errors in URL 264 + const params = new URLSearchParams(window.location.search); 265 + if (params.has("error")) { 266 + const error = params.get("error"); 267 + const description = params.get("error_description") || error; 268 + showError(description); 269 + // Clean up URL 270 + window.history.replaceState({}, "", window.location.pathname); 271 + } 272 + 273 + if (window.location.search.includes("code=")) { 274 + if (!CLIENT_ID) { 275 + showError("OAuth callback received but CLIENT_ID is not configured."); 276 + renderLoginForm(); 277 + return; 278 + } 279 + 280 + try { 281 + client = await QuicksliceClient.createQuicksliceClient({ 282 + server: SERVER_URL, 283 + clientId: CLIENT_ID, 284 + }); 285 + await client.handleRedirectCallback(); 286 + } catch (error) { 287 + console.error("OAuth callback error:", error); 288 + showError(`Authentication failed: ${error.message}`); 289 + renderLoginForm(); 290 + return; 291 + } 292 + } else if (CLIENT_ID) { 293 + try { 294 + client = await QuicksliceClient.createQuicksliceClient({ 295 + server: SERVER_URL, 296 + clientId: CLIENT_ID, 297 + }); 298 + } catch (error) { 299 + console.error("Failed to initialize client:", error); 300 + } 301 + } 302 + 303 + await renderApp(); 304 + } 305 + 306 + async function renderApp() { 307 + const isLoggedIn = client && (await client.isAuthenticated()); 308 + 309 + if (isLoggedIn) { 310 + try { 311 + const viewer = await fetchViewer(); 312 + renderUserCard(viewer); 313 + renderContent(viewer); 314 + } catch (error) { 315 + console.error("Failed to fetch viewer:", error); 316 + renderUserCard(null); 317 + } 318 + } else { 319 + renderLoginForm(); 320 + } 321 + } 322 + 323 + // ============================================================================= 324 + // DATA FETCHING 325 + // ============================================================================= 326 + 327 + async function fetchViewer() { 328 + const query = ` 329 + query { 330 + viewer { 331 + did 332 + handle 333 + appBskyActorProfileByDid { 334 + displayName 335 + avatar { url } 336 + } 337 + } 338 + } 339 + `; 340 + 341 + const data = await client.query(query); 342 + return data?.viewer; 343 + } 344 + 345 + // ============================================================================= 346 + // EVENT HANDLERS 347 + // ============================================================================= 348 + 349 + async function handleLogin(event) { 350 + event.preventDefault(); 351 + 352 + const handle = document.getElementById("handle").value.trim(); 353 + 354 + if (!handle) { 355 + showError("Please enter your { }"); 356 + return; 357 + } 358 + 359 + try { 360 + client = await QuicksliceClient.createQuicksliceClient({ 361 + server: SERVER_URL, 362 + clientId: CLIENT_ID, 363 + }); 364 + 365 + await client.loginWithRedirect({ handle }); 366 + } catch (error) { 367 + showError(`Login failed: ${error.message}`); 368 + } 369 + } 370 + 371 + function logout() { 372 + if (client) { 373 + client.logout(); 374 + } else { 375 + window.location.reload(); 376 + } 377 + } 378 + 379 + // ============================================================================= 380 + // UI RENDERING 381 + // ============================================================================= 382 + 383 + function showError(message) { 384 + const banner = document.getElementById("error-banner"); 385 + banner.innerHTML = ` 386 + <span>${escapeHtml(message)}</span> 387 + <button onclick="hideError()">&times;</button> 388 + `; 389 + banner.classList.remove("hidden"); 390 + } 391 + 392 + function hideError() { 393 + document.getElementById("error-banner").classList.add("hidden"); 394 + } 395 + 396 + function escapeHtml(text) { 397 + const div = document.createElement("div"); 398 + div.textContent = text; 399 + return div.innerHTML; 400 + } 401 + 402 + function renderLoginForm() { 403 + const container = document.getElementById("auth-section"); 404 + 405 + if (!CLIENT_ID) { 406 + container.innerHTML = ` 407 + <div class="card"> 408 + <p style="color: var(--error-text); text-align: center; margin-bottom: 1rem;"> 409 + <strong>Configuration Required</strong> 410 + </p> 411 + <p style="color: var(--gray-700); text-align: center;"> 412 + Set the <code style="background: var(--gray-100); padding: 0.125rem 0.375rem; border-radius: 0.25rem;">CLIENT_ID</code> constant after registering an OAuth client. 413 + </p> 414 + </div> 415 + `; 416 + return; 417 + } 418 + 419 + container.innerHTML = ` 420 + <div class="card"> 421 + <form class="login-form" onsubmit="handleLogin(event)"> 422 + <div class="form-group"> 423 + <label for="handle">Handle</label> 424 + <qs-actor-autocomplete 425 + id="handle" 426 + name="handle" 427 + placeholder="you.bsky.social" 428 + required 429 + ></qs-actor-autocomplete> 430 + </div> 431 + <button type="submit" class="btn btn-primary">Login</button> 432 + </form> 433 + </div> 434 + `; 435 + } 436 + 437 + function renderUserCard(viewer) { 438 + const container = document.getElementById("auth-section"); 439 + const displayName = viewer?.appBskyActorProfileByDid?.displayName || "User"; 440 + const handle = viewer?.handle || "unknown"; 441 + const avatarUrl = viewer?.appBskyActorProfileByDid?.avatar?.url; 442 + 443 + container.innerHTML = ` 444 + <div class="card user-card"> 445 + <div class="user-info"> 446 + <div class="user-avatar"> 447 + ${avatarUrl ? `<img src="${escapeHtml(avatarUrl)}" alt="Avatar">` : "👤"} 448 + </div> 449 + <div> 450 + <div class="user-name">Hi, ${escapeHtml(displayName)}!</div> 451 + <div class="user-handle">@${escapeHtml(handle)}</div> 452 + </div> 453 + </div> 454 + <button class="btn btn-secondary" onclick="logout()">Logout</button> 455 + </div> 456 + `; 457 + } 458 + 459 + function renderContent(viewer) { 460 + const container = document.getElementById("content"); 461 + container.innerHTML = ` 462 + <div class="card"> 463 + <p style="color: var(--gray-700);">You're logged in! #getsliced</p> 464 + </div> 465 + `; 466 + } 467 + 468 + main(); 469 + </script> 470 + </body> 471 + </html>
lexicons.zip

This is a binary file and will not be displayed.

+74
lexicons/app/bsky/actor/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.actor.profile", 4 + "defs": { 5 + "main": { 6 + "key": "literal:self", 7 + "type": "record", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "avatar": { 12 + "type": "blob", 13 + "accept": [ 14 + "image/png", 15 + "image/jpeg" 16 + ], 17 + "maxSize": 1000000, 18 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 19 + }, 20 + "banner": { 21 + "type": "blob", 22 + "accept": [ 23 + "image/png", 24 + "image/jpeg" 25 + ], 26 + "maxSize": 1000000, 27 + "description": "Larger horizontal image to display behind profile view." 28 + }, 29 + "labels": { 30 + "refs": [ 31 + "com.atproto.label.defs#selfLabels" 32 + ], 33 + "type": "union", 34 + "description": "Self-label values, specific to the Bluesky application, on the overall account." 35 + }, 36 + "website": { 37 + "type": "string", 38 + "format": "uri" 39 + }, 40 + "pronouns": { 41 + "type": "string", 42 + "maxLength": 200, 43 + "description": "Free-form pronouns text.", 44 + "maxGraphemes": 20 45 + }, 46 + "createdAt": { 47 + "type": "string", 48 + "format": "datetime" 49 + }, 50 + "pinnedPost": { 51 + "ref": "com.atproto.repo.strongRef", 52 + "type": "ref" 53 + }, 54 + "description": { 55 + "type": "string", 56 + "maxLength": 2560, 57 + "description": "Free-form profile description text.", 58 + "maxGraphemes": 256 59 + }, 60 + "displayName": { 61 + "type": "string", 62 + "maxLength": 640, 63 + "maxGraphemes": 64 64 + }, 65 + "joinedViaStarterPack": { 66 + "ref": "com.atproto.repo.strongRef", 67 + "type": "ref" 68 + } 69 + } 70 + }, 71 + "description": "A declaration of a Bluesky account profile." 72 + } 73 + } 74 + }
+192
lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "label": { 6 + "type": "object", 7 + "required": [ 8 + "src", 9 + "uri", 10 + "val", 11 + "cts" 12 + ], 13 + "properties": { 14 + "cid": { 15 + "type": "string", 16 + "format": "cid", 17 + "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to." 18 + }, 19 + "cts": { 20 + "type": "string", 21 + "format": "datetime", 22 + "description": "Timestamp when this label was created." 23 + }, 24 + "exp": { 25 + "type": "string", 26 + "format": "datetime", 27 + "description": "Timestamp at which this label expires (no longer applies)." 28 + }, 29 + "neg": { 30 + "type": "boolean", 31 + "description": "If true, this is a negation label, overwriting a previous label." 32 + }, 33 + "sig": { 34 + "type": "bytes", 35 + "description": "Signature of dag-cbor encoded label." 36 + }, 37 + "src": { 38 + "type": "string", 39 + "format": "did", 40 + "description": "DID of the actor who created this label." 41 + }, 42 + "uri": { 43 + "type": "string", 44 + "format": "uri", 45 + "description": "AT URI of the record, repository (account), or other resource that this label applies to." 46 + }, 47 + "val": { 48 + "type": "string", 49 + "maxLength": 128, 50 + "description": "The short string name of the value or type of this label." 51 + }, 52 + "ver": { 53 + "type": "integer", 54 + "description": "The AT Protocol version of the label object." 55 + } 56 + }, 57 + "description": "Metadata tag on an atproto resource (eg, repo or record)." 58 + }, 59 + "selfLabel": { 60 + "type": "object", 61 + "required": [ 62 + "val" 63 + ], 64 + "properties": { 65 + "val": { 66 + "type": "string", 67 + "maxLength": 128, 68 + "description": "The short string name of the value or type of this label." 69 + } 70 + }, 71 + "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel." 72 + }, 73 + "labelValue": { 74 + "type": "string", 75 + "knownValues": [ 76 + "!hide", 77 + "!no-promote", 78 + "!warn", 79 + "!no-unauthenticated", 80 + "dmca-violation", 81 + "doxxing", 82 + "porn", 83 + "sexual", 84 + "nudity", 85 + "nsfl", 86 + "gore" 87 + ] 88 + }, 89 + "selfLabels": { 90 + "type": "object", 91 + "required": [ 92 + "values" 93 + ], 94 + "properties": { 95 + "values": { 96 + "type": "array", 97 + "items": { 98 + "ref": "#selfLabel", 99 + "type": "ref" 100 + }, 101 + "maxLength": 10 102 + } 103 + }, 104 + "description": "Metadata tags on an atproto record, published by the author within the record." 105 + }, 106 + "labelValueDefinition": { 107 + "type": "object", 108 + "required": [ 109 + "identifier", 110 + "severity", 111 + "blurs", 112 + "locales" 113 + ], 114 + "properties": { 115 + "blurs": { 116 + "type": "string", 117 + "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 118 + "knownValues": [ 119 + "content", 120 + "media", 121 + "none" 122 + ] 123 + }, 124 + "locales": { 125 + "type": "array", 126 + "items": { 127 + "ref": "#labelValueDefinitionStrings", 128 + "type": "ref" 129 + } 130 + }, 131 + "severity": { 132 + "type": "string", 133 + "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 134 + "knownValues": [ 135 + "inform", 136 + "alert", 137 + "none" 138 + ] 139 + }, 140 + "adultOnly": { 141 + "type": "boolean", 142 + "description": "Does the user need to have adult content enabled in order to configure this label?" 143 + }, 144 + "identifier": { 145 + "type": "string", 146 + "maxLength": 100, 147 + "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 148 + "maxGraphemes": 100 149 + }, 150 + "defaultSetting": { 151 + "type": "string", 152 + "default": "warn", 153 + "description": "The default setting for this label.", 154 + "knownValues": [ 155 + "ignore", 156 + "warn", 157 + "hide" 158 + ] 159 + } 160 + }, 161 + "description": "Declares a label value and its expected interpretations and behaviors." 162 + }, 163 + "labelValueDefinitionStrings": { 164 + "type": "object", 165 + "required": [ 166 + "lang", 167 + "name", 168 + "description" 169 + ], 170 + "properties": { 171 + "lang": { 172 + "type": "string", 173 + "format": "language", 174 + "description": "The code of the language these strings are written in." 175 + }, 176 + "name": { 177 + "type": "string", 178 + "maxLength": 640, 179 + "description": "A short human-readable name for the label.", 180 + "maxGraphemes": 64 181 + }, 182 + "description": { 183 + "type": "string", 184 + "maxLength": 100000, 185 + "description": "A longer description of what the label means and why it might be applied.", 186 + "maxGraphemes": 10000 187 + } 188 + }, 189 + "description": "Strings which describe the label in the UI, localized into a specific language." 190 + } 191 + } 192 + }
+24
lexicons/com/atproto/repo/strongRef.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.strongRef", 4 + "description": "A URI with a content-hash fingerprint.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": [ 9 + "uri", 10 + "cid" 11 + ], 12 + "properties": { 13 + "cid": { 14 + "type": "string", 15 + "format": "cid" 16 + }, 17 + "uri": { 18 + "type": "string", 19 + "format": "at-uri" 20 + } 21 + } 22 + } 23 + } 24 + }