+9
.editorconfig
+9
.editorconfig
+1
.gitignore
+1
.gitignore
···
1
+
data/
+9
.prettierrc
+9
.prettierrc
+60
AGENTS.md
+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
CLAUDE.md
···
1
+
See AGENTS.md
+10
Makefile
+10
Makefile
+72
README.md
+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
+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
+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
+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()">×</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
lexicons.zip
This is a binary file and will not be displayed.
+74
lexicons/app/bsky/actor/profile.json
+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
+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
+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
+
}